✨ 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