forked from Botanical/BotanJS
Initial commit
This commit is contained in:
0
botanjs/service/__init__.py
Normal file
0
botanjs/service/__init__.py
Normal file
335
botanjs/service/jclassresv.py
Normal file
335
botanjs/service/jclassresv.py
Normal file
@@ -0,0 +1,335 @@
|
||||
|
||||
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 )
|
49
botanjs/service/jwork.py
Normal file
49
botanjs/service/jwork.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
from celery import Celery
|
||||
from celery.utils.log import get_task_logger
|
||||
from botanjs.compressor.closure import Wrapper as ClosureWrapper
|
||||
from botanjs.compressor.yui import Wrapper as YUIWrapper
|
||||
from botanjs.classmap import ClassMap
|
||||
|
||||
app = Celery( "botanJWork" )
|
||||
log = get_task_logger( __name__ )
|
||||
|
||||
if os.path.exists( "settings.ini" ):
|
||||
from botanjs.config import Config
|
||||
app.conf.update( BROKER_URL = Config["BotanJS"]["CeleryBroker"] )
|
||||
|
||||
class JWork:
|
||||
|
||||
@app.task()
|
||||
def saveCache( location, content = None, mode = None, externs = "" ):
|
||||
if content != None:
|
||||
log.info( "Writing file(" + str( len( content ) ) + "): " + os.path.abspath( location ) )
|
||||
with open( location, "w" ) as f:
|
||||
f.write( content )
|
||||
|
||||
if mode == "js":
|
||||
JWork.compressJs.delay( location, externs )
|
||||
elif mode == "css":
|
||||
JWork.compressCss.delay( location )
|
||||
|
||||
@app.task()
|
||||
def compressJs( md5, externs ):
|
||||
log.info( "Compress js: " + md5 )
|
||||
w = ClosureWrapper()
|
||||
w.scanExterns( externs )
|
||||
w.compress( md5 )
|
||||
|
||||
@app.task()
|
||||
def compressCss( md5 ):
|
||||
log.info( "Compress css: " + md5 )
|
||||
w = YUIWrapper()
|
||||
w.compress( md5 )
|
||||
|
||||
@app.task()
|
||||
def buildClassMap( src, location ):
|
||||
log.info( "Building Class Map" )
|
||||
c = ClassMap( src )
|
||||
|
||||
with open( location, "w" ) as f:
|
||||
f.write( c.build() )
|
@@ -0,0 +1,40 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Botan JS - Test</title>
|
||||
<script>
|
||||
var debugEnv = true;
|
||||
</script>
|
||||
|
||||
<link href="/ocss/Dandelion.CSSAnimations.MovieClip" rel="stylesheet" type="text/css" />
|
||||
<script src="/ojs/Dandelion.CSSAnimations.MovieClip"></script>
|
||||
<script>
|
||||
function load ()
|
||||
{
|
||||
var CSSAnimations = BotanJS.import( "Dandelion.CSSAnimations.*" );
|
||||
var mc = new CSSAnimations.MovieClip(
|
||||
"http://file.astropenguin.net/blog/Gothic/__A.png"
|
||||
// Row, Col
|
||||
, 6 , 7
|
||||
// wh
|
||||
, 125 , 125
|
||||
// Frame, start
|
||||
, 42 , 0
|
||||
);
|
||||
|
||||
// CSSAnimations.MouseOverMovie( mc );
|
||||
|
||||
document.body.appendChild( mc.stage );
|
||||
mc.nextFrame();
|
||||
mc.nextFrame();
|
||||
mc.nextFrame();
|
||||
mc.nextFrame();
|
||||
mc.nextFrame();
|
||||
|
||||
mc.prevFrame();
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<body onload="load()">
|
||||
</body>
|
||||
</html>
|
25
botanjs/service/templates/test/System.Net.ClassLoader.html
Normal file
25
botanjs/service/templates/test/System.Net.ClassLoader.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Botan JS - Test</title>
|
||||
<script>
|
||||
var debugEnv = true;
|
||||
var _AstConf_ = { };
|
||||
</script>
|
||||
|
||||
<link href="/ocss/System.Net.ClassLoader" rel="stylesheet" type="text/css" />
|
||||
<script src="/ojs/System.Net.ClassLoader"></script>
|
||||
<script>
|
||||
var Loader = BotanJS.import( "System.Net.ClassLoader" );
|
||||
|
||||
var makeMessagebox = function()
|
||||
{
|
||||
var MessageBox = BotanJS.import( "Components.MessageBox" );
|
||||
new MessageBox( "Title", "Message" ).show();
|
||||
};
|
||||
|
||||
var ldr = new Loader( "/" );
|
||||
ldr.load( "Components.MessageBox", makeMessagebox );
|
||||
</script>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
13
botanjs/service/templates/test/System.utils.IKey.html
Normal file
13
botanjs/service/templates/test/System.utils.IKey.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Botan JS - Test</title>
|
||||
<script>
|
||||
var debugEnv = true;
|
||||
var _AstConf_ = { };
|
||||
</script>
|
||||
|
||||
<link href="/ocss/System.utils.IKey" rel="stylesheet" type="text/css" />
|
||||
<script src="/ojs/System.utils.IKey"></script>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
19
botanjs/service/templates/test_list.html
Normal file
19
botanjs/service/templates/test_list.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Botan JS - List of Tests</title>
|
||||
<style>
|
||||
<!--
|
||||
body {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
-->
|
||||
</style>
|
||||
<body>
|
||||
<pre>
|
||||
{% for d in data %}
|
||||
<a href="/test/{{ d }}">{{ d }}</a>
|
||||
<br>
|
||||
{% endfor %}
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
72
botanjs/service/webapi.py
Executable file
72
botanjs/service/webapi.py
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
from flask import Flask
|
||||
from flask import Response
|
||||
from flask import render_template
|
||||
from botanjs.service.jclassresv import BotanClassResolver as JCResv
|
||||
from botanjs.service.jwork import app as CeleryApp, JWork
|
||||
import os
|
||||
|
||||
class WebAPI:
|
||||
|
||||
app = None
|
||||
|
||||
BRoot = None
|
||||
BMap = None
|
||||
BCache = None
|
||||
|
||||
def __init__( self, jsRoot = "../src", jsCache = "/tmp", brokerURL = None ):
|
||||
|
||||
self.test_templates = os.path.join(
|
||||
os.path.dirname( os.path.realpath( __file__ ) )
|
||||
, "templates"
|
||||
, "test"
|
||||
)
|
||||
|
||||
self.BRoot = os.path.abspath( jsRoot )
|
||||
self.BCache = os.path.join( jsCache, "botanjs" )
|
||||
self.BMap = os.path.join( self.BCache, "bmap.xml" )
|
||||
|
||||
if brokerURL != None:
|
||||
CeleryApp.conf.update( BROKER_URL = brokerURL )
|
||||
|
||||
self.app = Flask( __name__, static_url_path = '', static_folder = self.BCache )
|
||||
self.app.jinja_env.add_extension( "compressinja.html.HtmlCompressor" )
|
||||
|
||||
self.app.add_url_rule( "/test" , view_func = self.r_test_list )
|
||||
self.app.add_url_rule( "/test/<string:test_file>" , view_func = self.r_test )
|
||||
self.app.add_url_rule( "/<path:mode>/<path:code>" , view_func = self.api_request )
|
||||
|
||||
self.app.run( host="0.0.0.0" )
|
||||
|
||||
def r_test_list( self ):
|
||||
for root, dirs, files in os.walk( self.test_templates ):
|
||||
break
|
||||
|
||||
files.sort()
|
||||
files = ( os.path.splitext( x )[0] for x in files )
|
||||
|
||||
return render_template( "test_list.html", data = files )
|
||||
|
||||
def r_test( self, test_file ):
|
||||
return render_template( os.path.join( "test", test_file + ".html" ) )
|
||||
|
||||
|
||||
def api_request( self, mode, code ):
|
||||
try:
|
||||
t = mode[1:]
|
||||
if t == "js":
|
||||
t = "application/javascript"
|
||||
elif t == "css":
|
||||
t = "text/css"
|
||||
|
||||
srvHandler = JCResv( JWork, self.BRoot, self.BMap, self.BCache )
|
||||
return Response( srvHandler.getAPI( code, mode = mode ), mimetype = t )
|
||||
except LookupError as e:
|
||||
return str(e), 404
|
||||
# except Exception as e:
|
||||
# return str(e), 404
|
||||
|
||||
return "Invalid request", 404
|
||||
|
||||
if __name__ == "__main__":
|
||||
WebAPI()
|
Reference in New Issue
Block a user