More trip planner testing with colors
[busui.git] / labs / openlayers / tools / mergejs.py
blob:a/labs/openlayers/tools/mergejs.py -> blob:b/labs/openlayers/tools/mergejs.py
  #!/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 <file path>
  #
  # e.g.
  #
  # // @requires Geo/DataSource.js
  #
  # This script should be executed like so:
  #
  # mergejs.py <output.js> <directory> [...]
  #
  # 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 <config file>] <output.js> <directory> [...]" % 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 )