├── .github └── workflows │ ├── CI.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── MAINTAINING.md ├── README.md ├── rockspec ├── api7-lua-tinyyaml-0.4.2-0.rockspec ├── api7-lua-tinyyaml-0.4.3-0.rockspec ├── api7-lua-tinyyaml-0.4.4-0.rockspec ├── lua-tinyyaml-0.1-0.rockspec └── lua-tinyyaml-0.2-0.rockspec ├── spec ├── array_multiline_string_spec.lua ├── dupekey_spec.lua ├── emptylines_spec.lua ├── end_spec.lua ├── example.yaml ├── example_spec.lua ├── map_spec.lua ├── number_spec.lua ├── seq_spec.lua └── string_spec.lua └── tinyyaml.lua /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | [push, pull_request] 5 | 6 | 7 | jobs: 8 | ci: 9 | strategy: 10 | matrix: 11 | luaVersion: ["5.1", "5.2", "5.3", "5.4", "luajit"] 12 | 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@master 16 | - uses: leafo/gh-actions-lua@v8 17 | with: 18 | luaVersion: ${{ matrix.luaVersion }} 19 | - uses: leafo/gh-actions-luarocks@v4 20 | 21 | - name: install lua rocks 22 | run: | 23 | luarocks install busted 24 | luarocks install luassert 25 | 26 | - name: run test 27 | run: busted -m "?.lua" spec/ 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | paths: 8 | - 'rockspec/**' 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | - name: Install Lua 19 | uses: leafo/gh-actions-lua@v8 20 | 21 | - name: Install Luarocks 22 | uses: leafo/gh-actions-luarocks@v4 23 | 24 | - name: Extract release name 25 | id: release_env 26 | shell: bash 27 | run: | 28 | title="${{ github.event.head_commit.message }}" 29 | re="^feat: release v*(\S+)" 30 | if [[ $title =~ $re ]]; then 31 | v=v${BASH_REMATCH[1]} 32 | echo "##[set-output name=version;]${v}" 33 | echo "##[set-output name=version_withou_v;]${BASH_REMATCH[1]}" 34 | else 35 | echo "commit format is not correct" 36 | exit 1 37 | fi 38 | 39 | - name: Create Release 40 | uses: actions/create-release@v1 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | with: 44 | tag_name: ${{ steps.release_env.outputs.version }} 45 | release_name: ${{ steps.release_env.outputs.version }} 46 | draft: false 47 | prerelease: false 48 | 49 | - name: Upload to luarocks 50 | env: 51 | LUAROCKS_TOKEN: ${{ secrets.LUAROCKS_TOKEN }} 52 | run: | 53 | luarocks install dkjson 54 | luarocks upload rockspec/api7-lua-tinyyaml-${{ steps.release_env.outputs.version_withou_v }}-0.rockspec --api-key=${LUAROCKS_TOKEN} 55 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /MAINTAINING.md: -------------------------------------------------------------------------------- 1 | ## Version Publish 2 | 3 | We could publish new version of lua-tinyyaml easily. All you need to do is: 4 | 5 | - Create the release PR following the format `feat: release VERSION`, where `VERSION` should be the version used in the rockspec name, like `0.1` for `lua-tinyyaml-0.1-0.rockspec`. 6 | 7 | When the PR got merged, it would trigger Github Actions to upload to both github release and luarocks. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-tinyyaml 2 | a tiny yaml (subset) parser for pure lua 3 | -------------------------------------------------------------------------------- /rockspec/api7-lua-tinyyaml-0.4.2-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "api7-lua-tinyyaml" 2 | version = "0.4.2-0" 3 | source = { 4 | url = "git://github.com/api7/lua-tinyyaml", 5 | tag = "v0.4.2" 6 | } 7 | 8 | description = { 9 | summary = "a tiny yaml (subset) parser for pure lua", 10 | homepage = "https://github.com/api7/lua-tinyyaml", 11 | license = "MIT License", 12 | } 13 | 14 | build = { 15 | type = "builtin", 16 | modules = { 17 | ["tinyyaml"] = "tinyyaml.lua" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /rockspec/api7-lua-tinyyaml-0.4.3-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "api7-lua-tinyyaml" 2 | version = "0.4.3-0" 3 | source = { 4 | url = "git://github.com/api7/lua-tinyyaml", 5 | tag = "v0.4.3" 6 | } 7 | 8 | description = { 9 | summary = "a tiny yaml (subset) parser for pure lua", 10 | homepage = "https://github.com/api7/lua-tinyyaml", 11 | license = "MIT License", 12 | } 13 | 14 | build = { 15 | type = "builtin", 16 | modules = { 17 | ["tinyyaml"] = "tinyyaml.lua" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /rockspec/api7-lua-tinyyaml-0.4.4-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "api7-lua-tinyyaml" 2 | version = "0.4.4-0" 3 | source = { 4 | url = "git://github.com/api7/lua-tinyyaml", 5 | tag = "v0.4.4" 6 | } 7 | 8 | description = { 9 | summary = "a tiny yaml (subset) parser for pure lua", 10 | homepage = "https://github.com/api7/lua-tinyyaml", 11 | license = "MIT License", 12 | } 13 | 14 | build = { 15 | type = "builtin", 16 | modules = { 17 | ["tinyyaml"] = "tinyyaml.lua" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /rockspec/lua-tinyyaml-0.1-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "api7-lua-tinyyaml" 2 | version = "0.1.0-0" 3 | source = { 4 | url = "git://github.com/api7/lua-tinyyaml", 5 | tag = "v0.1.0" 6 | } 7 | 8 | description = { 9 | summary = "a tiny yaml (subset) parser for pure lua", 10 | homepage = "https://github.com/api7/lua-tinyyaml", 11 | license = "MIT License", 12 | } 13 | 14 | build = { 15 | type = "builtin", 16 | modules = { 17 | ["tinyyaml"] = "tinyyaml.lua" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /rockspec/lua-tinyyaml-0.2-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "api7-lua-tinyyaml" 2 | version = "0.2.0-0" 3 | source = { 4 | url = "git://github.com/api7/lua-tinyyaml", 5 | tag = "v0.2.0" 6 | } 7 | 8 | description = { 9 | summary = "a tiny yaml (subset) parser for pure lua", 10 | homepage = "https://github.com/api7/lua-tinyyaml", 11 | license = "MIT License", 12 | } 13 | 14 | build = { 15 | type = "builtin", 16 | modules = { 17 | ["tinyyaml"] = "tinyyaml.lua" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /spec/array_multiline_string_spec.lua: -------------------------------------------------------------------------------- 1 | local busted = require('busted') 2 | local assert = require('luassert') 3 | local yaml = require('tinyyaml') 4 | 5 | busted.describe("multi line array", function() 6 | busted.it("multi-line array of string", function() 7 | assert.same( 8 | { 9 | abstract = { 10 | [[ 11 | This is the abstract. 12 | First item 13 | ]], 14 | [[ 15 | This is the abstract. 16 | Second Item 17 | ]], 18 | }, 19 | }, 20 | yaml.parse([[ 21 | abstract: 22 | - | 23 | This is the abstract. 24 | First item 25 | - | 26 | This is the abstract. 27 | Second Item 28 | ]]) 29 | ) 30 | end) 31 | end) 32 | -------------------------------------------------------------------------------- /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 | 69 | busted.it("bad alias syntax", function() 70 | assert.has_error( 71 | function() 72 | yaml.parse([[ 73 | anchor_value: &anchor "schema" 74 | map: 75 | Fruit : * 76 | Fruit_1 : orange 77 | Fruit : banana 78 | Vegetable : celery 79 | Vegetable_1 : cucumber 80 | Vegetable : broccoli 81 | ]]) 82 | end, 83 | "did not find expected alphabetic or numeric character" 84 | ) 85 | end) 86 | end) 87 | end) -------------------------------------------------------------------------------- /spec/emptylines_spec.lua: -------------------------------------------------------------------------------- 1 | local busted = require 'busted' 2 | local assert = require 'luassert' 3 | local yaml = require 'tinyyaml' 4 | 5 | busted.describe("empty lines", function() 6 | busted.it("multi-line scalar", function() 7 | assert.same( 8 | { 9 | abstract = "This is the abstract.\nIt consists of two paragraphs.\n" 10 | }, 11 | yaml.parse([[ 12 | abstract: | 13 | This is the abstract. 14 | It consists of two paragraphs. 15 | ]]) 16 | ) 17 | assert.same( 18 | { 19 | abstract = "This is the abstract.\n\nIt consists of two paragraphs.\n" 20 | }, 21 | yaml.parse([[ 22 | abstract: | 23 | This is the abstract. 24 | 25 | It consists of two paragraphs. 26 | ]]) 27 | ) 28 | assert.same( 29 | { 30 | abstract = "This is the abstract.\n\n\nIt consists of two paragraphs.\n" 31 | }, 32 | yaml.parse([[ 33 | abstract: | 34 | This is the abstract. 35 | 36 | 37 | It consists of two paragraphs. 38 | ]]) 39 | ) 40 | end) 41 | end) 42 | -------------------------------------------------------------------------------- /spec/end_spec.lua: -------------------------------------------------------------------------------- 1 | local busted = require 'busted' 2 | local assert = require 'luassert' 3 | local yaml = require 'tinyyaml' 4 | 5 | busted.describe("example", function() 6 | 7 | local t = yaml.parse(io.open("spec/example.yaml"):read('*a')) 8 | 9 | busted.it("end", function() 10 | assert.same( 11 | null, 12 | t.after_end 13 | ) 14 | end) 15 | end) 16 | -------------------------------------------------------------------------------- /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 | 199 | ... 200 | 201 | after_end: true 202 | -------------------------------------------------------------------------------- /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, 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/map_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("map", function() 10 | 11 | busted.it("map with colon for item", function() 12 | 13 | assert.same( 14 | { 15 | value = {"a:1"} 16 | }, 17 | yaml.parse([[ 18 | value: 19 | - a:1 20 | ]]) 21 | ) 22 | 23 | assert.same( 24 | { 25 | value = {"a:1"} 26 | }, 27 | yaml.parse([[ 28 | value: 29 | - "a:1" 30 | ]]) 31 | ) 32 | 33 | assert.same( 34 | { 35 | value = {{a = 1}} 36 | }, 37 | yaml.parse([[ 38 | value: 39 | - a: 1 40 | ]]) 41 | ) 42 | 43 | assert.same( 44 | { 45 | a = {{b = {c = 1}}} 46 | }, 47 | yaml.parse([[ 48 | a: 49 | - b: 50 | c: 1 51 | ]]) 52 | ) 53 | 54 | -- trailing whitespace after colon 55 | assert.same( 56 | { 57 | a = {{b = {c = 1}}} 58 | }, 59 | yaml.parse([[ 60 | a: 61 | - b: 62 | c: 1 63 | ]]) 64 | ) 65 | end) 66 | 67 | busted.it("map with slash for item", function() 68 | 69 | assert.same( 70 | { 71 | value = "/a" 72 | }, 73 | yaml.parse([[ 74 | value: /a 75 | ]]) 76 | ) 77 | 78 | assert.same( 79 | { 80 | value = {"/a"} 81 | }, 82 | yaml.parse([[ 83 | value: 84 | - /a 85 | ]]) 86 | ) 87 | end) 88 | 89 | busted.it("map with underscore for item", function() 90 | 91 | assert.same( 92 | { 93 | value = "_a" 94 | }, 95 | yaml.parse([[ 96 | value: _a 97 | ]]) 98 | ) 99 | 100 | assert.same( 101 | { 102 | value = {"_a"} 103 | }, 104 | yaml.parse([[ 105 | value: 106 | - _a 107 | ]]) 108 | ) 109 | end) 110 | 111 | busted.it("map with dash for item", function() 112 | 113 | assert.same( 114 | { 115 | value = "-a" 116 | }, 117 | yaml.parse([[ 118 | value: -a 119 | ]]) 120 | ) 121 | 122 | assert.same( 123 | { 124 | value = {"-a"} 125 | }, 126 | yaml.parse([[ 127 | value: 128 | - -a 129 | ]]) 130 | ) 131 | end) 132 | 133 | busted.it("map with space for item", function() 134 | 135 | assert.same( 136 | { 137 | value = "a 1" 138 | }, 139 | yaml.parse([[ 140 | value: a 1 141 | ]]) 142 | ) 143 | 144 | assert.same( 145 | { 146 | value = {"a 1"} 147 | }, 148 | yaml.parse([[ 149 | value: 150 | - a 1 151 | ]]) 152 | ) 153 | end) 154 | 155 | busted.it("map with magic word", function() 156 | assert.same( 157 | { 158 | def = 1 159 | }, 160 | yaml.parse([[ 161 | def: 1 162 | ]]) 163 | ) 164 | end) 165 | 166 | end) 167 | -------------------------------------------------------------------------------- /spec/number_spec.lua: -------------------------------------------------------------------------------- 1 | local busted = require('busted') 2 | local assert = require('luassert') 3 | local yaml = require('tinyyaml') 4 | 5 | busted.describe("numbers in nested seq:", function () 6 | local parsed_yaml = yaml.parse('case: [["status", "==", 302]]') 7 | local parsed_yaml2 = yaml.parse('case: [["status", "==", "302"]]') 8 | busted.it("numbers", function () 9 | assert.same( 10 | { 11 | case = {{"status", "==", 302}} 12 | }, 13 | yaml.parse([[ 14 | case: 15 | - - "status" 16 | - "==" 17 | - 302 18 | ]] 19 | ) 20 | ) 21 | end) 22 | 23 | busted.it("numbers in inline nested seq:", function () 24 | assert.same( 25 | { 26 | case = {{"status", "==", 302}} 27 | }, 28 | yaml.parse([[ 29 | case: 30 | - 31 | - "status" 32 | - "==" 33 | - 302 34 | ]] 35 | ) 36 | ) 37 | end) 38 | 39 | busted.it("number inside [] brackets", function () 40 | assert.same( 41 | { 42 | case = {{"status", "==", 302}} 43 | }, parsed_yaml 44 | ) 45 | end) 46 | 47 | busted.it("number inside [] brackets in double quotes", function () 48 | assert.same( 49 | { 50 | case = {{"status", "==", "302"}} 51 | }, parsed_yaml2 52 | ) 53 | end) 54 | end) 55 | -------------------------------------------------------------------------------- /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 | 40 | busted.it("seq bad alias syntax:", function() 41 | assert.has_error( 42 | function() 43 | yaml.parse([[ 44 | anchor_value: &anchor "schema" 45 | vars: 46 | - * 47 | ]]) 48 | end, 49 | "did not find expected alphabetic or numeric character" 50 | ) 51 | end) 52 | end) 53 | -------------------------------------------------------------------------------- /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 | 163 | busted.it("string: bad alias syntax", function() 164 | assert.has_error( 165 | function() 166 | yaml.parse([[ 167 | value: * 168 | ]]) 169 | end, 170 | "did not find expected alphabetic or numeric character" 171 | ) 172 | end) 173 | 174 | busted.it("string: escaped single quotes", function() 175 | assert.same( 176 | { 177 | ["single-quoted"] = "here's to \"quotes\"" 178 | }, 179 | yaml.parse([[ 180 | single-quoted: 'here''s to "quotes"' 181 | ]]) 182 | ) 183 | end) 184 | end) 185 | -------------------------------------------------------------------------------- /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, tconcat, tremove = table.insert, table.concat, table.remove 11 | local setmetatable = setmetatable 12 | local pairs = pairs 13 | local rawget = rawget 14 | local type = type 15 | local tonumber = tonumber 16 | local math = math 17 | local getmetatable = getmetatable 18 | local error = error 19 | local end_symbol = "..." 20 | local end_break_symbol = "...\n" 21 | 22 | local UNESCAPES = { 23 | ['0'] = "\x00", z = "\x00", N = "\x85", 24 | a = "\x07", b = "\x08", t = "\x09", 25 | n = "\x0a", v = "\x0b", f = "\x0c", 26 | r = "\x0d", e = "\x1b", ['\\'] = '\\', 27 | }; 28 | 29 | ------------------------------------------------------------------------------- 30 | -- utils 31 | local function select(list, pred) 32 | local selected = {} 33 | for i = 0, #list do 34 | local v = list[i] 35 | if v and pred(v, i) then 36 | tinsert(selected, v) 37 | end 38 | end 39 | return selected 40 | end 41 | 42 | local function startswith(haystack, needle) 43 | return ssub(haystack, 1, #needle) == needle 44 | end 45 | 46 | local function ltrim(str) 47 | return smatch(str, "^%s*(.-)$") 48 | end 49 | 50 | local function rtrim(str) 51 | return smatch(str, "^(.-)%s*$") 52 | end 53 | 54 | local function trim(str) 55 | return smatch(str, "^%s*(.-)%s*$") 56 | end 57 | 58 | ------------------------------------------------------------------------------- 59 | -- Implementation. 60 | -- 61 | local class = {__meta={}} 62 | function class.__meta.__call(cls, ...) 63 | local self = setmetatable({}, cls) 64 | if cls.__init then 65 | cls.__init(self, ...) 66 | end 67 | return self 68 | end 69 | 70 | function class.def(base, typ, cls) 71 | base = base or class 72 | local mt = {__metatable=base, __index=base} 73 | for k, v in pairs(base.__meta) do mt[k] = v end 74 | cls = setmetatable(cls or {}, mt) 75 | cls.__index = cls 76 | cls.__metatable = cls 77 | cls.__type = typ 78 | cls.__meta = mt 79 | return cls 80 | end 81 | 82 | 83 | local types = { 84 | null = class:def('null'), 85 | map = class:def('map'), 86 | omap = class:def('omap'), 87 | pairs = class:def('pairs'), 88 | set = class:def('set'), 89 | seq = class:def('seq'), 90 | timestamp = class:def('timestamp'), 91 | } 92 | 93 | local Null = types.null 94 | function Null.__tostring() return 'yaml.null' end 95 | function Null.isnull(v) 96 | if v == nil then return true end 97 | if type(v) == 'table' and getmetatable(v) == Null then return true end 98 | return false 99 | end 100 | local null = Null() 101 | 102 | function types.timestamp:__init(y, m, d, h, i, s, f, z) 103 | self.year = tonumber(y) 104 | self.month = tonumber(m) 105 | self.day = tonumber(d) 106 | self.hour = tonumber(h or 0) 107 | self.minute = tonumber(i or 0) 108 | self.second = tonumber(s or 0) 109 | if type(f) == 'string' and sfind(f, '^%d+$') then 110 | self.fraction = tonumber(f) * math.pow(10, 3 - #f) 111 | elseif f then 112 | self.fraction = f 113 | else 114 | self.fraction = 0 115 | end 116 | self.timezone = z 117 | end 118 | 119 | function types.timestamp:__tostring() 120 | return string.format( 121 | '%04d-%02d-%02dT%02d:%02d:%02d.%03d%s', 122 | self.year, self.month, self.day, 123 | self.hour, self.minute, self.second, self.fraction, 124 | self:gettz()) 125 | end 126 | 127 | function types.timestamp:gettz() 128 | if not self.timezone then 129 | return '' 130 | end 131 | if self.timezone == 0 then 132 | return 'Z' 133 | end 134 | local sign = self.timezone > 0 135 | local z = sign and self.timezone or -self.timezone 136 | local zh = math.floor(z) 137 | local zi = (z - zh) * 60 138 | return string.format( 139 | '%s%02d:%02d', sign and '+' or '-', zh, zi) 140 | end 141 | 142 | 143 | local function countindent(line) 144 | local _, j = sfind(line, '^%s+') 145 | if not j then 146 | return 0, line 147 | end 148 | return j, ssub(line, j+1) 149 | end 150 | 151 | local Parser = { 152 | timestamps=true,-- parse timestamps as objects instead of strings 153 | } 154 | 155 | function Parser:parsestring(line, stopper) 156 | stopper = stopper or '' 157 | local q = ssub(line, 1, 1) 158 | if q == ' ' or q == '\t' then 159 | return self:parsestring(ssub(line, 2)) 160 | end 161 | if q == "'" then 162 | local i = sfind(line, "'", 2, true) 163 | if not i then 164 | return nil, line 165 | end 166 | -- Unescape repeated single quotes. 167 | while i < #line and ssub(line, i+1, i+1) == "'" do 168 | i = sfind(line, "'", i + 2, true) 169 | if not i then 170 | return nil, line 171 | end 172 | end 173 | return ssub(line, 2, i-1):gsub("''", "'"), ssub(line, i+1) 174 | end 175 | if q == '"' then 176 | local i, buf = 2, '' 177 | while i < #line do 178 | local c = ssub(line, i, i) 179 | if c == '\\' then 180 | local n = ssub(line, i+1, i+1) 181 | if UNESCAPES[n] ~= nil then 182 | buf = buf..UNESCAPES[n] 183 | elseif n == 'x' then 184 | local h = ssub(i+2,i+3) 185 | if sfind(h, '^[0-9a-fA-F]$') then 186 | buf = buf..schar(tonumber(h, 16)) 187 | i = i + 2 188 | else 189 | buf = buf..'x' 190 | end 191 | else 192 | buf = buf..n 193 | end 194 | i = i + 1 195 | elseif c == q then 196 | break 197 | else 198 | buf = buf..c 199 | end 200 | i = i + 1 201 | end 202 | return buf, ssub(line, i+1) 203 | end 204 | if q == '{' or q == '[' then -- flow style 205 | return nil, line 206 | end 207 | if q == '|' or q == '>' then -- block 208 | return nil, line 209 | end 210 | if q == '-' or q == ':' then 211 | if ssub(line, 2, 2) == ' ' or ssub(line, 2, 2) == '\n' or #line == 1 then 212 | return nil, line 213 | end 214 | end 215 | 216 | if line == "*" then 217 | error("did not find expected alphabetic or numeric character") 218 | end 219 | 220 | local buf = '' 221 | while #line > 0 do 222 | local c = ssub(line, 1, 1) 223 | if sfind(stopper, c, 1, true) then 224 | break 225 | elseif c == ':' and (ssub(line, 2, 2) == ' ' or ssub(line, 2, 2) == '\n' or #line == 1) then 226 | break 227 | elseif c == '#' and (ssub(buf, #buf, #buf) == ' ') then 228 | break 229 | else 230 | buf = buf..c 231 | end 232 | line = ssub(line, 2) 233 | end 234 | buf = rtrim(buf) 235 | local val = tonumber(buf) or buf 236 | return val, line 237 | end 238 | 239 | local function isemptyline(line) 240 | return line == '' or sfind(line, '^%s*$') or sfind(line, '^%s*#') 241 | end 242 | 243 | local function equalsline(line, needle) 244 | return startswith(line, needle) and isemptyline(ssub(line, #needle+1)) 245 | end 246 | 247 | local function compactifyemptylines(lines) 248 | -- Appends empty lines as "\n" to the end of the nearest preceding non-empty line 249 | local compactified = {} 250 | local lastline = {} 251 | for i = 1, #lines do 252 | local line = lines[i] 253 | if isemptyline(line) then 254 | if #compactified > 0 and i < #lines then 255 | tinsert(lastline, "\n") 256 | end 257 | else 258 | if #lastline > 0 then 259 | tinsert(compactified, tconcat(lastline, "")) 260 | end 261 | lastline = {line} 262 | end 263 | end 264 | if #lastline > 0 then 265 | tinsert(compactified, tconcat(lastline, "")) 266 | end 267 | return compactified 268 | end 269 | 270 | local function checkdupekey(map, key) 271 | if rawget(map, key) ~= nil then 272 | -- print("found a duplicate key '"..key.."' in line: "..line) 273 | local suffix = 1 274 | while rawget(map, key..'_'..suffix) do 275 | suffix = suffix + 1 276 | end 277 | key = key ..'_'..suffix 278 | end 279 | return key 280 | end 281 | 282 | 283 | function Parser:parseflowstyle(line, lines) 284 | local stack = {} 285 | while true do 286 | if #line == 0 then 287 | if #lines == 0 then 288 | break 289 | else 290 | line = tremove(lines, 1) 291 | end 292 | end 293 | local c = ssub(line, 1, 1) 294 | if c == '#' then 295 | line = '' 296 | elseif c == ' ' or c == '\t' or c == '\r' or c == '\n' then 297 | line = ssub(line, 2) 298 | elseif c == '{' or c == '[' then 299 | tinsert(stack, {v={},t=c}) 300 | line = ssub(line, 2) 301 | elseif c == ':' then 302 | local s = tremove(stack) 303 | tinsert(stack, {v=s.v, t=':'}) 304 | line = ssub(line, 2) 305 | elseif c == ',' then 306 | local value = tremove(stack) 307 | if value.t == ':' or value.t == '{' or value.t == '[' then error() end 308 | if stack[#stack].t == ':' then 309 | -- map 310 | local key = tremove(stack) 311 | key.v = checkdupekey(stack[#stack].v, key.v) 312 | stack[#stack].v[key.v] = value.v 313 | elseif stack[#stack].t == '{' then 314 | -- set 315 | stack[#stack].v[value.v] = true 316 | elseif stack[#stack].t == '[' then 317 | -- seq 318 | tinsert(stack[#stack].v, value.v) 319 | end 320 | line = ssub(line, 2) 321 | elseif c == '}' then 322 | if stack[#stack].t == '{' then 323 | if #stack == 1 then break end 324 | stack[#stack].t = '}' 325 | line = ssub(line, 2) 326 | else 327 | line = ','..line 328 | end 329 | elseif c == ']' then 330 | if stack[#stack].t == '[' then 331 | if #stack == 1 then break end 332 | stack[#stack].t = ']' 333 | line = ssub(line, 2) 334 | else 335 | line = ','..line 336 | end 337 | else 338 | local s, rest = self:parsestring(line, ',{}[]') 339 | if not s then 340 | error('invalid flowstyle line: '..line) 341 | end 342 | tinsert(stack, {v=s, t='s'}) 343 | line = rest 344 | end 345 | end 346 | return stack[1].v, line 347 | end 348 | 349 | function Parser:parseblockstylestring(line, lines, indent) 350 | if #lines == 0 then 351 | error("failed to find multi-line scalar content") 352 | end 353 | local s = {} 354 | local firstindent = -1 355 | local endline = -1 356 | for i = 1, #lines do 357 | local ln = lines[i] 358 | local idt = countindent(ln) 359 | if idt <= indent then 360 | break 361 | end 362 | if ln == '' then 363 | tinsert(s, '') 364 | else 365 | if firstindent == -1 then 366 | firstindent = idt 367 | elseif idt < firstindent then 368 | break 369 | end 370 | tinsert(s, ssub(ln, firstindent + 1)) 371 | end 372 | endline = i 373 | end 374 | 375 | local striptrailing = true 376 | local sep = '\n' 377 | local newlineatend = true 378 | if line == '|' then 379 | striptrailing = true 380 | sep = '\n' 381 | newlineatend = true 382 | elseif line == '|+' then 383 | striptrailing = false 384 | sep = '\n' 385 | newlineatend = true 386 | elseif line == '|-' then 387 | striptrailing = true 388 | sep = '\n' 389 | newlineatend = false 390 | elseif line == '>' then 391 | striptrailing = true 392 | sep = ' ' 393 | newlineatend = true 394 | elseif line == '>+' then 395 | striptrailing = false 396 | sep = ' ' 397 | newlineatend = true 398 | elseif line == '>-' then 399 | striptrailing = true 400 | sep = ' ' 401 | newlineatend = false 402 | else 403 | error('invalid blockstyle string:'..line) 404 | end 405 | 406 | if #s == 0 then 407 | return "" 408 | end 409 | 410 | local _, eonl = s[#s]:gsub('\n', '\n') 411 | s[#s] = rtrim(s[#s]) 412 | if striptrailing then 413 | eonl = 0 414 | end 415 | if newlineatend then 416 | eonl = eonl + 1 417 | end 418 | for i = endline, 1, -1 do 419 | tremove(lines, i) 420 | end 421 | return tconcat(s, sep)..string.rep('\n', eonl) 422 | end 423 | 424 | function Parser:parsetimestamp(line) 425 | local _, p1, y, m, d = sfind(line, '^(%d%d%d%d)%-(%d%d)%-(%d%d)') 426 | if not p1 then 427 | return nil, line 428 | end 429 | if p1 == #line then 430 | return types.timestamp(y, m, d), '' 431 | end 432 | local _, p2, h, i, s = sfind(line, '^[Tt ](%d+):(%d+):(%d+)', p1+1) 433 | if not p2 then 434 | return types.timestamp(y, m, d), ssub(line, p1+1) 435 | end 436 | if p2 == #line then 437 | return types.timestamp(y, m, d, h, i, s), '' 438 | end 439 | local _, p3, f = sfind(line, '^%.(%d+)', p2+1) 440 | if not p3 then 441 | p3 = p2 442 | f = 0 443 | end 444 | local zc = ssub(line, p3+1, p3+1) 445 | local _, p4, zs, z = sfind(line, '^ ?([%+%-])(%d+)', p3+1) 446 | if p4 then 447 | z = tonumber(z) 448 | local _, p5, zi = sfind(line, '^:(%d+)', p4+1) 449 | if p5 then 450 | z = z + tonumber(zi) / 60 451 | end 452 | z = zs == '-' and -tonumber(z) or tonumber(z) 453 | elseif zc == 'Z' then 454 | p4 = p3 + 1 455 | z = 0 456 | else 457 | p4 = p3 458 | z = false 459 | end 460 | return types.timestamp(y, m, d, h, i, s, f, z), ssub(line, p4+1) 461 | end 462 | 463 | function Parser:parsescalar(line, lines, indent) 464 | line = trim(line) 465 | line = gsub(line, '^%s*#.*$', '') -- comment only -> '' 466 | line = gsub(line, '^%s*', '') -- trim head spaces 467 | 468 | if line == '' or line == '~' then 469 | return null 470 | end 471 | 472 | if self.timestamps then 473 | local ts, _ = self:parsetimestamp(line) 474 | if ts then 475 | return ts 476 | end 477 | end 478 | 479 | local s, _ = self:parsestring(line) 480 | -- startswith quote ... string 481 | -- not startswith quote ... maybe string 482 | if s and (startswith(line, '"') or startswith(line, "'")) then 483 | return s 484 | end 485 | 486 | if startswith('!', line) then -- unexpected tagchar 487 | error('unsupported line: '..line) 488 | end 489 | 490 | if equalsline(line, '{}') then 491 | return {} 492 | end 493 | if equalsline(line, '[]') then 494 | return {} 495 | end 496 | 497 | if startswith(line, '{') or startswith(line, '[') then 498 | return self:parseflowstyle(line, lines) 499 | end 500 | 501 | if startswith(line, '|') or startswith(line, '>') then 502 | return self:parseblockstylestring(line, lines, indent) 503 | end 504 | 505 | -- Regular unquoted string 506 | line = gsub(line, '%s*#.*$', '') -- trim tail comment 507 | local v = line 508 | if v == 'null' or v == 'Null' or v == 'NULL'then 509 | return null 510 | elseif v == 'true' or v == 'True' or v == 'TRUE' then 511 | return true 512 | elseif v == 'false' or v == 'False' or v == 'FALSE' then 513 | return false 514 | elseif v == '.inf' or v == '.Inf' or v == '.INF' then 515 | return math.huge 516 | elseif v == '+.inf' or v == '+.Inf' or v == '+.INF' then 517 | return math.huge 518 | elseif v == '-.inf' or v == '-.Inf' or v == '-.INF' then 519 | return -math.huge 520 | elseif v == '.nan' or v == '.NaN' or v == '.NAN' then 521 | return 0 / 0 522 | elseif sfind(v, '^[%+%-]?[0-9]+$') or sfind(v, '^[%+%-]?[0-9]+%.$')then 523 | return tonumber(v) -- : int 524 | elseif sfind(v, '^[%+%-]?[0-9]+%.[0-9]+$') then 525 | return tonumber(v) 526 | end 527 | return s or v 528 | end 529 | 530 | function Parser:parseseq(line, lines, indent) 531 | local seq = setmetatable({}, types.seq) 532 | if line ~= '' then 533 | error() 534 | end 535 | while #lines > 0 do 536 | -- Check for a new document 537 | line = lines[1] 538 | if startswith(line, '---') then 539 | while #lines > 0 and not startswith(lines, '---') do 540 | tremove(lines, 1) 541 | end 542 | return seq 543 | end 544 | 545 | -- Check the indent level 546 | local level = countindent(line) 547 | if level < indent then 548 | return seq 549 | elseif level > indent then 550 | error("found bad indenting in line: ".. line) 551 | end 552 | 553 | local i, j = sfind(line, '%-%s+') 554 | if not i then 555 | i, j = sfind(line, '%-$') 556 | if not i then 557 | return seq 558 | end 559 | end 560 | local rest = ssub(line, j+1) 561 | 562 | if sfind(rest, '^[^\'\"%s]*:%s*$') or sfind(rest, '^[^\'\"%s]*:%s+.') then 563 | -- Inline nested hash 564 | -- There are two patterns need to match as inline nested hash 565 | -- first one should have no other characters except whitespace after `:` 566 | -- and the second one should have characters besides whitespace after `:` 567 | -- 568 | -- value: 569 | -- - foo: 570 | -- bar: 1 571 | -- 572 | -- and 573 | -- 574 | -- value: 575 | -- - foo: bar 576 | -- 577 | -- And there is one pattern should not be matched, where there is no space after `:` 578 | -- in below, `foo:bar` should be parsed into a single string 579 | -- 580 | -- value: 581 | -- - foo:bar 582 | local indent2 = j 583 | lines[1] = string.rep(' ', indent2)..rest 584 | tinsert(seq, self:parsemap('', lines, indent2)) 585 | elseif sfind(rest, '^%-%s+') then 586 | -- Inline nested seq 587 | local indent2 = j 588 | lines[1] = string.rep(' ', indent2)..rest 589 | tinsert(seq, self:parseseq('', lines, indent2)) 590 | elseif isemptyline(rest) then 591 | tremove(lines, 1) 592 | if #lines == 0 then 593 | tinsert(seq, null) 594 | return seq 595 | end 596 | if sfind(lines[1], '^%s*%-') then 597 | local nextline = lines[1] 598 | local indent2 = countindent(nextline) 599 | if indent2 == indent then 600 | -- Null seqay entry 601 | tinsert(seq, null) 602 | else 603 | tinsert(seq, self:parseseq('', lines, indent2)) 604 | end 605 | else 606 | -- - # comment 607 | -- key: value 608 | local nextline = lines[1] 609 | local indent2 = countindent(nextline) 610 | tinsert(seq, self:parsemap('', lines, indent2)) 611 | end 612 | elseif line == "*" then 613 | error("did not find expected alphabetic or numeric character") 614 | elseif rest then 615 | -- Array entry with a value 616 | local nextline = lines[1] 617 | local indent2 = countindent(nextline) 618 | tremove(lines, 1) 619 | tinsert(seq, self:parsescalar(rest, lines, indent2)) 620 | end 621 | end 622 | return seq 623 | end 624 | 625 | function Parser:parseset(line, lines, indent) 626 | if not isemptyline(line) then 627 | error('not seq line: '..line) 628 | end 629 | local set = setmetatable({}, types.set) 630 | while #lines > 0 do 631 | -- Check for a new document 632 | line = lines[1] 633 | if startswith(line, '---') then 634 | while #lines > 0 and not startswith(lines, '---') do 635 | tremove(lines, 1) 636 | end 637 | return set 638 | end 639 | 640 | -- Check the indent level 641 | local level = countindent(line) 642 | if level < indent then 643 | return set 644 | elseif level > indent then 645 | error("found bad indenting in line: ".. line) 646 | end 647 | 648 | local i, j = sfind(line, '%?%s+') 649 | if not i then 650 | i, j = sfind(line, '%?$') 651 | if not i then 652 | return set 653 | end 654 | end 655 | local rest = ssub(line, j+1) 656 | 657 | if sfind(rest, '^[^\'\"%s]*:') then 658 | -- Inline nested hash 659 | local indent2 = j 660 | lines[1] = string.rep(' ', indent2)..rest 661 | set[self:parsemap('', lines, indent2)] = true 662 | elseif sfind(rest, '^%s+$') then 663 | tremove(lines, 1) 664 | if #lines == 0 then 665 | tinsert(set, null) 666 | return set 667 | end 668 | if sfind(lines[1], '^%s*%?') then 669 | local indent2 = countindent(lines[1]) 670 | if indent2 == indent then 671 | -- Null array entry 672 | set[null] = true 673 | else 674 | set[self:parseseq('', lines, indent2)] = true 675 | end 676 | end 677 | 678 | elseif rest then 679 | tremove(lines, 1) 680 | set[self:parsescalar(rest, lines)] = true 681 | else 682 | error("failed to classify line: "..line) 683 | end 684 | end 685 | return set 686 | end 687 | 688 | function Parser:parsemap(line, lines, indent) 689 | if not isemptyline(line) then 690 | error('not map line: '..line) 691 | end 692 | local map = setmetatable({}, types.map) 693 | while #lines > 0 do 694 | -- Check for a new document 695 | line = lines[1] 696 | if line == end_symbol or line == end_break_symbol then 697 | for i, _ in ipairs(lines) do 698 | lines[i] = nil 699 | end 700 | return map 701 | end 702 | 703 | if startswith(line, '---') then 704 | while #lines > 0 and not startswith(lines, '---') do 705 | tremove(lines, 1) 706 | end 707 | return map 708 | end 709 | 710 | -- Check the indent level 711 | local level, _ = countindent(line) 712 | if level < indent then 713 | return map 714 | elseif level > indent then 715 | error("found bad indenting in line: ".. line) 716 | end 717 | 718 | -- Find the key 719 | local key 720 | local s, rest = self:parsestring(line) 721 | 722 | -- Quoted keys 723 | if s and startswith(rest, ':') then 724 | local sc = self:parsescalar(s, {}, 0) 725 | if sc and type(sc) ~= 'string' then 726 | key = sc 727 | else 728 | key = s 729 | end 730 | line = ssub(rest, 2) 731 | else 732 | error("failed to classify line: "..line) 733 | end 734 | 735 | key = checkdupekey(map, key) 736 | line = ltrim(line) 737 | 738 | if ssub(line, 1, 1) == '!' then 739 | -- ignore type 740 | local rh = ltrim(ssub(line, 3)) 741 | local typename = smatch(rh, '^!?[^%s]+') 742 | line = ltrim(ssub(rh, #typename+1)) 743 | end 744 | 745 | if not isemptyline(line) then 746 | tremove(lines, 1) 747 | line = ltrim(line) 748 | map[key] = self:parsescalar(line, lines, indent) 749 | else 750 | -- An indent 751 | tremove(lines, 1) 752 | if #lines == 0 then 753 | map[key] = null 754 | return map; 755 | end 756 | if sfind(lines[1], '^%s*%-') then 757 | local indent2 = countindent(lines[1]) 758 | map[key] = self:parseseq('', lines, indent2) 759 | elseif sfind(lines[1], '^%s*%?') then 760 | local indent2 = countindent(lines[1]) 761 | map[key] = self:parseset('', lines, indent2) 762 | else 763 | local indent2 = countindent(lines[1]) 764 | if indent >= indent2 then 765 | -- Null hash entry 766 | map[key] = null 767 | else 768 | map[key] = self:parsemap('', lines, indent2) 769 | end 770 | end 771 | end 772 | end 773 | return map 774 | end 775 | 776 | 777 | -- : (list)->dict 778 | function Parser:parsedocuments(lines) 779 | lines = compactifyemptylines(lines) 780 | 781 | if sfind(lines[1], '^%%YAML') then tremove(lines, 1) end 782 | 783 | local root = {} 784 | local in_document = false 785 | while #lines > 0 do 786 | local line = lines[1] 787 | -- Do we have a document header? 788 | local docright; 789 | if sfind(line, '^%-%-%-') then 790 | -- Handle scalar documents 791 | docright = ssub(line, 4) 792 | tremove(lines, 1) 793 | in_document = true 794 | end 795 | if docright then 796 | if (not sfind(docright, '^%s+$') and 797 | not sfind(docright, '^%s+#')) then 798 | tinsert(root, self:parsescalar(docright, lines)) 799 | end 800 | elseif #lines == 0 or startswith(line, '---') then 801 | -- A naked document 802 | tinsert(root, null) 803 | while #lines > 0 and not sfind(lines[1], '---') do 804 | tremove(lines, 1) 805 | end 806 | in_document = false 807 | -- XXX The final '-+$' is to look for -- which ends up being an 808 | -- error later. 809 | elseif not in_document and #root > 0 then 810 | -- only the first document can be explicit 811 | error('parse error: '..line) 812 | elseif sfind(line, '^%s*%-') then 813 | -- An array at the root 814 | tinsert(root, self:parseseq('', lines, 0)) 815 | elseif sfind(line, '^%s*[^%s]') then 816 | -- A hash at the root 817 | local level = countindent(line) 818 | tinsert(root, self:parsemap('', lines, level)) 819 | else 820 | -- Shouldn't get here. @lines have whitespace-only lines 821 | -- stripped, and previous match is a line with any 822 | -- non-whitespace. So this clause should only be reachable via 823 | -- a perlbug where \s is not symmetric with \S 824 | 825 | -- uncoverable statement 826 | error('parse error: '..line) 827 | end 828 | end 829 | if #root > 1 and Null.isnull(root[1]) then 830 | tremove(root, 1) 831 | return root 832 | end 833 | return root 834 | end 835 | 836 | --- Parse yaml string into table. 837 | function Parser:parse(source) 838 | local lines = {} 839 | for line in string.gmatch(source .. '\n', '(.-)\r?\n') do 840 | tinsert(lines, line) 841 | end 842 | 843 | local docs = self:parsedocuments(lines) 844 | if #docs == 1 then 845 | return docs[1] 846 | end 847 | 848 | return docs 849 | end 850 | 851 | local function parse(source, options) 852 | local options = options or {} 853 | local parser = setmetatable (options, {__index=Parser}) 854 | return parser:parse(source) 855 | end 856 | 857 | return { 858 | version = 0.1, 859 | parse = parse, 860 | } 861 | --------------------------------------------------------------------------------