Untitled

Run Settings
LanguageLua
Language Version
Run Command
function lookupify(tb) for _, v in pairs(tb) do tb[v] = true end return tb end local WhiteChars = lookupify{' ', '\n', '\t', '\r'} local LowerChars = lookupify{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'} local UpperChars = lookupify{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'} local Digits = lookupify{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'} local HexDigits = lookupify{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'a', 'B', 'b', 'C', 'c', 'D', 'd', 'E', 'e', 'F', 'f'} local Symbols = lookupify{'+', '-', '*', '/', '^', '%', ',', '{', '}', '[', ']', '(', ')', ';', '#'} local Keywords = lookupify{ 'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while', }; function LexLua(src) --token dump local tokens = {} local st, err = pcall(function() --line / char / pointer tracking local p = 1 local line = 1 local char = 1 --get / peek functions local function get() local c = src:sub(p,p) if c == '\n' then char = 1 line = line + 1 else char = char + 1 end p = p + 1 return c end local function peek(n) n = n or 0 return src:sub(p+n,p+n) end local function consume(chars) local c = peek() for i = 1, #chars do if c == chars:sub(i,i) then return get() end end end --shared stuff local function generateError(err) return error(">> :"..line..":"..char..": "..err, 0) end local function tryGetLongString() local start = p if peek() == '[' then local equalsCount = 0 while peek(equalsCount+1) == '=' do equalsCount = equalsCount + 1 end if peek(equalsCount+1) == '[' then --start parsing the string. Strip the starting bit for _ = 0, equalsCount+1 do get() end --get the contents local contentStart = p while true do --check for eof if peek() == '' then generateError("Expected `]"..string.rep('=', equalsCount).."]` near <eof>.", 3) end --check for the end local foundEnd = true if peek() == ']' then for i = 1, equalsCount do if peek(i) ~= '=' then foundEnd = false end end if peek(equalsCount+1) ~= ']' then foundEnd = false end else foundEnd = false end -- if foundEnd then break else get() end end --get the interior string local contentString = src:sub(contentStart, p-1) --found the end. Get rid of the trailing bit for i = 0, equalsCount+1 do get() end --get the exterior string local longString = src:sub(start, p-1) --return the stuff return contentString, longString else return nil end else return nil end end --main token emitting loop while true do --get leading whitespace. The leading whitespace will include any comments --preceding the token. This prevents the parser needing to deal with comments --separately. local leadingWhite = '' while true do local c = peek() if WhiteChars[c] then --whitespace leadingWhite = leadingWhite..get() elseif c == '-' and peek(1) == '-' then --comment get();get() leadingWhite = leadingWhite..'--' local _, wholeText = tryGetLongString() if wholeText then leadingWhite = leadingWhite..wholeText else while peek() ~= '\n' and peek() ~= '' do leadingWhite = leadingWhite..get() end end else break end end --get the initial char local thisLine = line local thisChar = char local errorAt = ":"..line..":"..char..":> " local c = peek() --symbol to emit local toEmit = nil --branch on type if c == '' then --eof toEmit = {Type = 'Eof'} elseif UpperChars[c] or LowerChars[c] or c == '_' then --ident or keyword local start = p repeat get() c = peek() until not (UpperChars[c] or LowerChars[c] or Digits[c] or c == '_') local dat = src:sub(start, p-1) if Keywords[dat] then toEmit = {Type = 'Keyword', Data = dat} else toEmit = {Type = 'Ident', Data = dat} end elseif Digits[c] or (peek() == '.' and Digits[peek(1)]) then --number const local start = p if c == '0' and peek(1) == 'x' then get();get() while HexDigits[peek()] do get() end if consume('Pp') then consume('+-') while Digits[peek()] do get() end end else while Digits[peek()] do get() end if consume('.') then while Digits[peek()] do get() end end if consume('Ee') then consume('+-') while Digits[peek()] do get() end end end toEmit = {Type = 'Number', Data = src:sub(start, p-1)} elseif c == '\'' or c == '\"' then local start = p --string const local delim = get() local contentStart = p while true do local c = get() if c == '\\' then get() --get the escape char elseif c == delim then break elseif c == '' then generateError("Unfinished string near <eof>") end end local content = src:sub(contentStart, p-2) local constant = src:sub(start, p-1) toEmit = {Type = 'String', Data = constant, Constant = content} elseif c == '[' then local content, wholetext = tryGetLongString() if wholetext then toEmit = {Type = 'String', Data = wholetext, Constant = content} else get() toEmit = {Type = 'Symbol', Data = '['} end elseif consume('>=<') then if consume('=') then toEmit = {Type = 'Symbol', Data = c..'='} else toEmit = {Type = 'Symbol', Data = c} end elseif consume('~') then if consume('=') then toEmit = {Type = 'Symbol', Data = '~='} else generateError("Unexpected symbol `~` in source.", 2) end elseif consume('.') then if consume('.') then if consume('.') then toEmit = {Type = 'Symbol', Data = '...'} else toEmit = {Type = 'Symbol', Data = '..'} end else toEmit = {Type = 'Symbol', Data = '.'} end elseif consume(':') then if consume(':') then toEmit = {Type = 'Symbol', Data = '::'} else toEmit = {Type = 'Symbol', Data = ':'} end elseif Symbols[c] then get() toEmit = {Type = 'Symbol', Data = c} else local contents, all = tryGetLongString() if contents then toEmit = {Type = 'String', Data = all, Constant = contents} else generateError("Unexpected Symbol `"..c.."` in source.", 2) end end --add the emitted symbol, after adding some common data toEmit.LeadingWhite = leadingWhite toEmit.Line = thisLine toEmit.Char = thisChar toEmit.Print = function() return "<"..(toEmit.Type..string.rep(' ', 7-#toEmit.Type)).." "..(toEmit.Data or '').." >" end tokens[#tokens+1] = toEmit --halt after eof has been emitted if toEmit.Type == 'Eof' then break end end end) if not st then return false, err end --public interface: local tok = {} local savedP = {} local p = 1 --getters function tok:Peek(n) n = n or 0 return tokens[math.min(#tokens, p+n)] end function tok:Get() local t = tokens[p] p = math.min(p + 1, #tokens) return t end function tok:Is(t) return tok:Peek().Type == t end --save / restore points in the stream function tok:Save() savedP[#savedP+1] = p end function tok:Commit() savedP[#savedP] = nil end function tok:Restore() p = savedP[#savedP] savedP[#savedP] = nil end --either return a symbol if there is one, or return true if the requested --symbol was gotten. function tok:ConsumeSymbol(symb) local t = self:Peek() if t.Type == 'Symbol' then if symb then if t.Data == symb then self:Get() return true else return nil end else self:Get() return t end else return nil end end function tok:ConsumeKeyword(kw) local t = self:Peek() if t.Type == 'Keyword' and t.Data == kw then self:Get() return true else return nil end end function tok:IsKeyword(kw) local t = tok:Peek() return t.Type == 'Keyword' and t.Data == kw end function tok:IsSymbol(s) local t = tok:Peek() return t.Type == 'Symbol' and t.Data == s end function tok:IsEof() return tok:Peek().Type == 'Eof' end return true, tok end function ParseLua(src) local st, tok = LexLua(src) if not st then return false, tok end -- local function GenerateError(msg) local err = ">> :"..tok:Peek().Line..":"..tok:Peek().Char..": "..msg.."\n" --find the line local lineNum = 0 for line in src:gmatch("[^\n]*\n?") do if line:sub(-1,-1) == '\n' then line = line:sub(1,-2) end lineNum = lineNum+1 if lineNum == tok:Peek().Line then err = err..">> `"..line:gsub('\t',' ').."`\n" for i = 1, tok:Peek().Char do local c = line:sub(i,i) if c == '\t' then err = err..' ' else err = err..' ' end end err = err.." ^---" break end end return err end -- local VarUid = 0 local GlobalVarGetMap = {} local VarDigits = {'_', 'a', 'b', 'c', 'd'} local function CreateScope(parent) local scope = {} scope.Parent = parent scope.LocalList = {} scope.LocalMap = {} function scope:RenameVars() for _, var in pairs(scope.LocalList) do local id; VarUid = 0 repeat VarUid = VarUid + 1 local varToUse = VarUid id = '' while varToUse > 0 do local d = varToUse % #VarDigits varToUse = (varToUse - d) / #VarDigits id = id..VarDigits[d+1] end until not GlobalVarGetMap[id] and not parent:GetLocal(id) and not scope.LocalMap[id] var.Name = id scope.LocalMap[id] = var end end function scope:GetLocal(name) --first, try to get my variable local my = scope.LocalMap[name] if my then return my end --next, try parent if scope.Parent then local par = scope.Parent:GetLocal(name) if par then return par end end return nil end function scope:CreateLocal(name) --create my own var local my = {} my.Scope = scope my.Name = name my.CanRename = true -- scope.LocalList[#scope.LocalList+1] = my scope.LocalMap[name] = my -- return my end scope.Print = function() return "<Scope>" end return scope end local ParseExpr; local ParseStatementList; local function ParseFunctionArgsAndBody(scope) local funcScope = CreateScope(scope) if not tok:ConsumeSymbol('(') then return false, GenerateError("`(` expected.") end --arg list local argList = {} local isVarArg = false while not tok:ConsumeSymbol(')') do if tok:Is('Ident') then local arg = funcScope:CreateLocal(tok:Get().Data) argList[#argList+1] = arg if not tok:ConsumeSymbol(',') then if tok:ConsumeSymbol(')') then break else return false, GenerateError("`)` expected.") end end elseif tok:ConsumeSymbol('...') then isVarArg = true if not tok:ConsumeSymbol(')') then return false, GenerateError("`...` must be the last argument of a function.") end break else return false, GenerateError("Argument name or `...` expected") end end --body local st, body = ParseStatementList(funcScope) if not st then return false, body end --end if not tok:ConsumeKeyword('end') then return false, GenerateError("`end` expected after function body") end local nodeFunc = {} nodeFunc.AstType = 'Function' nodeFunc.Scope = funcScope nodeFunc.Arguments = argList nodeFunc.Body = body nodeFunc.VarArg = isVarArg -- return true, nodeFunc end local function ParsePrimaryExpr(scope) if tok:ConsumeSymbol('(') then local st, ex = ParseExpr(scope) if not st then return false, ex end if not tok:ConsumeSymbol(')') then return false, GenerateError("`)` Expected.") end --save the information about parenthesized expressions somewhere ex.ParenCount = (ex.ParenCount or 0) + 1 return true, ex elseif tok:Is('Ident') then local id = tok:Get() local var = scope:GetLocal(id.Data) if not var then GlobalVarGetMap[id.Data] = true end -- local nodePrimExp = {} nodePrimExp.AstType = 'VarExpr' nodePrimExp.Name = id.Data nodePrimExp.Local = var -- return true, nodePrimExp else return false, GenerateError("primary expression expected") end end local function ParseSuffixedExpr(scope, onlyDotColon) --base primary expression local st, prim = ParsePrimaryExpr(scope) if not st then return false, prim end -- while true do if tok:IsSymbol('.') or tok:IsSymbol(':') then local symb = tok:Get().Data if not tok:Is('Ident') then return false, GenerateError("<Ident> expected.") end local id = tok:Get() local nodeIndex = {} nodeIndex.AstType = 'MemberExpr' nodeIndex.Base = prim nodeIndex.Indexer = symb nodeIndex.Ident = id -- prim = nodeIndex elseif not onlyDotColon and tok:ConsumeSymbol('[') then local st, ex = ParseExpr(scope) if not st then return false, ex end if not tok:ConsumeSymbol(']') then return false, GenerateError("`]` expected.") end local nodeIndex = {} nodeIndex.AstType = 'IndexExpr' nodeIndex.Base = prim nodeIndex.Index = ex -- prim = nodeIndex elseif not onlyDotColon and tok:ConsumeSymbol('(') then local args = {} while not tok:ConsumeSymbol(')') do local st, ex = ParseExpr(scope) if not st then return false, ex end args[#args+1] = ex if not tok:ConsumeSymbol(',') then if tok:ConsumeSymbol(')') then break else return false, GenerateError("`)` Expected.") end end end local nodeCall = {} nodeCall.AstType = 'CallExpr' nodeCall.Base = prim nodeCall.Arguments = args -- prim = nodeCall elseif not onlyDotColon and tok:Is('String') then --string call local nodeCall = {} nodeCall.AstType = 'StringCallExpr' nodeCall.Base = prim nodeCall.Arguments = {tok:Get()} -- prim = nodeCall elseif not onlyDotColon and tok:IsSymbol('{') then --table call local st, ex = ParseExpr(scope) if not st then return false, ex end local nodeCall = {} nodeCall.AstType = 'TableCallExpr' nodeCall.Base = prim nodeCall.Arguments = {ex} -- prim = nodeCall else break end end return true, prim end local function ParseSimpleExpr(scope) if tok:Is('Number') then local nodeNum = {} nodeNum.AstType = 'NumberExpr' nodeNum.Value = tok:Get() return true, nodeNum elseif tok:Is('String') then local nodeStr = {} nodeStr.AstType = 'StringExpr' nodeStr.Value = tok:Get() return true, nodeStr elseif tok:ConsumeKeyword('nil') then local nodeNil = {} nodeNil.AstType = 'NilExpr' return true, nodeNil elseif tok:IsKeyword('false') or tok:IsKeyword('true') then local nodeBoolean = {} nodeBoolean.AstType = 'BooleanExpr' nodeBoolean.Value = (tok:Get().Data == 'true') return true, nodeBoolean elseif tok:ConsumeSymbol('...') then local nodeDots = {} nodeDots.AstType = 'DotsExpr' return true, nodeDots elseif tok:ConsumeSymbol('{') then local v = {} v.AstType = 'ConstructorExpr' v.EntryList = {} -- while true do if tok:IsSymbol('[') then --key tok:Get() local st, key = ParseExpr(scope) if not st then return false, GenerateError("Key Expression Expected") end if not tok:ConsumeSymbol(']') then return false, GenerateError("`]` Expected") end if not tok:ConsumeSymbol('=') then return false, GenerateError("`=` Expected") end local st, value = ParseExpr(scope) if not st then return false, GenerateError("Value Expression Expected") end v.EntryList[#v.EntryList+1] = { Type = 'Key'; Key = key; Value = value; } elseif tok:Is('Ident') then --value or key local lookahead = tok:Peek(1) if lookahead.Type == 'Symbol' and lookahead.Data == '=' then --we are a key local key = tok:Get() if not tok:ConsumeSymbol('=') then return false, GenerateError("`=` Expected") end local st, value = ParseExpr(scope) if not st then return false, GenerateError("Value Expression Expected") end v.EntryList[#v.EntryList+1] = { Type = 'KeyString'; Key = key.Data; Value = value; } else --we are a value local st, value = ParseExpr(scope) if not st then return false, GenerateError("Value Exected") end v.EntryList[#v.EntryList+1] = { Type = 'Value'; Value = value; } end elseif tok:ConsumeSymbol('}') then break else --value local st, value = ParseExpr(scope) v.EntryList[#v.EntryList+1] = { Type = 'Value'; Value = value; } if not st then return false, GenerateError("Value Expected") end end if tok:ConsumeSymbol(';') or tok:ConsumeSymbol(',') then --all is good elseif tok:ConsumeSymbol('}') then break else return false, GenerateError("`}` or table entry Expected") end end return true, v elseif tok:ConsumeKeyword('function') then local st, func = ParseFunctionArgsAndBody(scope) if not st then return false, func end -- func.IsLocal = true return true, func else return ParseSuffixedExpr(scope) end end local unops = lookupify{'-', 'not', '#'} local unopprio = 8 local priority = { ['+'] = {6,6}; ['-'] = {6,6}; ['%'] = {7,7}; ['/'] = {7,7}; ['*'] = {7,7}; ['^'] = {10,9}; ['..'] = {5,4}; ['=='] = {3,3}; ['<'] = {3,3}; ['<='] = {3,3}; ['~='] = {3,3}; ['>'] = {3,3}; ['>='] = {3,3}; ['and'] = {2,2}; ['or'] = {1,1}; } local function ParseSubExpr(scope, level) --base item, possibly with unop prefix local st, exp if unops[tok:Peek().Data] then local op = tok:Get().Data st, exp = ParseSubExpr(scope, unopprio) if not st then return false, exp end local nodeEx = {} nodeEx.AstType = 'UnopExpr' nodeEx.Rhs = exp nodeEx.Op = op exp = nodeEx else st, exp = ParseSimpleExpr(scope) if not st then return false, exp end end --next items in chain while true do local prio = priority[tok:Peek().Data] if prio and prio[1] > level then local op = tok:Get().Data local st, rhs = ParseSubExpr(scope, prio[2]) if not st then return false, rhs end local nodeEx = {} nodeEx.AstType = 'BinopExpr' nodeEx.Lhs = exp nodeEx.Op = op nodeEx.Rhs = rhs -- exp = nodeEx else break end end return true, exp end ParseExpr = function(scope) return ParseSubExpr(scope, 0) end local function ParseStatement(scope) local stat = nil if tok:ConsumeKeyword('if') then --setup local nodeIfStat = {} nodeIfStat.AstType = 'IfStatement' nodeIfStat.Clauses = {} --clauses repeat local st, nodeCond = ParseExpr(scope) if not st then return false, nodeCond end if not tok:ConsumeKeyword('then') then return false, GenerateError("`then` expected.") end local st, nodeBody = ParseStatementList(scope) if not st then return false, nodeBody end nodeIfStat.Clauses[#nodeIfStat.Clauses+1] = { Condition = nodeCond; Body = nodeBody; } until not tok:ConsumeKeyword('elseif') --else clause if tok:ConsumeKeyword('else') then local st, nodeBody = ParseStatementList(scope) if not st then return false, nodeBody end nodeIfStat.Clauses[#nodeIfStat.Clauses+1] = { Body = nodeBody; } end --end if not tok:ConsumeKeyword('end') then return false, GenerateError("`end` expected.") end stat = nodeIfStat elseif tok:ConsumeKeyword('while') then --setup local nodeWhileStat = {} nodeWhileStat.AstType = 'WhileStatement' --condition local st, nodeCond = ParseExpr(scope) if not st then return false, nodeCond end --do if not tok:ConsumeKeyword('do') then return false, GenerateError("`do` expected.") end --body local st, nodeBody = ParseStatementList(scope) if not st then return false, nodeBody end --end if not tok:ConsumeKeyword('end') then return false, GenerateError("`end` expected.") end --return nodeWhileStat.Condition = nodeCond nodeWhileStat.Body = nodeBody stat = nodeWhileStat elseif tok:ConsumeKeyword('do') then --do block local st, nodeBlock = ParseStatementList(scope) if not st then return false, nodeBlock end if not tok:ConsumeKeyword('end') then return false, GenerateError("`end` expected.") end local nodeDoStat = {} nodeDoStat.AstType = 'DoStatement' nodeDoStat.Body = nodeBlock stat = nodeDoStat elseif tok:ConsumeKeyword('for') then --for block if not tok:Is('Ident') then return false, GenerateError("<ident> expected.") end local baseVarName = tok:Get() if tok:ConsumeSymbol('=') then --numeric for local forScope = CreateScope(scope) local forVar = forScope:CreateLocal(baseVarName.Data) -- local st, startEx = ParseExpr(scope) if not st then return false, startEx end if not tok:ConsumeSymbol(',') then return false, GenerateError("`,` Expected") end local st, endEx = ParseExpr(scope) if not st then return false, endEx end local st, stepEx; if tok:ConsumeSymbol(',') then st, stepEx = ParseExpr(scope) if not st then return false, stepEx end end if not tok:ConsumeKeyword('do') then return false, GenerateError("`do` expected") end -- local st, body = ParseStatementList(forScope) if not st then return false, body end if not tok:ConsumeKeyword('end') then return false, GenerateError("`end` expected") end -- local nodeFor = {} nodeFor.AstType = 'NumericForStatement' nodeFor.Scope = forScope nodeFor.Variable = forVar nodeFor.Start = startEx nodeFor.End = endEx nodeFor.Step = stepEx nodeFor.Body = body stat = nodeFor else --generic for local forScope = CreateScope(scope) -- local varList = {forScope:CreateLocal(baseVarName.Data)} while tok:ConsumeSymbol(',') do if not tok:Is('Ident') then return false, GenerateError("for variable expected.") end varList[#varList+1] = forScope:CreateLocal(tok:Get().Data) end if not tok:ConsumeKeyword('in') then return false, GenerateError("`in` expected.") end local generators = {} local st, firstGenerator = ParseExpr(scope) if not st then return false, firstGenerator end generators[#generators+1] = firstGenerator while tok:ConsumeSymbol(',') do local st, gen = ParseExpr(scope) if not st then return false, gen end generators[#generators+1] = gen end if not tok:ConsumeKeyword('do') then return false, GenerateError("`do` expected.") end local st, body = ParseStatementList(forScope) if not st then return false, body end if not tok:ConsumeKeyword('end') then return false, GenerateError("`end` expected.") end -- local nodeFor = {} nodeFor.AstType = 'GenericForStatement' nodeFor.Scope = forScope nodeFor.VariableList = varList nodeFor.Generators = generators nodeFor.Body = body stat = nodeFor end elseif tok:ConsumeKeyword('repeat') then local st, body = ParseStatementList(scope) if not st then return false, body end -- if not tok:ConsumeKeyword('until') then return false, GenerateError("`until` expected.") end -- local st, cond = ParseExpr(scope) if not st then return false, cond end -- local nodeRepeat = {} nodeRepeat.AstType = 'RepeatStatement' nodeRepeat.Condition = cond nodeRepeat.Body = body stat = nodeRepeat elseif tok:ConsumeKeyword('function') then if not tok:Is('Ident') then return false, GenerateError("Function name expected") end local st, name = ParseSuffixedExpr(scope, true) --true => only dots and colons if not st then return false, name end -- local st, func = ParseFunctionArgsAndBody(scope) if not st then return false, func end -- func.IsLocal = false func.Name = name stat = func elseif tok:ConsumeKeyword('local') then if tok:Is('Ident') then local varList = {tok:Get().Data} while tok:ConsumeSymbol(',') do if not tok:Is('Ident') then return false, GenerateError("local var name expected") end varList[#varList+1] = tok:Get().Data end local initList = {} if tok:ConsumeSymbol('=') then repeat local st, ex = ParseExpr(scope) if not st then return false, ex end initList[#initList+1] = ex until not tok:ConsumeSymbol(',') end --now patch var list --we can't do this before getting the init list, because the init list does not --have the locals themselves in scope. for i, v in pairs(varList) do varList[i] = scope:CreateLocal(v) end local nodeLocal = {} nodeLocal.AstType = 'LocalStatement' nodeLocal.LocalList = varList nodeLocal.InitList = initList -- stat = nodeLocal elseif tok:ConsumeKeyword('function') then if not tok:Is('Ident') then return false, GenerateError("Function name expected") end local name = tok:Get().Data local localVar = scope:CreateLocal(name) -- local st, func = ParseFunctionArgsAndBody(scope) if not st then return false, func end -- func.Name = localVar func.IsLocal = true stat = func else return false, GenerateError("local var or function def expected") end elseif tok:ConsumeSymbol('::') then if not tok:Is('Ident') then return false, GenerateError('Label name expected') end local label = tok:Get().Data if not tok:ConsumeSymbol('::') then return false, GenerateError("`::` expected") end local nodeLabel = {} nodeLabel.AstType = 'LabelStatement' nodeLabel.Label = label stat = nodeLabel elseif tok:ConsumeKeyword('return') then local exList = {} if not tok:IsKeyword('end') then local st, firstEx = ParseExpr(scope) if st then exList[1] = firstEx while tok:ConsumeSymbol(',') do local st, ex = ParseExpr(scope) if not st then return false, ex end exList[#exList+1] = ex end end end local nodeReturn = {} nodeReturn.AstType = 'ReturnStatement' nodeReturn.Arguments = exList stat = nodeReturn elseif tok:ConsumeKeyword('break') then local nodeBreak = {} nodeBreak.AstType = 'BreakStatement' stat = nodeBreak elseif tok:IsKeyword('goto') then if not tok:Is('Ident') then return false, GenerateError("Label expected") end local label = tok:Get().Data local nodeGoto = {} nodeGoto.AstType = 'GotoStatement' nodeGoto.Label = label stat = nodeGoto else --statementParseExpr local st, suffixed = ParseSuffixedExpr(scope) if not st then return false, suffixed end --assignment or call? if tok:IsSymbol(',') or tok:IsSymbol('=') then --check that it was not parenthesized, making it not an lvalue if (suffixed.ParenCount or 0) > 0 then return false, GenerateError("Can not assign to parenthesized expression, is not an lvalue") end --more processing needed local lhs = {suffixed} while tok:ConsumeSymbol(',') do local st, lhsPart = ParseSuffixedExpr(scope) if not st then return false, lhsPart end lhs[#lhs+1] = lhsPart end --equals if not tok:ConsumeSymbol('=') then return false, GenerateError("`=` Expected.") end --rhs local rhs = {} local st, firstRhs = ParseExpr(scope) if not st then return false, firstRhs end rhs[1] = firstRhs while tok:ConsumeSymbol(',') do local st, rhsPart = ParseExpr(scope) if not st then return false, rhsPart end rhs[#rhs+1] = rhsPart end --done local nodeAssign = {} nodeAssign.AstType = 'AssignmentStatement' nodeAssign.Lhs = lhs nodeAssign.Rhs = rhs stat = nodeAssign elseif suffixed.AstType == 'CallExpr' or suffixed.AstType == 'TableCallExpr' or suffixed.AstType == 'StringCallExpr' then --it's a call statement local nodeCall = {} nodeCall.AstType = 'CallStatement' nodeCall.Expression = suffixed stat = nodeCall else return false, GenerateError("Assignment Statement Expected") end end stat.HasSemicolon = tok:ConsumeSymbol(';') return true, stat end local statListCloseKeywords = lookupify{'end', 'else', 'elseif', 'until'} ParseStatementList = function(scope) local nodeStatlist = {} nodeStatlist.Scope = CreateScope(scope) nodeStatlist.AstType = 'Statlist' -- local stats = {} -- while not statListCloseKeywords[tok:Peek().Data] and not tok:IsEof() do local st, nodeStatement = ParseStatement(nodeStatlist.Scope) if not st then return false, nodeStatement end stats[#stats+1] = nodeStatement end -- nodeStatlist.Body = stats return true, nodeStatlist end local function mainfunc() local topScope = CreateScope() return ParseStatementList(topScope) end local st, main = mainfunc() return st, main end export type TextObject = TextLabel | TextBox export type TokenName = "background" | "iden" | "keyword" | "builtin" | "string" | "number" | "comment" | "operator" | "custom" export type TokenColors = { ["background"]: Color3?, ["iden"]: Color3?, ["keyword"]: Color3?, ["builtin"]: Color3?, ["string"]: Color3?, ["number"]: Color3?, ["comment"]: Color3?, ["operator"]: Color3?, ["custom"]: Color3?, } export type HighlightProps = { textObject: TextObject, src: string?, forceUpdate: boolean?, lexer: Lexer?, customLang: { [string]: string }?, } export type Lexer = { scan: (src: string) -> () -> (string, string), navigator: () -> any, finished: boolean?, } export type ObjectData = { Text: string, Labels: { TextLabel }, Lines: { string }, Lexer: Lexer?, CustomLang: { [string]: string }?, } local Utility = {} function Utility.sanitizeRichText(s: string): string return string.gsub( string.gsub(string.gsub(string.gsub(string.gsub(s, "&", "&amp;"), "<", "&lt;"), ">", "&gt;"), '"', "&quot;"), "'", "&apos;" ) end function Utility.convertTabsToSpaces(s: string): string return string.gsub(s, "\t", " ") end function Utility.removeControlChars(s: string): string return string.gsub(s, "[\0\1\2\3\4\5\6\7\8\11\12\13\14\15\16\17\18\19\20\21\22\23\24\25\26\27\28\29\30\31]+", "") end function Utility.getInnerAbsoluteSize(textObject: TextObject): Vector2 local fullSize = textObject.AbsoluteSize local padding = textObject:FindFirstChildWhichIsA("UIPadding") if padding then local offsetX = padding.PaddingLeft.Offset + padding.PaddingRight.Offset local scaleX = (fullSize.X * padding.PaddingLeft.Scale) + (fullSize.X * padding.PaddingRight.Scale) local offsetY = padding.PaddingTop.Offset + padding.PaddingBottom.Offset local scaleY = (fullSize.Y * padding.PaddingTop.Scale) + (fullSize.Y * padding.PaddingBottom.Scale) return Vector2.new(fullSize.X - (scaleX + offsetX), fullSize.Y - (scaleY + offsetY)) else return fullSize end end function Utility.getTextBounds(textObject: TextObject): Vector2 if textObject.ContentText == "" then return Vector2.zero end local textBounds = textObject.TextBounds -- Wait for TextBounds to be non-NaN and non-zero because Roblox while (textBounds.Y ~= textBounds.Y) or (textBounds.Y < 1) do task.wait() textBounds = textObject.TextBounds end return textBounds end local utility = Utility local DEFAULT_TOKEN_COLORS = { ["background"] = Color3.fromRGB(47, 47, 47), ["iden"] = Color3.fromRGB(234, 234, 234), ["keyword"] = Color3.fromRGB(215, 174, 255), ["builtin"] = Color3.fromRGB(131, 206, 255), ["string"] = Color3.fromRGB(196, 255, 193), ["number"] = Color3.fromRGB(255, 125, 125), ["comment"] = Color3.fromRGB(140, 140, 155), ["operator"] = Color3.fromRGB(255, 239, 148), ["custom"] = Color3.fromRGB(119, 122, 255), } local Theme = { tokenColors = {}, tokenRichTextFormatter = {}, } function Theme.setColors(tokenColors: TokenColors) assert(type(tokenColors) == "table", "Theme.updateColors expects a table") for tokenName, color in tokenColors do Theme.tokenColors[tokenName] = color end end function Theme.getColoredRichText(color: Color3, text: string): string return '<font color="#' .. color:ToHex() .. '">' .. text .. "</font>" end function Theme.getColor(tokenName: TokenName): Color3 return Theme.tokenColors[tokenName] end function Theme.matchStudioSettings(refreshCallback: () -> ()): boolean local success = pcall(function() -- When not used in a Studio plugin, this will error -- and the pcall will just silently return local studio = settings().Studio local studioTheme = studio.Theme local function getTokens() return { ["background"] = studioTheme:GetColor(Enum.StudioStyleGuideColor.ScriptBackground), ["iden"] = studioTheme:GetColor(Enum.StudioStyleGuideColor.ScriptText), ["keyword"] = studioTheme:GetColor(Enum.StudioStyleGuideColor.ScriptKeyword), ["builtin"] = studioTheme:GetColor(Enum.StudioStyleGuideColor.ScriptBuiltInFunction), ["string"] = studioTheme:GetColor(Enum.StudioStyleGuideColor.ScriptString), ["number"] = studioTheme:GetColor(Enum.StudioStyleGuideColor.ScriptNumber), ["comment"] = studioTheme:GetColor(Enum.StudioStyleGuideColor.ScriptComment), ["operator"] = studioTheme:GetColor(Enum.StudioStyleGuideColor.ScriptOperator), ["custom"] = studioTheme:GetColor(Enum.StudioStyleGuideColor.ScriptBool), } end Theme.setColors(getTokens()) studio.ThemeChanged:Connect(function() studioTheme = studio.Theme Theme.setColors(getTokens()) refreshCallback() end) end) return success end -- Initialize Theme.setColors(DEFAULT_TOKEN_COLORS) local theme = Theme --[=[ Lexical scanner for creating a sequence of tokens from Lua source code. This is a heavily modified and Roblox-optimized version of the original Penlight Lexer module: https://github.com/stevedonovan/Penlight Authors: stevedonovan <https://github.com/stevedonovan> ----------- Original Penlight lexer author ryanjmulder <https://github.com/ryanjmulder> ------------- Penlight lexer contributer mpeterv <https://github.com/mpeterv> --------------------- Penlight lexer contributer Tieske <https://github.com/Tieske> ----------------------- Penlight lexer contributer boatbomber <https://github.com/boatbomber> --------------- Roblox port, added builtin token, added patterns for incomplete syntax, bug fixes, behavior changes, token optimization, thread optimization Added lexer.navigator() for non-sequential reads Sleitnick <https://github.com/Sleitnick> ----------------- Roblox optimizations howmanysmall <https://github.com/howmanysmall> ----------- Lua + Roblox optimizations List of possible tokens: - iden - keyword - builtin - string - number - comment - operator --]=] local lexer = {} local Prefix, Suffix, Cleaner = "^[%c%s]*", "[%c%s]*", "[%c%s]+" local UNICODE = "[%z\x01-\x7F\xC2-\xF4][\x80-\xBF]+" local NUMBER_A = "0[xX][%da-fA-F_]+" local NUMBER_B = "0[bB][01_]+" local NUMBER_C = "%d+%.?%d*[eE][%+%-]?%d+" local NUMBER_D = "%d+[%._]?[%d_eE]*" local OPERATORS = "[:;<>/~%*%(%)%-={},%.#%^%+%%]+" local BRACKETS = "[%[%]]+" -- needs to be separate pattern from other operators or it'll mess up multiline strings local IDEN = "[%a_][%w_]*" local STRING_EMPTY = "(['\"])%1" --Empty String local STRING_PLAIN = "(['\"])[^\n]-([^\\]%1)" --TODO: Handle escaping escapes local STRING_INTER = "`[^\n]-`" local STRING_INCOMP_A = "(['\"]).-\n" --Incompleted String with next line local STRING_INCOMP_B = "(['\"])[^\n]*" --Incompleted String without next line local STRING_MULTI = "%[(=*)%[.-%]%1%]" --Multiline-String local STRING_MULTI_INCOMP = "%[=*%[.-.*" --Incompleted Multiline-String local COMMENT_MULTI = "%-%-%[(=*)%[.-%]%1%]" --Completed Multiline-Comment local COMMENT_MULTI_INCOMP = "%-%-%[=*%[.-.*" --Incompleted Multiline-Comment local COMMENT_PLAIN = "%-%-.-\n" --Completed Singleline-Comment local COMMENT_INCOMP = "%-%-.*" --Incompleted Singleline-Comment -- local TYPED_VAR = ":%s*([%w%?%| \t]+%s*)" --Typed variable, parameter, function local language = { keyword = { ["and"] = "keyword", ["break"] = "keyword", ["continue"] = "keyword", ["do"] = "keyword", ["else"] = "keyword", ["elseif"] = "keyword", ["end"] = "keyword", ["export"] = "keyword", ["false"] = "keyword", ["for"] = "keyword", ["function"] = "keyword", ["if"] = "keyword", ["in"] = "keyword", ["local"] = "keyword", ["nil"] = "keyword", ["not"] = "keyword", ["or"] = "keyword", ["repeat"] = "keyword", ["return"] = "keyword", ["self"] = "keyword", ["then"] = "keyword", ["true"] = "keyword", ["type"] = "keyword", ["typeof"] = "keyword", ["until"] = "keyword", ["while"] = "keyword", }, builtin = { -- Luau Functions ["assert"] = "function", ["error"] = "function", ["getfenv"] = "function", ["getmetatable"] = "function", ["ipairs"] = "function", ["loadstring"] = "function", ["newproxy"] = "function", ["next"] = "function", ["pairs"] = "function", ["pcall"] = "function", ["print"] = "function", ["rawequal"] = "function", ["rawget"] = "function", ["rawlen"] = "function", ["rawset"] = "function", ["select"] = "function", ["setfenv"] = "function", ["setmetatable"] = "function", ["tonumber"] = "function", ["tostring"] = "function", ["unpack"] = "function", ["xpcall"] = "function", -- Luau Functions (Deprecated) ["collectgarbage"] = "function", -- Luau Variables ["_G"] = "table", ["_VERSION"] = "string", -- Luau Tables ["bit32"] = "table", ["coroutine"] = "table", ["debug"] = "table", ["math"] = "table", ["os"] = "table", ["string"] = "table", ["table"] = "table", ["utf8"] = "table", -- Roblox Functions ["DebuggerManager"] = "function", ["delay"] = "function", ["gcinfo"] = "function", ["PluginManager"] = "function", ["require"] = "function", ["settings"] = "function", ["spawn"] = "function", ["tick"] = "function", ["time"] = "function", ["UserSettings"] = "function", ["wait"] = "function", ["warn"] = "function", -- Roblox Functions (Deprecated) ["Delay"] = "function", ["ElapsedTime"] = "function", ["elapsedTime"] = "function", ["printidentity"] = "function", ["Spawn"] = "function", ["Stats"] = "function", ["stats"] = "function", ["Version"] = "function", ["version"] = "function", ["Wait"] = "function", ["ypcall"] = "function", -- Roblox Variables ["game"] = "Instance", ["plugin"] = "Instance", ["script"] = "Instance", ["shared"] = "Instance", ["workspace"] = "Instance", -- Roblox Variables (Deprecated) ["Game"] = "Instance", ["Workspace"] = "Instance", -- Roblox Tables ["Axes"] = "table", ["BrickColor"] = "table", ["CatalogSearchParams"] = "table", ["CFrame"] = "table", ["Color3"] = "table", ["ColorSequence"] = "table", ["ColorSequenceKeypoint"] = "table", ["DateTime"] = "table", ["DockWidgetPluginGuiInfo"] = "table", ["Enum"] = "table", ["Faces"] = "table", ["FloatCurveKey"] = "table", ["Font"] = "table", ["Instance"] = "table", ["NumberRange"] = "table", ["NumberSequence"] = "table", ["NumberSequenceKeypoint"] = "table", ["OverlapParams"] = "table", ["PathWaypoint"] = "table", ["PhysicalProperties"] = "table", ["Random"] = "table", ["Ray"] = "table", ["RaycastParams"] = "table", ["Rect"] = "table", ["Region3"] = "table", ["Region3int16"] = "table", ["RotationCurveKey"] = "table", ["SharedTable"] = "table", ["task"] = "table", ["TweenInfo"] = "table", ["UDim"] = "table", ["UDim2"] = "table", ["Vector2"] = "table", ["Vector2int16"] = "table", ["Vector3"] = "table", ["Vector3int16"] = "table", }, libraries = { -- Luau Libraries bit32 = { arshift = "function", band = "function", bnot = "function", bor = "function", btest = "function", bxor = "function", countlz = "function", countrz = "function", extract = "function", lrotate = "function", lshift = "function", replace = "function", rrotate = "function", rshift = "function", }, coroutine = { close = "function", create = "function", isyieldable = "function", resume = "function", running = "function", status = "function", wrap = "function", yield = "function", }, debug = { dumpheap = "function", getmemorycategory = "function", info = "function", loadmodule = "function", profilebegin = "function", profileend = "function", resetmemorycategory = "function", setmemorycategory = "function", traceback = "function", }, math = { abs = "function", acos = "function", asin = "function", atan2 = "function", atan = "function", ceil = "function", clamp = "function", cos = "function", cosh = "function", deg = "function", exp = "function", floor = "function", fmod = "function", frexp = "function", ldexp = "function", log10 = "function", log = "function", max = "function", min = "function", modf = "function", noise = "function", pow = "function", rad = "function", random = "function", randomseed = "function", round = "function", sign = "function", sin = "function", sinh = "function", sqrt = "function", tan = "function", tanh = "function", huge = "number", pi = "number", }, os = { clock = "function", date = "function", difftime = "function", time = "function", }, string = { byte = "function", char = "function", find = "function", format = "function", gmatch = "function", gsub = "function", len = "function", lower = "function", match = "function", pack = "function", packsize = "function", rep = "function", reverse = "function", split = "function", sub = "function", unpack = "function", upper = "function", }, table = { clear = "function", clone = "function", concat = "function", create = "function", find = "function", foreach = "function", foreachi = "function", freeze = "function", getn = "function", insert = "function", isfrozen = "function", maxn = "function", move = "function", pack = "function", remove = "function", sort = "function", unpack = "function", }, utf8 = { char = "function", codepoint = "function", codes = "function", graphemes = "function", len = "function", nfcnormalize = "function", nfdnormalize = "function", offset = "function", charpattern = "string", }, -- Roblox Libraries Axes = { new = "function", }, BrickColor = { Black = "function", Blue = "function", DarkGray = "function", Gray = "function", Green = "function", new = "function", New = "function", palette = "function", Random = "function", random = "function", Red = "function", White = "function", Yellow = "function", }, CatalogSearchParams = { new = "function", }, CFrame = { Angles = "function", fromAxisAngle = "function", fromEulerAngles = "function", fromEulerAnglesXYZ = "function", fromEulerAnglesYXZ = "function", fromMatrix = "function", fromOrientation = "function", lookAt = "function", new = "function", identity = "CFrame", }, Color3 = { fromHex = "function", fromHSV = "function", fromRGB = "function", new = "function", toHSV = "function", }, ColorSequence = { new = "function", }, ColorSequenceKeypoint = { new = "function", }, DateTime = { fromIsoDate = "function", fromLocalTime = "function", fromUniversalTime = "function", fromUnixTimestamp = "function", fromUnixTimestampMillis = "function", now = "function", }, DockWidgetPluginGuiInfo = { new = "function", }, Enum = {}, Faces = { new = "function", }, FloatCurveKey = { new = "function", }, Font = { fromEnum = "function", fromId = "function", fromName = "function", new = "function", }, Instance = { new = "function", }, NumberRange = { new = "function", }, NumberSequence = { new = "function", }, NumberSequenceKeypoint = { new = "function", }, OverlapParams = { new = "function", }, PathWaypoint = { new = "function", }, PhysicalProperties = { new = "function", }, Random = { new = "function", }, Ray = { new = "function", }, RaycastParams = { new = "function", }, Rect = { new = "function", }, Region3 = { new = "function", }, Region3int16 = { new = "function", }, RotationCurveKey = { new = "function", }, SharedTable = { clear = "function", clone = "function", cloneAndFreeze = "function", increment = "function", isFrozen = "function", new = "function", size = "function", update = "function", }, task = { cancel = "function", defer = "function", delay = "function", desynchronize = "function", spawn = "function", synchronize = "function", wait = "function", }, TweenInfo = { new = "function", }, UDim = { new = "function", }, UDim2 = { fromOffset = "function", fromScale = "function", new = "function", }, Vector2 = { new = "function", one = "Vector2", xAxis = "Vector2", yAxis = "Vector2", zero = "Vector2", }, Vector2int16 = { new = "function", }, Vector3 = { fromAxis = "function", FromAxis = "function", fromNormalId = "function", FromNormalId = "function", new = "function", one = "Vector3", xAxis = "Vector3", yAxis = "Vector3", zAxis = "Vector3", zero = "Vector3", }, Vector3int16 = { new = "function", }, }, } -- Filling up language.libraries.Enum table local enumLibraryTable = language.libraries.Enum for _, enum in ipairs(Enum:GetEnums()) do --TODO: Remove tostring from here once there is a better way to get the name of an Enum enumLibraryTable[tostring(enum)] = "Enum" end local lang = language local lua_keyword = lang.keyword local lua_builtin = lang.builtin local lua_libraries = lang.libraries lexer.language = lang local lua_matches = { -- Indentifiers { Prefix .. IDEN .. Suffix, "var" }, -- Numbers { Prefix .. NUMBER_A .. Suffix, "number" }, { Prefix .. NUMBER_B .. Suffix, "number" }, { Prefix .. NUMBER_C .. Suffix, "number" }, { Prefix .. NUMBER_D .. Suffix, "number" }, -- Strings { Prefix .. STRING_EMPTY .. Suffix, "string" }, { Prefix .. STRING_PLAIN .. Suffix, "string" }, { Prefix .. STRING_INCOMP_A .. Suffix, "string" }, { Prefix .. STRING_INCOMP_B .. Suffix, "string" }, { Prefix .. STRING_MULTI .. Suffix, "string" }, { Prefix .. STRING_MULTI_INCOMP .. Suffix, "string" }, { Prefix .. STRING_INTER .. Suffix, "string_inter" }, -- Comments { Prefix .. COMMENT_MULTI .. Suffix, "comment" }, { Prefix .. COMMENT_MULTI_INCOMP .. Suffix, "comment" }, { Prefix .. COMMENT_PLAIN .. Suffix, "comment" }, { Prefix .. COMMENT_INCOMP .. Suffix, "comment" }, -- Operators { Prefix .. OPERATORS .. Suffix, "operator" }, { Prefix .. BRACKETS .. Suffix, "operator" }, -- Unicode { Prefix .. UNICODE .. Suffix, "iden" }, -- Unknown { "^.", "iden" }, } -- To reduce the amount of table indexing during lexing, we separate the matches now local PATTERNS, TOKENS = {}, {} for i, m in lua_matches do PATTERNS[i] = m[1] TOKENS[i] = m[2] end --- Create a plain token iterator from a string. -- @tparam string s a string. function lexer.scan(s: string) local index = 1 local size = #s local previousContent1, previousContent2, previousContent3, previousToken = "", "", "", "" local thread = coroutine.create(function() while index <= size do local matched = false for tokenType, pattern in ipairs(PATTERNS) do -- Find match local start, finish = string.find(s, pattern, index) if start == nil then continue end -- Move head index = finish + 1 matched = true -- Gather results local content = string.sub(s, start, finish) local rawToken = TOKENS[tokenType] local processedToken = rawToken -- Process token if rawToken == "var" then -- Since we merge spaces into the tok, we need to remove them -- in order to check the actual word it contains local cleanContent = string.gsub(content, Cleaner, "") if lua_keyword[cleanContent] then processedToken = "keyword" elseif lua_builtin[cleanContent] then processedToken = "builtin" elseif string.find(previousContent1, "%.[%s%c]*$") and previousToken ~= "comment" then -- The previous was a . so we need to special case indexing things local parent = string.gsub(previousContent2, Cleaner, "") local lib = lua_libraries[parent] if lib and lib[cleanContent] and not string.find(previousContent3, "%.[%s%c]*$") then -- Indexing a builtin lib with existing item, treat as a builtin processedToken = "builtin" else -- Indexing a non builtin, can't be treated as a keyword/builtin processedToken = "iden" end -- print("indexing",parent,"with",cleanTok,"as",t2) else processedToken = "iden" end elseif rawToken == "string_inter" then if not string.find(content, "[^\\]{") then -- This inter string doesnt actually have any inters processedToken = "string" else -- We're gonna do our own yields, so the main loop won't need to -- Our yields will be a mix of string and whatever is inside the inters processedToken = nil local isString = true local subIndex = 1 local subSize = #content while subIndex <= subSize do -- Find next brace local subStart, subFinish = string.find(content, "^.-[^\\][{}]", subIndex) if subStart == nil then -- No more braces, all string coroutine.yield("string", string.sub(content, subIndex)) break end if isString then -- We are currently a string subIndex = subFinish + 1 coroutine.yield("string", string.sub(content, subStart, subFinish)) -- This brace opens code isString = false else -- We are currently in code subIndex = subFinish local subContent = string.sub(content, subStart, subFinish - 1) for innerToken, innerContent in lexer.scan(subContent) do coroutine.yield(innerToken, innerContent) end -- This brace opens string/closes code isString = true end end end end -- Record last 3 tokens for the indexing context check previousContent3 = previousContent2 previousContent2 = previousContent1 previousContent1 = content previousToken = processedToken or rawToken if processedToken then coroutine.yield(processedToken, content) end break end -- No matches found if not matched then return end end -- Completed the scan return end) return function() if coroutine.status(thread) == "dead" then return end local success, token, content = coroutine.resume(thread) if success and token then return token, content end return end end function lexer.navigator() local nav = { Source = "", TokenCache = table.create(50), _RealIndex = 0, _UserIndex = 0, _ScanThread = nil, } function nav:Destroy() self.Source = nil self._RealIndex = nil self._UserIndex = nil self.TokenCache = nil self._ScanThread = nil end function nav:SetSource(SourceString) self.Source = SourceString self._RealIndex = 0 self._UserIndex = 0 table.clear(self.TokenCache) self._ScanThread = coroutine.create(function() for Token, Src in lexer.scan(self.Source) do self._RealIndex += 1 self.TokenCache[self._RealIndex] = { Token, Src } coroutine.yield(Token, Src) end end) end function nav.Next() nav._UserIndex += 1 if nav._RealIndex >= nav._UserIndex then -- Already scanned, return cached return table.unpack(nav.TokenCache[nav._UserIndex]) else if coroutine.status(nav._ScanThread) == "dead" then -- Scan thread dead return else local success, token, src = coroutine.resume(nav._ScanThread) if success and token then -- Scanned new data return token, src else -- Lex completed return end end end end function nav.Peek(PeekAmount) local GoalIndex = nav._UserIndex + PeekAmount if nav._RealIndex >= GoalIndex then -- Already scanned, return cached if GoalIndex > 0 then return table.unpack(nav.TokenCache[GoalIndex]) else -- Invalid peek return end else if coroutine.status(nav._ScanThread) == "dead" then -- Scan thread dead return else local IterationsAway = GoalIndex - nav._RealIndex local success, token, src = nil, nil, nil for _ = 1, IterationsAway do success, token, src = coroutine.resume(nav._ScanThread) if not (success or token) then -- Lex completed break end end return token, src end end end return nav end local Highlighter = { defaultLexer = lexer :: Lexer, _textObjectData = {} :: { [TextObject]: ObjectData }, _cleanups = {} :: { [TextObject]: () -> () }, } --[[ Gathers the info that is needed in order to set up a line label. ]] function Highlighter._getLabelingInfo(textObject: TextObject) local data = Highlighter._textObjectData[textObject] if not data then return end local src = utility.convertTabsToSpaces(utility.removeControlChars(textObject.Text)) local numLines = #string.split(src, "\n") if numLines == 0 then return end local textBounds = utility.getTextBounds(textObject) local textHeight = textBounds.Y / numLines return { data = data, numLines = numLines, textBounds = textBounds, textHeight = textHeight, innerAbsoluteSize = utility.getInnerAbsoluteSize(textObject), textColor = theme.getColor("iden"), textFont = textObject.FontFace, textSize = textObject.TextSize, labelSize = UDim2.new(1, 0, 0, math.ceil(textHeight)), } end --[[ Aligns and matches the line labels to the textObject. ]] function Highlighter._alignLabels(textObject: TextObject) local labelingInfo = Highlighter._getLabelingInfo(textObject) if not labelingInfo then return end for lineNumber, lineLabel in labelingInfo.data.Labels do -- Align line label lineLabel.TextColor3 = labelingInfo.textColor lineLabel.FontFace = labelingInfo.textFont lineLabel.TextSize = labelingInfo.textSize lineLabel.Size = labelingInfo.labelSize lineLabel.Position = UDim2.fromScale(0, labelingInfo.textHeight * (lineNumber - 1) / labelingInfo.innerAbsoluteSize.Y) end end --[[ Creates and populates the line labels with the appropriate rich text. ]] function Highlighter._populateLabels(props: HighlightProps) -- Gather props local textObject = props.textObject local src = utility.convertTabsToSpaces(utility.removeControlChars(props.src or textObject.Text)) local lexer = props.lexer or Highlighter.defaultLexer local customLang = props.customLang local forceUpdate = props.forceUpdate -- Avoid updating when unnecessary local data = Highlighter._textObjectData[textObject] if (data == nil) or (data.Text == src) then if forceUpdate ~= true then return end end -- Ensure textObject matches sanitized src textObject.Text = src local lineLabels = data.Labels local previousLines = data.Lines local lines = string.split(src, "\n") data.Lines = lines data.Text = src data.Lexer = lexer data.CustomLang = customLang -- Shortcut empty textObjects if src == "" then for l = 1, #lineLabels do if lineLabels[l].Text == "" then continue end lineLabels[l].Text = "" end return end local idenColor = theme.getColor("iden") local labelingInfo = Highlighter._getLabelingInfo(textObject) local richTextBuffer, bufferIndex, lineNumber = table.create(5), 0, 1 for token: TokenName, content: string in lexer.scan(src) do local Color = if customLang and customLang[content] then theme.getColor("custom") else theme.getColor(token) or idenColor local tokenLines = string.split(utility.sanitizeRichText(content), "\n") for l, tokenLine in tokenLines do -- Find line label local lineLabel = lineLabels[lineNumber] if not lineLabel then local newLabel = Instance.new("TextLabel") newLabel.Name = "Line_" .. lineNumber newLabel.AutoLocalize = false newLabel.RichText = true newLabel.BackgroundTransparency = 1 newLabel.Text = "" newLabel.TextXAlignment = Enum.TextXAlignment.Left newLabel.TextYAlignment = Enum.TextYAlignment.Top newLabel.TextColor3 = labelingInfo.textColor newLabel.FontFace = labelingInfo.textFont newLabel.TextSize = labelingInfo.textSize newLabel.Size = labelingInfo.labelSize newLabel.Position = UDim2.fromScale(0, labelingInfo.textHeight * (lineNumber - 1) / labelingInfo.innerAbsoluteSize.Y) newLabel.Parent = textObject.SyntaxHighlights lineLabels[lineNumber] = newLabel lineLabel = newLabel end -- If multiline token, then set line & move to next if l > 1 then if forceUpdate or lines[lineNumber] ~= previousLines[lineNumber] then -- Set line lineLabels[lineNumber].Text = table.concat(richTextBuffer) end -- Move to next line lineNumber += 1 bufferIndex = 0 table.clear(richTextBuffer) end -- If changed, add token to line if forceUpdate or lines[lineNumber] ~= previousLines[lineNumber] then bufferIndex += 1 -- Only add RichText tags when the color is non-default and the characters are non-whitespace if Color ~= idenColor and string.find(tokenLine, "[%S%C]") then richTextBuffer[bufferIndex] = theme.getColoredRichText(Color, tokenLine) else richTextBuffer[bufferIndex] = tokenLine end end end end -- Set final line if richTextBuffer[1] and lineLabels[lineNumber] then lineLabels[lineNumber].Text = table.concat(richTextBuffer) end -- Clear unused line labels for l = lineNumber + 1, #lineLabels do if lineLabels[l].Text == "" then continue end lineLabels[l].Text = "" end end --[[ Highlights the given textObject with the given props and returns a cleanup function. Highlighting will automatically update when needed, so the cleanup function will disconnect those connections and remove all labels. ]] function Highlighter.highlight(props: HighlightProps): () -> () -- Gather props local textObject = props.textObject local src = utility.convertTabsToSpaces(utility.removeControlChars(props.src or textObject.Text)) local lexer = props.lexer or Highlighter.defaultLexer local customLang = props.customLang -- Avoid updating when unnecessary if Highlighter._cleanups[textObject] then -- Already been initialized, so just update Highlighter._populateLabels(props) Highlighter._alignLabels(textObject) return Highlighter._cleanups[textObject] end -- Ensure valid object properties textObject.RichText = false textObject.Text = src textObject.TextXAlignment = Enum.TextXAlignment.Left textObject.TextYAlignment = Enum.TextYAlignment.Top textObject.BackgroundColor3 = theme.getColor("background") textObject.TextColor3 = theme.getColor("iden") textObject.TextTransparency = 0.5 -- Build the highlight labels local lineFolder = textObject:FindFirstChild("SyntaxHighlights") if lineFolder == nil then local newLineFolder = Instance.new("Folder") newLineFolder.Name = "SyntaxHighlights" newLineFolder.Parent = textObject lineFolder = newLineFolder end local data = { Text = "", Labels = {}, Lines = {}, Lexer = lexer, CustomLang = customLang, } Highlighter._textObjectData[textObject] = data -- Add a cleanup handler for this textObject local connections: { [string]: RBXScriptConnection } = {} local function cleanup() lineFolder:Destroy() Highlighter._textObjectData[textObject] = nil Highlighter._cleanups[textObject] = nil for _key, connection in connections do connection:Disconnect() end table.clear(connections) end Highlighter._cleanups[textObject] = cleanup connections["AncestryChanged"] = textObject.AncestryChanged:Connect(function() if textObject.Parent then return end cleanup() end) connections["TextChanged"] = textObject:GetPropertyChangedSignal("Text"):Connect(function() Highlighter._populateLabels(props) end) connections["TextBoundsChanged"] = textObject:GetPropertyChangedSignal("TextBounds"):Connect(function() Highlighter._alignLabels(textObject) end) connections["AbsoluteSizeChanged"] = textObject:GetPropertyChangedSignal("AbsoluteSize"):Connect(function() Highlighter._alignLabels(textObject) end) connections["FontFaceChanged"] = textObject:GetPropertyChangedSignal("FontFace"):Connect(function() Highlighter._alignLabels(textObject) end) -- Populate the labels Highlighter._populateLabels(props) Highlighter._alignLabels(textObject) return cleanup end --[[ Refreshes all highlighted textObjects. Used when the theme changes. ]] function Highlighter.refresh(): () -- Rehighlight existing labels using latest colors for textObject, data in Highlighter._textObjectData do for _, lineLabel in data.Labels do lineLabel.TextColor3 = theme.getColor("iden") end Highlighter.highlight({ textObject = textObject, forceUpdate = true, src = data.Text, lexer = data.Lexer, customLang = data.CustomLang, }) end end --[[ Sets the token colors to the given colors and refreshes all highlighted textObjects. ]] function Highlighter.setTokenColors(colors: TokenColors): () theme.setColors(colors) Highlighter.refresh() end --[[ Gets a token color by name. Mainly useful for setting "background" token color on other UI objects behind your text. ]] function Highlighter.getTokenColor(tokenName: TokenName): Color3 return theme.getColor(tokenName) end --[[ Matches the token colors to the Studio theme settings and refreshes all highlighted textObjects. Does nothing when not run in a Studio plugin. ]] function Highlighter.matchStudioSettings(): () local applied = theme.matchStudioSettings(Highlighter.refresh) if applied then Highlighter.refresh() end end local Part = Instance.new("Part") Part.Transparency = 0 Part.Size = Vector3.new(17, 10, 0.1) Part.CanCollide = false Part.CanQuery = false Part.Massless = true Part.CanTouch = false Part.Parent = script local Attachment = Instance.new("Attachment", Part) local Linear = Instance.new("LinearVelocity") Linear.VectorVelocity = Vector3.zero Linear.MaxForce = math.huge Linear.Attachment0 = Attachment Linear.RelativeTo = Enum.ActuatorRelativeTo.World Linear.VelocityConstraintMode = Enum.VelocityConstraintMode.Vector Linear.Parent = Attachment Part:SetNetworkOwner(owner) local SGUI = Instance.new("SurfaceGui") SGUI.LightInfluence = 0 SGUI.SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud SGUI.PixelsPerStud = 300 SGUI.Face = Enum.NormalId.Back SGUI.Parent = Part local Elements = {} function Element(Class, Properties, Children) local Inst = Instance.new(Class) for i, v in Properties do Inst[i] = v end if Children then for i, v in Children do if not tonumber(i) then Elements[i] = v end v.Parent = Inst end end return Inst end local Client = NLS([[ local Part = script.Value.Value local Player = game.Players.LocalPlayer local Character = Player.Character game:GetService("RunService").RenderStepped:Connect(function() local Root = Character:FindFirstChild("HumanoidRootPart") if Root then Part.CFrame = Root.CFrame * CFrame.new(0, Part.Size.Y * 0.5, -7) end end) ]], owner.PlayerGui) local ObjectValue = Instance.new("ObjectValue") ObjectValue.Value = Part ObjectValue.Parent = Client local URL = "http://109.148.160.223:8080" local S = 6 local Background = Element("Frame", { Size = UDim2.fromScale(1, 1), BackgroundColor3 = Color3.new(0.15, 0.15, 0.15), Parent = SGUI }, { Element("UIPadding", { PaddingBottom = UDim.new(0, 9 * S), PaddingLeft = UDim.new(0, 9 * S), PaddingRight = UDim.new(0, 9 * S), PaddingTop = UDim.new(0, 9 * S), }), Inner = Element("Frame", { BorderSizePixel = S, BorderColor3 = Color3.new(0.18, 0.18, 0.18), BackgroundColor3 = Color3.new(0.2, 0.2, 0.2), Size = UDim2.fromScale(1, 1) }, { LineCount = Element("TextLabel", { BorderSizePixel = 0, Size = UDim2.new(0, 40 * S, 1, 0), Font = "Code", Text = "1", TextSize = 24 * S, TextColor3 = Color3.new(0.8, 0.8, 0.8), TextYAlignment = "Top", BackgroundColor3 = Color3.new(0.18, 0.18, 0.18) }), MainText = Element("TextLabel", { Size = UDim2.new(1, -45 * S, 1, 0), Position = UDim2.fromOffset(45 * S, 0), BorderSizePixel = 0, TextYAlignment = "Top", TextXAlignment = "Left", TextColor3 = Color3.new(0.8, 0.8, 0.8), RichText = true, TextSize = 24 * S, Text = "warn('hi')", Font = "Code", BackgroundTransparency = 1 }) }) }) Highlighter.highlight({ textObject = Elements.MainText, }) while true do local Data = game.HttpService:GetAsync(URL .. "/get") Elements.MainText.Text = Data print("Updated") task.wait(1.5) end
Editor Settings
Theme
Key bindings
Full width
Lines