--- a/labs/openlayers/tools/mergejs.py +++ b/labs/openlayers/tools/mergejs.py @@ -1,1 +1,253 @@ - +#!/usr/bin/env python +# +# Merge multiple JavaScript source code files into one. +# +# Usage: +# This script requires source files to have dependencies specified in them. +# +# Dependencies are specified with a comment of the form: +# +# // @requires +# +# e.g. +# +# // @requires Geo/DataSource.js +# +# This script should be executed like so: +# +# mergejs.py [...] +# +# e.g. +# +# mergejs.py openlayers.js Geo/ CrossBrowser/ +# +# This example will cause the script to walk the `Geo` and +# `CrossBrowser` directories--and subdirectories thereof--and import +# all `*.js` files encountered. The dependency declarations will be extracted +# and then the source code from imported files will be output to +# a file named `openlayers.js` in an order which fulfils the dependencies +# specified. +# +# +# Note: This is a very rough initial version of this code. +# +# -- Copyright 2005-2010 OpenLayers contributors / OpenLayers project -- +# + +# TODO: Allow files to be excluded. e.g. `Crossbrowser/DebugMode.js`? +# TODO: Report error when dependency can not be found rather than KeyError. + +import re +import os +import sys + +SUFFIX_JAVASCRIPT = ".js" + +RE_REQUIRE = "@requires:? (.*)\n" # TODO: Ensure in comment? +class SourceFile: + """ + Represents a Javascript source code file. + """ + + def __init__(self, filepath, source): + """ + """ + self.filepath = filepath + self.source = source + + self.requiredBy = [] + + + def _getRequirements(self): + """ + Extracts the dependencies specified in the source code and returns + a list of them. + """ + # TODO: Cache? + return re.findall(RE_REQUIRE, self.source) + + requires = property(fget=_getRequirements, doc="") + + + +def usage(filename): + """ + Displays a usage message. + """ + print "%s [-c ] [...]" % filename + + +class Config: + """ + Represents a parsed configuration file. + + A configuration file should be of the following form: + + [first] + 3rd/prototype.js + core/application.js + core/params.js + # A comment + + [last] + core/api.js # Another comment + + [exclude] + 3rd/logger.js + + All headings are required. + + The files listed in the `first` section will be forced to load + *before* all other files (in the order listed). The files in `last` + section will be forced to load *after* all the other files (in the + order listed). + + The files list in the `exclude` section will not be imported. + + Any text appearing after a # symbol indicates a comment. + + """ + + def __init__(self, filename): + """ + Parses the content of the named file and stores the values. + """ + lines = [re.sub("#.*?$", "", line).strip() # Assumes end-of-line character is present + for line in open(filename) + if line.strip() and not line.strip().startswith("#")] # Skip blank lines and comments + + self.forceFirst = lines[lines.index("[first]") + 1:lines.index("[last]")] + + self.forceLast = lines[lines.index("[last]") + 1:lines.index("[include]")] + self.include = lines[lines.index("[include]") + 1:lines.index("[exclude]")] + self.exclude = lines[lines.index("[exclude]") + 1:] + +def run (sourceDirectory, outputFilename = None, configFile = None): + cfg = None + if configFile: + cfg = Config(configFile) + + allFiles = [] + + ## Find all the Javascript source files + for root, dirs, files in os.walk(sourceDirectory): + for filename in files: + if filename.endswith(SUFFIX_JAVASCRIPT) and not filename.startswith("."): + filepath = os.path.join(root, filename)[len(sourceDirectory)+1:] + filepath = filepath.replace("\\", "/") + if cfg and cfg.include: + if filepath in cfg.include or filepath in cfg.forceFirst: + allFiles.append(filepath) + elif (not cfg) or (filepath not in cfg.exclude): + allFiles.append(filepath) + + ## Header inserted at the start of each file in the output + HEADER = "/* " + "=" * 70 + "\n %s\n" + " " + "=" * 70 + " */\n\n" + + files = {} + + order = [] # List of filepaths to output, in a dependency satisfying order + + ## Import file source code + ## TODO: Do import when we walk the directories above? + for filepath in allFiles: + print "Importing: %s" % filepath + fullpath = os.path.join(sourceDirectory, filepath).strip() + content = open(fullpath, "U").read() # TODO: Ensure end of line @ EOF? + files[filepath] = SourceFile(filepath, content) # TODO: Chop path? + + print + + from toposort import toposort + + complete = False + resolution_pass = 1 + + while not complete: + order = [] # List of filepaths to output, in a dependency satisfying order + nodes = [] + routes = [] + ## Resolve the dependencies + print "Resolution pass %s... " % resolution_pass + resolution_pass += 1 + + for filepath, info in files.items(): + nodes.append(filepath) + for neededFilePath in info.requires: + routes.append((neededFilePath, filepath)) + + for dependencyLevel in toposort(nodes, routes): + for filepath in dependencyLevel: + order.append(filepath) + if not files.has_key(filepath): + print "Importing: %s" % filepath + fullpath = os.path.join(sourceDirectory, filepath).strip() + content = open(fullpath, "U").read() # TODO: Ensure end of line @ EOF? + files[filepath] = SourceFile(filepath, content) # TODO: Chop path? + + + + # Double check all dependencies have been met + complete = True + try: + for fp in order: + if max([order.index(rfp) for rfp in files[fp].requires] + + [order.index(fp)]) != order.index(fp): + complete = False + except: + complete = False + + print + + + ## Move forced first and last files to the required position + if cfg: + print "Re-ordering files..." + order = cfg.forceFirst + [item + for item in order + if ((item not in cfg.forceFirst) and + (item not in cfg.forceLast))] + cfg.forceLast + + print + ## Output the files in the determined order + result = [] + + for fp in order: + f = files[fp] + print "Exporting: ", f.filepath + result.append(HEADER % f.filepath) + source = f.source + result.append(source) + if not source.endswith("\n"): + result.append("\n") + + print "\nTotal files merged: %d " % len(files) + + if outputFilename: + print "\nGenerating: %s" % (outputFilename) + open(outputFilename, "w").write("".join(result)) + return "".join(result) + +if __name__ == "__main__": + import getopt + + options, args = getopt.getopt(sys.argv[1:], "-c:") + + try: + outputFilename = args[0] + except IndexError: + usage(sys.argv[0]) + raise SystemExit + else: + sourceDirectory = args[1] + if not sourceDirectory: + usage(sys.argv[0]) + raise SystemExit + + configFile = None + if options and options[0][0] == "-c": + configFile = options[0][1] + print "Parsing configuration file: %s" % filename + + run( sourceDirectory, outputFilename, configFile ) +