forked from Botanical/BotanJS
336 lines
6.9 KiB
Python
336 lines
6.9 KiB
Python
|
|
import os
|
|
import re
|
|
import base64
|
|
import zlib
|
|
import hashlib
|
|
import xml.etree.ElementTree as ET
|
|
|
|
PY_SEP = os.path.sep
|
|
|
|
def wrapScope( src ):
|
|
return "(function(){" + src + "})();"
|
|
|
|
class Resolver:
|
|
resolved = None
|
|
|
|
bMap = None
|
|
parentMap = None
|
|
|
|
EX_PROP = "prop"
|
|
EX_FUNC = "method"
|
|
EX_CLASS = "class"
|
|
|
|
def __init__( self, classMap ):
|
|
self.classMap = classMap
|
|
self._reload()
|
|
|
|
def _reload( self ):
|
|
self.bMap = ET.parse( self.classMap )
|
|
# ElementTree does not support Element.find( ".." )
|
|
# Make a parent map to work around
|
|
self.parentMap = { c:p for p in self.bMap.iter() for c in p }
|
|
|
|
self.resolved = []
|
|
|
|
def resource( self, elem ):
|
|
if "src" in elem.attrib:
|
|
return elem.attrib[ "src" ]
|
|
|
|
parent = self.parentMap[ elem ]
|
|
|
|
if parent != None:
|
|
return self.resource( parent )
|
|
|
|
def resolve( self, c, classList ):
|
|
self.resolved = []
|
|
self.__resolve( c, classList )
|
|
|
|
def __resolve( self, c, classList ):
|
|
|
|
lista = list( "[@name=\"" + x + "\"]" for x in c.split(".") )
|
|
|
|
fx = "/".join( self.EX_CLASS + x for x in lista[:-1] )
|
|
|
|
it = lista[-1]
|
|
# Test if class
|
|
elem = self.bMap.find( ".//" + fx + self.EX_CLASS + it )
|
|
|
|
if elem == None:
|
|
if fx != '': fx += "/"
|
|
|
|
# Test if prop
|
|
elem = self.bMap.find( ".//" + fx + self.EX_PROP + it )
|
|
|
|
# test if func
|
|
if elem == None:
|
|
elem = self.bMap.find( ".//" + fx + self.EX_FUNC + it )
|
|
|
|
if elem == None:
|
|
raise LookupError( "No such class: " + c )
|
|
|
|
imports = self.parentMap[ elem ].findall( "import" )
|
|
|
|
else:
|
|
imports = elem.findall( "import" )
|
|
|
|
self.resolved.append( c )
|
|
|
|
for imp in imports:
|
|
if imp.text not in self.resolved:
|
|
self.__resolve( imp.text, classList )
|
|
|
|
classList.append([ c, elem ])
|
|
|
|
class BotanClassResolver:
|
|
R = ""
|
|
CR = None
|
|
|
|
classMap = ""
|
|
flagCompress = True
|
|
returnHash = False
|
|
resv = None
|
|
|
|
def __init__( self, jwork, BotanRoot, classMap, cacheRoot ):
|
|
|
|
self.JWork = jwork;
|
|
self.R = os.path.abspath( BotanRoot )
|
|
self.CR = os.path.abspath( cacheRoot )
|
|
|
|
os.makedirs( self.CR, 0o755, exist_ok = True )
|
|
|
|
if not os.path.exists( classMap ):
|
|
self.JWork.buildClassMap( self.R, classMap )
|
|
|
|
self.resv = Resolver( classMap )
|
|
|
|
def BotanFile( self, t ):
|
|
content = ""
|
|
with open( os.path.join( self.R, t ), "r" ) as f:
|
|
content = f.read()
|
|
|
|
return content
|
|
|
|
def BotanCache( self, t ):
|
|
content = ""
|
|
with open( t, "r" ) as f:
|
|
content = f.read()
|
|
|
|
return content
|
|
|
|
def cleanList( self, lista ):
|
|
olist = []
|
|
for i in lista:
|
|
if i not in olist:
|
|
olist.append( i )
|
|
return olist
|
|
|
|
def jsLookup( self, classList, classFiles ):
|
|
for c in classList:
|
|
src = self.resv.resource( c[1] )
|
|
|
|
if src == None:
|
|
raise LookupError( "Cannot find src file for: " + c[0] )
|
|
|
|
if src not in classFiles:
|
|
classFiles.append( src )
|
|
|
|
def cssLookup( self, jsList, cssList ):
|
|
|
|
for f in jsList:
|
|
possibleList = []
|
|
|
|
cssFile = os.path.splitext( f )[0] + ".css"
|
|
|
|
if cssFile not in possibleList:
|
|
possibleList.append( cssFile )
|
|
|
|
f = f.split( PY_SEP )
|
|
l = len( f )
|
|
|
|
for i in range( 1, l ):
|
|
cssFile = PY_SEP.join( x for x in f[:-i] ) + PY_SEP + "@_this.css"
|
|
if cssFile not in possibleList:
|
|
possibleList.append( cssFile )
|
|
|
|
possibleList.sort()
|
|
|
|
for f in possibleList:
|
|
f = f.replace( "@_this.css", "_this.css" )
|
|
if os.path.exists( os.path.join( self.R, f ) ):
|
|
cssList.append( f )
|
|
|
|
|
|
def getCache( self, fileList, cName, mode ):
|
|
if self.CR == None:
|
|
return None
|
|
|
|
md5 = hashlib.md5( bytearray( "".join( fileList ), "utf-8" ) ).hexdigest()
|
|
|
|
cName[0] = oFHash = md5 + "." + mode
|
|
cFHash = md5 + ".c." + mode
|
|
|
|
# Raw file
|
|
oFile = os.path.join( self.CR, oFHash )
|
|
# Compressed file
|
|
cFile = os.path.join( self.CR, cFHash )
|
|
|
|
dates = list(
|
|
os.path.getmtime( os.path.join( self.R, x ) )
|
|
if os.path.exists( os.path.join( self.R, x ) ) else -1
|
|
for x in fileList
|
|
)
|
|
|
|
# Root file date
|
|
dates.append( os.path.getmtime( os.path.join( self.R, "_this.js" ) ) );
|
|
|
|
if self.useCache( cFile, dates ) and self.flagCompress == True:
|
|
return cFHash if self.returnHash else self.BotanCache( cFile )
|
|
|
|
elif self.useCache( oFile, dates ):
|
|
self.JWork.saveCache.delay(
|
|
oFile
|
|
# Content is None to initiate a compression
|
|
, None
|
|
, mode
|
|
, os.path.join( self.R, "externs" )
|
|
)
|
|
|
|
return oFHash if self.returnHash else self.BotanCache( oFile )
|
|
|
|
def useCache( self, f, dList ):
|
|
if not os.path.exists( f ):
|
|
return False
|
|
|
|
t = os.path.getmtime( f )
|
|
|
|
for i in dList:
|
|
if t < i:
|
|
return False
|
|
|
|
return True
|
|
|
|
def compileJs( self, cList, xList ):
|
|
md5 = [ None ]
|
|
|
|
for x in xList:
|
|
cList.remove( x ) if x in cList else None
|
|
|
|
cacheFile = self.getCache( cList, md5, "js" )
|
|
|
|
if cacheFile != None:
|
|
return cacheFile;
|
|
|
|
# The root file
|
|
outputJs = self.BotanFile( "_this.js" )
|
|
|
|
|
|
for f in cList:
|
|
path = (
|
|
os.path.splitext( f )[0]
|
|
.replace( PY_SEP, "." )
|
|
.replace( "._this", "" )
|
|
)
|
|
struct = ";BotanJS.define( \"" + path + "\" );"
|
|
|
|
outputJs += struct + self.BotanFile( f )
|
|
|
|
outputJs = wrapScope( outputJs )
|
|
|
|
[ self.JWork.saveCache if self.returnHash else self.JWork.saveCache.delay ][0] (
|
|
os.path.join( self.CR, md5[0] )
|
|
, outputJs
|
|
, "js"
|
|
, os.path.join( self.R, "externs" )
|
|
)
|
|
|
|
if self.returnHash:
|
|
return md5[0]
|
|
|
|
return outputJs
|
|
|
|
def compileCss( self, cList, xList ):
|
|
cssIList = []
|
|
cssXList = []
|
|
self.cssLookup( cList, cssIList )
|
|
self.cssLookup( xList, cssXList )
|
|
|
|
cList = []
|
|
xList = cssXList
|
|
|
|
for x in cssIList:
|
|
cList.append( x ) if x not in xList else None
|
|
|
|
md5 = [ None ]
|
|
cacheFile = self.getCache( cList, md5, "css" )
|
|
|
|
if cacheFile != None:
|
|
return cacheFile;
|
|
|
|
outputCss = ""
|
|
|
|
for f in self.cleanList( cList ):
|
|
outputCss += self.BotanFile( f )
|
|
|
|
[ self.JWork.saveCache if self.returnHash else self.JWork.saveCache.delay ][0] (
|
|
os.path.join( self.CR, md5[0] ), outputCss, "css"
|
|
)
|
|
|
|
if self.returnHash:
|
|
return md5[0]
|
|
|
|
return outputCss
|
|
|
|
def getAPI( self, code, mode ):
|
|
self.flagCompress = True
|
|
# Should reload on debug mode only
|
|
self.resv._reload()
|
|
flag = mode[0]
|
|
requestAPIs = code
|
|
sp = "/"
|
|
|
|
if flag == "o":
|
|
mode = mode[1:]
|
|
elif flag == "r":
|
|
mode = mode[1:]
|
|
self.flagCompress = False
|
|
else:
|
|
self.returnHash = True
|
|
requestAPIs = (
|
|
# decode -> decompress -> split
|
|
zlib.decompress( base64.b64decode( code, None, True ) )
|
|
.decode( "utf-8" )
|
|
)
|
|
sp = ","
|
|
|
|
# strip malicious
|
|
requestAPIs = (
|
|
requestAPIs
|
|
.replace( "[^A-Za-z\.\*" + re.escape( sp ) + " ]", "" )
|
|
.split( sp )
|
|
)
|
|
|
|
imports = []
|
|
excludes = []
|
|
|
|
for apis in requestAPIs:
|
|
|
|
if apis == None: continue
|
|
|
|
classList = []
|
|
lookupList = imports
|
|
|
|
if apis[0] == "-":
|
|
apis = apis[1:]
|
|
lookupList = excludes
|
|
|
|
self.resv.resolve( apis, classList )
|
|
self.jsLookup( classList, lookupList )
|
|
|
|
if mode == "js":
|
|
return self.compileJs( imports, excludes )
|
|
elif mode == "css":
|
|
return self.compileCss( imports, excludes )
|
|
|
|
raise TypeError( "Invalid mode: " + js )
|