-
Notifications
You must be signed in to change notification settings - Fork 40
/
assets.py
221 lines (148 loc) · 6.22 KB
/
assets.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# -*- encoding: utf-8 -*-
#
# Copyright 2013 Martin Zimmermann <info@posativ.org>. All rights reserved.
# License: BSD Style, 2 clauses -- see LICENSE.
import os
import io
import re
import time
import stat
from tempfile import mkstemp
from functools import partial
from collections import defaultdict
from os.path import join, isfile, getmtime, split, splitext
from acrylamid import core, helpers, log
from acrylamid.errors import AcrylamidException
from acrylamid.helpers import mkfile, event
from acrylamid.readers import relfilelist
ns = "assets"
__writers = None
__defaultwriter = None
class Writer(object):
"""A 'open-file-and-write-to-dest' writer. Only operates if the source
file has been modified or the destination does not exists."""
uses = None
def __init__(self, conf, env):
self.conf = conf
self.env = env
def filter(self, input, directory):
"""Filter input set for includes and imports using `uses` pattern.
The pattern must include a group 'file' that holds the included item.
If the pattern is the empty string (the default), return input.
Note, that Acrylamid will only read the first 512 bytes of a file
to check for includes. Therefore, do not move your includes to the
end of file."""
if not self.uses:
return input
imports = set()
for path in input:
with io.open(join(directory, path)) as fp:
text = fp.read(512)
for m in re.finditer(self.uses, text, re.MULTILINE):
imports.add(m.group('file'))
return input.difference(imports)
def modified(self, src, dest):
return not isfile(dest) or getmtime(src) > getmtime(dest)
def generate(self, src, dest):
return io.open(src, 'rb')
def write(self, src, dest, force=False, dryrun=False):
if not force and not self.modified(src, dest):
return event.skip(ns, dest)
mkfile(self.generate(src, dest), dest, ns=ns, force=force, dryrun=dryrun)
def shutdown(self):
pass
class HTML(Writer):
"""Copy HTML files to output if not in theme directory."""
ext = '.html'
def write(self, src, dest, **kw):
if src.startswith(self.conf['theme'].rstrip('/') + '/'):
return
return super(HTML, self).write(src, dest, **kw)
class XML(HTML):
ext = '.xml'
class Template(HTML):
"""Transform HTML files using the current markup engine. You can inherit
from all theme files inside the theme directory."""
def __init__(self, conf, env):
map(env.engine.extend, conf['static'])
super(Template, self).__init__(conf, env)
def generate(self, src, dest):
relpath = split(src[::-1])[0][::-1] # (head, tail) but reversed behavior
return self.env.engine.fromfile(relpath).render(env=self.env, conf=self.conf)
def write(self, src, dest, force=False, dryrun=False):
dest = dest.replace(splitext(src)[-1], self.target)
return super(Template, self).write(src, dest, force=force, dryrun=dryrun)
@property
def ext(self):
return tuple(self.env.engine.extension)
target = '.html'
class System(Writer):
def write(self, src, dest, force=False, dryrun=False):
dest = dest.replace(self.ext, self.target)
if not force and isfile(dest) and getmtime(dest) > getmtime(src):
return event.skip(ns, dest)
if isinstance(self.cmd, basestring):
self.cmd = [self.cmd, ]
tt = time.time()
fd, path = mkstemp(dir=core.cache.cache_dir)
# make destination group/world-readable as other files from Acrylamid
os.chmod(path, os.stat(path).st_mode | stat.S_IRGRP | stat.S_IROTH)
try:
res = helpers.system(self.cmd + [src])
except (OSError, AcrylamidException) as e:
if isfile(dest):
os.unlink(dest)
log.exception('%s: %s' % (e.__class__.__name__, e.args[0]))
else:
with os.fdopen(fd, 'w') as fp:
fp.write(res)
with io.open(path, 'rb') as fp:
mkfile(fp, dest, time.time()-tt, ns, force, dryrun)
finally:
os.unlink(path)
class SASS(System):
ext, target = '.sass', '.css'
cmd = ['sass', ]
# matches @import 'foo.sass' (and optionally without quotes)
uses = r'^@import ["\']?(?P<file>.+?\.sass)["\']?'
class SCSS(System):
ext, target = '.scss', '.css'
cmd = ['sass', '--scss']
# matches @import 'foo.scss', we do not support import 'foo'; or url(foo);
uses = r'^@import ["\'](?P<file>.+?\.scss)["\'];'
class LESS(System):
ext, target = '.less', '.css'
cmd = ['lessc', ]
# matches @import 'foo.less'; and @import-once ...
uses = r'^@import(-once)? ["\'](?P<file>.+?\.less)["\'];'
class CoffeeScript(System):
ext, target = '.coffee', '.js'
cmd = ['coffee', '-cp']
class IcedCoffeeScript(System):
ext, target = '.iced', '.js'
cmd = ['iced', '-cp']
def worker(conf, env, args):
"""Compile each file extension for each folder in its own process.
"""
(ext, directory), items = args[0], args[1]
writer = __writers.get(ext, __defaultwriter)
for path in writer.filter(items, directory):
src, dest = join(directory, path), join(conf['output_dir'], path)
writer.write(src, dest, force=env.options.force, dryrun=env.options.dryrun)
def compile(conf, env):
"""Copy/Compile assets to output directory. All assets from the theme
directory (except for templates) and static directories can be compiled or
just copied using several built-in writers."""
global __writers, __defaultwriter
__writers = {}
__defaultwriter = Writer(conf, env)
files = defaultdict(set)
__writers = dict((cls.ext, cls) for cls in (
globals()[writer](conf, env) for writer in conf.static_filter
))
for path, directory in relfilelist(conf['theme'], conf['theme_ignore'], env.engine.templates):
files[(splitext(path)[1], directory)].add(path)
for prefix in conf['static']:
for path, directory in relfilelist(prefix, conf['static_ignore']):
files[(splitext(path)[1], directory)].add(path)
map(partial(worker, conf, env), files.iteritems())