Source

Structris / AsbFilter.py

Full commit
#!/usr/bin/env python

import re, sys

referencedLines = set()
firstRefdLine = None

##############################################################################
class TransformError(Exception):
  """ Error raised if we encounter a circumstance that can't be recovered from. """
  def __init__(self, message):
    Exception.__init__(self, message)
    

##############################################################################
class BasicLine:
  """ Holds information about one line of the BASIC program. """
  
  def __init__(self, lineCount, origText):
    (self.lineCount, self.origText) = (lineCount, origText)
    
    # Figure out the indent level, line number, and text of the statements
    m = re.match("^(\s*)(\d*)(.*)$", origText)
    self.indent = m.group(1)
    self.lineNum = int(m.group(2)) if m.group(2) else ""
    stmtsText = m.group(3)
    self.pieces = []
    
    # Break up multiple statements on the line into a list. Handle strings
    # and REM statements correctly. Put separators into their own pieces.
    i = s = 0
    inString = False
    while i < len(stmtsText):
      c = stmtsText[i]
      if c == '"':
        inString = not inString
        i += 1
      elif inString:
        i += 1
      elif re.match("^\s*:\s*", stmtsText[i:]):
        self.pieces.append(stmtsText[s:i])
        m = re.match("^\s*:\s*", stmtsText[i:])
        self.pieces.append(m.group(0))
        s = i = i + len(m.group(0))
      elif i == s and re.match("^\s+", stmtsText[i:]):
        m = re.match("^\s+", stmtsText[i:])
        self.pieces.append(m.group(0))
        s = i = i + len(m.group(0))
      elif re.match("^REM.*", stmtsText[i:], re.IGNORECASE):
        i = len(stmtsText)
      else:
        i += 1
        
    # Finish up.
    self.pieces.append(stmtsText[s:i])
   
  def isNoOp(self):
    if not self.stmts:
      return True
    if len(self.stmts) > 1:
      return False
    return self.stmts[0].strip().upper().startswith("REM")
  
  def __str__(self):
    return self.indent + str(self.lineNum) + "".join(self.pieces)


##############################################################################
def transformForEditing(lines):
  """ Perform transformations to make the BASIC program easy to edit. This
      makes it un-pasteable however, and it needs to be transformed back
      before pasting. """
      
  global firstRefdLine
  
  # Identify every line number that is referenced
  for line in lines:
    for piece in line.pieces:
      m = re.match(".*(THEN|GOTO|GOSUB)(\s*\d+.*)$", piece, re.IGNORECASE)
      if not m: continue
      for lineNum in re.findall("\d+", m.group(2)):
        lineNum = int(lineNum)
        referencedLines.add(lineNum)
        if not firstRefdLine or lineNum < firstRefdLine:
          firstRefdLine = lineNum
  
  # Remove line numbers that aren't referenced
  for line in lines:
    if line.lineNum > firstRefdLine and not line.lineNum in referencedLines:
      line.lineNum = ""
      if line.pieces and line.pieces[0].strip() == "":
        del line.pieces[0]
  
  # Add a line so we can tell the transformation needs to be reversed.
  if lines[0].origText.strip().upper() == "NEW":
    lines.pop(0)
  lines.insert(0, BasicLine(-1, "*** Transformed for editing ***"))
  return lines


##############################################################################
def abortTransform(message):
  """ Raises an exception that aborts the transformation. """
  
  raise TransformError(message)
  
      
##############################################################################
def flushBlock(block, startLine, endLine, out):
  """ Auto-number a block of lines with the given start and end constraints. """
  
  numLinesNeeded = len([l for l in block if not l.isNoOp()])
  if numLinesNeeded == 0:
    out.extend(block)
    return
    
  s = startLine if startLine else 0
  e = endLine if endLine else s + 10*numLinesNeeded
    
  incr = (e - s) / numLinesNeeded
  if incr >= 10:
    incr = 10
  elif incr >= 5:
    incr = 5
  elif incr >= 2:
    incr = 2
  elif incr >= 1:
    incr = 1
  else:
    abortTransform("Cannot auto-number block starting at line %d" % startLine)
    
  cur = s
  for line in block:
    if not line.isNoOp():
      line.lineNum = cur
      cur += incr
    out.append(line)
    
      
##############################################################################
def transformForApple(lines):
  """ Un-do the editing transformations, so the program can be pasted into
      an Apple II emulator. """
      
  # Gather blocks of lines that need to be numbered
  out = []
  startLine = 0
  block = []
  for line in lines:
    if "Transformed for editing" in line.origText:
      out.append(line)
    elif line.lineNum != "":
      flushBlock(block, startLine, line.lineNum, out)
      block = [line]
      startLine = line.lineNum
    else:
      block.append(line)
  flushBlock(block, startLine, None, out)
  return out
      
      
##############################################################################
def main():

  # Read in the file.
  origLines = [line.rstrip() for line in open(sys.argv[1], "r")]
  
  # Parse each line
  lines = []
  for i in range(len(origLines)):
    lines.append(BasicLine(i, origLines[i]))
  
  # Transform either for editing or for Apple
  try:
    if [ln for ln in lines if "Transformed for editing" in ln.origText]:
      lines = transformForApple(lines)
    else:
      lines = transformForEditing(lines)
      
    # All done.
    for line in lines:
      print line
      
  except TransformError, e:
    print "*** Transformation error: " + str(e) + "***"
    for line in origLines:
      print line
      

##############################################################################
main()