新增: u-tpl SQL 模板引擎完整实现
- Lexer/Parser/Executor 三阶段架构
- #{param} 参数化 + ${raw} 原样替换 + 白名单安全策略
- @if/@for/@tpl/@include/@namespace 控制流
- 表达式引擎: 比较、逻辑、nil 检查、len() 内置函数
- 支持 ?/$1/:1 多数据库占位符风格
- 零依赖,纯 Go 标准库实现
This commit is contained in:
121
engine.go
Normal file
121
engine.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package utpl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.1216.top/lxy/u-tpl/internal"
|
||||
)
|
||||
|
||||
type PlaceholderStyle = internal.PlaceholderStyle
|
||||
|
||||
const (
|
||||
QuestionMark PlaceholderStyle = internal.QuestionMark
|
||||
DollarNumber = internal.DollarNumber
|
||||
ColonNumber = internal.ColonNumber
|
||||
)
|
||||
|
||||
type IncludeResolver func(path string) (string, error)
|
||||
|
||||
type Option func(*Engine)
|
||||
|
||||
type Engine struct {
|
||||
style internal.PlaceholderStyle
|
||||
rawPolicy RawPolicy
|
||||
includeResolver IncludeResolver
|
||||
strict bool
|
||||
}
|
||||
|
||||
func New(opts ...Option) *Engine {
|
||||
e := &Engine{
|
||||
style: internal.QuestionMark,
|
||||
strict: true,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(e)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func WithPlaceholderStyle(style PlaceholderStyle) Option {
|
||||
return func(e *Engine) { e.style = style }
|
||||
}
|
||||
|
||||
func WithRawPolicy(policy RawPolicy) Option {
|
||||
return func(e *Engine) { e.rawPolicy = policy }
|
||||
}
|
||||
|
||||
func WithIncludeResolver(resolver IncludeResolver) Option {
|
||||
return func(e *Engine) { e.includeResolver = resolver }
|
||||
}
|
||||
|
||||
func WithStrictMode(strict bool) Option {
|
||||
return func(e *Engine) { e.strict = strict }
|
||||
}
|
||||
|
||||
func (e *Engine) Parse(name string, source string) (*Template, error) {
|
||||
lexer := internal.NewLexer(source)
|
||||
tokens, err := lexer.Tokenize()
|
||||
if err != nil {
|
||||
return nil, wrapParseError(err, name)
|
||||
}
|
||||
|
||||
var includeMgr *internal.IncludeManager
|
||||
if e.includeResolver != nil {
|
||||
includeMgr = internal.NewIncludeManager(internal.IncludeResolver(e.includeResolver))
|
||||
}
|
||||
|
||||
parser := internal.NewParser(source, tokens, includeMgr)
|
||||
nodes, err := parser.Parse()
|
||||
if err != nil {
|
||||
return nil, wrapParseError(err, name)
|
||||
}
|
||||
|
||||
namespace := ""
|
||||
blocks := make(map[string][]internal.Node)
|
||||
var bodyNodes []internal.Node
|
||||
|
||||
hasBlocks := false
|
||||
for _, n := range nodes {
|
||||
if ns, ok := n.(*internal.NamespaceNode); ok {
|
||||
namespace = ns.Name
|
||||
continue
|
||||
}
|
||||
if blk, ok := n.(*internal.BlockNode); ok {
|
||||
hasBlocks = true
|
||||
fullName := blk.Name
|
||||
if namespace != "" {
|
||||
fullName = namespace + "." + blk.Name
|
||||
}
|
||||
blocks[fullName] = blk.Body
|
||||
continue
|
||||
}
|
||||
bodyNodes = append(bodyNodes, n)
|
||||
}
|
||||
|
||||
return &Template{
|
||||
name: name,
|
||||
engine: e,
|
||||
nodes: bodyNodes,
|
||||
blocks: blocks,
|
||||
hasBlocks: hasBlocks,
|
||||
namespace: namespace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *Engine) MustParse(name string, source string) *Template {
|
||||
tpl, err := e.Parse(name, source)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return tpl
|
||||
}
|
||||
|
||||
func wrapParseError(err error, name string) error {
|
||||
if _, ok := err.(*ParseError); ok {
|
||||
return err
|
||||
}
|
||||
return &ParseError{
|
||||
Pos: Position{Line: 0, Column: 0},
|
||||
Message: fmt.Sprintf("template %q: %s", name, err.Error()),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user