* Performance optimizations
[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 viewns = {
48         write      = io.write,
49         include    = function(name) Template(name):render(getfenv(2)) end,      
50 }
51
52 -- Compiles a given template into an executable Lua module
53 function compile(template)      
54         -- Search all <% %> expressions (remember: Lua table indexes begin with #1)
55         local function expr_add(command)
56                 table.insert(expr, command)
57                 return "<%" .. tostring(#expr) .. "%>"
58         end
59         
60         -- As "expr" should be local, we have to assign it to the "expr_add" scope 
61         local expr = {}
62         luci.util.extfenv(expr_add, "expr", expr)
63         
64         -- Save all expressiosn to table "expr"
65         template = template:gsub("<%%(.-)%%>", expr_add)
66         
67         local function sanitize(s)
68                 s = luci.util.escape(s)
69                 s = luci.util.escape(s, "'")
70                 s = luci.util.escape(s, "\n")
71                 return s
72         end
73         
74         -- Escape and sanitize all the template (all non-expressions)
75         template = sanitize(template)
76
77         -- Template module header/footer declaration
78         local header = "write('"
79         local footer = "')"
80         
81         template = header .. template .. footer
82         
83         -- Replacements
84         local r_include = "')\ninclude('%s')\nwrite('"
85         local r_i18n    = "'..translate('%1','%2')..'"
86         local r_pexec   = "'..(%s or '')..'"
87         local r_exec    = "')\n%s\nwrite('"
88         
89         -- Parse the expressions
90         for k,v in pairs(expr) do
91                 local p = v:sub(1, 1)
92                 local re = nil
93                 if p == "+" then
94                         re = r_include:format(sanitize(string.sub(v, 2)))
95                 elseif p == ":" then
96                         re = sanitize(v):gsub(":(.-) (.+)", r_i18n)
97                 elseif p == "=" then
98                         re = r_pexec:format(v:sub(2))
99                 else
100                         re = r_exec:format(v)
101                 end
102                 template = template:gsub("<%%"..tostring(k).."%%>", re)
103         end
104
105         return loadstring(template)
106 end
107
108 -- Oldstyle render shortcut
109 function render(name, scope, ...)
110         scope = scope or getfenv(2)
111         local s, t = pcall(Template, name)
112         if not s then
113                 error(t)
114         else
115                 t:render(scope, ...)
116         end
117 end
118
119
120 -- Template class
121 Template = luci.util.class()
122
123 -- Shared template cache to store templates in to avoid unnecessary reloading
124 Template.cache = {}
125
126
127 -- Constructor - Reads and compiles the template on-demand
128 function Template.__init__(self, name)  
129         if self.cache[name] then
130                 self.template = self.cache[name]
131         else
132                 self.template = nil
133         end
134         
135         -- Create a new namespace for this template
136         self.viewns = {}
137         
138         -- Copy over from general namespace
139         for k, v in pairs(viewns) do
140                 self.viewns[k] = v
141         end     
142         
143         -- If we have a cached template, skip compiling and loading
144         if self.template then
145                 return
146         end
147         
148         -- Compile and build
149         local sourcefile   = viewdir    .. "/" .. name .. ".htm"
150         local compiledfile = compiledir .. "/" .. name .. ".lua"
151         local err       
152         
153         if compiler_mode == "file" then
154                 local tplmt = luci.fs.mtime(sourcefile)
155                 local commt = luci.fs.mtime(compiledfile)
156                                 
157                 -- Build if there is no compiled file or if compiled file is outdated
158                 if ((commt == nil) and not (tplmt == nil))
159                 or (not (commt == nil) and not (tplmt == nil) and commt < tplmt) then
160                         local source
161                         source, err = luci.fs.readfile(sourcefile)
162                         
163                         if source then
164                                 local compiled, err = compile(source)
165                                 
166                                 local compiledfile_dir  = luci.fs.dirname(compiledfile)
167                                 if not luci.fs.mtime(compiledfile_dir) then
168                                         luci.fs.mkdir(compiledfile_dir)
169                                 end
170                                 
171                                 luci.fs.writefile(compiledfile, luci.util.dump(compiled))
172                                 self.template = compiled
173                         end
174                 else
175                         self.template, err = loadfile(compiledfile)
176                 end
177                 
178         elseif compiler_mode == "none" then
179                 self.template, err = loadfile(self.compiledfile)
180                 
181         elseif compiler_mode == "memory" then
182                 local source
183                 source, err = luci.fs.readfile(sourcefile)
184                 if source then
185                         self.template, err = compile(source)
186                 end
187                         
188         end
189         
190         -- If we have no valid template throw error, otherwise cache the template
191         if not self.template then
192                 error(err)
193         else
194                 self.cache[name] = self.template
195         end
196 end
197
198
199 -- Renders a template
200 function Template.render(self, scope)
201         scope = scope or getfenv(2)
202         
203         -- Save old environment
204         local oldfenv = getfenv(self.template)
205         
206         -- Put our predefined objects in the scope of the template
207         luci.util.resfenv(self.template)
208         luci.util.updfenv(self.template, scope)
209         luci.util.updfenv(self.template, self.viewns)
210         
211         -- Now finally render the thing
212         self.template()
213         
214         -- Reset environment
215         setfenv(self.template, oldfenv)
216 end