render function fixes, http optimizations
This commit is contained in:
parent
ca85e735b0
commit
9f71fd7c0f
@ -2,6 +2,7 @@ package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"Moonshark/routers"
|
||||
@ -26,6 +27,7 @@ type Server struct {
|
||||
config *config.Config
|
||||
sessionManager *sessions.SessionManager
|
||||
errorConfig utils.ErrorPageConfig
|
||||
ctxPool sync.Pool
|
||||
}
|
||||
|
||||
// New creates a new HTTP server
|
||||
@ -45,6 +47,11 @@ func New(luaRouter *routers.LuaRouter, staticRouter *routers.StaticRouter,
|
||||
OverrideDir: overrideDir,
|
||||
DebugMode: debugMode,
|
||||
},
|
||||
ctxPool: sync.Pool{
|
||||
New: func() any {
|
||||
return make(map[string]any, 8)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
server.fasthttpServer = &fasthttp.Server{
|
||||
@ -78,53 +85,49 @@ func (s *Server) Shutdown(ctx context.Context) error {
|
||||
// handleRequest processes the HTTP request
|
||||
func (s *Server) handleRequest(ctx *fasthttp.RequestCtx) {
|
||||
start := time.Now()
|
||||
method := string(ctx.Method())
|
||||
path := string(ctx.Path())
|
||||
methodBytes := ctx.Method()
|
||||
pathBytes := ctx.Path()
|
||||
|
||||
// Only convert to string once
|
||||
method := string(methodBytes)
|
||||
path := string(pathBytes)
|
||||
|
||||
if s.debugMode && path == "/debug/stats" {
|
||||
s.handleDebugStats(ctx)
|
||||
|
||||
if s.loggingEnabled {
|
||||
duration := time.Since(start)
|
||||
LogRequest(ctx.Response.StatusCode(), method, path, duration)
|
||||
LogRequest(ctx.Response.StatusCode(), method, path, time.Since(start))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
s.processRequest(ctx)
|
||||
s.processRequest(ctx, method, path)
|
||||
|
||||
if s.loggingEnabled {
|
||||
duration := time.Since(start)
|
||||
LogRequest(ctx.Response.StatusCode(), method, path, duration)
|
||||
LogRequest(ctx.Response.StatusCode(), method, path, time.Since(start))
|
||||
}
|
||||
}
|
||||
|
||||
// In server.go, modify the processRequest method
|
||||
func (s *Server) processRequest(ctx *fasthttp.RequestCtx) {
|
||||
method := string(ctx.Method())
|
||||
path := string(ctx.Path())
|
||||
|
||||
func (s *Server) processRequest(ctx *fasthttp.RequestCtx, method, path string) {
|
||||
logger.Debug("Processing request %s %s", method, path)
|
||||
|
||||
params := &routers.Params{}
|
||||
bytecode, scriptPath, routeErr, found := s.luaRouter.GetRouteInfo(method, path, params)
|
||||
|
||||
// Check if we found a route but it has an error or no valid bytecode
|
||||
if found && (len(bytecode) == 0 || routeErr != nil) {
|
||||
errorMsg := "Route exists but failed to compile. Check server logs for details."
|
||||
|
||||
if routeErr != nil {
|
||||
errorMsg = routeErr.Error()
|
||||
}
|
||||
|
||||
logger.Error("%s %s - %s", method, path, errorMsg)
|
||||
|
||||
ctx.SetContentType("text/html; charset=utf-8")
|
||||
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
||||
errorHTML := utils.InternalErrorPage(s.errorConfig, path, errorMsg)
|
||||
ctx.SetBody([]byte(errorHTML))
|
||||
ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, path, errorMsg)))
|
||||
return
|
||||
} else if found {
|
||||
}
|
||||
|
||||
if found {
|
||||
logger.Debug("Found Lua route match for %s %s with %d params", method, path, params.Count)
|
||||
s.handleLuaRoute(ctx, bytecode, scriptPath, params, method, path)
|
||||
return
|
||||
@ -143,58 +146,62 @@ func (s *Server) processRequest(ctx *fasthttp.RequestCtx) {
|
||||
}
|
||||
|
||||
// handleLuaRoute executes a Lua route
|
||||
func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string, params *routers.Params, method string, path string) {
|
||||
// Create context for Lua execution
|
||||
func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string, params *routers.Params, method, path string) {
|
||||
luaCtx := runner.NewHTTPContext(ctx)
|
||||
defer luaCtx.Release()
|
||||
|
||||
host := string(ctx.Host())
|
||||
// Get pooled map for session data
|
||||
sessionMap := s.ctxPool.Get().(map[string]any)
|
||||
defer func() {
|
||||
// Clear and return to pool
|
||||
for k := range sessionMap {
|
||||
delete(sessionMap, k)
|
||||
}
|
||||
s.ctxPool.Put(sessionMap)
|
||||
}()
|
||||
|
||||
session := s.sessionManager.GetSessionFromRequest(ctx)
|
||||
sessionMap["id"] = session.ID
|
||||
sessionMap["data"] = session.Data
|
||||
|
||||
luaCtx.Set("method", method)
|
||||
luaCtx.Set("path", path)
|
||||
luaCtx.Set("host", host)
|
||||
|
||||
session := s.sessionManager.GetSessionFromRequest(ctx)
|
||||
luaCtx.Set("session", map[string]any{
|
||||
"id": session.ID,
|
||||
"data": session.Data,
|
||||
})
|
||||
luaCtx.Set("host", string(ctx.Host())) // Only convert when needed
|
||||
luaCtx.Set("session", sessionMap)
|
||||
|
||||
// Optimize params handling
|
||||
if params.Count > 0 {
|
||||
paramMap := make(map[string]any, params.Count)
|
||||
paramMap := make(map[string]any, params.Count) // Pre-size
|
||||
for i, key := range params.Keys {
|
||||
paramMap[key] = params.Values[i]
|
||||
}
|
||||
luaCtx.Set("params", paramMap)
|
||||
} else {
|
||||
luaCtx.Set("params", make(map[string]any))
|
||||
luaCtx.Set("params", emptyMap) // Reuse empty map
|
||||
}
|
||||
|
||||
// Optimize form handling for POST methods
|
||||
if method == "POST" || method == "PUT" || method == "PATCH" {
|
||||
formData, err := ParseForm(ctx)
|
||||
if err == nil && len(formData) > 0 {
|
||||
if formData, err := ParseForm(ctx); err == nil {
|
||||
luaCtx.Set("form", formData)
|
||||
} else if err != nil {
|
||||
logger.Warning("Error parsing form: %v", err)
|
||||
luaCtx.Set("form", make(map[string]any))
|
||||
} else {
|
||||
luaCtx.Set("form", make(map[string]any))
|
||||
logger.Warning("Error parsing form: %v", err)
|
||||
luaCtx.Set("form", emptyMap)
|
||||
}
|
||||
} else {
|
||||
luaCtx.Set("form", make(map[string]any))
|
||||
luaCtx.Set("form", emptyMap)
|
||||
}
|
||||
|
||||
response, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath)
|
||||
if err != nil {
|
||||
logger.Error("Error executing Lua route: %v", err)
|
||||
|
||||
ctx.SetContentType("text/html; charset=utf-8")
|
||||
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
||||
errorHTML := utils.InternalErrorPage(s.errorConfig, path, err.Error())
|
||||
ctx.SetBody([]byte(errorHTML))
|
||||
ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, path, err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
// Session handling optimization
|
||||
if _, clearAll := response.SessionData["__clear_all"]; clearAll {
|
||||
session.Clear()
|
||||
delete(response.SessionData, "__clear_all")
|
||||
@ -207,8 +214,8 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
|
||||
session.Set(k, v)
|
||||
}
|
||||
}
|
||||
s.sessionManager.ApplySessionCookie(ctx, session)
|
||||
|
||||
s.sessionManager.ApplySessionCookie(ctx, session)
|
||||
runner.ApplyResponse(response, ctx)
|
||||
runner.ReleaseResponse(response)
|
||||
}
|
||||
|
145
http/utils.go
145
http/utils.go
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"Moonshark/utils/logger"
|
||||
@ -13,63 +14,91 @@ import (
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
var emptyMap = make(map[string]any)
|
||||
|
||||
var (
|
||||
stringPool = sync.Pool{
|
||||
New: func() any {
|
||||
return make([]string, 0, 4)
|
||||
},
|
||||
}
|
||||
formDataPool = sync.Pool{
|
||||
New: func() any {
|
||||
return make(map[string]any, 16)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// LogRequest logs an HTTP request with its status code and duration
|
||||
func LogRequest(statusCode int, method, path string, duration time.Duration) {
|
||||
var statusColor, resetColor, methodColor string
|
||||
var statusColor, methodColor string
|
||||
|
||||
if statusCode >= 200 && statusCode < 300 {
|
||||
// Simplified color assignment
|
||||
switch {
|
||||
case statusCode < 300:
|
||||
statusColor = "\u001b[32m" // Green for 2xx
|
||||
} else if statusCode >= 300 && statusCode < 400 {
|
||||
case statusCode < 400:
|
||||
statusColor = "\u001b[36m" // Cyan for 3xx
|
||||
} else if statusCode >= 400 && statusCode < 500 {
|
||||
case statusCode < 500:
|
||||
statusColor = "\u001b[33m" // Yellow for 4xx
|
||||
} else {
|
||||
statusColor = "\u001b[31m" // Red for 5xx and others
|
||||
default:
|
||||
statusColor = "\u001b[31m" // Red for 5xx+
|
||||
}
|
||||
|
||||
switch method {
|
||||
case "GET":
|
||||
methodColor = "\u001b[32m" // Green
|
||||
methodColor = "\u001b[32m"
|
||||
case "POST":
|
||||
methodColor = "\u001b[34m" // Blue
|
||||
methodColor = "\u001b[34m"
|
||||
case "PUT":
|
||||
methodColor = "\u001b[33m" // Yellow
|
||||
methodColor = "\u001b[33m"
|
||||
case "DELETE":
|
||||
methodColor = "\u001b[31m" // Red
|
||||
methodColor = "\u001b[31m"
|
||||
default:
|
||||
methodColor = "\u001b[35m" // Magenta for others
|
||||
methodColor = "\u001b[35m"
|
||||
}
|
||||
|
||||
resetColor = "\u001b[0m"
|
||||
|
||||
// Optimized duration formatting
|
||||
var durationStr string
|
||||
if duration.Milliseconds() < 1 {
|
||||
durationStr = fmt.Sprintf("%.2fµs", float64(duration.Microseconds()))
|
||||
} else if duration.Milliseconds() < 1000 {
|
||||
durationStr = fmt.Sprintf("%.2fms", float64(duration.Microseconds())/1000)
|
||||
micros := duration.Microseconds()
|
||||
if micros < 1000 {
|
||||
durationStr = fmt.Sprintf("%.0fµs", float64(micros))
|
||||
} else if micros < 1000000 {
|
||||
durationStr = fmt.Sprintf("%.1fms", float64(micros)/1000)
|
||||
} else {
|
||||
durationStr = fmt.Sprintf("%.2fs", duration.Seconds())
|
||||
}
|
||||
|
||||
logger.Server("%s%d%s %s%s%s %s %s",
|
||||
statusColor, statusCode, resetColor,
|
||||
methodColor, method, resetColor,
|
||||
logger.Server("%s%d\u001b[0m %s%s\u001b[0m %s %s",
|
||||
statusColor, statusCode,
|
||||
methodColor, method,
|
||||
path, durationStr)
|
||||
}
|
||||
|
||||
// QueryToLua converts HTTP query args to a Lua-friendly map
|
||||
func QueryToLua(ctx *fasthttp.RequestCtx) map[string]any {
|
||||
queryMap := make(map[string]any)
|
||||
args := ctx.QueryArgs()
|
||||
if args.Len() == 0 {
|
||||
return emptyMap
|
||||
}
|
||||
|
||||
ctx.QueryArgs().VisitAll(func(key, value []byte) {
|
||||
queryMap := make(map[string]any, args.Len()) // Pre-size
|
||||
|
||||
args.VisitAll(func(key, value []byte) {
|
||||
k := string(key)
|
||||
v := string(value)
|
||||
|
||||
if existing, ok := queryMap[k]; ok {
|
||||
if arr, ok := existing.([]string); ok {
|
||||
queryMap[k] = append(arr, v)
|
||||
} else if str, ok := existing.(string); ok {
|
||||
queryMap[k] = []string{str, v}
|
||||
if existing, exists := queryMap[k]; exists {
|
||||
// Handle multiple values more efficiently
|
||||
switch typed := existing.(type) {
|
||||
case []string:
|
||||
queryMap[k] = append(typed, v)
|
||||
case string:
|
||||
// Get slice from pool
|
||||
slice := stringPool.Get().([]string)
|
||||
slice = slice[:0] // Reset length
|
||||
slice = append(slice, typed, v)
|
||||
queryMap[k] = slice
|
||||
}
|
||||
} else {
|
||||
queryMap[k] = v
|
||||
@ -81,21 +110,36 @@ func QueryToLua(ctx *fasthttp.RequestCtx) map[string]any {
|
||||
|
||||
// ParseForm extracts form data from a request
|
||||
func ParseForm(ctx *fasthttp.RequestCtx) (map[string]any, error) {
|
||||
formData := make(map[string]any)
|
||||
contentType := string(ctx.Request.Header.ContentType())
|
||||
|
||||
if strings.Contains(string(ctx.Request.Header.ContentType()), "multipart/form-data") {
|
||||
if strings.Contains(contentType, "multipart/form-data") {
|
||||
return parseMultipartForm(ctx)
|
||||
}
|
||||
|
||||
ctx.PostArgs().VisitAll(func(key, value []byte) {
|
||||
args := ctx.PostArgs()
|
||||
if args.Len() == 0 {
|
||||
return emptyMap, nil
|
||||
}
|
||||
|
||||
formData := formDataPool.Get().(map[string]any)
|
||||
// Clear the map (should already be clean from pool)
|
||||
for k := range formData {
|
||||
delete(formData, k)
|
||||
}
|
||||
|
||||
args.VisitAll(func(key, value []byte) {
|
||||
k := string(key)
|
||||
v := string(value)
|
||||
|
||||
if existing, ok := formData[k]; ok {
|
||||
if arr, ok := existing.([]string); ok {
|
||||
formData[k] = append(arr, v)
|
||||
} else if str, ok := existing.(string); ok {
|
||||
formData[k] = []string{str, v}
|
||||
if existing, exists := formData[k]; exists {
|
||||
switch typed := existing.(type) {
|
||||
case []string:
|
||||
formData[k] = append(typed, v)
|
||||
case string:
|
||||
slice := stringPool.Get().([]string)
|
||||
slice = slice[:0]
|
||||
slice = append(slice, typed, v)
|
||||
formData[k] = slice
|
||||
}
|
||||
} else {
|
||||
formData[k] = v
|
||||
@ -107,36 +151,43 @@ func ParseForm(ctx *fasthttp.RequestCtx) (map[string]any, error) {
|
||||
|
||||
// parseMultipartForm handles multipart/form-data requests
|
||||
func parseMultipartForm(ctx *fasthttp.RequestCtx) (map[string]any, error) {
|
||||
formData := make(map[string]any)
|
||||
|
||||
form, err := ctx.MultipartForm()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
formData := formDataPool.Get().(map[string]any)
|
||||
for k := range formData {
|
||||
delete(formData, k)
|
||||
}
|
||||
|
||||
// Process form values
|
||||
for key, values := range form.Value {
|
||||
if len(values) == 1 {
|
||||
switch len(values) {
|
||||
case 0:
|
||||
// Skip empty
|
||||
case 1:
|
||||
formData[key] = values[0]
|
||||
} else if len(values) > 1 {
|
||||
default:
|
||||
formData[key] = values
|
||||
}
|
||||
}
|
||||
|
||||
// Process files if present
|
||||
if len(form.File) > 0 {
|
||||
files := make(map[string]any)
|
||||
|
||||
files := make(map[string]any, len(form.File))
|
||||
for fieldName, fileHeaders := range form.File {
|
||||
if len(fileHeaders) == 1 {
|
||||
switch len(fileHeaders) {
|
||||
case 1:
|
||||
files[fieldName] = fileInfoToMap(fileHeaders[0])
|
||||
} else if len(fileHeaders) > 1 {
|
||||
fileInfos := make([]map[string]any, 0, len(fileHeaders))
|
||||
for _, fh := range fileHeaders {
|
||||
fileInfos = append(fileInfos, fileInfoToMap(fh))
|
||||
default:
|
||||
fileInfos := make([]map[string]any, len(fileHeaders))
|
||||
for i, fh := range fileHeaders {
|
||||
fileInfos[i] = fileInfoToMap(fh)
|
||||
}
|
||||
files[fieldName] = fileInfos
|
||||
}
|
||||
}
|
||||
|
||||
formData["_files"] = files
|
||||
}
|
||||
|
||||
|
@ -438,17 +438,14 @@ _G.render = function(template_str, env)
|
||||
|
||||
local pos, chunks = 1, {}
|
||||
while pos <= #template_str do
|
||||
local escaped_start = template_str:find("<%?", pos, true)
|
||||
local unescaped_start = template_str:find("<!", pos, true)
|
||||
local code_start = template_str:find("<%", pos, true)
|
||||
local unescaped_start = template_str:find("{{{", pos, true)
|
||||
local escaped_start = template_str:find("{{", pos, true)
|
||||
|
||||
local start, tag_type, open_len
|
||||
if escaped_start and (not unescaped_start or escaped_start < unescaped_start) and (not code_start or escaped_start < code_start) then
|
||||
if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then
|
||||
start, tag_type, open_len = unescaped_start, "-", 3
|
||||
elseif escaped_start then
|
||||
start, tag_type, open_len = escaped_start, "=", 2
|
||||
elseif unescaped_start and (not code_start or unescaped_start < code_start) then
|
||||
start, tag_type, open_len = unescaped_start, "-", 2
|
||||
elseif code_start then
|
||||
start, tag_type, open_len = code_start, "code", 2
|
||||
else
|
||||
table.insert(chunks, template_str:sub(pos))
|
||||
break
|
||||
@ -459,25 +456,19 @@ _G.render = function(template_str, env)
|
||||
end
|
||||
|
||||
pos = start + open_len
|
||||
local close_tag = tag_type == "=" and "?>" or tag_type == "-" and "!>" or "%>"
|
||||
local close_tag = tag_type == "-" and "}}}" or "}}"
|
||||
local close_start, close_stop = template_str:find(close_tag, pos, true)
|
||||
if not close_start then
|
||||
error("Failed to find closing tag at position " .. pos)
|
||||
end
|
||||
|
||||
local trim_newline = false
|
||||
if template_str:sub(close_start-1, close_start-1) == "-" then
|
||||
close_start = close_start - 1
|
||||
trim_newline = true
|
||||
end
|
||||
local code = template_str:sub(pos, close_start-1):match("^%s*(.-)%s*$")
|
||||
|
||||
local code = template_str:sub(pos, close_start-1)
|
||||
table.insert(chunks, {tag_type, code, pos})
|
||||
-- Check if it's a simple variable name for escaped output
|
||||
local is_simple_var = tag_type == "=" and code:match("^[%w_]+$")
|
||||
|
||||
table.insert(chunks, {tag_type, code, pos, is_simple_var})
|
||||
pos = close_stop + 1
|
||||
if trim_newline and template_str:sub(pos, pos) == "\n" then
|
||||
pos = pos + 1
|
||||
end
|
||||
end
|
||||
|
||||
local buffer = {"local _tostring, _escape, _b, _b_i = ...\n"}
|
||||
@ -488,16 +479,16 @@ _G.render = function(template_str, env)
|
||||
table.insert(buffer, "_b[_b_i] = " .. string.format("%q", chunk) .. "\n")
|
||||
else
|
||||
t = chunk[1]
|
||||
if t == "code" then
|
||||
table.insert(buffer, "--[[" .. chunk[3] .. "]] " .. chunk[2] .. "\n")
|
||||
elseif t == "=" or t == "-" then
|
||||
table.insert(buffer, "_b_i = _b_i + 1\n")
|
||||
table.insert(buffer, "--[[" .. chunk[3] .. "]] _b[_b_i] = ")
|
||||
if t == "=" then
|
||||
table.insert(buffer, "_escape(_tostring(" .. chunk[2] .. "))\n")
|
||||
if t == "=" then
|
||||
if chunk[4] then -- is_simple_var
|
||||
table.insert(buffer, "_b_i = _b_i + 1\n")
|
||||
table.insert(buffer, "--[[" .. chunk[3] .. "]] _b[_b_i] = _escape(_tostring(" .. chunk[2] .. "))\n")
|
||||
else
|
||||
table.insert(buffer, "_tostring(" .. chunk[2] .. ")\n")
|
||||
table.insert(buffer, "--[[" .. chunk[3] .. "]] " .. chunk[2] .. "\n")
|
||||
end
|
||||
elseif t == "-" then
|
||||
table.insert(buffer, "_b_i = _b_i + 1\n")
|
||||
table.insert(buffer, "--[[" .. chunk[3] .. "]] _b[_b_i] = _tostring(" .. chunk[2] .. ")\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -521,14 +512,14 @@ _G.parse = function(template_str, env)
|
||||
env = env or {}
|
||||
|
||||
while pos <= #template_str do
|
||||
local escaped_start, escaped_end, escaped_name = template_str:find("<%?%s*([%w_]+)%s*%?>", pos)
|
||||
local unescaped_start, unescaped_end, unescaped_name = template_str:find("<!%s*([%w_]+)%s*!>", pos)
|
||||
local unescaped_start, unescaped_end, unescaped_name = template_str:find("{{{%s*([%w_]+)%s*}}}", pos)
|
||||
local escaped_start, escaped_end, escaped_name = template_str:find("{{%s*([%w_]+)%s*}}", pos)
|
||||
|
||||
local next_pos, placeholder_end, name, escaped
|
||||
if escaped_start and (not unescaped_start or escaped_start < unescaped_start) then
|
||||
next_pos, placeholder_end, name, escaped = escaped_start, escaped_end, escaped_name, true
|
||||
elseif unescaped_start then
|
||||
if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then
|
||||
next_pos, placeholder_end, name, escaped = unescaped_start, unescaped_end, unescaped_name, false
|
||||
elseif escaped_start then
|
||||
next_pos, placeholder_end, name, escaped = escaped_start, escaped_end, escaped_name, true
|
||||
else
|
||||
local text = template_str:sub(pos)
|
||||
if text and #text > 0 then
|
||||
@ -561,14 +552,14 @@ _G.iparse = function(template_str, values)
|
||||
values = values or {}
|
||||
|
||||
while pos <= #template_str do
|
||||
local escaped_start, escaped_end = template_str:find("<%?>", pos, true)
|
||||
local unescaped_start, unescaped_end = template_str:find("<!>", pos, true)
|
||||
local unescaped_start, unescaped_end = template_str:find("{{{}}}", pos, true)
|
||||
local escaped_start, escaped_end = template_str:find("{{}}", pos, true)
|
||||
|
||||
local next_pos, placeholder_end, escaped
|
||||
if escaped_start and (not unescaped_start or escaped_start < unescaped_start) then
|
||||
next_pos, placeholder_end, escaped = escaped_start, escaped_end, true
|
||||
elseif unescaped_start then
|
||||
if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then
|
||||
next_pos, placeholder_end, escaped = unescaped_start, unescaped_end, false
|
||||
elseif escaped_start then
|
||||
next_pos, placeholder_end, escaped = escaped_start, escaped_end, true
|
||||
else
|
||||
local text = template_str:sub(pos)
|
||||
if text and #text > 0 then
|
||||
|
Loading…
x
Reference in New Issue
Block a user