| 1 | __doc__ = """ |
|---|
| 2 | |
|---|
| 3 | YUI_include -- YUI Loader as Django middleware |
|---|
| 4 | |
|---|
| 5 | (c) Antti Kaihola 2008 http://djangopeople.net/akaihola/ |
|---|
| 6 | akaihol+django@ambitone.com |
|---|
| 7 | |
|---|
| 8 | This server-side middleware implements some of the functionality in the Yahoo |
|---|
| 9 | User Interface Loader component. YUI JavaScript and CSS modules requirements |
|---|
| 10 | can be declared anywhere in the base, inherited or included templates, and the |
|---|
| 11 | resulting, optimized <script> and <link rel=stylesheet> tags are inserted at |
|---|
| 12 | the specified position of the resulting page. |
|---|
| 13 | |
|---|
| 14 | Requirements may be specified in multiple locations. This is useful when zero |
|---|
| 15 | or more components are included in the HTML head section, and inherited and/or |
|---|
| 16 | included templates require possibly overlapping sets of YUI components in the |
|---|
| 17 | body across inherited and included templates. All tags are collected in the |
|---|
| 18 | head section, and duplicate tags are automatically eliminated. |
|---|
| 19 | |
|---|
| 20 | The middleware understands component dependencies and ensures that resources |
|---|
| 21 | are loaded in the right order. It knows about built-in rollup files that ship |
|---|
| 22 | with YUI. By automatically using rolled-up files, the number of HTTP requests |
|---|
| 23 | is reduced. |
|---|
| 24 | |
|---|
| 25 | The default syntax looks like HTML comments. Markup for the insertion point is |
|---|
| 26 | replaced with <script> and <link> tags: |
|---|
| 27 | <!-- YUI_init --> |
|---|
| 28 | |
|---|
| 29 | Component requirements are indicated, possibly in multiple locations, with the |
|---|
| 30 | ``YUI_include`` markup. It is removed from the resulting page by the |
|---|
| 31 | middleware. Example: |
|---|
| 32 | <!-- YUI_include fonts grids event dragdrop --> |
|---|
| 33 | |
|---|
| 34 | Non-minified and compressed versions are requested, respectively, by: |
|---|
| 35 | <!-- YUI_version raw --> |
|---|
| 36 | <!-- YUI_version debug --> |
|---|
| 37 | |
|---|
| 38 | Example: |
|---|
| 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 | |
|---|
| 47 | Renders: |
|---|
| 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 | |
|---|
| 58 | The markup format can be customized with global Django settings. Example: |
|---|
| 59 | YUI_INCLUDE_PREFIX_RE = r'{!' |
|---|
| 60 | YUI_INCLUDE_SUFFIX_RE = r'!}' |
|---|
| 61 | would change markup to e.g. ``{! init !}`` and ``{! include dom event !}``. |
|---|
| 62 | |
|---|
| 63 | The base URL is customized with the ``YUI_INCLUDE_BASE`` setting, e.g.: |
|---|
| 64 | YUI_INCLUDE_BASE = 'http://localhost:8000/yui/build/' |
|---|
| 65 | |
|---|
| 66 | To remove the XHTML trailing slash from the <link> tag, use: |
|---|
| 67 | YUI_INCLUDE_CSS_TAG = '<link rel="stylesheet" type="text/css" href="%s">' |
|---|
| 68 | """ |
|---|
| 69 | |
|---|
| 70 | import re |
|---|
| 71 | from shlex import shlex |
|---|
| 72 | |
|---|
| 73 | from django.conf import settings |
|---|
| 74 | |
|---|
| 75 | from module_info_2_5_1 import MODULE_INFO, SKIN |
|---|
| 76 | from components import Components |
|---|
| 77 | |
|---|
| 78 | |
|---|
| 79 | DEFAULT_BASE = 'http://yui.yahooapis.com/2.5.1/build/' |
|---|
| 80 | DEFAULT_JS_TAG = '<script type="text/javascript" src="%s"></script>' |
|---|
| 81 | DEFAULT_CSS_TAG = '<link rel="stylesheet" type="text/css" href="%s" />' |
|---|
| 82 | |
|---|
| 83 | YUI_BASE = getattr(settings, 'YUI_INCLUDE_BASE', DEFAULT_BASE) |
|---|
| 84 | PREFIX_RE = getattr(settings, 'YUI_INCLUDE_PREFIX_RE', '<!-- *YUI_') |
|---|
| 85 | SUFFIX_RE = getattr(settings, 'YUI_INCLUDE_SUFFIX_RE', ' *-->') |
|---|
| 86 | TAGS = {'js': getattr(settings, 'YUI_INCLUDE_JS_TAG', DEFAULT_JS_TAG), |
|---|
| 87 | 'css': getattr(settings, 'YUI_INCLUDE_CSS_TAG', DEFAULT_CSS_TAG)} |
|---|
| 88 | VERSIONS = {'raw': '', '': '', 'min': '-min', 'debug': '-debug'} |
|---|
| 89 | |
|---|
| 90 | |
|---|
| 91 | class 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 | |
|---|
| 230 | YUI_RE = re.compile( |
|---|
| 231 | r'%s(include|version) +(.*?)%s' % (PREFIX_RE, SUFFIX_RE)) |
|---|
| 232 | YUI_ADDMODULE_RE = re.compile( |
|---|
| 233 | r'(?s)%saddModule\s*(\{\s*.*?\s*})\s*%s' % (PREFIX_RE, SUFFIX_RE)) |
|---|
| 234 | YUI_INIT_RE = re.compile( |
|---|
| 235 | '%sinit%s' % (PREFIX_RE, SUFFIX_RE)) |
|---|
| 236 | |
|---|
| 237 | class 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 |
|---|