mirror of
https://github.com/semihalev/twig.git
synced 2026-03-14 13:55:46 +01:00
777 lines
17 KiB
Go
777 lines
17 KiB
Go
package twig
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// ExpressionType represents the type of an expression
|
|
type ExpressionType int
|
|
|
|
// Expression types
|
|
const (
|
|
ExprLiteral ExpressionType = iota
|
|
ExprVariable
|
|
ExprUnary
|
|
ExprBinary
|
|
ExprFunction
|
|
ExprFilter
|
|
ExprTest
|
|
ExprGetAttr
|
|
ExprGetItem
|
|
ExprMethodCall
|
|
ExprArray
|
|
ExprHash
|
|
ExprConditional
|
|
ExprModuleMethod
|
|
)
|
|
|
|
// ExpressionNode represents a Twig expression
|
|
type ExpressionNode struct {
|
|
exprType ExpressionType
|
|
line int
|
|
}
|
|
|
|
// LiteralNode represents a literal value (string, number, boolean, null)
|
|
type LiteralNode struct {
|
|
ExpressionNode
|
|
value interface{}
|
|
}
|
|
|
|
// VariableNode represents a variable reference
|
|
type VariableNode struct {
|
|
ExpressionNode
|
|
name string
|
|
}
|
|
|
|
// UnaryNode represents a unary operation (not, -, +)
|
|
type UnaryNode struct {
|
|
ExpressionNode
|
|
operator string
|
|
node Node
|
|
}
|
|
|
|
// BinaryNode represents a binary operation (+, -, *, /, etc)
|
|
type BinaryNode struct {
|
|
ExpressionNode
|
|
operator string
|
|
left Node
|
|
right Node
|
|
}
|
|
|
|
// FunctionNode represents a function call
|
|
type FunctionNode struct {
|
|
ExpressionNode
|
|
name string
|
|
args []Node
|
|
moduleExpr Node // Optional module for module.function() calls
|
|
}
|
|
|
|
// FilterNode represents a filter application
|
|
type FilterNode struct {
|
|
ExpressionNode
|
|
node Node
|
|
filter string
|
|
args []Node
|
|
}
|
|
|
|
// TestNode represents a test (is defined, is null, etc)
|
|
type TestNode struct {
|
|
ExpressionNode
|
|
node Node
|
|
test string
|
|
args []Node
|
|
}
|
|
|
|
// GetAttrNode represents attribute access (obj.attr)
|
|
type GetAttrNode struct {
|
|
ExpressionNode
|
|
node Node
|
|
attribute Node
|
|
}
|
|
|
|
// GetItemNode represents item access (array[key])
|
|
type GetItemNode struct {
|
|
ExpressionNode
|
|
node Node
|
|
item Node
|
|
}
|
|
|
|
// MethodCallNode represents method call (obj.method())
|
|
type MethodCallNode struct {
|
|
ExpressionNode
|
|
node Node
|
|
method string
|
|
args []Node
|
|
}
|
|
|
|
// ArrayNode represents an array literal
|
|
type ArrayNode struct {
|
|
ExpressionNode
|
|
items []Node
|
|
}
|
|
|
|
// HashNode represents a hash/map literal
|
|
type HashNode struct {
|
|
ExpressionNode
|
|
items map[Node]Node
|
|
}
|
|
|
|
// ConditionalNode represents ternary operator (condition ? true : false)
|
|
type ConditionalNode struct {
|
|
ExpressionNode
|
|
condition Node
|
|
trueExpr Node
|
|
falseExpr Node
|
|
}
|
|
|
|
// Type implementation for ExpressionNode
|
|
func (n *ExpressionNode) Type() NodeType {
|
|
return NodeExpression
|
|
}
|
|
|
|
func (n *ExpressionNode) Line() int {
|
|
return n.line
|
|
}
|
|
|
|
// Render implementation for LiteralNode
|
|
func (n *LiteralNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
var str string
|
|
|
|
switch v := n.value.(type) {
|
|
case string:
|
|
str = v
|
|
case int:
|
|
str = strconv.Itoa(v)
|
|
case float64:
|
|
str = strconv.FormatFloat(v, 'f', -1, 64)
|
|
case bool:
|
|
str = strconv.FormatBool(v)
|
|
case nil:
|
|
str = ""
|
|
default:
|
|
str = ctx.ToString(v)
|
|
}
|
|
|
|
_, err := WriteString(w, str)
|
|
return err
|
|
}
|
|
|
|
// Release returns a LiteralNode to the pool
|
|
func (n *LiteralNode) Release() {
|
|
ReleaseLiteralNode(n)
|
|
}
|
|
|
|
// NewLiteralNode creates a new literal node
|
|
func NewLiteralNode(value interface{}, line int) *LiteralNode {
|
|
node := GetLiteralNode(value, line)
|
|
node.ExpressionNode.exprType = ExprLiteral
|
|
return node
|
|
}
|
|
|
|
// NewVariableNode creates a new variable node
|
|
func NewVariableNode(name string, line int) *VariableNode {
|
|
node := GetVariableNode(name, line)
|
|
node.ExpressionNode.exprType = ExprVariable
|
|
return node
|
|
}
|
|
|
|
// NewBinaryNode creates a new binary operation node
|
|
func NewBinaryNode(operator string, left, right Node, line int) *BinaryNode {
|
|
return GetBinaryNode(operator, left, right, line)
|
|
}
|
|
|
|
// NewGetAttrNode creates a new attribute access node
|
|
func NewGetAttrNode(node, attribute Node, line int) *GetAttrNode {
|
|
return GetGetAttrNode(node, attribute, line)
|
|
}
|
|
|
|
// NewGetItemNode creates a new item access node
|
|
func NewGetItemNode(node, item Node, line int) *GetItemNode {
|
|
return GetGetItemNode(node, item, line)
|
|
}
|
|
|
|
// Render implementation for VariableNode
|
|
func (n *VariableNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
value, err := ctx.GetVariable(n.name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If debug is enabled, log variable access and value
|
|
if IsDebugEnabled() {
|
|
if value == nil {
|
|
// Log undefined variable at error level if debug is enabled
|
|
message := fmt.Sprintf("Variable lookup at line %d", n.line)
|
|
LogError(fmt.Errorf("%w: %s", ErrUndefinedVar, n.name), message)
|
|
|
|
// If in strict debug mode with error level, return an error for undefined variables
|
|
if debugger.level >= DebugError && ctx.engine != nil && ctx.engine.debug {
|
|
templateName := "unknown"
|
|
if ctx.engine.currentTemplate != "" {
|
|
templateName = ctx.engine.currentTemplate
|
|
}
|
|
return NewError(fmt.Errorf("%w: %s", ErrUndefinedVar, n.name), templateName, n.line, 0, "")
|
|
}
|
|
} else if debugger.level >= DebugVerbose {
|
|
// Log defined variables at verbose level
|
|
LogVerbose("Variable access at line %d: %s = %v (type: %T)", n.line, n.name, value, value)
|
|
}
|
|
}
|
|
|
|
str := ctx.ToString(value)
|
|
_, err = WriteString(w, str)
|
|
return err
|
|
}
|
|
|
|
// Release returns a VariableNode to the pool
|
|
func (n *VariableNode) Release() {
|
|
ReleaseVariableNode(n)
|
|
}
|
|
|
|
// Render implementation for GetAttrNode
|
|
func (n *GetAttrNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
obj, err := ctx.EvaluateExpression(n.node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
attrName, err := ctx.EvaluateExpression(n.attribute)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
attrStr, ok := attrName.(string)
|
|
if !ok {
|
|
return fmt.Errorf("attribute name must be a string")
|
|
}
|
|
|
|
value, err := ctx.getAttribute(obj, attrStr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
str := ctx.ToString(value)
|
|
_, err = WriteString(w, str)
|
|
return err
|
|
}
|
|
|
|
// Release returns a GetAttrNode to the pool
|
|
func (n *GetAttrNode) Release() {
|
|
ReleaseGetAttrNode(n)
|
|
}
|
|
|
|
// Render implementation for GetItemNode
|
|
func (n *GetItemNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
container, err := ctx.EvaluateExpression(n.node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
index, err := ctx.EvaluateExpression(n.item)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
value, err := ctx.getItem(container, index)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
str := ctx.ToString(value)
|
|
_, err = WriteString(w, str)
|
|
return err
|
|
}
|
|
|
|
// Release returns a GetItemNode to the pool
|
|
func (n *GetItemNode) Release() {
|
|
ReleaseGetItemNode(n)
|
|
}
|
|
|
|
// Render implementation for BinaryNode
|
|
func (n *BinaryNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
result, err := ctx.EvaluateExpression(n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
str := ctx.ToString(result)
|
|
_, err = WriteString(w, str)
|
|
return err
|
|
}
|
|
|
|
// Release returns a BinaryNode to the pool
|
|
func (n *BinaryNode) Release() {
|
|
ReleaseBinaryNode(n)
|
|
}
|
|
|
|
// Render implementation for FilterNode
|
|
func (n *FilterNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
result, err := ctx.EvaluateExpression(n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
str := ctx.ToString(result)
|
|
_, err = WriteString(w, str)
|
|
return err
|
|
}
|
|
|
|
// Release returns a FilterNode to the pool
|
|
func (n *FilterNode) Release() {
|
|
ReleaseFilterNode(n)
|
|
}
|
|
|
|
// Render implementation for TestNode
|
|
func (n *TestNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
result, err := ctx.EvaluateExpression(n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
str := ctx.ToString(result)
|
|
_, err = WriteString(w, str)
|
|
return err
|
|
}
|
|
|
|
// Release returns a TestNode to the pool
|
|
func (n *TestNode) Release() {
|
|
ReleaseTestNode(n)
|
|
}
|
|
|
|
// Render implementation for UnaryNode
|
|
func (n *UnaryNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
result, err := ctx.EvaluateExpression(n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
str := ctx.ToString(result)
|
|
_, err = WriteString(w, str)
|
|
return err
|
|
}
|
|
|
|
// Release returns a UnaryNode to the pool
|
|
func (n *UnaryNode) Release() {
|
|
ReleaseUnaryNode(n)
|
|
}
|
|
|
|
// Render implementation for ConditionalNode
|
|
func (n *ConditionalNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
result, err := ctx.EvaluateExpression(n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
str := ctx.ToString(result)
|
|
_, err = WriteString(w, str)
|
|
return err
|
|
}
|
|
|
|
// Release returns a ConditionalNode to the pool
|
|
func (n *ConditionalNode) Release() {
|
|
ReleaseConditionalNode(n)
|
|
}
|
|
|
|
// Render implementation for ArrayNode
|
|
func (n *ArrayNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
result, err := ctx.EvaluateExpression(n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
str := ctx.ToString(result)
|
|
_, err = WriteString(w, str)
|
|
return err
|
|
}
|
|
|
|
// Release returns an ArrayNode to the pool
|
|
func (n *ArrayNode) Release() {
|
|
ReleaseArrayNode(n)
|
|
}
|
|
|
|
// Render implementation for HashNode
|
|
func (n *HashNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
result, err := ctx.EvaluateExpression(n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
str := ctx.ToString(result)
|
|
_, err = WriteString(w, str)
|
|
return err
|
|
}
|
|
|
|
// Release returns a HashNode to the pool
|
|
func (n *HashNode) Release() {
|
|
ReleaseHashNode(n)
|
|
}
|
|
|
|
// Render implementation for FunctionNode
|
|
func (n *FunctionNode) Render(w io.Writer, ctx *RenderContext) error {
|
|
result, err := ctx.EvaluateExpression(n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
str := ctx.ToString(result)
|
|
_, err = WriteString(w, str)
|
|
return err
|
|
}
|
|
|
|
// Release returns a FunctionNode to the pool
|
|
func (n *FunctionNode) Release() {
|
|
ReleaseFunctionNode(n)
|
|
}
|
|
|
|
// NewFilterNode creates a new filter node
|
|
func NewFilterNode(node Node, filter string, args []Node, line int) *FilterNode {
|
|
return GetFilterNode(node, filter, args, line)
|
|
}
|
|
|
|
// NewTestNode creates a new test node
|
|
func NewTestNode(node Node, test string, args []Node, line int) *TestNode {
|
|
return GetTestNode(node, test, args, line)
|
|
}
|
|
|
|
// NewUnaryNode creates a new unary operation node
|
|
func NewUnaryNode(operator string, node Node, line int) *UnaryNode {
|
|
return GetUnaryNode(operator, node, line)
|
|
}
|
|
|
|
// NewConditionalNode creates a new conditional (ternary) node
|
|
func NewConditionalNode(condition, trueExpr, falseExpr Node, line int) *ConditionalNode {
|
|
return GetConditionalNode(condition, trueExpr, falseExpr, line)
|
|
}
|
|
|
|
// NewArrayNode creates a new array node
|
|
func NewArrayNode(items []Node, line int) *ArrayNode {
|
|
return GetArrayNode(items, line)
|
|
}
|
|
|
|
// NewHashNode creates a new hash node
|
|
func NewHashNode(items map[Node]Node, line int) *HashNode {
|
|
return GetHashNode(items, line)
|
|
}
|
|
|
|
// NewFunctionNode creates a new function call node
|
|
func NewFunctionNode(name string, args []Node, line int) *FunctionNode {
|
|
return GetFunctionNode(name, args, line)
|
|
}
|
|
|
|
// ParseExpressionOptimized parses simple expressions with minimal allocations
|
|
// Returns the parsed expression node and a boolean indicating success
|
|
func ParseExpressionOptimized(expr string) (Node, bool) {
|
|
// Trim whitespace
|
|
expr = strings.TrimSpace(expr)
|
|
|
|
// Quick empty check
|
|
if len(expr) == 0 {
|
|
return nil, false
|
|
}
|
|
|
|
// Try parsing as literal
|
|
if val, ok := ParseLiteralOptimized(expr); ok {
|
|
return NewLiteralNode(val, 0), true
|
|
}
|
|
|
|
// Check for simple variable references
|
|
if IsValidVariableName(expr) {
|
|
return NewVariableNode(expr, 0), true
|
|
}
|
|
|
|
// More complex expression - will need the full parser
|
|
return nil, false
|
|
}
|
|
|
|
// ParseLiteralOptimized parses literals (strings, numbers, booleans) with minimal allocations
|
|
func ParseLiteralOptimized(expr string) (interface{}, bool) {
|
|
// Quick check for common literal types
|
|
if len(expr) == 0 {
|
|
return nil, false
|
|
}
|
|
|
|
// Check for string literals
|
|
if len(expr) >= 2 && ((expr[0] == '"' && expr[len(expr)-1] == '"') ||
|
|
(expr[0] == '\'' && expr[len(expr)-1] == '\'')) {
|
|
// String literal
|
|
return expr[1 : len(expr)-1], true
|
|
}
|
|
|
|
// Check for number literals
|
|
if isDigit(expr[0]) || (expr[0] == '-' && len(expr) > 1 && isDigit(expr[1])) {
|
|
// Try parsing as integer first (most common case)
|
|
if i, err := strconv.Atoi(expr); err == nil {
|
|
return i, true
|
|
}
|
|
|
|
// Try parsing as float
|
|
if f, err := strconv.ParseFloat(expr, 64); err == nil {
|
|
return f, true
|
|
}
|
|
}
|
|
|
|
// Check for boolean literals
|
|
if expr == "true" {
|
|
return true, true
|
|
}
|
|
if expr == "false" {
|
|
return false, true
|
|
}
|
|
if expr == "null" || expr == "nil" {
|
|
return nil, true
|
|
}
|
|
|
|
// Not a simple literal
|
|
return nil, false
|
|
}
|
|
|
|
// IsValidVariableName checks if a string is a valid variable name
|
|
func IsValidVariableName(name string) bool {
|
|
if len(name) == 0 {
|
|
return false
|
|
}
|
|
|
|
// First character must be a letter or underscore
|
|
if !isAlpha(name[0]) {
|
|
return false
|
|
}
|
|
|
|
// Rest can be letters, digits, or underscores
|
|
for i := 1; i < len(name); i++ {
|
|
if !isNameChar(name[i]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Check for reserved keywords
|
|
switch name {
|
|
case "true", "false", "null", "nil", "not", "and", "or", "in", "is":
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ProcessStringEscapes efficiently processes string escapes with minimal allocations
|
|
// Returns the processed string and a flag indicating if any processing was done
|
|
func ProcessStringEscapes(text string) (string, bool) {
|
|
// Check if there are any escapes to process
|
|
hasEscape := false
|
|
for i := 0; i < len(text); i++ {
|
|
if text[i] == '\\' && i+1 < len(text) {
|
|
hasEscape = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// If no escapes, return the original string
|
|
if !hasEscape {
|
|
return text, false
|
|
}
|
|
|
|
// Process escapes
|
|
var sb strings.Builder
|
|
sb.Grow(len(text))
|
|
|
|
i := 0
|
|
for i < len(text) {
|
|
if text[i] == '\\' && i+1 < len(text) {
|
|
// Escape sequence found
|
|
i++
|
|
|
|
// Process the escape sequence
|
|
switch text[i] {
|
|
case 'n':
|
|
sb.WriteByte('\n')
|
|
case 'r':
|
|
sb.WriteByte('\r')
|
|
case 't':
|
|
sb.WriteByte('\t')
|
|
case 'b':
|
|
sb.WriteByte('\b')
|
|
case 'f':
|
|
sb.WriteByte('\f')
|
|
case '\'':
|
|
sb.WriteByte('\'')
|
|
case '"':
|
|
sb.WriteByte('"')
|
|
case '\\':
|
|
sb.WriteByte('\\')
|
|
default:
|
|
// Unknown escape, just keep it as-is
|
|
sb.WriteByte('\\')
|
|
sb.WriteByte(text[i])
|
|
}
|
|
} else {
|
|
// Regular character
|
|
sb.WriteByte(text[i])
|
|
}
|
|
i++
|
|
}
|
|
|
|
// Return the processed string
|
|
return sb.String(), true
|
|
}
|
|
|
|
// ParseNumberOptimized parses a number without using strconv for standard cases
|
|
// This provides better performance for typical integers and simple floats
|
|
func ParseNumberOptimized(text string) (interface{}, bool) {
|
|
// Empty string is not a number
|
|
if len(text) == 0 {
|
|
return nil, false
|
|
}
|
|
|
|
// Check for negative sign
|
|
isNegative := false
|
|
pos := 0
|
|
|
|
if text[pos] == '-' {
|
|
isNegative = true
|
|
pos++
|
|
|
|
// Just a minus sign is not a number
|
|
if pos >= len(text) {
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
// Parse the integer part
|
|
var intValue int64
|
|
hasDigits := false
|
|
|
|
for pos < len(text) && isDigit(text[pos]) {
|
|
digit := int64(text[pos] - '0')
|
|
intValue = intValue*10 + digit
|
|
hasDigits = true
|
|
pos++
|
|
}
|
|
|
|
if !hasDigits {
|
|
return nil, false
|
|
}
|
|
|
|
// Check for decimal point
|
|
hasDecimal := false
|
|
var floatValue float64
|
|
|
|
if pos < len(text) && text[pos] == '.' {
|
|
hasDecimal = true
|
|
pos++
|
|
|
|
// Need at least one digit after decimal point
|
|
if pos >= len(text) || !isDigit(text[pos]) {
|
|
return nil, false
|
|
}
|
|
|
|
// Parse the fractional part
|
|
floatValue = float64(intValue)
|
|
fraction := 0.0
|
|
multiplier := 0.1
|
|
|
|
for pos < len(text) && isDigit(text[pos]) {
|
|
digit := float64(text[pos] - '0')
|
|
fraction += digit * multiplier
|
|
multiplier *= 0.1
|
|
pos++
|
|
}
|
|
|
|
floatValue += fraction
|
|
}
|
|
|
|
// Check for exponent
|
|
if pos < len(text) && (text[pos] == 'e' || text[pos] == 'E') {
|
|
hasDecimal = true
|
|
pos++
|
|
|
|
// Parse exponent sign
|
|
expNegative := false
|
|
if pos < len(text) && (text[pos] == '+' || text[pos] == '-') {
|
|
expNegative = text[pos] == '-'
|
|
pos++
|
|
}
|
|
|
|
// Need at least one digit in exponent
|
|
if pos >= len(text) || !isDigit(text[pos]) {
|
|
return nil, false
|
|
}
|
|
|
|
// Parse exponent value
|
|
exp := 0
|
|
for pos < len(text) && isDigit(text[pos]) {
|
|
digit := int(text[pos] - '0')
|
|
exp = exp*10 + digit
|
|
pos++
|
|
}
|
|
|
|
// Apply exponent
|
|
// Convert to float regardless of hasDecimal
|
|
hasDecimal = true
|
|
if floatValue == 0 { // Not set yet
|
|
floatValue = float64(intValue)
|
|
}
|
|
|
|
// Apply exponent using a more efficient approach
|
|
if expNegative {
|
|
// For negative exponents, divide by 10^exp
|
|
multiplier := 1.0
|
|
for i := 0; i < exp; i++ {
|
|
multiplier *= 0.1
|
|
}
|
|
floatValue *= multiplier
|
|
} else {
|
|
// For positive exponents, multiply by 10^exp
|
|
multiplier := 1.0
|
|
for i := 0; i < exp; i++ {
|
|
multiplier *= 10
|
|
}
|
|
floatValue *= multiplier
|
|
}
|
|
}
|
|
|
|
// Check that we consumed the whole string
|
|
if pos < len(text) {
|
|
return nil, false
|
|
}
|
|
|
|
// Return the appropriate value
|
|
if hasDecimal {
|
|
if isNegative {
|
|
return -floatValue, true
|
|
}
|
|
return floatValue, true
|
|
} else {
|
|
if isNegative {
|
|
return -intValue, true
|
|
}
|
|
return intValue, true
|
|
}
|
|
}
|
|
|
|
// Helper functions for character classification
|
|
|
|
// isAlpha checks if a character is a letter or underscore
|
|
func isAlpha(c byte) bool {
|
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
|
|
}
|
|
|
|
// isNameChar checks if a character is valid in a name
|
|
func isNameChar(c byte) bool {
|
|
return isAlpha(c) || isDigit(c)
|
|
}
|
|
|
|
// isDigit is defined elsewhere
|
|
|
|
// Helper for optimized hash calculation
|
|
// This is useful for consistent hashing of variable names or strings
|
|
func calcStringHash(s string) uint32 {
|
|
var h uint32
|
|
|
|
for i := 0; i < len(s); i++ {
|
|
h = 31*h + uint32(s[i])
|
|
}
|
|
|
|
return h
|
|
}
|