template_engine.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. local utils = require("utils")
  2. local fw = require("fastweb")
  3. local config = require("fwutils.config")
  4. local request = require("fastweb.request")
  5. local response = require("fastweb.response")
  6. local cjson = require("cjson")
  7. -- 允许的扩展名
  8. local allowed_extensions = {
  9. "shtml",
  10. "html",
  11. "js"
  12. }
  13. local template_engine_bc_this_role = nil
  14. local cfg = nil
  15. local M = {}
  16. function file_get_contents(filepath)
  17. local file, errmsg = io.open(fw.website_dir()..filepath, "r")
  18. if not file then
  19. err.server(errmsg)
  20. end
  21. local content = file:read("*a")
  22. if content == nil or content == "" then
  23. print("file_get_contents error: ",filepath)
  24. return ""
  25. end
  26. local replaced,c2 = M.replace(content)
  27. if replaced then
  28. return c2
  29. end
  30. return content
  31. end
  32. function menu_top()
  33. -- 当前请求路径
  34. local request_path = request.filepath() or ""
  35. -- 取配置
  36. local menuData = require("fwutils.menu").get(string.format("%d",cfg.role_id()))
  37. if not menuData then
  38. return "<!-- no menu config -->"
  39. end
  40. -- 提取并排序主菜单
  41. local menuItems = {}
  42. for name, item in pairs(menuData) do
  43. table.insert(menuItems, {
  44. name = name,
  45. item = item,
  46. sort = item.sort or 0
  47. })
  48. end
  49. table.sort(menuItems, function(a, b)
  50. return a.sort > b.sort
  51. end)
  52. local html = [[
  53. <ul class="navbar-nav me-auto mb-2 mb-lg-0">
  54. ]]
  55. -- 渲染主菜单
  56. for _, entry in ipairs(menuItems) do
  57. local name = entry.name
  58. local item = entry.item
  59. local icon = item.icon or ""
  60. local path = item.path or "#"
  61. local activeClass = ""
  62. if item.children then
  63. -- 有子菜单
  64. local isMegamenu = item.megamenu and " dropdown-megamenu" or ""
  65. -- 检查子菜单是否有激活
  66. local hasActiveChild = false
  67. local children = {}
  68. for childName, childItem in pairs(item.children) do
  69. table.insert(children, {
  70. name = childName,
  71. item = childItem,
  72. sort = childItem.sort or 0
  73. })
  74. if not hasActiveChild and (request_path == (childItem.path or "")) then
  75. hasActiveChild = true
  76. end
  77. end
  78. table.sort(children, function(a, b)
  79. return a.sort > b.sort
  80. end)
  81. -- 父菜单激活:当前页面是父菜单path,或是任一子菜单path
  82. if request_path == path or hasActiveChild then
  83. activeClass = "active-link"
  84. end
  85. html = html .. [[
  86. <li class="nav-item dropdown ]] .. activeClass .. [[">
  87. <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
  88. <i class="]] .. icon .. [["></i> ]] .. name .. [[
  89. </a>
  90. <ul class="dropdown-menu]] .. isMegamenu .. [[">
  91. ]]
  92. -- 渲染子菜单
  93. for _, child in ipairs(children) do
  94. local childName = child.name
  95. local childItem = child.item
  96. local childPath = childItem.path or "#"
  97. -- 子菜单不设置激活样式
  98. html = html .. [[
  99. <li>
  100. <a class="dropdown-item" href="]] .. childPath .. [[">
  101. <span>]] .. childName .. [[</span>
  102. </a>
  103. </li>
  104. ]]
  105. end
  106. html = html .. [[
  107. </ul>
  108. </li>
  109. ]]
  110. else
  111. -- 没有子菜单
  112. -- 简单的“当前页面是否激活”检查
  113. if request_path == path then
  114. activeClass = "active-link"
  115. end
  116. html = html .. [[
  117. <li class="nav-item ]] .. activeClass .. [[">
  118. <a class="nav-link" href="]] .. path .. [[">
  119. <i class="]] .. icon .. [["></i> ]] .. name .. [[
  120. </a>
  121. </li>
  122. ]]
  123. end
  124. end
  125. html = html .. [[
  126. </ul>
  127. ]]
  128. return [=[
  129. <nav class="navbar navbar-expand-lg">
  130. <div class="container">
  131. <div class="offcanvas offcanvas-end" id="MobileMenu">
  132. <div class="offcanvas-header">
  133. <h5 class="offcanvas-title semibold">Navigation</h5>
  134. <button type="button" class="btn btn-danger btn-sm ms-auto" data-bs-dismiss="offcanvas">
  135. <i class="icon-clear"></i>
  136. </button>
  137. </div>
  138. ]=]..html..[[
  139. </div>
  140. </div>
  141. </nav>
  142. ]]
  143. end
  144. function template(filepath)
  145. local path = "/public/user/template/"..config.agent[user_agent_id()].template.."/"..filepath
  146. return file_get_contents(path)
  147. end
  148. function teacher_photos()
  149. require("app.app")
  150. local agent_id = user_agent_id()
  151. local teacher = require("app.function.teacher")
  152. local teacher_info = teacher.get_by_id(pint("id"))
  153. if teacher_info == nil then
  154. return ""
  155. end
  156. local photos = cjson.decode(teacher_info.photo)
  157. local content = "<div><div class='home-img'><img src='".. teacher_info.avatar.."' class='img-fluid bg-img' alt=''></div></div>"
  158. for i,v in ipairs(photos) do
  159. if type(v) == "string" then
  160. content = content..[[<div>
  161. <div class="home-img">
  162. <img src="]]..v..[[" class="img-fluid bg-img" alt="">
  163. </div>
  164. </div>
  165. ]]
  166. end
  167. end
  168. return content
  169. end
  170. -- 更新
  171. M.update = function(conn)
  172. -- 查询权限表
  173. local select = conn:select()
  174. select:table("fw_template")
  175. select:where_i32("enable","=",1)
  176. local result = select:query()
  177. local bc = {
  178. public = {}
  179. }
  180. while result:next() do
  181. local id = result:get("id")
  182. local role_id = string.format("%d",result:get("role_id"))
  183. local key = result:get("key")
  184. local value = result:get("value")
  185. if bc[role_id] == nil then
  186. bc[role_id] = {}
  187. end
  188. if role_id == "0" then
  189. bc["public"][key] = value
  190. else
  191. bc[role_id][key] = value
  192. end
  193. end
  194. local code = "return " .. require("serpent").serialize(bc, {comment = false})
  195. utils.save_file(fw.website_dir().."/"..(config.path.luabytecode:gsub("%.", "/")).."/template_engine_bc.lua",code)
  196. return true
  197. end
  198. -- 处理
  199. -- @param static_content 静态内容
  200. -- @param cfg 配置
  201. -- @return 是否替换(TRUE则不需要继续处理,FALSE则继续处理)
  202. M.handle = function(__cfg)
  203. local function has_ext(ext)
  204. for _, v in ipairs(allowed_extensions) do
  205. if v == ext then
  206. return true
  207. end
  208. end
  209. return false
  210. end
  211. cfg = __cfg
  212. template_engine_bc_this_role = {
  213. template = {
  214. private = {},
  215. public = {}
  216. },
  217. }
  218. local ext = utils.ext(cfg.filepath())
  219. local static_content = nil
  220. if ext ~= nil then
  221. if not has_ext(ext) then
  222. return false
  223. end
  224. -- 读取资源文件
  225. static_content = utils.read_file(fw.website_dir()..cfg.filepath())
  226. if static_content == nil or static_content == "" then
  227. return false
  228. end
  229. else
  230. -- 无需替换的扩展名
  231. return false
  232. end
  233. local template_engine_bc = require(config.path.luabytecode..".template_engine_bc")
  234. template_engine_bc_this_role["template"]["private"] = template_engine_bc[string.format("%d",cfg.role_id())]
  235. template_engine_bc_this_role["template"]["public"] = template_engine_bc["public"]
  236. local replaced,content = M.replace(static_content)
  237. if replaced then
  238. static_content = content
  239. end
  240. -- 执行函数
  241. static_content, n = static_content:gsub("%${<<<%s*(.-)%s*>>>}", function(code)
  242. -- 尝试编译代码(Lua 5.2+ 使用 load;Lua 5.1 可用 loadstring)
  243. local chunk, errmsg = load(code)
  244. if not chunk then
  245. fw.throw_string(errmsg)
  246. end
  247. -- 使用 pcall 安全执行代码块
  248. local status, result = pcall(chunk)
  249. if not status then
  250. fw.throw_string(result)
  251. end
  252. -- 如果代码没有返回值,则替换为空字符串,否则转换成字符串返回
  253. return tostring(result)
  254. end)
  255. if n > 0 then
  256. replaced = true
  257. end
  258. if replaced then
  259. if ext == "shtml" or ext == "html" then
  260. response.header("Content-Type","text/html")
  261. elseif ext == "js" then
  262. response.header("Content-Type","application/javascript")
  263. end
  264. response.send(static_content)
  265. return true
  266. end
  267. return false
  268. end
  269. M.replace = function (content)
  270. if content == nil or content == "" then
  271. return false
  272. end
  273. -- 支持多级kvs替换,如 ${people.age}
  274. local function flatten_kvs(tbl, prefix, out)
  275. if tbl == nil then
  276. return {}
  277. end
  278. out = out or {}
  279. prefix = prefix or ""
  280. for k, v in pairs(tbl) do
  281. local key = prefix ~= "" and (prefix .. "." .. k) or k
  282. if type(v) == "table" then
  283. flatten_kvs(v, key, out)
  284. else
  285. out[key] = v
  286. end
  287. end
  288. return out
  289. end
  290. -- 先提取 content 中所有需要替换的占位符
  291. -- 需要正确处理 ${<<<...>>>} 块:块内部的 ${...} 需要替换,但块本身不替换
  292. local placeholders = {}
  293. -- 第一步:提取所有 ${<<<...>>>} 块,临时替换它们,并收集所有占位符
  294. local blocks = {}
  295. local block_index = 0
  296. local temp_content = content:gsub("%${<<<%s*(.-)%s*>>>}", function(block_content)
  297. block_index = block_index + 1
  298. local placeholder = "${__TEMP_BLOCK_" .. block_index .. "__}"
  299. -- 收集块内部的 ${...} 占位符
  300. for ph in string.gmatch(block_content, "%${([^}]+)}") do
  301. placeholders[ph] = true
  302. end
  303. blocks[block_index] = {
  304. placeholder = placeholder,
  305. content = block_content
  306. }
  307. return placeholder
  308. end)
  309. -- 第二步:提取外部(不在 ${<<<...>>>} 块中)的 ${...} 占位符
  310. for placeholder in string.gmatch(temp_content, "%${([^}]+)}") do
  311. -- 忽略临时占位符
  312. if not placeholder:match("^__TEMP_BLOCK_%d+__$") then
  313. placeholders[placeholder] = true
  314. end
  315. end
  316. -- print("PLACEHOLDERS:",cjson.encode(placeholders))
  317. -- 如果没有任何占位符,直接返回
  318. if next(placeholders) == nil then
  319. return false
  320. end
  321. -- 合并所有数据源到一个查找表中(只 flatten 一次)
  322. local value_map = {}
  323. -- PUBLIC
  324. local flat_kvs = flatten_kvs(template_engine_bc_this_role["template"]["public"])
  325. for k, v in pairs(flat_kvs) do
  326. value_map[k] = v
  327. end
  328. -- PRIVATE
  329. flat_kvs = flatten_kvs(template_engine_bc_this_role["template"]["private"])
  330. for k, v in pairs(flat_kvs) do
  331. value_map[k] = v
  332. end
  333. -- REQUEST
  334. flat_kvs = flatten_kvs(request.gets(), "request")
  335. for k, v in pairs(flat_kvs) do
  336. value_map[k] = v
  337. end
  338. -- TOKEN
  339. flat_kvs = flatten_kvs(cfg.user_data(), "token")
  340. for k, v in pairs(flat_kvs) do
  341. value_map[k] = v
  342. end
  343. -- 转义函数:将 Lua 模式特殊字符转义为字面匹配
  344. local function escape_pattern(str)
  345. return str:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")
  346. end
  347. -- 第三步:先处理 ${<<<...>>>} 块内部的占位符
  348. local replaced = false
  349. for i, block in ipairs(blocks) do
  350. local block_content = block.content
  351. for placeholder, _ in pairs(placeholders) do
  352. if value_map[placeholder] ~= nil then
  353. local escaped_placeholder = escape_pattern(placeholder)
  354. local new_content, count = string.gsub(block_content, "%${" .. escaped_placeholder .. "}", tostring(value_map[placeholder]))
  355. if count > 0 then
  356. block_content = new_content
  357. replaced = true
  358. end
  359. end
  360. end
  361. blocks[i].processed_content = block_content
  362. end
  363. -- 第四步:替换外部的 ${...} 占位符(在临时内容中,此时块已被替换为临时占位符)
  364. local n = 0
  365. for placeholder, _ in pairs(placeholders) do
  366. if value_map[placeholder] ~= nil then
  367. local escaped_placeholder = escape_pattern(placeholder)
  368. temp_content, n = string.gsub(temp_content, "%${" .. escaped_placeholder .. "}", tostring(value_map[placeholder]))
  369. if n > 0 then
  370. replaced = true
  371. end
  372. end
  373. end
  374. -- 第五步:恢复 ${<<<...>>>} 块(使用处理后的内容)
  375. for i, block in ipairs(blocks) do
  376. local processed_block = "${<<<" .. blocks[i].processed_content .. ">>>}"
  377. temp_content = temp_content:gsub(block.placeholder:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1"), processed_block)
  378. end
  379. content = temp_content
  380. if replaced then
  381. return true, content
  382. end
  383. return false
  384. end
  385. -- 检查内容中是否有TOKEN变量
  386. -- @return boolean
  387. M.hasToken = function()
  388. if M.static_content == nil or M.static_content == "" then
  389. return false
  390. end
  391. return M.static_content:match("%${token%.") ~= nil
  392. end
  393. return M