8c7f07f94ad7e6f3824614249a3bbb4369a8ceaf
[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