local oats = {} --- @class tag --- @field name string --- @overload fun(table): tag local tag = setmetatable({}, {__call = function(self, new) if not new.name then error("Attempting to create a tag without a name", 2) end setmetatable(new, self) end}) tag.__index = tag local t1 = tag { name = "tester" } --- @alias node string|tag --- @alias callback fun(event: "text"|"open"|"close"|"warn", content: string|nil): nil --- @param callback callback --- @param last number --- @param current number --- @param number number local function handledepth(callback, last, current, number) for _ = current, last do callback("close") end if current > last+1 then error(string.format("Line %i: Indentation increased by more than one level: %i -> %i", number, last, current)) end end local function parsetext(callback, text, number) local start = 1 local inline while true do inline = text:find("[", start, true) if inline then callback("text", text:sub(start, inline - 1)) local close = text:find("]", inline, true) if close then local inline_content = text:sub(inline+1, close-1) local inline_name, inline_text inline_name, inline_text = inline_content:match("^([^%s]+)%s+(.+)$") if not inline_name then inline_name = inline_content end callback("open", inline_name) if inline_text then callback("text", inline_text) end callback("close") start = close + 1 else error(string.format("Endless inline tag on %i:%i", number, inline)) end else break end end callback("text", (start == 1) and text or text:sub(start)) end --- @param callback callback --- @param line string --- @param number number Line number currently being processed local function parseline(callback, line, lastdepth, number) local depth, name, text depth, name = line:match("^\t*()%[([^%s]+)%]$") if depth then handledepth(callback, lastdepth, depth, number) callback("open", name) return depth end depth, name, text = line:match("^\t*()%[([^%s]+)%]%s*(.*)$") if depth then handledepth(callback, lastdepth, depth, number) callback("open", name) parsetext(callback, text, number) callback("close") return depth-1 -- Minus one because the tag is already closed end if line:match("^%s*$") then return lastdepth end depth, text = line:match("^\t*()(.*)$") if depth then handledepth(callback, lastdepth, depth, number) parsetext(callback, text, number) return depth-1 -- Minus one because text doesn't get closed end end --- Decodes a stream of lines --- @param callback callback function oats.parse(callback, ...) local line = 1 local depth = 0 for content in ... do depth = parseline(callback, content, depth, line) line = line + 1 end end --- @return (node)[] --- @return callback local function consumer(document) document = document or {} local current = document local path = {current} local function callback(event, content) if event == "text" then table.insert(current, content) elseif event == "open" then local next = {name=content} table.insert(current, next) table.insert(path, next) current = next elseif event == "close" then local i = #path path[i] = nil current = path[i-1] end end return document, callback end --- Decodes a string --- @param input string An entire OATS document contained in a string --- @return (node)[] # A list of top-level elements --- @overload fun(path:string): nil,string function oats.decode(input) local document, callback = consumer() oats.parse(callback, input:gmatch("[^\n]+")) return document end --- Decodes a file --- @param path string Path to the file to decode --- @return (node)[] # A list of top-level elements --- @overload fun(path:string): nil,string function oats.decodefile(path) local file, err = io.open(path) if not file then err = err or "Unknown error opening file" return nil, err end local document, callback = consumer() oats.parse(callback, file:lines()) return document end return oats