Files
daydaytalk-fwutils/target/fwutils/template_engine.lua
2026-01-08 21:58:41 +08:00

435 lines
13 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
local utils = require("utils")
local fw = require("fastweb")
local config = require("fwutils.config")
local request = require("fastweb.request")
local response = require("fastweb.response")
local cjson = require("cjson")
-- 允许的扩展名
local allowed_extensions = {
"shtml",
"html",
"js"
}
local template_engine_bc_this_role = nil
local cfg = nil
local M = {}
function file_get_contents(filepath)
local file, errmsg = io.open(fw.website_dir()..filepath, "r")
if not file then
err.server(errmsg)
end
local content = file:read("*a")
if content == nil or content == "" then
print("file_get_contents error: ",filepath)
return ""
end
local replaced,c2 = M.replace(content)
if replaced then
return c2
end
return content
end
function menu_top()
-- 当前请求路径
local request_path = request.filepath() or ""
-- 取配置
local menuData = require("fwutils.menu").get(string.format("%d",cfg.role_id()))
if not menuData then
return "<!-- no menu config -->"
end
-- 提取并排序主菜单
local menuItems = {}
for name, item in pairs(menuData) do
table.insert(menuItems, {
name = name,
item = item,
sort = item.sort or 0
})
end
table.sort(menuItems, function(a, b)
return a.sort > b.sort
end)
local html = [[
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
]]
-- 渲染主菜单
for _, entry in ipairs(menuItems) do
local name = entry.name
local item = entry.item
local icon = item.icon or ""
local path = item.path or "#"
local activeClass = ""
if item.children then
-- 有子菜单
local isMegamenu = item.megamenu and " dropdown-megamenu" or ""
-- 检查子菜单是否有激活
local hasActiveChild = false
local children = {}
for childName, childItem in pairs(item.children) do
table.insert(children, {
name = childName,
item = childItem,
sort = childItem.sort or 0
})
if not hasActiveChild and (request_path == (childItem.path or "")) then
hasActiveChild = true
end
end
table.sort(children, function(a, b)
return a.sort > b.sort
end)
-- 父菜单激活当前页面是父菜单path或是任一子菜单path
if request_path == path or hasActiveChild then
activeClass = "active-link"
end
html = html .. [[
<li class="nav-item dropdown ]] .. activeClass .. [[">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="]] .. icon .. [["></i> ]] .. name .. [[
</a>
<ul class="dropdown-menu]] .. isMegamenu .. [[">
]]
-- 渲染子菜单
for _, child in ipairs(children) do
local childName = child.name
local childItem = child.item
local childPath = childItem.path or "#"
-- 子菜单不设置激活样式
html = html .. [[
<li>
<a class="dropdown-item" href="]] .. childPath .. [[">
<span>]] .. childName .. [[</span>
</a>
</li>
]]
end
html = html .. [[
</ul>
</li>
]]
else
-- 没有子菜单
-- 简单的“当前页面是否激活”检查
if request_path == path then
activeClass = "active-link"
end
html = html .. [[
<li class="nav-item ]] .. activeClass .. [[">
<a class="nav-link" href="]] .. path .. [[">
<i class="]] .. icon .. [["></i> ]] .. name .. [[
</a>
</li>
]]
end
end
html = html .. [[
</ul>
]]
return [=[
<nav class="navbar navbar-expand-lg">
<div class="container">
<div class="offcanvas offcanvas-end" id="MobileMenu">
<div class="offcanvas-header">
<h5 class="offcanvas-title semibold">Navigation</h5>
<button type="button" class="btn btn-danger btn-sm ms-auto" data-bs-dismiss="offcanvas">
<i class="icon-clear"></i>
</button>
</div>
]=]..html..[[
</div>
</div>
</nav>
]]
end
function template(filepath)
local path = "/public/user/template/"..config.agent[user_agent_id()].template.."/"..filepath
return file_get_contents(path)
end
function teacher_photos()
require("app.app")
local agent_id = user_agent_id()
local teacher = require("app.function.teacher")
local teacher_info = teacher.get_by_id(pint("id"))
if teacher_info == nil then
return ""
end
local photos = cjson.decode(teacher_info.photo)
local content = "<div><div class='home-img'><img src='".. teacher_info.avatar.."' class='img-fluid bg-img' alt=''></div></div>"
for i,v in ipairs(photos) do
if type(v) == "string" then
content = content..[[<div>
<div class="home-img">
<img src="]]..v..[[" class="img-fluid bg-img" alt="">
</div>
</div>
]]
end
end
return content
end
-- 更新
M.update = function(conn)
-- 查询权限表
local select = conn:select()
select:table("fw_template")
select:where_i32("enable","=",1)
local result = select:query()
local bc = {
public = {}
}
while result:next() do
local id = result:get("id")
local role_id = string.format("%d",result:get("role_id"))
local key = result:get("key")
local value = result:get("value")
if bc[role_id] == nil then
bc[role_id] = {}
end
if role_id == "0" then
bc["public"][key] = value
else
bc[role_id][key] = value
end
end
local code = "return " .. require("serpent").serialize(bc, {comment = false})
utils.save_file(fw.website_dir().."/"..(config.path.luabytecode:gsub("%.", "/")).."/template_engine_bc.lua",code)
return true
end
-- 处理
-- @param static_content 静态内容
-- @param cfg 配置
-- @return 是否替换(TRUE则不需要继续处理FALSE则继续处理)
M.handle = function(__cfg)
local function has_ext(ext)
for _, v in ipairs(allowed_extensions) do
if v == ext then
return true
end
end
return false
end
cfg = __cfg
template_engine_bc_this_role = {
template = {
private = {},
public = {}
},
}
local ext = utils.ext(cfg.filepath())
local static_content = nil
if ext ~= nil then
if not has_ext(ext) then
return false
end
-- 读取资源文件
static_content = utils.read_file(fw.website_dir()..cfg.filepath())
if static_content == nil or static_content == "" then
return false
end
else
-- 无需替换的扩展名
return false
end
local template_engine_bc = require(config.path.luabytecode..".template_engine_bc")
template_engine_bc_this_role["template"]["private"] = template_engine_bc[string.format("%d",cfg.role_id())]
template_engine_bc_this_role["template"]["public"] = template_engine_bc["public"]
local replaced,content = M.replace(static_content)
if replaced then
static_content = content
end
-- 执行函数
static_content, n = static_content:gsub("%${<<<%s*(.-)%s*>>>}", function(code)
-- 尝试编译代码Lua 5.2+ 使用 loadLua 5.1 可用 loadstring
local chunk, errmsg = load(code)
if not chunk then
fw.throw_string(errmsg)
end
-- 使用 pcall 安全执行代码块
local status, result = pcall(chunk)
if not status then
fw.throw_string(result)
end
-- 如果代码没有返回值,则替换为空字符串,否则转换成字符串返回
return tostring(result)
end)
if n > 0 then
replaced = true
end
if replaced then
if ext == "shtml" or ext == "html" then
response.header("Content-Type","text/html")
elseif ext == "js" then
response.header("Content-Type","application/javascript")
end
response.send(static_content)
return true
end
return false
end
M.replace = function (content)
if content == nil or content == "" then
return false
end
-- 支持多级kvs替换如 ${people.age}
local function flatten_kvs(tbl, prefix, out)
if tbl == nil then
return {}
end
out = out or {}
prefix = prefix or ""
for k, v in pairs(tbl) do
local key = prefix ~= "" and (prefix .. "." .. k) or k
if type(v) == "table" then
flatten_kvs(v, key, out)
else
out[key] = v
end
end
return out
end
-- 先提取 content 中所有需要替换的占位符
-- 需要正确处理 ${<<<...>>>} 块:块内部的 ${...} 需要替换,但块本身不替换
local placeholders = {}
-- 第一步:提取所有 ${<<<...>>>} 块,临时替换它们,并收集所有占位符
local blocks = {}
local block_index = 0
local temp_content = content:gsub("%${<<<%s*(.-)%s*>>>}", function(block_content)
block_index = block_index + 1
local placeholder = "${__TEMP_BLOCK_" .. block_index .. "__}"
-- 收集块内部的 ${...} 占位符
for ph in string.gmatch(block_content, "%${([^}]+)}") do
placeholders[ph] = true
end
blocks[block_index] = {
placeholder = placeholder,
content = block_content
}
return placeholder
end)
-- 第二步:提取外部(不在 ${<<<...>>>} 块中)的 ${...} 占位符
for placeholder in string.gmatch(temp_content, "%${([^}]+)}") do
-- 忽略临时占位符
if not placeholder:match("^__TEMP_BLOCK_%d+__$") then
placeholders[placeholder] = true
end
end
-- print("PLACEHOLDERS:",cjson.encode(placeholders))
-- 如果没有任何占位符,直接返回
if next(placeholders) == nil then
return false
end
-- 合并所有数据源到一个查找表中(只 flatten 一次)
local value_map = {}
-- PUBLIC
local flat_kvs = flatten_kvs(template_engine_bc_this_role["template"]["public"])
for k, v in pairs(flat_kvs) do
value_map[k] = v
end
-- PRIVATE
flat_kvs = flatten_kvs(template_engine_bc_this_role["template"]["private"])
for k, v in pairs(flat_kvs) do
value_map[k] = v
end
-- REQUEST
flat_kvs = flatten_kvs(request.gets(), "request")
for k, v in pairs(flat_kvs) do
value_map[k] = v
end
-- TOKEN
flat_kvs = flatten_kvs(cfg.user_data(), "token")
for k, v in pairs(flat_kvs) do
value_map[k] = v
end
-- 转义函数:将 Lua 模式特殊字符转义为字面匹配
local function escape_pattern(str)
return str:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")
end
-- 第三步:先处理 ${<<<...>>>} 块内部的占位符
local replaced = false
for i, block in ipairs(blocks) do
local block_content = block.content
for placeholder, _ in pairs(placeholders) do
if value_map[placeholder] ~= nil then
local escaped_placeholder = escape_pattern(placeholder)
local new_content, count = string.gsub(block_content, "%${" .. escaped_placeholder .. "}", tostring(value_map[placeholder]))
if count > 0 then
block_content = new_content
replaced = true
end
end
end
blocks[i].processed_content = block_content
end
-- 第四步:替换外部的 ${...} 占位符(在临时内容中,此时块已被替换为临时占位符)
local n = 0
for placeholder, _ in pairs(placeholders) do
if value_map[placeholder] ~= nil then
local escaped_placeholder = escape_pattern(placeholder)
temp_content, n = string.gsub(temp_content, "%${" .. escaped_placeholder .. "}", tostring(value_map[placeholder]))
if n > 0 then
replaced = true
end
end
end
-- 第五步:恢复 ${<<<...>>>} 块(使用处理后的内容)
for i, block in ipairs(blocks) do
local processed_block = "${<<<" .. blocks[i].processed_content .. ">>>}"
temp_content = temp_content:gsub(block.placeholder:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1"), processed_block)
end
content = temp_content
if replaced then
return true, content
end
return false
end
-- 检查内容中是否有TOKEN变量
-- @return boolean
M.hasToken = function()
if M.static_content == nil or M.static_content == "" then
return false
end
return M.static_content:match("%${token%.") ~= nil
end
return M