root/ff-luci/trunk/libs/web/luasrc/template.lua @ 3124

Revision 3124, 6.3 KB (checked in by Cyrus, 5 years ago)

Heavy memory/performance optimizations #3

  • Property svn:keywords set to Id
Line 
1--[[
2LuCI - Template Parser
3
4Description:
5A template parser supporting includes, translations, Lua code blocks
6and more. It can be used either as a compiler or as an interpreter.
7
8FileId: $Id$
9
10License:
11Copyright 2008 Steven Barth <steven@midlink.org>
12
13Licensed under the Apache License, Version 2.0 (the "License");
14you may not use this file except in compliance with the License.
15You may obtain a copy of the License at
16
17    http://www.apache.org/licenses/LICENSE-2.0
18
19Unless required by applicable law or agreed to in writing, software
20distributed under the License is distributed on an "AS IS" BASIS,
21WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22See the License for the specific language governing permissions and
23limitations under the License.
24
25]]--
26
27local fs = require"luci.fs"
28local sys = require "luci.sys"
29local util = require "luci.util"
30local table = require "table"
31local string = require "string"
32local config = require "luci.config"
33local coroutine = require "coroutine"
34
35local tostring, pairs, loadstring = tostring, pairs, loadstring
36local setmetatable, loadfile = setmetatable, loadfile
37local getfenv, setfenv, rawget = getfenv, setfenv, rawget
38local assert, type, error = assert, type, error
39
40--- LuCI template library.
41module "luci.template"
42
43config.template = config.template or {}
44
45viewdir    = config.template.viewdir or util.libpath() .. "/view"
46compiledir = config.template.compiledir or util.libpath() .. "/view"
47
48
49-- Compile modes:
50-- memory:  Always compile, do not save compiled files, ignore precompiled
51-- file:    Compile on demand, save compiled files, update precompiled
52compiler_mode = config.template.compiler_mode or "memory"
53
54
55-- Define the namespace for template modules
56context = util.threadlocal()
57
58--- Manually  compile a given template into an executable Lua function
59-- @param template  LuCI template
60-- @return          Lua template function
61function compile(template) 
62    local expr = {}
63
64    -- Search all <% %> expressions
65    local function expr_add(ws1, skip1, command, skip2, ws2)
66        table.insert(expr, command)
67        return ( #skip1 > 0 and "" or ws1 ) .. 
68               "<%" .. tostring(#expr) .. "%>" ..
69               ( #skip2 > 0 and "" or ws2 )
70    end
71   
72    -- Save all expressiosn to table "expr"
73    template = template:gsub("(%s*)<%%(%-?)(.-)(%-?)%%>(%s*)", expr_add)
74   
75    local function sanitize(s)
76        s = "%q" % s
77        return s:sub(2, #s-1)
78    end
79   
80    -- Escape and sanitize all the template (all non-expressions)
81    template = sanitize(template)
82
83    -- Template module header/footer declaration
84    local header = 'write("'
85    local footer = '")'
86   
87    template = header .. template .. footer
88   
89    -- Replacements
90    local r_include = '")\ninclude("%s")\nwrite("'
91    local r_i18n    = '"..translate("%1","%2").."'
92    local r_i18n2    = '"..translate("%1", "").."'
93    local r_pexec   = '"..(%s or "").."'
94    local r_exec    = '")\n%s\nwrite("'
95   
96    -- Parse the expressions
97    for k,v in pairs(expr) do
98        local p = v:sub(1, 1)
99        v = v:gsub("%%", "%%%%")
100        local re = nil
101        if p == "+" then
102            re = r_include:format(sanitize(string.sub(v, 2)))
103        elseif p == ":" then
104            if v:find(" ") then
105                re = sanitize(v):gsub(":(.-) (.*)", r_i18n)
106            else
107                re = sanitize(v):gsub(":(.+)", r_i18n2)
108            end
109        elseif p == "=" then
110            re = r_pexec:format(v:sub(2))
111        elseif p == "#" then
112            re = ""
113        else
114            re = r_exec:format(v)
115        end
116        template = template:gsub("<%%"..tostring(k).."%%>", re)
117    end
118
119    return loadstring(template)
120end
121
122--- Render a certain template.
123-- @param name      Template name
124-- @param scope     Scope to assign to template (optional)
125function render(name, scope)
126    return Template(name):render(scope or getfenv(2))
127end
128
129
130-- Template class
131Template = util.class()
132
133-- Shared template cache to store templates in to avoid unnecessary reloading
134Template.cache = setmetatable({}, {__mode = "v"})
135
136
137-- Constructor - Reads and compiles the template on-demand
138function Template.__init__(self, name) 
139    local function _encode_filename(str)
140
141        local function __chrenc( chr )
142            return "%%%02x" % string.byte( chr )
143        end
144
145        if type(str) == "string" then
146            str = str:gsub(
147                "([^a-zA-Z0-9$_%-%.%+!*'(),])",
148                __chrenc
149            )
150        end
151
152        return str
153    end
154
155    self.template = self.cache[name]
156    self.name = name
157   
158    -- Create a new namespace for this template
159    self.viewns = context.viewns
160   
161    -- If we have a cached template, skip compiling and loading
162    if self.template then
163        return
164    end
165   
166    -- Enforce cache security
167    local cdir = compiledir .. "/" .. sys.process.info("uid")
168   
169    -- Compile and build
170    local sourcefile   = viewdir    .. "/" .. name
171    local compiledfile = cdir .. "/" .. _encode_filename(name) .. ".lua"
172    local err   
173   
174    if compiler_mode == "file" then
175        local tplmt = fs.mtime(sourcefile) or fs.mtime(sourcefile .. ".htm")
176        local commt = fs.mtime(compiledfile)
177       
178        if not fs.mtime(cdir) then
179            fs.mkdir(cdir, true)
180            fs.chmod(fs.dirname(cdir), "a+rxw")
181        end
182       
183        assert(tplmt or commt, "No such template: " .. name)
184               
185        -- Build if there is no compiled file or if compiled file is outdated
186        if not commt or (commt  and tplmt and commt < tplmt) then
187            local source
188            source, err = fs.readfile(sourcefile) or fs.readfile(sourcefile .. ".htm")
189           
190            if source then
191                local compiled, err = compile(source)
192               
193                fs.writefile(compiledfile, util.get_bytecode(compiled))
194                fs.chmod(compiledfile, "a-rwx,u+rw")
195                self.template = compiled
196            end
197        else
198            assert(
199                sys.process.info("uid") == fs.stat(compiledfile, "uid")
200                and fs.stat(compiledfile, "mode") == "rw-------",
201                "Fatal: Cachefile is not sane!"
202            )
203            self.template, err = loadfile(compiledfile)
204        end
205       
206    elseif compiler_mode == "memory" then
207        local source
208        source, err = fs.readfile(sourcefile) or fs.readfile(sourcefile .. ".htm")
209        if source then
210            self.template, err = compile(source)
211        end
212           
213    end
214   
215    -- If we have no valid template throw error, otherwise cache the template
216    if not self.template then
217        error(err)
218    else
219        self.cache[name] = self.template
220    end
221end
222
223
224-- Renders a template
225function Template.render(self, scope)
226    scope = scope or getfenv(2)
227   
228    -- Put our predefined objects in the scope of the template
229    setfenv(self.template, setmetatable({}, {__index =
230        function(tbl, key)
231            return rawget(tbl, key) or self.viewns[key] or scope[key]
232        end}))
233   
234    -- Now finally render the thing
235    local stat, err = util.copcall(self.template)
236    if not stat then
237        error("Error in template %s: %s" % {self.name, err})
238    end
239end
Note: See TracBrowser for help on using the browser.