#!/usr/bin/python3 # -*- coding: utf-8 -*- # ############################################################################# # Copyright (C) 2018 manatlan manatlan[at]gmail(dot)com # # MIT licence # # https://github.com/manatlan/vbuild # ############################################################################# __version__ = "0.8.1" # py2.7 & py3.5 !!!! import re, os, json, glob, itertools, traceback, 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 == "html": if self._p1 is not None: raise VBuildException( "Component %s contains more than one html" % 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 == "html" 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 = [ (prefix + " " + i.replace(":scope", "").strip()).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(":", "-").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) # print("==============>", html, "<==============") self.tags = [name] # self.html="""<script type="text/x-template" id="%s">%s</script>""" % (tplId, transHtml(html) ) # self._html = [(tplId, html)] self._html = vp.html.value 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) return self._html @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: import pscript 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[:] else: raise Exception("Can't find valid content inside '{' and '}'") return js 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) """ import pscript 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())