---- middleware.py import re from django.conf import settings from module_info_2_5_1 import MODULE_INFO from components import Components DEFAULT_BASE = 'http://yui.yahooapis.com/2.5.1/build/' DEFAULT_JS_TAG = '' DEFAULT_CSS_TAG = '' YUI_BASE = getattr(settings, 'YUI_INCLUDE_BASE', DEFAULT_BASE) PREFIX_RE = getattr(settings, 'YUI_INCLUDE_PREFIX_RE', '') TAGS = {'js': getattr(settings, 'YUI_INCLUDE_JS_TAG', DEFAULT_JS_TAG), 'css': getattr(settings, 'YUI_INCLUDE_CSS_TAG', DEFAULT_CSS_TAG)} VERSIONS = {'raw': '', '': '', 'min': '-min', 'debug': '-debug'} COMPONENTS = Components(MODULE_INFO) class YUILoader: def __init__(self): self._components = set() self._rolled_up_components = {} self._rollup_counters = {} self.set_version('min') def set_version(self, version): self._version = VERSIONS[version] def add_component(self, new_component_name): if not self._has_component(new_component_name): self._add_requirements(new_component_name) self._count_in_rollups(new_component_name) rollup_name = self._get_satisfied_rollup(new_component_name) if rollup_name: self.add_component(rollup_name) else: self._components.add(new_component_name) self._roll_up_superseded(new_component_name) def render(self): return '\n'.join(self._render_component(component) for component in self._sort_components()) def _has_component(self, component_name): return component_name in self._components \ or component_name in self._rolled_up_components def _get_satisfied_rollup(self, component_name): if self._version == '-min': for rollup_name in COMPONENTS.get_rollups(component_name): rollup_status = self._rollup_counters.get(rollup_name, set()) if len(rollup_status) >= COMPONENTS[rollup_name].rollup: return rollup_name def _count_in_rollups(self, component_name): for rollup_name in COMPONENTS.get_rollups(component_name): rolled_up = self._rollup_counters.setdefault(rollup_name, set()) rolled_up.add(component_name) for superseded in COMPONENTS[component_name].supersedes: self._count_in_rollups(superseded) def _roll_up_superseded(self, component_name): for superseded in COMPONENTS[component_name].supersedes: self._rolled_up_components[superseded] = component_name if superseded in self._components: self._components.remove(superseded) def _add_requirements(self, component_name): for requirement in COMPONENTS[component_name].requires: self.add_component(requirement) def _render_component(self, component_name): component = COMPONENTS[component_name] path = component.path if component.type == 'js': if self._version != '-min' and path.endswith('-min.js'): path = path[:-7] + self._version + '.js' elif component.type == 'css': if self._version == '' and path.endswith('-min.css'): path = path[:-8] + '.css' return TAGS[component.type] % (YUI_BASE + path,) def _sort_components(self, component_names=None): if component_names is None: comps = self._components.copy() else: comps = component_names while comps: component_name = comps.pop() component = COMPONENTS[component_name] direct_deps = component.requires + component.after indirect_deps = [ self._rolled_up_components[r] for r in direct_deps if r in self._rolled_up_components] all_deps = set(direct_deps).union(set(indirect_deps)) deps_left = comps.intersection(all_deps) for r in self._sort_components(deps_left): yield r comps.remove(r) yield component_name YUI_RE = re.compile( r'%s(include|version) +(.*?)%s' % (PREFIX_RE, SUFFIX_RE)) YUI_INIT_RE = re.compile( '%sinit%s' % (PREFIX_RE, SUFFIX_RE)) class YUIIncludeMiddleware(object): def process_response(self, request, response): components = set() node = YUILoader() def collect(match): cmd, data = match.groups() if cmd == 'include': components.update(data.split()) elif cmd == 'version': node.set_version(data) else: return '' % cmd return '' content = YUI_RE.sub(collect, response.content) for component in components: node.add_component(component) content = YUI_INIT_RE.sub(node.render(), content, 1) response.content = YUI_INIT_RE.sub( '', content) return response ---- components.py class ComponentAdapter: def __init__(self, components, name, data): self.components = components self.name = name self.data = data @property def supersedes(self): return self.data.get('supersedes', []) @property def requires(self): r = self.data.get('requires', []) if self.type != 'js' \ or self.name == 'yahoo' \ or 'yahoo' in self.supersedes \ or 'yahoo' in r: return r else: return ['yahoo'] + r @property def after(self): if self.type == 'css': return self.data.get('after', []) elif self.type == 'js': return self.data.get('after', []) + \ self.components.get_css_components() @property def rollup(self): return self.data.get('rollup', 1) def __getattr__(self, attname): return self.data[attname] class Components(dict): def __init__(self, components_dict): super(Components, self).__init__(components_dict) self.rollup_mapping = {} for component_name, data in components_dict.items(): self.rollup_mapping.setdefault(component_name, set()) if 'rollup' not in data: continue for rolled_up in data['supersedes']: self.rollup_mapping.setdefault(rolled_up, set()).add( component_name) def __getitem__(self, component_name): data = super(Components, self).__getitem__(component_name) return ComponentAdapter(self, component_name, data) def get_rollups(self, component_name): return self.rollup_mapping[component_name] def get_all_rollups(self, component_name): rollups = self.get_rollups(component_name) for superseded in self[component_name].supersedes: rollups.update(self.get_all_rollups(superseded)) return rollups def get_css_components(self): return [name for name, data in self.items() if data['type'] == 'css'] ---- module_info_2_5_1.py MODULE_INFO = { 'animation': {'path': 'animation/animation-min.js', 'requires': ['dom', 'event'], 'type': 'js'}, 'autocomplete': {'optional': ['connection', 'animation'], 'path': 'autocomplete/autocomplete-min.js', 'requires': ['dom', 'event'], 'skinnable': True, 'type': 'js'}, 'base': {'after': ['reset', 'fonts', 'grids'], 'path': 'base/base-min.css', 'type': 'css'}, 'button': {'optional': ['menu'], 'path': 'button/button-min.js', 'requires': ['element'], 'skinnable': True, 'type': 'js'}, 'calendar': {'path': 'calendar/calendar-min.js', 'requires': ['event', 'dom'], 'skinnable': True, 'type': 'js'}, 'charts': {'path': 'charts/charts-experimental-min.js', 'requires': ['element', 'json', 'datasource'], 'type': 'js'}, 'colorpicker': {'optional': ['animation'], 'path': 'colorpicker/colorpicker-min.js', 'requires': ['slider', 'element'], 'skinnable': True, 'type': 'js'}, 'connection': {'path': 'connection/connection-min.js', 'requires': ['event'], 'type': 'js'}, 'container': {'optional': ['dragdrop', 'animation', 'connection'], 'path': 'container/container-min.js', 'requires': ['dom', 'event'], 'skinnable': True, 'supersedes': ['containercore'], 'type': 'js'}, 'containercore': {'path': 'container/container_core-min.js', 'pkg': 'container', 'requires': ['dom', 'event'], 'type': 'js'}, 'cookie': {'path': 'cookie/cookie-beta-min.js', 'requires': ['yahoo'], 'type': 'js'}, 'datasource': {'optional': ['connection'], 'path': 'datasource/datasource-beta-min.js', 'requires': ['event'], 'type': 'js'}, 'datatable': {'optional': ['calendar', 'dragdrop'], 'path': 'datatable/datatable-beta-min.js', 'requires': ['element', 'datasource'], 'skinnable': True, 'type': 'js'}, 'dom': {'path': 'dom/dom-min.js', 'requires': ['yahoo'], 'type': 'js'}, 'dragdrop': {'path': 'dragdrop/dragdrop-min.js', 'requires': ['dom', 'event'], 'type': 'js'}, 'editor': {'optional': ['animation', 'dragdrop'], 'path': 'editor/editor-beta-min.js', 'requires': ['menu', 'element', 'button'], 'skinnable': True, 'type': 'js'}, 'element': {'path': 'element/element-beta-min.js', 'requires': ['dom', 'event'], 'type': 'js'}, 'event': {'path': 'event/event-min.js', 'requires': ['yahoo'], 'type': 'js'}, 'fonts': {'path': 'fonts/fonts-min.css', 'type': 'css'}, 'get': {'path': 'get/get-min.js', 'requires': ['yahoo'], 'type': 'js'}, 'grids': {'optional': ['reset'], 'path': 'grids/grids-min.css', 'requires': ['fonts'], 'type': 'css'}, 'history': {'path': 'history/history-min.js', 'requires': ['event'], 'type': 'js'}, 'imagecropper': {'path': 'imagecropper/imagecropper-beta-min.js', 'requires': ['dom', 'event', 'dragdrop', 'element', 'resize'], 'skinnable': True, 'type': 'js'}, 'imageloader': {'path': 'imageloader/imageloader-min.js', 'requires': ['event', 'dom'], 'type': 'js'}, 'json': {'path': 'json/json-min.js', 'requires': ['yahoo'], 'type': 'js'}, 'layout': {'optional': ['animation', 'dragdrop', 'resize', 'selector'], 'path': 'layout/layout-beta-min.js', 'requires': ['dom', 'event', 'element'], 'skinnable': True, 'type': 'js'}, 'logger': {'optional': ['dragdrop'], 'path': 'logger/logger-min.js', 'requires': ['event', 'dom'], 'skinnable': True, 'type': 'js'}, 'menu': {'path': 'menu/menu-min.js', 'requires': ['containercore'], 'skinnable': True, 'type': 'js'}, 'profiler': {'path': 'profiler/profiler-beta-min.js', 'requires': ['yahoo'], 'type': 'js'}, 'profilerviewer': {'path': 'profilerviewer/profilerviewer-beta-min.js', 'requires': ['profiler', 'yuiloader', 'element'], 'skinnable': True, 'type': 'js'}, 'reset': {'path': 'reset/reset-min.css', 'type': 'css'}, 'reset-fonts': {'path': 'reset-fonts/reset-fonts.css', 'rollup': 2, 'supersedes': ['reset', 'fonts'], 'type': 'css'}, 'reset-fonts-grids': {'path': 'reset-fonts-grids/reset-fonts-grids.css', 'rollup': 4, 'supersedes': ['reset', 'fonts', 'grids', 'reset-fonts'], 'type': 'css'}, 'resize': {'optional': ['animation'], 'path': 'resize/resize-beta-min.js', 'requires': ['dom', 'event', 'dragdrop', 'element'], 'skinnable': True, 'type': 'js'}, 'selector': {'path': 'selector/selector-beta-min.js', 'requires': ['yahoo', 'dom'], 'type': 'js'}, 'simpleeditor': {'optional': ['containercore', 'menu', 'button', 'animation', 'dragdrop'], 'path': 'editor/simpleeditor-beta-min.js', 'pkg': 'editor', 'requires': ['element'], 'skinnable': True, 'type': 'js'}, 'slider': {'optional': ['animation'], 'path': 'slider/slider-min.js', 'requires': ['dragdrop'], 'type': 'js'}, 'tabview': {'optional': ['connection'], 'path': 'tabview/tabview-min.js', 'requires': ['element'], 'skinnable': True, 'type': 'js'}, 'treeview': {'path': 'treeview/treeview-min.js', 'requires': ['event'], 'skinnable': True, 'type': 'js'}, 'uploader': {'path': 'uploader/uploader-experimental.js', 'requires': ['yahoo', 'element'], 'type': 'js'}, 'utilities': {'path': 'utilities/utilities.js', 'rollup': 8, 'supersedes': ['yahoo', 'event', 'dragdrop', 'animation', 'dom', 'connection', 'element', 'yahoo-dom-event', 'get', 'yuiloader', 'yuiloader-dom-event'], 'type': 'js'}, 'yahoo': {'path': 'yahoo/yahoo-min.js', 'type': 'js'}, 'yahoo-dom-event': {'path': 'yahoo-dom-event/yahoo-dom-event.js', 'rollup': 3, 'supersedes': ['yahoo', 'event', 'dom'], 'type': 'js'}, 'yuiloader': {'path': 'yuiloader/yuiloader-beta-min.js', 'supersedes': ['yahoo', 'get'], 'type': 'js'}, 'yuiloader-dom-event': {'path': 'yuiloader-dom-event/yuiloader-dom-event.js', 'rollup': 5, 'supersedes': ['yahoo', 'dom', 'event', 'get', 'yuiloader', 'yahoo-dom-event'], 'type': 'js'}, 'yuitest': {'path': 'yuitest/yuitest-min.js', 'requires': ['logger'], 'skinnable': True, 'type': 'js'}}