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 local NONE = {} 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 callback = nil local earliest = {} --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 earliest.index) or (earliest.index > new_index)) then if not new_index then return end -- (later:) what why? earliest.index = new_index earliest.final = new_final earliest.pattern = pattern callback = new_func earliest.excess = self.range.reader:get( self.range.first or 1, earliest.index - 1 ) end end -- Pass into the func if not callback then if patterns[NONE] then return patterns[NONE]() end return nil end assert(earliest.index) assert(earliest.final) self.range:move(earliest.final+1) -- Move range to after the match local fmatch = self.range.reader:match(earliest.pattern,earliest.final) -- This seems to be broken: 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 callback(earliest,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.multiLineCommentClose = "%*/" patterns.colonSyntax = ":" -- Give me: {patterns that take (match,state)}, the state, and a handler for comments -- I will give you a function that will return the result of consumePatterns(patterns) -- With escapes handled. local function makeConsumerEscaping(patterns,state,handler) return function() handler = handler or function() end local escaped = true local function escape() escaped = true end while escaped do escaped = false consumer:consumePatterns(union({ [patterns.singleLineComment] = function(...) handler(...) consumer:consumePatterns({ [patterns.newLine] = escape }) end, [patterns.multiLineCommentOpen] = function(...) handler(...) consumer:consumePatterns({ [NONE] = error, [patterns.multiLineCommentClose] = escape }) end, },patterns),state) end end end -- Give me {patterns that take (match,current)} and current (expression) local function makeConsumerAppending(patterns,state) local appendingPatterns = map(patterns,function(func) return function(match,current) current:insert(match.excess,func(match,current)) end end) makeConsumerEscaping(appendingPatterns,function(match,current) current:insert(match.excess) end) end local function makeConsumerExpressive() local function consumeSingleLineString() return end local function consumeNamed() end local function consumeMultiLineString() end local function consumeURI() end return makeConsumerAppending({ [patterns.singleLineString] = function(match,current) return consumer:consumePatterns({ ["[\n\r]"] = function() error("Incomplete string literal") end, [patterns.singleLineString] = function(words,_) return expression({"text",Expression({words})}) end }) end, [patterns.named] = function(words,_,current) current:insert(words,consumePatternsEscaping({ [patterns.named] = function(words,_) return expression({"the",Expression({words})}) end })) end, [patterns.multiLineStringOpen] = function(match,current) return consumer:consumePatterns({ [patterns.multiLineStringClose] = function(words,_) return expression({"text",Expression({words})}) end }) end, [patterns.multiLineStringOpen] = function(words,_,current) current:insert(words,consumePatternsEscaping({ [patterns.multiLineStringClose] = function(words,_) return expression({"text",Expression({words})}) end })) end, [patterns.uriOpen] = function(words,_,current) current:insert() current:insert(words,consumePatternsEscaping({ ["[\n\r]"] = function() current:error("Incomplete URI literal") end, [patterns.uriClose] = function(path) return read(path) end })) end, [patterns.expressionOpen] = function(match,current) return consumeExpression() end, [patterns.colonSyntax] = function(match,current) return consumeExpression() end, [patterns.blockOpen] = function(match,current) return consumeBlock() end, }) end local function consumePatternsExpressive(patterns,current) return consumePatternsEscaping 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 = consumePatternsEscaping(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 = consumePatternsEscaping(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