├── .gitignore ├── LICENSE ├── README.md ├── rockspec ├── lua-tinyyaml-0.1-0.rockspec └── lua-tinyyaml-1.0-0.rockspec ├── spec ├── dupekey_spec.lua ├── example.yaml ├── example_spec.lua ├── seq_spec.lua └── string_spec.lua └── tinyyaml.lua /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | .DS_Store 43 | /.vscode 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 peposso 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-tinyyaml 2 | a tiny yaml (subset) parser for pure lua 3 | -------------------------------------------------------------------------------- /rockspec/lua-tinyyaml-0.1-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-tinyyaml" 2 | version = "0.1-0" 3 | source = { 4 | url = "git://github.com/iresty/lua-tinyyaml", 5 | tag = "v0.1" 6 | } 7 | 8 | description = { 9 | summary = "a tiny yaml (subset) parser for pure lua", 10 | homepage = "https://github.com/iresty/lua-tinyyaml", 11 | license = "MIT License", 12 | maintainer = "peposso" 13 | } 14 | 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["tinyyaml"] = "tinyyaml.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /rockspec/lua-tinyyaml-1.0-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-tinyyaml" 2 | version = "1.0-0" 3 | source = { 4 | url = "git://github.com/peposso/lua-tinyyaml", 5 | tag = "1.0" 6 | } 7 | 8 | description = { 9 | summary = "a tiny yaml (subset) parser for pure lua", 10 | homepage = "https://github.com/peposso/lua-tinyyaml", 11 | license = "MIT License", 12 | maintainer = "peposso" 13 | } 14 | 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["tinyyaml"] = "tinyyaml.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /spec/dupekey_spec.lua: -------------------------------------------------------------------------------- 1 | -- .vscode/settings.json << 2 | -- "Lua.workspace.library": { 3 | -- "C:\\ProgramData\\chocolatey\\lib\\luarocks\\luarocks-2.4.4-win32\\systree\\share\\lua\\5.1": true 4 | -- }, 5 | local busted = require 'busted' 6 | local assert = require 'luassert' 7 | local yaml = require 'tinyyaml' 8 | 9 | local expected = { 10 | map = { 11 | Fruit = "apple", 12 | Fruit_1 = "orange", 13 | Fruit_2 = "banana", 14 | Vegetable = "celery", 15 | Vegetable_1 = "cucumber", 16 | Vegetable_2 = "broccoli" 17 | } 18 | } 19 | 20 | busted.describe("duplicate keys", function() 21 | busted.describe("in block style", function() 22 | busted.it("preserves duplicate map keys", function() 23 | assert.same( 24 | expected, 25 | yaml.parse([[ 26 | map: 27 | Fruit : apple 28 | Fruit : orange 29 | Fruit : banana 30 | Vegetable : celery 31 | Vegetable : cucumber 32 | Vegetable : broccoli 33 | ]]) 34 | ) 35 | end) 36 | busted.it("recognizes existing _ keys", function() 37 | assert.same( 38 | expected, 39 | yaml.parse([[ 40 | map: 41 | Fruit : apple 42 | Fruit_1 : orange 43 | Fruit : banana 44 | Vegetable : celery 45 | Vegetable_1 : cucumber 46 | Vegetable : broccoli 47 | ]]) 48 | ) 49 | end) 50 | end) 51 | busted.describe("in flow style", function() 52 | busted.it("preserves duplicate map keys", function() 53 | assert.same( 54 | expected, 55 | yaml.parse([[ 56 | map: { Fruit: apple, Fruit: orange, Fruit: banana, Vegetable: celery, Vegetable: cucumber, Vegetable: broccoli } 57 | ]]) 58 | ) 59 | end) 60 | busted.it("recognizes existing _ keys", function() 61 | assert.same( 62 | expected, 63 | yaml.parse([[ 64 | map: { Fruit: apple, Fruit_1: orange, Fruit: banana, Vegetable: celery, Vegetable_1: cucumber, Vegetable: broccoli } 65 | ]]) 66 | ) 67 | end) 68 | end) 69 | end) -------------------------------------------------------------------------------- /spec/example.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Collection Types ############################################################# 3 | ################################################################################ 4 | 5 | # http://yaml.org/type/map.html -----------------------------------------------# 6 | 7 | map: 8 | # Unordered set of key: value pairs. 9 | Block style: !!map 10 | Clark : Evans 11 | Ingy : döt Net 12 | Oren : Ben-Kiki 13 | Flow style: !!map { Clark: Evans, Ingy: döt Net, Oren: Ben-Kiki } 14 | 15 | # http://yaml.org/type/omap.html ----------------------------------------------# 16 | 17 | omap: 18 | # Explicitly typed ordered map (dictionary). 19 | Bestiary: !!omap 20 | - aardvark: African pig-like ant eater. Ugly. 21 | - anteater: South-American ant eater. Two species. 22 | - anaconda: South-American constrictor snake. Scaly. 23 | # Etc. 24 | # Flow style 25 | Numbers: !!omap [ one: 1, two: 2, three : 3 ] 26 | 27 | # http://yaml.org/type/pairs.html ---------------------------------------------# 28 | 29 | pairs: 30 | # Explicitly typed pairs. 31 | Block tasks: !!pairs 32 | - meeting: with team. 33 | - meeting: with boss. 34 | - break: lunch. 35 | - meeting: with client. 36 | Flow tasks: !!pairs [ meeting: with team, meeting: with boss ] 37 | 38 | # http://yaml.org/type/set.html -----------------------------------------------# 39 | 40 | set: 41 | # Explicitly typed set. 42 | baseball players: !!set 43 | ? Mark McGwire 44 | ? Sammy Sosa 45 | ? Ken Griffey 46 | # Flow style 47 | baseball teams: !!set { Boston Red Sox, Detroit Tigers, New York Yankees } 48 | 49 | # http://yaml.org/type/seq.html -----------------------------------------------# 50 | 51 | seq: 52 | # Ordered sequence of nodes 53 | Block style: !!seq 54 | - Mercury # Rotates - no light/dark sides. 55 | - Venus # Deadliest. Aptly named. 56 | - Earth # Mostly dirt. 57 | - Mars # Seems empty. 58 | - Jupiter # The king. 59 | - Saturn # Pretty. 60 | - Uranus # Where the sun hardly shines. 61 | - Neptune # Boring. No rings. 62 | - Pluto # You call this a planet? 63 | Flow style: !!seq [ Mercury, Venus, Earth, Mars, # Rocks 64 | Jupiter, Saturn, Uranus, Neptune, # Gas 65 | Pluto ] # Overrated 66 | 67 | 68 | # Scalar Types ################################################################# 69 | ################################################################################ 70 | 71 | # http://yaml.org/type/bool.html ----------------------------------------------# 72 | 73 | bool: 74 | - true 75 | - True 76 | - TRUE 77 | - false 78 | - False 79 | - FALSE 80 | 81 | # http://yaml.org/type/float.html ---------------------------------------------# 82 | 83 | float: 84 | canonical: 6.8523015e+5 85 | exponentioal: 685.230_15e+03 86 | fixed: 685_230.15 87 | sexagesimal: 190:20:30.15 88 | negative infinity: -.inf 89 | not a number: .NaN 90 | 91 | # http://yaml.org/type/int.html -----------------------------------------------# 92 | 93 | int: 94 | canonical: 685230 95 | decimal: +685_230 96 | octal: 02472256 97 | hexadecimal: 0x_0A_74_AE 98 | binary: 0b1010_0111_0100_1010_1110 99 | sexagesimal: 190:20:30 100 | 101 | # http://yaml.org/type/merge.html ---------------------------------------------# 102 | 103 | merge: 104 | - &CENTER { x: 1, y: 2 } 105 | - &LEFT { x: 0, y: 2 } 106 | - &BIG { r: 10 } 107 | - &SMALL { r: 1 } 108 | 109 | # All the following maps are equal: 110 | 111 | - # Explicit keys 112 | x: 1 113 | y: 2 114 | r: 10 115 | label: nothing 116 | 117 | - # Merge one map 118 | << : *CENTER 119 | r: 10 120 | label: center 121 | 122 | - # Merge multiple maps 123 | << : [ *CENTER, *BIG ] 124 | label: center/big 125 | 126 | - # Override 127 | << : [ *BIG, *LEFT, *SMALL ] 128 | x: 1 129 | label: big/left/small 130 | 131 | # http://yaml.org/type/null.html ----------------------------------------------# 132 | 133 | null: 134 | # This mapping has four keys, 135 | # one has a value. 136 | empty: 137 | canonical: ~ 138 | english: null 139 | ~: null key 140 | # This sequence has five 141 | # entries, two have values. 142 | sparse: 143 | - ~ 144 | - 2nd entry 145 | - 146 | - 4th entry 147 | - Null 148 | 149 | # http://yaml.org/type/str.html -----------------------------------------------# 150 | 151 | string: 152 | inline1: abcd 153 | inline2: "abcd" 154 | inline3: 'abcd' 155 | # block0: 156 | # aaa 157 | # bbb 158 | # ccc 159 | 160 | block1: | 161 | aaa 162 | bbb 163 | ccc 164 | 165 | block2: |+ 166 | aaa 167 | bbb 168 | ccc 169 | 170 | block3: |- 171 | aaa 172 | bbb 173 | ccc 174 | 175 | block4: > 176 | aaa 177 | bbb 178 | ccc 179 | 180 | block5: >+ 181 | aaa 182 | bbb 183 | ccc 184 | 185 | block6: >- 186 | aaa 187 | bbb 188 | ccc 189 | 190 | # http://yaml.org/type/timestamp.html -----------------------------------------# 191 | 192 | timestamp: 193 | canonical: 2001-12-15T02:59:43.1Z 194 | valid iso8601: 2001-12-14t21:59:43.10-05:00 195 | space separated: 2001-12-14 21:59:43.10 -5 196 | no time zone (Z): 2001-12-15 2:59:43.10 197 | date (00:00:00Z): 2002-12-14 198 | -------------------------------------------------------------------------------- /spec/example_spec.lua: -------------------------------------------------------------------------------- 1 | -- .vscode/settings.json << 2 | -- "Lua.workspace.library": { 3 | -- "C:\\ProgramData\\chocolatey\\lib\\luarocks\\luarocks-2.4.4-win32\\systree\\share\\lua\\5.1": true 4 | -- }, 5 | local busted = require 'busted' 6 | local assert = require 'luassert' 7 | local yaml = require 'tinyyaml' 8 | 9 | busted.describe("example", function() 10 | 11 | local t = yaml.parse(io.open("spec/example.yaml"):read('*a')) 12 | 13 | busted.it("map", function() 14 | assert.same( 15 | 'Ben-Kiki', 16 | t.map['Block style'].Oren 17 | ) 18 | assert.same( 19 | 'Evans', 20 | t.map['Flow style'].Clark 21 | ) 22 | end) 23 | 24 | busted.it("omap", function() 25 | assert.same( 26 | 'South-American constrictor snake. Scaly.', 27 | t.omap.Bestiary[3].anaconda 28 | ) 29 | -- assert.same( 30 | -- {one='1', two='2', three='3'}, 31 | -- t.omap.Numbers 32 | -- ) 33 | end) 34 | 35 | busted.it("pairs", function() 36 | assert.same( 37 | 'with client.', 38 | t.pairs['Block tasks'][4].meeting 39 | ) 40 | -- not supported. 41 | -- assert.same( 42 | -- {meeting='with boss'}, 43 | -- t.pairs['Flow tasks'][2] 44 | -- ) 45 | end) 46 | 47 | busted.it("set", function() 48 | assert.same( 49 | true, 50 | t.set['baseball players']['Sammy Sosa'] 51 | ) 52 | assert.same( 53 | true, 54 | t.set['baseball teams']['New York Yankees'] 55 | ) 56 | end) 57 | 58 | busted.it("seq", function() 59 | local expected = {"Mercury", "Venus", "Earth", "Mars", 60 | "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"} 61 | assert.same( 62 | expected, 63 | t.seq['Block style'] 64 | ) 65 | assert.same( 66 | expected, 67 | t.seq['Flow style'] 68 | ) 69 | end) 70 | 71 | busted.it("bool", function() 72 | for _, v in ipairs(t.bool) do 73 | assert.same( 74 | 'boolean', 75 | type(v) 76 | ) 77 | end 78 | assert.same( 79 | {true, true, true, false, false, false}, 80 | t.bool 81 | ) 82 | end) 83 | 84 | busted.it("float", function() 85 | -- canonical: 6.8523015e+5 86 | -- exponentioal: 685.230_15e+03 87 | -- fixed: 685_230.15 88 | -- sexagesimal: 190:20:30.15 89 | -- negative infinity: -.inf 90 | -- not a number: .NaN 91 | local function isnan(n) 92 | if type(n) ~= 'number' then return false end 93 | return n ~= n 94 | end 95 | 96 | assert.same( 97 | '6.8523015e+5', -- not supported 98 | t.float.canonical 99 | ) 100 | assert.same( 101 | '685.230_15e+03', -- not supported 102 | t.float.exponentioal 103 | ) 104 | assert.same( 105 | '685_230.15', -- not supported 106 | t.float.fixed 107 | ) 108 | assert.same( 109 | '190:20:30.15', -- not supported 110 | t.float.sexagesimal 111 | ) 112 | assert.same( 113 | -math.huge, 114 | t.float['negative infinity'] 115 | ) 116 | assert.same( 117 | true, 118 | isnan(t.float['not a number']) 119 | ) 120 | end) 121 | 122 | busted.it("int", function() 123 | -- canonical: 685230 124 | -- decimal: +685_230 125 | -- octal: 02472256 126 | -- hexadecimal: 0x_0A_74_AE 127 | -- binary: 0b1010_0111_0100_1010_1110 128 | -- sexagesimal: 190:20:30 129 | 130 | assert.same( 131 | 685230, 132 | t.int.canonical 133 | ) 134 | assert.same( 135 | '+685_230', -- not supported 136 | t.int.decimal 137 | ) 138 | assert.same( 139 | 2472256, -- not correct yaml spec 140 | t.int.octal 141 | ) 142 | assert.same( 143 | '0x_0A_74_AE', -- not supported 144 | t.int.hexadecimal 145 | ) 146 | assert.same( 147 | '0b1010_0111_0100_1010_1110', -- not supported 148 | t.int.binary 149 | ) 150 | assert.same( 151 | '190:20:30', -- not supported 152 | t.int.sexagesimal 153 | ) 154 | end) 155 | 156 | busted.it("merge", function() 157 | -- not supported 158 | end) 159 | 160 | -- busted.it("null", function() 161 | -- assert.same( 162 | -- 'yaml.null', 163 | -- tostring(t.null.empty) 164 | -- ) 165 | -- assert.same( 166 | -- 'yaml.null', 167 | -- tostring(t.null.canonical) 168 | -- ) 169 | -- assert.same( 170 | -- 'yaml.null', 171 | -- tostring(t.null.english) 172 | -- ) 173 | -- assert.same( 174 | -- 'yaml.null', 175 | -- tostring(t.null.sparse[1]) 176 | -- ) 177 | -- assert.same( 178 | -- 'yaml.null', 179 | -- tostring(t.null.sparse[3]) 180 | -- ) 181 | -- assert.same( 182 | -- 'yaml.null', 183 | -- tostring(t.null.sparse[5]) 184 | -- ) 185 | -- end) 186 | 187 | busted.it("string", function() 188 | assert.same( 189 | 'abcd', 190 | t.string.inline1 191 | ) 192 | assert.same( 193 | 'abcd', 194 | t.string.inline2 195 | ) 196 | assert.same( 197 | 'abcd', 198 | t.string.inline3 199 | ) 200 | assert.same( 201 | 'aaa\nbbb\nccc\n', 202 | t.string.block1 203 | ) 204 | assert.same( 205 | 'aaa bbb ccc\n', 206 | t.string.block4 207 | ) 208 | end) 209 | end) 210 | -------------------------------------------------------------------------------- /spec/seq_spec.lua: -------------------------------------------------------------------------------- 1 | -- .vscode/settings.json << 2 | -- "Lua.workspace.library": { 3 | -- "C:\\ProgramData\\chocolatey\\lib\\luarocks\\luarocks-2.4.4-win32\\systree\\share\\lua\\5.1": true 4 | -- }, 5 | local busted = require 'busted' 6 | local assert = require 'luassert' 7 | local yaml = require 'tinyyaml' 8 | 9 | busted.describe("seq", function() 10 | 11 | busted.it("nested seq:", function() 12 | assert.same( 13 | { 14 | vars = {{"scheme", "==", "http"}} 15 | }, 16 | yaml.parse([[ 17 | vars: 18 | - 19 | - "scheme" 20 | - "==" 21 | - "http" 22 | ]]) 23 | ) 24 | end) 25 | 26 | busted.it("inline nested seq:", function() 27 | assert.same( 28 | { 29 | vars = {{"scheme", "==", "http"}} 30 | }, 31 | yaml.parse([[ 32 | vars: 33 | - - "scheme" 34 | - "==" 35 | - "http" 36 | ]]) 37 | ) 38 | end) 39 | end) 40 | -------------------------------------------------------------------------------- /spec/string_spec.lua: -------------------------------------------------------------------------------- 1 | -- .vscode/settings.json << 2 | -- "Lua.workspace.library": { 3 | -- "C:\\ProgramData\\chocolatey\\lib\\luarocks\\luarocks-2.4.4-win32\\systree\\share\\lua\\5.1": true 4 | -- }, 5 | local busted = require 'busted' 6 | local assert = require 'luassert' 7 | local yaml = require 'tinyyaml' 8 | 9 | busted.describe("string", function() 10 | 11 | busted.it("string:", function() 12 | assert.same( 13 | { 14 | value = "hello" 15 | }, 16 | yaml.parse([[ 17 | value: hello #world 18 | ]]) 19 | ) 20 | 21 | assert.same( 22 | { 23 | value = "hello# world" 24 | }, 25 | yaml.parse([[ 26 | value: hello# world 27 | ]]) 28 | ) 29 | 30 | assert.same( 31 | { 32 | value = "hello" 33 | }, 34 | yaml.parse([[ 35 | value: 'hello' # world 36 | ]]) 37 | ) 38 | end) 39 | 40 | busted.it("string: key contains controlchar", function() 41 | assert.same( 42 | { 43 | ["hello:world"] = true 44 | }, 45 | yaml.parse([[ 46 | hello:world: true 47 | ]]) 48 | ) 49 | 50 | assert.same( 51 | { 52 | ["hello:world"] = true 53 | }, 54 | yaml.parse([[ 55 | hello:world : true 56 | ]]) 57 | ) 58 | 59 | assert.same( 60 | { 61 | ["hello world"] = true 62 | }, 63 | yaml.parse([[ 64 | hello world: true 65 | ]]) 66 | ) 67 | 68 | assert.same( 69 | { 70 | ["hello world"] = true 71 | }, 72 | yaml.parse([[ 73 | hello world : true 74 | ]]) 75 | ) 76 | 77 | assert.same( 78 | { 79 | ["hello#world"] = true 80 | }, 81 | yaml.parse([[ 82 | hello#world: true 83 | ]]) 84 | ) 85 | 86 | assert.same( 87 | { 88 | ["hello#world"] = true 89 | }, 90 | yaml.parse([[ 91 | hello#world : true 92 | ]]) 93 | ) 94 | end) 95 | 96 | busted.it("string: quoted key", function() 97 | assert.same( 98 | { 99 | ["hello world"] = true 100 | }, 101 | yaml.parse([[ 102 | "hello world": true 103 | ]]) 104 | ) 105 | 106 | assert.same( 107 | { 108 | ["hello world"] = true 109 | }, 110 | yaml.parse([[ 111 | "hello world": true 112 | ]]) 113 | ) 114 | 115 | assert.same( 116 | { 117 | ["hello world"] = true 118 | }, 119 | yaml.parse([[ 120 | 'hello world': true 121 | ]]) 122 | ) 123 | 124 | assert.same( 125 | { 126 | ["hello#world"] = true 127 | }, 128 | yaml.parse([[ 129 | 'hello#world': true 130 | ]]) 131 | ) 132 | 133 | assert.same( 134 | { 135 | ["hello':'world"] = true 136 | }, 137 | yaml.parse([[ 138 | "hello':'world": true 139 | ]]) 140 | ) 141 | end) 142 | 143 | busted.it("string: hash in quoted", function() 144 | assert.same( 145 | { 146 | value = "hello #world" 147 | }, 148 | yaml.parse([[ 149 | value: "hello #world" # hello world 150 | ]]) 151 | ) 152 | 153 | assert.same( 154 | { 155 | value = "hello #world" 156 | }, 157 | yaml.parse([[ 158 | value: 'hello #world' # hello world 159 | ]]) 160 | ) 161 | end) 162 | end) 163 | -------------------------------------------------------------------------------- /tinyyaml.lua: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------- 2 | -- tinyyaml - YAML subset parser 3 | ------------------------------------------------------------------------------- 4 | 5 | local table = table 6 | local string = string 7 | local schar = string.char 8 | local ssub, gsub = string.sub, string.gsub 9 | local sfind, smatch = string.find, string.match 10 | local tinsert, tremove = table.insert, table.remove 11 | local setmetatable = setmetatable 12 | local pairs = pairs 13 | local type = type 14 | local tonumber = tonumber 15 | local math = math 16 | local getmetatable = getmetatable 17 | local error = error 18 | 19 | local UNESCAPES = { 20 | ['0'] = "\x00", z = "\x00", N = "\x85", 21 | a = "\x07", b = "\x08", t = "\x09", 22 | n = "\x0a", v = "\x0b", f = "\x0c", 23 | r = "\x0d", e = "\x1b", ['\\'] = '\\', 24 | }; 25 | 26 | ------------------------------------------------------------------------------- 27 | -- utils 28 | local function select(list, pred) 29 | local selected = {} 30 | for i = 0, #list do 31 | local v = list[i] 32 | if v and pred(v, i) then 33 | tinsert(selected, v) 34 | end 35 | end 36 | return selected 37 | end 38 | 39 | local function startswith(haystack, needle) 40 | return ssub(haystack, 1, #needle) == needle 41 | end 42 | 43 | local function ltrim(str) 44 | return smatch(str, "^%s*(.-)$") 45 | end 46 | 47 | local function rtrim(str) 48 | return smatch(str, "^(.-)%s*$") 49 | end 50 | 51 | ------------------------------------------------------------------------------- 52 | -- Implementation. 53 | -- 54 | local class = {__meta={}} 55 | function class.__meta.__call(cls, ...) 56 | local self = setmetatable({}, cls) 57 | if cls.__init then 58 | cls.__init(self, ...) 59 | end 60 | return self 61 | end 62 | 63 | function class.def(base, typ, cls) 64 | base = base or class 65 | local mt = {__metatable=base, __index=base} 66 | for k, v in pairs(base.__meta) do mt[k] = v end 67 | cls = setmetatable(cls or {}, mt) 68 | cls.__index = cls 69 | cls.__metatable = cls 70 | cls.__type = typ 71 | cls.__meta = mt 72 | return cls 73 | end 74 | 75 | 76 | local types = { 77 | null = class:def('null'), 78 | map = class:def('map'), 79 | omap = class:def('omap'), 80 | pairs = class:def('pairs'), 81 | set = class:def('set'), 82 | seq = class:def('seq'), 83 | timestamp = class:def('timestamp'), 84 | } 85 | 86 | local Null = types.null 87 | function Null.__tostring() return 'yaml.null' end 88 | function Null.isnull(v) 89 | if v == nil then return true end 90 | if type(v) == 'table' and getmetatable(v) == Null then return true end 91 | return false 92 | end 93 | local null = Null() 94 | 95 | function types.timestamp:__init(y, m, d, h, i, s, f, z) 96 | self.year = tonumber(y) 97 | self.month = tonumber(m) 98 | self.day = tonumber(d) 99 | self.hour = tonumber(h or 0) 100 | self.minute = tonumber(i or 0) 101 | self.second = tonumber(s or 0) 102 | if type(f) == 'string' and sfind(f, '^%d+$') then 103 | self.fraction = tonumber(f) * math.pow(10, 3 - #f) 104 | elseif f then 105 | self.fraction = f 106 | else 107 | self.fraction = 0 108 | end 109 | self.timezone = z 110 | end 111 | 112 | function types.timestamp:__tostring() 113 | return string.format( 114 | '%04d-%02d-%02dT%02d:%02d:%02d.%03d%s', 115 | self.year, self.month, self.day, 116 | self.hour, self.minute, self.second, self.fraction, 117 | self:gettz()) 118 | end 119 | 120 | function types.timestamp:gettz() 121 | if not self.timezone then 122 | return '' 123 | end 124 | if self.timezone == 0 then 125 | return 'Z' 126 | end 127 | local sign = self.timezone > 0 128 | local z = sign and self.timezone or -self.timezone 129 | local zh = math.floor(z) 130 | local zi = (z - zh) * 60 131 | return string.format( 132 | '%s%02d:%02d', sign and '+' or '-', zh, zi) 133 | end 134 | 135 | 136 | local function countindent(line) 137 | local _, j = sfind(line, '^%s+') 138 | if not j then 139 | return 0, line 140 | end 141 | return j, ssub(line, j+1) 142 | end 143 | 144 | local function parsestring(line, stopper) 145 | stopper = stopper or '' 146 | local q = ssub(line, 1, 1) 147 | if q == ' ' or q == '\t' then 148 | return parsestring(ssub(line, 2)) 149 | end 150 | if q == "'" then 151 | local i = sfind(line, "'", 2, true) 152 | if not i then 153 | return nil, line 154 | end 155 | return ssub(line, 2, i-1), ssub(line, i+1) 156 | end 157 | if q == '"' then 158 | local i, buf = 2, '' 159 | while i < #line do 160 | local c = ssub(line, i, i) 161 | if c == '\\' then 162 | local n = ssub(line, i+1, i+1) 163 | if UNESCAPES[n] ~= nil then 164 | buf = buf..UNESCAPES[n] 165 | elseif n == 'x' then 166 | local h = ssub(i+2,i+3) 167 | if sfind(h, '^[0-9a-fA-F]$') then 168 | buf = buf..schar(tonumber(h, 16)) 169 | i = i + 2 170 | else 171 | buf = buf..'x' 172 | end 173 | else 174 | buf = buf..n 175 | end 176 | i = i + 1 177 | elseif c == q then 178 | break 179 | else 180 | buf = buf..c 181 | end 182 | i = i + 1 183 | end 184 | return buf, ssub(line, i+1) 185 | end 186 | if q == '{' or q == '[' then -- flow style 187 | return nil, line 188 | end 189 | if q == '|' or q == '>' then -- block 190 | return nil, line 191 | end 192 | if q == '-' or q == ':' then 193 | if ssub(line, 2, 2) == ' ' or #line == 1 then 194 | return nil, line 195 | end 196 | end 197 | local buf = '' 198 | while #line > 0 do 199 | local c = ssub(line, 1, 1) 200 | if sfind(stopper, c, 1, true) then 201 | break 202 | elseif c == ':' and (ssub(line, 2, 2) == ' ' or #line == 1) then 203 | break 204 | elseif c == '#' and (ssub(buf, #buf, #buf) == ' ') then 205 | break 206 | else 207 | buf = buf..c 208 | end 209 | line = ssub(line, 2) 210 | end 211 | return rtrim(buf), line 212 | end 213 | 214 | local function isemptyline(line) 215 | return line == '' or sfind(line, '^%s*$') or sfind(line, '^%s*#') 216 | end 217 | 218 | local function equalsline(line, needle) 219 | return startswith(line, needle) and isemptyline(ssub(line, #needle+1)) 220 | end 221 | 222 | local function checkdupekey(map, key) 223 | if map[key] ~= nil then 224 | -- print("found a duplicate key '"..key.."' in line: "..line) 225 | local suffix = 1 226 | while map[key..'_'..suffix] do 227 | suffix = suffix + 1 228 | end 229 | key = key ..'_'..suffix 230 | end 231 | return key 232 | end 233 | 234 | local function parseflowstyle(line, lines) 235 | local stack = {} 236 | while true do 237 | if #line == 0 then 238 | if #lines == 0 then 239 | break 240 | else 241 | line = tremove(lines, 1) 242 | end 243 | end 244 | local c = ssub(line, 1, 1) 245 | if c == '#' then 246 | line = '' 247 | elseif c == ' ' or c == '\t' or c == '\r' or c == '\n' then 248 | line = ssub(line, 2) 249 | elseif c == '{' or c == '[' then 250 | tinsert(stack, {v={},t=c}) 251 | line = ssub(line, 2) 252 | elseif c == ':' then 253 | local s = tremove(stack) 254 | tinsert(stack, {v=s.v, t=':'}) 255 | line = ssub(line, 2) 256 | elseif c == ',' then 257 | local value = tremove(stack) 258 | if value.t == ':' or value.t == '{' or value.t == '[' then error() end 259 | if stack[#stack].t == ':' then 260 | -- map 261 | local key = tremove(stack) 262 | key.v = checkdupekey(stack[#stack].v, key.v) 263 | stack[#stack].v[key.v] = value.v 264 | elseif stack[#stack].t == '{' then 265 | -- set 266 | stack[#stack].v[value.v] = true 267 | elseif stack[#stack].t == '[' then 268 | -- seq 269 | tinsert(stack[#stack].v, value.v) 270 | end 271 | line = ssub(line, 2) 272 | elseif c == '}' then 273 | if stack[#stack].t == '{' then 274 | if #stack == 1 then break end 275 | stack[#stack].t = '}' 276 | line = ssub(line, 2) 277 | else 278 | line = ','..line 279 | end 280 | elseif c == ']' then 281 | if stack[#stack].t == '[' then 282 | if #stack == 1 then break end 283 | stack[#stack].t = ']' 284 | line = ssub(line, 2) 285 | else 286 | line = ','..line 287 | end 288 | else 289 | local s, rest = parsestring(line, ',{}[]') 290 | if not s then 291 | error('invalid flowstyle line: '..line) 292 | end 293 | tinsert(stack, {v=s, t='s'}) 294 | line = rest 295 | end 296 | end 297 | return stack[1].v, line 298 | end 299 | 300 | local function parseblockstylestring(line, lines, indent) 301 | if #lines == 0 then 302 | error("failed to find multi-line scalar content") 303 | end 304 | local s = {} 305 | local firstindent = -1 306 | local endline = -1 307 | for i = 1, #lines do 308 | local ln = lines[i] 309 | local idt = countindent(ln) 310 | if idt <= indent then 311 | break 312 | end 313 | if ln == '' then 314 | tinsert(s, '') 315 | else 316 | if firstindent == -1 then 317 | firstindent = idt 318 | elseif idt < firstindent then 319 | break 320 | end 321 | tinsert(s, ssub(ln, firstindent + 1)) 322 | end 323 | endline = i 324 | end 325 | 326 | local striptrailing = true 327 | local sep = '\n' 328 | local newlineatend = true 329 | if line == '|' then 330 | striptrailing = true 331 | sep = '\n' 332 | newlineatend = true 333 | elseif line == '|+' then 334 | striptrailing = false 335 | sep = '\n' 336 | newlineatend = true 337 | elseif line == '|-' then 338 | striptrailing = true 339 | sep = '\n' 340 | newlineatend = false 341 | elseif line == '>' then 342 | striptrailing = true 343 | sep = ' ' 344 | newlineatend = true 345 | elseif line == '>+' then 346 | striptrailing = false 347 | sep = ' ' 348 | newlineatend = true 349 | elseif line == '>-' then 350 | striptrailing = true 351 | sep = ' ' 352 | newlineatend = false 353 | else 354 | error('invalid blockstyle string:'..line) 355 | end 356 | local eonl = 0 357 | for i = #s, 1, -1 do 358 | if s[i] == '' then 359 | tremove(s, i) 360 | eonl = eonl + 1 361 | end 362 | end 363 | if striptrailing then 364 | eonl = 0 365 | end 366 | if newlineatend then 367 | eonl = eonl + 1 368 | end 369 | for i = endline, 1, -1 do 370 | tremove(lines, i) 371 | end 372 | return table.concat(s, sep)..string.rep('\n', eonl) 373 | end 374 | 375 | local function parsetimestamp(line) 376 | local _, p1, y, m, d = sfind(line, '^(%d%d%d%d)%-(%d%d)%-(%d%d)') 377 | if not p1 then 378 | return nil, line 379 | end 380 | if p1 == #line then 381 | return types.timestamp(y, m, d), '' 382 | end 383 | local _, p2, h, i, s = sfind(line, '^[Tt ](%d+):(%d+):(%d+)', p1+1) 384 | if not p2 then 385 | return types.timestamp(y, m, d), ssub(line, p1+1) 386 | end 387 | if p2 == #line then 388 | return types.timestamp(y, m, d, h, i, s), '' 389 | end 390 | local _, p3, f = sfind(line, '^%.(%d+)', p2+1) 391 | if not p3 then 392 | p3 = p2 393 | f = 0 394 | end 395 | local zc = ssub(line, p3+1, p3+1) 396 | local _, p4, zs, z = sfind(line, '^ ?([%+%-])(%d+)', p3+1) 397 | if p4 then 398 | z = tonumber(z) 399 | local _, p5, zi = sfind(line, '^:(%d+)', p4+1) 400 | if p5 then 401 | z = z + tonumber(zi) / 60 402 | end 403 | z = zs == '-' and -tonumber(z) or tonumber(z) 404 | elseif zc == 'Z' then 405 | p4 = p3 + 1 406 | z = 0 407 | else 408 | p4 = p3 409 | z = false 410 | end 411 | return types.timestamp(y, m, d, h, i, s, f, z), ssub(line, p4+1) 412 | end 413 | 414 | local function parsescalar(line, lines, indent) 415 | line = ltrim(line) 416 | line = gsub(line, '^%s*#.*$', '') -- comment only -> '' 417 | line = gsub(line, '^%s*', '') -- trim head spaces 418 | 419 | if line == '' or line == '~' then 420 | return null 421 | end 422 | 423 | local ts, _ = parsetimestamp(line) 424 | if ts then 425 | return ts 426 | end 427 | 428 | local s, _ = parsestring(line) 429 | -- startswith quote ... string 430 | -- not startswith quote ... maybe string 431 | if s and (startswith(line, '"') or startswith(line, "'")) then 432 | return s 433 | end 434 | 435 | if startswith('!', line) then -- unexpected tagchar 436 | error('unsupported line: '..line) 437 | end 438 | 439 | if equalsline(line, '{}') then 440 | return {} 441 | end 442 | if equalsline(line, '[]') then 443 | return {} 444 | end 445 | 446 | if startswith(line, '{') or startswith(line, '[') then 447 | return parseflowstyle(line, lines) 448 | end 449 | 450 | if startswith(line, '|') or startswith(line, '>') then 451 | return parseblockstylestring(line, lines, indent) 452 | end 453 | 454 | -- Regular unquoted string 455 | line = gsub(line, '%s*#.*$', '') -- trim tail comment 456 | local v = line 457 | if v == 'null' or v == 'Null' or v == 'NULL'then 458 | return null 459 | elseif v == 'true' or v == 'True' or v == 'TRUE' then 460 | return true 461 | elseif v == 'false' or v == 'False' or v == 'FALSE' then 462 | return false 463 | elseif v == '.inf' or v == '.Inf' or v == '.INF' then 464 | return math.huge 465 | elseif v == '+.inf' or v == '+.Inf' or v == '+.INF' then 466 | return math.huge 467 | elseif v == '-.inf' or v == '-.Inf' or v == '-.INF' then 468 | return -math.huge 469 | elseif v == '.nan' or v == '.NaN' or v == '.NAN' then 470 | return 0 / 0 471 | elseif sfind(v, '^[%+%-]?[0-9]+$') or sfind(v, '^[%+%-]?[0-9]+%.$')then 472 | return tonumber(v) -- : int 473 | elseif sfind(v, '^[%+%-]?[0-9]+%.[0-9]+$') then 474 | return tonumber(v) 475 | end 476 | return s or v 477 | end 478 | 479 | local parsemap; -- : func 480 | 481 | local function parseseq(line, lines, indent) 482 | local seq = setmetatable({}, types.seq) 483 | if line ~= '' then 484 | error() 485 | end 486 | while #lines > 0 do 487 | -- Check for a new document 488 | line = lines[1] 489 | if startswith(line, '---') then 490 | while #lines > 0 and not startswith(lines, '---') do 491 | tremove(lines, 1) 492 | end 493 | return seq 494 | end 495 | 496 | -- Check the indent level 497 | local level = countindent(line) 498 | if level < indent then 499 | return seq 500 | elseif level > indent then 501 | error("found bad indenting in line: ".. line) 502 | end 503 | 504 | local i, j = sfind(line, '%-%s+') 505 | if not i then 506 | i, j = sfind(line, '%-$') 507 | if not i then 508 | return seq 509 | end 510 | end 511 | local rest = ssub(line, j+1) 512 | 513 | if sfind(rest, '^[^\'\"%s]*:') then 514 | -- Inline nested hash 515 | local indent2 = j 516 | lines[1] = string.rep(' ', indent2)..rest 517 | tinsert(seq, parsemap('', lines, indent2)) 518 | elseif sfind(rest, '^%-%s+') then 519 | -- Inline nested seq 520 | local indent2 = j 521 | lines[1] = string.rep(' ', indent2)..rest 522 | tinsert(seq, parseseq('', lines, indent2)) 523 | elseif isemptyline(rest) then 524 | tremove(lines, 1) 525 | if #lines == 0 then 526 | tinsert(seq, null) 527 | return seq 528 | end 529 | if sfind(lines[1], '^%s*%-') then 530 | local nextline = lines[1] 531 | local indent2 = countindent(nextline) 532 | if indent2 == indent then 533 | -- Null seqay entry 534 | tinsert(seq, null) 535 | else 536 | tinsert(seq, parseseq('', lines, indent2)) 537 | end 538 | else 539 | -- - # comment 540 | -- key: value 541 | local nextline = lines[1] 542 | local indent2 = countindent(nextline) 543 | tinsert(seq, parsemap('', lines, indent2)) 544 | end 545 | elseif rest then 546 | -- Array entry with a value 547 | tremove(lines, 1) 548 | tinsert(seq, parsescalar(rest, lines)) 549 | end 550 | end 551 | return seq 552 | end 553 | 554 | local function parseset(line, lines, indent) 555 | if not isemptyline(line) then 556 | error('not seq line: '..line) 557 | end 558 | local set = setmetatable({}, types.set) 559 | while #lines > 0 do 560 | -- Check for a new document 561 | line = lines[1] 562 | if startswith(line, '---') then 563 | while #lines > 0 and not startswith(lines, '---') do 564 | tremove(lines, 1) 565 | end 566 | return set 567 | end 568 | 569 | -- Check the indent level 570 | local level = countindent(line) 571 | if level < indent then 572 | return set 573 | elseif level > indent then 574 | error("found bad indenting in line: ".. line) 575 | end 576 | 577 | local i, j = sfind(line, '%?%s+') 578 | if not i then 579 | i, j = sfind(line, '%?$') 580 | if not i then 581 | return set 582 | end 583 | end 584 | local rest = ssub(line, j+1) 585 | 586 | if sfind(rest, '^[^\'\"%s]*:') then 587 | -- Inline nested hash 588 | local indent2 = j 589 | lines[1] = string.rep(' ', indent2)..rest 590 | set[parsemap('', lines, indent2)] = true 591 | elseif sfind(rest, '^%s+$') then 592 | tremove(lines, 1) 593 | if #lines == 0 then 594 | tinsert(set, null) 595 | return set 596 | end 597 | if sfind(lines[1], '^%s*%?') then 598 | local indent2 = countindent(lines[1]) 599 | if indent2 == indent then 600 | -- Null array entry 601 | set[null] = true 602 | else 603 | set[parseseq('', lines, indent2)] = true 604 | end 605 | end 606 | 607 | elseif rest then 608 | tremove(lines, 1) 609 | set[parsescalar(rest, lines)] = true 610 | else 611 | error("failed to classify line: "..line) 612 | end 613 | end 614 | return set 615 | end 616 | 617 | function parsemap(line, lines, indent) 618 | if not isemptyline(line) then 619 | error('not map line: '..line) 620 | end 621 | local map = setmetatable({}, types.map) 622 | while #lines > 0 do 623 | -- Check for a new document 624 | line = lines[1] 625 | if startswith(line, '---') then 626 | while #lines > 0 and not startswith(lines, '---') do 627 | tremove(lines, 1) 628 | end 629 | return map 630 | end 631 | 632 | -- Check the indent level 633 | local level, _ = countindent(line) 634 | if level < indent then 635 | return map 636 | elseif level > indent then 637 | error("found bad indenting in line: ".. line) 638 | end 639 | 640 | -- Find the key 641 | local key 642 | local s, rest = parsestring(line) 643 | 644 | -- Quoted keys 645 | if s and startswith(rest, ':') then 646 | local sc = parsescalar(s, {}, 0) 647 | if sc and type(sc) ~= 'string' then 648 | key = sc 649 | else 650 | key = s 651 | end 652 | line = ssub(rest, 2) 653 | else 654 | error("failed to classify line: "..line) 655 | end 656 | 657 | key = checkdupekey(map, key) 658 | line = ltrim(line) 659 | 660 | if ssub(line, 1, 1) == '!' then 661 | -- ignore type 662 | local rh = ltrim(ssub(line, 3)) 663 | local typename = smatch(rh, '^!?[^%s]+') 664 | line = ltrim(ssub(rh, #typename+1)) 665 | end 666 | 667 | if not isemptyline(line) then 668 | tremove(lines, 1) 669 | line = ltrim(line) 670 | map[key] = parsescalar(line, lines, indent) 671 | else 672 | -- An indent 673 | tremove(lines, 1) 674 | if #lines == 0 then 675 | map[key] = null 676 | return map; 677 | end 678 | if sfind(lines[1], '^%s*%-') then 679 | local indent2 = countindent(lines[1]) 680 | map[key] = parseseq('', lines, indent2) 681 | elseif sfind(lines[1], '^%s*%?') then 682 | local indent2 = countindent(lines[1]) 683 | map[key] = parseset('', lines, indent2) 684 | else 685 | local indent2 = countindent(lines[1]) 686 | if indent >= indent2 then 687 | -- Null hash entry 688 | map[key] = null 689 | else 690 | map[key] = parsemap('', lines, indent2) 691 | end 692 | end 693 | end 694 | end 695 | return map 696 | end 697 | 698 | 699 | -- : (list)->dict 700 | local function parsedocuments(lines) 701 | lines = select(lines, function(s) return not isemptyline(s) end) 702 | 703 | if sfind(lines[1], '^%%YAML') then tremove(lines, 1) end 704 | 705 | local root = {} 706 | local in_document = false 707 | while #lines > 0 do 708 | local line = lines[1] 709 | -- Do we have a document header? 710 | local docright; 711 | if sfind(line, '^%-%-%-') then 712 | -- Handle scalar documents 713 | docright = ssub(line, 4) 714 | tremove(lines, 1) 715 | in_document = true 716 | end 717 | if docright then 718 | if (not sfind(docright, '^%s+$') and 719 | not sfind(docright, '^%s+#')) then 720 | tinsert(root, parsescalar(docright, lines)) 721 | end 722 | elseif #lines == 0 or startswith(line, '---') then 723 | -- A naked document 724 | tinsert(root, null) 725 | while #lines > 0 and not sfind(lines[1], '---') do 726 | tremove(lines, 1) 727 | end 728 | in_document = false 729 | -- XXX The final '-+$' is to look for -- which ends up being an 730 | -- error later. 731 | elseif not in_document and #root > 0 then 732 | -- only the first document can be explicit 733 | error('parse error: '..line) 734 | elseif sfind(line, '^%s*%-') then 735 | -- An array at the root 736 | tinsert(root, parseseq('', lines, 0)) 737 | elseif sfind(line, '^%s*[^%s]') then 738 | -- A hash at the root 739 | local level = countindent(line) 740 | tinsert(root, parsemap('', lines, level)) 741 | else 742 | -- Shouldn't get here. @lines have whitespace-only lines 743 | -- stripped, and previous match is a line with any 744 | -- non-whitespace. So this clause should only be reachable via 745 | -- a perlbug where \s is not symmetric with \S 746 | 747 | -- uncoverable statement 748 | error('parse error: '..line) 749 | end 750 | end 751 | if #root > 1 and Null.isnull(root[1]) then 752 | tremove(root, 1) 753 | return root 754 | end 755 | return root 756 | end 757 | 758 | --- Parse yaml string into table. 759 | local function parse(source) 760 | local lines = {} 761 | for line in string.gmatch(source .. '\n', '(.-)\r?\n') do 762 | tinsert(lines, line) 763 | end 764 | 765 | local docs = parsedocuments(lines) 766 | if #docs == 1 then 767 | return docs[1] 768 | end 769 | 770 | return docs 771 | end 772 | 773 | return { 774 | version = 0.1, 775 | parse = parse, 776 | } 777 | --------------------------------------------------------------------------------