422 lines
13 KiB
Python
Executable File
422 lines
13 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# -*- coding: UTF-8 -*-
|
|
|
|
import sys, os, re
|
|
import cssmin
|
|
import jsmin
|
|
import codecs
|
|
import shutil
|
|
import StringIO
|
|
import urllib
|
|
|
|
import main
|
|
|
|
#===============================================================================
|
|
|
|
class FrontendBuilder(object):
|
|
|
|
def __init__ (self, frontend, settings, repositoryVersion):
|
|
if '.' in frontend:
|
|
moduleComponents = frontend.split('.')
|
|
self.module = moduleComponents[0]
|
|
self.submodule = moduleComponents[1]
|
|
else:
|
|
self.module = frontend
|
|
self.submodule = frontend
|
|
|
|
self.settings = settings
|
|
self.projectDir = main.projectBaseDir()
|
|
# self.repository = repository.repositoryWithPath(self.projectDir)
|
|
self.repositoryVersion = repositoryVersion
|
|
self.processedFiles = {}
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
def name (self):
|
|
raise NotImplementedError()
|
|
|
|
def projectResourceTypes (self):
|
|
raise NotImplementedError()
|
|
|
|
# def copyStaticResources (self, targetFolder):
|
|
# raise NotImplementedError()
|
|
|
|
def copyResourcesToFolder (self, targetFolder, backendSettings):
|
|
raise NotImplementedError()
|
|
|
|
def preprocessCSS (self, targetFile):
|
|
pass
|
|
|
|
#---------------------------------------------------------------------------
|
|
|
|
def log (self, message):
|
|
module = self.module
|
|
if (self.module != self.submodule):
|
|
module = module + "." + self.submodule
|
|
print "frontend [" + module + "]: " + message
|
|
|
|
|
|
def absolutePathForSources (self):
|
|
return os.path.join(self.projectDir, 'frontend', self.module)
|
|
|
|
|
|
def absolutePathForSourceFile (self, basePath, file):
|
|
return os.path.join(self.absolutePathForSources(), basePath, file)
|
|
|
|
|
|
def absolutePathForTargetFile (self, folder, basePath, file):
|
|
return os.path.join(folder, self.module, basePath, file)
|
|
|
|
|
|
def filterFiles (self, files):
|
|
result = []
|
|
|
|
for file in files:
|
|
if file.startswith('--'):
|
|
pass
|
|
else:
|
|
result.append(file)
|
|
|
|
return result
|
|
|
|
|
|
def copyResources (self, sourceFolder, destinationFolder, fileType):
|
|
if fileType in self.settings:
|
|
for file in self.filterFiles(self.settings[fileType]):
|
|
src = self.absolutePathForSourceFile(fileType, file)
|
|
dst = self.absolutePathForTargetFile(destinationFolder, fileType, file)
|
|
main.createFolder(os.path.dirname(dst))
|
|
shutil.copy2(src, dst)
|
|
else:
|
|
srcFolder = os.path.join(self.absolutePathForSources(), fileType)
|
|
dstFolder = os.path.join(destinationFolder, self.module, fileType)
|
|
if not(os.path.exists(dstFolder)):
|
|
shutil.copytree(srcFolder, dstFolder)
|
|
|
|
# try:
|
|
# shutil.copytree(srcFolder, dstFolder)
|
|
# except:
|
|
# pass
|
|
|
|
|
|
|
|
# def copyResourcesToFolder (self, targetFolder, backendSettings):
|
|
# for resoureceType in self.projectResourceTypes():
|
|
# self.copyResources(self.projectDir, targetFolder, resoureceType)
|
|
# self.copyStaticResources(targetFolder)
|
|
|
|
def copyDebugResourcesToFolder (self, targetFolder):
|
|
for resoureceType in self.projectResourceTypes():
|
|
self.copyResources(self.projectDir, targetFolder, resoureceType)
|
|
|
|
|
|
def loadIndividualFilesContent (self, basePath, files):
|
|
result = {}
|
|
|
|
for file in self.filterFiles(files):
|
|
try:
|
|
fileHandler = codecs.open(self.absolutePathForSourceFile(basePath, file), 'r', 'utf-8')
|
|
except:
|
|
print "FILE: " + file
|
|
|
|
result[file] = fileHandler.read()
|
|
fileHandler.close()
|
|
|
|
return result
|
|
|
|
|
|
def loadFilesContent (self, basePath, files):
|
|
result = ""
|
|
|
|
fileContent = self.loadIndividualFilesContent(basePath, files)
|
|
for file in self.filterFiles(files):
|
|
result += fileContent[file] + '\n'
|
|
|
|
return result
|
|
|
|
|
|
# def packFilesContent (self, filesContent):
|
|
# result = ""
|
|
#
|
|
# for name, content in filesContent:
|
|
# result += content + '\n'
|
|
#
|
|
# return result
|
|
|
|
|
|
def template (self):
|
|
processedFile = 'html_template'
|
|
if not self.processedFiles.has_key(processedFile):
|
|
# self.processedFiles[processedFile] = self.loadFilesContent('html', ['index_template.html'])
|
|
self.processedFiles[processedFile] = self.loadFilesContent('html', [self.settings['html.template']])
|
|
|
|
return self.processedFiles[processedFile]
|
|
|
|
|
|
#==========================================================================
|
|
|
|
def cssminCompressor (self, css):
|
|
# package found here:
|
|
# - http://stackoverflow.com/questions/222581/python-script-for-minifying-css/2396777#2396777
|
|
# actual downloaded version: http://pypi.python.org/pypi/cssmin/0.1.4 -> 0.2.0
|
|
return cssmin.cssmin(css)
|
|
|
|
|
|
def regexCssCompressor (self, css):
|
|
# http://stackoverflow.com/questions/222581/python-script-for-minifying-css/223689#223689
|
|
|
|
# remove comments - this will break a lot of hacks :-P
|
|
css = re.sub( r'\s*/\*\s*\*/', "$$HACK1$$", css ) # preserve IE<6 comment hack
|
|
css = re.sub( r'/\*[\s\S]*?\*/', "", css )
|
|
css = css.replace( "$$HACK1$$", '/**/' ) # preserve IE<6 comment hack
|
|
|
|
# url() doesn't need quotes
|
|
css = re.sub( r'url\((["\'])([^)]*)\1\)', r'url(\2)', css )
|
|
|
|
# spaces may be safely collapsed as generated content will collapse them anyway
|
|
css = re.sub( r'\s+', ' ', css )
|
|
|
|
# shorten collapsable colors: #aabbcc to #abc
|
|
css = re.sub( r'#([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3(\s|;)', r'#\1\2\3\4', css )
|
|
|
|
# fragment values can loose zeros
|
|
css = re.sub( r':\s*0(\.\d+([cm]m|e[mx]|in|p[ctx]))\s*;', r':\1;', css )
|
|
|
|
for rule in re.findall( r'([^{]+){([^}]*)}', css ):
|
|
|
|
# we don't need spaces around operators
|
|
selectors = [re.sub( r'(?<=[\[\(>+=])\s+|\s+(?=[=~^$*|>+\]\)])', r'', selector.strip() ) for selector in rule[0].split( ',' )]
|
|
|
|
# order is important, but we still want to discard repetitions
|
|
properties = {}
|
|
porder = []
|
|
for prop in re.findall( '(.*?):(.*?)(;|$)', rule[1] ):
|
|
key = prop[0].strip().lower()
|
|
if key not in porder: porder.append( key )
|
|
properties[ key ] = prop[1].strip()
|
|
|
|
# output rule if it contains any declarations
|
|
if properties:
|
|
print "%s{%s}" % ( ','.join( selectors ), ''.join(['%s:%s;' % (key, properties[key]) for key in porder])[:-1] )
|
|
|
|
return css
|
|
|
|
|
|
def mockCssCompressor (self, css):
|
|
return css;
|
|
|
|
|
|
def compressCSS (self, css):
|
|
self.log("compressing CSS")
|
|
#return self.regexCssCompressor(css)
|
|
#return self.cssminCompressor(css)
|
|
return self.mockCssCompressor(css)
|
|
|
|
#--------------------------------------------------------------------------
|
|
|
|
#==========================================================================
|
|
|
|
def compressJS_jsmin (self, js, description):
|
|
self.log("compressing " + description + " code")
|
|
original = StringIO.StringIO(js)
|
|
output = StringIO.StringIO()
|
|
|
|
jsMinifier = jsmin.JavascriptMinify()
|
|
jsMinifier.minify(original, output)
|
|
|
|
result = output.getvalue()
|
|
|
|
original.close()
|
|
output.close()
|
|
|
|
return result
|
|
|
|
def compressJS_closureCompiler (self, js, description):
|
|
# Googles Closure compiler
|
|
# java -jar compiler.jar --js=in1.js --js=in2.js ... --js_output_file=out.js
|
|
|
|
result = js
|
|
|
|
return result
|
|
|
|
|
|
def compressJS (self, js, description):
|
|
return self.compressJS_jsmin(js, description)
|
|
#return self.compressJS_closureCompiler(js, description)
|
|
|
|
|
|
#==========================================================================
|
|
|
|
def packBookmarklet (self, bookmakeletCode, version):
|
|
replacers = [
|
|
('isLoginForm', 'ilf'),
|
|
('findLoginForm', 'flf'),
|
|
('findLoginForm', 'flf'),
|
|
('formParameters', 'fp' ),
|
|
('pageParameters', 'pp' ),
|
|
('serializeJSON', 'sj' ),
|
|
('reprString', 'rs' ),
|
|
('logFormParameters', 'lfp'),
|
|
('loadClipperzBookmarklet', 'lcb'),
|
|
('loginForm', 'lf' ),
|
|
('parameters', 'p' ),
|
|
('inputElementValues', 'iev'),
|
|
]
|
|
result = self.compressJS(bookmakeletCode, version + " bookmarklet")
|
|
|
|
result = re.sub('\n', ' ', result) # Fit all in a single line
|
|
# result = re.sub('\s+', ' ', result) # Collapse "redundant" spaces. WARNING: this could have some evil side effects on constant strings used inside to code!!
|
|
# result = re.sub('\s?([,\+=\(\)\{\};])\s?', '\\1', result)
|
|
|
|
for replacer in replacers:
|
|
result = re.sub(replacer[0], replacer[1], result)
|
|
|
|
# <!-- escaping required to handle the bookmarklet code within the javascript code -->
|
|
result = re.sub('\://', '%3a%2f%2f', result)
|
|
result = re.sub('/', '%2f', result)
|
|
# result = re.sub('"', '%22', result)
|
|
result = re.sub('"', '\\"', result)
|
|
result = re.sub('\"', '%22', result)
|
|
result = re.sub('\'', '%22', result)
|
|
result = re.sub('\\\\', '%5c', result)
|
|
result = result.strip()
|
|
result = 'javascript:' + result
|
|
|
|
return result
|
|
|
|
|
|
def bookmarklet (self):
|
|
cacheKey = 'bookmarklet'
|
|
if not self.processedFiles.has_key(cacheKey):
|
|
result = 'bookmarklet="' + self.packBookmarklet(self.loadFilesContent('js', ['Bookmarklet.js']), "regular") + '";bookmarklet_ie="' + self.packBookmarklet(self.loadFilesContent('js', ['Bookmarklet_IE.js']), "IE") + '";'
|
|
self.processedFiles[cacheKey] = result
|
|
else:
|
|
result = self.processedFiles[cacheKey]
|
|
|
|
return result
|
|
|
|
|
|
#==========================================================================
|
|
|
|
def replaceTemplatePlaceholders (self, pageTitle, copyright, css, code, jsLoadMode, version, versionType):
|
|
result = self.template()
|
|
|
|
result = result.replace('@page.title@', pageTitle)
|
|
result = result.replace('@copyright@', copyright)
|
|
result = result.replace('@css@', css)
|
|
#result = result.replace('@bookmarklet@', bookmarklet)
|
|
result = result.replace('@application.version@', version)
|
|
result = result.replace('@application.version.type@', versionType)
|
|
result = result.replace('@js_' + jsLoadMode + '@', code)
|
|
|
|
result = re.sub('@js_[^@]+@', '', result)
|
|
|
|
return result
|
|
|
|
|
|
def assembleCopyrightHeader (self):
|
|
processedFile = 'copyright'
|
|
if not self.processedFiles.has_key(processedFile):
|
|
#self.log("assembling copyright header")
|
|
copyrightValues = self.settings['copyright.values']
|
|
license = self.loadFilesContent('../../properties', ['license.txt'])
|
|
result = self.loadFilesContent('properties', ['creditsAndCopyrights.txt'])
|
|
|
|
result = re.sub('@clipperz.license@', license, result)
|
|
for key in copyrightValues:
|
|
result = re.sub('@'+key+'@', copyrightValues[key], result)
|
|
|
|
self.processedFiles[processedFile] = result
|
|
|
|
return self.processedFiles[processedFile]
|
|
|
|
|
|
def cssTagsForFiles (self, basePath, files):
|
|
#<link rel="stylesheet" type="text/css" href="./css/reset-min.css" />
|
|
return '\n'.join(map(lambda file: '<link rel="stylesheet" type="text/css" href="' + basePath + '/' + file + '" />', files))
|
|
|
|
|
|
def cssTagForContent (self, content):
|
|
return '<style type="text/css">' + content + '</style>'
|
|
|
|
|
|
def scriptTagsForFiles (self, basePath, files):
|
|
#<script type='text/javascript' src='./js/src/bookmarklet.js'></script>
|
|
return '\n'.join(map(lambda file: '<script type="text/javascript" src="' + basePath + '/' + file + '" charset="utf-8"></script>', files))
|
|
|
|
|
|
def scriptTagForContent (self, content):
|
|
return '<script>' + content + '</script>'
|
|
|
|
|
|
def assembleVersion (self, pageTitle, copyright, css, js, jsLoadMode, version, versionType):
|
|
cacheKey = version + "-" + versionType
|
|
if not self.processedFiles.has_key(cacheKey):
|
|
result = self.replaceTemplatePlaceholders(pageTitle, copyright, css, js, jsLoadMode, version, versionType)
|
|
self.processedFiles[cacheKey] = result
|
|
else:
|
|
result = self.processedFiles[cacheKey]
|
|
|
|
#self.log("# cacheKey:\n" + result)
|
|
return result
|
|
|
|
|
|
def assemble (self, assemblyMode='INSTALL', versionType='LIVE'):
|
|
|
|
if versionType == 'LIVE':
|
|
pageTitle = "Clipperz - " + self.module
|
|
else:
|
|
pageTitle = "Clipperz - " + self.module + " [" + versionType + " - " + assemblyMode +"]"
|
|
|
|
for cssFile in self.settings['css']:
|
|
# self.preprocessCSS(self.absolutePathForSourceFile('css', cssFile))
|
|
pass
|
|
|
|
if assemblyMode == 'INSTALL':
|
|
copyright = self.assembleCopyrightHeader()
|
|
css = self.cssTagForContent(self.compressCSS(self.loadFilesContent('css', self.settings['css'])))
|
|
js = self.scriptTagForContent(
|
|
self.bookmarklet() +
|
|
'\n' +
|
|
self.compressJS(self.loadFilesContent('js', self.settings['js']), "application")
|
|
)
|
|
jsLoadMode = 'EMBEDDED'
|
|
|
|
elif assemblyMode == 'DEBUG':
|
|
copyright = self.assembleCopyrightHeader()
|
|
css = self.cssTagsForFiles('./css', self.filterFiles(self.settings['css']))
|
|
js = self.scriptTagForContent(
|
|
self.bookmarklet()) + \
|
|
'\n' + \
|
|
self.scriptTagsForFiles('./js', self.filterFiles(self.settings['js'])
|
|
)
|
|
jsLoadMode = 'LINKED'
|
|
|
|
elif assemblyMode == 'DEVELOPMENT':
|
|
copyright = ""
|
|
css = self.cssTagsForFiles('file://' + str(os.path.join(self.absolutePathForSources(), 'css')), self.filterFiles(self.settings['css']))
|
|
js = self.scriptTagForContent(
|
|
self.bookmarklet()) + \
|
|
'\n' + \
|
|
self.scriptTagsForFiles('file://' + str(os.path.join(self.absolutePathForSources(), 'js')), self.filterFiles(self.settings['js'])
|
|
)
|
|
jsLoadMode = 'LINKED'
|
|
versionType = 'development'
|
|
|
|
else:
|
|
raise NotImplementedError()
|
|
|
|
return self.assembleVersion(
|
|
pageTitle = pageTitle,
|
|
copyright = copyright,
|
|
css = css,
|
|
js = js,
|
|
jsLoadMode = jsLoadMode,
|
|
version = self.repositoryVersion,
|
|
versionType = versionType
|
|
)
|
|
|
|
|
|
|
|
|