root / trunk / yui / middleware.py

Revision 258, 10.5 kB (checked in by akaihola, 5 months ago)

[yui] Added support for optional modules. Improved error handling.

  • Property svn:eol-style set to native
Line 
1__doc__ = """
2
3YUI_include -- YUI Loader as Django middleware
4
5(c) Antti Kaihola 2008  http://djangopeople.net/akaihola/
6                        akaihol+django@ambitone.com
7
8This server-side middleware implements some of the functionality in the Yahoo
9User Interface Loader component.  YUI JavaScript and CSS modules requirements
10can be declared anywhere in the base, inherited or included templates, and the
11resulting, optimized <script> and <link rel=stylesheet> tags are inserted at
12the specified position of the resulting page.
13
14Requirements may be specified in multiple locations.  This is useful when zero
15or more components are included in the HTML head section, and inherited and/or
16included templates require possibly overlapping sets of YUI components in the
17body across inherited and included templates.  All tags are collected in the
18head section, and duplicate tags are automatically eliminated.
19
20The middleware understands component dependencies and ensures that resources
21are loaded in the right order.  It knows about built-in rollup files that ship
22with YUI.  By automatically using rolled-up files, the number of HTTP requests
23is reduced.
24
25The default syntax looks like HTML comments.  Markup for the insertion point is
26replaced with <script> and <link> tags:
27<!-- YUI_init -->
28
29Component requirements are indicated, possibly in multiple locations, with the
30``YUI_include`` markup.  It is removed from the resulting page by the
31middleware. Example:
32<!-- YUI_include fonts grids event dragdrop -->
33
34Non-minified and compressed versions are requested, respectively, by:
35<!-- YUI_version raw -->
36<!-- YUI_version debug -->
37
38Example:
39
40<html><head>
41<!-- YUI_init -->
42<!-- YUI_include dom event -->
43</head><body>
44<!-- YUI_include element selector reset fonts base -->
45</body></html>
46
47Renders:
48
49<html><head>
50<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.5.1/build/reset-fonts/reset-fonts.css" />
51<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.5.1/build/base/base-min.css" />
52<script type="text/javascript" src="http://yui.yahooapis.com/2.5.1/build/yahoo-dom-event/yahoo-dom-event.js"></script>
53<script type="text/javascript" src="http://yui.yahooapis.com/2.5.1/build/element/element-beta-min.js"></script>
54<script type="text/javascript" src="http://yui.yahooapis.com/2.5.1/build/selector/selector-beta-min.js"></script>
55</head><body>
56</body></html>
57
58The markup format can be customized with global Django settings.  Example:
59YUI_INCLUDE_PREFIX_RE = r'{!'
60YUI_INCLUDE_SUFFIX_RE = r'!}'
61would change markup to e.g. ``{! init !}`` and ``{! include dom event !}``.
62
63The base URL is customized with the ``YUI_INCLUDE_BASE`` setting, e.g.:
64YUI_INCLUDE_BASE = 'http://localhost:8000/yui/build/'
65
66To remove the XHTML trailing slash from the <link> tag, use:
67YUI_INCLUDE_CSS_TAG = '<link rel="stylesheet" type="text/css" href="%s">'
68"""
69
70import re
71from shlex import shlex
72
73from django.conf import settings
74
75from module_info_2_5_1 import MODULE_INFO, SKIN
76from components import Components
77
78
79DEFAULT_BASE = 'http://yui.yahooapis.com/2.5.1/build/'
80DEFAULT_JS_TAG = '<script type="text/javascript" src="%s"></script>'
81DEFAULT_CSS_TAG = '<link rel="stylesheet" type="text/css" href="%s" />'
82
83YUI_BASE = getattr(settings, 'YUI_INCLUDE_BASE', DEFAULT_BASE)
84PREFIX_RE = getattr(settings, 'YUI_INCLUDE_PREFIX_RE', '<!-- *YUI_')
85SUFFIX_RE = getattr(settings, 'YUI_INCLUDE_SUFFIX_RE', ' *-->')
86TAGS = {'js': getattr(settings, 'YUI_INCLUDE_JS_TAG', DEFAULT_JS_TAG),
87        'css': getattr(settings, 'YUI_INCLUDE_CSS_TAG', DEFAULT_CSS_TAG)}
88VERSIONS = {'raw': '', '': '', 'min': '-min', 'debug': '-debug'}
89
90
91class YUILoader:
92
93    def __init__(self):
94        self._module_info = Components(MODULE_INFO)
95        self._components = set()
96        self._rolled_up_components = {}
97        self._rollup_counters = {}
98        self.set_version('min')
99
100    def set_version(self, version):
101        self._version = VERSIONS[version]
102
103    def add_component(self, new_component_name):
104        if not self._has_component(new_component_name):
105            self._add_requirements(new_component_name)
106            self._count_in_rollups(new_component_name)
107            rollup_name = self._get_satisfied_rollup(new_component_name)
108            if rollup_name:
109                self.add_component(rollup_name)
110            else:
111                self._components.add(new_component_name)
112                self._roll_up_superseded(new_component_name)
113
114    def add_module(self, module_def):
115        module_data = {}
116        lexer = shlex(module_def, posix=True)
117
118        def expect(*patterns):
119            token = lexer.get_token()
120            if token not in patterns:
121                raise ValueError, '%s expected instead of %s' % \
122                      (' or '.join(repr(s) for s in patterns),
123                       token and repr(token) or 'end of data')
124            return token
125
126        str_attrs = 'name', 'type', 'path', 'fullpath', 'varName'
127        list_attrs = 'requires', 'optional', 'after'
128        state = 'ATTR'
129        expect('{')
130        while state != 'STOP':
131            if state == 'ATTR':
132                token = expect(*str_attrs+list_attrs)
133                expect(':')
134                if token in str_attrs:
135                    module_data[token] = lexer.get_token()
136                    if module_data[token] is None:
137                        raise ValueError, \
138                              'string expected instead of end of data'
139                    state = 'DELIM'
140                elif token in list_attrs:
141                    expect('[')
142                    lst = module_data[token] = []
143                    state = 'LIST'
144            elif state == 'LIST':
145                lst.append(lexer.get_token())
146                if re.search(r'\W', lst[-1]):
147                    raise ValueError, 'invalid component name %r' % token
148                if expect(',', ']') == ']':
149                    state = 'DELIM'
150            elif state == 'DELIM':
151                if expect(',', '}') == '}':
152                    expect(None)
153                    state = 'STOP'
154                else:
155                    state = 'ATTR'
156
157        if 'type' not in module_data:
158            raise ValueError, 'type missing in %r' % module_def
159        self._module_info.add(module_data['name'], module_data)
160        return module_data
161
162    def render(self):
163        return '\n'.join(self._render_component(component)
164                         for component in self._sort_components())
165
166    def _has_component(self, component_name):
167        return component_name in self._components \
168               or component_name in self._rolled_up_components
169
170    def _get_satisfied_rollup(self, component_name):
171        if self._version == '-min':
172            for rollup_name in self._module_info.get_rollups(component_name):
173                rollup_status = self._rollup_counters.get(rollup_name, set())
174                if len(rollup_status) >= self._module_info[rollup_name].rollup:
175                    return rollup_name
176
177    def _count_in_rollups(self, component_name):
178        for rollup_name in self._module_info.get_rollups(component_name):
179            rolled_up = self._rollup_counters.setdefault(rollup_name, set())
180            rolled_up.add(component_name)
181        for superseded in self._module_info[component_name].supersedes:
182            self._count_in_rollups(superseded)
183
184    def _roll_up_superseded(self, component_name):
185        for superseded in self._module_info[component_name].supersedes:
186            self._rolled_up_components[superseded] = component_name
187            if superseded in self._components:
188                self._components.remove(superseded)
189
190    def _add_requirements(self, component_name):
191        component = self._module_info[component_name]
192        for requirement in component.requires:
193            self.add_component(requirement)
194        if component.skinnable:
195            self.add_component(SKIN['defaultSkin'])
196
197    def _render_component(self, component_name):
198        component = self._module_info[component_name]
199        path = component.fullpath or YUI_BASE + component.path
200        if component.type == 'js':
201            if self._version != '-min' and path.endswith('-min.js'):
202                path = path[:-7] + self._version + '.js'
203        elif component.type == 'css':
204            if self._version == '' and path.endswith('-min.css'):
205                path = path[:-8] + '.css'
206        return TAGS[component.type] % path
207
208    def _sort_components(self, component_names=None):
209        if component_names is None:
210            comps = self._components.copy()
211        else:
212            comps = component_names
213        while comps:
214            component_name = comps.pop()
215            component = self._module_info[component_name]
216            direct_deps = component.requires + component.after
217            indirect_deps = [
218                self._rolled_up_components[r] for r in direct_deps
219                if r in self._rolled_up_components]
220            all_deps = set(direct_deps) \
221                       .union(set(indirect_deps)) \
222                       .union(set(component.optional))
223            deps_left = comps.intersection(all_deps)
224            for r in self._sort_components(deps_left):
225                yield r
226                comps.remove(r)
227            yield component_name
228
229
230YUI_RE = re.compile(
231    r'%s(include|version) +(.*?)%s' % (PREFIX_RE, SUFFIX_RE))
232YUI_ADDMODULE_RE = re.compile(
233    r'(?s)%saddModule\s*(\{\s*.*?\s*})\s*%s' % (PREFIX_RE, SUFFIX_RE))
234YUI_INIT_RE = re.compile(
235    '%sinit%s' % (PREFIX_RE, SUFFIX_RE))
236
237class YUIIncludeMiddleware(object):
238    def process_response(self, request, response):
239        components = set()
240        loader = YUILoader()
241
242        def add_module(match):
243            loader.add_module(match.group(1))
244            return ''
245        content = YUI_ADDMODULE_RE.sub(add_module, response.content)
246
247        def collect(match):
248            cmd, data = match.groups()
249            if cmd == 'include':
250                components.update(data.split())
251            elif cmd == 'version':
252                loader.set_version(data)
253            else:
254                return '<!-- UNKNOWN COMMAND YUI_%s -->' % cmd
255            return ''
256        content = YUI_RE.sub(collect, content)
257
258        for component in components:
259            loader.add_component(component)
260
261        tags = loader.render()
262        if tags:
263            content, count = YUI_INIT_RE.subn(tags, content, 1)
264            if count != 1:
265                content += ('<p>%d YUI init tags found,'
266                            'at least one expected</p>' % count)
267            response.content = YUI_INIT_RE.sub(
268                '<!-- WARNING: MULTIPLE YUI init STATEMENTS -->', content)
269
270        return response
Note: See TracBrowser for help on using the browser.