* new project: ff-luci - Freifunk Lua Configuration Interface
[project/luci.git] / src / ffluci / template.lua
1 --[[
2 FFLuCI - Template Parser
3
4 Description:
5 A template parser supporting includes, translations, Lua code blocks
6 and more. It can be used either as a compiler or as an interpreter.
7
8 FileId: $Id$
9
10 License:
11 Copyright 2008 Steven Barth <steven@midlink.org>
12
13 Licensed under the Apache License, Version 2.0 (the "License");
14 you may not use this file except in compliance with the License.
15 You may obtain a copy of the License at
16
17 http://www.apache.org/licenses/LICENSE-2.0
18
19 Unless required by applicable law or agreed to in writing, software
20 distributed under the License is distributed on an "AS IS" BASIS,
21 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 See the License for the specific language governing permissions and
23 limitations under the License.
24
25 ]]--
26 module("ffluci.template", package.seeall)
27
28 require("ffluci.config")
29 require("ffluci.util")
30 require("ffluci.fs")
31 require("ffluci.i18n")
32 require("ffluci.model.uci")
33
34 viewdir = ffluci.fs.dirname(ffluci.util.__file__()) .. "view/"
35
36
37 -- Compile modes:
38 -- none: Never compile, only render precompiled
39 -- memory: Always compile, do not save compiled files, ignore precompiled
40 -- always: Same as "memory" but also saves compiled files
41 -- smart: Compile on demand, save compiled files, update precompiled
42 compiler_mode = "smart"
43
44
45 -- This applies to compiler modes "always" and "smart"
46 --
47 -- Produce compiled lua code rather than lua sourcecode
48 -- WARNING: Increases template size heavily!!!
49 -- This produces the same bytecode as luac but does not have a strip option
50 compiler_enable_bytecode = false
51
52
53 -- Define the namespace for template modules
54 viewns = {
55 translate = ffluci.i18n.translate,
56 config = ffluci.model.uci.get,
57 controller = os.getenv("SCRIPT_NAME"),
58 media = ffluci.config.mediaurlbase,
59 include = function(name) return render(name, getfenv(2)) end,
60 write = io.write
61 }
62
63
64 -- Compiles and builds a given template
65 function build(template, compiled)
66 local template = compile(ffluci.fs.readfile(template))
67
68 if compiled then
69 ffluci.fs.writefile(compiled, template)
70 end
71
72 return template
73 end
74
75
76 -- Compiles a given template into an executable Lua module
77 function compile(template)
78 -- Search all <% %> expressions (remember: Lua table indexes begin with #1)
79 local function expr_add(command)
80 table.insert(expr, command)
81 return "<%" .. tostring(#expr) .. "%>"
82 end
83
84 -- As "expr" should be local, we have to assign it to the "expr_add" scope
85 local expr = {}
86 ffluci.util.extfenv(expr_add, "expr", expr)
87
88 -- Save all expressiosn to table "expr"
89 template = template:gsub("<%%(.-)%%>", expr_add)
90
91 local function sanitize(s)
92 s = ffluci.util.escape(s)
93 s = ffluci.util.escape(s, "'")
94 s = ffluci.util.escape(s, "\n")
95 return s
96 end
97
98 -- Escape and sanitize all the template (all non-expressions)
99 template = sanitize(template)
100
101 -- Template module header/footer declaration
102 local header = "write('"
103 local footer = "')"
104
105 template = header .. template .. footer
106
107 -- Replacements
108 local r_include = "')\ninclude('%s')\nwrite('"
109 local r_i18n = "'..translate('%1','%2')..'"
110 local r_uci = "'..config('%1','%2','%3')..'"
111 local r_pexec = "'..%s..'"
112 local r_exec = "')\n%s\nwrite('"
113
114 -- Parse the expressions
115 for k,v in pairs(expr) do
116 local p = v:sub(1, 1)
117 local re = nil
118 if p == "+" then
119 re = r_include:format(sanitize(string.sub(v, 2)))
120 elseif p == ":" then
121 re = sanitize(v):gsub(":(.-) (.+)", r_i18n)
122 elseif p == "~" then
123 re = sanitize(v):gsub("~(.-)%.(.-)%.(.+)", r_uci)
124 elseif p == "=" then
125 re = r_pexec:format(string.sub(v, 2))
126 else
127 re = r_exec:format(v)
128 end
129 template = template:gsub("<%%"..tostring(k).."%%>", re)
130 end
131
132 if compiler_enable_bytecode then
133 tf = loadstring(template)
134 template = string.dump(tf)
135 end
136
137 return template
138 end
139
140
141 -- Returns and builds the template for "name" depending on the compiler mode
142 function get(name)
143 local templatefile = viewdir .. name .. ".htm"
144 local compiledfile = viewdir .. name .. ".lua"
145 local template = nil
146
147 if compiler_mode == "smart" then
148 local tplmt = ffluci.fs.mtime(templatefile)
149 local commt = ffluci.fs.mtime(compiledfile)
150
151 -- Build if there is no compiled file or if compiled file is outdated
152 if ((commt == nil) and not (tplmt == nil))
153 or (not (commt == nil) and not (tplmt == nil) and commt < tplmt) then
154 template = loadstring(build(templatefile, compiledfile))
155 else
156 template = loadfile(compiledfile)
157 end
158
159 elseif compiler_mode == "none" then
160 template = loadfile(compiledfile)
161
162 elseif compiler_mode == "memory" then
163 template = loadstring(build(templatefile))
164
165 elseif compiler_mode == "always" then
166 template = loadstring(build(templatefile, compiledfile))
167
168 else
169 error("Invalid compiler mode: " .. compiler_mode)
170
171 end
172
173 return template or error("Unable to load template: " .. name)
174 end
175
176 -- Renders a template
177 function render(name, scope)
178 scope = scope or getfenv(2)
179
180 -- Our template module
181 local view = get(name)
182
183 -- Put our predefined objects in the scope of the template
184 ffluci.util.updfenv(view, scope)
185 ffluci.util.updfenv(view, viewns)
186
187 -- Now finally render the thing
188 return view()
189 end