2024-11-20 20:33:22 +00:00
|
|
|
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
|
|
|
|
|
2024-12-20 22:21:46 +00:00
|
|
|
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
|
2025-03-12 10:56:11 +00:00
|
|
|
callback("text", (start == 1) and text or text:sub(start))
|
2024-12-20 22:21:46 +00:00
|
|
|
end
|
|
|
|
|
2024-11-20 20:33:22 +00:00
|
|
|
--- @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)
|
2024-12-20 22:21:46 +00:00
|
|
|
parsetext(callback, text, number)
|
2024-11-20 20:33:22 +00:00
|
|
|
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)
|
2024-12-20 22:21:46 +00:00
|
|
|
parsetext(callback, text, number)
|
2024-11-20 20:33:22 +00:00
|
|
|
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
|