✨ Initial Commit ✨
This commit is contained in:
commit
1be4d8db49
10 changed files with 262 additions and 0 deletions
0
.luarc.json
Normal file
0
.luarc.json
Normal file
48
readme.md
Normal file
48
readme.md
Normal 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
3
spec/fixtures/files/basic.oats
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[person]
|
||||
[name]
|
||||
[age]
|
5
spec/fixtures/files/bob.oats
vendored
Normal file
5
spec/fixtures/files/bob.oats
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
[person]
|
||||
[name]
|
||||
Bob
|
||||
[age]
|
||||
20
|
6
spec/fixtures/files/deep.oats
vendored
Normal file
6
spec/fixtures/files/deep.oats
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
[first]
|
||||
[second]
|
||||
[third]
|
||||
[second]
|
||||
[third]
|
||||
[third]
|
12
spec/fixtures/files/document.oats
vendored
Normal file
12
spec/fixtures/files/document.oats
vendored
Normal 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
3
spec/fixtures/files/person.oats
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[person]
|
||||
[name]
|
||||
[age]
|
3
spec/fixtures/files/rose.oats
vendored
Normal file
3
spec/fixtures/files/rose.oats
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[person]
|
||||
[name] Rose
|
||||
[age] 22
|
52
spec/oats_spec.moon
Normal file
52
spec/oats_spec.moon
Normal 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
130
src/oats.lua
Normal 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
|
Loading…
Reference in a new issue