Completed
Push — dev-4.1-unstable ( 2ae7a5...aefe68 )
by Felipe A.
01:09
created

SGMLCompressContext   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 104
Duplicated Lines 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 104
rs 10
wmc 29

5 Methods

Rating   Name   Duplication   Size   Complexity  
F feed() 0 30 9
B _options() 0 13 6
A __init__() 0 7 1
C _minify() 0 19 11
A finalize() 0 6 2
1
import re
2
3
import jinja2
4
import jinja2.ext
5
import jinja2.lexer
6
7
8
class SGMLCompressContext(object):
9
    re_whitespace = re.compile('[ \\t\\r\\n]+')
10
    token_class = jinja2.lexer.Token
11
    block_tokens = {
12
        'variable_begin': 'variable_end',
13
        'block_begin': 'block_end'
14
        }
15
    block_tags = {}  # block content will be treated as literal text
16
    jumps = {  # state machine jumps
17
        'text': {
18
            '<': 'tag',
19
            '<!--': 'comment',
20
            '<![CDATA[': 'cdata',
21
            },
22
        'lit1': {'"': 'tag'},
23
        'lit2': ("'", 'tag'),
24
        'tag': {
25
            '>': 'text',
26
            '"': 'lit1',
27
            "'": 'lit2'
28
            },
29
        'comment': {'-->': 'text'},
30
        'cdata': {']]>': 'text'}
31
        }
32
33
    def __init__(self):
34
        self.start = ''  # character which started current stae
35
        self.current = 'text'  # current state
36
        self.pending = ''  # buffer of current state data
37
        self.lineno = 0  # current token lineno
38
        self.skip_until_token = None  # inside token until this is met
39
        self.skip_until = None  # inside literal tag until this is met
40
41
    def finalize(self):
42
        if self.pending:
43
            data = self._minify(self.pending, self.current, self.start, True)
44
            yield self.token_class(self.lineno, 'data', data)
45
        self.start = ''
46
        self.pending = ''
47
48
    def _minify(self, data, current, start, partial=False):
49
        if current == 'tag':
50
            tagstart = start == '<'
51
            data = self.re_whitespace.sub(' ', data[1:] if tagstart else data)
52
            if tagstart:
53
                data = data.lstrip() if partial else data.strip()
54
                tagname = data.split(' ', 1)[0]
55
                self.skip_until = self.block_tags.get(tagname)
56
                return '<' + data
57
            elif partial:
58
                return data.rstrip()
59
            return start if data.strip() == start else data
60
        elif current == 'text':
61
            if not self.skip_until:
62
                return start if data.strip() == start else data
63
            elif not partial:
64
                self.skip_until = None
65
            return data
66
        return data
67
68
    def _options(self, value, current, start):
69
        offset = len(start)
70
        if self.skip_until and current == 'text':
71
            mark = self.skip_until
72
            index = value.find(mark, offset)
73
            if -1 != index:
74
                yield index, mark, current
75
        else:
76
            for mark, next in self.jumps[current].items():
77
                index = value.find(mark, offset)
78
                if -1 != index:
79
                    yield index, mark, next
80
        yield len(value), '', None  # avoid value errors on empty min()
81
82
    def feed(self, token):
83
        if self.skip_until_token:
84
            yield token
85
            if token.type == self.skip_until_token:
86
                self.skip_until_token = None
87
            return
88
89
        if token.type in self.block_tokens:
90
            for data in self.finalize():
91
                yield data
92
            yield token
93
            self.skip_until_token = self.block_tokens[token.type]
94
            return
95
96
        size = len(token.value)
97
        lineno = token.lineno
98
        self.pending += token.value
99
        while True:
100
            index, mark, next = min(
101
                self._options(self.pending, self.current, self.start),
102
                key=lambda x: (x[0], -len(x[1]))
103
                )
104
            if next is None:
105
                break
106
            data = self._minify(self.pending[:index], self.current, self.start)
107
            self.lineno = lineno if size > len(self.pending) else self.lineno
108
            self.start = mark
109
            self.current = next
110
            self.pending = self.pending[index:]
111
            yield self.token_class(self.lineno, 'data', data)
112
113
114
class HTMLCompressContext(SGMLCompressContext):
115
    block_tags = {
116
        'textarea': '</textarea>',
117
        'pre': '</pre>',
118
        'script': '</script>',
119
        'style': '</style>',
120
        }
121
122
123
class HTMLCompress(jinja2.ext.Extension):
124
    context_class = HTMLCompressContext
125
126
    def filter_stream(self, stream):
127
        feed = self.context_class()
128
        for token in stream:
129
            for data in feed.feed(token):
130
                yield data
131
        for data in feed.finalize():
132
            yield data
133