From 1be4d8db4905a29027c92bdfeebec327a96f9ed3 Mon Sep 17 00:00:00 2001 From: DarkWiiPlayer Date: Wed, 20 Nov 2024 21:33:22 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Initial=20Commit=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .luarc.json | 0 readme.md | 48 +++++++++++ spec/fixtures/files/basic.oats | 3 + spec/fixtures/files/bob.oats | 5 ++ spec/fixtures/files/deep.oats | 6 ++ spec/fixtures/files/document.oats | 12 +++ spec/fixtures/files/person.oats | 3 + spec/fixtures/files/rose.oats | 3 + spec/oats_spec.moon | 52 ++++++++++++ src/oats.lua | 130 ++++++++++++++++++++++++++++++ 10 files changed, 262 insertions(+) create mode 100644 .luarc.json create mode 100644 readme.md create mode 100644 spec/fixtures/files/basic.oats create mode 100644 spec/fixtures/files/bob.oats create mode 100644 spec/fixtures/files/deep.oats create mode 100644 spec/fixtures/files/document.oats create mode 100644 spec/fixtures/files/person.oats create mode 100644 spec/fixtures/files/rose.oats create mode 100644 spec/oats_spec.moon create mode 100644 src/oats.lua diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..e69de29 diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..f75dd9a --- /dev/null +++ b/readme.md @@ -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 diff --git a/spec/fixtures/files/basic.oats b/spec/fixtures/files/basic.oats new file mode 100644 index 0000000..fd1b464 --- /dev/null +++ b/spec/fixtures/files/basic.oats @@ -0,0 +1,3 @@ +[person] + [name] + [age] diff --git a/spec/fixtures/files/bob.oats b/spec/fixtures/files/bob.oats new file mode 100644 index 0000000..60eed75 --- /dev/null +++ b/spec/fixtures/files/bob.oats @@ -0,0 +1,5 @@ +[person] + [name] + Bob + [age] + 20 diff --git a/spec/fixtures/files/deep.oats b/spec/fixtures/files/deep.oats new file mode 100644 index 0000000..a05b778 --- /dev/null +++ b/spec/fixtures/files/deep.oats @@ -0,0 +1,6 @@ +[first] + [second] + [third] + [second] + [third] + [third] diff --git a/spec/fixtures/files/document.oats b/spec/fixtures/files/document.oats new file mode 100644 index 0000000..0d591de --- /dev/null +++ b/spec/fixtures/files/document.oats @@ -0,0 +1,12 @@ +[document] + [section] + [title] Document + + Paragraph + + Multiline + Paragraph + + Text with a [nested nested] node + + Text with an [nested] empty nested node diff --git a/spec/fixtures/files/person.oats b/spec/fixtures/files/person.oats new file mode 100644 index 0000000..fd1b464 --- /dev/null +++ b/spec/fixtures/files/person.oats @@ -0,0 +1,3 @@ +[person] + [name] + [age] diff --git a/spec/fixtures/files/rose.oats b/spec/fixtures/files/rose.oats new file mode 100644 index 0000000..9f07aab --- /dev/null +++ b/spec/fixtures/files/rose.oats @@ -0,0 +1,3 @@ +[person] + [name] Rose + [age] 22 diff --git a/spec/oats_spec.moon b/spec/oats_spec.moon new file mode 100644 index 0000000..2db041c --- /dev/null +++ b/spec/oats_spec.moon @@ -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") diff --git a/src/oats.lua b/src/oats.lua new file mode 100644 index 0000000..c84effc --- /dev/null +++ b/src/oats.lua @@ -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