├── .github └── workflows │ └── go.yaml ├── .gitignore ├── LICENSE ├── README.md ├── TODO ├── ast ├── node.go └── node_test.go ├── bundle.go ├── data ├── convert.go ├── convert_test.go ├── value.go └── value_test.go ├── doc.go ├── errortypes ├── filepos.go └── filepos_test.go ├── features_test.go ├── globals.go ├── go.mod ├── go.sum ├── parse ├── lexer.go ├── lexer_test.go ├── parse.go ├── parse_test.go ├── quote.go ├── quote_test.go ├── rawtext.go └── rawtext_test.go ├── parsepasses ├── datarefcheck.go ├── datarefcheck_test.go ├── globals.go └── msgids.go ├── soyhtml ├── directives.go ├── eval.go ├── eval_test.go ├── exec.go ├── exec_test.go ├── funcs.go ├── funcs_test.go ├── renderer.go ├── scope.go └── tofu.go ├── soyjs ├── directives.go ├── doc.go ├── exec.go ├── exec_test.go ├── formatters.go ├── funcs.go ├── generator.go ├── generator_test.go ├── lib │ ├── soyutils.js │ └── soyutils_usegoog.js └── scope.go ├── soymsg ├── id.go ├── placeholder.go ├── placeholder_test.go ├── pomsg │ ├── fallback.go │ ├── fallback_test.go │ ├── msgid.go │ ├── msgid_test.go │ ├── pomsg.go │ ├── pomsg_test.go │ ├── testdata │ │ ├── en.po │ │ └── zz.po │ └── xgettext-soy │ │ └── main.go ├── soymsg.go └── soymsg_test.go ├── soyweb └── soyweb.go ├── template ├── registry.go └── template.go └── testdata ├── FeaturesUsage_globals.txt ├── features.soy └── simple.soy /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | name: "Go" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | go: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | go: [ '1.14', '1.13', '1.12' ] 15 | os: [ 'ubuntu-22.04' ] 16 | runs-on: ${{ matrix.os }} 17 | name: Go ${{ matrix.go }} on ${{ matrix.os }} 18 | steps: 19 | - name: Checkout repo 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Go 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version: ${{ matrix.go }} 26 | 27 | - name: Build 28 | run: go test ./... 29 | env: 30 | GO111MODULE: on 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Rob Figueiredo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # soy 2 | 3 | [![GoDoc](http://godoc.org/github.com/robfig/soy?status.png)](http://godoc.org/github.com/robfig/soy) 4 | [![Build Status](https://github.com/robfig/soy/actions/workflows/go.yaml/badge.svg?query=branch%3Amaster)](https://github.com/robfig/soy/actions/workflows/go.yaml?query=branch%3Amaster) 5 | [![Go Report Card](https://goreportcard.com/badge/robfig/soy)](https://goreportcard.com/report/robfig/soy) 6 | 7 | Go implementation for Soy templates aka [Google Closure 8 | Templates](https://github.com/google/closure-templates). See 9 | [godoc](http://godoc.org/github.com/robfig/soy) for more details and usage 10 | examples. 11 | 12 | This project requires Go 1.12 or higher due to one of the transitive 13 | dependencies requires it as a minimum version; otherwise, Go 1.11 would 14 | suffice for `go mod` support. 15 | 16 | Be sure to set the env var `GO111MODULE=on` to use the `go mod` dependency 17 | versioning when building and testing this project. 18 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - content "kind": text, html, uri, js (code), js (str chars), html attributes 2 | - Contextual autoescape 3 | - js: figure out / unify print directives / funcs 4 | - js: implement / test all functions 5 | - js: command line tool to compile 6 | - js: combine nodes into expressions for output when possible 7 | - js: generate goog.provide/goog.require 8 | - js: generate jsdoc 9 | - js: goog.getCssName 10 | - {msg} 11 | - Delegates (delpackage, delcall, deltemplate) 12 | - parsepasses (optimizations) (Simplify, CombineConsecutiveRawText, Prerender) 13 | - CSS renaming 14 | - Go code generation 15 | - Bidi 16 | - use xliff message bundles 17 | - use PO messages 18 | - Message extractor (placeholders/phname) 19 | - "private" templates (and optimizations) 20 | - Caching 21 | -------------------------------------------------------------------------------- /ast/node.go: -------------------------------------------------------------------------------- 1 | // Package ast contains definitions for the in-memory representation of a Soy 2 | // template. 3 | package ast 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "strconv" 9 | 10 | "github.com/robfig/soy/data" 11 | ) 12 | 13 | // Node represents any singular piece of a Soy template. For example, a 14 | // sequence of raw text or a print tag. 15 | type Node interface { 16 | String() string // String returns the Soy source representation of this node. 17 | Position() Pos // byte position of start of node in full original input string 18 | } 19 | 20 | // ParentNode is any Node that has descendent nodes. For example, the Children 21 | // of a AddNode are the two nodes that should be added. 22 | type ParentNode interface { 23 | Node 24 | Children() []Node 25 | } 26 | 27 | // Pos represents a byte position in the original input text from which this 28 | // template was parsed. It is useful to construct helpful error messages. 29 | type Pos int 30 | 31 | // Position returns this position. It is implemented as a method so that Nodes 32 | // may embed a Pos and fulfill this part of the Node interface for free. 33 | func (p Pos) Position() Pos { 34 | return p 35 | } 36 | 37 | // SoyFileNode represents a Soy file. 38 | type SoyFileNode struct { 39 | Name string 40 | Text string 41 | Body []Node 42 | } 43 | 44 | func (n SoyFileNode) Position() Pos { 45 | return 0 46 | } 47 | 48 | func (n SoyFileNode) Children() []Node { 49 | return n.Body 50 | } 51 | 52 | func (n SoyFileNode) String() string { 53 | var b bytes.Buffer 54 | for _, n := range n.Body { 55 | fmt.Fprint(&b, n) 56 | } 57 | return b.String() 58 | } 59 | 60 | // ListNode holds a sequence of nodes. 61 | type ListNode struct { 62 | Pos 63 | Nodes []Node // The element nodes in lexical order. 64 | } 65 | 66 | func (l *ListNode) String() string { 67 | b := new(bytes.Buffer) 68 | for _, n := range l.Nodes { 69 | fmt.Fprint(b, n) 70 | } 71 | return b.String() 72 | } 73 | 74 | func (l *ListNode) Children() []Node { 75 | return l.Nodes 76 | } 77 | 78 | type RawTextNode struct { 79 | Pos 80 | Text []byte // The text; may span newlines. 81 | } 82 | 83 | func (t *RawTextNode) String() string { 84 | return string(t.Text) 85 | } 86 | 87 | // NamespaceNode registers the namespace of the Soy file. 88 | type NamespaceNode struct { 89 | Pos 90 | Name string 91 | Autoescape AutoescapeType 92 | } 93 | 94 | func (c *NamespaceNode) String() string { 95 | return "{namespace " + c.Name + "}" 96 | } 97 | 98 | type AutoescapeType int 99 | 100 | const ( 101 | AutoescapeUnspecified AutoescapeType = iota 102 | AutoescapeOn 103 | AutoescapeOff 104 | AutoescapeContextual 105 | ) 106 | 107 | // TemplateNode holds a template body. 108 | type TemplateNode struct { 109 | Pos 110 | Name string 111 | Body *ListNode 112 | Autoescape AutoescapeType 113 | Private bool 114 | } 115 | 116 | func (n *TemplateNode) String() string { 117 | return fmt.Sprintf("{template %s}\n%s\n{/template}\n", n.Name, n.Body) 118 | } 119 | 120 | func (n *TemplateNode) Children() []Node { 121 | return []Node{n.Body} 122 | } 123 | 124 | // TypeNode holds a type definition for a template parameter. 125 | // 126 | // Presently this is just a string value which is not validated or processed. 127 | // Backwards-incompatible changes in the future may elaborate this data model to 128 | // add functionality. 129 | type TypeNode struct { 130 | Pos 131 | Expr string 132 | } 133 | 134 | func (n TypeNode) String() string { 135 | return n.Expr 136 | } 137 | 138 | // HeaderParamNode holds a parameter declaration. 139 | // 140 | // HeaderParams MUST appear at the beginning of a TemplateNode's Body. 141 | type HeaderParamNode struct { 142 | Pos 143 | Optional bool 144 | Name string 145 | Type TypeNode // empty if inferred from the default value 146 | Default Node // nil if no default was specified 147 | } 148 | 149 | func (n *HeaderParamNode) String() string { 150 | var expr string 151 | if !n.Optional { 152 | expr = "{@param " 153 | } else { 154 | expr = "{@param? " 155 | } 156 | expr += n.Name + ":" 157 | if typ := n.Type.String(); typ != "" { 158 | expr += " " + typ + " " 159 | } 160 | if n.Default != nil { 161 | expr += "= " + n.Default.String() 162 | } 163 | expr += "}\n" 164 | return expr 165 | } 166 | 167 | type SoyDocNode struct { 168 | Pos 169 | Params []*SoyDocParamNode 170 | } 171 | 172 | func (n *SoyDocNode) String() string { 173 | if len(n.Params) == 0 { 174 | return "\n/** */\n" 175 | } 176 | var expr = "\n/**" 177 | for _, param := range n.Params { 178 | expr += "\n * " + param.String() 179 | } 180 | return expr + "\n */\n" 181 | } 182 | 183 | func (n *SoyDocNode) Children() []Node { 184 | var nodes []Node 185 | for _, param := range n.Params { 186 | nodes = append(nodes, param) 187 | } 188 | return nodes 189 | } 190 | 191 | // SoyDocParam represents a parameter to a Soy template. 192 | // e.g. 193 | // /** 194 | // * Says hello to the person 195 | // * @param name The name of the person to say hello to. 196 | // */ 197 | type SoyDocParamNode struct { 198 | Pos 199 | Name string // e.g. "name" 200 | Optional bool 201 | } 202 | 203 | func (n *SoyDocParamNode) String() string { 204 | var expr = "@param" 205 | if n.Optional { 206 | expr += "?" 207 | } 208 | return expr + " " + n.Name 209 | } 210 | 211 | type PrintNode struct { 212 | Pos 213 | Arg Node 214 | Directives []*PrintDirectiveNode 215 | } 216 | 217 | func (n *PrintNode) String() string { 218 | var expr = "{" + n.Arg.String() 219 | for _, d := range n.Directives { 220 | expr += d.String() 221 | } 222 | return expr + "}" 223 | } 224 | 225 | func (n *PrintNode) Children() []Node { 226 | var nodes = []Node{n.Arg} 227 | for _, child := range n.Directives { 228 | nodes = append(nodes, child) 229 | } 230 | return nodes 231 | } 232 | 233 | type PrintDirectiveNode struct { 234 | Pos 235 | Name string 236 | Args []Node 237 | } 238 | 239 | func (n *PrintDirectiveNode) String() string { 240 | if len(n.Args) == 0 { 241 | return "|" + n.Name 242 | } 243 | var expr = "|" + n.Name + ":" 244 | var first = true 245 | for _, arg := range n.Args { 246 | if !first { 247 | expr += "," 248 | } 249 | expr += arg.String() 250 | first = false 251 | } 252 | return expr 253 | } 254 | 255 | func (n *PrintDirectiveNode) Children() []Node { 256 | return n.Args 257 | } 258 | 259 | type LiteralNode struct { 260 | Pos 261 | Body string 262 | } 263 | 264 | func (n *LiteralNode) String() string { 265 | return "{literal}" + n.Body + "{/literal}" 266 | } 267 | 268 | type CssNode struct { 269 | Pos 270 | Expr Node 271 | Suffix string 272 | } 273 | 274 | func (n *CssNode) String() string { 275 | var expr = "{css " 276 | if n.Expr != nil { 277 | expr += n.Expr.String() + ", " 278 | } 279 | return expr + n.Suffix + "}" 280 | } 281 | 282 | func (n *CssNode) Children() []Node { 283 | return []Node{n.Expr} 284 | } 285 | 286 | type LogNode struct { 287 | Pos 288 | Body Node 289 | } 290 | 291 | func (n *LogNode) String() string { 292 | return "{log}" + n.Body.String() + "{/log}" 293 | } 294 | 295 | func (n *LogNode) Children() []Node { 296 | return []Node{n.Body} 297 | } 298 | 299 | type DebuggerNode struct { 300 | Pos 301 | } 302 | 303 | func (n *DebuggerNode) String() string { 304 | return "{debugger}" 305 | } 306 | 307 | type LetValueNode struct { 308 | Pos 309 | Name string 310 | Expr Node 311 | } 312 | 313 | func (n *LetValueNode) String() string { 314 | return fmt.Sprintf("{let $%s: %s /}", n.Name, n.Expr) 315 | } 316 | 317 | func (n *LetValueNode) Children() []Node { 318 | return []Node{n.Expr} 319 | } 320 | 321 | type LetContentNode struct { 322 | Pos 323 | Name string 324 | Body Node 325 | } 326 | 327 | func (n *LetContentNode) String() string { 328 | return fmt.Sprintf("{let $%s}%s{/let}", n.Name, n.Body) 329 | } 330 | 331 | func (n *LetContentNode) Children() []Node { 332 | return []Node{n.Body} 333 | } 334 | 335 | type IdentNode struct { 336 | Pos 337 | Ident string // The ident's name. 338 | } 339 | 340 | func (i *IdentNode) String() string { 341 | return i.Ident 342 | } 343 | 344 | // MsgNode represents a localized message. 345 | type MsgNode struct { 346 | Pos 347 | ID uint64 348 | Meaning string 349 | Desc string 350 | Body ParentNode // top-level children: RawTextNode, MsgPlaceholderNode, MsgPluralNode 351 | } 352 | 353 | func (n *MsgNode) String() string { 354 | var meaning = " " 355 | if n.Meaning != "" { 356 | meaning = fmt.Sprintf(" meaning=%q ", n.Meaning) 357 | } 358 | return fmt.Sprintf("{msg%sdesc=%q}%s{/msg}", meaning, n.Desc, n.Body.String()) 359 | } 360 | 361 | func (n *MsgNode) Children() []Node { 362 | return n.Body.Children() 363 | } 364 | 365 | // Placeholder returns a placeholder node with the given name within this 366 | // message node. It requires placeholder names to have been calculated. 367 | func (n *MsgNode) Placeholder(name string) *MsgPlaceholderNode { 368 | var q = n.Body.Children() 369 | for len(q) > 0 { 370 | var node Node 371 | node, q = q[0], q[1:] 372 | switch node := node.(type) { 373 | case *MsgPlaceholderNode: 374 | if node.Name == name { 375 | return node 376 | } 377 | default: 378 | if node, ok := node.(ParentNode); ok { 379 | q = append(q, node.Children()...) 380 | } 381 | } 382 | } 383 | return nil 384 | } 385 | 386 | type MsgPlaceholderNode struct { 387 | Pos 388 | Name string 389 | Body Node 390 | } 391 | 392 | func (n *MsgPlaceholderNode) String() string { 393 | return n.Body.String() 394 | } 395 | 396 | func (n *MsgPlaceholderNode) Children() []Node { 397 | return []Node{n.Body} 398 | } 399 | 400 | type MsgHtmlTagNode struct { 401 | Pos 402 | Text []byte 403 | } 404 | 405 | func (n *MsgHtmlTagNode) String() string { 406 | return string(n.Text) 407 | } 408 | 409 | type MsgPluralNode struct { 410 | Pos 411 | VarName string 412 | Value Node 413 | Cases []*MsgPluralCaseNode 414 | Default ParentNode 415 | } 416 | 417 | func (n *MsgPluralNode) String() string { 418 | var expr = "{plural " + n.Value.String() + "}" 419 | for _, caseNode := range n.Cases { 420 | expr += caseNode.String() 421 | } 422 | expr += "{default}" + n.Default.String() 423 | return expr + "{/plural}" 424 | } 425 | 426 | func (n *MsgPluralNode) Children() []Node { 427 | var children []Node 428 | children = append(children, n.Value) 429 | for _, plCase := range n.Cases { 430 | children = append(children, plCase) 431 | } 432 | children = append(children, n.Default) 433 | return children 434 | } 435 | 436 | type MsgPluralCaseNode struct { 437 | Pos 438 | Value int 439 | Body ParentNode // top level children: RawTextNode, MsgPlaceholderNode 440 | } 441 | 442 | func (n *MsgPluralCaseNode) String() string { 443 | return "{case " + strconv.Itoa(n.Value) + "}" + n.Body.String() 444 | } 445 | 446 | func (n *MsgPluralCaseNode) Children() []Node { 447 | return []Node{n.Body} 448 | } 449 | 450 | type CallNode struct { 451 | Pos 452 | Name string 453 | AllData bool 454 | Data Node 455 | Params []Node 456 | } 457 | 458 | func (n *CallNode) String() string { 459 | var expr = fmt.Sprintf("{call %s", n.Name) 460 | if n.AllData { 461 | expr += ` data="all"` 462 | } else if n.Data != nil { 463 | expr += fmt.Sprintf(` data="%s"`, n.Data.String()) 464 | } 465 | if n.Params == nil { 466 | return expr + "/}" 467 | } 468 | expr += "}" 469 | for _, param := range n.Params { 470 | expr += param.String() 471 | } 472 | return expr + "{/call}" 473 | } 474 | 475 | func (n *CallNode) Children() []Node { 476 | var nodes []Node 477 | nodes = append(nodes, n.Data) 478 | for _, child := range n.Params { 479 | nodes = append(nodes, child) 480 | } 481 | return nodes 482 | } 483 | 484 | type CallParamValueNode struct { 485 | Pos 486 | Key string 487 | Value Node 488 | } 489 | 490 | func (n *CallParamValueNode) String() string { 491 | return fmt.Sprintf("{param %s: %s/}", n.Key, n.Value.String()) 492 | } 493 | 494 | func (n *CallParamValueNode) Children() []Node { 495 | return []Node{n.Value} 496 | } 497 | 498 | type CallParamContentNode struct { 499 | Pos 500 | Key string 501 | Content Node 502 | } 503 | 504 | func (n *CallParamContentNode) String() string { 505 | return fmt.Sprintf("{param %s}%s{/param}", n.Key, n.Content.String()) 506 | } 507 | 508 | func (n *CallParamContentNode) Children() []Node { 509 | return []Node{n.Content} 510 | } 511 | 512 | // Control flow ---------- 513 | 514 | type IfNode struct { 515 | Pos 516 | Conds []*IfCondNode 517 | } 518 | 519 | func (n *IfNode) String() string { 520 | var expr string 521 | for i, cond := range n.Conds { 522 | if i == 0 { 523 | expr += "{if " 524 | } else if cond.Cond == nil { 525 | expr += "{else}" 526 | } else { 527 | expr += "{elseif " 528 | } 529 | expr += cond.String() 530 | } 531 | return expr + "{/if}" 532 | } 533 | 534 | func (n *IfNode) Children() []Node { 535 | var nodes []Node 536 | for _, child := range n.Conds { 537 | nodes = append(nodes, child) 538 | } 539 | return nodes 540 | } 541 | 542 | type IfCondNode struct { 543 | Pos 544 | Cond Node // nil if "else" 545 | Body Node 546 | } 547 | 548 | func (n *IfCondNode) String() string { 549 | var expr string 550 | if n.Cond != nil { 551 | expr = n.Cond.String() + "}" 552 | } 553 | return expr + n.Body.String() 554 | } 555 | 556 | func (n *IfCondNode) Children() []Node { 557 | return []Node{n.Cond, n.Body} 558 | } 559 | 560 | type SwitchNode struct { 561 | Pos 562 | Value Node 563 | Cases []*SwitchCaseNode 564 | } 565 | 566 | func (n *SwitchNode) String() string { 567 | var expr = "{switch " + n.Value.String() + "}" 568 | for _, caseNode := range n.Cases { 569 | expr += caseNode.String() 570 | } 571 | return expr + "{/switch}" 572 | } 573 | 574 | func (n *SwitchNode) Children() []Node { 575 | var nodes = []Node{n.Value} 576 | for _, child := range n.Cases { 577 | nodes = append(nodes, child) 578 | } 579 | return nodes 580 | } 581 | 582 | type SwitchCaseNode struct { 583 | Pos 584 | Values []Node // len(Values) == 0 => default case 585 | Body Node 586 | } 587 | 588 | func (n *SwitchCaseNode) String() string { 589 | var expr = "{case " 590 | for i, val := range n.Values { 591 | if i > 0 { 592 | expr += "," 593 | } 594 | expr += val.String() 595 | } 596 | return expr + "}" + n.Body.String() 597 | } 598 | 599 | func (n *SwitchCaseNode) Children() []Node { 600 | var nodes = []Node{n.Body} 601 | for _, child := range n.Values { 602 | nodes = append(nodes, child) 603 | } 604 | return nodes 605 | } 606 | 607 | // Note: 608 | // - "For" node may have anything as the List 609 | // - "Foreach" node is required to have a DataRefNode as the List 610 | // 611 | // There is no difference in the parsed representation between them. 612 | // We do not bother to maintain that, because it's not important, as 613 | // upstream has dropped support for {foreach} entirely, so we treat 614 | // this as deprecated. 615 | type ForNode struct { 616 | Pos 617 | Var string // without the leading $ 618 | List Node 619 | Body Node 620 | IfEmpty Node 621 | } 622 | 623 | func (n *ForNode) String() string { 624 | var expr = "{for " 625 | expr += "$" + n.Var + " in " + n.List.String() + "}" + n.Body.String() 626 | if n.IfEmpty != nil { 627 | expr += "{ifempty}" + n.IfEmpty.String() 628 | } 629 | return expr + "{/for}" 630 | } 631 | 632 | func (n *ForNode) Children() []Node { 633 | var children = make([]Node, 2, 3) 634 | children[0] = n.List 635 | children[1] = n.Body 636 | if n.IfEmpty != nil { 637 | children = append(children, n.IfEmpty) 638 | } 639 | return children 640 | } 641 | 642 | // Values ---------- 643 | 644 | type NullNode struct { 645 | Pos 646 | } 647 | 648 | func (s *NullNode) String() string { 649 | return "null" 650 | } 651 | 652 | type BoolNode struct { 653 | Pos 654 | True bool 655 | } 656 | 657 | func (b *BoolNode) String() string { 658 | if b.True { 659 | return "true" 660 | } 661 | return "false" 662 | } 663 | 664 | type IntNode struct { 665 | Pos 666 | Value int64 667 | } 668 | 669 | func (n *IntNode) String() string { 670 | return strconv.FormatInt(n.Value, 10) 671 | } 672 | 673 | type FloatNode struct { 674 | Pos 675 | Value float64 676 | } 677 | 678 | func (n *FloatNode) String() string { 679 | return strconv.FormatFloat(n.Value, 'g', -1, 64) 680 | } 681 | 682 | type StringNode struct { 683 | Pos 684 | Quoted string // e.g. 'hello\tworld' 685 | Value string // e.g. hello world 686 | } 687 | 688 | func (s *StringNode) String() string { 689 | return s.Quoted 690 | } 691 | 692 | type GlobalNode struct { 693 | Pos 694 | Name string 695 | data.Value 696 | } 697 | 698 | func (n *GlobalNode) String() string { 699 | return n.Name 700 | } 701 | 702 | type FunctionNode struct { 703 | Pos 704 | Name string 705 | Args []Node 706 | } 707 | 708 | func (n *FunctionNode) String() string { 709 | var expr = n.Name + "(" 710 | for i, arg := range n.Args { 711 | if i > 0 { 712 | expr += "," 713 | } 714 | expr += arg.String() 715 | } 716 | return expr + ")" 717 | } 718 | 719 | func (n *FunctionNode) Children() []Node { 720 | return n.Args 721 | } 722 | 723 | type ListLiteralNode struct { 724 | Pos 725 | Items []Node 726 | } 727 | 728 | func (n *ListLiteralNode) String() string { 729 | var expr = "[" 730 | for i, item := range n.Items { 731 | if i > 0 { 732 | expr += ", " 733 | } 734 | expr += item.String() 735 | } 736 | return expr + "]" 737 | } 738 | 739 | func (n *ListLiteralNode) Children() []Node { 740 | return n.Items 741 | } 742 | 743 | type MapLiteralNode struct { 744 | Pos 745 | Items map[string]Node 746 | } 747 | 748 | func (n *MapLiteralNode) String() string { 749 | if len(n.Items) == 0 { 750 | return "[:]" 751 | } 752 | var expr = "[" 753 | var first = true 754 | for k, v := range n.Items { 755 | if !first { 756 | expr += ", " 757 | } 758 | expr += fmt.Sprintf("'%s': %s", k, v.String()) 759 | first = false 760 | } 761 | return expr + "]" 762 | } 763 | 764 | func (n *MapLiteralNode) Children() []Node { 765 | var nodes []Node 766 | for _, v := range n.Items { 767 | nodes = append(nodes, v) 768 | } 769 | return nodes 770 | } 771 | 772 | // Data References ---------- 773 | 774 | type DataRefNode struct { 775 | Pos 776 | Key string 777 | Access []Node 778 | } 779 | 780 | func (n *DataRefNode) String() string { 781 | var expr = "$" + n.Key 782 | for _, access := range n.Access { 783 | expr += access.String() 784 | } 785 | return expr 786 | } 787 | 788 | func (n *DataRefNode) Children() []Node { 789 | return n.Access 790 | } 791 | 792 | type DataRefIndexNode struct { 793 | Pos 794 | NullSafe bool 795 | Index int 796 | } 797 | 798 | func (n *DataRefIndexNode) String() string { 799 | var expr = "." 800 | if n.NullSafe { 801 | expr = "?" + expr 802 | } 803 | return expr + strconv.Itoa(n.Index) 804 | } 805 | 806 | type DataRefExprNode struct { 807 | Pos 808 | NullSafe bool 809 | Arg Node 810 | } 811 | 812 | func (n *DataRefExprNode) String() string { 813 | var expr = "[" 814 | if n.NullSafe { 815 | expr = "?" + expr 816 | } 817 | return expr + n.Arg.String() + "]" 818 | } 819 | 820 | func (n *DataRefExprNode) Children() []Node { 821 | return []Node{n.Arg} 822 | } 823 | 824 | type DataRefKeyNode struct { 825 | Pos 826 | NullSafe bool 827 | Key string 828 | } 829 | 830 | func (n *DataRefKeyNode) String() string { 831 | var expr = "." 832 | if n.NullSafe { 833 | expr = "?" + expr 834 | } 835 | return expr + n.Key 836 | } 837 | 838 | // Operators ---------- 839 | 840 | type NotNode struct { 841 | Pos 842 | Arg Node 843 | } 844 | 845 | func (n *NotNode) String() string { 846 | return "not " + n.Arg.String() 847 | } 848 | 849 | func (n *NotNode) Children() []Node { 850 | return []Node{n.Arg} 851 | } 852 | 853 | type NegateNode struct { 854 | Pos 855 | Arg Node 856 | } 857 | 858 | func (n *NegateNode) String() string { 859 | return "-" + n.Arg.String() 860 | } 861 | 862 | func (n *NegateNode) Children() []Node { 863 | return []Node{n.Arg} 864 | } 865 | 866 | type BinaryOpNode struct { 867 | Name string 868 | Pos 869 | Arg1, Arg2 Node 870 | } 871 | 872 | func (n *BinaryOpNode) String() string { 873 | return n.Arg1.String() + " " + n.Name + " " + n.Arg2.String() 874 | } 875 | 876 | func (n *BinaryOpNode) Children() []Node { 877 | return []Node{n.Arg1, n.Arg2} 878 | } 879 | 880 | type ( 881 | MulNode struct{ BinaryOpNode } 882 | DivNode struct{ BinaryOpNode } 883 | ModNode struct{ BinaryOpNode } 884 | AddNode struct{ BinaryOpNode } 885 | SubNode struct{ BinaryOpNode } 886 | EqNode struct{ BinaryOpNode } 887 | NotEqNode struct{ BinaryOpNode } 888 | GtNode struct{ BinaryOpNode } 889 | GteNode struct{ BinaryOpNode } 890 | LtNode struct{ BinaryOpNode } 891 | LteNode struct{ BinaryOpNode } 892 | OrNode struct{ BinaryOpNode } 893 | AndNode struct{ BinaryOpNode } 894 | ElvisNode struct{ BinaryOpNode } 895 | ) 896 | 897 | type TernNode struct { 898 | Pos 899 | Arg1, Arg2, Arg3 Node 900 | } 901 | 902 | func (n *TernNode) String() string { 903 | return n.Arg1.String() + "?" + n.Arg2.String() + ":" + n.Arg3.String() 904 | } 905 | 906 | func (n *TernNode) Children() []Node { 907 | return []Node{n.Arg1, n.Arg2, n.Arg3} 908 | } 909 | -------------------------------------------------------------------------------- /ast/node_test.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import "testing" 4 | 5 | // "[:]" 6 | // "['aaa': 'blah', 'bbb': 123, $boo: $foo]" 7 | func TestMapLiteralNode(t *testing.T) {} 8 | 9 | // "[]" 10 | // "['blah', 123, $foo]" 11 | func TestListLiteralNode(t *testing.T) {} 12 | -------------------------------------------------------------------------------- /bundle.go: -------------------------------------------------------------------------------- 1 | package soy 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | 12 | "github.com/fsnotify/fsnotify" 13 | "github.com/robfig/soy/data" 14 | "github.com/robfig/soy/parse" 15 | "github.com/robfig/soy/parsepasses" 16 | "github.com/robfig/soy/soyhtml" 17 | "github.com/robfig/soy/template" 18 | ) 19 | 20 | // Logger is used to print notifications and compile errors when using the 21 | // "WatchFiles" feature. 22 | var Logger = log.New(os.Stderr, "[soy] ", 0) 23 | 24 | type soyFile struct{ name, content string } 25 | 26 | // Bundle is a collection of Soy content (templates and globals). It acts as 27 | // input for the Soy compiler. 28 | type Bundle struct { 29 | files []soyFile 30 | globals data.Map 31 | err error 32 | watcher *fsnotify.Watcher 33 | parsepasses []func(template.Registry) error 34 | recompilationCallback func(*template.Registry) 35 | } 36 | 37 | // NewBundle returns an empty bundle. 38 | func NewBundle() *Bundle { 39 | return &Bundle{globals: make(data.Map)} 40 | } 41 | 42 | // WatchFiles tells Soy to watch any template files added to this bundle, 43 | // re-compile as necessary, and propagate the updates to your tofu. It should 44 | // be called once, before adding any files. 45 | func (b *Bundle) WatchFiles(watch bool) *Bundle { 46 | if watch && b.err == nil && b.watcher == nil { 47 | b.watcher, b.err = fsnotify.NewWatcher() 48 | } 49 | return b 50 | } 51 | 52 | // AddTemplateDir adds all *.soy files found within the given directory 53 | // (including sub-directories) to the bundle. 54 | func (b *Bundle) AddTemplateDir(root string) *Bundle { 55 | var err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 56 | if err != nil { 57 | return err 58 | } 59 | if info.IsDir() { 60 | return nil 61 | } 62 | if !strings.HasSuffix(path, ".soy") { 63 | return nil 64 | } 65 | b.AddTemplateFile(path) 66 | return nil 67 | }) 68 | if err != nil { 69 | b.err = err 70 | } 71 | return b 72 | } 73 | 74 | // AddTemplateFile adds the given Soy template file text to this bundle. 75 | // If WatchFiles is on, it will be subsequently watched for updates. 76 | func (b *Bundle) AddTemplateFile(filename string) *Bundle { 77 | content, err := ioutil.ReadFile(filename) 78 | if err != nil { 79 | b.err = err 80 | } 81 | if b.err == nil && b.watcher != nil { 82 | b.err = b.watcher.Add(filename) 83 | } 84 | return b.AddTemplateString(filename, string(content)) 85 | } 86 | 87 | // AddTemplateString adds the given template to the bundle. The name is only 88 | // used for error messages - it does not need to be provided nor does it need to 89 | // be a real filename. 90 | func (b *Bundle) AddTemplateString(filename, soyfile string) *Bundle { 91 | b.files = append(b.files, soyFile{filename, soyfile}) 92 | return b 93 | } 94 | 95 | // AddGlobalsFile opens and parses the given filename for Soy globals, and adds 96 | // the resulting data map to the bundle. 97 | func (b *Bundle) AddGlobalsFile(filename string) *Bundle { 98 | var f, err = os.Open(filename) 99 | if err != nil { 100 | b.err = err 101 | return b 102 | } 103 | globals, err := ParseGlobals(f) 104 | if err != nil { 105 | b.err = err 106 | } 107 | f.Close() 108 | return b.AddGlobalsMap(globals) 109 | } 110 | 111 | func (b *Bundle) AddGlobalsMap(globals data.Map) *Bundle { 112 | for k, v := range globals { 113 | if existing, ok := b.globals[k]; ok { 114 | b.err = fmt.Errorf("global %q already defined as %q", k, existing) 115 | return b 116 | } 117 | b.globals[k] = v 118 | } 119 | return b 120 | } 121 | 122 | // SetRecompilationCallback assigns the bundle a function to call after 123 | // recompilation. This is called before updating the in-use registry. 124 | func (b *Bundle) SetRecompilationCallback(c func(*template.Registry)) *Bundle { 125 | b.recompilationCallback = c 126 | return b 127 | } 128 | 129 | // AddParsePass adds a function to the bundle that will be called 130 | // after the Soy is parsed. 131 | func (b *Bundle) AddParsePass(f func(template.Registry) error) *Bundle { 132 | b.parsepasses = append(b.parsepasses, f) 133 | return b 134 | } 135 | 136 | // Compile parses all of the Soy files in this bundle, verifies a number of 137 | // rules about data references, and returns the completed template registry. 138 | func (b *Bundle) Compile() (*template.Registry, error) { 139 | if b.err != nil { 140 | return nil, b.err 141 | } 142 | 143 | // Compile all the Soy (globals are already parsed). 144 | var registry = template.Registry{} 145 | for _, soyfile := range b.files { 146 | var tree, err = parse.SoyFile(soyfile.name, soyfile.content) 147 | if err != nil { 148 | return nil, err 149 | } 150 | if err = registry.Add(tree); err != nil { 151 | return nil, err 152 | } 153 | } 154 | 155 | // Apply the post-parse processing 156 | for _, parsepass := range b.parsepasses { 157 | if err := parsepass(registry); err != nil { 158 | return nil, err 159 | } 160 | } 161 | if err := parsepasses.CheckDataRefs(registry); err != nil { 162 | return nil, err 163 | } 164 | if err := parsepasses.SetGlobals(registry, b.globals); err != nil { 165 | return nil, err 166 | } 167 | parsepasses.ProcessMessages(registry) 168 | 169 | if b.watcher != nil { 170 | go b.recompiler(®istry) 171 | } 172 | return ®istry, nil 173 | } 174 | 175 | // CompileToTofu returns a soyhtml.Tofu object that allows you to render soy 176 | // templates to HTML. 177 | func (b *Bundle) CompileToTofu() (*soyhtml.Tofu, error) { 178 | var registry, err = b.Compile() 179 | // TODO: Verify all used funcs exist and have the right # args. 180 | return soyhtml.NewTofu(registry), err 181 | } 182 | 183 | func (b *Bundle) recompiler(reg *template.Registry) { 184 | for { 185 | select { 186 | case ev := <-b.watcher.Events: 187 | // If it's a rename, then fsnotify has removed the watch. 188 | // Add it back, after a delay. 189 | if ev.Op == fsnotify.Rename || ev.Op == fsnotify.Remove { 190 | time.Sleep(10 * time.Millisecond) 191 | if err := b.watcher.Add(ev.Name); err != nil { 192 | Logger.Println(err) 193 | } 194 | } 195 | 196 | // Recompile all the Soy. 197 | var bundle = NewBundle(). 198 | AddGlobalsMap(b.globals) 199 | for _, soyfile := range b.files { 200 | bundle.AddTemplateFile(soyfile.name) 201 | } 202 | var registry, err = bundle.Compile() 203 | if err != nil { 204 | Logger.Println(err) 205 | continue 206 | } 207 | 208 | if b.recompilationCallback != nil { 209 | b.recompilationCallback(registry) 210 | } 211 | 212 | // update the existing template registry. 213 | // (this is not goroutine-safe, but that seems ok for a development aid, 214 | // as long as it works in practice) 215 | *reg = *registry 216 | Logger.Printf("update successful (%v)", ev) 217 | 218 | case err := <-b.watcher.Errors: 219 | // Nothing to do with errors 220 | Logger.Println(err) 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /data/convert.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "time" 7 | "unicode" 8 | "unicode/utf8" 9 | ) 10 | 11 | var timeType = reflect.TypeOf(time.Time{}) 12 | 13 | // New converts the given data into a Soy data value, using 14 | // DefaultStructOptions for structs. 15 | func New(value interface{}) Value { 16 | return NewWith(DefaultStructOptions, value) 17 | } 18 | 19 | // NewWith converts the given data value Soy data value, using the provided 20 | // StructOptions for any structs encountered. 21 | func NewWith(convert StructOptions, value interface{}) Value { 22 | // quick return if we're passed an existing data.Value 23 | if val, ok := value.(Value); ok { 24 | return val 25 | } 26 | 27 | if value == nil { 28 | return Null{} 29 | } 30 | 31 | // see if value implements MarshalValue 32 | if mar, ok := value.(Marshaler); ok { 33 | return mar.MarshalValue() 34 | } 35 | 36 | // drill through pointers and interfaces to the underlying type 37 | var v = reflect.ValueOf(value) 38 | for v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr { 39 | v = v.Elem() 40 | } 41 | if !v.IsValid() { 42 | return Null{} 43 | } 44 | 45 | if v.Type() == timeType { 46 | return String(v.Interface().(time.Time).Format(convert.TimeFormat)) 47 | } 48 | 49 | switch v.Kind() { 50 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 51 | return Int(v.Int()) 52 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 53 | return Int(v.Uint()) 54 | case reflect.Float32, reflect.Float64: 55 | return Float(v.Float()) 56 | case reflect.Bool: 57 | return Bool(v.Bool()) 58 | case reflect.String: 59 | return String(v.String()) 60 | case reflect.Slice: 61 | if v.IsNil() { 62 | return List(nil) 63 | } 64 | slice := make(List, v.Len()) 65 | for i := range slice { 66 | slice[i] = NewWith(convert, v.Index(i).Interface()) 67 | } 68 | return slice 69 | case reflect.Map: 70 | var keys = v.MapKeys() 71 | var m = make(Map, len(keys)) 72 | for _, key := range keys { 73 | if key.Kind() != reflect.String { 74 | panic("map keys must be strings") 75 | } 76 | m[key.String()] = NewWith(convert, v.MapIndex(key).Interface()) 77 | } 78 | return m 79 | case reflect.Struct: 80 | return convert.Data(v.Interface()) 81 | default: 82 | panic(fmt.Errorf("unexpected data type: %T (%v)", value, value)) 83 | } 84 | } 85 | 86 | var DefaultStructOptions = StructOptions{ 87 | LowerCamel: true, 88 | TimeFormat: time.RFC3339, 89 | } 90 | 91 | // StructOptions provides flexibility in conversion of structs to soy's 92 | // data.Map format. 93 | type StructOptions struct { 94 | LowerCamel bool // if true, convert field names to lowerCamel. 95 | TimeFormat string // format string for time.Time. (if empty, use ISO-8601) 96 | } 97 | 98 | func (c StructOptions) Data(obj interface{}) Map { 99 | var v = reflect.ValueOf(obj) 100 | var valType = v.Type() 101 | var n = valType.NumField() 102 | var m = make(Map, n) 103 | for i := 0; i < n; i++ { 104 | if !v.Field(i).CanInterface() { 105 | continue 106 | } 107 | var key = valType.Field(i).Name 108 | if c.LowerCamel { 109 | var firstRune, size = utf8.DecodeRuneInString(key) 110 | key = string(unicode.ToLower(firstRune)) + key[size:] 111 | } 112 | m[key] = NewWith(c, v.Field(i).Interface()) 113 | } 114 | return m 115 | } 116 | 117 | // Marshaler is the interface implemented by entities that can marshal 118 | // themselves into a data.Value. 119 | type Marshaler interface { 120 | MarshalValue() Value 121 | } 122 | -------------------------------------------------------------------------------- /data/convert_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | type AInt struct{ A int } 10 | 11 | var jan1, _ = time.Parse(time.RFC3339, "2014-01-01T00:00:00Z") 12 | 13 | func TestNew(t *testing.T) { 14 | tests := []struct{ input, expected interface{} }{ 15 | // basic types 16 | {nil, Null{}}, 17 | {true, Bool(true)}, 18 | {int(0), Int(0)}, 19 | {int64(0), Int(0)}, 20 | {uint32(0), Int(0)}, 21 | {float32(0), Float(0)}, 22 | {"", String("")}, 23 | {[]bool(nil), List(nil)}, 24 | {[]bool{}, List{}}, 25 | {[]string{"a"}, List{String("a")}}, 26 | {[]interface{}{"a"}, List{String("a")}}, 27 | {map[string]string{}, Map{}}, 28 | {map[string]string{"a": "b"}, Map{"a": String("b")}}, 29 | {map[string]interface{}{"a": nil}, Map{"a": Null{}}}, 30 | {map[string]interface{}{"a": []int{1}}, Map{"a": List{Int(1)}}}, 31 | 32 | // type aliases 33 | {[]Int{5}, List{Int(5)}}, 34 | {map[string]Value{"a": List{Int(1)}}, Map{"a": List{Int(1)}}}, 35 | {Map{"foo": Null{}}, Map{"foo": Null{}}}, 36 | 37 | // pointers 38 | {pInt(5), Int(5)}, 39 | {&jan1, String(jan1.Format(time.RFC3339))}, 40 | 41 | // structs with all of the above, and unexported fields. 42 | // also, structs have their fields lowerCamel and Time's default formatting. 43 | {struct { 44 | A Int 45 | L List 46 | PI *int 47 | no Int 48 | T time.Time 49 | }{Int(5), List{}, pInt(2), 5, jan1}, 50 | Map{"a": Int(5), "l": List{}, "pI": Int(2), "t": String(jan1.Format(time.RFC3339))}}, 51 | {[]*struct { 52 | PI *AInt 53 | }{{nil}}, 54 | List{Map{"pI": Null{}}}}, 55 | {testIDURL{1, "https://github.com/robfig/soy"}, 56 | Map{"iD": Int(1), "uRL": String("https://github.com/robfig/soy")}}, 57 | {testIDURLMarshaler{1, "https://github.com/robfig/soy"}, 58 | Map{"id": Int(1), "url": String("https://github.com/robfig/soy")}}, 59 | } 60 | 61 | for _, test := range tests { 62 | output := New(test.input) 63 | if !reflect.DeepEqual(test.expected, output) { 64 | t.Errorf("%#v =>\n %#v, expected:\n%#v", test.input, output, test.expected) 65 | } 66 | } 67 | } 68 | 69 | type testIDURL struct { 70 | ID int 71 | URL string 72 | } 73 | 74 | type testIDURLMarshaler testIDURL 75 | 76 | func (t testIDURLMarshaler) MarshalValue() Value { 77 | return Map{ 78 | "id": New(t.ID), 79 | "url": New(t.URL), 80 | } 81 | } 82 | 83 | func TestStructOptions(t *testing.T) { 84 | var testStruct = struct { 85 | CaseFormat int 86 | Time time.Time 87 | unexported int 88 | Nested struct { 89 | CaseFormat *bool 90 | Time *time.Time 91 | } 92 | NestedSlice []interface{} 93 | NestedMap map[string]interface{} 94 | }{ 95 | CaseFormat: 5, 96 | Time: jan1, 97 | NestedSlice: []interface{}{ 98 | "a", 99 | 2, 100 | DefaultStructOptions, 101 | true, 102 | nil, 103 | 5.0, 104 | []uint8{1, 2, 3}, 105 | []string{"a", "b", "c"}, 106 | map[string]interface{}{ 107 | "foo": 1, 108 | "bar": 2, 109 | "baz": 3, 110 | }, 111 | }, 112 | NestedMap: map[string]interface{}{ 113 | "string": "a", 114 | "int": 1, 115 | "float": 5.0, 116 | "nil": nil, 117 | "slice": []*int{pInt(1), pInt(2), pInt(3)}, 118 | "Struct": DefaultStructOptions, 119 | }, 120 | } 121 | 122 | tests := []struct { 123 | input interface{} 124 | convert StructOptions 125 | expected Map 126 | }{ 127 | {testStruct, DefaultStructOptions, Map{ 128 | "caseFormat": Int(5), 129 | "time": String(jan1.Format(time.RFC3339)), 130 | "nested": Map{ 131 | "caseFormat": Null{}, 132 | "time": Null{}, 133 | }, 134 | "nestedSlice": List{ 135 | String("a"), 136 | Int(2), 137 | Map{ 138 | "lowerCamel": Bool(true), 139 | "timeFormat": String(time.RFC3339), 140 | }, 141 | Bool(true), 142 | Null{}, 143 | Float(5.), 144 | List{Int(1), Int(2), Int(3)}, 145 | List{String("a"), String("b"), String("c")}, 146 | Map{"foo": Int(1), "bar": Int(2), "baz": Int(3)}, 147 | }, 148 | "nestedMap": Map{ 149 | "string": String("a"), 150 | "int": Int(1), 151 | "float": Float(5.0), 152 | "nil": Null{}, 153 | "slice": List{Int(1), Int(2), Int(3)}, 154 | "Struct": Map{ 155 | "lowerCamel": Bool(true), 156 | "timeFormat": String(time.RFC3339), 157 | }}, 158 | }}, 159 | 160 | {testStruct, StructOptions{false, time.Stamp}, Map{ 161 | "CaseFormat": Int(5), 162 | "Time": String(jan1.Format(time.Stamp)), 163 | "Nested": Map{ 164 | "CaseFormat": Null{}, 165 | "Time": Null{}, 166 | }, 167 | "NestedSlice": List{ 168 | String("a"), 169 | Int(2), 170 | Map{ 171 | "LowerCamel": Bool(true), 172 | "TimeFormat": String(time.RFC3339), 173 | }, 174 | Bool(true), 175 | Null{}, 176 | Float(5.), 177 | List{Int(1), Int(2), Int(3)}, 178 | List{String("a"), String("b"), String("c")}, 179 | Map{"foo": Int(1), "bar": Int(2), "baz": Int(3)}, 180 | }, 181 | "NestedMap": Map{ 182 | "string": String("a"), 183 | "int": Int(1), 184 | "float": Float(5.0), 185 | "nil": Null{}, 186 | "slice": List{Int(1), Int(2), Int(3)}, 187 | "Struct": Map{ 188 | "LowerCamel": Bool(true), 189 | "TimeFormat": String(time.RFC3339), 190 | }}, 191 | }}, 192 | } 193 | 194 | for _, test := range tests { 195 | output := test.convert.Data(test.input) 196 | if !reflect.DeepEqual(test.expected, output) { 197 | t.Errorf("%#v =>\n%#v, expected:\n%#v", test.input, output, test.expected) 198 | } 199 | } 200 | } 201 | 202 | func BenchmarkStructOptions(b *testing.B) { 203 | var testStruct = struct { 204 | CaseFormat int 205 | Time time.Time 206 | unexported int 207 | Nested struct { 208 | CaseFormat *bool 209 | Time *time.Time 210 | } 211 | NestedSlice []interface{} 212 | NestedMap map[string]interface{} 213 | }{ 214 | CaseFormat: 5, 215 | Time: jan1, 216 | NestedSlice: []interface{}{ 217 | "a", 218 | 2, 219 | DefaultStructOptions, 220 | true, 221 | nil, 222 | 5.0, 223 | []uint8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 224 | []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"}, 225 | map[string]interface{}{ 226 | "foo": 1, 227 | "bar": 2, 228 | "baz": 3, 229 | "boo": 4, 230 | "poo": 5, 231 | }, 232 | }, 233 | NestedMap: map[string]interface{}{ 234 | "string": "a", 235 | "int": 1, 236 | "float": 5.0, 237 | "nil": nil, 238 | "slice": []*int{pInt(1), pInt(2), pInt(3)}, 239 | "Struct": DefaultStructOptions, 240 | }, 241 | } 242 | 243 | for i := 0; i < b.N; i++ { 244 | var output = NewWith(DefaultStructOptions, testStruct).(Map) 245 | if len(output) != 5 { 246 | b.Errorf("unexpected output") 247 | } 248 | } 249 | } 250 | 251 | func pInt(i int) *int { 252 | return &i 253 | } 254 | -------------------------------------------------------------------------------- /data/value.go: -------------------------------------------------------------------------------- 1 | // Package data contains the definitions for the Soy data types. 2 | package data 3 | 4 | import ( 5 | "math" 6 | "reflect" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // Value represents a Soy data value, which may be one of the enumerated types. 13 | type Value interface { 14 | // Truthy returns true according to the Soy definition of truthy and falsy values. 15 | Truthy() bool 16 | 17 | // String formats this value for display in a template. 18 | String() string 19 | 20 | // Equals returns true if the two values are equal. Specifically, if: 21 | // - They are comparable: they have the same Type, or they are Int and Float 22 | // - (Primitives) They have the same value 23 | // - (Lists, Maps) They are the same instance 24 | // Uncomparable types and unequal values return false. 25 | Equals(other Value) bool 26 | } 27 | 28 | type ( 29 | Undefined struct{} 30 | Null struct{} 31 | Bool bool 32 | Int int64 33 | Float float64 34 | String string 35 | List []Value 36 | Map map[string]Value 37 | ) 38 | 39 | // Index retrieves a value from this list, or Undefined if out of bounds. 40 | func (v List) Index(i int) Value { 41 | if !(0 <= i && i < len(v)) { 42 | return Undefined{} 43 | } 44 | return v[i] 45 | } 46 | 47 | // Key retrieves a value under the named key, or Undefined if it doesn't exist. 48 | func (v Map) Key(k string) Value { 49 | var result, ok = v[k] 50 | if !ok { 51 | return Undefined{} 52 | } 53 | return result 54 | } 55 | 56 | // Marshal --------- 57 | 58 | func (v Undefined) MarshalJSON() ([]byte, error) { return []byte("null"), nil } 59 | func (v Null) MarshalJSON() ([]byte, error) { return []byte("null"), nil } 60 | 61 | // Truthy ---------- 62 | 63 | func (v Undefined) Truthy() bool { return false } 64 | func (v Null) Truthy() bool { return false } 65 | func (v Bool) Truthy() bool { return bool(v) } 66 | func (v Int) Truthy() bool { return v != 0 } 67 | func (v Float) Truthy() bool { return v != 0.0 && float64(v) != math.NaN() } 68 | func (v String) Truthy() bool { return v != "" } 69 | func (v List) Truthy() bool { return true } 70 | func (v Map) Truthy() bool { return true } 71 | 72 | // String ---------- 73 | 74 | func (v Undefined) String() string { panic("Attempted to coerce undefined value into a string.") } 75 | func (v Null) String() string { return "null" } 76 | func (v Bool) String() string { return strconv.FormatBool(bool(v)) } 77 | func (v Int) String() string { return strconv.FormatInt(int64(v), 10) } 78 | func (v Float) String() string { return strconv.FormatFloat(float64(v), 'g', -1, 64) } 79 | func (v String) String() string { return string(v) } 80 | 81 | func (v List) String() string { 82 | var items = make([]string, len(v)) 83 | for i, item := range v { 84 | items[i] = item.String() 85 | } 86 | return "[" + strings.Join(items, ", ") + "]" 87 | } 88 | 89 | func (v Map) String() string { 90 | var items = make([]string, len(v)) 91 | var i = 0 92 | for k, v := range v { 93 | var vstr string 94 | if _, ok := v.(Undefined); ok { 95 | vstr = "undefined" // have mercy 96 | } else { 97 | vstr = v.String() 98 | } 99 | items[i] = k + ": " + vstr 100 | i++ 101 | } 102 | sort.Strings(items) 103 | return "{" + strings.Join(items, ", ") + "}" 104 | } 105 | 106 | // Equals ---------- 107 | 108 | func (v Undefined) Equals(other Value) bool { 109 | _, ok := other.(Undefined) 110 | return ok 111 | } 112 | 113 | func (v Null) Equals(other Value) bool { 114 | _, ok := other.(Null) 115 | return ok 116 | } 117 | 118 | func (v Bool) Equals(other Value) bool { 119 | if o, ok := other.(Bool); ok { 120 | return bool(v) == bool(o) 121 | } 122 | return false 123 | } 124 | 125 | func (v String) Equals(other Value) bool { 126 | if o, ok := other.(String); ok { 127 | return string(v) == string(o) 128 | } 129 | return false 130 | } 131 | 132 | func (v List) Equals(other Value) bool { 133 | if o, ok := other.(List); ok { 134 | return reflect.ValueOf(v).Pointer() == reflect.ValueOf(o).Pointer() 135 | } 136 | return false 137 | } 138 | 139 | func (v Map) Equals(other Value) bool { 140 | if o, ok := other.(Map); ok { 141 | return reflect.ValueOf(v).Pointer() == reflect.ValueOf(o).Pointer() 142 | } 143 | return false 144 | } 145 | 146 | func (v Int) Equals(other Value) bool { 147 | switch o := other.(type) { 148 | case Int: 149 | return v == o 150 | case Float: 151 | return float64(v) == float64(o) 152 | } 153 | return false 154 | } 155 | 156 | func (v Float) Equals(other Value) bool { 157 | switch o := other.(type) { 158 | case Int: 159 | return float64(v) == float64(o) 160 | case Float: 161 | return v == o 162 | } 163 | return false 164 | } 165 | -------------------------------------------------------------------------------- /data/value_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | // Ensure all of the data types implement Value 10 | var ( 11 | _ Value = Undefined{} 12 | _ Value = Null{} 13 | _ Value = Bool(false) 14 | _ Value = Int(0) 15 | _ Value = Float(0.0) 16 | _ Value = String("") 17 | _ Value = List{} 18 | _ Value = Map{} 19 | ) 20 | 21 | // Ensure custom marshalers are implemented 22 | 23 | var ( 24 | _ json.Marshaler = Undefined{} 25 | _ json.Marshaler = Null{} 26 | ) 27 | 28 | func TestKey(t *testing.T) { 29 | tests := []struct { 30 | input interface{} 31 | key string 32 | expected interface{} 33 | }{ 34 | {map[string]interface{}{}, "foo", Undefined{}}, 35 | {map[string]interface{}{"foo": nil}, "foo", Null{}}, 36 | } 37 | 38 | for _, test := range tests { 39 | actual := New(test.input).(Map).Key(test.key) 40 | if !reflect.DeepEqual(test.expected, actual) { 41 | t.Errorf("%v => %#v, expected %#v", test.input, actual, test.expected) 42 | } 43 | } 44 | } 45 | 46 | func TestIndex(t *testing.T) { 47 | tests := []struct { 48 | input interface{} 49 | index int 50 | expected interface{} 51 | }{ 52 | {[]interface{}{}, 0, Undefined{}}, 53 | {[]interface{}{1}, 0, Int(1)}, 54 | } 55 | 56 | for _, test := range tests { 57 | actual := New(test.input).(List).Index(test.index) 58 | if !reflect.DeepEqual(test.expected, actual) { 59 | t.Errorf("%v => %#v, expected %#v", test.input, actual, test.expected) 60 | } 61 | } 62 | } 63 | 64 | func TestCustomMarhshaling(t *testing.T) { 65 | tests := []struct { 66 | input interface{} 67 | expected interface{} 68 | }{ 69 | {Null{}, []byte("null")}, 70 | {Undefined{}, []byte("null")}, 71 | } 72 | 73 | for _, test := range tests { 74 | actual, err := json.Marshal(test.input) 75 | if err != nil { 76 | t.Errorf("unexpected error: %#v", err.Error()) 77 | } 78 | 79 | if !reflect.DeepEqual(test.expected, actual) { 80 | t.Errorf("%v => %#v, expected %#v", test.input, actual, test.expected) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package soy is an implementation of Google's Closure Templates, which are 3 | data-driven templates for generating HTML. 4 | 5 | Compared to html/template, Closure Templates have a few advantages 6 | 7 | * Intuitive templating language that supports simple control flow, expressions and arithmetic. 8 | * The same templates may be used from Go, Java, and Javascript. 9 | * Internationalization is built in 10 | 11 | and specific to this implementation: 12 | 13 | * High performance (> 3x faster than html/template in BenchmarkSimpleTemplate) 14 | * Hot reload for templates 15 | * Parse a directory tree of templates 16 | 17 | Refer to the official language spec for details: 18 | 19 | https://developers.google.com/closure/templates/ 20 | 21 | Template example 22 | 23 | Here is Hello World 24 | 25 | {namespace examples.simple} 26 | 27 | /** 28 | * Says hello to the world.*/ 29 | // */ 30 | /* {template .helloWorld} 31 | Hello world! 32 | {/template} 33 | 34 | 35 | Here is a more customized version that addresses us by name and can use 36 | greetings other than "Hello". 37 | 38 | /** 39 | * Greets a person using "Hello" by default. 40 | * @param name The name of the person. 41 | * @param? greetingWord Optional greeting word to use instead of "Hello".*/ 42 | // */ 43 | /* {template .helloName} 44 | {if not $greetingWord} 45 | Hello {$name}! 46 | {else} 47 | {$greetingWord} {$name}! 48 | {/if} 49 | {/template} 50 | 51 | This last example renders a greeting for each person in a list of names. 52 | 53 | It demonstrates a [foreach] loop with an [ifempty] command. It also shows how to 54 | call other templates and insert their output using the [call] command. Note that 55 | the [data="all"] attribute in the call command passes all of the caller's 56 | template data to the callee template. 57 | 58 | /** 59 | * Greets a person and optionally a list of other people. 60 | * @param name The name of the person. 61 | * @param additionalNames The additional names to greet. May be an empty list.*/ 62 | // */ 63 | /* {template .helloNames} 64 | // Greet the person. 65 | {call .helloName data="all" /}
66 | // Greet the additional people. 67 | {foreach $additionalName in $additionalNames} 68 | {call .helloName} 69 | {param name: $additionalName /} 70 | {/call} 71 | {if not isLast($additionalName)} 72 |
// break after every line except the last 73 | {/if} 74 | {ifempty} 75 | No additional people to greet. 76 | {/foreach} 77 | {/template} 78 | 79 | This example is from 80 | https://developers.google.com/closure/templates/docs/helloworld_java. 81 | 82 | Many more examples of Soy language features/commands may be seen here: 83 | https://github.com/robfig/soy/blob/master/testdata/features.soy 84 | 85 | Usage example 86 | 87 | These are the high level steps: 88 | 89 | * Create a soy.Bundle and add templates to it (the literal template strings, 90 | files, or directories). 91 | * Compile the bundle of templates, resulting in a "Tofu" instance. It provides 92 | access to all your soy. 93 | * Render a HTML template from Tofu by providing the template name and a data 94 | object. 95 | 96 | Typically in a web application you have a directory containing views for all of 97 | your pages. For example: 98 | 99 | app/views/ 100 | app/views/account/ 101 | app/views/feed/ 102 | ... 103 | 104 | This code snippet will parse a file of globals, all Soy templates within 105 | app/views, and provide back a Tofu intance that can be used to render any 106 | declared template. Additionally, if "mode == dev", it will watch the Soy files 107 | for changes and update your compiled templates in the background (or log compile 108 | errors to soy.Logger). Error checking is omitted. 109 | 110 | On startup: 111 | 112 | tofu, _ := soy.NewBundle(). 113 | WatchFiles(true). // watch Soy files, reload on changes 114 | AddGlobalsFile("views/globals.txt"). // parse a file of globals 115 | AddTemplateDir("views"). // load *.soy in all sub-directories 116 | CompileToTofu() 117 | 118 | To render a page: 119 | 120 | var obj = map[string]interface{}{ 121 | "user": user, 122 | "account": account, 123 | } 124 | tofu.Render(resp, "acme.account.overview", obj) 125 | 126 | Structs may be used as the data context too, but keep in mind that they are 127 | converted to data maps -- unlike html/template, the context is pure data, and 128 | you can not call methods on it. 129 | 130 | var obj = HomepageContext{ 131 | User: user, 132 | Account: account, 133 | } 134 | tofu.Render(resp, "acme.account.overview", obj) 135 | 136 | See soyhtml.StructOptions for knobs to control how your structs get converted to 137 | data maps. 138 | 139 | Project Status 140 | 141 | The goal is full compatibility and feature parity with the official Closure 142 | Templates project. 143 | 144 | The server-side templating functionality is well tested and nearly complete, 145 | except for two notable areas: contextual autoescaping and 146 | internationalization/bidi support. Contributions welcome. 147 | 148 | The Javascript generation is early and lacks many generation options, but 149 | it successfully passes the server-side template test suite. Note that it is 150 | possible to run the official Soy compiler to generate your javascript templates 151 | at build time, even if you use this package for server-side templates. 152 | 153 | Please see the TODO file for features that have yet to be implemented. 154 | 155 | Please open a Github Issue for any bugs / problems / comments, or if you find a 156 | template that renders differently than with the official compiler. 157 | */ 158 | package soy 159 | -------------------------------------------------------------------------------- /errortypes/filepos.go: -------------------------------------------------------------------------------- 1 | package errortypes 2 | 3 | import "fmt" 4 | 5 | // ErrFilePos extends the error interface to add details on the file position where the error occurred. 6 | type ErrFilePos interface { 7 | error 8 | File() string 9 | Line() int 10 | Col() int 11 | } 12 | 13 | // NewErrFilePosf creates an error conforming to the ErrFilePos interface. 14 | func NewErrFilePosf(file string, line, col int, format string, args ...interface{}) error { 15 | return &errFilePos{ 16 | error: fmt.Errorf(format, args...), 17 | file: file, 18 | line: line, 19 | col: col, 20 | } 21 | } 22 | 23 | // IsErrFilePos identifies whethere or not the root cause of the provided error is of the ErrFilePos type. 24 | // Wrapped errors are unwrapped via the Cause() function. 25 | func IsErrFilePos(err error) bool { 26 | if err == nil { 27 | return false 28 | } 29 | _, isErrFilePos := err.(ErrFilePos) 30 | return isErrFilePos 31 | } 32 | 33 | // ToErrFilePos converts the input error to an ErrFilePos if possible, or nil if not. 34 | // If IsErrFilePos returns true, this will not return nil. 35 | func ToErrFilePos(err error) ErrFilePos { 36 | if err == nil { 37 | return nil 38 | } 39 | if out, isErrFilePos := err.(ErrFilePos); isErrFilePos { 40 | return out 41 | } 42 | return nil 43 | } 44 | 45 | var _ ErrFilePos = &errFilePos{} 46 | 47 | type errFilePos struct { 48 | error 49 | file string 50 | line int 51 | col int 52 | } 53 | 54 | func (e *errFilePos) File() string { 55 | return e.file 56 | } 57 | 58 | func (e *errFilePos) Line() int { 59 | return e.line 60 | } 61 | 62 | func (e *errFilePos) Col() int { 63 | return e.col 64 | } 65 | -------------------------------------------------------------------------------- /errortypes/filepos_test.go: -------------------------------------------------------------------------------- 1 | package errortypes_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/robfig/soy/errortypes" 8 | ) 9 | 10 | func TestIsErrFilePos(t *testing.T) { 11 | var tests = []struct { 12 | name string 13 | in error 14 | out bool 15 | }{ 16 | { 17 | name: "nil", 18 | out: false, 19 | }, 20 | { 21 | name: "errors.New", 22 | in: errors.New("an error"), 23 | out: false, 24 | }, 25 | { 26 | name: "new ErrFilePos", 27 | in: errortypes.NewErrFilePosf("file.soy", 1, 2, "message"), 28 | out: true, 29 | }, 30 | } 31 | for _, test := range tests { 32 | got := errortypes.IsErrFilePos(test.in) 33 | if got != test.out { 34 | t.Errorf("%s: Expected %v, got %v", test.name, test.out, got) 35 | } 36 | } 37 | } 38 | 39 | func TestToErrFilePos(t *testing.T) { 40 | var tests = []struct { 41 | name string 42 | in error 43 | expectNil bool 44 | expectedFilename string 45 | expectedLine int 46 | expectedCol int 47 | }{ 48 | { 49 | name: "nil", 50 | expectNil: true, 51 | }, 52 | { 53 | name: "errors.New", 54 | in: errors.New("an error"), 55 | expectNil: true, 56 | }, 57 | { 58 | name: "new ErrFilePos", 59 | in: errortypes.NewErrFilePosf("file.soy", 1, 2, "message"), 60 | expectNil: false, 61 | expectedFilename: "file.soy", 62 | expectedLine: 1, 63 | expectedCol: 2, 64 | }, 65 | } 66 | for _, test := range tests { 67 | got := errortypes.ToErrFilePos(test.in) 68 | if test.expectNil && got != nil { 69 | t.Errorf("%s: expected ErrFilePos to be nil", test.name) 70 | } 71 | if !test.expectNil { 72 | if got == nil { 73 | t.Errorf("%s: expected ErrFilePos to be non-nil", test.name) 74 | return 75 | } 76 | if got.File() != test.expectedFilename { 77 | t.Errorf("%s: expected file '%s', got '%s'", test.name, test.expectedFilename, got.File()) 78 | } 79 | if got.Line() != test.expectedLine { 80 | t.Errorf("%s: expected line %d, got %d", test.name, test.expectedLine, got.Line()) 81 | } 82 | if got.Col() != test.expectedCol { 83 | t.Errorf("%s: expected col %d, got %d", test.name, test.expectedCol, got.Col()) 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /features_test.go: -------------------------------------------------------------------------------- 1 | package soy 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "html/template" 9 | "io/ioutil" 10 | "math/rand" 11 | "os" 12 | "reflect" 13 | "testing" 14 | 15 | "github.com/robertkrimen/otto" 16 | "github.com/robfig/soy/data" 17 | "github.com/robfig/soy/soyjs" 18 | ) 19 | 20 | type d map[string]interface{} 21 | 22 | type featureTest struct { 23 | name string 24 | data d 25 | output string 26 | } 27 | 28 | var featureTests = []featureTest{ 29 | {"demoComments", nil, `blah blah
http://www.google.com
`}, 30 | 31 | {"demoLineJoining", nil, 32 | `First second.
` + 33 | `Firstsecond.
` + 34 | `Firstsecond.
` + 35 | `First second.
` + 36 | `Firstsecond.
`}, 37 | 38 | {"demoRawTextCommands", nil, 39 | `
Space       : AA BB
` + 40 | `Empty string: AABB
` + 41 | `New line : AA 42 | BB
` + 43 | "Carriage ret: AA\rBB
" + 44 | `Tab : AA BB
` + 45 | `Left brace : AA{BB
` + 46 | `Right brace : AA}BB
` + 47 | `Literal : AA BB { CC 48 | DD } EE {sp}{\n}{rb} FF
`}, 49 | 50 | {"demoPrint", d{"boo": "Boo!", "two": 2}, 51 | `Boo!
` + 52 | `Boo!
` + 53 | `3
` + 54 | `Boo!
` + 55 | `3
` + 56 | `88, false.
`}, 57 | 58 | {"demoPrintDirectives", d{ 59 | "longVarName": "thisIsSomeRidiculouslyLongVariableName", 60 | "elementId": "my_element_id", 61 | "cssClass": "my_css_class", 62 | }, `insertWordBreaks:
` + 63 | `
thisIsSomeRidiculouslyLongVariableName
` + 64 | `thisIsSomeRidiculouslyLongVariableName
` + 65 | `
id:
` + 66 | `Hello`}, 67 | 68 | {"demoAutoescapeTrue", d{"italicHtml": "italic"}, 69 | `<i>italic</i>
` + 70 | `italic
`}, 71 | 72 | {"demoAutoescapeFalse", d{"italicHtml": "italic"}, 73 | `italic
` + 74 | `<i>italic</i>
`}, 75 | 76 | {"demoMsg", d{"name": "Ed", "labsUrl": "http://labs.google.com"}, 77 | `Hello Ed!
` + 78 | `Click here to access Labs.
` + 79 | `Archive
` + 80 | `Archive
`}, 81 | 82 | {"demoPlural", d{"eggs": 1}, "You have one egg
"}, 83 | {"demoPlural", d{"eggs": 2}, "You have 2 eggs
"}, 84 | {"demoPlural", d{"eggs": 0}, "You have 0 eggs
"}, 85 | 86 | {"demoIf", d{"pi": 3.14159}, `3.14159 is a good approximation of pi.
`}, 87 | {"demoIf", d{"pi": 2.71828}, `2.71828 is a bad approximation of pi.
`}, 88 | {"demoIf", d{"pi": 1.61803}, `1.61803 is nowhere near the value of pi.
`}, 89 | 90 | {"demoSwitch", d{"name": "Fay"}, `Dear Fay,  You've been good this year.  --Santa
`}, 91 | {"demoSwitch", d{"name": "Go"}, `Dear Go,  You've been bad this year.  --Santa
`}, 92 | {"demoSwitch", d{"name": "Hal"}, `Dear Hal,  You don't really believe in me, do you?  --Santa
`}, 93 | {"demoSwitch", d{"name": "Ivy"}, `Dear Ivy,  You've been good this year.  --Santa
`}, 94 | 95 | {"demoForeach", d{"persons": []d{ 96 | {"name": "Jen", "numWaffles": 1}, 97 | {"name": "Kai", "numWaffles": 3}, 98 | {"name": "Lex", "numWaffles": 1}, 99 | {"name": "Mel", "numWaffles": 2}, 100 | }}, `First, Jen ate 1 waffle.
` + 101 | `Then Kai ate 3 waffles.
` + 102 | `Then Lex ate 1 waffle.
` + 103 | `Finally, Mel ate 2 waffles.
`}, 104 | 105 | {"demoFor", d{"numLines": 3}, 106 | `Line 1 of 3.
` + 107 | `Line 2 of 3.
` + 108 | `Line 3 of 3.
` + 109 | `2... 4... 6... 8... Who do we appreciate?
`}, 110 | 111 | {"demoCallWithoutParam", 112 | d{"name": "Neo", "tripInfo": d{"name": "Neo", "destination": "The Matrix"}}, 113 | `Hello world!
` + 114 | `A trip was taken.
` + 115 | `Neo took a trip.
` + 116 | `Neo took a trip to The Matrix.
`}, 117 | 118 | {"demoCallWithParam", d{ 119 | "name": "Oz", 120 | "companionName": "Pip", 121 | "destinations": []string{ 122 | "Gillikin Country", 123 | "Munchkin Country", 124 | "Quadling Country", 125 | "Winkie Country"}}, 126 | `Oz took a trip to Gillikin Country.
` + 127 | `Pip took a trip to Gillikin Country.
` + 128 | `Oz took a trip to Munchkin Country.
` + 129 | `Oz took a trip to Quadling Country.
` + 130 | `Pip took a trip to Quadling Country.
` + 131 | `Oz took a trip to Winkie Country.
`}, 132 | 133 | {"demoCallWithParamBlock", d{"name": "Quo"}, `Quo took a trip to Zurich.
`}, 134 | 135 | {"demoExpressions", d{ 136 | "currentYear": 2008, 137 | "students": []d{ 138 | {"name": "Rob", "major": "Physics", "year": 1999}, 139 | {"name": "Sha", "major": "Finance", "year": 1980}, 140 | {"name": "Tim", "major": "Engineering", "year": 2005}, 141 | {"name": "Uma", "major": "Biology", "year": 1972}, 142 | }}, 143 | `First student's major: Physics
` + 144 | `Last student's year: 1972
` + 145 | `Random student's major: Biology
` + 146 | `Rob: First. Physics. Scientist. Young. 90s. 90s.
` + 147 | `Sha: Middle. Even. Finance. 80s. 80s.
` + 148 | `Tim: Engineering. Young. 00s. 00s.
` + 149 | `Uma: Last. Even. Biology. Scientist. 70s. 70s.
`}, 150 | 151 | {"demoDoubleBraces", d{ 152 | "setName": "prime numbers", 153 | "setMembers": []int{2, 3, 5, 7, 11, 13}, 154 | }, 155 | `The set of prime numbers is {2, 3, 5, 7, 11, 13, ...}.`}, 156 | 157 | // {"demoBidiSupport", d{ 158 | // "title": "2008: A BiDi Odyssey", 159 | // "author": "John Doe, Esq.", 160 | // "year": "1973", 161 | // "keywords": []string{ 162 | // "Bi(Di)", 163 | // "2008 (\u05E9\u05E0\u05D4)", 164 | // "2008 (year)", 165 | // }}, 166 | // `
2008: A BiDi Odyssey
` + 167 | // `
2008: A BiDi Odyssey
by John Doe, Esq. (1973)` + 168 | // `
Your favorite keyword: ` + 169 | // `
` + 172 | // `Help
`}, 173 | 174 | } 175 | 176 | // TestFeatures runs through the feature examples from: 177 | // http://closure-templates.googlecode.com/svn/trunk/examples/features.soy 178 | // The expected output is taken directly from that produced by the Java program. 179 | func TestFeatures(t *testing.T) { 180 | rand.Seed(1) // two of the templates use a random number. 181 | runFeatureTests(t, featureTests) 182 | } 183 | 184 | func TestMsgs(t *testing.T) { 185 | 186 | } 187 | 188 | func BenchmarkLexParseFeatures(b *testing.B) { 189 | var ( 190 | features = mustReadFile("testdata/features.soy") 191 | simple = mustReadFile("testdata/simple.soy") 192 | ) 193 | b.ResetTimer() 194 | for i := 0; i < b.N; i++ { 195 | var _, err = NewBundle(). 196 | AddGlobalsFile("testdata/FeaturesUsage_globals.txt"). 197 | AddTemplateString("", features). 198 | AddTemplateString("", simple). 199 | Compile() 200 | if err != nil { 201 | b.Error(err) 202 | } 203 | } 204 | } 205 | 206 | func BenchmarkExecuteFeatures(b *testing.B) { 207 | var ( 208 | features = mustReadFile("testdata/features.soy") 209 | simple = mustReadFile("testdata/simple.soy") 210 | ) 211 | var tofu, err = NewBundle(). 212 | AddGlobalsFile("testdata/FeaturesUsage_globals.txt"). 213 | AddTemplateString("", features). 214 | AddTemplateString("", simple). 215 | CompileToTofu() 216 | if err != nil { 217 | panic(err) 218 | } 219 | b.ResetTimer() 220 | 221 | var buf = new(bytes.Buffer) 222 | for i := 0; i < b.N; i++ { 223 | for _, test := range featureTests { 224 | // if test.name != "demoAutoescapeTrue" { 225 | // continue 226 | // } 227 | buf.Reset() 228 | err = tofu.Render(buf, "soy.examples.features."+test.name, test.data) 229 | if err != nil { 230 | b.Error(err) 231 | } 232 | } 233 | } 234 | } 235 | 236 | func BenchmarkExecuteSimple_Soy(b *testing.B) { 237 | var tofu, err = NewBundle(). 238 | AddTemplateString("", mustReadFile("testdata/simple.soy")). 239 | CompileToTofu() 240 | if err != nil { 241 | panic(err) 242 | } 243 | b.ResetTimer() 244 | var buf = new(bytes.Buffer) 245 | var testdata = []data.Map{ 246 | {"names": data.List{}}, 247 | {"names": data.List{data.String("Rob")}}, 248 | {"names": data.List{data.String("Rob"), data.String("Joe")}}, 249 | } 250 | for i := 0; i < b.N; i++ { 251 | for _, data := range testdata { 252 | buf.Reset() 253 | err = tofu.Render(buf, "soy.examples.simple.helloNames", data) 254 | if err != nil { 255 | b.Error(err) 256 | } 257 | } 258 | } 259 | } 260 | 261 | func BenchmarkExecuteSimple_Go(b *testing.B) { 262 | // from https://groups.google.com/forum/#!topic/golang-nuts/mqRbR7AFJj0 263 | var fns = template.FuncMap{ 264 | "last": func(x int, a interface{}) bool { 265 | return x == reflect.ValueOf(a).Len()-1 266 | }, 267 | } 268 | 269 | var tmpl = template.Must(template.New("").Funcs(fns).Parse(` 270 | {{define "go.examples.simple.helloWorld"}} 271 | Hello world! 272 | {{end}} 273 | 274 | {{define "go.examples.simple.helloName"}} 275 | {{if .}} 276 | Hello {{.}}! 277 | {{else}} 278 | {{template "go.examples.simple.helloWorld"}} 279 | {{end}} 280 | {{end}} 281 | 282 | {{define "go.examples.simple.helloNames"}} 283 | {{range $i, $name := .names}} 284 | {{template "go.examples.simple.helloName" $name}} 285 | {{if last $i $.names | not }} 286 |
287 | {{end}} 288 | {{else}} 289 | {{template "go.examples.simple.helloWorld"}} 290 | {{end}} 291 | {{end}}`)) 292 | 293 | var buf = new(bytes.Buffer) 294 | var testdata = []map[string]interface{}{ 295 | {"names": nil}, 296 | {"names": []string{"Rob"}}, 297 | {"names": []string{"Rob", "Joe"}}, 298 | } 299 | 300 | b.ResetTimer() 301 | for i := 0; i < b.N; i++ { 302 | for _, data := range testdata { 303 | buf.Reset() 304 | var err = tmpl.ExecuteTemplate(buf, "go.examples.simple.helloNames", data) 305 | if err != nil { 306 | b.Error(err) 307 | } 308 | } 309 | } 310 | } 311 | 312 | func BenchmarkSimpleTemplate_Soy(b *testing.B) { 313 | var tofu, err = NewBundle(). 314 | AddTemplateString("", ` 315 | {namespace small} 316 | /** 317 | * @param foo 318 | * @param bar 319 | * @param baz 320 | */ 321 | {template .test} 322 | some {$foo}, some {$bar}, more {$baz} 323 | {/template}`). 324 | CompileToTofu() 325 | if err != nil { 326 | panic(err) 327 | } 328 | b.ResetTimer() 329 | var buf = new(bytes.Buffer) 330 | for i := 0; i < b.N; i++ { 331 | buf.Reset() 332 | err = tofu.Render(buf, "small.test", data.Map{ 333 | "foo": data.String("foostring"), 334 | "bar": data.Int(42), 335 | "baz": data.Bool(true), 336 | }) 337 | if err != nil { 338 | b.Error(err) 339 | } 340 | } 341 | } 342 | 343 | func BenchmarkSimpleTemplate_Go(b *testing.B) { 344 | var tmpl = template.Must(template.New("").Parse(` 345 | {{define "small.test"}} 346 | some {{.foo}}, some {{.bar}}, more {{.baz}} 347 | {{end}}`)) 348 | b.ResetTimer() 349 | var buf = new(bytes.Buffer) 350 | for i := 0; i < b.N; i++ { 351 | buf.Reset() 352 | var err = tmpl.ExecuteTemplate(buf, "small.test", data.Map{ 353 | "foo": data.String("foostring"), 354 | "bar": data.Int(42), 355 | "baz": data.Bool(true), 356 | }) 357 | if err != nil { 358 | b.Error(err) 359 | } 360 | } 361 | } 362 | 363 | // TestFeaturesJavascript runs the javascript compiled by this implementation 364 | // against that compiled by the reference implementation. 365 | func TestFeaturesJavascript(t *testing.T) { 366 | rand.Seed(14) 367 | var registry, err = NewBundle(). 368 | AddGlobalsFile("testdata/FeaturesUsage_globals.txt"). 369 | AddTemplateFile("testdata/simple.soy"). 370 | AddTemplateFile("testdata/features.soy"). 371 | Compile() 372 | if err != nil { 373 | t.Error(err) 374 | return 375 | } 376 | var otto = initJs(t) 377 | for _, soyfile := range registry.SoyFiles { 378 | var buf bytes.Buffer 379 | var err = soyjs.Write(&buf, soyfile, soyjs.Options{}) 380 | if err != nil { 381 | t.Error(err) 382 | return 383 | } 384 | _, err = otto.Run(buf.String()) 385 | if err != nil { 386 | t.Error(err) 387 | return 388 | } 389 | } 390 | 391 | // Now run all the tests. 392 | for _, test := range featureTests { 393 | var jsonData, _ = json.Marshal(test.data) 394 | var renderStatement = fmt.Sprintf("%s(JSON.parse(%q));", 395 | "soy.examples.features."+test.name, string(jsonData)) 396 | var actual, err = otto.Run(renderStatement) 397 | if err != nil { 398 | t.Errorf("render error: %v\n%v", err, string(jsonData)) 399 | continue 400 | } 401 | 402 | if actual.String() != test.output { 403 | t.Errorf("%s\nexpected\n%q\n\ngot\n%q", test.name, test.output, actual.String()) 404 | } 405 | } 406 | } 407 | 408 | func initJs(t *testing.T) *otto.Otto { 409 | var otto = otto.New() 410 | soyutilsFile, err := os.Open("soyjs/lib/soyutils.js") 411 | if err != nil { 412 | panic(err) 413 | } 414 | // remove any non-otto compatible regular expressions 415 | var soyutilsBuf bytes.Buffer 416 | var scanner = bufio.NewScanner(soyutilsFile) 417 | var i = 1 418 | for scanner.Scan() { 419 | switch i { 420 | case 2565, 2579, 2586: 421 | // skip these regexes 422 | // soy.esc.$$FILTER_FOR_FILTER_CSS_VALUE_ 423 | // soy.esc.$$FILTER_FOR_FILTER_HTML_ATTRIBUTES_ 424 | // soy.esc.$$FILTER_FOR_FILTER_HTML_ELEMENT_NAME_ 425 | default: 426 | soyutilsBuf.Write(scanner.Bytes()) 427 | soyutilsBuf.Write([]byte("\n")) 428 | } 429 | i++ 430 | } 431 | // load the soyutils library 432 | _, err = otto.Run(soyutilsBuf.String()) 433 | if err != nil { 434 | t.Errorf("soyutils error: %v", err) 435 | panic(err) 436 | } 437 | return otto 438 | } 439 | 440 | func runFeatureTests(t *testing.T, tests []featureTest) { 441 | var features = mustReadFile("testdata/features.soy") 442 | var tofu, err = NewBundle(). 443 | AddGlobalsFile("testdata/FeaturesUsage_globals.txt"). 444 | AddTemplateString("", features). 445 | AddTemplateFile("testdata/simple.soy"). 446 | CompileToTofu() 447 | if err != nil { 448 | t.Error(err) 449 | return 450 | } 451 | 452 | b := new(bytes.Buffer) 453 | for _, test := range tests { 454 | b.Reset() 455 | err = tofu.Render(b, "soy.examples.features."+test.name, test.data) 456 | if err != nil { 457 | t.Error(err) 458 | continue 459 | } 460 | if b.String() != test.output { 461 | t.Errorf("%s\nexpected\n%q\n\ngot\n%q", test.name, test.output, b.String()) 462 | } 463 | } 464 | } 465 | 466 | func mustReadFile(filename string) string { 467 | f, err := os.Open(filename) 468 | if err != nil { 469 | panic(err) 470 | } 471 | content, err := ioutil.ReadAll(f) 472 | if err != nil { 473 | panic(err) 474 | } 475 | return string(content) 476 | } 477 | -------------------------------------------------------------------------------- /globals.go: -------------------------------------------------------------------------------- 1 | package soy 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "strings" 8 | 9 | "github.com/robfig/soy/data" 10 | "github.com/robfig/soy/parse" 11 | "github.com/robfig/soy/soyhtml" 12 | ) 13 | 14 | // ParseGlobals parses the given input, expecting the form: 15 | // = 16 | // 17 | // Furthermore: 18 | // - Empty lines and lines beginning with '//' are ignored. 19 | // - must be a valid template expression literal for a primitive 20 | // type (null, boolean, integer, float, or string) 21 | func ParseGlobals(input io.Reader) (data.Map, error) { 22 | var globals = make(data.Map) 23 | var scanner = bufio.NewScanner(input) 24 | for scanner.Scan() { 25 | var line = scanner.Text() 26 | if len(line) == 0 || strings.HasPrefix(line, "//") { 27 | continue 28 | } 29 | var eq = strings.Index(line, "=") 30 | if eq == -1 { 31 | return nil, fmt.Errorf("no equals on line: %q", line) 32 | } 33 | var ( 34 | name = strings.TrimSpace(line[:eq]) 35 | expr = strings.TrimSpace(line[eq+1:]) 36 | ) 37 | var node, err = parse.Expr(expr) 38 | if err != nil { 39 | return nil, err 40 | } 41 | exprValue, err := soyhtml.EvalExpr(node) 42 | if err != nil { 43 | return nil, err 44 | } 45 | globals[name] = exprValue 46 | } 47 | if err := scanner.Err(); err != nil { 48 | return nil, err 49 | } 50 | return globals, nil 51 | } 52 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/robfig/soy 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 7 | github.com/fsnotify/fsnotify v1.4.9 8 | github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff 9 | github.com/robfig/gettext v0.0.0-20200526193151-a093425df149 10 | github.com/sergi/go-diff v1.1.0 // indirect 11 | golang.org/x/text v0.3.8 12 | gopkg.in/sourcemap.v1 v1.0.5 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= 2 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 7 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 8 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 9 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 10 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 11 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 12 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff h1:+6NUiITWwE5q1KO6SAfUX918c+Tab0+tGAM/mtdlUyA= 16 | github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY= 17 | github.com/robfig/gettext v0.0.0-20200526193151-a093425df149 h1:UdCKxM6GqWm6z97V7jFUctMG5ZCIMYKROuJzamw5P+w= 18 | github.com/robfig/gettext v0.0.0-20200526193151-a093425df149/go.mod h1:5KSZdCir8kQ33UwFOeBzxIXDVCb7ine4/iCMiJ9D1oQ= 19 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 20 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 21 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 22 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 23 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 24 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 25 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 26 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 27 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 28 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 29 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 30 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 31 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 32 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 33 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 34 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 36 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 37 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= 39 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 41 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 42 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 43 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 44 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 45 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 46 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 47 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 48 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 49 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 50 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 51 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 52 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 53 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= 55 | gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= 56 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 57 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 58 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 59 | -------------------------------------------------------------------------------- /parse/quote.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | "unicode/utf8" 7 | ) 8 | 9 | var unescapes = map[rune]rune{ 10 | '\\': '\\', 11 | '\'': '\'', 12 | 'n': '\n', 13 | 'r': '\r', 14 | 't': '\t', 15 | 'b': '\b', 16 | 'f': '\f', 17 | } 18 | 19 | var escapes = make(map[rune]rune) 20 | 21 | func init() { 22 | for k, v := range unescapes { 23 | escapes[v] = k 24 | } 25 | } 26 | 27 | // quoteString quotes the given string with single quotes, according to the Soy 28 | // spec for string literals. 29 | func quoteString(s string) string { 30 | var q = make([]rune, 1, len(s)+10) 31 | q[0] = '\'' 32 | for _, ch := range s { 33 | if seq, ok := escapes[ch]; ok { 34 | q = append(q, '\\', seq) 35 | continue 36 | } 37 | q = append(q, ch) 38 | } 39 | return string(append(q, '\'')) 40 | } 41 | 42 | // unquoteString takes a quoted Soy string (including the surrounding quotes) 43 | // and returns the unquoted string, along with any error encountered. 44 | func unquoteString(s string) (string, error) { 45 | n := len(s) 46 | if n < 2 { 47 | return "", errors.New("too short a string") 48 | } 49 | 50 | if '\'' != s[0] || '\'' != s[n-1] { 51 | return "", errors.New("string not surrounded by quotes") 52 | } 53 | 54 | s = s[1 : n-1] 55 | if !contains(s, '\\') && !contains(s, '\'') { 56 | return s, nil 57 | } 58 | 59 | var escaping = false 60 | var result = make([]rune, 0, len(s)) 61 | for i := 0; i < len(s); { 62 | r, size := utf8.DecodeRuneInString(s[i:]) 63 | i += size 64 | 65 | if escaping { 66 | if r == 'u' { 67 | if i+4 > len(s) { 68 | return "", errors.New("error scanning unicode escape, expect \\uNNNN") 69 | } 70 | num, err := strconv.ParseInt(s[i:i+4], 16, 0) 71 | if err != nil { 72 | return "", err 73 | } 74 | r = rune(num) 75 | i += 4 76 | } else { 77 | replacement, ok := unescapes[r] 78 | if !ok { 79 | return "", errors.New("unrecognized escape code: \\" + s[i-1:i]) 80 | } 81 | r = rune(replacement) 82 | } 83 | } 84 | 85 | escaping = ((r == '\\') && !escaping) 86 | if !escaping { 87 | result = append(result, r) 88 | } 89 | } 90 | return string(result), nil 91 | } 92 | 93 | func contains(s string, c byte) bool { 94 | for i := 0; i < len(s); i++ { 95 | if s[i] == c { 96 | return true 97 | } 98 | } 99 | return false 100 | } 101 | -------------------------------------------------------------------------------- /parse/quote_test.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import "testing" 4 | 5 | func TestQuote(t *testing.T) { 6 | var tests = []struct{ input, output string }{ 7 | {"", `''`}, 8 | {"a", `'a'`}, 9 | {"\n", `'\n'`}, 10 | {"\u2222", "'\u2222'"}, // (doesn't turn it back into escape sequence) 11 | {"Aa`! \n \r \t \\ ' \"", "'Aa`! \\n \\r \\t \\\\ \\' \"'"}, 12 | {"\u2222 \uEEEE \u9EC4 \u607A", "'\u2222 \uEEEE \u9EC4 \u607A'"}, 13 | {"\\.", `'\\.'`}, 14 | {"\\\n", `'\\\n'`}, 15 | } 16 | for _, test := range tests { 17 | if quoteString(test.input) != test.output { 18 | t.Errorf("%v => %v, expected %v", test.input, quoteString(test.input), test.output) 19 | } 20 | } 21 | } 22 | 23 | func TestUnquote(t *testing.T) { 24 | var tests = []struct{ input, output string }{ 25 | {`''`, ""}, 26 | {`'a'`, "a"}, 27 | {`'\n'`, "\n"}, 28 | {`'\u2222'`, "\u2222"}, 29 | {`'\\'`, "\\"}, 30 | {`'\\.'`, "\\."}, 31 | {`'\\\n'`, "\\\n"}, 32 | } 33 | for _, test := range tests { 34 | actual, err := unquoteString(test.input) 35 | if err != nil { 36 | t.Error(err) 37 | continue 38 | } 39 | if actual != test.output { 40 | t.Errorf("%v => %v, expected %v", test.input, actual, test.output) 41 | } 42 | } 43 | } 44 | 45 | func TestUnquoteUnrecognized(t *testing.T) { 46 | var tests = []string{ 47 | `'\0'`, 48 | `'\a'`, 49 | `'\z'`, 50 | } 51 | for _, tc := range tests { 52 | _, err := unquoteString(tc) 53 | if err == nil { 54 | t.Errorf("expected unrecognized escape sequence %s to fail", tc) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /parse/rawtext.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import "unicode/utf8" 4 | 5 | type rawtextlexer struct { 6 | str string 7 | pos int 8 | lastpos int 9 | } 10 | 11 | func (l *rawtextlexer) eof() bool { 12 | return l.pos >= len(l.str) 13 | } 14 | func (l *rawtextlexer) next() rune { 15 | l.lastpos = l.pos 16 | var r, width = utf8.DecodeRuneInString(l.str[l.pos:]) 17 | l.pos += width 18 | return r 19 | } 20 | 21 | // rawtext processes the raw text found in templates: 22 | // - trim leading/trailing whitespace if either: 23 | // a. the whitespace includes a newline, or 24 | // b. the caller tells us the surrounding content is a tight joiner by trimBefore/After 25 | // - trim leading and trailing whitespace on each internal line 26 | // - join lines with no space if '<' or '>' are on either side, else with 1 space. 27 | func rawtext(s string, trimBefore, trimAfter bool) []byte { 28 | var lex = rawtextlexer{s, 0, 0} 29 | var ( 30 | spaces = 0 31 | seenNewline = trimBefore 32 | lastChar rune 33 | charBeforeTrim rune 34 | result = make([]byte, len(s)) 35 | resultLen = 0 36 | ) 37 | if trimBefore { 38 | spaces = 1 39 | } 40 | 41 | for { 42 | if lex.eof() { 43 | // if we haven't seen a newline, add all the space we've been trimming. 44 | if !seenNewline && spaces > 0 && !trimAfter { 45 | for i := lex.pos - spaces; i < lex.pos; i++ { 46 | result[resultLen] = s[i] 47 | resultLen++ 48 | } 49 | } 50 | return result[:resultLen] 51 | } 52 | var r = lex.next() 53 | 54 | // join lines 55 | if spaces > 0 { 56 | // more space, keep going 57 | if isSpace(r) { 58 | spaces++ 59 | continue 60 | } 61 | if isEndOfLine(r) { 62 | spaces++ 63 | seenNewline = true 64 | continue 65 | } 66 | 67 | // done with scanning a set of space. actions: 68 | // - add the run of space to the result if we haven't seen a newline. 69 | // - add one space if the character before and after the newline are not tight joiners. 70 | // - else, ignore the space. 71 | switch { 72 | case !seenNewline: 73 | for i := lex.lastpos - spaces; i < lex.lastpos; i++ { 74 | result[resultLen] = s[i] 75 | resultLen++ 76 | } 77 | case seenNewline && !isTightJoiner(charBeforeTrim) && !isTightJoiner(r): 78 | result[resultLen] = ' ' 79 | resultLen++ 80 | default: 81 | // ignore the space 82 | } 83 | spaces = 0 84 | } 85 | 86 | // begin to trim 87 | seenNewline = isEndOfLine(r) 88 | if isSpace(r) || seenNewline { 89 | spaces = 1 90 | charBeforeTrim = lastChar 91 | continue 92 | } 93 | 94 | // non-space characters are added verbatim. 95 | for i := lex.lastpos; i < lex.pos; i++ { 96 | result[resultLen] = lex.str[i] 97 | resultLen++ 98 | } 99 | lastChar = r 100 | } 101 | } 102 | 103 | func isTightJoiner(r rune) bool { 104 | switch r { 105 | case 0, '<', '>': 106 | return true 107 | } 108 | return false 109 | } 110 | -------------------------------------------------------------------------------- /parse/rawtext_test.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import "testing" 4 | 5 | func TestRawTextTrim(t *testing.T) { 6 | type test struct{ input, output string } 7 | var tests = []test{ 8 | {"", ""}, 9 | {" ", ""}, 10 | {" ", ""}, 11 | {"\n", ""}, 12 | {"\n\n \r\n\t ", ""}, 13 | {"a", "a"}, 14 | {"a ", "a"}, 15 | {" a", "a"}, 16 | {"a\n", "a"}, 17 | {"\na", "a"}, 18 | {"a \n ", "a"}, 19 | {" \n a", "a"}, 20 | {"a\nb", "a b"}, 21 | {" b ", " b"}, 22 | } 23 | for _, test := range tests { 24 | var actual = string(rawtext(test.input, true, true)) 25 | if test.output != actual { 26 | t.Errorf("input: %q, expected %q, got %q", test.input, test.output, actual) 27 | } 28 | } 29 | } 30 | 31 | func TestRawTextNoTrim(t *testing.T) { 32 | type test struct{ input, output string } 33 | var tests = []test{ 34 | {"", ""}, 35 | {" ", " "}, 36 | {" ", " "}, 37 | {"\n", ""}, 38 | {"\n\n \r\n\t ", ""}, 39 | {"a", "a"}, 40 | {"a ", "a "}, 41 | {" a", " a"}, 42 | {"a\n", "a"}, 43 | {"\na", "a"}, 44 | {"a \n ", "a"}, 45 | {" \n a", "a"}, 46 | {"a\nb", "a b"}, 47 | {"\n\t a \nb\n\n", "a b"}, 48 | {"a / b", "a / b"}, 49 | {"a \t /\nb", "a \t / b"}, 50 | {"
\n", "
"}, 51 | {"
", ""}, 52 | {" \n\t", " "}, 53 | {" \n\t b \r\n\t ", "b"}, 54 | {"a \n\t \n d\nd", "a d d"}, 55 | {"a
\n\t b \n\n\t \n\t c", "a
b c"}, 56 | {"\u2222", "\u2222"}, 57 | {" \u2222", " \u2222"}, 58 | {"\u2222 ", "\u2222 "}, 59 | {" \n \u2222", "\u2222"}, 60 | {"\u2222 \n ", "\u2222"}, 61 | {" \n\t\u2222 \n\t\r ", "\u2222"}, 62 | {"\u2222 <\uEEEE> \n\t<\u9EC4> \n \u607A\n\u607A", "\u2222 <\uEEEE><\u9EC4>\u607A \u607A"}, 63 | } 64 | 65 | for _, test := range tests { 66 | var actual = string(rawtext(test.input, false, false)) 67 | if test.output != actual { 68 | t.Errorf("input: %q, expected %q, got %q", test.input, test.output, actual) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /parsepasses/datarefcheck.go: -------------------------------------------------------------------------------- 1 | // Package parsepasses contains routines that validate or rewrite a Soy AST. 2 | package parsepasses 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/robfig/soy/ast" 8 | "github.com/robfig/soy/template" 9 | ) 10 | 11 | // CheckDataRefs validates that: 12 | // 1. all data refs are provided by @params or {let} nodes (except $ij) 13 | // 2. any data declared as a @param is used by the template (or passed via {call}) 14 | // 3. all {call} params are declared as @params in the called template soydoc. 15 | // 4. a {call}'ed template is passed all required @params, or a data="$var" 16 | // 5. {call}'d templates actually exist in the registry. 17 | // 6. any variable created by {let} is used somewhere 18 | // 7. {let} variable names are valid. ('ij' is not allowed.) 19 | // 8. Only one parameter declaration mechanism (soydoc vs headers) is used. 20 | func CheckDataRefs(reg template.Registry) (err error) { 21 | var currentTemplate string 22 | defer func() { 23 | if err2 := recover(); err2 != nil { 24 | err = fmt.Errorf("template %v: %v", currentTemplate, err2) 25 | } 26 | }() 27 | 28 | for _, t := range reg.Templates { 29 | currentTemplate = t.Node.Name 30 | tc := newTemplateChecker(reg, t) 31 | tc.checkTemplate(t.Node) 32 | 33 | // check that all params appear in the usedKeys 34 | var unusedParamNames []string 35 | for _, param := range tc.params { 36 | if !contains(tc.usedKeys, param) { 37 | unusedParamNames = append(unusedParamNames, param) 38 | } 39 | } 40 | if len(unusedParamNames) > 0 { 41 | panic(fmt.Errorf("params %q are unused", unusedParamNames)) 42 | } 43 | } 44 | return nil 45 | } 46 | 47 | type templateChecker struct { 48 | registry template.Registry 49 | params []string 50 | letVars []string 51 | forVars []string 52 | usedKeys []string 53 | } 54 | 55 | func newTemplateChecker(reg template.Registry, tpl template.Template) *templateChecker { 56 | var paramNames []string 57 | for _, param := range tpl.Doc.Params { 58 | paramNames = append(paramNames, param.Name) 59 | } 60 | return &templateChecker{reg, paramNames, nil, nil, nil} 61 | } 62 | 63 | func (tc *templateChecker) checkTemplate(node ast.Node) { 64 | switch node := node.(type) { 65 | case *ast.LetValueNode: 66 | tc.checkLet(node.Name) 67 | tc.letVars = append(tc.letVars, node.Name) 68 | case *ast.LetContentNode: 69 | tc.checkLet(node.Name) 70 | tc.letVars = append(tc.letVars, node.Name) 71 | case *ast.CallNode: 72 | tc.checkCall(node) 73 | case *ast.ForNode: 74 | tc.forVars = append(tc.forVars, node.Var) 75 | case *ast.DataRefNode: 76 | tc.visitKey(node.Key) 77 | case *ast.HeaderParamNode: 78 | panic(fmt.Errorf("unexpected {@param ...} tag found")) 79 | } 80 | if parent, ok := node.(ast.ParentNode); ok { 81 | tc.recurse(parent) 82 | } 83 | } 84 | 85 | // checkLet ensures that the let variable has an allowed name. 86 | func (tc *templateChecker) checkLet(varName string) { 87 | if varName == "ij" { 88 | panic("Invalid variable name in 'let' command text: '$ij'") 89 | } 90 | } 91 | 92 | func (tc *templateChecker) checkCall(node *ast.CallNode) { 93 | var callee, ok = tc.registry.Template(node.Name) 94 | if !ok { 95 | panic(fmt.Errorf("{call}: template %q not found", node.Name)) 96 | } 97 | 98 | // collect callee's list of required/allowed params 99 | var allCalleeParamNames, requiredCalleeParamNames []string 100 | for _, param := range callee.Doc.Params { 101 | allCalleeParamNames = append(allCalleeParamNames, param.Name) 102 | if !param.Optional { 103 | requiredCalleeParamNames = append(requiredCalleeParamNames, param.Name) 104 | } 105 | } 106 | 107 | // collect caller's list of params. 108 | // if {call} passes data="all", expand that into all of the key names that 109 | // the caller has in common with params of the callee. 110 | var callerParamNames []string 111 | if node.AllData { 112 | for _, param := range tc.params { 113 | if contains(allCalleeParamNames, param) { 114 | tc.usedKeys = append(tc.usedKeys, param) 115 | callerParamNames = append(callerParamNames, param) 116 | } 117 | } 118 | } 119 | // add the {param}'s 120 | for _, callParam := range node.Params { 121 | switch callParam := callParam.(type) { 122 | case *ast.CallParamValueNode: 123 | callerParamNames = append(callerParamNames, callParam.Key) 124 | case *ast.CallParamContentNode: 125 | callerParamNames = append(callerParamNames, callParam.Key) 126 | default: 127 | panic("unexpected call param type") 128 | } 129 | } 130 | 131 | // reconcile the two param lists. 132 | // check: all {call} params are declared as @params in the called template soydoc. 133 | var undeclaredCallParamNames []string 134 | for _, callParamName := range callerParamNames { 135 | if !contains(allCalleeParamNames, callParamName) { 136 | undeclaredCallParamNames = append(undeclaredCallParamNames, callParamName) 137 | } 138 | } 139 | if len(undeclaredCallParamNames) > 0 { 140 | panic(fmt.Errorf("Params %q are not declared by the callee.", undeclaredCallParamNames)) 141 | } 142 | 143 | // check: a {call}'ed template is passed all required @params, or a data="$var" 144 | if node.Data != nil { 145 | return 146 | } 147 | var missingRequiredParamNames []string 148 | for _, requiredCalleeParam := range requiredCalleeParamNames { 149 | if !contains(callerParamNames, requiredCalleeParam) { 150 | missingRequiredParamNames = append(missingRequiredParamNames, requiredCalleeParam) 151 | } 152 | } 153 | if len(missingRequiredParamNames) > 0 { 154 | panic(fmt.Errorf("Required params %q are not passed by the call: %v", 155 | missingRequiredParamNames, node)) 156 | } 157 | } 158 | 159 | func (tc *templateChecker) recurse(parent ast.ParentNode) { 160 | var initialForVars = len(tc.forVars) 161 | var initialLetVars = len(tc.letVars) 162 | var initialUsedKeys = len(tc.usedKeys) 163 | for _, child := range parent.Children() { 164 | tc.checkTemplate(child) 165 | } 166 | tc.forVars = tc.forVars[:initialForVars] 167 | 168 | // quick return if there were no {let}s 169 | if initialLetVars == len(tc.letVars) { 170 | return 171 | } 172 | 173 | // "pop" the {let} variables, as well as their usages. 174 | // (this is necessary to handle shadowing of @params by {let} vars) 175 | var letVarsGoingOutOfScope = tc.letVars[initialLetVars:] 176 | var usedKeysToKeep, usedLets []string 177 | for _, key := range tc.usedKeys[initialUsedKeys:] { 178 | if contains(letVarsGoingOutOfScope, key) { 179 | usedLets = append(usedLets, key) 180 | } else { 181 | usedKeysToKeep = append(usedKeysToKeep, key) 182 | } 183 | } 184 | 185 | // check that any let variables leaving scope have been used 186 | var unusedLetVarNames []string 187 | for _, letVar := range letVarsGoingOutOfScope { 188 | if !contains(usedLets, letVar) { 189 | unusedLetVarNames = append(unusedLetVarNames, letVar) 190 | } 191 | } 192 | if len(unusedLetVarNames) > 0 { 193 | panic(fmt.Errorf("{let} variables %q are not used.", unusedLetVarNames)) 194 | } 195 | 196 | tc.usedKeys = append(tc.usedKeys[:initialUsedKeys], usedKeysToKeep...) 197 | tc.letVars = tc.letVars[:initialLetVars] 198 | } 199 | 200 | func (tc *templateChecker) visitKey(key string) { 201 | // record that this key was used in the template. 202 | tc.usedKeys = append(tc.usedKeys, key) 203 | 204 | // check that the key was provided by a @param or {let} 205 | if !tc.checkKey(key) { 206 | panic(fmt.Errorf("data ref %q not found. params: %v, let variables: %v", 207 | key, tc.params, tc.letVars)) 208 | } 209 | } 210 | 211 | // checkKey returns true if the given key exists as a param or {let} variable. 212 | func (tc *templateChecker) checkKey(key string) bool { 213 | if key == "ij" { 214 | return true 215 | } 216 | for _, param := range tc.params { 217 | if param == key { 218 | return true 219 | } 220 | } 221 | for _, varName := range tc.letVars { 222 | if varName == key { 223 | return true 224 | } 225 | } 226 | for _, varName := range tc.forVars { 227 | if varName == key { 228 | return true 229 | } 230 | } 231 | return false 232 | } 233 | 234 | func contains(slice []string, item string) bool { 235 | for _, candidate := range slice { 236 | if candidate == item { 237 | return true 238 | } 239 | } 240 | return false 241 | } 242 | -------------------------------------------------------------------------------- /parsepasses/datarefcheck_test.go: -------------------------------------------------------------------------------- 1 | package parsepasses 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/robfig/soy/ast" 7 | "github.com/robfig/soy/parse" 8 | "github.com/robfig/soy/template" 9 | ) 10 | 11 | type checkerTest struct { 12 | body []string 13 | success bool 14 | } 15 | 16 | type simpleCheckerTest struct { 17 | body string 18 | success bool 19 | } 20 | 21 | // Test: all data references are provided by @param declarations or {let}, 22 | // {for}, {foreach} nodes 23 | func TestAllDataRefsProvided(t *testing.T) { 24 | runSimpleCheckerTests(t, []simpleCheckerTest{ 25 | {` 26 | /** no data refs */ 27 | {template .noDataRefs} 28 | Hello world 29 | {/template}`, true}, 30 | 31 | {` 32 | /** let only */ 33 | {template .letOnly} 34 | {let $ref: 0/}Hello {$ref} 35 | {/template}`, true}, 36 | 37 | {` 38 | /** @param paramName */ 39 | {template .paramOnly} 40 | Hello {$paramName} 41 | {/template}`, true}, 42 | 43 | {` 44 | {template .paramOnly} 45 | {@param paramName: ?} 46 | Hello {$paramName} 47 | {/template}`, true}, 48 | 49 | {` 50 | /** 51 | * @param param1 52 | * @param? param2 53 | */ 54 | {template .everything} 55 | {let $let1: 'hello'/} 56 | {if true}{let $let2}let body{/let} 57 | Hello {$param1} {$param2} {$let1} {$let2} 58 | {else} 59 | Goodbye {$param1} {$param2} {$let1} 60 | {/if} 61 | {/template}`, true}, 62 | 63 | {` 64 | {template .everything} 65 | {@param param1: ?} 66 | {@param? param2: ?} 67 | {let $let1: 'hello'/} 68 | {if true}{let $let2}let body{/let} 69 | Hello {$param1} {$param2} {$let1} {$let2} 70 | {else} 71 | Goodbye {$param1} {$param2} {$let1} 72 | {/if} 73 | {/template}`, true}, 74 | 75 | {` 76 | /** for loop */ 77 | {template .for} 78 | {for $x in range(5)} 79 | Hello {$x} 80 | {/for} 81 | {/template}`, true}, 82 | 83 | {` 84 | /** @param vars */ 85 | {template .foreach} 86 | {foreach $x in $vars} 87 | Hello world 88 | {/for} 89 | {/template}`, true}, 90 | 91 | {` 92 | /** missing param */ 93 | {template .missingParam} 94 | Hello {$param} 95 | {/template}`, false}, 96 | 97 | {` 98 | /** out of scope */ 99 | {template .letOutOfScope} 100 | {if true} 101 | {let $param: true/} 102 | Hello {$param} 103 | {/if} 104 | Hello {$param} 105 | {/template}`, false}, 106 | 107 | {` 108 | /** @param foo (within expression) */ 109 | {template .for} 110 | {foreach $x in $foo[$bar]} 111 | Hello {$x} 112 | {/for} 113 | {/template}`, false}, 114 | }) 115 | 116 | } 117 | 118 | // Test: any data declared as a @param is used by the template (or passed via {call}) 119 | func TestAllParamsAreUsed(t *testing.T) { 120 | runSimpleCheckerTests(t, []simpleCheckerTest{ 121 | 122 | // Check successful 123 | {` 124 | /** @param used */ 125 | {template .ParamUsedInExpr} 126 | Hello {not true ? 'a' : 'b' + $used}. 127 | {/template}`, true}, 128 | 129 | {` 130 | {template .ParamUsedInExpr} 131 | {@param used: ?} 132 | Hello {not true ? 'a' : 'b' + $used}. 133 | {/template}`, true}, 134 | 135 | {` 136 | /** @param param */ 137 | {template .UsedInCallData} 138 | Hello {call .Other data="$param"/}. 139 | {/template} 140 | /** No params */ 141 | {template .Other} 142 | {/template}`, true}, 143 | 144 | {` 145 | /** @param param */ 146 | {template .PassedByCall_AllData} 147 | Hello {call .Other data="all"/}. 148 | {/template} 149 | /** @param param */ 150 | {template .Other} 151 | Hello {$param} 152 | {/template}`, true}, 153 | 154 | {` 155 | /** 156 | * @param foo 157 | * @param bar 158 | */ 159 | {template .for} 160 | {foreach $x in $foo[$bar]} 161 | Hello {$x} 162 | {/for} 163 | {/template}`, true}, 164 | 165 | // Check fails 166 | {` 167 | /** @param param not used */ 168 | {template .ParamNotUsed} 169 | Hello {call .Other/} 170 | {/template} 171 | /** @param? param not used */ 172 | {template .Other} 173 | {$param} 174 | {/template}`, false}, 175 | 176 | {` 177 | /** 178 | * @param used 179 | * @param notused 180 | */ 181 | {template .CallPassesAllDataButNotDeclaredByCallee} 182 | Hello {call .Other data="all"/}. 183 | {/template} 184 | /** 185 | * @param used 186 | * @param? other 187 | */ 188 | {template .Other} 189 | {$used} 190 | {/template}`, false}, 191 | 192 | {` 193 | /** 194 | * @param used 195 | * @param notused 196 | */ 197 | {template .CallPassesAllDataButNotDeclaredByCallee} 198 | Hello {call .Other data="all"/}. 199 | {/template} 200 | 201 | {template .Other} 202 | {@param used: ?} 203 | {@param? other: ?} 204 | {$used} 205 | {/template}`, false}, 206 | 207 | {` 208 | /** @param var */ 209 | {template .ParamShadowedByLet} 210 | {let $var: 0/} 211 | Hello {$var} 212 | {/template}`, false}, 213 | }) 214 | } 215 | 216 | // Test: all {call} params are declared as @params in the called template soydoc. 217 | func TestAllCallParamsAreDeclaredByCallee(t *testing.T) { 218 | runSimpleCheckerTests(t, []simpleCheckerTest{ 219 | {` 220 | /** */ 221 | {template .ParamsPresent} 222 | {call .Other} 223 | {param param1: 0/} 224 | {param param2}hello{/param} 225 | {/call} 226 | {/template} 227 | /** 228 | * @param param1 229 | * @param? param2 230 | */ 231 | {template .Other} 232 | {$param1} {$param2} 233 | {/template} 234 | `, true}, 235 | 236 | {` 237 | {template .ParamsPresent} 238 | {call .Other} 239 | {param param1: 0/} 240 | {param param2}hello{/param} 241 | {/call} 242 | {/template} 243 | 244 | {template .Other} 245 | {@param param1: ?} 246 | {@param? param2: ?} 247 | {$param1} {$param2} 248 | {/template} 249 | `, true}, 250 | 251 | {` 252 | /** */ 253 | {template .ParamsNotPresent} 254 | {call .Other} 255 | {param param1}hello{/param} 256 | {/call} 257 | {/template} 258 | /** */ 259 | {template .Other} 260 | {/template} 261 | `, false}, 262 | }) 263 | } 264 | 265 | // Test: a {call}'ed template is passed all required @params, or a data="$var" 266 | func TestCalledTemplatesReceiveAllRequiredParams(t *testing.T) { 267 | runSimpleCheckerTests(t, []simpleCheckerTest{ 268 | {` 269 | /** */ 270 | {template .NotPassingRequiredParam} 271 | {call .Other/} 272 | {/template} 273 | /** @param required */ 274 | {template .Other} 275 | {$required} 276 | {/template} 277 | `, false}, 278 | 279 | {` 280 | /** @param required */ 281 | {template .PassingRequiredParam_AllData} 282 | {call .Other data="all"/} 283 | {/template} 284 | /** @param required */ 285 | {template .Other} 286 | {$required} 287 | {/template} 288 | `, true}, 289 | {` 290 | /** */ 291 | {template .NotPassingRequiredParam_AllData} 292 | {call .Other data="all"/} 293 | {/template} 294 | /** @param required */ 295 | {template .Other} 296 | {$required} 297 | {/template} 298 | `, false}, 299 | 300 | {` 301 | /** @param something */ 302 | {template .PassingRequiredParam_OneData} 303 | {call .Other data="$something"/} 304 | {/template} 305 | /** @param required */ 306 | {template .Other} 307 | {$required} 308 | {/template} 309 | `, true}, 310 | 311 | {` 312 | /** @param something */ 313 | {template .PassingRequiredParam_AsParam} 314 | {call .Other} 315 | {param required: $something/} 316 | {/call} 317 | {/template} 318 | /** @param required */ 319 | {template .Other} 320 | {$required} 321 | {/template} 322 | `, true}, 323 | }) 324 | } 325 | 326 | // Test: {call}'d templates actually exist in the registry. 327 | func TestCalledTemplatesRequiredToExist(t *testing.T) { 328 | runSimpleCheckerTests(t, []simpleCheckerTest{ 329 | {` 330 | /** @param var */ 331 | {template .CalledTemplateDoesNotExist} 332 | {call .NotExist data="$var"/} 333 | {/template} 334 | `, false}, 335 | }) 336 | } 337 | 338 | // Test: any variable created by {let}, {for}, {foreach} is used somewhere 339 | func TestLetVariablesAreUsed(t *testing.T) { 340 | runSimpleCheckerTests(t, []simpleCheckerTest{ 341 | {` 342 | /** */ 343 | {template .UnusedLetVariable} 344 | {let $var}hello{/let} 345 | Hello world. 346 | {/template} 347 | `, false}, 348 | 349 | {` 350 | /** @param var */ 351 | {template .UnusedShadowingLetVariable} 352 | {if true} 353 | {let $var}hello{/let} 354 | {/if} 355 | Hello {$var} 356 | {/template} 357 | `, false}, 358 | }) 359 | } 360 | 361 | // Test that {call} checks work on calls across namespaces too. 362 | func TestCrossNamespace(t *testing.T) { 363 | runCheckerTests(t, []checkerTest{ 364 | {[]string{` 365 | {namespace ns.a} 366 | /** */ 367 | {template .ParamsPresent} 368 | {call ns.b.Other} 369 | {param param1: 0/} 370 | {param param2}hello{/param} 371 | {/call} 372 | {/template} 373 | `, ` 374 | {namespace ns.b} 375 | /** 376 | * @param param1 377 | * @param? param2 378 | */ 379 | {template .Other} 380 | {$param1} {$param2} 381 | {/template} 382 | `}, true}, 383 | 384 | {[]string{` 385 | {namespace ns.a} 386 | /** */ 387 | {template .ParamsNotPresent} 388 | {call ns.b.Other} 389 | {param param1}hello{/param} 390 | {/call} 391 | {/template} 392 | `, ` 393 | {namespace ns.b} 394 | /** */ 395 | {template .Other} 396 | {/template} 397 | `}, false}, 398 | }) 399 | } 400 | 401 | // Test: {let} variables are not named $ij 402 | func TestLetVariablesNotNamedIJ(t *testing.T) { 403 | runSimpleCheckerTests(t, []simpleCheckerTest{ 404 | {` 405 | /** */ 406 | {template .noCollideIJ} 407 | {let $ij}hello{/let} 408 | Hello {$ij} 409 | {/template} 410 | `, false}, 411 | }) 412 | } 413 | 414 | // Test: $ij named variables are allowed without declaration 415 | func TestIJVarsAllowed(t *testing.T) { 416 | runSimpleCheckerTests(t, []simpleCheckerTest{ 417 | {` 418 | /** */ 419 | {template .IJAllowed} 420 | {$ij.foo} 421 | {/template} 422 | `, true}, 423 | }) 424 | } 425 | 426 | func TestTwoTypesOfParamsDisallowed(t *testing.T) { 427 | runSimpleCheckerTests(t, []simpleCheckerTest{ 428 | {` 429 | /** @param var */ 430 | {template .CalledTemplateDoesNotExist} 431 | {@param var: ?} 432 | Hello {$var} 433 | {/template} 434 | `, false}, 435 | }) 436 | } 437 | 438 | func runSimpleCheckerTests(t *testing.T, tests []simpleCheckerTest) { 439 | var result []checkerTest 440 | for _, simpleTest := range tests { 441 | result = append(result, checkerTest{ 442 | []string{"{namespace test}\n" + simpleTest.body}, 443 | simpleTest.success, 444 | }) 445 | } 446 | runCheckerTests(t, result) 447 | } 448 | 449 | func runCheckerTests(t *testing.T, tests []checkerTest) { 450 | for _, test := range tests { 451 | var ( 452 | reg template.Registry 453 | tree *ast.SoyFileNode 454 | err error 455 | ) 456 | for _, body := range test.body { 457 | tree, err = parse.SoyFile("", body) 458 | if err != nil { 459 | break 460 | } 461 | 462 | if err = reg.Add(tree); err != nil { 463 | break 464 | } 465 | } 466 | if err != nil { 467 | if test.success { 468 | t.Error(err) 469 | } 470 | continue 471 | } 472 | 473 | err = CheckDataRefs(reg) 474 | if test.success && err != nil { 475 | t.Error(err) 476 | } else if !test.success && err == nil { 477 | var name = "(empty)" 478 | if len(reg.Templates) > 0 { 479 | name = reg.Templates[0].Node.Name 480 | } 481 | t.Errorf("%s: expected to fail validation, but no error was raised.", name) 482 | } 483 | } 484 | } 485 | -------------------------------------------------------------------------------- /parsepasses/globals.go: -------------------------------------------------------------------------------- 1 | package parsepasses 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/robfig/soy/ast" 7 | "github.com/robfig/soy/data" 8 | "github.com/robfig/soy/template" 9 | ) 10 | 11 | // SetGlobals sets the value of all global nodes in the given registry. 12 | // An error is returned if any globals were left undefined. 13 | func SetGlobals(reg template.Registry, globals data.Map) error { 14 | for _, t := range reg.Templates { 15 | if err := SetNodeGlobals(t.Node, globals); err != nil { 16 | return fmt.Errorf("template %v: %v", t.Node.Name, err) 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | // SetNodeGlobals sets global values on the given node and all children nodes, 23 | // using the given data map. An error is returned if any global nodes were left 24 | // undefined. 25 | func SetNodeGlobals(node ast.Node, globals data.Map) error { 26 | switch node := node.(type) { 27 | case *ast.GlobalNode: 28 | if val, ok := globals[node.Name]; ok { 29 | node.Value = val 30 | } else { 31 | return fmt.Errorf("global %q is undefined", node.Name) 32 | } 33 | default: 34 | if parent, ok := node.(ast.ParentNode); ok { 35 | for _, child := range parent.Children() { 36 | if err := SetNodeGlobals(child, globals); err != nil { 37 | return err 38 | } 39 | } 40 | } 41 | } 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /parsepasses/msgids.go: -------------------------------------------------------------------------------- 1 | package parsepasses 2 | 3 | import ( 4 | "github.com/robfig/soy/ast" 5 | "github.com/robfig/soy/soymsg" 6 | "github.com/robfig/soy/template" 7 | ) 8 | 9 | // ProcessMessages calculates the message ids and placeholder names for {msg} 10 | // nodes and sets that information on the node. 11 | func ProcessMessages(reg template.Registry) { 12 | for _, t := range reg.Templates { 13 | processTemplateMsgs(t.Node) 14 | } 15 | } 16 | 17 | func processTemplateMsgs(node ast.Node) { 18 | switch node := node.(type) { 19 | case *ast.MsgNode: 20 | soymsg.SetPlaceholdersAndID(node) 21 | default: 22 | if parent, ok := node.(ast.ParentNode); ok { 23 | for _, child := range parent.Children() { 24 | processTemplateMsgs(child) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /soyhtml/directives.go: -------------------------------------------------------------------------------- 1 | package soyhtml 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/url" 8 | "regexp" 9 | "text/template" 10 | "unicode/utf8" 11 | 12 | "github.com/robfig/soy/data" 13 | ) 14 | 15 | // PrintDirective represents a transformation applied when printing a value. 16 | type PrintDirective struct { 17 | Apply func(value data.Value, args []data.Value) data.Value 18 | ValidArgLengths []int 19 | CancelAutoescape bool 20 | } 21 | 22 | // PrintDirectives are the builtin print directives. 23 | // Callers may add their own print directives to this map. 24 | var PrintDirectives = map[string]PrintDirective{ 25 | "insertWordBreaks": {directiveInsertWordBreaks, []int{1}, true}, 26 | "changeNewlineToBr": {directiveChangeNewlineToBr, []int{0}, true}, 27 | "truncate": {directiveTruncate, []int{1, 2}, false}, 28 | "id": {directiveNoAutoescape, []int{0}, true}, 29 | "noAutoescape": {directiveNoAutoescape, []int{0}, true}, 30 | "escapeHtml": {directiveEscapeHtml, []int{0}, true}, 31 | "escapeUri": {directiveEscapeUri, []int{0}, true}, 32 | "escapeJsString": {directiveEscapeJsString, []int{0}, true}, 33 | "bidiSpanWrap": {nil, []int{0}, false}, // unimplemented 34 | "bidiUnicodeWrap": {nil, []int{0}, false}, // unimplemented 35 | "json": {directiveJson, []int{0}, true}, 36 | } 37 | 38 | // ObligatoryPrintDirectives are always called 39 | // These directives can't take arguments 40 | // Callers may add their own print directives to this list. 41 | var ObligatoryPrintDirectiveNames = []string{} 42 | 43 | func directiveInsertWordBreaks(value data.Value, args []data.Value) data.Value { 44 | var ( 45 | input = template.HTMLEscapeString(value.String()) 46 | maxChars = int(args[0].(data.Int)) 47 | chars = 0 48 | output *bytes.Buffer // create the buffer lazily 49 | ) 50 | for i, ch := range input { 51 | switch { 52 | case ch == ' ': 53 | chars = 0 54 | case chars >= maxChars: 55 | if output == nil { 56 | output = bytes.NewBufferString(input[:i]) 57 | } 58 | output.WriteString("") 59 | chars = 1 60 | default: 61 | chars++ 62 | } 63 | if output != nil { 64 | output.WriteRune(ch) 65 | } 66 | } 67 | if output == nil { 68 | return value 69 | } 70 | return data.String(output.String()) 71 | } 72 | 73 | var newlinePattern = regexp.MustCompile(`\r\n|\r|\n`) 74 | 75 | func directiveChangeNewlineToBr(value data.Value, _ []data.Value) data.Value { 76 | return data.String(newlinePattern.ReplaceAllString( 77 | template.HTMLEscapeString(value.String()), 78 | "
")) 79 | } 80 | 81 | func directiveTruncate(value data.Value, args []data.Value) data.Value { 82 | if !isInt(args[0]) { 83 | panic(fmt.Errorf("First parameter of '|truncate' is not an integer: %v", args[0])) 84 | } 85 | var maxLen = int(args[0].(data.Int)) 86 | var str = value.String() 87 | if len(str) <= maxLen { 88 | return value 89 | } 90 | 91 | var ellipsis = data.Bool(true) 92 | if len(args) == 2 { 93 | var ok bool 94 | ellipsis, ok = args[1].(data.Bool) 95 | if !ok { 96 | panic(fmt.Errorf("Second parameter of '|truncate' is not a bool: %v", args[1])) 97 | } 98 | } 99 | 100 | if ellipsis { 101 | if maxLen > 3 { 102 | maxLen -= 3 103 | } else { 104 | ellipsis = false 105 | } 106 | } 107 | 108 | for !utf8.RuneStart(str[maxLen]) { 109 | maxLen-- 110 | } 111 | 112 | str = str[:maxLen] 113 | if ellipsis { 114 | str += "..." 115 | } 116 | return data.String(str) 117 | } 118 | 119 | func directiveNoAutoescape(value data.Value, _ []data.Value) data.Value { 120 | return value 121 | } 122 | 123 | func directiveEscapeHtml(value data.Value, _ []data.Value) data.Value { 124 | return data.String(template.HTMLEscapeString(value.String())) 125 | } 126 | 127 | func directiveEscapeUri(value data.Value, _ []data.Value) data.Value { 128 | return data.String(url.QueryEscape(value.String())) 129 | } 130 | 131 | func directiveEscapeJsString(value data.Value, _ []data.Value) data.Value { 132 | return data.String(template.JSEscapeString(value.String())) 133 | } 134 | 135 | func directiveJson(value data.Value, _ []data.Value) data.Value { 136 | j, err := json.Marshal(value) 137 | if err != nil { 138 | panic(fmt.Errorf("Error JSON encoding value: %v", err)) 139 | } 140 | return data.String(j) 141 | } 142 | -------------------------------------------------------------------------------- /soyhtml/eval.go: -------------------------------------------------------------------------------- 1 | package soyhtml 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "github.com/robfig/soy/ast" 7 | "github.com/robfig/soy/data" 8 | ) 9 | 10 | // EvalExpr evaluates the given expression node and returns the result. The 11 | // given node must be a simple Soy expression, such as what may appear inside a 12 | // print tag. 13 | // 14 | // This is useful for evaluating Globals, or anything returned from parse.Expr. 15 | func EvalExpr(node ast.Node) (val data.Value, err error) { 16 | state := &state{wr: ioutil.Discard} 17 | defer state.errRecover(&err) 18 | state.walk(node) 19 | return state.val, nil 20 | } 21 | -------------------------------------------------------------------------------- /soyhtml/eval_test.go: -------------------------------------------------------------------------------- 1 | package soyhtml 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/robfig/soy/data" 7 | "github.com/robfig/soy/parse" 8 | ) 9 | 10 | func TestEvalExpr(t *testing.T) { 11 | var tests = []struct { 12 | input string 13 | expected interface{} 14 | }{ 15 | {"0", 0}, 16 | {"1+1", 2}, 17 | {"'abc'", "abc"}, 18 | } 19 | 20 | for _, test := range tests { 21 | var tree, err = parse.SoyFile("", "{"+test.input+"}") 22 | if err != nil { 23 | t.Error(err) 24 | return 25 | } 26 | 27 | actual, err := EvalExpr(tree) 28 | if err != nil { 29 | t.Error(err) 30 | continue 31 | } 32 | if actual != data.New(test.expected) { 33 | t.Errorf("EvalExpr(%v) => %v, expected %v", test.input, actual, test.expected) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /soyhtml/funcs.go: -------------------------------------------------------------------------------- 1 | package soyhtml 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | "strings" 7 | 8 | "github.com/robfig/soy/data" 9 | ) 10 | 11 | type loopFunc func(s *state, key string) data.Value 12 | 13 | var loopFuncs = map[string]loopFunc{ 14 | "index": funcIndex, 15 | "isFirst": funcIsFirst, 16 | "isLast": funcIsLast, 17 | } 18 | 19 | func funcIndex(s *state, key string) data.Value { 20 | return s.context.lookup(key + "__index") 21 | } 22 | 23 | func funcIsFirst(s *state, key string) data.Value { 24 | return data.Bool(s.context.lookup(key+"__index").(data.Int) == 0) 25 | } 26 | 27 | func funcIsLast(s *state, key string) data.Value { 28 | return data.Bool( 29 | s.context.lookup(key+"__index").(data.Int) == s.context.lookup(key+"__lastIndex").(data.Int)) 30 | } 31 | 32 | // Func represents a Soy function that may be invoked within a Soy template. 33 | type Func struct { 34 | Apply func([]data.Value) data.Value 35 | ValidArgLengths []int 36 | } 37 | 38 | // Funcs contains the builtin Soy functions. 39 | // Callers may add their own functions to this map as well. 40 | var Funcs = map[string]Func{ 41 | "isNonnull": {funcIsNonnull, []int{1}}, 42 | "length": {funcLength, []int{1}}, 43 | "keys": {funcKeys, []int{1}}, 44 | "augmentMap": {funcAugmentMap, []int{2}}, 45 | "round": {funcRound, []int{1, 2}}, 46 | "floor": {funcFloor, []int{1}}, 47 | "ceiling": {funcCeiling, []int{1}}, 48 | "min": {funcMin, []int{2}}, 49 | "max": {funcMax, []int{2}}, 50 | "randomInt": {funcRandomInt, []int{1}}, 51 | "strContains": {funcStrContains, []int{2}}, 52 | "range": {funcRange, []int{1, 2, 3}}, 53 | "hasData": {funcHasData, []int{0}}, 54 | } 55 | 56 | func funcIsNonnull(v []data.Value) data.Value { 57 | return data.Bool(!(v[0] == data.Null{} || v[0] == data.Undefined{})) 58 | } 59 | 60 | func funcLength(v []data.Value) data.Value { 61 | return data.Int(len(v[0].(data.List))) 62 | } 63 | 64 | func funcKeys(v []data.Value) data.Value { 65 | var keys data.List 66 | for k := range v[0].(data.Map) { 67 | keys = append(keys, data.String(k)) 68 | } 69 | return keys 70 | } 71 | 72 | func funcAugmentMap(v []data.Value) data.Value { 73 | var m1 = v[0].(data.Map) 74 | var m2 = v[1].(data.Map) 75 | var result = make(data.Map, len(m1)+len(m2)+4) 76 | for k, v := range m1 { 77 | result[k] = v 78 | } 79 | for k, v := range m2 { 80 | result[k] = v 81 | } 82 | return result 83 | } 84 | 85 | func funcRound(v []data.Value) data.Value { 86 | var digitsAfterPt = 0 87 | if len(v) == 2 { 88 | digitsAfterPt = int(v[1].(data.Int)) 89 | } 90 | var result = round(toFloat(v[0]), digitsAfterPt) 91 | if digitsAfterPt <= 0 { 92 | return data.Int(result) 93 | } 94 | return data.Float(result) 95 | } 96 | 97 | func round(x float64, prec int) float64 { 98 | pow := math.Pow(10, float64(prec)) 99 | intermed := x * pow 100 | if intermed < 0.0 { 101 | intermed -= 0.5 102 | } else { 103 | intermed += 0.5 104 | } 105 | return float64(int64(intermed)) / float64(pow) 106 | } 107 | 108 | func funcFloor(v []data.Value) data.Value { 109 | if isInt(v[0]) { 110 | return v[0] 111 | } 112 | return data.Int(math.Floor(toFloat(v[0]))) 113 | } 114 | 115 | func funcCeiling(v []data.Value) data.Value { 116 | if isInt(v[0]) { 117 | return v[0] 118 | } 119 | return data.Int(math.Ceil(toFloat(v[0]))) 120 | } 121 | 122 | func funcMin(v []data.Value) data.Value { 123 | if isInt(v[0]) && isInt(v[1]) { 124 | if v[0].(data.Int) < v[1].(data.Int) { 125 | return v[0] 126 | } 127 | return v[1] 128 | } 129 | return data.Float(math.Min(toFloat(v[0]), toFloat(v[1]))) 130 | } 131 | 132 | func funcMax(v []data.Value) data.Value { 133 | if isInt(v[0]) && isInt(v[1]) { 134 | if v[0].(data.Int) > v[1].(data.Int) { 135 | return v[0] 136 | } 137 | return v[1] 138 | } 139 | return data.Float(math.Max(toFloat(v[0]), toFloat(v[1]))) 140 | } 141 | 142 | func funcRandomInt(v []data.Value) data.Value { 143 | return data.Int(rand.Int63n(int64(v[0].(data.Int)))) 144 | } 145 | 146 | func funcStrContains(v []data.Value) data.Value { 147 | return data.Bool(strings.Contains(string(v[0].(data.String)), string(v[1].(data.String)))) 148 | } 149 | 150 | func funcRange(v []data.Value) data.Value { 151 | var ( 152 | increment = 1 153 | init = 0 154 | limit int 155 | ) 156 | switch len(v) { 157 | case 3: 158 | increment = int(v[2].(data.Int)) 159 | fallthrough 160 | case 2: 161 | init = int(v[0].(data.Int)) 162 | limit = int(v[1].(data.Int)) 163 | case 1: 164 | limit = int(v[0].(data.Int)) 165 | } 166 | 167 | var indices data.List 168 | var i = 0 169 | for index := init; index < limit; index += increment { 170 | indices = append(indices, data.Int(index)) 171 | i++ 172 | } 173 | return indices 174 | } 175 | 176 | func funcHasData(v []data.Value) data.Value { 177 | return data.Bool(true) 178 | } 179 | -------------------------------------------------------------------------------- /soyhtml/funcs_test.go: -------------------------------------------------------------------------------- 1 | package soyhtml 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/robfig/soy/data" 7 | ) 8 | 9 | var rangeTests = []struct{ args, result []int }{ 10 | {[]int{0}, []int{}}, 11 | {[]int{1}, []int{0}}, 12 | {[]int{2}, []int{0, 1}}, 13 | {[]int{0, 1}, []int{0}}, 14 | {[]int{0, 2}, []int{0, 1}}, 15 | {[]int{1, 2}, []int{1}}, 16 | {[]int{1, 3, 1}, []int{1, 2}}, 17 | {[]int{1, 3, 2}, []int{1}}, 18 | {[]int{1, 4, 2}, []int{1, 3}}, 19 | } 20 | 21 | func TestRange(t *testing.T) { 22 | for _, test := range rangeTests { 23 | var args []data.Value 24 | for _, a := range test.args { 25 | args = append(args, data.New(a)) 26 | } 27 | result := funcRange(args).(data.List) 28 | if len(result) != len(test.result) { 29 | t.Errorf("%v => %v, expected %v", test.args, result, test.result) 30 | continue 31 | } 32 | for i, r := range test.result { 33 | if int64(result[i].(data.Int)) != int64(r) { 34 | t.Errorf("%v => %v, expected %v", test.args, result, test.result) 35 | break 36 | } 37 | } 38 | } 39 | } 40 | 41 | var strContainsTests = []struct { 42 | arg1, arg2 string 43 | result bool 44 | }{ 45 | {"", "", true}, 46 | {"abc", "", true}, 47 | {"abc", "a", true}, 48 | {"abc", "b", true}, 49 | {"abc", "c", true}, 50 | {"abc", "d", false}, 51 | {"abc", "A", false}, 52 | {"abc", "abc", true}, 53 | {"abc", "abcd", false}, 54 | } 55 | 56 | func TestStrContains(t *testing.T) { 57 | for _, test := range strContainsTests { 58 | actual := bool(funcStrContains([]data.Value{data.New(test.arg1), data.New(test.arg2)}).(data.Bool)) 59 | if actual != test.result { 60 | t.Errorf("strcontains %s %s => %v, expected %v", test.arg1, test.arg2, actual, test.result) 61 | } 62 | } 63 | } 64 | 65 | func TestRound(t *testing.T) { 66 | type i []interface{} 67 | var tests = []struct { 68 | input []interface{} 69 | expected interface{} 70 | }{ 71 | {i{0}, 0}, 72 | {i{-5}, -5}, 73 | {i{5}, 5}, 74 | {i{1.01}, 1}, 75 | {i{1.99}, 2}, 76 | {i{1.0}, 1}, 77 | {i{-1.01}, -1}, 78 | {i{-1.99}, -2}, 79 | {i{-1.5}, -2}, 80 | 81 | {i{1.2345, 1}, 1.2}, 82 | {i{1.2345, 2}, 1.23}, 83 | {i{1.2345, 3}, 1.235}, 84 | {i{1.2345, 4}, 1.2345}, 85 | {i{-1.2345, 1}, -1.2}, 86 | {i{-1.2345, 2}, -1.23}, 87 | {i{-1.2345, 3}, -1.235}, 88 | {i{-1.2345, 4}, -1.2345}, 89 | {i{1.0, 5}, 1.0}, 90 | 91 | {i{123.456, -1}, 120}, 92 | {i{123.456, -2}, 100}, 93 | {i{123.456, -3}, 000}, 94 | } 95 | 96 | for _, test := range tests { 97 | var inputValues []data.Value 98 | for _, num := range test.input { 99 | inputValues = append(inputValues, data.New(num)) 100 | } 101 | actual := funcRound(inputValues) 102 | if len(inputValues) == 1 { 103 | // Passing one arg should have the same result as passing the second as 0 104 | if actual != funcRound(append(inputValues, data.Int(0))) { 105 | t.Errorf("round %v returned %v, but changed when passed explicit 0", test.input, actual) 106 | } 107 | } 108 | if actual != data.New(test.expected) { 109 | t.Errorf("round %v => %v, expected %v", test.input, actual, test.expected) 110 | } 111 | } 112 | } 113 | 114 | func TestFloor(t *testing.T) { 115 | var tests = []struct { 116 | input interface{} 117 | expected interface{} 118 | }{ 119 | {0, 0}, 120 | {1, 1}, 121 | {1.1, 1}, 122 | {1.5, 1}, 123 | {1.99, 1}, 124 | {-1, -1}, 125 | {-1.1, -2}, 126 | {-1.9, -2}, 127 | } 128 | 129 | for _, test := range tests { 130 | var actual = funcFloor([]data.Value{data.New(test.input)}) 131 | if actual != data.New(test.expected) { 132 | t.Errorf("floor(%v) => %v, expected %v", test.input, actual, test.expected) 133 | } 134 | } 135 | } 136 | 137 | func TestCeil(t *testing.T) { 138 | var tests = []struct { 139 | input interface{} 140 | expected interface{} 141 | }{ 142 | {0, 0}, 143 | {1, 1}, 144 | {1.1, 2}, 145 | {1.5, 2}, 146 | {1.99, 2}, 147 | {-1, -1}, 148 | {-1.1, -1}, 149 | {-1.9, -1}, 150 | } 151 | 152 | for _, test := range tests { 153 | var actual = funcCeiling([]data.Value{data.New(test.input)}) 154 | if actual != data.New(test.expected) { 155 | t.Errorf("ceiling(%v) => %v, expected %v", test.input, actual, test.expected) 156 | } 157 | } 158 | } 159 | 160 | func TestMin(t *testing.T) { 161 | type i []interface{} 162 | var tests = []struct { 163 | input []interface{} 164 | expected interface{} 165 | }{ 166 | {i{0, 0}, 0}, 167 | {i{1, 2}, 1}, 168 | {i{1.1, 2}, 1.1}, 169 | {i{-1.9, -1.8}, -1.9}, 170 | } 171 | 172 | for _, test := range tests { 173 | var actual = funcMin(data.New(test.input).(data.List)) 174 | if actual != data.New(test.expected) { 175 | t.Errorf("min(%v) => %v, expected %v", test.input, actual, test.expected) 176 | } 177 | } 178 | } 179 | 180 | func TestMax(t *testing.T) { 181 | type i []interface{} 182 | var tests = []struct { 183 | input []interface{} 184 | expected interface{} 185 | }{ 186 | {i{0, 0}, 0}, 187 | {i{1, 2}, 2}, 188 | {i{1.1, 2}, 2.0}, // only returns int if both are ints. 189 | {i{-1.9, -1.8}, -1.8}, 190 | } 191 | 192 | for _, test := range tests { 193 | var actual = funcMax(data.New(test.input).(data.List)) 194 | if actual != data.New(test.expected) { 195 | t.Errorf("max(%v) => %v, expected %v", test.input, actual, test.expected) 196 | } 197 | } 198 | } 199 | 200 | func TestIsnonnull(t *testing.T) { 201 | var tests = []struct { 202 | input data.Value 203 | expected bool 204 | }{ 205 | {data.Null{}, false}, 206 | {data.Undefined{}, false}, 207 | {data.Bool(false), true}, 208 | {data.Int(0), true}, 209 | {data.Float(0), true}, 210 | {data.String(""), true}, 211 | {data.List{}, true}, 212 | {data.Map{}, true}, 213 | } 214 | 215 | for _, test := range tests { 216 | var actual = funcIsNonnull([]data.Value{test.input}).(data.Bool) 217 | if bool(actual) != test.expected { 218 | t.Errorf("isNonnull(%v) => %v, expected %v", test.input, actual, test.expected) 219 | } 220 | } 221 | } 222 | 223 | func TestAugmentMap(t *testing.T) { 224 | type m map[string]interface{} 225 | var tests = []struct { 226 | arg1, arg2 map[string]interface{} 227 | expected map[string]interface{} 228 | }{ 229 | {m{}, m{}, m{}}, 230 | {m{"a": 0}, m{}, m{"a": 0}}, 231 | {m{}, m{"a": 0}, m{"a": 0}}, 232 | {m{"a": 0}, m{"a": 1}, m{"a": 1}}, 233 | {m{"a": 0}, m{"b": 1}, m{"a": 0, "b": 1}}, 234 | } 235 | 236 | for _, test := range tests { 237 | var actual = funcAugmentMap([]data.Value{data.New(test.arg1), data.New(test.arg2)}).(data.Map) 238 | 239 | if len(actual) != len(test.expected) { 240 | t.Errorf("augmentMap(%v, %v) => %v, expected %v", 241 | test.arg1, test.arg2, actual, test.expected) 242 | } 243 | for k, v := range actual { 244 | if v != data.New(test.expected[k]) { 245 | t.Errorf("augmentMap(%v, %v) => %v, expected %v", 246 | test.arg1, test.arg2, actual, test.expected) 247 | } 248 | } 249 | } 250 | } 251 | 252 | func TestKeys(t *testing.T) { 253 | type m map[string]interface{} 254 | type i []interface{} 255 | var tests = []struct { 256 | input map[string]interface{} 257 | expected []interface{} 258 | }{ 259 | {m{}, i{}}, 260 | {m{"a": 0}, i{"a"}}, 261 | } 262 | 263 | for _, test := range tests { 264 | var actual = funcKeys([]data.Value{data.New(test.input)}).(data.List) 265 | if len(actual) != len(test.expected) { 266 | t.Errorf("keys(%v) => %v, expected %v", test.input, actual, test.expected) 267 | } 268 | for i, v := range actual { 269 | if v != data.New(test.expected[i]) { 270 | t.Errorf("keys(%v) => %v, expected %v", test.input, actual, test.expected) 271 | } 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /soyhtml/renderer.go: -------------------------------------------------------------------------------- 1 | // Package soyhtml renders a compiled set of Soy to HTML. 2 | package soyhtml 3 | 4 | import ( 5 | "errors" 6 | "io" 7 | 8 | "github.com/robfig/soy/ast" 9 | "github.com/robfig/soy/data" 10 | "github.com/robfig/soy/soymsg" 11 | ) 12 | 13 | var ErrTemplateNotFound = errors.New("template not found") 14 | 15 | // Renderer provides parameters to template execution. 16 | // At minimum, Registry and Template are required to render a template.. 17 | type Renderer struct { 18 | tofu *Tofu // a registry of all templates in a bundle 19 | name string // fully-qualified name of the template to render 20 | ij data.Map // data for the $ij map 21 | msgs soymsg.Bundle 22 | } 23 | 24 | // Inject sets the given data map as the $ij injected data. 25 | func (r *Renderer) Inject(ij data.Map) *Renderer { 26 | r.ij = ij 27 | return r 28 | } 29 | 30 | // WithMessages provides a message bundle to use during execution. 31 | func (r *Renderer) WithMessages(bundle soymsg.Bundle) *Renderer { 32 | r.msgs = bundle 33 | return r 34 | } 35 | 36 | // Execute applies a parsed template to the specified data object, 37 | // and writes the output to wr. 38 | func (t Renderer) Execute(wr io.Writer, obj data.Map) (err error) { 39 | if t.tofu == nil || t.tofu.registry == nil { 40 | return errors.New("Template Registry required") 41 | } 42 | if t.name == "" { 43 | return errors.New("Template name required") 44 | } 45 | 46 | var tmpl, ok = t.tofu.registry.Template(t.name) 47 | if !ok { 48 | return ErrTemplateNotFound 49 | } 50 | 51 | var autoescapeMode = tmpl.Namespace.Autoescape 52 | if autoescapeMode == ast.AutoescapeUnspecified { 53 | autoescapeMode = ast.AutoescapeOn 54 | } 55 | 56 | var initialScope = newScope(obj) 57 | initialScope.enter() 58 | 59 | state := &state{ 60 | tmpl: tmpl, 61 | registry: *t.tofu.registry, 62 | namespace: tmpl.Namespace.Name, 63 | autoescape: autoescapeMode, 64 | wr: wr, 65 | context: initialScope, 66 | ij: t.ij, 67 | msgs: t.msgs, 68 | } 69 | defer state.errRecover(&err) 70 | state.walk(tmpl.Node) 71 | return 72 | } 73 | -------------------------------------------------------------------------------- /soyhtml/scope.go: -------------------------------------------------------------------------------- 1 | package soyhtml 2 | 3 | import "github.com/robfig/soy/data" 4 | 5 | // scope handles variable assignment and lookup within a template. 6 | // it is a stack of data maps, each of which corresponds to variable scope. 7 | // assignments made deeper in the stack take precedence over earlier ones. 8 | type scope []scopeframe 9 | 10 | // scopeframe is a single piece of the overall variable assignment. 11 | type scopeframe struct { 12 | vars data.Map // map of variable name to value 13 | entered bool // true if this was the initial frame for a template 14 | } 15 | 16 | func newScope(m data.Map) scope { 17 | return scope{{m, false}} 18 | } 19 | 20 | // push creates a new scope 21 | func (s *scope) push() { 22 | *s = append(*s, scopeframe{make(data.Map), false}) 23 | } 24 | 25 | // pop discards the last scope pushed. 26 | func (s *scope) pop() { 27 | *s = (*s)[:len(*s)-1] 28 | } 29 | 30 | // set adds a new binding to the deepest scope 31 | func (s scope) set(k string, v data.Value) { 32 | s[len(s)-1].vars[k] = v 33 | } 34 | 35 | // lookup checks the variable scopes, deepest out, for the given key 36 | func (s scope) lookup(k string) data.Value { 37 | for i := range s { 38 | var elem = s[len(s)-i-1].vars 39 | if val, ok := elem[k]; ok { 40 | return val 41 | } 42 | } 43 | return data.Undefined{} 44 | } 45 | 46 | // alldata returns a new scope for use when passing data="all" to a template. 47 | func (s scope) alldata() scope { 48 | for i := range s { 49 | var ri = len(s) - i - 1 50 | if s[ri].entered { 51 | return s[: ri+1 : ri+1] 52 | } 53 | } 54 | panic("impossible") 55 | } 56 | 57 | // enter records that this is the frame where we enter a template. 58 | // only the frames up to here will be passed in the next data="all" 59 | func (s *scope) enter() { 60 | (*s)[len(*s)-1].entered = true 61 | s.push() 62 | } 63 | -------------------------------------------------------------------------------- /soyhtml/tofu.go: -------------------------------------------------------------------------------- 1 | package soyhtml 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/robfig/soy/data" 8 | "github.com/robfig/soy/template" 9 | ) 10 | 11 | // Tofu is a bundle of compiled soy, ready to render to HTML. 12 | type Tofu struct { 13 | registry *template.Registry 14 | } 15 | 16 | // NewTofu returns a new instance that is ready to provide HTML rendering 17 | // services for the given templates, with the default functions and print 18 | // directives. 19 | func NewTofu(registry *template.Registry) *Tofu { 20 | return &Tofu{registry} 21 | } 22 | 23 | // Render is a convenience function that executes the Soy template of the given 24 | // name, using the given object (converted to data.Map) as context, and writes 25 | // the results to the given Writer. 26 | // 27 | // When converting structs to soy's data format, the DefaultStructOptions are 28 | // used. In particular, note that struct properties are converted to lowerCamel 29 | // by default, since that is the Soy naming convention. The caller may update 30 | // those options to change the behavior of this function. 31 | func (tofu Tofu) Render(wr io.Writer, name string, obj interface{}) error { 32 | var m data.Map 33 | if obj != nil { 34 | var ok bool 35 | m, ok = data.New(obj).(data.Map) 36 | if !ok { 37 | return fmt.Errorf("invalid data type. expected map/struct, got %T", obj) 38 | } 39 | } 40 | return tofu.NewRenderer(name).Execute(wr, m) 41 | } 42 | 43 | // NewRenderer returns a new instance of a Soy html renderer, given the 44 | // fully-qualified name of the template to render. 45 | func (tofu *Tofu) NewRenderer(name string) *Renderer { 46 | return &Renderer{ 47 | tofu: tofu, 48 | name: name, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /soyjs/directives.go: -------------------------------------------------------------------------------- 1 | package soyjs 2 | 3 | // PrintDirective represents a transformation applied when printing a value. 4 | type PrintDirective struct { 5 | Name string 6 | CancelAutoescape bool 7 | } 8 | 9 | // PrintDirectives are the builtin print directives. 10 | // Callers may add their own print directives to this map. 11 | var PrintDirectives = map[string]PrintDirective{ 12 | "insertWordBreaks": {"soy.$$insertWordBreaks", true}, 13 | "changeNewlineToBr": {"soy.$$changeNewlineToBr", true}, 14 | "truncate": {"soy.$$truncate", false}, 15 | "id": {"", true}, // visitPrint() will turn into a noop 16 | "noAutoescape": {"", true}, // visitPrint() will turn into a noop 17 | "escapeHtml": {"soy.$$escapeHtml", true}, 18 | "escapeUri": {"soy.$$escapeUri", true}, 19 | "escapeJsString": {"soy.$$escapeJsString", true}, 20 | "bidiSpanWrap": {"soy.$$bidiSpanWrap", false}, 21 | "bidiUnicodeWrap": {"soy.$$bidiUnicodeWrap", false}, 22 | "json": {"JSON.stringify", true}, 23 | } 24 | -------------------------------------------------------------------------------- /soyjs/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package soyjs compiles Soy to javascript. 3 | 4 | It fulfills the same interface as the javascript produced by the official Soy 5 | compiler and should work as a drop-in replacement. 6 | https://developers.google.com/closure/templates/docs/javascript_usage 7 | 8 | It is presently alpha quality. See ../TODO for unimplemented features. 9 | */ 10 | package soyjs 11 | -------------------------------------------------------------------------------- /soyjs/exec.go: -------------------------------------------------------------------------------- 1 | package soyjs 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | "text/template" 11 | 12 | "github.com/robfig/soy/ast" 13 | "github.com/robfig/soy/data" 14 | "github.com/robfig/soy/soymsg" 15 | ) 16 | 17 | type state struct { 18 | wr io.Writer 19 | node ast.Node // current node, for errors 20 | indentLevels int 21 | namespace string 22 | bufferName string 23 | varnum int 24 | scope scope 25 | autoescape ast.AutoescapeType 26 | lastNode ast.Node 27 | options Options 28 | funcsCalled map[string]string 29 | funcsInFile map[string]bool 30 | } 31 | 32 | func difference(a map[string]string, b map[string]bool) []string { 33 | new := []string{} 34 | for key1 := range a { 35 | if _, ok := b[key1]; !ok { 36 | new = append(new, key1) 37 | } 38 | } 39 | return new 40 | } 41 | 42 | // Write writes the javascript represented by the given node to the given 43 | // writer. The first error encountered is returned. 44 | func Write(out io.Writer, node ast.Node, options Options) (err error) { 45 | defer errRecover(&err) 46 | 47 | if options.Formatter == nil { 48 | options.Formatter = &ES5Formatter{} 49 | } 50 | 51 | var ( 52 | tmpOut = &bytes.Buffer{} 53 | importsBuf = &bytes.Buffer{} 54 | s = &state{ 55 | wr: tmpOut, 56 | options: options, 57 | funcsCalled: map[string]string{}, 58 | funcsInFile: map[string]bool{}, 59 | } 60 | ) 61 | 62 | s.scope.push() 63 | s.walk(node) 64 | 65 | if len(s.funcsCalled) > 0 { 66 | for _, f := range difference(s.funcsCalled, s.funcsInFile) { 67 | importsBuf.WriteString(s.funcsCalled[f]) 68 | importsBuf.WriteRune('\n') 69 | } 70 | importsBuf.WriteRune('\n') 71 | } 72 | 73 | out.Write(importsBuf.Bytes()) 74 | out.Write(tmpOut.Bytes()) 75 | 76 | return nil 77 | } 78 | 79 | // at marks the state to be on node n, for error reporting. 80 | func (s *state) at(node ast.Node) { 81 | s.lastNode = s.node 82 | s.node = node 83 | } 84 | 85 | // errorf formats the error and terminates processing. 86 | func (s *state) errorf(format string, args ...interface{}) { 87 | panic(fmt.Sprintf(format, args...)) 88 | } 89 | 90 | // errRecover is the handler that turns panics into returns from the top 91 | // level of Parse. 92 | func errRecover(errp *error) { 93 | e := recover() 94 | if e != nil { 95 | *errp = fmt.Errorf("%v", e) 96 | } 97 | } 98 | 99 | // walk recursively goes through each node and translates the nodes to 100 | // javascript, writing the result to s.wr 101 | func (s *state) walk(node ast.Node) { 102 | s.at(node) 103 | switch node := node.(type) { 104 | case *ast.SoyFileNode: 105 | s.visitSoyFile(node) 106 | case *ast.NamespaceNode: 107 | s.visitNamespace(node) 108 | case *ast.SoyDocNode: 109 | return 110 | case *ast.TemplateNode: 111 | s.visitTemplate(node) 112 | case *ast.ListNode: 113 | s.visitChildren(node) 114 | 115 | // Output nodes ---------- 116 | case *ast.RawTextNode: 117 | s.writeRawText(node.Text) 118 | case *ast.PrintNode: 119 | s.visitPrint(node) 120 | case *ast.MsgNode: 121 | s.visitMsg(node) 122 | case *ast.MsgHtmlTagNode: 123 | s.writeRawText(node.Text) 124 | case *ast.CssNode: 125 | if node.Expr != nil { 126 | s.jsln(s.bufferName, " += ", node.Expr, " + '-';") 127 | } 128 | s.writeRawText([]byte(node.Suffix)) 129 | case *ast.DebuggerNode: 130 | s.jsln("debugger;") 131 | case *ast.LogNode: 132 | s.bufferName += "_" 133 | s.jsln("var ", s.bufferName, " = '';") 134 | s.walk(node.Body) 135 | s.jsln("console.log(", s.bufferName, ");") 136 | s.bufferName = s.bufferName[:len(s.bufferName)-1] 137 | 138 | // Control flow ---------- 139 | case *ast.IfNode: 140 | s.visitIf(node) 141 | case *ast.ForNode: 142 | s.visitFor(node) 143 | case *ast.SwitchNode: 144 | s.visitSwitch(node) 145 | case *ast.CallNode: 146 | s.visitCall(node) 147 | case *ast.LetValueNode: 148 | s.jsln("var ", s.scope.makevar(node.Name), " = ", node.Expr, ";") 149 | case *ast.LetContentNode: 150 | var oldBufferName = s.bufferName 151 | s.bufferName = s.scope.makevar(node.Name) 152 | s.jsln("var ", s.bufferName, " = '';") 153 | s.walk(node.Body) 154 | s.bufferName = oldBufferName 155 | 156 | // Values ---------- 157 | case *ast.NullNode: 158 | s.js("null") 159 | case *ast.StringNode: 160 | s.js("'") 161 | template.JSEscape(s.wr, []byte(node.Value)) 162 | s.js("'") 163 | case *ast.IntNode: 164 | s.js(node.String()) 165 | case *ast.FloatNode: 166 | s.js(node.String()) 167 | case *ast.BoolNode: 168 | s.js(node.String()) 169 | case *ast.GlobalNode: 170 | s.visitGlobal(node) 171 | case *ast.ListLiteralNode: 172 | s.js("[") 173 | for i, item := range node.Items { 174 | if i != 0 { 175 | s.js(",") 176 | } 177 | s.walk(item) 178 | } 179 | s.js("]") 180 | case *ast.MapLiteralNode: 181 | s.js("{") 182 | var ( 183 | first = true 184 | keys = make([]string, len(node.Items)) 185 | i = 0 186 | ) 187 | for k := range node.Items { 188 | keys[i] = k 189 | i++ 190 | } 191 | sort.Strings(keys) 192 | for _, k := range keys { 193 | if !first { 194 | s.js(",") 195 | } 196 | first = false 197 | s.js("\"", k, "\"", ":") 198 | s.walk(node.Items[k]) 199 | } 200 | s.js("}") 201 | case *ast.FunctionNode: 202 | s.visitFunction(node) 203 | case *ast.DataRefNode: 204 | s.visitDataRef(node) 205 | 206 | // Arithmetic operators ---------- 207 | case *ast.NegateNode: 208 | s.js("(-", node.Arg, ")") 209 | case *ast.AddNode: 210 | s.op("+", node) 211 | case *ast.SubNode: 212 | s.op("-", node) 213 | case *ast.DivNode: 214 | s.op("/", node) 215 | case *ast.MulNode: 216 | s.op("*", node) 217 | case *ast.ModNode: 218 | s.op("%", node) 219 | 220 | // Arithmetic comparisons ---------- 221 | case *ast.EqNode: 222 | s.op("==", node) 223 | case *ast.NotEqNode: 224 | s.op("!=", node) 225 | case *ast.LtNode: 226 | s.op("<", node) 227 | case *ast.LteNode: 228 | s.op("<=", node) 229 | case *ast.GtNode: 230 | s.op(">", node) 231 | case *ast.GteNode: 232 | s.op(">=", node) 233 | 234 | // Boolean operators ---------- 235 | case *ast.NotNode: 236 | s.js("!(", node.Arg, ")") 237 | case *ast.AndNode: 238 | s.op("&&", node) 239 | case *ast.OrNode: 240 | s.op("||", node) 241 | case *ast.ElvisNode: 242 | // ?: is specified to check for null. 243 | s.js("((", node.Arg1, ") != null ? ", node.Arg1, " : ", node.Arg2, ")") 244 | case *ast.TernNode: 245 | s.js("((", node.Arg1, ") ?", node.Arg2, ":", node.Arg3, ")") 246 | 247 | default: 248 | s.errorf("unknown node (%T): %v", node, node) 249 | } 250 | } 251 | 252 | func (s *state) visitSoyFile(node *ast.SoyFileNode) { 253 | s.jsln("// This file was automatically generated from ", node.Name, ".") 254 | s.jsln("// Please don't edit this file by hand.") 255 | s.jsln("") 256 | s.visitChildren(node) 257 | } 258 | 259 | func (s *state) visitChildren(parent ast.ParentNode) { 260 | for _, child := range parent.Children() { 261 | s.walk(child) 262 | } 263 | } 264 | 265 | func (s *state) visitNamespace(node *ast.NamespaceNode) { 266 | s.namespace = node.Name 267 | s.autoescape = node.Autoescape 268 | 269 | // iterate through the dot segments. 270 | var i = 0 271 | for i < len(node.Name) { 272 | var decl = "var " 273 | var prev = i + 1 274 | i = strings.Index(node.Name[prev:], ".") 275 | if i == -1 { 276 | i = len(node.Name) 277 | } else { 278 | i += prev 279 | } 280 | if strings.Contains(node.Name[:i], ".") { 281 | decl = "" 282 | } 283 | s.jsln("if (typeof ", node.Name[:i], " == 'undefined') { ", decl, node.Name[:i], " = {}; }") 284 | } 285 | } 286 | 287 | func (s *state) visitTemplate(node *ast.TemplateNode) { 288 | var oldAutoescape = s.autoescape 289 | if node.Autoescape != ast.AutoescapeUnspecified { 290 | s.autoescape = node.Autoescape 291 | } 292 | 293 | // Determine if we need nullsafe initialization for opt_data 294 | var allOptionalParams = false 295 | if soydoc, ok := s.lastNode.(*ast.SoyDocNode); ok { 296 | allOptionalParams = len(soydoc.Params) > 0 297 | for _, param := range soydoc.Params { 298 | if !param.Optional { 299 | allOptionalParams = false 300 | } 301 | } 302 | } 303 | 304 | s.jsln("") 305 | callName, callStyle := s.options.Formatter.Template(node.Name) 306 | s.jsln(callStyle, "(opt_data, opt_sb, opt_ijData) {") 307 | s.funcsInFile[callName] = true 308 | s.indentLevels++ 309 | if allOptionalParams { 310 | s.jsln("opt_data = opt_data || {};") 311 | } 312 | s.jsln("var output = '';") 313 | s.bufferName = "output" 314 | s.scope.push() 315 | defer s.scope.pop() 316 | s.walk(node.Body) 317 | s.jsln("return output;") 318 | s.indentLevels-- 319 | s.jsln("};") 320 | s.autoescape = oldAutoescape 321 | } 322 | 323 | // TODO: unify print directives 324 | func (s *state) visitPrint(node *ast.PrintNode) { 325 | var escape = s.autoescape 326 | var directives []*ast.PrintDirectiveNode 327 | for _, dir := range node.Directives { 328 | var directive, ok = PrintDirectives[dir.Name] 329 | if !ok { 330 | s.errorf("Print directive %q not found", dir.Name) 331 | } 332 | if directive.CancelAutoescape { 333 | escape = ast.AutoescapeOff 334 | } 335 | switch dir.Name { 336 | case "id", "noAutoescape": 337 | // no implementation, they just serve as a marker to cancel autoescape. 338 | default: 339 | directives = append(directives, dir) 340 | if impt := s.options.Formatter.Directive(directive); impt != "" { 341 | s.funcsCalled[dir.Name] = impt 342 | } 343 | } 344 | } 345 | if escape != ast.AutoescapeOff { 346 | directives = append([]*ast.PrintDirectiveNode{{0, "escapeHtml", nil}}, directives...) 347 | } 348 | 349 | s.indent() 350 | s.js(s.bufferName, " += ") 351 | for _, dir := range directives { 352 | s.js(PrintDirectives[dir.Name].Name, "(") 353 | } 354 | s.walk(node.Arg) 355 | for i := range directives { 356 | var dir = directives[len(directives)-1-i] 357 | for _, arg := range dir.Args { 358 | s.js(",") 359 | s.walk(arg) 360 | } 361 | // Soy specifies truncate adds ellipsis by default, so we have to pass 362 | // doAddEllipsis = true to soy.$$truncate 363 | if dir.Name == "truncate" && len(dir.Args) == 1 { 364 | s.js(",true") 365 | } 366 | s.js(")") 367 | } 368 | s.js(";\n") 369 | } 370 | 371 | func (s *state) visitFunction(node *ast.FunctionNode) { 372 | if fn, ok := Funcs[node.Name]; ok { 373 | fn.Apply(s, node.Args) 374 | if impt := s.options.Formatter.Function(fn); impt != "" { 375 | s.funcsCalled[node.Name] = impt 376 | } 377 | return 378 | } 379 | 380 | switch node.Name { 381 | case "isFirst": 382 | // TODO: Add compile-time check that this is only called on loop variable. 383 | s.js("(", s.scope.loopindex(), " == 0)") 384 | case "isLast": 385 | s.js("(", s.scope.loopindex(), " == ", s.scope.looplimit(), " - 1)") 386 | case "index": 387 | s.js(s.scope.loopindex()) 388 | default: 389 | s.errorf("unimplemented function: %v", node.Name) 390 | } 391 | } 392 | 393 | func (s *state) visitDataRef(node *ast.DataRefNode) { 394 | var expr string 395 | if node.Key == "ij" { 396 | expr = "opt_ijData" 397 | } else if genVarName := s.scope.lookup(node.Key); genVarName != "" { 398 | expr = genVarName 399 | } else { 400 | expr = "opt_data." + node.Key 401 | } 402 | 403 | // Nullsafe access makes this complicated. 404 | // FOO.BAR?.BAZ => (FOO.BAR == null ? null : FOO.BAR.BAZ) 405 | for _, accessNode := range node.Access { 406 | switch node := accessNode.(type) { 407 | case *ast.DataRefIndexNode: 408 | if node.NullSafe { 409 | s.js("(", expr, " == null) ? null : ") 410 | } 411 | expr += "[" + strconv.Itoa(node.Index) + "]" 412 | case *ast.DataRefKeyNode: 413 | if node.NullSafe { 414 | s.js("(", expr, " == null) ? null : ") 415 | } 416 | expr += "." + node.Key 417 | case *ast.DataRefExprNode: 418 | if node.NullSafe { 419 | s.js("(", expr, " == null) ? null : ") 420 | } 421 | expr += "[" + s.block(node.Arg) + "]" 422 | } 423 | } 424 | s.js(expr) 425 | } 426 | 427 | func (s *state) visitCall(node *ast.CallNode) { 428 | var dataExpr = "{}" 429 | if node.Data != nil { 430 | dataExpr = s.block(node.Data) 431 | } else if node.AllData { 432 | dataExpr = "opt_data" 433 | } 434 | 435 | if len(node.Params) > 0 { 436 | dataExpr = "soy.$$augmentMap(" + dataExpr + ", {" 437 | for i, param := range node.Params { 438 | if i > 0 { 439 | dataExpr += ", " 440 | } 441 | switch param := param.(type) { 442 | case *ast.CallParamValueNode: 443 | dataExpr += param.Key + ": " + s.block(param.Value) 444 | case *ast.CallParamContentNode: 445 | var oldBufferName = s.bufferName 446 | s.bufferName = s.scope.makevar("param") 447 | s.jsln("var ", s.bufferName, " = '';") 448 | s.walk(param.Content) 449 | dataExpr += param.Key + ": " + s.bufferName 450 | s.bufferName = oldBufferName 451 | } 452 | } 453 | dataExpr += "})" 454 | } 455 | callName, importString := s.options.Formatter.Call(node.Name) 456 | s.jsln(s.bufferName, " += ", callName, "(", dataExpr, ", opt_sb, opt_ijData);") 457 | if importString != "" { 458 | s.funcsCalled[callName] = importString 459 | } 460 | } 461 | 462 | func (s *state) visitIf(node *ast.IfNode) { 463 | s.indent() 464 | for i, branch := range node.Conds { 465 | if i > 0 { 466 | s.js(" else ") 467 | } 468 | if branch.Cond != nil { 469 | s.js("if (", branch.Cond, ") ") 470 | } 471 | s.js("{\n") 472 | s.indentLevels++ 473 | s.walk(branch.Body) 474 | s.indentLevels-- 475 | s.indent() 476 | s.js("}") 477 | } 478 | s.js("\n") 479 | } 480 | 481 | func (s *state) visitFor(node *ast.ForNode) { 482 | if rangeNode, ok := node.List.(*ast.FunctionNode); ok && rangeNode.Name == "range" { 483 | s.visitForRange(node) 484 | } else { 485 | s.visitForeach(node) 486 | } 487 | } 488 | 489 | func (s *state) visitForRange(node *ast.ForNode) { 490 | var rangeNode = node.List.(*ast.FunctionNode) 491 | var ( 492 | increment ast.Node = &ast.IntNode{0, 1} 493 | init ast.Node = &ast.IntNode{0, 0} 494 | limit ast.Node 495 | ) 496 | switch len(rangeNode.Args) { 497 | case 3: 498 | increment = rangeNode.Args[2] 499 | fallthrough 500 | case 2: 501 | init = rangeNode.Args[0] 502 | limit = rangeNode.Args[1] 503 | case 1: 504 | limit = rangeNode.Args[0] 505 | } 506 | 507 | var varIndex, 508 | varLimit = s.scope.pushForRange(node.Var) 509 | defer s.scope.pop() 510 | s.jsln("var ", varLimit, " = ", limit, ";") 511 | s.jsln("for (var ", varIndex, " = ", init, "; ", 512 | varIndex, " < ", varLimit, "; ", 513 | varIndex, " += ", increment, ") {") 514 | s.indentLevels++ 515 | s.walk(node.Body) 516 | s.indentLevels-- 517 | s.jsln("}") 518 | } 519 | 520 | func (s *state) visitForeach(node *ast.ForNode) { 521 | var itemData, 522 | itemList, 523 | itemListLen, 524 | itemIndex = s.scope.pushForEach(node.Var) 525 | defer s.scope.pop() 526 | s.jsln("var ", itemList, " = ", node.List, ";") 527 | s.jsln("var ", itemListLen, " = ", itemList, ".length;") 528 | if node.IfEmpty != nil { 529 | s.jsln("if (", itemListLen, " > 0) {") 530 | s.indentLevels++ 531 | } 532 | s.jsln("for (var ", itemIndex, " = 0; ", itemIndex, " < ", itemListLen, "; ", itemIndex, "++) {") 533 | s.indentLevels++ 534 | s.jsln("var ", itemData, " = ", itemList, "[", itemIndex, "];") 535 | s.walk(node.Body) 536 | s.indentLevels-- 537 | s.jsln("}") 538 | if node.IfEmpty != nil { 539 | s.indentLevels-- 540 | s.jsln("} else {") 541 | s.indentLevels++ 542 | s.walk(node.IfEmpty) 543 | s.indentLevels-- 544 | s.jsln("}") 545 | } 546 | } 547 | 548 | func (s *state) visitSwitch(node *ast.SwitchNode) { 549 | s.jsln("switch (", node.Value, ") {") 550 | s.indentLevels++ 551 | for _, switchCase := range node.Cases { 552 | for _, switchCaseValue := range switchCase.Values { 553 | s.jsln("case ", switchCaseValue, ":") 554 | } 555 | if len(switchCase.Values) == 0 { 556 | s.jsln("default:") 557 | } 558 | s.indentLevels++ 559 | s.walk(switchCase.Body) 560 | s.jsln("break;") 561 | s.indentLevels-- 562 | } 563 | s.indentLevels-- 564 | s.jsln("}") 565 | } 566 | 567 | func (s *state) visitMsg(node *ast.MsgNode) { 568 | // If no bundle was provided, walk the message sub-nodes. 569 | if s.options.Messages == nil { 570 | s.visitMsgNode(node) 571 | return 572 | } 573 | 574 | // Look up the message in the bundle. 575 | var msg = s.options.Messages.Message(node.ID) 576 | if msg == nil { 577 | s.visitMsgNode(node) 578 | return 579 | } 580 | 581 | // Render each part. 582 | s.evalMsgParts(node, msg.Parts) 583 | } 584 | 585 | func (s *state) evalMsgParts(msgNode *ast.MsgNode, parts []soymsg.Part) { 586 | for _, part := range parts { 587 | switch part := part.(type) { 588 | 589 | case soymsg.RawTextPart: 590 | s.writeRawText([]byte(part.Text)) 591 | 592 | case soymsg.PlaceholderPart: 593 | // Find the node corresponding to the placeholder, and walk it. 594 | var phnode = msgNode.Placeholder(part.Name) 595 | if phnode == nil { 596 | s.errorf("failed to find placeholder %q in %q", 597 | part.Name, soymsg.PlaceholderString(msgNode)) 598 | } 599 | s.walk(phnode.Body) 600 | 601 | case soymsg.PluralPart: 602 | // Find the corresponding node for this part. 603 | child := s.findPluralNode(msgNode, part.VarName) 604 | 605 | s.jsln("switch (soy.$$pluralIndex(", child.Value, ")) {") 606 | s.indentLevels++ 607 | 608 | for i, pluralPart := range part.Cases { 609 | s.jsln("case ", i, ":") 610 | s.indentLevels++ 611 | s.evalMsgParts(msgNode, pluralPart.Parts) 612 | s.jsln("break;") 613 | s.indentLevels-- 614 | } 615 | 616 | s.indentLevels-- 617 | s.jsln("}") 618 | } 619 | } 620 | } 621 | 622 | func (s *state) findPluralNode(node *ast.MsgNode, pluralVarName string) *ast.MsgPluralNode { 623 | for _, plnode := range node.Body.Children() { 624 | if plnode, ok := plnode.(*ast.MsgPluralNode); ok && plnode.VarName == pluralVarName { 625 | return plnode 626 | } 627 | } 628 | s.errorf("failed to find placeholder %q in %v", pluralVarName, node.Body) 629 | panic("unreachable") 630 | } 631 | 632 | func (s *state) visitMsgNode(n ast.ParentNode) { 633 | for _, child := range n.Children() { 634 | switch child := child.(type) { 635 | case *ast.RawTextNode: 636 | s.walk(child) 637 | case *ast.MsgPlaceholderNode: 638 | s.walk(child.Body) 639 | case *ast.MsgPluralNode: 640 | s.walkPlural(child) 641 | } 642 | } 643 | } 644 | 645 | func (s *state) walkPlural(n *ast.MsgPluralNode) { 646 | s.jsln("switch (", n.Value, ") {") 647 | s.indentLevels++ 648 | for _, pluralCase := range n.Cases { 649 | s.jsln("case ", pluralCase.Value, ":") 650 | s.indentLevels++ 651 | s.visitMsgNode(pluralCase.Body) 652 | s.jsln("break;") 653 | s.indentLevels-- 654 | } 655 | { 656 | s.jsln("default:") 657 | s.indentLevels++ 658 | s.visitMsgNode(n.Default) 659 | s.indentLevels-- 660 | } 661 | s.indentLevels-- 662 | s.jsln("}") 663 | } 664 | 665 | // visitGlobal constructs a primitive node from its value and uses walk to 666 | // render the right thing. 667 | func (s *state) visitGlobal(node *ast.GlobalNode) { 668 | s.walk(s.nodeFromValue(node.Pos, node.Value)) 669 | } 670 | 671 | func (s *state) nodeFromValue(pos ast.Pos, val data.Value) ast.Node { 672 | switch val := val.(type) { 673 | case data.Undefined: 674 | s.errorf("undefined value can not be converted to node") 675 | case data.Null: 676 | return &ast.NullNode{pos} 677 | case data.Bool: 678 | return &ast.BoolNode{pos, bool(val)} 679 | case data.Int: 680 | return &ast.IntNode{pos, int64(val)} 681 | case data.Float: 682 | return &ast.FloatNode{pos, float64(val)} 683 | case data.String: 684 | return &ast.StringNode{pos, "", string(val)} 685 | case data.List: 686 | var items = make([]ast.Node, len(val)) 687 | for i, item := range val { 688 | items[i] = s.nodeFromValue(pos, item) 689 | } 690 | return &ast.ListLiteralNode{pos, items} 691 | case data.Map: 692 | var items = make(map[string]ast.Node, len(val)) 693 | for k, v := range val { 694 | items[k] = s.nodeFromValue(pos, v) 695 | } 696 | return &ast.MapLiteralNode{pos, items} 697 | } 698 | panic("unreachable") 699 | } 700 | 701 | func (s *state) writeRawText(text []byte) { 702 | s.indent() 703 | s.js(s.bufferName, " += '") 704 | template.JSEscape(s.wr, text) 705 | s.js("';\n") 706 | } 707 | 708 | // block renders the given node to a temporary buffer and returns the string. 709 | func (s *state) block(node ast.Node) string { 710 | var buf bytes.Buffer 711 | (&state{ 712 | wr: &buf, 713 | scope: s.scope, 714 | options: s.options, 715 | funcsCalled: s.funcsCalled, 716 | funcsInFile: s.funcsInFile, 717 | }).walk(node) 718 | return buf.String() 719 | } 720 | 721 | func (s *state) op(symbol string, node ast.ParentNode) { 722 | var children = node.Children() 723 | s.js("((", children[0], ") ", symbol, " (", children[1], "))") 724 | } 725 | 726 | func (s *state) indent() { 727 | for i := 0; i < s.indentLevels; i++ { 728 | s.wr.Write([]byte(" ")) 729 | } 730 | } 731 | 732 | func (s *state) js(args ...interface{}) { 733 | for _, arg := range args { 734 | switch arg := arg.(type) { 735 | case string: 736 | s.wr.Write([]byte(arg)) 737 | case ast.Node: 738 | s.walk(arg) 739 | default: 740 | fmt.Fprintf(s.wr, "%v", arg) 741 | } 742 | } 743 | } 744 | 745 | func (s *state) jsln(args ...interface{}) { 746 | s.indent() 747 | s.js(args...) 748 | s.wr.Write([]byte("\n")) 749 | } 750 | -------------------------------------------------------------------------------- /soyjs/formatters.go: -------------------------------------------------------------------------------- 1 | package soyjs 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // The JSFormatter interface allows for callers to choose which 8 | // version of Javascript they would like soyjs to output. To 9 | // maintain backwards compatibility, if no JSFormatter is specified 10 | // in the Options, soyjs will default to the ES5Formatter implemented 11 | // in exec.go 12 | type JSFormatter interface { 13 | // Template returns two values, the name of the template to save 14 | // in the defined functions map, and how the function should be defined. 15 | Template(name string) (string, string) 16 | // Call returns two values, the name of the template to save 17 | // in the called functions map, and a string that is written 18 | // into the imports 19 | Call(name string) (string, string) 20 | // Directive takes in a PrintDirective and returns a string 21 | // that is written into the imports 22 | Directive(PrintDirective) string 23 | // Function takes in a Func and returns a string 24 | // that is written into the imports 25 | Function(Func) string 26 | } 27 | 28 | // ES5Formatter implements the JSFormatter interface 29 | // and creates Javascript files following the ES5 30 | // Javascript format (without imports) 31 | type ES5Formatter struct{} 32 | 33 | // ES6Formatter implements the JSFormatter interface 34 | // and creates Javascript files following the ES6 35 | // Javascript format (with imports) 36 | type ES6Formatter struct{} 37 | 38 | var _ JSFormatter = (*ES6Formatter)(nil) 39 | var _ JSFormatter = (*ES5Formatter)(nil) 40 | 41 | // Template returns two values, the name of the template to save 42 | // in the defined functions map, and how the function should be defined. 43 | // For ES5, the function is not exported, but defined globally 44 | func (f ES5Formatter) Template(name string) (string, string) { 45 | return name, name + " = function" 46 | } 47 | 48 | // Call returns two values, the name of the template to save 49 | // in the called functions map, and a string that is written 50 | // into the imports - for ES5, there are no imports 51 | func (f ES5Formatter) Call(name string) (string, string) { 52 | return name, "" 53 | } 54 | 55 | // Directive takes in a PrintDirective and returns a string 56 | // that is written into the imports - for ES5, there 57 | // are no imports 58 | func (f ES5Formatter) Directive(dir PrintDirective) string { 59 | return "" 60 | } 61 | 62 | // Function takes in a Func and returns a string 63 | // that is written into the imports - for ES5, there 64 | // are no imports 65 | func (f ES5Formatter) Function(fn Func) string { 66 | return "" 67 | } 68 | 69 | // ES6Identifier creates an ES6 compatible function name 70 | // without periods. It replaces all periods, which usually 71 | // denominate namespaces in soy, with a double underscore. 72 | // For example, from the file 73 | // {namespace say} 74 | // {template .hello_world} 75 | // Hello World 76 | // {/template} 77 | // when ES6Identifier is called on 78 | // say.hello_world 79 | // it will return 80 | // say__hello_world 81 | func ES6Identifier(s string) string { 82 | return strings.Replace(s, ".", "__", -1) 83 | } 84 | 85 | // Template returns two values, the name of the template to save 86 | // in the defined functions map, and how the function should be defined. 87 | // For ES6, the function is not defined globally, but exported 88 | func (f ES6Formatter) Template(name string) (string, string) { 89 | return ES6Identifier(name), "export function " + ES6Identifier(name) 90 | } 91 | 92 | // Call returns two values, the name of the template to save 93 | // in the called functions map, and a string that is written 94 | // into the imports 95 | func (f ES6Formatter) Call(name string) (string, string) { 96 | return ES6Identifier(name), "import { " + ES6Identifier(name) + " } from '" + name + ".js';" 97 | } 98 | 99 | // Directive takes in a PrintDirective and returns a string 100 | // that is written into the imports 101 | func (f ES6Formatter) Directive(dir PrintDirective) string { 102 | return "import { " + ES6Identifier(dir.Name) + " } from '" + dir.Name + ".js';" 103 | } 104 | 105 | // Function takes in a Func and returns a string 106 | // that is written into the imports 107 | func (f ES6Formatter) Function(fn Func) string { 108 | return "import { " + ES6Identifier(fn.Name) + " } from '" + fn.Name + ".js';" 109 | } 110 | -------------------------------------------------------------------------------- /soyjs/funcs.go: -------------------------------------------------------------------------------- 1 | package soyjs 2 | 3 | import ( 4 | "github.com/robfig/soy/ast" 5 | ) 6 | 7 | // JSWriter is provided to functions to write to the generated javascript. 8 | type JSWriter interface { 9 | // Write writes the given arguments into the generated javascript. It is 10 | // recommended to only pass strings and ast.Nodes to Write. Other types 11 | // are printed using their default string representation (fmt.Sprintf("%v")). 12 | Write(...interface{}) 13 | } 14 | 15 | func (s *state) Write(args ...interface{}) { 16 | s.js(args...) 17 | } 18 | 19 | // Func represents a Soy function that may invoked within a template. 20 | type Func struct { 21 | Name string 22 | Apply func(js JSWriter, args []ast.Node) 23 | ValidArgLengths []int 24 | } 25 | 26 | var funcs = []Func{ 27 | {"isNonnull", funcIsNonnull, []int{1}}, 28 | {"length", funcLength, []int{1}}, 29 | {"keys", builtinFunc("getMapKeys"), []int{1}}, 30 | {"augmentMap", builtinFunc("augmentMap"), []int{2}}, 31 | {"round", funcRound, []int{1, 2}}, 32 | {"floor", funcFloor, []int{1}}, 33 | {"ceiling", funcCeiling, []int{1}}, 34 | {"min", funcMin, []int{2}}, 35 | {"max", funcMax, []int{2}}, 36 | {"randomInt", funcRandomInt, []int{1}}, 37 | {"strContains", funcStrContains, []int{2}}, 38 | {"hasData", funcHasData, []int{0}}, 39 | {"bidiGlobalDir", funcBidiGlobalDir, []int{0}}, 40 | {"bidiDirAttr", funcBidiDirAttr, []int{0}}, 41 | {"bidiStartEdge", funcBidiStartEdge, []int{0}}, 42 | {"bidiEndEdge", funcBidiEndEdge, []int{0}}, 43 | } 44 | 45 | // Funcs contains the available Soy functions. 46 | // Callers may add custom functions to this map. 47 | var Funcs = make(map[string]Func, len(funcs)) 48 | 49 | func init() { 50 | for _, f := range funcs { 51 | Funcs[f.Name] = f 52 | } 53 | } 54 | 55 | // builtinFunc returns a function that writes a call to a soy.$$ builtin func. 56 | func builtinFunc(name string) func(js JSWriter, args []ast.Node) { 57 | var funcStart = "soy.$$" + name + "(" 58 | return func(js JSWriter, args []ast.Node) { 59 | js.Write(funcStart) 60 | for i, arg := range args { 61 | if i != 0 { 62 | js.Write(",") 63 | } 64 | js.Write(arg) 65 | } 66 | js.Write(")") 67 | } 68 | } 69 | 70 | func funcIsNonnull(js JSWriter, args []ast.Node) { 71 | js.Write(args[0], "!= null") 72 | } 73 | 74 | func funcLength(js JSWriter, args []ast.Node) { 75 | js.Write(args[0], ".length") 76 | } 77 | 78 | func funcRound(js JSWriter, args []ast.Node) { 79 | switch len(args) { 80 | case 1: 81 | js.Write("Math.round(", args[0], ")") 82 | default: 83 | js.Write( 84 | "Math.round(", args[0], "* Math.pow(10, ", args[1], ")) / Math.pow(10, ", args[1], ")") 85 | } 86 | } 87 | 88 | func funcFloor(js JSWriter, args []ast.Node) { 89 | js.Write("Math.floor(", args[0], ")") 90 | } 91 | 92 | func funcCeiling(js JSWriter, args []ast.Node) { 93 | js.Write("Math.ceil(", args[0], ")") 94 | } 95 | 96 | func funcMin(js JSWriter, args []ast.Node) { 97 | js.Write("Math.min(", args[0], ",", args[1], ")") 98 | } 99 | 100 | func funcMax(js JSWriter, args []ast.Node) { 101 | js.Write("Math.max(", args[0], ",", args[1], ")") 102 | } 103 | 104 | func funcRandomInt(js JSWriter, args []ast.Node) { 105 | js.Write("Math.floor(Math.random() * ", args[0], ")") 106 | } 107 | 108 | func funcStrContains(js JSWriter, args []ast.Node) { 109 | js.Write(args[0], ".indexOf(", args[1], ") != -1") 110 | } 111 | 112 | func funcHasData(js JSWriter, args []ast.Node) { 113 | js.Write("true") 114 | } 115 | 116 | func funcBidiGlobalDir(js JSWriter, args []ast.Node) { 117 | js.Write("1") 118 | } 119 | 120 | func funcBidiDirAttr(js JSWriter, args []ast.Node) { 121 | js.Write("soy.$$bidiDirAttr(0, ", args[0], ")") 122 | } 123 | 124 | func funcBidiStartEdge(js JSWriter, args []ast.Node) { 125 | js.Write("'left'") 126 | } 127 | 128 | func funcBidiEndEdge(js JSWriter, args []ast.Node) { 129 | js.Write("'right'") 130 | } 131 | -------------------------------------------------------------------------------- /soyjs/generator.go: -------------------------------------------------------------------------------- 1 | package soyjs 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | 7 | "github.com/robfig/soy/soymsg" 8 | "github.com/robfig/soy/template" 9 | ) 10 | 11 | // Options for js source generation. 12 | // When no Formatter is defined, soyjs 13 | // will default to ES5Formatter from exec.go 14 | type Options struct { 15 | Messages soymsg.Bundle 16 | Formatter JSFormatter 17 | } 18 | 19 | // Generator provides an interface to a template registry capable of generating 20 | // javascript to execute the embodied templates. 21 | // The generated javascript requires lib/soyutils.js to already have been loaded. 22 | type Generator struct { 23 | registry *template.Registry 24 | } 25 | 26 | // NewGenerator returns a new javascript generator capable of producing 27 | // javascript for the templates contained in the given registry. 28 | func NewGenerator(registry *template.Registry) *Generator { 29 | return &Generator{registry} 30 | } 31 | 32 | var ErrNotFound = errors.New("file not found") 33 | 34 | // WriteFile generates javascript corresponding to the Soy file of the given name. 35 | func (gen *Generator) WriteFile(out io.Writer, filename string) error { 36 | for _, soyfile := range gen.registry.SoyFiles { 37 | if soyfile.Name == filename { 38 | return Write(out, soyfile, Options{}) 39 | } 40 | } 41 | return ErrNotFound 42 | } 43 | -------------------------------------------------------------------------------- /soyjs/generator_test.go: -------------------------------------------------------------------------------- 1 | package soyjs 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/robertkrimen/otto" 8 | "github.com/robfig/soy/ast" 9 | "github.com/robfig/soy/parse" 10 | "github.com/robfig/soy/template" 11 | ) 12 | 13 | func TestGenerator(t *testing.T) { 14 | var otto = otto.New() 15 | var _, err = otto.Run(` 16 | var soy = {}; 17 | soy.$$escapeHtml = function(arg) { return arg; }; 18 | `) 19 | if err != nil { 20 | t.Error(err) 21 | return 22 | } 23 | 24 | Funcs["capitalize"] = Func{"capitalize", func(js JSWriter, args []ast.Node) { 25 | js.Write("(", args[0], ".charAt(0).toUpperCase() + ", args[0], ".slice(1))") 26 | }, []int{1}} 27 | defer delete(Funcs, "capitalize") 28 | 29 | soyfile, err := parse.SoyFile("name.soy", ` 30 | {namespace test} 31 | {template .funcs} 32 | {let $place: 'world'/} 33 | {capitalize('hel' + 'lo')}, {capitalize($place)} 34 | {/template}`) 35 | if err != nil { 36 | t.Error(err) 37 | return 38 | } 39 | 40 | var registry = template.Registry{} 41 | if err = registry.Add(soyfile); err != nil { 42 | t.Error(err) 43 | return 44 | } 45 | 46 | var gen = NewGenerator(®istry) 47 | var buf bytes.Buffer 48 | err = gen.WriteFile(&buf, "name.soy") 49 | if err != nil { 50 | t.Error(err) 51 | return 52 | } 53 | 54 | _, err = otto.Run(buf.String()) 55 | if err != nil { 56 | t.Error(err) 57 | return 58 | } 59 | 60 | output, err := otto.Run(`test.funcs();`) 61 | if err != nil { 62 | t.Error(err) 63 | return 64 | } 65 | if output.String() != "Hello, World" { 66 | t.Errorf("Got %q, expected Hello, World", output.String()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /soyjs/scope.go: -------------------------------------------------------------------------------- 1 | package soyjs 2 | 3 | import "strconv" 4 | 5 | // scope provides a lookup from Soy variable name to the JS name. 6 | // it is pushed and popped upon entering and leaving loop scopes. 7 | type scope struct { 8 | stack []map[string]string 9 | n int 10 | } 11 | 12 | func (s *scope) push() { 13 | s.stack = append(s.stack, make(map[string]string)) 14 | } 15 | 16 | func (s *scope) pop() { 17 | s.stack = s.stack[:len(s.stack)-1] 18 | } 19 | 20 | // makevar generates and returns a new JS name for the given variable name, adds 21 | // that mapping to this scope. 22 | func (s *scope) makevar(varname string) string { 23 | s.n++ 24 | var genName = varname + strconv.Itoa(s.n) 25 | s.stack[len(s.stack)-1][varname] = genName 26 | return genName 27 | } 28 | 29 | func (s *scope) lookup(varname string) string { 30 | for i := range s.stack { 31 | val, ok := s.stack[len(s.stack)-i-1][varname] 32 | if ok { 33 | return val 34 | } 35 | } 36 | return "" 37 | } 38 | 39 | func (s *scope) pushForRange(loopVar string) (lVar, lLimit string) { 40 | s.n++ 41 | n := strconv.Itoa(s.n) 42 | s.stack = append(s.stack, map[string]string{ 43 | loopVar: loopVar + n, 44 | "__limit": loopVar + "Limit" + n, 45 | "__index": loopVar + n, 46 | }) 47 | return loopVar + n, 48 | loopVar + "Limit" + n 49 | } 50 | 51 | func (s *scope) pushForEach(loopVar string) (lVar, lList, lLen, lIndex string) { 52 | s.n++ 53 | n := strconv.Itoa(s.n) 54 | s.stack = append(s.stack, map[string]string{ 55 | loopVar: loopVar + n, 56 | "__limit": loopVar + "Limit" + n, 57 | "__index": loopVar + "Index" + n, 58 | }) 59 | return loopVar + n, 60 | loopVar + "List" + n, 61 | loopVar + "Limit" + n, 62 | loopVar + "Index" + n 63 | } 64 | 65 | // looplimit returns the JS variable name for the innermost loop limit. 66 | func (s *scope) looplimit() string { 67 | return s.lookup("__limit") 68 | } 69 | 70 | // looplimit returns the JS variable name for the innermost loop index. 71 | func (s *scope) loopindex() string { 72 | return s.lookup("__index") 73 | } 74 | -------------------------------------------------------------------------------- /soymsg/id.go: -------------------------------------------------------------------------------- 1 | package soymsg 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/robfig/soy/ast" 9 | ) 10 | 11 | // calcID calculates the message ID for the given message node. 12 | // The ID changes if the text content or meaning attribute changes. 13 | // It is invariant to changes in description. 14 | func calcID(n *ast.MsgNode) uint64 { 15 | var buf bytes.Buffer 16 | writeFingerprint(&buf, n, false) 17 | 18 | var fp = fingerprint(buf.Bytes()) 19 | if n.Meaning != "" { 20 | var topbit uint64 21 | if fp&(1<<63) > 0 { 22 | topbit = 1 23 | } 24 | fp = (fp << 1) + topbit + fingerprint([]byte(n.Meaning)) 25 | } 26 | 27 | return fp & 0x7fffffffffffffff 28 | } 29 | 30 | // writeFingerprint writes the string used to fingerprint a message to the buffer. 31 | // if braces is true, the string written has placeholders surrounded by braces. 32 | // Plural messages always have braced placeholders. 33 | func writeFingerprint(buf *bytes.Buffer, part ast.Node, braces bool) { 34 | switch part := part.(type) { 35 | case *ast.MsgNode: 36 | for _, part := range part.Body.Children() { 37 | writeFingerprint(buf, part, braces) 38 | } 39 | case *ast.RawTextNode: 40 | buf.Write(part.Text) 41 | case *ast.MsgPlaceholderNode: 42 | if braces { 43 | buf.WriteString("{" + part.Name + "}") 44 | } else { 45 | buf.WriteString(part.Name) 46 | } 47 | case *ast.MsgPluralNode: 48 | buf.WriteString("{" + part.VarName + ",plural,") 49 | for _, plCase := range part.Cases { 50 | buf.WriteString("=" + strconv.Itoa(plCase.Value) + "{") 51 | for _, child := range plCase.Body.Children() { 52 | writeFingerprint(buf, child, true) 53 | } 54 | buf.WriteString("}") 55 | } 56 | buf.WriteString("other{") 57 | for _, child := range part.Default.Children() { 58 | writeFingerprint(buf, child, true) 59 | } 60 | buf.WriteString("}}") 61 | default: 62 | panic(fmt.Sprintf("unrecognized type %T", part)) 63 | } 64 | } 65 | 66 | // fingerprinting functions ported from official Soy, so that we end up with the 67 | // same message ids. 68 | 69 | func fingerprint(str []byte) uint64 { 70 | var hi = hash32(str, 0, len(str), 0) 71 | var lo = hash32(str, 0, len(str), 102072) 72 | if (hi == 0) && (lo == 0 || lo == 1) { 73 | // Turn 0/1 into another fingerprint 74 | hi ^= 0x130f9bef 75 | lo ^= 0x94a0a928 76 | } 77 | return (uint64(hi) << 32) | uint64(lo&0xffffffff) 78 | } 79 | 80 | func hash32(str []byte, start, limit int, c uint32) uint32 { 81 | var a uint32 = 0x9e3779b9 82 | var b uint32 = 0x9e3779b9 83 | 84 | var i int 85 | for i = start; i+12 <= limit; i += 12 { 86 | a += (uint32(str[i+0]&0xff) << 0) | 87 | (uint32(str[i+1]&0xff) << 8) | 88 | (uint32(str[i+2]&0xff) << 16) | 89 | (uint32(str[i+3]&0xff) << 24) 90 | b += (uint32(str[i+4]&0xff) << 0) | 91 | (uint32(str[i+5]&0xff) << 8) | 92 | (uint32(str[i+6]&0xff) << 16) | 93 | (uint32(str[i+7]&0xff) << 24) 94 | c += (uint32(str[i+8]&0xff) << 0) | 95 | (uint32(str[i+9]&0xff) << 8) | 96 | (uint32(str[i+10]&0xff) << 16) | 97 | (uint32(str[i+11]&0xff) << 24) 98 | 99 | // Mix. 100 | a -= b 101 | a -= c 102 | a ^= (c >> 13) 103 | b -= c 104 | b -= a 105 | b ^= (a << 8) 106 | c -= a 107 | c -= b 108 | c ^= (b >> 13) 109 | a -= b 110 | a -= c 111 | a ^= (c >> 12) 112 | b -= c 113 | b -= a 114 | b ^= (a << 16) 115 | c -= a 116 | c -= b 117 | c ^= (b >> 5) 118 | a -= b 119 | a -= c 120 | a ^= (c >> 3) 121 | b -= c 122 | b -= a 123 | b ^= (a << 10) 124 | c -= a 125 | c -= b 126 | c ^= (b >> 15) 127 | } 128 | 129 | c += uint32(limit - start) 130 | switch limit - i { // Deal with rest. Cases fall through. 131 | case 11: 132 | c += uint32(str[i+10]&0xff) << 24 133 | fallthrough 134 | case 10: 135 | c += uint32(str[i+9]&0xff) << 16 136 | fallthrough 137 | case 9: 138 | c += uint32(str[i+8]&0xff) << 8 139 | // the first byte of c is reserved for the length 140 | fallthrough 141 | case 8: 142 | b += uint32(str[i+7]&0xff) << 24 143 | fallthrough 144 | case 7: 145 | b += uint32(str[i+6]&0xff) << 16 146 | fallthrough 147 | case 6: 148 | b += uint32(str[i+5]&0xff) << 8 149 | fallthrough 150 | case 5: 151 | b += uint32(str[i+4] & 0xff) 152 | fallthrough 153 | case 4: 154 | a += uint32(str[i+3]&0xff) << 24 155 | fallthrough 156 | case 3: 157 | a += uint32(str[i+2]&0xff) << 16 158 | fallthrough 159 | case 2: 160 | a += uint32(str[i+1]&0xff) << 8 161 | fallthrough 162 | case 1: 163 | a += uint32(str[i+0] & 0xff) 164 | // case 0 : nothing left to add 165 | } 166 | 167 | // Mix. 168 | a -= b 169 | a -= c 170 | a ^= (c >> 13) 171 | b -= c 172 | b -= a 173 | b ^= (a << 8) 174 | c -= a 175 | c -= b 176 | c ^= (b >> 13) 177 | a -= b 178 | a -= c 179 | a ^= (c >> 12) 180 | b -= c 181 | b -= a 182 | b ^= (a << 16) 183 | c -= a 184 | c -= b 185 | c ^= (b >> 5) 186 | a -= b 187 | a -= c 188 | a ^= (c >> 3) 189 | b -= c 190 | b -= a 191 | b ^= (a << 10) 192 | c -= a 193 | c -= b 194 | c ^= (b >> 15) 195 | 196 | return c 197 | } 198 | -------------------------------------------------------------------------------- /soymsg/placeholder.go: -------------------------------------------------------------------------------- 1 | package soymsg 2 | 3 | import ( 4 | "bytes" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/robfig/soy/ast" 10 | ) 11 | 12 | // setPlaceholderNames generates the placeholder names for all children of the 13 | // given message node, setting the .Name property on them. 14 | func setPlaceholderNames(n *ast.MsgNode) { 15 | // Step 1: Determine representative nodes and build preliminary map 16 | var ( 17 | baseNameToRepNodes = make(map[string][]ast.Node) 18 | equivNodeToRepNodes = make(map[ast.Node]ast.Node) 19 | ) 20 | 21 | var nodeQueue []ast.Node = phNodes(n.Body) 22 | for len(nodeQueue) > 0 { 23 | var node = nodeQueue[0] 24 | nodeQueue = nodeQueue[1:] 25 | 26 | var baseName string 27 | switch node := node.(type) { 28 | case *ast.MsgPlaceholderNode: 29 | baseName = genBasePlaceholderName(node.Body, "XXX") 30 | case *ast.MsgPluralNode: 31 | nodeQueue = append(nodeQueue, pluralCaseBodies(node)...) 32 | baseName = genBasePlaceholderName(node.Value, "NUM") 33 | default: 34 | panic("unexpected") 35 | } 36 | 37 | if nodes, ok := baseNameToRepNodes[baseName]; !ok { 38 | baseNameToRepNodes[baseName] = []ast.Node{node} 39 | } else { 40 | var isNew = true 41 | var str = node.String() 42 | for _, other := range nodes { 43 | if other.String() == str { 44 | equivNodeToRepNodes[node] = other 45 | isNew = false 46 | break 47 | } 48 | } 49 | if isNew { 50 | baseNameToRepNodes[baseName] = append(nodes, node) 51 | } 52 | } 53 | } 54 | 55 | // Step 2: Build final maps of name to representative node 56 | var nameToRepNodes = make(map[string]ast.Node) 57 | for baseName, nodes := range baseNameToRepNodes { 58 | if len(nodes) == 1 { 59 | nameToRepNodes[baseName] = nodes[0] 60 | continue 61 | } 62 | 63 | var nextSuffix = 1 64 | for _, node := range nodes { 65 | for { 66 | var newName = baseName + "_" + strconv.Itoa(nextSuffix) 67 | if _, ok := nameToRepNodes[newName]; !ok { 68 | nameToRepNodes[newName] = node 69 | break 70 | } 71 | nextSuffix++ 72 | } 73 | } 74 | } 75 | 76 | // Step 3: Create maps of every node to its name 77 | var nodeToName = make(map[ast.Node]string) 78 | for name, node := range nameToRepNodes { 79 | nodeToName[node] = name 80 | } 81 | for other, repNode := range equivNodeToRepNodes { 82 | nodeToName[other] = nodeToName[repNode] 83 | } 84 | 85 | // Step 4: Set the calculated names on all the nodes. 86 | for node, name := range nodeToName { 87 | switch node := node.(type) { 88 | case *ast.MsgPlaceholderNode: 89 | node.Name = name 90 | case *ast.MsgPluralNode: 91 | node.VarName = name 92 | default: 93 | panic("unexpected: " + node.String()) 94 | } 95 | } 96 | } 97 | 98 | func phNodes(n ast.ParentNode) []ast.Node { 99 | var nodeQueue []ast.Node 100 | for _, child := range n.Children() { 101 | switch child := child.(type) { 102 | case *ast.MsgPlaceholderNode, *ast.MsgPluralNode: 103 | nodeQueue = append(nodeQueue, child) 104 | } 105 | } 106 | return nodeQueue 107 | } 108 | 109 | func pluralCaseBodies(node *ast.MsgPluralNode) []ast.Node { 110 | var r []ast.Node 111 | for _, plCase := range node.Cases { 112 | r = append(r, phNodes(plCase.Body)...) 113 | } 114 | return append(r, phNodes(node.Default)...) 115 | } 116 | 117 | func genBasePlaceholderName(node ast.Node, defaultName string) string { 118 | // TODO: user supplied placeholder (phname) 119 | switch part := node.(type) { 120 | case *ast.PrintNode: 121 | return genBasePlaceholderNameFromExpr(part.Arg, defaultName) 122 | case *ast.MsgHtmlTagNode: 123 | return genBasePlaceholderNameFromHtml(part) 124 | case *ast.DataRefNode: 125 | return genBasePlaceholderNameFromExpr(node, defaultName) 126 | } 127 | return defaultName 128 | } 129 | 130 | func genBasePlaceholderNameFromExpr(expr ast.Node, defaultName string) string { 131 | switch expr := expr.(type) { 132 | case *ast.GlobalNode: 133 | return toUpperUnderscore(expr.Name) 134 | case *ast.DataRefNode: 135 | if len(expr.Access) == 0 { 136 | return toUpperUnderscore(expr.Key) 137 | } 138 | var lastChild = expr.Access[len(expr.Access)-1] 139 | if lastChild, ok := lastChild.(*ast.DataRefKeyNode); ok { 140 | return toUpperUnderscore(lastChild.Key) 141 | } 142 | } 143 | return defaultName 144 | } 145 | 146 | var htmlTagNames = map[string]string{ 147 | "a": "link", 148 | "br": "break", 149 | "b": "bold", 150 | "i": "italic", 151 | "li": "item", 152 | "ol": "ordered_list", 153 | "ul": "unordered_list", 154 | "p": "paragraph", 155 | "img": "image", 156 | "em": "emphasis", 157 | } 158 | 159 | func genBasePlaceholderNameFromHtml(node *ast.MsgHtmlTagNode) string { 160 | var tag, tagType = tagName(node.Text) 161 | if prettyName, ok := htmlTagNames[tag]; ok { 162 | tag = prettyName 163 | } 164 | return toUpperUnderscore(tagType + tag) 165 | } 166 | 167 | func tagName(text []byte) (name, tagType string) { 168 | switch { 169 | case bytes.HasPrefix(text, []byte("")): 172 | tagType = "" 173 | default: 174 | tagType = "START_" 175 | } 176 | 177 | text = bytes.TrimPrefix(text, []byte("<")) 178 | text = bytes.TrimPrefix(text, []byte("/")) 179 | for i, ch := range text { 180 | if !isAlphaNumeric(ch) { 181 | return strings.ToLower(string(text[:i])), tagType 182 | } 183 | } 184 | // the parser should never produce html tag nodes that tagName can't handle. 185 | panic("no tag name found: " + string(text)) 186 | } 187 | 188 | func isAlphaNumeric(r byte) bool { 189 | return 'A' <= r && r <= 'Z' || 190 | 'a' <= r && r <= 'z' || 191 | '0' <= r && r <= '9' 192 | } 193 | 194 | var ( 195 | leadingOrTrailing_ = regexp.MustCompile("^_+|_+$") 196 | consecutive_ = regexp.MustCompile("__+") 197 | wordBoundary1 = regexp.MustCompile("([a-zA-Z])([A-Z][a-z])") // _ 198 | wordBoundary2 = regexp.MustCompile("([a-zA-Z])([0-9])") // _ 199 | wordBoundary3 = regexp.MustCompile("([0-9])([a-zA-Z])") // _ 200 | ) 201 | 202 | func toUpperUnderscore(ident string) string { 203 | ident = leadingOrTrailing_.ReplaceAllString(ident, "") 204 | ident = consecutive_.ReplaceAllString(ident, "${1}_${2}") 205 | ident = wordBoundary1.ReplaceAllString(ident, "${1}_${2}") 206 | ident = wordBoundary2.ReplaceAllString(ident, "${1}_${2}") 207 | ident = wordBoundary3.ReplaceAllString(ident, "${1}_${2}") 208 | return strings.ToUpper(ident) 209 | } 210 | -------------------------------------------------------------------------------- /soymsg/placeholder_test.go: -------------------------------------------------------------------------------- 1 | package soymsg 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/robfig/soy/ast" 7 | "github.com/robfig/soy/parse" 8 | ) 9 | 10 | func TestSetPlaceholders(t *testing.T) { 11 | type test struct { 12 | node *ast.MsgNode 13 | phstr string 14 | } 15 | 16 | var tests = []test{ 17 | {newMsg("Hello world"), "Hello world"}, 18 | 19 | // Data refs 20 | {newMsg("Hello {$name}"), "Hello {NAME}"}, 21 | {newMsg("{$a}, {$b}, and {$c}"), "{A}, {B}, and {C}"}, 22 | {newMsg("{$a} {$a}"), "{A} {A}"}, 23 | {newMsg("{$a} {$b.a}"), "{A_1} {A_2}"}, 24 | {newMsg("{$a.a}{$a.b.a}"), "{A_1}{A_2}"}, 25 | 26 | // Command sequences 27 | {newMsg("hello{sp}world"), "hello world"}, 28 | 29 | // HTML 30 | {newMsg("Click
here"), "Click {START_LINK}here{END_LINK}"}, 31 | {newMsg("


"), "{START_BREAK}{BREAK}{BREAK}"}, 32 | {newMsg("Click here"), 33 | "{START_LINK_1}Click{END_LINK_1} {START_LINK_2}here{END_LINK_2}"}, 34 | {newMsg("

P1

P2

P3

"), 35 | "{START_PARAGRAPH}P1{END_PARAGRAPH}{START_PARAGRAPH}P2{END_PARAGRAPH}{START_PARAGRAPH}P3{END_PARAGRAPH}"}, 36 | 37 | // BUG: Data refs + HTML 38 | // {newMsg("Click"), "{START_LINK}Click{END_LINK}"}, 39 | 40 | // TODO: phname 41 | 42 | // TODO: investigate globals 43 | // {newMsg("{GLOBAL}"), "{GLOBAL}"}, 44 | // {newMsg("{sub.global}"), "{GLOBAL}"}, 45 | } 46 | 47 | for _, test := range tests { 48 | var actual = PlaceholderString(test.node) 49 | if actual != test.phstr { 50 | t.Errorf("(actual) %v != %v (expected)", actual, test.phstr) 51 | } 52 | } 53 | } 54 | 55 | func TestSetPluralVarName(t *testing.T) { 56 | type test struct { 57 | node *ast.MsgNode 58 | varname string 59 | } 60 | 61 | var tests = []test{ 62 | {newMsg("{plural $eggs}{case 1}one{default}other{/plural}"), "EGGS"}, 63 | {newMsg("{plural $eggs}{case 1}one{default}{$eggs}{/plural}"), "EGGS_1"}, 64 | {newMsg("{plural length($eggs)}{case 1}one{default}other{/plural}"), "NUM"}, 65 | } 66 | 67 | for _, test := range tests { 68 | var actual = test.node.Body.Children()[0].(*ast.MsgPluralNode).VarName 69 | if actual != test.varname { 70 | t.Errorf("(actual) %v != %v (expected)", actual, test.varname) 71 | } 72 | } 73 | } 74 | 75 | func newMsg(msg string) *ast.MsgNode { 76 | // TODO: data.Map{"GLOBAL": data.Int(1), "sub.global": data.Int(2)}) 77 | var sf, err = parse.SoyFile("", `{msg desc=""}`+msg+`{/msg}`) 78 | if err != nil { 79 | panic(err) 80 | } 81 | var msgnode = sf.Body[0].(*ast.MsgNode) 82 | SetPlaceholdersAndID(msgnode) 83 | return msgnode 84 | } 85 | 86 | func TestBaseName(t *testing.T) { 87 | type test struct { 88 | expr string 89 | ph string 90 | } 91 | var tests = []test{ 92 | {"$foo", "FOO"}, 93 | {"$foo.boo", "BOO"}, 94 | {"$foo.boo[0].zoo", "ZOO"}, 95 | {"$foo.boo.0.zoo", "ZOO"}, 96 | 97 | // parse.Expr doesn't accept undefined globals. 98 | // {"GLOBAL", "GLOBAL"}, 99 | // {"sub.GLOBAL", "GLOBAL"}, 100 | 101 | {"$foo[0]", "XXX"}, 102 | {"$foo.boo[0]", "XXX"}, 103 | {"$foo.boo.0", "XXX"}, 104 | {"$foo + 1", "XXX"}, 105 | {"'text'", "XXX"}, 106 | {"max(1, 3)", "XXX"}, 107 | } 108 | 109 | for _, test := range tests { 110 | var n, err = parse.Expr(test.expr) 111 | if err != nil { 112 | t.Error(err) 113 | return 114 | } 115 | 116 | var actual = genBasePlaceholderName(&ast.PrintNode{0, n, nil}, "XXX") 117 | if actual != test.ph { 118 | t.Errorf("(actual) %v != %v (expected)", actual, test.ph) 119 | } 120 | } 121 | } 122 | 123 | func TestToUpperUnderscore(t *testing.T) { 124 | var tests = []struct{ in, out string }{ 125 | {"booFoo", "BOO_FOO"}, 126 | {"_booFoo", "BOO_FOO"}, 127 | {"booFoo_", "BOO_FOO"}, 128 | {"BooFoo", "BOO_FOO"}, 129 | {"boo_foo", "BOO_FOO"}, 130 | {"BOO_FOO", "BOO_FOO"}, 131 | {"__BOO__FOO__", "BOO_FOO"}, 132 | {"Boo_Foo", "BOO_FOO"}, 133 | {"boo8Foo", "BOO_8_FOO"}, 134 | {"booFoo88", "BOO_FOO_88"}, 135 | {"boo88_foo", "BOO_88_FOO"}, 136 | {"_boo_8foo", "BOO_8_FOO"}, 137 | {"boo_foo8", "BOO_FOO_8"}, 138 | {"_BOO__8_FOO_", "BOO_8_FOO"}, 139 | } 140 | for _, test := range tests { 141 | var actual = toUpperUnderscore(test.in) 142 | if actual != test.out { 143 | t.Errorf("(actual) %v != %v (expected)", actual, test.out) 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /soymsg/pomsg/fallback.go: -------------------------------------------------------------------------------- 1 | package pomsg 2 | 3 | import ( 4 | "golang.org/x/text/language" 5 | ) 6 | 7 | // fallbacks returns a slice of tags that can be substituted for a tag, ordered by increasing 8 | // generality. 9 | // TODO: potentially support extensions and variants 10 | func fallbacks(tag language.Tag) []language.Tag { 11 | result := []language.Tag{} 12 | lang, script, region := tag.Raw() 13 | // The language package returns ZZ for an unspecified region, similar quirk for script. 14 | if region.String() != "ZZ" { 15 | t, _ := language.Compose(lang, script, region) 16 | result = append(result, t) 17 | } 18 | if script.String() != "Zzzz" { 19 | t, _ := language.Compose(lang, script) 20 | result = append(result, t) 21 | } 22 | t, _ := language.Compose(lang) 23 | result = append(result, t) 24 | return result 25 | } 26 | -------------------------------------------------------------------------------- /soymsg/pomsg/fallback_test.go: -------------------------------------------------------------------------------- 1 | package pomsg 2 | 3 | import ( 4 | "testing" 5 | "golang.org/x/text/language" 6 | ) 7 | 8 | func TestFallback(t *testing.T) { 9 | tests := []struct{ 10 | name string 11 | tag language.Tag 12 | expectedTags []language.Tag 13 | }{ 14 | { 15 | name: "When given a generic locale code, no extra fallbacks are provided", 16 | tag: language.MustParse("en"), 17 | expectedTags: []language.Tag{language.English}, 18 | }, 19 | { 20 | name: "When given a regional locale code, generic fallback is provided", 21 | tag: language.MustParse("en_US"), 22 | expectedTags: []language.Tag{language.AmericanEnglish, language.English}, 23 | }, 24 | { 25 | name: "When given a locale code with script, generic fallback is provided", 26 | tag: language.MustParse("ar_Arab"), 27 | expectedTags: []language.Tag{ 28 | language.MustParse("ar_Arab"), 29 | language.Arabic, 30 | }, 31 | }, 32 | { 33 | name: "When given a locale code with script and region, generic and script fallbacks are provided", 34 | tag: language.MustParse("ar_Arab_EG"), 35 | expectedTags: []language.Tag{ 36 | language.MustParse("ar_Arab_EG"), 37 | language.MustParse("ar_Arab"), 38 | language.Arabic, 39 | }, 40 | }, 41 | { 42 | name: "When given an empty tag, no fallbacks are provided", 43 | tag: language.Tag{}, 44 | expectedTags: []language.Tag{}, 45 | }, 46 | } 47 | 48 | for _, test := range tests { 49 | t.Run(test.name, func(t *testing.T) { 50 | fb := fallbacks(test.tag) 51 | 52 | for i, tag := range test.expectedTags { 53 | if fb[i] != tag { 54 | t.Errorf("Expected tag %+v, got tag %+v", tag, fb[i]) 55 | } 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /soymsg/pomsg/msgid.go: -------------------------------------------------------------------------------- 1 | package pomsg 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/robfig/soy/ast" 8 | ) 9 | 10 | // Validate checks if the given message is representable in a PO file. 11 | // A MsgNode must be validated before trying to caculate its msgid or msgid_plural 12 | // 13 | // Rules: 14 | // - If a message contains a plural, it must be the sole child. 15 | // - A plural contains exactly {case 1} and {default} cases. 16 | func Validate(n *ast.MsgNode) error { 17 | for i, child := range n.Body.Children() { 18 | if n, ok := child.(*ast.MsgPluralNode); ok { 19 | if i != 0 { 20 | return fmt.Errorf("plural node must be the sole child") 21 | } 22 | if len(n.Cases) != 1 || n.Cases[0].Value != 1 { 23 | return fmt.Errorf("PO requires two plural cases [1, default]. found %v", n.Cases) 24 | } 25 | } 26 | } 27 | return nil 28 | } 29 | 30 | // MsgId returns the msgid for the given msg node. 31 | func Msgid(n *ast.MsgNode) string { 32 | return msgidn(n, true) 33 | } 34 | 35 | // MsgidPlural returns the msgid_plural for the given message. 36 | func MsgidPlural(n *ast.MsgNode) string { 37 | return msgidn(n, false) 38 | } 39 | 40 | func msgidn(n *ast.MsgNode, singular bool) string { 41 | var body = n.Body 42 | var children = body.Children() 43 | if len(children) == 0 { 44 | return "" 45 | } 46 | if pluralNode, ok := children[0].(*ast.MsgPluralNode); ok { 47 | body = pluralCase(pluralNode, singular) 48 | } else if !singular { 49 | return "" 50 | } 51 | var buf bytes.Buffer 52 | for _, child := range body.Children() { 53 | writeph(&buf, child) 54 | } 55 | return buf.String() 56 | } 57 | 58 | // pluralCase returns the singular or plural message body. 59 | func pluralCase(n *ast.MsgPluralNode, singular bool) ast.ParentNode { 60 | if singular { 61 | return n.Cases[0].Body 62 | } 63 | return n.Default 64 | } 65 | 66 | // writeph writes the placeholder string for the given node to the given buffer. 67 | func writeph(buf *bytes.Buffer, child ast.Node) { 68 | switch child := child.(type) { 69 | case *ast.RawTextNode: 70 | buf.Write(child.Text) 71 | case *ast.MsgPlaceholderNode: 72 | buf.WriteString("{" + child.Name + "}") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /soymsg/pomsg/msgid_test.go: -------------------------------------------------------------------------------- 1 | package pomsg 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/robfig/soy/ast" 8 | "github.com/robfig/soy/parse" 9 | "github.com/robfig/soy/soymsg" 10 | ) 11 | 12 | func TestValidate(t *testing.T) { 13 | type test struct { 14 | msg *ast.MsgNode 15 | validates bool 16 | } 17 | var tests = []test{ 18 | {msg(""), true}, 19 | {msg("hello world"), true}, 20 | {msg("{plural $n}{case 1}one{default}other{/plural}"), true}, 21 | {msg("{plural $n}{default}other{/plural}"), false}, 22 | {msg("{plural $n}{case 2}two{default}other{/plural}"), false}, 23 | } 24 | 25 | for _, test := range tests { 26 | var err = Validate(test.msg) 27 | switch { 28 | case test.validates && err != nil: 29 | t.Errorf("should validate, but got %v: %v", err, test.msg) 30 | case !test.validates && err == nil: 31 | t.Errorf("should fail, but didn't: %v", test.msg) 32 | } 33 | } 34 | } 35 | 36 | func TestMsgId(t *testing.T) { 37 | type test struct { 38 | msg *ast.MsgNode 39 | msgid string 40 | msgidPlural string 41 | } 42 | var tests = []test{ 43 | {msg(""), "", ""}, 44 | {msg("hello world"), "hello world", ""}, 45 | {msg("{plural length($users)}{case 1}one{default}other{/plural}"), "one", "other"}, 46 | {msg("{plural length($users)}{case 1}one{default}{length($users)} users{/plural}"), 47 | "one", "{XXX} users"}, 48 | } 49 | 50 | for _, test := range tests { 51 | var ( 52 | msgid = Msgid(test.msg) 53 | msgidPlural = MsgidPlural(test.msg) 54 | ) 55 | if msgid != test.msgid { 56 | t.Errorf("(actual) %v != %v (expected)", msgid, test.msgid) 57 | } 58 | if msgidPlural != test.msgidPlural { 59 | t.Errorf("(actual) %v != %v (expected)", msgidPlural, test.msgidPlural) 60 | } 61 | } 62 | } 63 | 64 | func msg(body string) *ast.MsgNode { 65 | var msgtmpl = fmt.Sprintf(`{msg desc=""}%s{/msg}`, body) 66 | var sf, err = parse.SoyFile("", msgtmpl) 67 | if err != nil { 68 | panic(err) 69 | } 70 | var msgnode = sf.Body[0].(*ast.MsgNode) 71 | soymsg.SetPlaceholdersAndID(msgnode) 72 | return msgnode 73 | } 74 | -------------------------------------------------------------------------------- /soymsg/pomsg/pomsg.go: -------------------------------------------------------------------------------- 1 | // Package pomsg provides a PO file implementation for Soy message bundles 2 | package pomsg 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/robfig/gettext/po" 14 | "github.com/robfig/soy/soymsg" 15 | "golang.org/x/text/language" 16 | ) 17 | 18 | type provider struct { 19 | bundles map[string]soymsg.Bundle 20 | } 21 | 22 | // FileOpener defines an abstraction for opening a po file given a locale 23 | type FileOpener interface { 24 | // Open returns ReadCloser for the po file indicated by locale. It returns 25 | // nil if the file does not exist 26 | Open(locale string) (io.ReadCloser, error) 27 | } 28 | 29 | // Load returns a soymsg.Provider that takes its translations by passing in the 30 | // specified locales to the given PoFileProvider. 31 | // 32 | // Supports fallbacks for when a given locale does not exist, as long as the fallback files are in 33 | // canonical form. 34 | func Load(opener FileOpener, locales []string) (soymsg.Provider, error) { 35 | var prov = provider{make(map[string]soymsg.Bundle)} 36 | for _, locale := range locales { 37 | r, err := opener.Open(locale) 38 | if err != nil { 39 | return nil, err 40 | } else if r == nil { 41 | continue 42 | } 43 | 44 | pofile, err := po.Parse(r) 45 | r.Close() 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | b, err := newBundle(locale, pofile) 51 | if err != nil { 52 | return nil, err 53 | } 54 | prov.bundles[locale] = b 55 | } 56 | return prov, nil 57 | } 58 | 59 | // fsFileOpener is a FileOpener based on the filesystem and rooted at Dirname 60 | type fsFileOpener struct { 61 | Dirname string 62 | } 63 | 64 | func (o fsFileOpener) Open(locale string) (io.ReadCloser, error) { 65 | switch f, err := os.Open(path.Join(o.Dirname, locale+".po")); { 66 | case os.IsNotExist(err): 67 | return nil, nil 68 | case err != nil: 69 | return nil, err 70 | default: 71 | return f, nil 72 | } 73 | } 74 | 75 | // Dir returns a soymsg.Provider that takes translations from the given path. 76 | // For example, if dir is "/usr/local/msgs", po files should be of the form: 77 | // /usr/local/msgs/.po 78 | // /usr/local/msgs/_.po 79 | func Dir(dirname string) (soymsg.Provider, error) { 80 | var files, err = ioutil.ReadDir(dirname) 81 | if err != nil { 82 | return nil, err 83 | } 84 | var locales []string 85 | for _, fi := range files { 86 | var name = fi.Name() 87 | if !fi.IsDir() && strings.HasSuffix(name, ".po") { 88 | locales = append(locales, name[:len(name)-3]) 89 | } 90 | } 91 | return Load(fsFileOpener{dirname}, locales) 92 | } 93 | 94 | func (p provider) Bundle(locale string) soymsg.Bundle { 95 | bundle, ok := p.bundles[locale] 96 | if !ok { 97 | tag, err := language.Parse(locale) 98 | if err != nil { 99 | return nil 100 | } 101 | for _, fb := range fallbacks(tag) { 102 | bundle, ok = p.bundles[fb.String()] 103 | if ok { 104 | break 105 | } 106 | } 107 | } 108 | return bundle 109 | } 110 | 111 | type bundle struct { 112 | messages map[uint64]soymsg.Message 113 | locale string 114 | pluralize po.PluralSelector 115 | } 116 | 117 | func newBundle(locale string, file po.File) (*bundle, error) { 118 | var pluralize = file.Pluralize 119 | if pluralize == nil { 120 | pluralize = po.PluralSelectorForLanguage(locale) 121 | } 122 | if pluralize == nil { 123 | return nil, fmt.Errorf("Plural-Forms must be specified") 124 | } 125 | 126 | var err error 127 | var msgs = make(map[uint64]soymsg.Message) 128 | for _, msg := range file.Messages { 129 | // Get the Message ID and plural var name 130 | var id uint64 131 | var varName string 132 | for _, ref := range msg.References { 133 | switch { 134 | case strings.HasPrefix(ref, "id="): 135 | id, err = strconv.ParseUint(ref[3:], 10, 64) 136 | if err != nil { 137 | return nil, err 138 | } 139 | case strings.HasPrefix(ref, "var="): 140 | varName = ref[len("var="):] 141 | } 142 | } 143 | if id == 0 { 144 | return nil, fmt.Errorf("no id found in message: %#v", msg) 145 | } 146 | msgs[id] = newMessage(id, varName, msg.Str) 147 | } 148 | return &bundle{msgs, locale, pluralize}, nil 149 | } 150 | 151 | func (b *bundle) Message(id uint64) *soymsg.Message { 152 | var msg, ok = b.messages[id] 153 | if !ok { 154 | return nil 155 | } 156 | return &msg 157 | } 158 | 159 | func (b *bundle) Locale() string { 160 | return b.locale 161 | } 162 | 163 | func (b *bundle) PluralCase(n int) int { 164 | return b.pluralize(n) 165 | } 166 | 167 | func newMessage(id uint64, varName string, msgstrs []string) soymsg.Message { 168 | if varName == "" && len(msgstrs) == 1 { 169 | return soymsg.Message{id, soymsg.Parts(msgstrs[0])} 170 | } 171 | 172 | var cases []soymsg.PluralCase 173 | for _, msgstr := range msgstrs { 174 | // TODO: Ideally this would convert from PO plural form to CLDR plural class. 175 | // Instead, just use PluralCase() to select one of these. 176 | cases = append(cases, soymsg.PluralCase{ 177 | Spec: soymsg.PluralSpec{soymsg.PluralSpecOther, -1}, // not used 178 | Parts: soymsg.Parts(msgstr), 179 | }) 180 | } 181 | return soymsg.Message{id, []soymsg.Part{soymsg.PluralPart{ 182 | VarName: varName, 183 | Cases: cases, 184 | }}} 185 | } 186 | -------------------------------------------------------------------------------- /soymsg/pomsg/pomsg_test.go: -------------------------------------------------------------------------------- 1 | package pomsg 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/robfig/soy/soymsg" 8 | ) 9 | 10 | func TestPOBundle(t *testing.T) { 11 | var pomsgs, err = Dir("testdata") 12 | if err != nil { 13 | t.Error(err) 14 | return 15 | } 16 | locales := []string{"en", "en_UK", "zz"} 17 | for _, locale := range locales { 18 | var bundle = pomsgs.Bundle(locale) 19 | var tests = []struct { 20 | id uint64 21 | str []string 22 | }{ 23 | {3329840836245051515, []string{"zA ztrip zwas ztaken."}}, 24 | {6936162475751860807, []string{"zHello z{NAME}!"}}, 25 | {7224011416745566687, []string{"zArchiveNoun"}}, 26 | {4826315192146469447, []string{"zArchiveVerb"}}, 27 | {1234567890123456789, []string{}}, 28 | {176798647517908084, []string{ 29 | "zYou zhave zone zegg", 30 | "zYou zhave z{$EGGS_2} zeggs", 31 | "zYou zhave ztwo zeggs", 32 | }}, 33 | } 34 | 35 | for _, test := range tests { 36 | var actual = bundle.Message(test.id) 37 | if actual == nil { 38 | if len(test.str) == 0 { 39 | continue 40 | } 41 | t.Errorf("msg not found: %v", test.id) 42 | } 43 | 44 | var pluralVar = "" 45 | if len(test.str) > 1 { 46 | pluralVar = "EGGS_1" 47 | } 48 | var expected = newMessage(test.id, pluralVar, test.str) 49 | if !reflect.DeepEqual(&expected, actual) { 50 | t.Errorf("expected:\n%v\ngot:\n%v", expected, actual) 51 | } 52 | } 53 | } 54 | } 55 | 56 | func TestPOBundleNotFound(t *testing.T) { 57 | var pomsgs, err = Dir("testdata") 58 | if err != nil { 59 | t.Error(err) 60 | return 61 | } 62 | 63 | var bundle = pomsgs.Bundle("xx") 64 | if bundle != nil { 65 | t.Errorf("expected null bundle, got %#v", bundle) 66 | } 67 | 68 | bundle = pomsgs.Bundle("es") 69 | if bundle != nil { 70 | t.Errorf("expected null bundle, got %#v", bundle) 71 | } 72 | } 73 | 74 | func TestPlural(t *testing.T) { 75 | var pomsgs, err = Dir("testdata") 76 | if err != nil { 77 | t.Error(err) 78 | return 79 | } 80 | 81 | const locale = "zz" 82 | var bundle = pomsgs.Bundle(locale) 83 | if bundle.Locale() != locale { 84 | t.Errorf("actual %v != %v expected", bundle.Locale(), locale) 85 | } 86 | 87 | type test struct{ n, r int } 88 | var tests = []test{ 89 | {1, 0}, 90 | {2, 1}, 91 | {3, 2}, 92 | {0, 2}, 93 | } 94 | for _, test := range tests { 95 | var actual = bundle.PluralCase(test.n) 96 | if actual != test.r { 97 | t.Errorf("actual %v != %v expected", actual, test.n) 98 | } 99 | } 100 | } 101 | 102 | func TestNewMessage(t *testing.T) { 103 | var tests = []struct { 104 | id uint64 105 | varName string 106 | msgstrs []string 107 | expected soymsg.Message 108 | }{ 109 | { 110 | id: 3329840836245051515, 111 | varName: "", 112 | msgstrs: []string{"zA ztrip zwas ztaken."}, 113 | expected: soymsg.Message{ 114 | ID: 0x2e35f8992af9d87b, 115 | Parts: []soymsg.Part{soymsg.RawTextPart{Text: "zA ztrip zwas ztaken."}}, 116 | }, 117 | }, 118 | { 119 | id: 176798647517908084, 120 | varName: "EGGS_1", 121 | msgstrs: []string{"zYou zhave zone zegg", "zYou zhave z{$EGGS_2} zeggs", "zYou zhave ztwo zeggs"}, 122 | expected: soymsg.Message{ 123 | ID: 0x2741d6ee6130c74, 124 | Parts: []soymsg.Part{ 125 | soymsg.PluralPart{ 126 | VarName: "EGGS_1", 127 | Cases: []soymsg.PluralCase{ 128 | { 129 | Spec: soymsg.PluralSpec{Type: soymsg.PluralSpecOther, ExplicitValue: -1}, 130 | Parts: []soymsg.Part{soymsg.RawTextPart{Text: "zYou zhave zone zegg"}}, 131 | }, 132 | { 133 | Spec: soymsg.PluralSpec{Type: soymsg.PluralSpecOther, ExplicitValue: -1}, 134 | Parts: []soymsg.Part{soymsg.RawTextPart{Text: "zYou zhave z{$EGGS_2} zeggs"}}, 135 | }, 136 | { 137 | Spec: soymsg.PluralSpec{Type: soymsg.PluralSpecOther, ExplicitValue: -1}, 138 | Parts: []soymsg.Part{soymsg.RawTextPart{Text: "zYou zhave ztwo zeggs"}}, 139 | }, 140 | }, 141 | }, 142 | }, 143 | }, 144 | }, 145 | } 146 | 147 | for _, test := range tests { 148 | var actual = newMessage(test.id, test.varName, test.msgstrs) 149 | if !reflect.DeepEqual(test.expected, actual) { 150 | t.Errorf("expected:\n%v\ngot:\n%#v", test.expected, actual) 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /soymsg/pomsg/testdata/en.po: -------------------------------------------------------------------------------- 1 | # Test data 2 | msgid "" 3 | msgstr "" 4 | "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;\n" 5 | 6 | #. 7 | #: id=3329840836245051515 8 | msgid "A trip was taken." 9 | msgstr "zA ztrip zwas ztaken." 10 | 11 | #. Says hello to a person. 12 | #: id=6936162475751860807 13 | msgid "Hello {NAME}!" 14 | msgstr "zHello z{NAME}!" 15 | 16 | #. The word 'Archive' used as a noun, i.e. an information store. 17 | #: id=7224011416745566687 18 | msgctxt "noun" 19 | msgid "Archive" 20 | msgstr "zArchiveNoun" 21 | 22 | #. The word 'Archive' used as a verb, i.e. to store information. 23 | #: id=4826315192146469447 24 | msgctxt "verb" 25 | msgid "Archive" 26 | msgstr "zArchiveVerb" 27 | 28 | #: id=176798647517908084 var=EGGS_1 29 | msgid "You have one egg" 30 | msgid_plural "You have {EGGS_2} eggs" 31 | msgstr[0] "zYou zhave zone zegg" 32 | msgstr[1] "zYou zhave z{$EGGS_2} zeggs" 33 | msgstr[2] "zYou zhave ztwo zeggs" 34 | -------------------------------------------------------------------------------- /soymsg/pomsg/testdata/zz.po: -------------------------------------------------------------------------------- 1 | # Test data 2 | msgid "" 3 | msgstr "" 4 | "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;\n" 5 | 6 | #. 7 | #: id=3329840836245051515 8 | msgid "A trip was taken." 9 | msgstr "zA ztrip zwas ztaken." 10 | 11 | #. Says hello to a person. 12 | #: id=6936162475751860807 13 | msgid "Hello {NAME}!" 14 | msgstr "zHello z{NAME}!" 15 | 16 | #. The word 'Archive' used as a noun, i.e. an information store. 17 | #: id=7224011416745566687 18 | msgctxt "noun" 19 | msgid "Archive" 20 | msgstr "zArchiveNoun" 21 | 22 | #. The word 'Archive' used as a verb, i.e. to store information. 23 | #: id=4826315192146469447 24 | msgctxt "verb" 25 | msgid "Archive" 26 | msgstr "zArchiveVerb" 27 | 28 | #: id=176798647517908084 var=EGGS_1 29 | msgid "You have one egg" 30 | msgid_plural "You have {EGGS_2} eggs" 31 | msgstr[0] "zYou zhave zone zegg" 32 | msgstr[1] "zYou zhave z{$EGGS_2} zeggs" 33 | msgstr[2] "zYou zhave ztwo zeggs" 34 | -------------------------------------------------------------------------------- /soymsg/pomsg/xgettext-soy/main.go: -------------------------------------------------------------------------------- 1 | // xgettext-soy is a tool to extract messages from Soy templates in the PO 2 | // (gettext) file format. 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/robfig/gettext/po" 13 | "github.com/robfig/soy/ast" 14 | "github.com/robfig/soy/parse" 15 | "github.com/robfig/soy/parsepasses" 16 | "github.com/robfig/soy/soymsg/pomsg" 17 | "github.com/robfig/soy/template" 18 | ) 19 | 20 | func usage() { 21 | fmt.Fprint(os.Stderr, `xgettext-soy is a tool to extract messages from Soy templates. 22 | 23 | Usage: 24 | 25 | ./xgettext-soy [INPUTPATH]... 26 | 27 | INPUTPATH elements may be files or directories. Input directories will be 28 | recursively searched for *.soy files. 29 | 30 | The resulting POT (PO template) file is written to STDOUT`) 31 | } 32 | 33 | var registry = template.Registry{} 34 | 35 | func main() { 36 | if len(os.Args) < 2 || strings.HasSuffix(os.Args[1], "help") { 37 | usage() 38 | os.Exit(1) 39 | } 40 | 41 | // Add all the sources to the registry. 42 | for _, src := range os.Args[1:] { 43 | err := filepath.Walk(src, walkSource) 44 | if err != nil { 45 | exit(err) 46 | } 47 | } 48 | parsepasses.ProcessMessages(registry) 49 | 50 | var e = extractor{&po.File{}} 51 | for _, t := range registry.Templates { 52 | e.extract(t.Node) 53 | } 54 | e.file.WriteTo(os.Stdout) 55 | } 56 | 57 | func walkSource(path string, info os.FileInfo, err error) error { 58 | if err != nil { 59 | return err 60 | } 61 | if !strings.HasSuffix(path, ".soy") { 62 | return nil 63 | } 64 | 65 | content, err := ioutil.ReadFile(path) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | tree, err := parse.SoyFile(path, string(content)) 71 | if err != nil { 72 | return err 73 | } 74 | if err = registry.Add(tree); err != nil { 75 | return err 76 | } 77 | return nil 78 | } 79 | 80 | type extractor struct { 81 | file *po.File 82 | } 83 | 84 | func (e extractor) extract(node ast.Node) { 85 | switch node := node.(type) { 86 | case *ast.MsgNode: 87 | if err := pomsg.Validate(node); err != nil { 88 | exit(err) 89 | } 90 | var pluralVar = "" 91 | if plural, ok := node.Body.Children()[0].(*ast.MsgPluralNode); ok { 92 | pluralVar = " var=" + plural.VarName 93 | } 94 | e.file.Messages = append(e.file.Messages, po.Message{ 95 | Comment: po.Comment{ 96 | ExtractedComments: []string{node.Desc}, 97 | References: []string{fmt.Sprintf("id=%d%v", node.ID, pluralVar)}, 98 | }, 99 | Ctxt: node.Meaning, 100 | Id: pomsg.Msgid(node), 101 | IdPlural: pomsg.MsgidPlural(node), 102 | }) 103 | default: 104 | if parent, ok := node.(ast.ParentNode); ok { 105 | for _, child := range parent.Children() { 106 | e.extract(child) 107 | } 108 | } 109 | } 110 | } 111 | 112 | func exit(err error) { 113 | fmt.Fprintln(os.Stderr, err) 114 | os.Exit(1) 115 | } 116 | -------------------------------------------------------------------------------- /soymsg/soymsg.go: -------------------------------------------------------------------------------- 1 | package soymsg 2 | 3 | import ( 4 | "bytes" 5 | "regexp" 6 | 7 | "github.com/robfig/soy/ast" 8 | ) 9 | 10 | // Provider provides access to message bundles by locale. 11 | type Provider interface { 12 | // Bundle returns messages for the given locale, which is in the form 13 | // [language_territory]. If no locale-specific messages could be found, an 14 | // empty bundle is returned, which will cause all messages to use the source 15 | // text. 16 | Bundle(locale string) Bundle 17 | } 18 | 19 | // Bundle is the set of messages available in a particular locale. 20 | type Bundle interface { 21 | // Locale returns the locale of the bundle. 22 | Locale() string 23 | 24 | // Message returns the message with the given id, or nil if none was found. 25 | Message(id uint64) *Message 26 | 27 | // PluralCase returns the index of the case to use for the given plural value. 28 | PluralCase(n int) int 29 | } 30 | 31 | // Message is a (possibly) translated message 32 | type Message struct { 33 | ID uint64 // ID is a content-based identifier for this message 34 | Parts []Part // Parts are the sequence of message parts that form the content. 35 | } 36 | 37 | // Part is an element of a Message. It may be one of the following concrete 38 | // types: RawTextPart, PlaceholderPart, PluralPart 39 | type Part interface{} 40 | 41 | // RawTextPart is a segment of a message that displays the contained text. 42 | type RawTextPart struct { 43 | Text string 44 | } 45 | 46 | // PlaceholderPart is a segment of a message that stands in for another node. 47 | type PlaceholderPart struct { 48 | Name string 49 | } 50 | 51 | // PluralPart is a segment of a message that has multiple forms depending on a value. 52 | type PluralPart struct { 53 | VarName string 54 | Cases []PluralCase 55 | } 56 | 57 | // PluralCase is one version of the message, for a particular plural case. 58 | type PluralCase struct { 59 | Spec PluralSpec 60 | Parts []Part 61 | } 62 | 63 | // PluralSpec is a description of a particular plural case. 64 | type PluralSpec struct { 65 | Type PluralSpecType 66 | ExplicitValue int // only set if Type == PluralSpecExplicit 67 | } 68 | 69 | // PluralSpecType is the CLDR plural class. 70 | type PluralSpecType int 71 | 72 | const ( 73 | PluralSpecExplicit PluralSpecType = iota 74 | PluralSpecZero 75 | PluralSpecOne 76 | PluralSpecTwo 77 | PluralSpecFew 78 | PluralSpecMany 79 | PluralSpecOther 80 | ) 81 | 82 | // NewMessage returns a new message, given its ID and placeholder string. 83 | // TODO: plural parts are not parsed from the placeholder string. 84 | func NewMessage(id uint64, phstr string) *Message { 85 | return &Message{id, Parts(phstr)} 86 | } 87 | 88 | // PlaceholderString returns a string representation of the message containing 89 | // braced placeholders for variables. 90 | func PlaceholderString(n *ast.MsgNode) string { 91 | var buf bytes.Buffer 92 | writeFingerprint(&buf, n, true) 93 | return buf.String() 94 | } 95 | 96 | var phRegex = regexp.MustCompile(`{[A-Z0-9_]+}`) 97 | 98 | // Parts returns the sequence of raw text and placeholders for the given 99 | // message placeholder string. 100 | func Parts(str string) []Part { 101 | var pos = 0 102 | var parts []Part 103 | for _, loc := range phRegex.FindAllStringIndex(str, -1) { 104 | var start, end = loc[0], loc[1] 105 | if start > pos { 106 | parts = append(parts, RawTextPart{str[pos:start]}) 107 | } 108 | parts = append(parts, PlaceholderPart{str[start+1 : end-1]}) 109 | pos = end 110 | } 111 | if pos < len(str) { 112 | parts = append(parts, RawTextPart{str[pos:]}) 113 | } 114 | return parts 115 | } 116 | 117 | // SetPlaceholdersAndID generates and sets placeholder names for all children 118 | // nodes, and generates and sets the message ID. 119 | func SetPlaceholdersAndID(n *ast.MsgNode) { 120 | setPlaceholderNames(n) 121 | n.ID = calcID(n) 122 | } 123 | -------------------------------------------------------------------------------- /soymsg/soymsg_test.go: -------------------------------------------------------------------------------- 1 | package soymsg 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/robfig/soy/ast" 9 | "github.com/robfig/soy/parse" 10 | ) 11 | 12 | // Test that NewMessage correctly splits a message string into message parts. 13 | func TestNewMessage(t *testing.T) { 14 | type test struct { 15 | input string 16 | output []Part 17 | } 18 | var txt = func(str string) Part { return RawTextPart{str} } 19 | var ph = func(name string) Part { return PlaceholderPart{name} } 20 | var tests = []test{ 21 | {"", nil}, 22 | {"hello world", []Part{txt("hello world")}}, 23 | {"hello {WORLD}", []Part{txt("hello "), ph("WORLD")}}, 24 | {"{HELLO_WORLD}", []Part{ph("HELLO_WORLD")}}, 25 | {"{A_1}{A_2}", []Part{ph("A_1"), ph("A_2")}}, 26 | {"{}", []Part{txt("{}")}}, 27 | {"{ }", []Part{txt("{ }")}}, 28 | {"{br}", []Part{txt("{br}")}}, 29 | {"x{A}{B} {C}.", []Part{txt("x"), ph("A"), ph("B"), txt(" "), ph("C"), txt(".")}}, 30 | } 31 | 32 | for _, test := range tests { 33 | var msg = NewMessage(0, test.input) 34 | if !reflect.DeepEqual(msg.Parts, test.output) { 35 | t.Errorf("(actual) %v != %v (expected)", msg.Parts, test.output) 36 | } 37 | } 38 | } 39 | 40 | // Tests that the set of messages (ids and placeholders) extracted from 41 | // features.soy is the same as that generated by the official java 42 | // implementation. 43 | func TestFeatureExtractedMsgs(t *testing.T) { 44 | type test struct { 45 | msg *ast.MsgNode 46 | id uint64 47 | phstr string 48 | } 49 | 50 | // test data taken from closure-templates/examples/examples_extracted.xlf 51 | var tests = []test{ 52 | // Simple messages 53 | {msg("noun", "The word 'Archive' used as a noun, i.e. an information store.", "Archive"), 54 | 7224011416745566687, "Archive"}, 55 | {msg("verb", "The word 'Archive' used as a verb, i.e. to store information.", "Archive"), 56 | 4826315192146469447, "Archive"}, 57 | {msg("", "", "A trip was taken."), 58 | 3329840836245051515, "A trip was taken."}, 59 | {msg("", "Ask user to pick best keyword", "Your favorite keyword"), 60 | 2209690285855487595, "Your favorite keyword"}, 61 | {msg("", "Link to Help", "Help"), 62 | 7911416166208830577, "Help"}, 63 | 64 | // Messages with dataref placeholders 65 | {msg("", "Example: Alice took a trip to wonderland.", "{$name} took a trip to {$destination}."), 66 | 768490705511913603, "{NAME} took a trip to {DESTINATION}."}, 67 | {msg("", "Example: 5 is nowhere near the value of pi.", "{$pi} is nowhere near the value of pi."), 68 | 889614911019327165, "{PI} is nowhere near the value of pi."}, 69 | {msg("", "Example: Alice took a trip.", "{$name} took a trip."), 70 | 3179387603303514412, "{NAME} took a trip."}, 71 | 72 | // Messages with html tags 73 | // {msg("", "Link to the unreleased 'Labs' feature.", `Click here to access Labs.`), 74 | // 5539341884085868292, `Click {START_LINK}here{END_LINK} to access Labs.`}, 75 | 76 | // Messages with calls 77 | {msg("", "Example: The set of prime numbers is {2, 3, 5, 7, 11, 13, ...}.", ` 78 | The set of {$setName} is {lb} 79 | {call .buildCommaSeparatedList_} 80 | {param items: $setMembers /} 81 | {/call} 82 | , ...{rb}.`), 83 | 135956960462609535, "The set of {SET_NAME} is {{XXX}, ...}."}, 84 | 85 | // Plural 86 | 87 | // TODO: Clarify with closure-templates mailing list whether ids should be 88 | // calculated with braced PHs or not. Presently we do not use braced phs for id. 89 | {msg("", "The number of eggs you need.", ` 90 | {plural $eggs} 91 | {case 1}You have one egg 92 | {default}You have {$eggs} eggs 93 | {/plural}`), 94 | 176798647517908084, "{EGGS_1,plural,=1{You have one egg}other{You have {EGGS_2} eggs}}"}, 95 | // would be 8336954131281929964 without/ braced phs 96 | 97 | // TODO: Add test that needs placeholder index 98 | // TODO: Test equivalent nodes 99 | } 100 | 101 | for _, test := range tests { 102 | SetPlaceholdersAndID(test.msg) 103 | if test.id != test.msg.ID { 104 | t.Errorf("(actual) %v != %v (expected)", test.msg.ID, test.id) 105 | } 106 | 107 | var actual = PlaceholderString(test.msg) 108 | if test.phstr != actual { 109 | t.Errorf("(actual) %v != %v (expected)", actual, test.phstr) 110 | } 111 | } 112 | } 113 | 114 | func msg(meaning, desc string, body string) *ast.MsgNode { 115 | var msgtmpl = fmt.Sprintf("{msg meaning=%q desc=%q}%s{/msg}", meaning, desc, body) 116 | var sf, err = parse.SoyFile("", msgtmpl) 117 | if err != nil { 118 | panic(err) 119 | } 120 | return sf.Body[0].(*ast.MsgNode) 121 | } 122 | 123 | func txt(str string) *ast.RawTextNode { 124 | return &ast.RawTextNode{0, []byte(str)} 125 | } 126 | -------------------------------------------------------------------------------- /soyweb/soyweb.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package soyweb is a simple development server that serves the given template. 3 | 4 | Invoke it like so: 5 | 6 | go get github.com/robfig/soy/soyweb 7 | soyweb test.soy 8 | 9 | It will attempt to execute the "soyweb.soyweb" template found in the given file. 10 | 11 | Parameters may be provided to the template in the URL query string. 12 | 13 | */ 14 | package main 15 | 16 | import ( 17 | "bytes" 18 | "flag" 19 | "fmt" 20 | "io" 21 | "log" 22 | "net/http" 23 | "os" 24 | 25 | "github.com/robfig/soy" 26 | "github.com/robfig/soy/data" 27 | ) 28 | 29 | var port = flag.Int("port", 9812, "port on which to listen") 30 | 31 | func main() { 32 | fmt.Print("Listening on :", *port, "...") 33 | log.Fatal(http.ListenAndServe( 34 | fmt.Sprintf(":%d", *port), 35 | http.HandlerFunc(handler))) 36 | } 37 | 38 | func handler(res http.ResponseWriter, req *http.Request) { 39 | var tofu, err = soy.NewBundle(). 40 | AddTemplateFile(os.Args[1]). 41 | CompileToTofu() 42 | if err != nil { 43 | http.Error(res, err.Error(), 500) 44 | return 45 | } 46 | 47 | var m = make(data.Map) 48 | for k, v := range req.URL.Query() { 49 | m[k] = data.String(v[0]) 50 | } 51 | 52 | var buf bytes.Buffer 53 | err = tofu.Render(&buf, "soyweb.soyweb", m) 54 | if err != nil { 55 | http.Error(res, err.Error(), 500) 56 | return 57 | } 58 | 59 | io.Copy(res, &buf) 60 | } 61 | -------------------------------------------------------------------------------- /template/registry.go: -------------------------------------------------------------------------------- 1 | // Package template provides convenient access to groups of parsed Soy files. 2 | package template 3 | 4 | import ( 5 | "fmt" 6 | "log" 7 | "strings" 8 | 9 | "github.com/robfig/soy/ast" 10 | ) 11 | 12 | // Registry provides convenient access to a collection of parsed Soy templates. 13 | type Registry struct { 14 | SoyFiles []*ast.SoyFileNode 15 | Templates []Template 16 | 17 | // sourceByTemplateName maps FQ template name to the input source it came from. 18 | sourceByTemplateName map[string]string 19 | fileByTemplateName map[string]string 20 | } 21 | 22 | // Add the given Soy file node (and all contained templates) to this registry. 23 | func (r *Registry) Add(soyfile *ast.SoyFileNode) error { 24 | if r.sourceByTemplateName == nil { 25 | r.sourceByTemplateName = make(map[string]string) 26 | } 27 | if r.fileByTemplateName == nil { 28 | r.fileByTemplateName = make(map[string]string) 29 | } 30 | var ns *ast.NamespaceNode 31 | for _, node := range soyfile.Body { 32 | switch node := node.(type) { 33 | case *ast.SoyDocNode: 34 | continue 35 | case *ast.NamespaceNode: 36 | ns = node 37 | default: 38 | return fmt.Errorf("expected namespace, found %v", node) 39 | } 40 | break 41 | } 42 | if ns == nil { 43 | return fmt.Errorf("namespace required") 44 | } 45 | 46 | r.SoyFiles = append(r.SoyFiles, soyfile) 47 | for i := 0; i < len(soyfile.Body); i++ { 48 | var tn, ok = soyfile.Body[i].(*ast.TemplateNode) 49 | if !ok { 50 | continue 51 | } 52 | 53 | // Technically every template requires soydoc, but having to add empty 54 | // soydoc just to get a template to compile is just stupid. (There is a 55 | // separate data ref check to ensure any variables used are declared as 56 | // params, anyway). 57 | sdn, ok := soyfile.Body[i-1].(*ast.SoyDocNode) 58 | if !ok { 59 | sdn = &ast.SoyDocNode{tn.Pos, nil} 60 | } 61 | hasSoyDocParams := len(sdn.Params) > 0 62 | 63 | // Extract leading Header Params from the template body. 64 | // Add them to Soy.Params for backwards compatibility. 65 | var headerParams []*ast.HeaderParamNode 66 | for _, n := range tn.Body.Nodes { 67 | if param, ok := n.(*ast.HeaderParamNode); ok { 68 | 69 | headerParams = append(headerParams, param) 70 | sdn.Params = append(sdn.Params, &ast.SoyDocParamNode{ 71 | Pos: param.Pos, 72 | Name: param.Name, 73 | Optional: param.Optional, 74 | }) 75 | } else { 76 | break 77 | } 78 | } 79 | if len(headerParams) > 0 && hasSoyDocParams { 80 | return fmt.Errorf("template may not have both soydoc and header params specified") 81 | } 82 | tn.Body.Nodes = tn.Body.Nodes[len(headerParams):] 83 | 84 | r.Templates = append(r.Templates, Template{sdn, tn, ns}) 85 | r.sourceByTemplateName[tn.Name] = soyfile.Text 86 | r.fileByTemplateName[tn.Name] = soyfile.Name 87 | } 88 | return nil 89 | } 90 | 91 | // Template allows lookup by (fully-qualified) template name. 92 | // The resulting template is returned and a boolean indicating if it was found. 93 | func (r *Registry) Template(name string) (Template, bool) { 94 | for _, t := range r.Templates { 95 | if t.Node.Name == name { 96 | return t, true 97 | } 98 | } 99 | return Template{}, false 100 | } 101 | 102 | // LineNumber computes the line number in the input source for the given node 103 | // within the given template. 104 | func (r *Registry) LineNumber(templateName string, node ast.Node) int { 105 | var src, ok = r.sourceByTemplateName[templateName] 106 | if !ok { 107 | log.Println("template not found:", templateName) 108 | return 0 109 | } 110 | return 1 + strings.Count(src[:node.Position()], "\n") 111 | } 112 | 113 | // ColNumber computes the column number in the relevant line of input source for the given node 114 | // within the given template. 115 | func (r *Registry) ColNumber(templateName string, node ast.Node) int { 116 | var src, ok = r.sourceByTemplateName[templateName] 117 | if !ok { 118 | log.Println("template not found:", templateName) 119 | return 0 120 | } 121 | return 1 + int(node.Position()) - strings.LastIndex(src[:node.Position()], "\n") 122 | } 123 | 124 | // Filename identifies the filename containing the specified template 125 | func (r *Registry) Filename(templateName string) string { 126 | var f, ok = r.fileByTemplateName[templateName] 127 | if !ok { 128 | log.Println("template not found:", templateName) 129 | return "" 130 | } 131 | return f 132 | } 133 | -------------------------------------------------------------------------------- /template/template.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import "github.com/robfig/soy/ast" 4 | 5 | // Template is a Soy template's parse tree, including the relevant context 6 | // (preceding soydoc and namespace). 7 | type Template struct { 8 | Doc *ast.SoyDocNode // this template's SoyDoc, w/ header params added to Doc.Params 9 | Node *ast.TemplateNode // this template's node 10 | Namespace *ast.NamespaceNode // this template's namespace 11 | } 12 | -------------------------------------------------------------------------------- /testdata/FeaturesUsage_globals.txt: -------------------------------------------------------------------------------- 1 | // Copyright 2009 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 16 | // Compile-time globals for the features examples. 17 | 18 | GLOBAL_STR = 'This is a compile-time global.' 19 | GLOBAL_INT = 88 20 | GLOBAL_BOOL = false 21 | GLOBAL_UNUSED = 'This is unused.' 22 | -------------------------------------------------------------------------------- /testdata/simple.soy: -------------------------------------------------------------------------------- 1 | // Copyright 2008 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Simple examples. 16 | // Author: Kai Huang 17 | 18 | {namespace soy.examples.simple} 19 | 20 | 21 | /** 22 | * Says hello to the world. 23 | */ 24 | {template .helloWorld} 25 | {msg desc="Says hello to the world."} 26 | Hello world! 27 | {/msg} 28 | {/template} 29 | 30 | 31 | /** 32 | * Says hello to a person (or to the world if no person is given). 33 | * @param? name The name of the person to say hello to. 34 | */ 35 | {template .helloName} 36 | {if hasData() and $name} 37 | {msg desc="Says hello to a person."} 38 | Hello {$name}! 39 | {/msg} 40 | {else} 41 | {call .helloWorld /} 42 | {/if} 43 | {/template} 44 | 45 | 46 | /** 47 | * Say hello to a list of people. 48 | * @param names List of names of the people to say hello to. 49 | */ 50 | {template .helloNames} 51 | {foreach $name in $names} 52 | {call .helloName} 53 | {param name: $name /} 54 | {/call} 55 | {if not isLast($name)} 56 |
// break after every line except the last 57 | {/if} 58 | {ifempty} 59 | // If names list is empty, say "Hello world". 60 | {call .helloWorld /} 61 | {/foreach} 62 | {/template} 63 | --------------------------------------------------------------------------------