* luci/libs/core: added inline documentation to luci.util, reordered and renamed...
[project/luci.git] / libs / web / luasrc / template.lua
1 --[[
2 LuCI - 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("luci.template", package.seeall)
27
28 require("luci.config")
29 require("luci.util")
30 require("luci.fs")
31 require("luci.http")
32
33 luci.config.template = luci.config.template or {}
34
35 viewdir    = luci.config.template.viewdir or luci.sys.libpath() .. "/view"
36 compiledir = luci.config.template.compiledir or luci.sys.libpath() .. "/view"
37
38
39 -- Compile modes:
40 -- none:        Never compile, only use precompiled data from files
41 -- memory:      Always compile, do not save compiled files, ignore precompiled 
42 -- file:        Compile on demand, save compiled files, update precompiled
43 compiler_mode = luci.config.template.compiler_mode or "memory"
44
45
46 -- Define the namespace for template modules
47 context = luci.util.threadlocal()
48
49 viewns = {
50         include    = function(name) Template(name):render(getfenv(2)) end,
51 }
52
53 -- Compiles a given template into an executable Lua module
54 function compile(template)      
55         -- Search all <% %> expressions (remember: Lua table indexes begin with #1)
56         local function expr_add(ws1, skip1, command, skip2, ws2)
57                 table.insert(expr, command)
58                 return ( #skip1 > 0 and "" or ws1 ) .. 
59                        "<%" .. tostring(#expr) .. "%>" ..
60                        ( #skip2 > 0 and "" or ws2 )
61         end
62         
63         -- As "expr" should be local, we have to assign it to the "expr_add" scope 
64         local expr = {}
65         luci.util.extfenv(expr_add, "expr", expr)
66         
67         -- Save all expressiosn to table "expr"
68         template = template:gsub("(%s*)<%%(%-?)(.-)(%-?)%%>(%s*)", expr_add)
69         
70         local function sanitize(s)
71                 s = luci.util.escape(s)
72                 s = luci.util.escape(s, "'")
73                 s = luci.util.escape(s, "\n")
74                 return s
75         end
76         
77         -- Escape and sanitize all the template (all non-expressions)
78         template = sanitize(template)
79
80         -- Template module header/footer declaration
81         local header = "write('"
82         local footer = "')"
83         
84         template = header .. template .. footer
85         
86         -- Replacements
87         local r_include = "')\ninclude('%s')\nwrite('"
88         local r_i18n    = "'..translate('%1','%2')..'"
89         local r_i18n2    = "'..translate('%1', '')..'"
90         local r_pexec   = "'..(%s or '')..'"
91         local r_exec    = "')\n%s\nwrite('"
92         
93         -- Parse the expressions
94         for k,v in pairs(expr) do
95                 local p = v:sub(1, 1)
96                 v = v:gsub("%%", "%%%%")
97                 local re = nil
98                 if p == "+" then
99                         re = r_include:format(sanitize(string.sub(v, 2)))
100                 elseif p == ":" then
101                         if v:find(" ") then
102                                 re = sanitize(v):gsub(":(.-) (.*)", r_i18n)
103                         else
104                                 re = sanitize(v):gsub(":(.+)", r_i18n2)
105                         end
106                 elseif p == "=" then
107                         re = r_pexec:format(v:sub(2))
108                 elseif p == "#" then
109                         re = ""
110                 else
111                         re = r_exec:format(v)
112                 end
113                 template = template:gsub("<%%"..tostring(k).."%%>", re)
114         end
115
116         return loadstring(template)
117 end
118
119 -- Oldstyle render shortcut
120 function render(name, scope, ...)
121         scope = scope or getfenv(2)
122         local s, t = luci.util.copcall(Template, name)
123         if not s then
124                 error(t)
125         else
126                 t:render(scope, ...)
127         end
128 end
129
130
131 -- Template class
132 Template = luci.util.class()
133
134 -- Shared template cache to store templates in to avoid unnecessary reloading
135 Template.cache = {}
136 setmetatable(Template.cache, {__mode = "v"})
137
138
139 -- Constructor - Reads and compiles the template on-demand
140 function Template.__init__(self, name)  
141         self.template = self.cache[name]
142         
143         -- Create a new namespace for this template
144         self.viewns = {}
145         
146         -- Copy over from general namespace
147         luci.util.update(self.viewns, viewns)
148         if context.viewns then
149                 luci.util.update(self.viewns, context.viewns)
150         end
151         
152         -- If we have a cached template, skip compiling and loading
153         if self.template then
154                 return
155         end
156         
157         -- Enforce cache security
158         local cdir = compiledir .. "/" .. luci.sys.process.info("uid")
159         
160         -- Compile and build
161         local sourcefile   = viewdir    .. "/" .. name .. ".htm"
162         local compiledfile = cdir .. "/" .. luci.http.urlencode(name) .. ".lua"
163         local err       
164         
165         if compiler_mode == "file" then
166                 local tplmt = luci.fs.mtime(sourcefile)
167                 local commt = luci.fs.mtime(compiledfile)
168                 
169                 if not luci.fs.mtime(cdir) then
170                         luci.fs.mkdir(cdir, true)
171                         luci.fs.chmod(luci.fs.dirname(cdir), "a+rxw")
172                 end
173                                 
174                 -- Build if there is no compiled file or if compiled file is outdated
175                 if ((commt == nil) and not (tplmt == nil))
176                 or (not (commt == nil) and not (tplmt == nil) and commt < tplmt) then
177                         local source
178                         source, err = luci.fs.readfile(sourcefile)
179                         
180                         if source then
181                                 local compiled, err = compile(source)
182                                 
183                                 luci.fs.writefile(compiledfile, luci.util.get_bytecode(compiled))
184                                 self.template = compiled
185                         end
186                 else
187                         self.template, err = loadfile(compiledfile)
188                 end
189                 
190         elseif compiler_mode == "none" then
191                 self.template, err = loadfile(self.compiledfile)
192                 
193         elseif compiler_mode == "memory" then
194                 local source
195                 source, err = luci.fs.readfile(sourcefile)
196                 if source then
197                         self.template, err = compile(source)
198                 end
199                         
200         end
201         
202         -- If we have no valid template throw error, otherwise cache the template
203         if not self.template then
204                 error(err)
205         else
206                 self.cache[name] = self.template
207         end
208 end
209
210
211 -- Renders a template
212 function Template.render(self, scope)
213         scope = scope or getfenv(2)
214         
215         -- Save old environment
216         local oldfenv = getfenv(self.template)
217         
218         -- Put our predefined objects in the scope of the template
219         luci.util.resfenv(self.template)
220         luci.util.updfenv(self.template, scope)
221         luci.util.updfenv(self.template, self.viewns)
222         
223         -- Now finally render the thing
224         self.template()
225         
226         -- Reset environment
227         setfenv(self.template, oldfenv)
228 end