-
Notifications
You must be signed in to change notification settings - Fork 0
/
vbuild.py
469 lines (398 loc) · 17.1 KB
/
vbuild.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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
#!/usr/bin/python2
# -*- coding: utf-8 -*-
# #############################################################################
# Copyright (C) 2018 manatlan manatlan[at]gmail(dot)com
#
# MIT licence
#
# https://github.com/manatlan/vbuild
# #############################################################################
__version__ = "0.7.4" # py2.7 & py3.5 !!!!
import re, os, json, glob, itertools, traceback, pscript, subprocess, pkgutil
try:
from HTMLParser import HTMLParser
import urllib2 as urlrequest
import urllib as urlparse
except ImportError:
from html.parser import HTMLParser
import urllib.request as urlrequest
import urllib.parse as urlparse
transHtml = lambda x: x # override them to use your own transformer/minifier
transStyle = lambda x: x
transScript = lambda x: x
partial = ""
fullPyComp = True # 3 states ;-)
# None : minimal py comp, it's up to u to include "pscript.get_full_std_lib()"
# False : minimal py comp, vbuild will include the std lib
# True : each component generate its needs (default)
hasLess = bool(pkgutil.find_loader("lesscpy"))
hasSass = bool(pkgutil.find_loader("scss"))
hasClosure = bool(pkgutil.find_loader("closure"))
class VBuildException(Exception): pass
def minimize(code):
if hasClosure:
return jsmin(code)
else:
return jsminOnline(code)
def jsminOnline(code):
""" JS-minimize (transpile to ES5 JS compliant) thru a online service
(https://closure-compiler.appspot.com/compile)
"""
data = [
('js_code', code),
('compilation_level', 'SIMPLE_OPTIMIZATIONS'),
('output_format', 'json'),
('output_info', 'compiled_code'),
('output_info', 'errors'),
]
try:
req = urlrequest.Request("https://closure-compiler.appspot.com/compile",
urlparse.urlencode(data).encode("utf8"),
{'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'})
response = urlrequest.urlopen(req)
r = json.loads(response.read())
response.close()
code = r.get("compiledCode", None)
except Exception as e:
raise VBuildException("minimize error: %s" % e)
if code:
return code
else:
raise VBuildException("minimize error: %s" % r.get("errors", None))
def jsmin(code): # need java & pip/closure
""" JS-minimize (transpile to ES5 JS compliant) with closure-compiler
(pip package 'closure', need java !)
"""
if hasClosure:
import closure # py2 or py3
else:
raise VBuildException("jsmin error: closure is not installed (sudo pip closure)")
cmd = ["java", "-jar", closure.get_jar_filename()]
try:
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
except Exception as e:
raise VBuildException("jsmin error: %s" % e)
out, err = p.communicate(code.encode("utf8"))
if p.returncode == 0:
return out.decode("utf8")
else:
raise VBuildException("jsmin error:" + err.decode("utf8"))
def preProcessCSS(cnt, partial=""):
""" Apply css-preprocessing on css rules (according css.type) using a partial or not
return the css pre-processed
"""
if cnt.type in ["scss", "sass"]:
if hasSass:
from scss.compiler import compile_string # lang="scss"
return compile_string(partial + "\n" + cnt.value)
else:
print("***WARNING*** : miss 'sass' preprocessor : sudo pip install pyscss")
return cnt.value
elif cnt.type in ["less"]:
if hasLess:
import lesscpy, six
return lesscpy.compile(six.StringIO(partial + "\n" + cnt.value), minify=True)
else:
print("***WARNING*** : miss 'less' preprocessor : sudo pip install lesscpy")
return cnt.value
else:
return cnt.value
class Content:
def __init__(self, v, typ=None):
self.type = typ
self.value = v.strip("\n\r\t ")
def __repr__(self):
return self.value
class VueParser(HTMLParser):
""" Just a class to extract <template/><script/><style/> from a buffer.
self.html/script/styles/scopedStyles are Content's object, or list of.
"""
voidElements = "area base br col command embed hr img input keygen link menuitem meta param source track wbr".split(
" ")
def __init__(self, buf, name=""):
""" Extract stuff from the vue/buffer 'buf'
(name is just useful for naming the component in exceptions)
"""
HTMLParser.__init__(self)
self.name = name
self._p1 = None
self._level = 0
self._scriptLang = None
self._styleLang = None
self.rootTag = None
self.html, self.script, self.styles, self.scopedStyles = None, None, [], []
self.feed(buf.strip("\n\r\t "))
def handle_starttag(self, tag, attrs):
self._tag = tag
# don't manage if it's a void element
if tag not in self.voidElements:
self._level += 1
attributes = dict([(k.lower(), v and v.lower()) for k, v in attrs])
if tag == "style" and attributes.get("lang", None):
self._styleLang = attributes["lang"]
if tag == "script" and attributes.get("lang", None):
self._scriptLang = attributes["lang"]
if self._level == 1 and tag == "template":
if self._p1 is not None: raise VBuildException(
"Component %s contains more than one template" % self.name)
self._p1 = self.getOffset() + len(self.get_starttag_text())
if self._level == 2 and self._p1: # test p1, to be sure to be in a template
if self.rootTag is not None: raise VBuildException(
"Component %s can have only one top level tag !" % self.name)
self.rootTag = tag
def handle_endtag(self, tag):
if tag not in self.voidElements:
if tag == "template" and self._p1: # don't watch the level (so it can accept mal formed html
self.html = Content(self.rawdata[self._p1:self.getOffset()])
self._level -= 1
def handle_data(self, data):
if self._level == 1:
if self._tag == "script": self.script = Content(data, self._scriptLang)
if self._tag == "style":
if "scoped" in self.get_starttag_text().lower():
self.scopedStyles.append(Content(data, self._styleLang))
else:
self.styles.append(Content(data, self._styleLang))
def getOffset(self):
lineno, off = self.getpos()
rtn = 0
for _ in range(lineno - 1):
rtn = self.rawdata.find('\n', rtn) + 1
return rtn + off
def mkPrefixCss(css, prefix=""):
"""Add the prexix (css selector) to all rules in the 'css'
(used to scope style in context)
"""
medias = []
while "@media" in css:
p1 = css.find("@media", 0)
p2 = css.find("{", p1) + 1
lv = 1
while lv > 0:
lv += 1 if css[p2] == "{" else -1 if css[p2] == "}" else 0
p2 += 1
block = css[p1:p2]
mediadef = block[:block.find("{")].strip()
mediacss = block[block.find("{") + 1:block.rfind("}")].strip()
css = css.replace(block, "")
medias.append((mediadef, mkPrefixCss(mediacss, prefix)))
lines = []
css = re.sub(re.compile("/\*.*?\*/", re.DOTALL), "", css)
css = re.sub(re.compile("[ \t\n]+", re.DOTALL), " ", css)
for rule in re.findall(r'[^}]+{[^}]+}', css):
sels, decs = rule.split("{", 1)
if prefix:
l = [(i.replace(":scope", "").strip() + " " + prefix).strip() for i in sels.split(",")]
else:
l = [(i.strip()) for i in sels.split(",")]
lines.append(", ".join(l) + " {" + decs.strip())
lines.extend(["%s {%s}" % (d, c) for d, c in medias])
return "\n".join(lines).strip("\n ")
class VBuild:
""" the main class, provide an instance :
.style : contains all the styles (scoped or not)
.script: contains a (js) Vue.component() statement to initialize the component
.html : contains the <script type="text/x-template"/>
.tags : list of component's name whose are in the vbuild instance
"""
def __init__(self, filename, content):
""" Create a VBuild class, by providing a :
filename: which will be used to name the component, and create the namespace for the template
content: the string buffer which contains the sfc/vue component
"""
if not filename:
raise VBuildException("Component %s should be named" % filename)
if type(content) != type(filename): # only py2, transform
if type(content) == unicode: # filename to the same type
filename = filename.decode("utf8") # of content to avoid
else: # troubles with implicit
filename = filename.encode("utf8") # ascii conversions (regex)
name = os.path.splitext(os.path.basename(filename))[0]
unique = filename[:-4].replace("/", "-").replace("\\", "-").replace(":", "-")
# unique = name+"-"+''.join(random.choice(string.letters + string.digits) for _ in range(8))
tplId = "tpl-" + unique
dataId = "data-" + unique
vp = VueParser(content, filename)
if vp.html is None:
raise VBuildException("Component %s doesn't have a template" % filename)
else:
html = re.sub(r'^<([\w-]+)', r"<\1 %s" % dataId, vp.html.value)
self.tags = [name]
# self.html="""<script type="text/x-template" id="%s">%s</script>""" % (tplId, transHtml(html) )
self._html = [(tplId, html)]
self._styles = []
for style in vp.styles:
self._styles.append(("", style, filename))
for style in vp.scopedStyles:
self._styles.append(("[%s]" % dataId, style, filename))
# and set self._script !
if vp.script and ("class Component:" in vp.script.value):
######################################################### python
try:
self._script = [mkPythonVueComponent(name, '#' + tplId, vp.script.value, fullPyComp)]
except Exception as e:
raise VBuildException("Python Component '%s' is broken : %s" % (filename, traceback.format_exc()))
else:
######################################################### js
try:
self._script = [mkClassicVueComponent(name, '#' + tplId, vp.script and vp.script.value)]
except Exception as e:
raise VBuildException("JS Component %s contains a bad script" % filename)
@property
def html(self):
""" Return HTML (script tags of embbeded components), after transHtml"""
l = []
for tplId, html in self._html:
l.append("""<script type="text/x-template" id="%s">%s</script>""" % (tplId, transHtml(html)))
return "\n".join(l)
@property
def script(self):
""" Return JS (js of embbeded components), after transScript"""
js = "\n".join(self._script)
isPyComp = "_pyfunc_op_instantiate(" in js # in fact : contains
isLibInside = "var _pyfunc_op_instantiate" in js
if (fullPyComp is False) and isPyComp and not isLibInside:
return transScript(pscript.get_full_std_lib() + "\n" + js)
else:
return transScript(js)
@property
def style(self):
""" Return CSS (styles of embbeded components), after preprocess css & transStyle"""
style = ""
try:
for prefix, s, filename in self._styles:
style += mkPrefixCss(preProcessCSS(s, partial), prefix) + "\n"
except Exception as e:
raise VBuildException("Component '%s' got a CSS-PreProcessor trouble : %s" % (filename, e))
return transStyle(style).strip()
def __add__(self, o):
same = set(self.tags).intersection(set(o.tags))
if same: raise VBuildException("You can't have multiple '%s'" % list(same)[0])
self._html.extend(o._html)
self._script.extend(o._script)
self._styles.extend(o._styles)
self.tags.extend(o.tags)
return self
def __radd__(self, o):
return self if o == 0 else self.__add__(o)
def __getstate__(self):
return self.__dict__
def __setstate__(self, d):
self.__dict__ = d
def __repr__(self):
""" return an html ready represenation of the component(s) """
hh = self.html
jj = self.script
ss = self.style
s = ""
if ss: s += "<style>\n%s\n</style>\n" % ss
if hh: s += "%s\n" % hh
if jj: s += "<script>\n%s\n</script>\n" % jj
return s
def mkClassicVueComponent(name, template, code):
if code is None:
js = "{}"
else:
p1 = code.find("{")
p2 = code.rfind("}")
if 0 <= p1 <= p2:
js = code[p1:p2 + 1]
else:
raise Exception("Can't find valid content inside '{' and '}'")
return """Vue.component('%s', %s)""" % (name, js.replace("{", "{template:'%s'," % template, 1))
def mkPythonVueComponent(name, template, code, genStdLibMethods=True):
""" Transpile the component 'name', which have the template 'template',
and the code 'code' (which should contains a valid Component class)
to a valid Vue.component js statement.
genStdLibMethods : generate own std lib method inline (with the js)
(if False: use pscript.get_full_std_lib() to get them)
"""
code = code.replace("class Component:", "class C:") # minimize class name (todo: use py2js option for that)
exec(code, globals(), locals())
klass = locals()["C"]
computeds = []
watchs = []
methods = []
lifecycles = []
classname = klass.__name__
props = []
for oname, obj in vars(klass).items():
if callable(obj):
if not oname.startswith("_"):
if oname.startswith("COMPUTED_"):
computeds.append('%s: %s.prototype.%s,' % (oname[9:], classname, oname))
elif oname.startswith("WATCH_"):
if obj.__defaults__:
varwatch = obj.__defaults__[0] # not neat (take the first default as whatch var)
watchs.append('"%s": %s.prototype.%s,' % (varwatch, classname, oname))
else:
raise VBuildException("name='var_to_watch' is not specified in %s" % oname)
elif oname in ["MOUNTED", "CREATED", "UPDATED", "BEFOREUPDATE", "BEFOREDESTROY", "DESTROYED"]:
lifecycles.append('%s: %s.prototype.%s,' % (oname.lower(), classname, oname))
else:
methods.append('%s: %s.prototype.%s,' % (oname, classname, oname))
elif oname == "__init__":
props = list(obj.__code__.co_varnames)[1:]
methods = "\n".join(methods)
computeds = "\n".join(computeds)
watchs = "\n".join(watchs)
lifecycles = "\n".join(lifecycles)
pyjs = pscript.py2js(code, inline_stdlib=genStdLibMethods) # https://pscript.readthedocs.io/en/latest/api.html
return """
var %(name)s=(function() {
%(pyjs)s
function construct(constructor, args) {
function F() {return constructor.apply(this, args);}
F.prototype = constructor.prototype;
return new F();
}
return Vue.component('%(name)s',{
name: "%(name)s",
props: %(props)s,
template: '%(template)s',
data: function() {
var props=[]
var ll=%(props)s;
for(var i in ll) props.push( this.$props[ ll[i] ] )
var i=construct(%(classname)s,props) // new %(classname)s(...props)
return JSON.parse(JSON.stringify( i ));
},
computed: {
%(computeds)s
},
methods: {
%(methods)s
},
watch: {
%(watchs)s
},
%(lifecycles)s
})
})();
""" % locals()
def render(*filenames):
""" Helpers to render VBuild's instances by providing filenames or pattern (glob's style)"""
isPattern = lambda f: ("*" in f) or ("?" in f)
files = []
for i in filenames:
if isinstance(i, list):
files.extend(i)
else:
files.append(i)
files = [glob.glob(i) if isPattern(i) else [i] for i in files]
files = list(itertools.chain(*files))
ll = []
for f in files:
try:
with open(f, "r+") as fid:
content = fid.read()
except IOError as e:
raise VBuildException(str(e))
ll.append(VBuild(f, content))
return sum(ll)
if __name__ == "__main__":
print("Less installed (lesscpy) :", hasLess)
print("Sass installed (pyScss) :", hasSass)
print("Closure installed (closure) :", hasClosure)
if (os.path.isfile("tests.py")): exec(open("tests.py").read())
# ~ if(os.path.isfile("test_py_comp.py")): exec(open("test_py_comp.py").read())