Esempio n. 1
0
class Compressor(object):

    def __init__(self, **kwargs):
        if 'environment' in kwargs:
            configs = self.get_configs_from_environment(kwargs['environment'])
            self.config = Config(**configs)
        else:
            self.config = Config(**kwargs)

    def compress(self, html, compression_type):

        if not self.config.compressor_enabled:
            return html

        compression_type = compression_type.lower()
        html_hash = self.make_hash(html)

        if not os.path.exists(u(self.config.compressor_output_dir)):
            os.makedirs(u(self.config.compressor_output_dir))

        cached_file = os.path.join(
            u(self.config.compressor_output_dir),
            u('{hash}.{extension}').format(
                hash=html_hash,
                extension=compression_type,
            ),
        )

        if os.path.exists(cached_file):
            filename = os.path.join(
                u(self.config.compressor_static_prefix),
                os.path.basename(cached_file),
            )
            return self.render_element(filename, compression_type)

        assets = OrderedDict()
        soup = BeautifulSoup(html, PARSER)
        for count, c in enumerate(self.find_compilable_tags(soup)):

            url = c.get('src') or c.get('href')
            if url:
                filename = os.path.basename(u(url)).split('.', 1)[0]
                uri_cwd = os.path.join(u(self.config.compressor_static_prefix), os.path.dirname(u(url)))
                text = open(self.find_file(u(url)), 'r', encoding='utf-8')
                cwd = os.path.dirname(text.name)
            else:
                filename = u('inline{0}').format(count)
                uri_cwd = None
                text = c.string
                cwd = None

            mimetype = c['type'].lower()
            try:
                compressor = self.config.compressor_classes[mimetype]
            except KeyError:
                msg = u('Unsupported type of compression {0}').format(mimetype)
                raise RuntimeError(msg)

            text = self.get_contents(text)
            compressed = compressor.compile(text,
                                            mimetype=mimetype,
                                            cwd=cwd,
                                            uri_cwd=uri_cwd,
                                            debug=self.config.compressor_debug)

            if not self.config.compressor_debug:
                outfile = cached_file
            else:
                outfile = os.path.join(
                    u(self.config.compressor_output_dir),
                    u('{hash}-{filename}.{extension}').format(
                        hash=html_hash,
                        filename=filename,
                        extension=compression_type,
                    ),
                )

            if assets.get(outfile) is None:
                assets[outfile] = u('')
            assets[outfile] += u("\n") + compressed

        blocks = u('')
        for outfile, asset in assets.items():
            with open(outfile, 'w', encoding='utf-8') as fh:
                fh.write(asset)
            filename = os.path.join(
                u(self.config.compressor_static_prefix),
                os.path.basename(outfile),
            )
            blocks += self.render_element(filename, compression_type)

        return blocks

    def make_hash(self, html):
        soup = BeautifulSoup(html, PARSER)
        compilables = self.find_compilable_tags(soup)
        html_hash = hashlib.md5(utf8_encode(html))

        for c in compilables:
            url = c.get('src') or c.get('href')
            if url:
                with open(self.find_file(url), 'r', encoding='utf-8') as f:
                    while True:
                        content = f.read(1024)
                        if content:
                            html_hash.update(utf8_encode(content))
                        else:
                            break

        return html_hash.hexdigest()

    def find_file(self, path):
        if callable(self.config.compressor_source_dirs):
            filename = self.config.compressor_source_dirs(path)
            if os.path.exists(filename):
                return filename
        else:
            if isinstance(self.config.compressor_source_dirs, basestring):
                dirs = [self.config.compressor_source_dirs]
            else:
                dirs = self.config.compressor_source_dirs

            for d in dirs:
                if self.config.compressor_static_prefix_precompress is not None and path.startswith('/'):
                    path = path.replace(self.config.compressor_static_prefix_precompress, '', 1).lstrip(os.sep).lstrip('/')
                filename = os.path.join(d, path)
                if os.path.exists(filename):
                    return filename

        raise IOError(2, u('File not found {0}').format(path))

    def find_compilable_tags(self, soup):
        tags = ['link', 'style', 'script']
        for tag in soup.find_all(tags):

            # don't compress externally hosted assets
            src = tag.get('src') or tag.get('href')
            if src and (src.startswith('http') or src.startswith('//')):
                continue

            if tag.get('type') is None:
                if tag.name == 'script':
                    tag['type'] = 'text/javascript'
                if tag.name == 'style':
                    tag['type'] = 'text/css'
            else:
                tag['type'] == tag['type'].lower()

            if tag.get('type') is None:
                raise RuntimeError(u('Tags to be compressed must have a type attribute: {0}').format(u(tag)))

            yield tag

    def get_contents(self, src):
        if isinstance(src, file):
            return u(src.read())
        else:
            return u(src)

    def render_element(self, filename, type):
        """Returns an html element pointing to filename as a string.
        """
        if type.lower() == 'css':
            return u('<link type="text/css" rel="stylesheet" href="{0}" />').format(filename)
        elif type.lower() == 'js':
            return u('<script type="text/javascript" src="{0}"></script>').format(filename)
        else:
            raise RuntimeError(u('Unsupported type of compression {0}').format(type))

    def get_configs_from_environment(self, environment):
        configs = {}
        for key in dir(environment):
            if key.startswith('compressor_'):
                configs[key] = getattr(environment, key)
        return configs

    def offline_compress(self, environment, template_dirs):

        if isinstance(template_dirs, basestring):
            template_dirs = [template_dirs]

        configs = self.get_configs_from_environment(environment)
        self.config.update(**configs)

        compressor_nodes = {}
        parser = Jinja2Parser(charset='utf-8', env=environment)
        for template_path in self.find_template_files(template_dirs):
            try:
                template = parser.parse(template_path)
            except IOError:  # unreadable file -> ignore
                continue
            except TemplateSyntaxError:  # broken template -> ignore
                continue
            except TemplateDoesNotExist:  # non existent template -> ignore
                continue
            except UnicodeDecodeError:
                continue

            try:
                nodes = list(parser.walk_nodes(template))
            except (TemplateDoesNotExist, TemplateSyntaxError):
                continue
            if nodes:
                template.template_name = template_path
                compressor_nodes.setdefault(template, []).extend(nodes)

        if not compressor_nodes:
            raise OfflineGenerationError(
                "No 'compress' template tags found in templates. "
                "Try setting follow_symlinks to True")

        for template, nodes in compressor_nodes.items():
            for node in nodes:
                parser.render_node({}, node, globals=environment.globals)

    def find_template_files(self, template_dirs):
        templates = set()
        for d in template_dirs:
            for root, dirs, files in os.walk(d,
                    followlinks=self.config.compressor_follow_symlinks):
                templates.update(os.path.join(root, name)
                    for name in files if not name.startswith('.'))
        return templates
Esempio n. 2
0
class Compressor(object):
    def __init__(self, **kwargs):
        if 'environment' in kwargs:
            configs = self.get_configs_from_environment(kwargs['environment'])
            self.config = Config(**configs)
        else:
            self.config = Config(**kwargs)

    def compress(self, html, compression_type):

        if not self.config.compressor_enabled:
            return html

        compression_type = compression_type.lower()
        html_hash = self.make_hash(html)

        if not os.path.exists(u(self.config.compressor_output_dir)):
            os.makedirs(u(self.config.compressor_output_dir))

        cached_file = os.path.join(
            u(self.config.compressor_output_dir),
            u('{hash}.{extension}').format(
                hash=html_hash,
                extension=compression_type,
            ),
        )

        if os.path.exists(cached_file):
            filename = os.path.join(
                u(self.config.compressor_static_prefix),
                os.path.basename(cached_file),
            )
            return self.render_element(filename, compression_type)

        assets = OrderedDict()
        soup = BeautifulSoup(html)
        for count, c in enumerate(self.find_compilable_tags(soup)):

            url = c.get('src') or c.get('href')
            if url:
                filename = os.path.basename(u(url)).split('.', 1)[0]
                uri_cwd = os.path.join(u(self.config.compressor_static_prefix),
                                       os.path.dirname(u(url)))
                text = open(self.find_file(u(url)), 'r', encoding='utf-8')
                cwd = os.path.dirname(text.name)
            else:
                filename = u('inline{0}').format(count)
                uri_cwd = None
                text = c.string
                cwd = None

            mimetype = c['type'].lower()
            try:
                compressor = self.config.compressor_classes[mimetype]
            except KeyError:
                msg = u('Unsupported type of compression {0}').format(mimetype)
                raise RuntimeError(msg)

            text = self.get_contents(text)
            compressed = compressor.compile(text,
                                            mimetype=mimetype,
                                            cwd=cwd,
                                            uri_cwd=uri_cwd,
                                            debug=self.config.compressor_debug)

            if not self.config.compressor_debug:
                outfile = cached_file
            else:
                outfile = os.path.join(
                    u(self.config.compressor_output_dir),
                    u('{hash}-{filename}.{extension}').format(
                        hash=html_hash,
                        filename=filename,
                        extension=compression_type,
                    ),
                )

            if assets.get(outfile) is None:
                assets[outfile] = u('')
            assets[outfile] += u("\n") + compressed

        blocks = u('')
        for outfile, asset in assets.items():
            with open(outfile, 'w', encoding='utf-8') as fh:
                fh.write(asset)
            filename = os.path.join(
                u(self.config.compressor_static_prefix),
                os.path.basename(outfile),
            )
            blocks += self.render_element(filename, compression_type)

        return blocks

    def make_hash(self, html):
        soup = BeautifulSoup(html)
        compilables = self.find_compilable_tags(soup)
        html_hash = hashlib.md5(utf8_encode(html))

        for c in compilables:
            url = c.get('src') or c.get('href')
            if url:
                with open(self.find_file(url), 'r', encoding='utf-8') as f:
                    while True:
                        content = f.read(1024)
                        if content:
                            html_hash.update(utf8_encode(content))
                        else:
                            break

        return html_hash.hexdigest()

    def find_file(self, path):
        if callable(self.config.compressor_source_dirs):
            filename = self.config.compressor_source_dirs(path)
            if os.path.exists(filename):
                return filename
        else:
            if isinstance(self.config.compressor_source_dirs, basestring):
                dirs = [self.config.compressor_source_dirs]
            else:
                dirs = self.config.compressor_source_dirs

            for d in dirs:
                if self.config.compressor_static_prefix_precompress is not None and path.startswith(
                        '/'):
                    path = path.replace(
                        self.config.compressor_static_prefix_precompress, '',
                        1).lstrip(os.sep).lstrip('/')
                filename = os.path.join(d, path)
                if os.path.exists(filename):
                    return filename

        raise IOError(2, u('File not found {0}').format(path))

    def find_compilable_tags(self, soup):
        tags = ['link', 'style', 'script']
        for tag in soup.find_all(tags):

            # don't compress externally hosted assets
            src = tag.get('src') or tag.get('href')
            if src and (src.startswith('http') or src.startswith('//')):
                continue

            if tag.get('type') is None:
                if tag.name == 'script':
                    tag['type'] = 'text/javascript'
                if tag.name == 'style':
                    tag['type'] = 'text/css'
            else:
                tag['type'] == tag['type'].lower()

            if tag.get('type') is None:
                raise RuntimeError(
                    u('Tags to be compressed must have a type attribute: {0}').
                    format(u(tag)))

            yield tag

    def get_contents(self, src):
        if isinstance(src, file):
            return u(src.read())
        else:
            return u(src)

    def render_element(self, filename, type):
        """Returns an html element pointing to filename as a string.
        """
        if type.lower() == 'css':
            return u('<link type="text/css" rel="stylesheet" href="{0}" />'
                     ).format(filename)
        elif type.lower() == 'js':
            return u('<script type="text/javascript" src="{0}"></script>'
                     ).format(filename)
        else:
            raise RuntimeError(
                u('Unsupported type of compression {0}').format(type))

    def get_configs_from_environment(self, environment):
        configs = {}
        for key in dir(environment):
            if key.startswith('compressor_'):
                configs[key] = getattr(environment, key)
        return configs

    def offline_compress(self, environment, template_dirs):

        if isinstance(template_dirs, basestring):
            template_dirs = [template_dirs]

        configs = self.get_configs_from_environment(environment)
        self.config.update(**configs)

        compressor_nodes = {}
        parser = Jinja2Parser(charset='utf-8', env=environment)
        for template_path in self.find_template_files(template_dirs):
            try:
                template = parser.parse(template_path)
            except IOError:  # unreadable file -> ignore
                continue
            except TemplateSyntaxError:  # broken template -> ignore
                continue
            except TemplateDoesNotExist:  # non existent template -> ignore
                continue
            except UnicodeDecodeError:
                continue

            try:
                nodes = list(parser.walk_nodes(template))
            except (TemplateDoesNotExist, TemplateSyntaxError):
                continue
            if nodes:
                template.template_name = template_path
                compressor_nodes.setdefault(template, []).extend(nodes)

        if not compressor_nodes:
            raise OfflineGenerationError(
                "No 'compress' template tags found in templates. "
                "Try setting follow_symlinks to True")

        for template, nodes in compressor_nodes.items():
            for node in nodes:
                parser.render_node({}, node, globals=environment.globals)

    def find_template_files(self, template_dirs):
        templates = set()
        for d in template_dirs:
            for root, dirs, files in os.walk(
                    d, followlinks=self.config.compressor_follow_symlinks):
                templates.update(
                    os.path.join(root, name) for name in files
                    if not name.startswith('.'))
        return templates