ed1c8081a5
Still very early draft with tons of issues still to address, and no styling at all. Disabled CSS preprocessor, due to limitations in Python SCSS processor. Updated date Copyright notice.
426 lines
13 KiB
Python
Executable File
426 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):
|
|
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):
|
|
# 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 = ""
|
|
|
|
# for file in self.filterFiles(files):
|
|
# try:
|
|
# fileHandler = codecs.open(self.absolutePathForSourceFile(basePath, file), 'r', 'utf-8')
|
|
# except:
|
|
# print "FILE: " + file
|
|
#
|
|
# result += fileHandler.read() + '\n'
|
|
# fileHandler.close()
|
|
|
|
# for name, content in self.loadIndividualFilesContent(basePath, files):
|
|
for name, content in list(self.loadIndividualFilesContent(basePath, files).items()):
|
|
result += content + '\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
|
|
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 compressCSS (self, css):
|
|
self.log("compressing CSS")
|
|
#return self.regexCssCompressor(css)
|
|
return self.cssminCompressor(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
|
|
)
|
|
|
|
|
|
|
|
|