Initial Commit

This commit is contained in:
Talia 2024-11-20 21:33:22 +01:00
commit 1be4d8db49
Signed by: darkwiiplayer
GPG key ID: 7808674088232B3E
10 changed files with 262 additions and 0 deletions

0
.luarc.json Normal file
View file

48
readme.md Normal file
View file

@ -0,0 +1,48 @@
# OATS
**O**utput **A**gnostic **T**agging **S**ystem aka. OATS implemented in Lua.
## Format
### Overview
- Tree structure like XML
- No attributes, only children
- No namespaces
- Nesting by Indentation
Nodes are enclosed with square brackets
```
[document]
```
Nested elements are indented
```
[document]
[nested-tag]
nested text
```
Multi-line text nodes are yet to be decided. As of now, the options are:
1. Consecutive non-empty lines of text are merged with a space
2. Any non-empty text line is always a single text element
**Note**: Consumers may have a better understanding of whether and how to join text
elements together, while the interpreter would have to decide on a joining
strategy (most likely concatenation with a space character in between).
### Conventions
OATS is a very simple format without many restrictions.
Nevertheless, the following suggestions are provided to ensure some reasonable
degree of uniformity between applications:
OATS tag names preserve case, but applications consuming OATS structures should
generally ignore case.
Tag names should use lowercase kebab-case.
## Interface

3
spec/fixtures/files/basic.oats vendored Normal file
View file

@ -0,0 +1,3 @@
[person]
[name]
[age]

5
spec/fixtures/files/bob.oats vendored Normal file
View file

@ -0,0 +1,5 @@
[person]
[name]
Bob
[age]
20

6
spec/fixtures/files/deep.oats vendored Normal file
View file

@ -0,0 +1,6 @@
[first]
[second]
[third]
[second]
[third]
[third]

12
spec/fixtures/files/document.oats vendored Normal file
View file

@ -0,0 +1,12 @@
[document]
[section]
[title] Document
Paragraph
Multiline
Paragraph
Text with a [nested nested] node
Text with an [nested] empty nested node

3
spec/fixtures/files/person.oats vendored Normal file
View file

@ -0,0 +1,3 @@
[person]
[name]
[age]

3
spec/fixtures/files/rose.oats vendored Normal file
View file

@ -0,0 +1,3 @@
[person]
[name] Rose
[age] 22

52
spec/oats_spec.moon Normal file
View file

@ -0,0 +1,52 @@
oats = require "oats"
describe "OATS", ->
it "parses a basic file", ->
assert.same {{name: "person", {name: "name"}, {name: "age"}}}, oats.decodefile("spec/fixtures/files/basic.oats")
deep = {
name: "first"
{
name: "second"
{ name: "third" }
}
{
name: "second",
{ name: "third" }
{ name: "third" }
}
}
assert.same {deep}, oats.decodefile("spec/fixtures/files/deep.oats")
it "parses basic text nodes", ->
bob = {name: "person", {name: "name", "Bob"}, {name: "age", "20"}}
assert.same {bob}, oats.decodefile("spec/fixtures/files/bob.oats")
it "parses one-line nodes", ->
rose = {name: "person", {name: "name", "Rose"}, {name: "age", "22"}}
assert.same {rose}, oats.decodefile("spec/fixtures/files/rose.oats")
pending "parses inline nodes", ->
document = {
name: "document"
{name: "title", "Document"}
"Paragraph"
"Multiline Paragraph"
"Text with a"
{name: "nested", "nested"}
"node"
"Text with an"
{name: "nested"}
"empty nested node"
}
assert.same {document}, oats.decodefile("spec/fixtures/files/document.oats")
it "parses strings files", ->
assert.same {{name: "tester"}}, oats.decode("[tester]")
assert.same {{name: "tester", {name: "nested", "text"}}}, oats.decode("[tester]\n\t[nested] text")
it "errors when indentation increases by more than one", ->
assert.error ->
oats.decode("[outer]\n\t\t[nested]")
it "ignores empty lines", ->
assert.same {{name: "tester", {name: "nested", "text"}}}, oats.decode("[tester]\n\n\t[nested] text")

130
src/oats.lua Normal file
View file

@ -0,0 +1,130 @@
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
--- @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)
callback("text", text)
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)
callback("text", text)
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