---- 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 = '<script type="text/javascript" src="%s"></script>'
DEFAULT_CSS_TAG = '<link rel="stylesheet" type="text/css" href="%s" />'

YUI_BASE = getattr(settings, 'YUI_INCLUDE_BASE', DEFAULT_BASE)
PREFIX_RE = getattr(settings, 'YUI_INCLUDE_PREFIX_RE', '<!-- *YUI_')
SUFFIX_RE = getattr(settings, 'YUI_INCLUDE_SUFFIX_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 '<!-- UNKNOWN COMMAND YUI_%s -->' % 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(
            '<!-- WARNING: MULTIPLE YUI_init STATEMENTS -->', 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'}}
