require("nellie.interpreter") -- A stream of text (kind of) -- with some helping methods Reader = class("Reader") -- Construct a reader from function Reader:new(text) assert_type("string")(text,"Arg 'text' #1: %s") self.text = text end -- Get the substring of the readers text in a bounds function Reader:get(first,final) assert_type("number")(first,"Arg 'first' #1: %s") assert_type("number","nil")(final,"Arg 'final' #2: %s") return self.text:sub(first,final) end -- Match on the reader's text from an index function Reader:find(pattern,init) assert_type("string")(pattern,"Arg 'pattern' #1: %s") assert_type("number","nil")(init,"Arg 'init' #2: %s") return self.text:find(pattern,init) end function Reader:match(pattern,init) assert_type("string")(pattern,"Arg 'pattern' #1: %s") assert_type("number")(init,"Arg 'init' #2: %s") return self.text:match(pattern,init) end -- A range of text being considered -- like a cursor Range = class("Range") -- Construct a range of text from a reader and bounds function Range:new(reader,first,final) assert_meta(Reader)(reader) assert_type("number","nil")(first,"Arg 'first' #1: %s") assert_type("number","nil")(final,"Arg 'final' #2: %s") self.reader = reader self.first = first self.final = final end function Range:text() return self.reader:get(self.first or 1,self.final) end function Range:find(pattern) return self.reader:find(pattern,self.first) end function Range:move(first,final) assert_type("number","nil")(first,"Arg 'first' #1: %s") assert_type("number","nil")(final,"Arg 'final' #2: %s") self.first = first self.final = final -- note that this function returns itself return self end --[[ A class which parses text using the 'consumer-meal' model. ]] Consumer = class("Consumer") function Consumer:new(content) self.range = Range(Reader(content)) self.col = 1 self.row = 1 end function Consumer:remaining() return self.range:text() end function Consumer:mark() end -- From a table of actions of patterns, -- consume the earliest pattern (index) -- and call the value function Consumer:consumePatterns(patterns,state) -- t: {string = function...} assert_type("table")(patterns,"Arg 't' #1: %s") if not next(patterns) then error("The match table cannot be empty!",2) end for index,func in pairs(patterns) do assert_type("string")(index,"Bad 't' index: %s") assert_type("function")(func,"Bad 't' item: %s") end local origin = self.range.first or 1 -- Get the next earliest match local findex,ffinal,fpattern,ffunc,fexcess -- TODO: make this not be a bunch of locals. New class? for pattern,new_func in pairs(patterns) do local new_index,new_final = self.range:find(pattern) if new_index and ((not findex) or (findex > new_index)) then if not new_index then return end findex = new_index ffinal = new_final fpattern = pattern ffunc = new_func fexcess = self.range.reader:get( self.range.first or 1, new_final - 1 ) end end -- Pass into the func if not ffunc then return nil end assert(findex) assert(ffinal) self.range:move(ffinal+1) -- Move range to after the match local fmatch = self.range.reader:match(fpattern,findex) -- Pass back consumed result to local sum = self.range.reader:get(origin+1,self.range.first) :gsub("\r\n","\n") :gsub("\r","\n") for char in sum:gmatch(".") do if char == "\n" then self.row = self.row + 1 self.col = 1 else self.col = self.col + 1 end end return ffunc(fexcess,fmatch,state) end -- Interpret some text function Parse(content,uri,chain) -- LOCALS -- So that all functions have access to eachother local consume, consumeExpression, consumeBlock local consumer = Consumer(content) local function expression(...) return Expression(...):locate( string.format( "%i:%i", consumer.row, consumer.col ),uri) end local baseExpression = expression() local patterns = {} patterns.contentClose = "$" -- The pattern for words inbetween expressions patterns.expressionWord = "(.*)" -- The patterns to open and close expressions, -- the words scattered inbetween them will be matches patterns.expressionOpen = "%(" patterns.expressionClose = "%)" patterns.subExpressionOpen = "%:" patterns.subExpressionClose = "%," patterns.blockOpen = "%{" patterns.blockClose = "%}" patterns.blockDelimiter = "[;]+" patterns.newLine = "[\n\r]+" patterns.singleLineString = "\"" patterns.named = "'" patterns.multiLineStringOpen = "%[%[" patterns.multiLineStringClose = "%]%]" patterns.uriOpen = "<" patterns.uriClose = ">" patterns.singleLineComment = "//" patterns.multiLineCommentOpen = "/%*" patterns.multiLineCommentOpen = "%*/" patterns.colonSyntax = ":" -- TODO: I don't like that this structure does: open(stuff;close) instead of open(stuff)close -- TODO: current:insert() is repeated in EVERY meal. I wonder if this should be fixed -__- local expressionMeals = { [patterns.singleLineString] = function(words,_,current) current:insert(words,consumer:consumePatterns({ [patterns.singleLineString] = function(words,_) return expression({"text",Expression({words})}) end })) end, [patterns.named] = function(words,_,current) current:insert(words,consumer:consumePatterns({ [patterns.named] = function(words,_) return expression({"the",Expression({words})}) end })) end, [patterns.multiLineStringOpen] = function(words,_,current) current:insert(words,consumer:consumePatterns({ ["[\n\r]"] = function() error("Incomplete string literal") end, [patterns.multiLineStringClose] = function(words,_) return expression({"text",Expression({words})}) end })) end, [patterns.uriOpen] = function(words,_,current) current:insert(words,consumer:consumePatterns({ ["[\n\r]"] = function() current:error("Incomplete URI literal") end, [patterns.uriClose] = function(path) return read(path) end })) end, [patterns.expressionOpen] = function(words,_,current) current:insert(words,consumeExpression()) end, [patterns.singleLineComment] = function(words,_,current) --current:insert(words) -- Consume what was left consumer:consumePatterns({ [patterns.newLine] = function() end }) end, [patterns.multiLineCommentOpen] = function(words,_,current) current:insert(words) consumer:consumePatterns({ [patterns.multiLineCommentClose] = function() end }) end, [patterns.colonSyntax] = function(words,_,current) current:insert(words,consumeExpression()) end, [patterns.blockOpen] = function(words,_,current) current:insert(words,consumeBlock()) end, } -- Consume a {} block of code function consumeBlock(closing_pattern) closing_pattern = closing_pattern or patterns.blockClose local expressions = {} local current = expression() local loop = true while loop do local remaining = consumer:remaining() local expr = consumer:consumePatterns(union(expressionMeals,{ [patterns.blockDelimiter] = function(words,_) current:insert(words) if current:empty() then --error("Extravenous semicolon.") else table.insert(expressions,current) end current = expression() end, [closing_pattern] = function(words,_) current:insert(words) loop = false end, }),current) end if #current.items ~= 0 then table.insert(expressions,current) end return expression(expressions) end function consumeExpression() local current = expression() -- Loop, adding new expressions and words, -- until closing that is local loop = true while loop do local remaining = consumer:remaining() local expr = consumer:consumePatterns(union(expressionMeals,{ [patterns.expressionClose] = function(words,_,_current) current:insert(words) loop = false end }),current) -- This single line below made me procrastinate life for a total of 4 hours on instagram reels; lock in. -- if not expr then print("brk") break end -- Since now closing end return current end return consumeBlock(patterns.contentClose) -- TODO: check for later expressions please? end