Quantcast
-- For All Indents And Purposes
local revision = 18
-- Maintainer: kristofer.karlsson@gmail.com

-- For All Indents And Purposes -
-- a indentation + syntax highlighting library
-- All valid lua code should be processed correctly.

-- Usage (for developers)
--------
-- Variant 1: - non embedded
-- 1) Add ForAllIndentsAndPurposes to your dependencies (or optional dependencies)

-- Variant 2: - embedded
-- 1.a) Copy indent.lua to your addon directory
-- 1.b) Put indent.lua first in your list of files in the TOC

-- For both variants:
-- 2) hook the editboxes that you want to have indentation like this:
-- IndentationLib.enable(editbox [, colorTable [, tabWidth] ])
-- if you don't select a color table, it will use the default.
-- Read through this code for further usage help.
-- (The documentation IS the code)

if not IndentationLib then
   IndentationLib = {}
end

if not IndentationLib.revision or revision > IndentationLib.revision then
   local lib = IndentationLib
   lib.revision = revision

   local stringlen = string.len
   local stringformat = string.format
   local stringfind = string.find
   local stringsub = string.sub
   local stringbyte = string.byte
   local stringchar = string.char
   local stringrep = string.rep
   local stringgsub = string.gsub

   local workingTable = {}
   local workingTable2 = {}
   local function tableclear(t)
      for k in next,t do
	 t[k] = nil
      end
   end

   local function stringinsert(s, pos, insertStr)
      return stringsub(s, 1, pos) .. insertStr .. stringsub(s, pos + 1)
   end
   lib.stringinsert = stringinsert

   local function stringdelete(s, pos1, pos2)
      return stringsub(s, 1, pos1 - 1) .. stringsub(s, pos2 + 1)
   end
   lib.stringdelete = stringdelete

   -- token types
   local tokens = {}
   lib.tokens = tokens

   tokens.TOKEN_UNKNOWN = 0
   tokens.TOKEN_NUMBER = 1
   tokens.TOKEN_LINEBREAK = 2
   tokens.TOKEN_WHITESPACE = 3
   tokens.TOKEN_IDENTIFIER = 4
   tokens.TOKEN_ASSIGNMENT = 5
   tokens.TOKEN_EQUALITY = 6
   tokens.TOKEN_MINUS = 7
   tokens.TOKEN_COMMENT_SHORT = 8
   tokens.TOKEN_COMMENT_LONG = 9
   tokens.TOKEN_STRING = 10
   tokens.TOKEN_LEFTBRACKET = 11
   tokens.TOKEN_PERIOD = 12
   tokens.TOKEN_DOUBLEPERIOD = 13
   tokens.TOKEN_TRIPLEPERIOD = 14
   tokens.TOKEN_LTE = 15
   tokens.TOKEN_LT = 16
   tokens.TOKEN_GTE = 17
   tokens.TOKEN_GT = 18
   tokens.TOKEN_NOTEQUAL = 19
   tokens.TOKEN_COMMA = 20
   tokens.TOKEN_SEMICOLON = 21
   tokens.TOKEN_COLON = 22
   tokens.TOKEN_LEFTPAREN = 23
   tokens.TOKEN_RIGHTPAREN = 24
   tokens.TOKEN_PLUS = 25
   tokens.TOKEN_SLASH = 27
   tokens.TOKEN_LEFTWING = 28
   tokens.TOKEN_RIGHTWING = 29
   tokens.TOKEN_CIRCUMFLEX = 30
   tokens.TOKEN_ASTERISK = 31
   tokens.TOKEN_RIGHTBRACKET = 32
   tokens.TOKEN_KEYWORD = 33
   tokens.TOKEN_SPECIAL = 34
   tokens.TOKEN_VERTICAL = 35
   tokens.TOKEN_TILDE = 36

   -- WoW specific tokens
   tokens.TOKEN_COLORCODE_START = 37
   tokens.TOKEN_COLORCODE_STOP = 38

-- new as of lua 5.1
   tokens.TOKEN_HASH = 39
   tokens.TOKEN_PERCENT = 40


   -- ascii codes
   local bytes = {}
   lib.bytes = bytes
   bytes.BYTE_LINEBREAK_UNIX = stringbyte("\n")
   bytes.BYTE_LINEBREAK_MAC = stringbyte("\r")
   bytes.BYTE_SINGLE_QUOTE = stringbyte("'")
   bytes.BYTE_DOUBLE_QUOTE = stringbyte('"')
   bytes.BYTE_0 = stringbyte("0")
   bytes.BYTE_9 = stringbyte("9")
   bytes.BYTE_PERIOD = stringbyte(".")
   bytes.BYTE_SPACE = stringbyte(" ")
   bytes.BYTE_TAB = stringbyte("\t")
   bytes.BYTE_E = stringbyte("E")
   bytes.BYTE_e = stringbyte("e")
   bytes.BYTE_MINUS = stringbyte("-")
   bytes.BYTE_EQUALS = stringbyte("=")
   bytes.BYTE_LEFTBRACKET = stringbyte("[")
   bytes.BYTE_RIGHTBRACKET = stringbyte("]")
   bytes.BYTE_BACKSLASH = stringbyte("\\")
   bytes.BYTE_COMMA = stringbyte(",")
   bytes.BYTE_SEMICOLON = stringbyte(";")
   bytes.BYTE_COLON = stringbyte(":")
   bytes.BYTE_LEFTPAREN = stringbyte("(")
   bytes.BYTE_RIGHTPAREN = stringbyte(")")
   bytes.BYTE_TILDE = stringbyte("~")
   bytes.BYTE_PLUS = stringbyte("+")
   bytes.BYTE_SLASH = stringbyte("/")
   bytes.BYTE_LEFTWING = stringbyte("{")
   bytes.BYTE_RIGHTWING = stringbyte("}")
   bytes.BYTE_CIRCUMFLEX = stringbyte("^")
   bytes.BYTE_ASTERISK = stringbyte("*")
   bytes.BYTE_LESSTHAN = stringbyte("<")
   bytes.BYTE_GREATERTHAN = stringbyte(">")
   -- WoW specific chars
   bytes.BYTE_VERTICAL = stringbyte("|")
   bytes.BYTE_r = stringbyte("r")
   bytes.BYTE_c = stringbyte("c")

-- new as of lua 5.1
   bytes.BYTE_HASH = stringbyte("#")
   bytes.BYTE_PERCENT = stringbyte("%")


   local linebreakCharacters = {}
   lib.linebreakCharacters = linebreakCharacters
   linebreakCharacters[bytes.BYTE_LINEBREAK_UNIX] = 1
   linebreakCharacters[bytes.BYTE_LINEBREAK_MAC] = 1

   local whitespaceCharacters = {}
   lib.whitespaceCharacters = whitespaceCharacters
   whitespaceCharacters[bytes.BYTE_SPACE] = 1
   whitespaceCharacters[bytes.BYTE_TAB] = 1

   local specialCharacters = {}
   lib.specialCharacters = specialCharacters
   specialCharacters[bytes.BYTE_PERIOD] = -1
   specialCharacters[bytes.BYTE_LESSTHAN] = -1
   specialCharacters[bytes.BYTE_GREATERTHAN] = -1
   specialCharacters[bytes.BYTE_LEFTBRACKET] = -1
   specialCharacters[bytes.BYTE_EQUALS] = -1
   specialCharacters[bytes.BYTE_MINUS] = -1
   specialCharacters[bytes.BYTE_SINGLE_QUOTE] = -1
   specialCharacters[bytes.BYTE_DOUBLE_QUOTE] = -1
   specialCharacters[bytes.BYTE_TILDE] = -1
   specialCharacters[bytes.BYTE_RIGHTBRACKET] = tokens.TOKEN_RIGHTBRACKET
   specialCharacters[bytes.BYTE_COMMA] = tokens.TOKEN_COMMA
   specialCharacters[bytes.BYTE_COLON] = tokens.TOKEN_COLON
   specialCharacters[bytes.BYTE_SEMICOLON] = tokens.TOKEN_SEMICOLON
   specialCharacters[bytes.BYTE_LEFTPAREN] = tokens.TOKEN_LEFTPAREN
   specialCharacters[bytes.BYTE_RIGHTPAREN] = tokens.TOKEN_RIGHTPAREN
   specialCharacters[bytes.BYTE_PLUS] = tokens.TOKEN_PLUS
   specialCharacters[bytes.BYTE_SLASH] = tokens.TOKEN_SLASH
   specialCharacters[bytes.BYTE_LEFTWING] = tokens.TOKEN_LEFTWING
   specialCharacters[bytes.BYTE_RIGHTWING] = tokens.TOKEN_RIGHTWING
   specialCharacters[bytes.BYTE_CIRCUMFLEX] = tokens.TOKEN_CIRCUMFLEX
   specialCharacters[bytes.BYTE_ASTERISK] = tokens.TOKEN_ASTERISK
   -- WoW specific
   specialCharacters[bytes.BYTE_VERTICAL] = -1
-- new as of lua 5.1
   specialCharacters[bytes.BYTE_HASH] = tokens.TOKEN_HASH
   specialCharacters[bytes.BYTE_PERCENT] = tokens.TOKEN_PERCENT

   local function nextNumberExponentPartInt(text, pos)
      while true do
	 local byte = stringbyte(text, pos)
	 if not byte then
	    return tokens.TOKEN_NUMBER, pos
	 end

	 if byte >= bytes.BYTE_0 and byte <= bytes.BYTE_9 then
	    pos = pos + 1
	 else
	    return tokens.TOKEN_NUMBER, pos
	 end
      end
   end

   local function nextNumberExponentPart(text, pos)
      local byte = stringbyte(text, pos)
      if not byte then
	 return tokens.TOKEN_NUMBER, pos
      end

      if byte == bytes.BYTE_MINUS then
	 -- handle this case: a = 1.2e-- some comment
	 -- i decide to let 1.2e be parsed as a a number
	 byte = stringbyte(text, pos + 1)
	 if byte == bytes.BYTE_MINUS then
	    return tokens.TOKEN_NUMBER, pos
	 end
	 return nextNumberExponentPartInt(text, pos + 1)
      end

      return nextNumberExponentPartInt(text, pos)
   end

   local function nextNumberFractionPart(text, pos)
      while true do
	 local byte = stringbyte(text, pos)
	 if not byte then
	    return tokens.TOKEN_NUMBER, pos
	 end

	 if byte >= bytes.BYTE_0 and byte <= bytes.BYTE_9 then
	    pos = pos + 1
	 elseif byte == bytes.BYTE_E or byte == bytes.BYTE_e then
	    return nextNumberExponentPart(text, pos + 1)
	 else
	    return tokens.TOKEN_NUMBER, pos
	 end
      end
   end

   local function nextNumberIntPart(text, pos)
      while true do
	 local byte = stringbyte(text, pos)
	 if not byte then
	    return tokens.TOKEN_NUMBER, pos
	 end

	 if byte >= bytes.BYTE_0 and byte <= bytes.BYTE_9 then
	    pos = pos + 1
	 elseif byte == bytes.BYTE_PERIOD then
	    return nextNumberFractionPart(text, pos + 1)
	 elseif byte == bytes.BYTE_E or byte == bytes.BYTE_e then
	    return nextNumberExponentPart(text, pos + 1)
	 else
	    return tokens.TOKEN_NUMBER, pos
	 end
      end
   end

   local function nextIdentifier(text, pos)
      while true do
	 local byte = stringbyte(text, pos)

	 if not byte or
	    linebreakCharacters[byte] or
	    whitespaceCharacters[byte] or
	    specialCharacters[byte] then
	    return tokens.TOKEN_IDENTIFIER, pos
	 end
	 pos = pos + 1
      end
   end

   -- returns false or: true, nextPos, equalsCount
   local function isBracketStringNext(text, pos)
      local byte = stringbyte(text, pos)
      if byte == bytes.BYTE_LEFTBRACKET then
	 local pos2 = pos + 1
	 byte = stringbyte(text, pos2)
	 while byte == bytes.BYTE_EQUALS do
	    pos2 = pos2 + 1
	    byte = stringbyte(text, pos2)
	 end
	 if byte == bytes.BYTE_LEFTBRACKET then
	    return true, pos2 + 1, (pos2 - 1) - pos
	 else
	    return false
	 end
      else
	 return false
      end
   end


   -- Already parsed the [==[ part when get here
   local function nextBracketString(text, pos, equalsCount)
      local state = 0
      while true do
	 local byte = stringbyte(text, pos)
	 if not byte then
	    return tokens.TOKEN_STRING, pos
	 end

	 if byte == bytes.BYTE_RIGHTBRACKET then
	    if state == 0 then
	       state = 1
	    elseif state == equalsCount + 1 then
	       return tokens.TOKEN_STRING, pos + 1
	    else
	       state = 0
	    end
	 elseif byte == bytes.BYTE_EQUALS then
	    if state > 0 then
	       state = state + 1
	    end
	 else
	    state = 0
	 end
	 pos = pos + 1
      end
   end

   local function nextComment(text, pos)
      -- When we get here we have already parsed the "--"
      -- Check for long comment
      local isBracketString, nextPos, equalsCount = isBracketStringNext(text, pos)
      if isBracketString then
	 local tokenType, nextPos2 = nextBracketString(text, nextPos, equalsCount)
	 return tokens.TOKEN_COMMENT_LONG, nextPos2
      end

      local byte = stringbyte(text, pos)

      -- Short comment, find the first linebreak
      while true do
	 byte = stringbyte(text, pos)
	 if not byte then
	    return tokens.TOKEN_COMMENT_SHORT, pos
	 end
	 if linebreakCharacters[byte] then
	    return tokens.TOKEN_COMMENT_SHORT, pos
	 end
	 pos = pos + 1
      end
   end

   local function nextString(text, pos, character)
      local even = true
      while true do
	 local byte = stringbyte(text, pos)
	 if not byte then
	    return tokens.TOKEN_STRING, pos
	 end

	 if byte == character then
	    if even then
	       return tokens.TOKEN_STRING, pos + 1
	    end
	 end
	 if byte == bytes.BYTE_BACKSLASH then
	    even = not even
	 else
	    even = true
	 end

	 pos = pos + 1
      end
   end

   -- INPUT
   -- 1: text: text to search in
   -- 2: tokenPos:  where to start searching
   -- OUTPUT
   -- 1: token type
   -- 2: position after the last character of the token
   function nextToken(text, pos)
      local byte = stringbyte(text, pos)
      if not byte then
	 return nil
      end

      if linebreakCharacters[byte] then
	 return tokens.TOKEN_LINEBREAK, pos + 1
      end

      if whitespaceCharacters[byte] then
	 while true do
	    pos = pos + 1
	    byte = stringbyte(text, pos)
	    if not byte or not whitespaceCharacters[byte] then
	       return tokens.TOKEN_WHITESPACE, pos
	    end
	 end
      end

      local token = specialCharacters[byte]
      if token then
	 if token ~= -1 then
	    return token, pos + 1
	 end

	 -- WoW specific (for color codes)
	 if byte == bytes.BYTE_VERTICAL then
	    byte = stringbyte(text, pos + 1)
	    if byte == bytes.BYTE_VERTICAL then
	       return tokens.TOKEN_VERTICAL, pos + 2
	    end
	    if byte == bytes.BYTE_c then
	       return tokens.TOKEN_COLORCODE_START, pos + 10
	    end
	    if byte == bytes.BYTE_r then
	       return tokens.TOKEN_COLORCODE_STOP, pos + 2
	    end
	    return tokens.TOKEN_UNKNOWN, pos + 1
	 end

	 if byte == bytes.BYTE_MINUS then
	    byte = stringbyte(text, pos + 1)
	    if byte == bytes.BYTE_MINUS then
	       return nextComment(text, pos + 2)
	    end
	    return tokens.TOKEN_MINUS, pos + 1
	 end

	 if byte == bytes.BYTE_SINGLE_QUOTE then
	    return nextString(text, pos + 1, bytes.BYTE_SINGLE_QUOTE)
	 end

	 if byte == bytes.BYTE_DOUBLE_QUOTE then
	    return nextString(text, pos + 1, bytes.BYTE_DOUBLE_QUOTE)
	 end

	 if byte == bytes.BYTE_LEFTBRACKET then
	    local isBracketString, nextPos, equalsCount = isBracketStringNext(text, pos)
	    if isBracketString then
	       return nextBracketString(text, nextPos, equalsCount)
	    else
	       return tokens.TOKEN_LEFTBRACKET, pos + 1
	    end
	 end

	 if byte == bytes.BYTE_EQUALS then
	    byte = stringbyte(text, pos + 1)
	    if not byte then
	       return tokens.TOKEN_ASSIGNMENT, pos + 1
	    end
	    if byte == bytes.BYTE_EQUALS then
	       return tokens.TOKEN_EQUALITY, pos + 2
	    end
	    return tokens.TOKEN_ASSIGNMENT, pos + 1
	 end

	 if byte == bytes.BYTE_PERIOD then
	    byte = stringbyte(text, pos + 1)
	    if not byte then
	       return tokens.TOKEN_PERIOD, pos + 1
	    end
	    if byte == bytes.BYTE_PERIOD then
	       byte = stringbyte(text, pos + 2)
	       if byte == bytes.BYTE_PERIOD then
		  return tokens.TOKEN_TRIPLEPERIOD, pos + 3
	       end
	       return tokens.TOKEN_DOUBLEPERIOD, pos + 2
	    elseif byte >= bytes.BYTE_0 and byte <= bytes.BYTE_9 then
	       return nextNumberFractionPart(text, pos + 2)
	    end
	    return tokens.TOKEN_PERIOD, pos + 1
	 end

	 if byte == bytes.BYTE_LESSTHAN then
	    byte = stringbyte(text, pos + 1)
	    if byte == bytes.BYTE_EQUALS then
	       return tokens.TOKEN_LTE, pos + 2
	    end
	    return tokens.TOKEN_LT, pos + 1
	 end

	 if byte == bytes.BYTE_GREATERTHAN then
	    byte = stringbyte(text, pos + 1)
	    if byte == bytes.BYTE_EQUALS then
	       return tokens.TOKEN_GTE, pos + 2
	    end
	    return tokens.TOKEN_GT, pos + 1
	 end

	 if byte == bytes.BYTE_TILDE then
	    byte = stringbyte(text, pos + 1)
	    if byte == bytes.BYTE_EQUALS then
	       return tokens.TOKEN_NOTEQUAL, pos + 2
	    end
	    return tokens.TOKEN_TILDE, pos + 1
	 end

	 return tokens.TOKEN_UNKNOWN, pos + 1
      elseif byte >= bytes.BYTE_0 and byte <= bytes.BYTE_9 then
	 return nextNumberIntPart(text, pos + 1)
      else
	 return nextIdentifier(text, pos + 1)
      end
   end

   -- Cool stuff begins here! (indentation and highlighting)

   local noIndentEffect = {0, 0}
   local indentLeft = {-1, 0}
   local indentRight = {0, 1}
   local indentBoth = {-1, 1}

   local keywords = {}
   lib.keywords = keywords
   keywords["and"] = noIndentEffect
   keywords["break"] = noIndentEffect
   keywords["false"] = noIndentEffect
   keywords["for"] = noIndentEffect
   keywords["if"] = noIndentEffect
   keywords["in"] = noIndentEffect
   keywords["local"] = noIndentEffect
   keywords["nil"] = noIndentEffect
   keywords["not"] = noIndentEffect
   keywords["or"] = noIndentEffect
   keywords["return"] = noIndentEffect
   keywords["true"] = noIndentEffect
   keywords["while"] = noIndentEffect

   keywords["until"] = indentLeft
   keywords["elseif"] = indentLeft
   keywords["end"] = indentLeft

   keywords["do"] = indentRight
   keywords["then"] = indentRight
   keywords["repeat"] = indentRight
   keywords["function"] = indentRight

   keywords["else"] = indentBoth

   tokenIndentation = {}
   lib.tokenIndentation = tokenIndentation
   tokenIndentation[tokens.TOKEN_LEFTPAREN] = indentRight
   tokenIndentation[tokens.TOKEN_LEFTBRACKET] = indentRight
   tokenIndentation[tokens.TOKEN_LEFTWING] = indentRight

   tokenIndentation[tokens.TOKEN_RIGHTPAREN] = indentLeft
   tokenIndentation[tokens.TOKEN_RIGHTBRACKET] = indentLeft
   tokenIndentation[tokens.TOKEN_RIGHTWING] = indentLeft

   local function fillWithTabs(n)
      return stringrep("\t", n)
   end

   local function fillWithSpaces(a, b)
      return stringrep(" ", a*b)
   end

   function lib.colorCodeCode(code, colorTable, caretPosition)
      local stopColor = colorTable and colorTable[0]
      if not stopColor then
	 return code, caretPosition
      end

      local stopColorLen = stringlen(stopColor)

      tableclear(workingTable)
      local tsize = 0
      local totalLen = 0

      local numLines = 0
      local newCaretPosition
      local prevTokenWasColored = false
      local prevTokenWidth = 0

      local pos = 1
      local level = 0

      while true do
	 if caretPosition and not newCaretPosition and pos >= caretPosition then
	    if pos == caretPosition then
	       newCaretPosition = totalLen
	    else
	       newCaretPosition = totalLen
	       local diff = pos - caretPosition
	       if diff > prevTokenWidth then
		  diff = prevTokenWidth
	       end
	       if prevTokenWasColored then
		  diff = diff + stopColorLen
	       end
	       newCaretPosition = newCaretPosition - diff
	    end
	 end

	 prevTokenWasColored = false
	 prevTokenWidth = 0

	 local tokenType, nextPos = nextToken(code, pos)

	 if not tokenType then
	    break
	 end

	 if tokenType == tokens.TOKEN_COLORCODE_START or tokenType == tokens.TOKEN_COLORCODE_STOP or tokenType == tokens.TOKEN_UNKNOWN then
	    -- ignore color codes

	 elseif tokenType == tokens.TOKEN_LINEBREAK or tokenType == tokens.TOKEN_WHITESPACE then
	    if tokenType == tokens.TOKEN_LINEBREAK then
	       numLines = numLines + 1
	    end
	    local str = stringsub(code, pos, nextPos - 1)
	    prevTokenWidth = nextPos - pos

	    tsize = tsize + 1
	    workingTable[tsize] = str
	    totalLen = totalLen + stringlen(str)
	 else
	    local str = stringsub(code, pos, nextPos - 1)

	    prevTokenWidth = nextPos - pos

	    -- Add coloring
	    if keywords[str] then
	       tokenType = tokens.TOKEN_KEYWORD
	    end

	    local color
	    if stopColor then
	       color = colorTable[str]
	       if not color then
		  color = colorTable[tokenType]
		  if not color then
		     if tokenType == tokens.TOKEN_IDENTIFIER then
			color = colorTable[tokens.TOKEN_IDENTIFIER]
		     else
			color = colorTable[tokens.TOKEN_SPECIAL]
		     end
		  end
	       end
	    end

	    if color then
	       tsize = tsize + 1
	       workingTable[tsize] = color
	       tsize = tsize + 1
	       workingTable[tsize] = str
	       tsize = tsize + 1
	       workingTable[tsize] = stopColor

	       totalLen = totalLen + stringlen(color) + (nextPos - pos) + stopColorLen
	       prevTokenWasColored = true
	    else
	       tsize = tsize + 1
	       workingTable[tsize] = str

	       totalLen = totalLen + stringlen(str)
	    end
	 end

	 pos = nextPos
      end
      return table.concat(workingTable), newCaretPosition, numLines
   end

   function lib.indentCode(code, tabWidth, colorTable, caretPosition)
      local fillFunction
      if tabWidth == nil then
	 tabWidth = defaultTabWidth
      end
      if tabWidth then
	 fillFunction = fillWithSpaces
      else
	 fillFunction = fillWithTabs
      end

      tableclear(workingTable)
      local tsize = 0
      local totalLen = 0

      tableclear(workingTable2)
      local tsize2 = 0
      local totalLen2 = 0


      local stopColor = colorTable and colorTable[0]
      local stopColorLen = not stopColor or stringlen(stopColor)

      local newCaretPosition
      local newCaretPositionFinalized = false
      local prevTokenWasColored = false
      local prevTokenWidth = 0

      local pos = 1
      local level = 0

      local hitNonWhitespace = false
      local hitIndentRight = false
      local preIndent = 0
      local postIndent = 0
      while true do
	 if caretPosition and not newCaretPosition and pos >= caretPosition then
	    if pos == caretPosition then
	       newCaretPosition = totalLen + totalLen2
	    else
	       newCaretPosition = totalLen + totalLen2
	       local diff = pos - caretPosition
	       if diff > prevTokenWidth then
		  diff = prevTokenWidth
	       end
	       if prevTokenWasColored then
		  diff = diff + stopColorLen
	       end
	       newCaretPosition = newCaretPosition - diff
	    end
	 end

	 prevTokenWasColored = false
	 prevTokenWidth = 0

	 local tokenType, nextPos = nextToken(code, pos)

	 if not tokenType or tokenType == tokens.TOKEN_LINEBREAK then
	    level = level + preIndent
	    if level < 0 then level = 0 end

	    local s = fillFunction(level, tabWidth)

	    tsize = tsize + 1
	    workingTable[tsize] = s
	    totalLen = totalLen + stringlen(s)

	    if newCaretPosition and not newCaretPositionFinalized then
	       newCaretPosition = newCaretPosition + stringlen(s)
	       newCaretPositionFinalized = true
	    end


	    for k, v in next,workingTable2 do
	       tsize = tsize + 1
	       workingTable[tsize] = v
	       totalLen = totalLen + stringlen(v)
	    end

	    if not tokenType then
	       break
	    end

	    tsize = tsize + 1
	    workingTable[tsize] = stringsub(code, pos, nextPos - 1)
	    totalLen = totalLen + nextPos - pos

	    level = level + postIndent
	    if level < 0 then level = 0 end

	    tableclear(workingTable2)
	    tsize2 = 0
	    totalLen2 = 0

	    hitNonWhitespace = false
	    hitIndentRight = false
	    preIndent = 0
	    postIndent = 0
	 elseif tokenType == tokens.TOKEN_WHITESPACE then
	    if hitNonWhitespace then
	       prevTokenWidth = nextPos - pos

	       tsize2 = tsize2 + 1
	       local s = stringsub(code, pos, nextPos - 1)
	       workingTable2[tsize2] = s
	       totalLen2 = totalLen2 + stringlen(s)
	    end
	 elseif tokenType == tokens.TOKEN_COLORCODE_START or tokenType == tokens.TOKEN_COLORCODE_STOP or tokenType == tokens.TOKEN_UNKNOWN then
	    -- skip these, though they shouldn't be encountered here anyway
	 else
	    hitNonWhitespace = true

	    local str = stringsub(code, pos, nextPos - 1)

	    prevTokenWidth = nextPos - pos

	    -- See if this is an indent-modifier
	    local indentTable
	    if tokenType == tokens.TOKEN_IDENTIFIER then
	       indentTable = keywords[str]
	    else
	       indentTable = tokenIndentation[tokenType]
	    end

	    if indentTable then
	       if hitIndentRight then
		  postIndent = postIndent + indentTable[1] + indentTable[2]
	       else
		  local pre = indentTable[1]
		  local post = indentTable[2]
		  if post > 0 then
		     hitIndentRight = true
		  end
		  preIndent = preIndent + pre
		  postIndent = postIndent + post
	       end
	    end

	    -- Add coloring
	    if keywords[str] then
	       tokenType = tokens.TOKEN_KEYWORD
	    end

	    local color
	    if stopColor then
	       color = colorTable[str]
	       if not color then
		  color = colorTable[tokenType]
		  if not color then
		     if tokenType == tokens.TOKEN_IDENTIFIER then
			color = colorTable[tokens.TOKEN_IDENTIFIER]
		     else
			color = colorTable[tokens.TOKEN_SPECIAL]
		     end
		  end
	       end
	    end

	    if color then
	       tsize2 = tsize2 + 1
	       workingTable2[tsize2] = color
	       totalLen2 = totalLen2 + stringlen(color)

	       tsize2 = tsize2 + 1
	       workingTable2[tsize2] = str
	       totalLen2 = totalLen2 + nextPos - pos

	       tsize2 = tsize2 + 1
	       workingTable2[tsize2] = stopColor
	       totalLen2 = totalLen2 + stopColorLen

	       prevTokenWasColored = true
	    else
	       tsize2 = tsize2 + 1
	       workingTable2[tsize2] = str
	       totalLen2 = totalLen2 + nextPos - pos

	    end
	 end
	 pos = nextPos
      end
      return table.concat(workingTable), newCaretPosition
   end



   -- WoW specific code:
   local GetTime = GetTime

   local editboxSetText
   local editboxGetText

   -- Caret code (thanks Tem!)
   local function critical_enter(editbox)
      local script = editbox:GetScript("OnTextSet")
      if script then
	 editbox:SetScript("OnTextSet", nil)
      end
      return script
   end

   local function critical_leave(editbox, script)
      if script then
	 editbox:SetScript("OnTextSet", script)
      end
   end

   local function setCaretPos_main(editbox, pos)
      local text = editboxGetText(editbox)

      if stringlen(text) > 0 then
	 editboxSetText(editbox, stringinsert(text, pos, "a"))
	 editbox:HighlightText(pos, pos + 1)
	 editbox:Insert("\0")
      end
   end

   local function getCaretPos(editbox)
      local script = critical_enter(editbox)

      local text = editboxGetText(editbox)
      editbox:Insert("\1")
      local pos = stringfind(editboxGetText(editbox), "\1", 1, 1)
      editboxSetText(editbox, text)

      if pos then
	 setCaretPos_main(editbox, pos - 1)
      end
      critical_leave(editbox, script)

      return (pos or 0) - 1
   end

   local function setCaretPos(editbox, pos)
      local script = critical_enter(editbox)
      setCaretPos_main(editbox, pos)
      critical_leave(editbox, script, script2)
   end
   -- end of caret code

   function lib.stripWowColors(code)

      -- HACK!
      -- This is a fix for a bug, where an unfinished string causes a lot of newlines to be created.
      -- The reason for the bug, is that a |r\n\n gets converted to \n\n|r after the next indent-run
      -- The fix is to remove those last two linebreaks when stripping
      code = stringgsub(code, "|r\n\n$", "|r")

      tableclear(workingTable)
      local tsize = 0

      local pos = 1

      local prevVertical = false
      local even = true
      local selectionStart = 1

      while true do
	 local byte = stringbyte(code, pos)
	 if not byte then
	    break
	 end
	 if byte == bytes.BYTE_VERTICAL then
	    even = not even
	    prevVertical = true
	 else
	    if prevVertical and not even then
	       if byte == bytes.BYTE_c then

		  if pos - 2 >= selectionStart then
		     tsize = tsize + 1
		     workingTable[tsize] = stringsub(code, selectionStart, pos - 2)
		  end

		  pos = pos + 8
		  selectionStart = pos + 1
	       elseif byte == bytes.BYTE_r then

		  if pos - 2 >= selectionStart then
		     tsize = tsize + 1
		     workingTable[tsize] = stringsub(code, selectionStart, pos - 2)
		  end
		  selectionStart = pos + 1
	       end
	    end
	    prevVertical = false
	    even = true
	 end
	 pos = pos + 1
      end
      if pos >= selectionStart then
	 tsize = tsize + 1
	 workingTable[tsize] = stringsub(code, selectionStart, pos - 1)
      end
      return table.concat(workingTable)
   end

   function lib.decode(code)
      if code then
	 code = lib.stripWowColors(code)
	 code = stringgsub(code, "||", "|")
      end
      return code or ""
   end

   function lib.encode(code)
      if code then
	 code = stringgsub(code, "|", "||")
      end
      return code or ""
   end

   function lib.stripWowColorsWithPos(code, pos)
      code = stringinsert(code, pos, "\2")
      code = lib.stripWowColors(code)
      pos = stringfind(code, "\2", 1, 1)
      code = stringdelete(code, pos, pos)
      return code, pos
   end

   -- returns the padded code, and true if modified, false if unmodified
   local linebreak = stringbyte("\n")
   function lib.padWithLinebreaks(code)
      local len = stringlen(code)
      if stringbyte(code, len) == linebreak then
         if stringbyte(code, len - 1) == linebreak then
            return code, false
         end
         return code .. "\n", true
      end
      return code, true

   end

   local defaultTabWidth = 2
   local defaultColorTable

   -- Data tables
   -- No weak table magic, since editboxes can never be removed in WoW
   local enabled = {}
   local dirty = {}

   local editboxIndentCache = {}
   local decodeCache = {}
   local editboxStringCache = {}
   local editboxNumLinesCache = {}

   function lib.colorCodeEditbox(editbox)
      dirty[editbox] = nil

      local colorTable = editbox.faiap_colorTable or defaultColorTable
      local tabWidth = editbox.faiap_tabWidth

      local orgCode = editboxGetText(editbox)
      local prevCode = editboxStringCache[editbox]
      if prevCode == orgCode then
	 return
      end

      local pos = getCaretPos(editbox)

      local code
      code, pos = lib.stripWowColorsWithPos(orgCode, pos)

      colorTable[0] = "|r"

      local newCode, newPos, numLines = lib.colorCodeCode(code, colorTable, pos)
      newCode = lib.padWithLinebreaks(newCode)

      editboxStringCache[editbox] = newCode
      if orgCode ~= newCode then
	 local script, script2 = critical_enter(editbox)
	 decodeCache[editbox] = nil
	 local stringlenNewCode = stringlen(newCode)

	 editboxSetText(editbox, newCode)
	 if newPos then
	    if newPos < 0 then newPos = 0 end
	    if newPos > stringlenNewCode then newPos = stringlenNewCode end

	    setCaretPos(editbox, newPos)
	 end
	 critical_leave(editbox, script, script2)
      end

      if editboxNumLinesCache[editbox] ~= numLines then
	 lib.indentEditbox(editbox)
      end
      editboxNumLinesCache[editbox] = numLines
   end

   function lib.indentEditbox(editbox)
      dirty[editbox] = nil

      local colorTable = editbox.faiap_colorTable or defaultColorTable
      local tabWidth = editbox.faiap_tabWidth

      local orgCode = editboxGetText(editbox)
      local prevCode = editboxIndentCache[editbox]
      if prevCode == orgCode then
	 return
      end

      local pos = getCaretPos(editbox)

      local code
      code, pos = lib.stripWowColorsWithPos(orgCode, pos)

      colorTable[0] = "|r"
      local newCode, newPos = lib.indentCode(code, tabWidth, colorTable, pos)
      newCode = lib.padWithLinebreaks(newCode)
      editboxIndentCache[editbox] = newCode
      if code ~= newCode then
	 local script, script2 = critical_enter(editbox)
	 decodeCache[editbox] = nil

	 local stringlenNewCode = stringlen(newCode)

	 editboxSetText(editbox, newCode)

	 if newPos then
	    if newPos < 0 then newPos = 0 end
	    if newPos > stringlenNewCode then newPos = stringlenNewCode end

	    setCaretPos(editbox, newPos)
	 end
	 critical_leave(editbox, script, script2)
      end
   end

   local function hookHandler(editbox, handler, newFun)
      local oldFun = editbox:GetScript(handler)
      if oldFun == newFun then
	 -- already hooked, ignore it
	 return
      end
      editbox["faiap_old_" .. handler] = oldFun
      editbox:SetScript(handler, newFun)
   end

   local function textChangedHook(editbox, ...)
      local oldFun = editbox["faiap_old_OnTextChanged"]
      if oldFun then
	 oldFun(editbox, ...)
      end
      if enabled[editbox] then
	 dirty[editbox] = GetTime()
      end
   end

   local function tabPressedHook(editbox, ...)
      local oldFun = editbox["faiap_old_OnTabPressed"]
      if oldFun then
	 oldFun(editbox, ...)
      end
      if enabled[editbox] then
	 return lib.indentEditbox(editbox)
      end
   end

   local function onUpdateHook(editbox, ...)
      local oldFun = editbox["faiap_old_OnUpdate"]
      if oldFun then
	 oldFun(editbox, ...)
      end
      if enabled[editbox] then
	 local now = GetTime()
	 local lastUpdate = dirty[editbox] or now
	 if now - lastUpdate > 0.2 then
	    decodeCache[editbox] = nil
	    return lib.colorCodeEditbox(editbox)
	 end
      end
   end

   local function newGetText(editbox)
      local decoded = decodeCache[editbox]
      if not decoded then
	 decoded = lib.decode(editboxGetText(editbox))
	 decodeCache[editbox] = decoded
      end
      return decoded or ""
   end

   local function newSetText(editbox, text)
      decodeCache[editbox] = nil
      if text then
	 local encoded = lib.encode(text)

	 return editboxSetText(editbox, encoded)
      end
   end

   function lib.enable(editbox, colorTable, tabWidth)
      if not editboxSetText then
	 editboxSetText = editbox.SetText
	 editboxGetText = editbox.GetText
      end

      local modified
      if editbox.faiap_colorTable ~= colorTable then
	 editbox.faiap_colorTable = colorTable
	 modified = true
      end
      if editbox.faiap_tabWidth ~= tabWidth then
	 editbox.faiap_tabWidth = tabWidth
	 modified = true
      end

      if enabled[editbox] then
	 if modified then
	    lib.indentEditbox(editbox)
	 end
	 return
      end

      -- Editbox is possibly hooked, but disabled
      enabled[editbox] = true

      editbox.oldMaxBytes = editbox:GetMaxBytes()
      editbox.oldMaxLetters = editbox:GetMaxLetters()
      editbox:SetMaxBytes(0)
      editbox:SetMaxLetters(0)

      editbox.GetText = newGetText
      editbox.SetText = newSetText

      hookHandler(editbox, "OnTextChanged", textChangedHook)
      hookHandler(editbox, "OnTabPressed", tabPressedHook)
      hookHandler(editbox, "OnUpdate", onUpdateHook)

      lib.indentEditbox(editbox)
   end

   -- Deprecated function
   lib.addSmartCode = lib.enable

   function lib.disable(editbox)
      if not enabled[editbox] then
	 return
      end
      enabled[editbox] = nil

      -- revert settings for max bytes / letters
      editbox:SetMaxBytes(editbox.oldMaxBytes)
      editbox:SetMaxLetters(editbox.oldMaxLetters)

      -- try a real unhooking, if possible
      if editbox:GetScript("OnTextChanged") == textChangedHook then
	 editbox:SetScript("OnTextChanged", editbox.faiap_old_OnTextChanged)
	 editbox.faiap_old_OnTextChanged = nil
      end

      if editbox:GetScript("OnTabPressed") == tabPressedHook then
	 editbox:SetScript("OnTabPressed", editbox.faiap_old_OnTabPressed)
	 editbox.faiap_old_OnTabPressed = nil
      end

      if editbox:GetScript("OnUpdate") == onUpdateHook then
	 editbox:SetScript("OnUpdate", editbox.faiap_old_OnUpdate)
	 editbox.faiap_old_OnUpdate = nil
      end

      editbox.GetText = nil
      editbox.SetText = nil

      -- change the text back to unformatted
      editbox:SetText(newGetText(editbox))

      -- clear caches
      editboxIndentCache[editbox] = nil
      decodeCache[editbox] = nil
      editboxStringCache[editbox] = nil
      editboxNumLinesCache[editbox] = nil
   end

   defaultColorTable = {}
   lib.defaultColorTable = defaultColorTable
   defaultColorTable[tokens.TOKEN_SPECIAL] = "|c00ff99ff"
   defaultColorTable[tokens.TOKEN_KEYWORD] = "|c006666ff"
   defaultColorTable[tokens.TOKEN_COMMENT_SHORT] = "|c00999999"
   defaultColorTable[tokens.TOKEN_COMMENT_LONG] = "|c00999999"

   local stringColor = "|c00ffff77"
   defaultColorTable[tokens.TOKEN_STRING] = stringColor
   defaultColorTable[".."] = stringColor

   local tableColor = "|c00ff9900"
   defaultColorTable["..."] = tableColor
   defaultColorTable["{"] = tableColor
   defaultColorTable["}"] = tableColor
   defaultColorTable["["] = tableColor
   defaultColorTable["]"] = tableColor

   local arithmeticColor = "|c0033ff55"
   defaultColorTable[tokens.TOKEN_NUMBER] = arithmeticColor
   defaultColorTable["+"] = arithmeticColor
   defaultColorTable["-"] = arithmeticColor
   defaultColorTable["/"] = arithmeticColor
   defaultColorTable["*"] = arithmeticColor

   local logicColor1 = "|c0055ff88"
   defaultColorTable["=="] = logicColor1
   defaultColorTable["<"] = logicColor1
   defaultColorTable["<="] = logicColor1
   defaultColorTable[">"] = logicColor1
   defaultColorTable[">="] = logicColor1
   defaultColorTable["~="] = logicColor1

   local logicColor2 = "|c0088ffbb"
   defaultColorTable["and"] = logicColor2
   defaultColorTable["or"] = logicColor2
   defaultColorTable["not"] = logicColor2

   defaultColorTable[0] = "|r"

end

-- just for testing
--[[
function testTokenizer()
   local str = ""
   for line in io.lines("indent.lua") do
      str = str .. line .. "\n"
   end

   local pos = 1

   while true do
      local tokenType, nextPos = nextToken(str, pos)

      if not tokenType then
	 break
      end

      if true or tokenType ~= tokens.TOKEN_WHITESPACE and tokenType ~= tokens.TOKEN_LINEBREAK then
	 print(stringformat("Found token %d (%d-%d): (%s)", tokenType, pos, nextPos - 1, stringsub(str, pos, nextPos - 1)))
      end

      if tokenType == tokens.TOKEN_UNKNOWN then
	 print("unknown token!")
	 break
      end

      pos = nextPos
   end
end


function testIndenter(i)
   local lib = IndentationLib
   local str = ""
   for line in io.lines("test.lua") do
      str = str .. line .. "\n"
   end

   local colorTable = lib.defaultColorTable
   print(lib.indentCode(str, 4, colorTable, i))
end


testIndenter()

--]]