├── test_data ├── json │ ├── z_ex_11.json_invalid │ ├── z_ex_25.json_invalid │ ├── z_ex_13.json │ ├── z_ex_02.json │ ├── z_ex_01.json │ ├── z_ex_07.json │ ├── z_ex_14.json │ ├── z_ex_26.json │ ├── z_ex_08.json │ ├── z_ex_21.json │ ├── z_ex_10.json │ ├── z_ex_06.json │ ├── z_ex_09.json │ ├── z_ex_19.json │ ├── z_ex_05.json │ ├── z_ex_15.json │ ├── z_ex_28.json │ ├── z_ex_12.json │ ├── z_ex_20.json │ ├── z_ex_16.json │ ├── z_ex_22.json │ ├── z_ex_03.json │ ├── z_ex_04.json │ ├── z_ex_24.json │ ├── z_ex_17.json │ ├── z_ex_23.json │ ├── z_ex_18.json │ └── z_ex_27.json ├── json.yaml ├── numbers.yaml ├── yaml-version.yaml ├── z_ex_01.yaml ├── z_ex_13.yaml ├── z_ex_21.yaml ├── z_ex_14.yaml ├── z_ex_19.yaml ├── z_ex_02.yaml ├── z_ex_05.yaml ├── z_ex_06.yaml ├── nested_objects.yaml ├── z_ex_04.yaml ├── z_ex_20.yaml ├── z_ex_22.yaml ├── z_ex_09.yaml ├── z_ex_15.yaml ├── z_ex_10.yaml ├── z_ex_25.yaml ├── z_ex_16.yaml ├── anchors.yaml ├── z_ex_08.yaml ├── z_ex_12.yaml ├── z_ex_07.yaml ├── z_ex_11.yaml ├── z_ex_26.yaml ├── z_ex_17.yaml ├── z_ex_03.yaml ├── strings.yaml ├── z_ex_23.yaml ├── z_ex_24.yaml ├── z_ex_18.yaml ├── z_ex_28.yaml ├── z_ex_27.yaml └── zz_add_01.yaml ├── CHANGELOG.md ├── text_scanner ├── A_README.md ├── bom.v └── text_scanner.v ├── .gitattributes ├── v.mod ├── examples └── yaml2json.v ├── edge2_test.v ├── LICENSE ├── .gitignore ├── ystrconv ├── parse_str_test.v ├── parse_str.v ├── interpolate.v └── interpolate_test.v ├── edge_test.v ├── yaml_to_json_test.v ├── varray_jdo_test.v ├── yaml_obj_accessor.v ├── step2_tokenizer_test.v ├── README.md ├── yaml_to_json.v ├── step1_scanner_test.v ├── step3_reader.v ├── step2_tokenizer.v ├── yaml_obj_accessor_test.v ├── step1_scanner.v └── step3_reader_test.v /test_data/json/z_ex_11.json_invalid: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_data/json/z_ex_25.json_invalid: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## V 0.1.0 2 | - initial release -------------------------------------------------------------------------------- /test_data/json.yaml: -------------------------------------------------------------------------------- 1 | {"key": ["value", 3]} 2 | -------------------------------------------------------------------------------- /test_data/json/z_ex_13.json: -------------------------------------------------------------------------------- 1 | "\//||\/||\n// || ||__" -------------------------------------------------------------------------------- /test_data/numbers.yaml: -------------------------------------------------------------------------------- 1 | [100, 12.5, -130, 1.3e+9] 2 | -------------------------------------------------------------------------------- /test_data/json/z_ex_02.json: -------------------------------------------------------------------------------- 1 | {"hr": 65, "avg": 0.278, "rbi": 147} -------------------------------------------------------------------------------- /test_data/yaml-version.yaml: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | --- 3 | [1, 2, 3] 4 | -------------------------------------------------------------------------------- /test_data/json/z_ex_01.json: -------------------------------------------------------------------------------- 1 | ["Mark McGwire", "Sammy Sosa", "Ken Griffey"] -------------------------------------------------------------------------------- /test_data/json/z_ex_07.json: -------------------------------------------------------------------------------- 1 | ["Mark McGwire", "Sammy Sosa", "Ken Griffey"] -------------------------------------------------------------------------------- /test_data/z_ex_01.yaml: -------------------------------------------------------------------------------- 1 | - Mark McGwire 2 | - Sammy Sosa 3 | - Ken Griffey -------------------------------------------------------------------------------- /test_data/z_ex_13.yaml: -------------------------------------------------------------------------------- 1 | # ASCII Art 2 | --- | 3 | \//||\/|| 4 | // || ||__ -------------------------------------------------------------------------------- /test_data/json/z_ex_14.json: -------------------------------------------------------------------------------- 1 | "Mark McGwire's year was crippled by a knee injury." -------------------------------------------------------------------------------- /test_data/z_ex_21.yaml: -------------------------------------------------------------------------------- 1 | null: 2 | booleans: [ true, false ] 3 | string: '012345' 4 | -------------------------------------------------------------------------------- /test_data/json/z_ex_26.json: -------------------------------------------------------------------------------- 1 | [{"Mark McGwire": 65}, {"Sammy Sosa": 63}, {"Ken Griffy": 58}] -------------------------------------------------------------------------------- /test_data/json/z_ex_08.json: -------------------------------------------------------------------------------- 1 | {"time": "20:03:20", "player": "Sammy Sosa", "action": "strike (miss)"} -------------------------------------------------------------------------------- /test_data/z_ex_14.yaml: -------------------------------------------------------------------------------- 1 | --- > 2 | Mark McGwire's 3 | year was crippled 4 | by a knee injury. -------------------------------------------------------------------------------- /test_data/z_ex_19.yaml: -------------------------------------------------------------------------------- 1 | canonical: 12345 2 | decimal: +12345 3 | octal: 0o14 4 | hexadecimal: 0xC 5 | -------------------------------------------------------------------------------- /test_data/json/z_ex_21.json: -------------------------------------------------------------------------------- 1 | { 2 | "null": "", 3 | "booleans": [true, false], 4 | "string": "012345" 5 | } -------------------------------------------------------------------------------- /test_data/z_ex_02.yaml: -------------------------------------------------------------------------------- 1 | hr: 65 # Home runs 2 | avg: 0.278 # Batting average 3 | rbi: 147 # Runs Batted In -------------------------------------------------------------------------------- /test_data/z_ex_05.yaml: -------------------------------------------------------------------------------- 1 | - [name , hr, avg ] 2 | - [Mark McGwire, 65, 0.278] 3 | - [Sammy Sosa , 63, 0.288] 4 | -------------------------------------------------------------------------------- /test_data/z_ex_06.yaml: -------------------------------------------------------------------------------- 1 | Mark McGwire: {hr: 65, avg: 0.278} 2 | Sammy Sosa: { 3 | hr: 63, 4 | avg: 0.288 5 | } -------------------------------------------------------------------------------- /text_scanner/A_README.md: -------------------------------------------------------------------------------- 1 | 2 | TODO Make this module re-usable. I'm using it with the YAML parser and also Rosie RPL -------------------------------------------------------------------------------- /test_data/json/z_ex_10.json: -------------------------------------------------------------------------------- 1 | { 2 | "hr": ["Mark McGwire", "Sammy Sosa"], 3 | "rbi": ["Sammy Sosa", "Ken Griffey"] 4 | } -------------------------------------------------------------------------------- /test_data/json/z_ex_06.json: -------------------------------------------------------------------------------- 1 | { 2 | "Mark McGwire": {"hr": 65, "avg": 0.278}, 3 | "Sammy Sosa": {"hr": 63, "avg": 0.288} 4 | } -------------------------------------------------------------------------------- /test_data/json/z_ex_09.json: -------------------------------------------------------------------------------- 1 | { 2 | "hr": ["Mark McGwire", "Sammy Sosa"], 3 | "rbi": ["Sammy Sosa", "Ken Griffey"] 4 | } 5 | -------------------------------------------------------------------------------- /test_data/json/z_ex_19.json: -------------------------------------------------------------------------------- 1 | { 2 | "canonical": 12345, 3 | "decimal": 12345, 4 | "octal": 12, 5 | "hexadecimal": 12 6 | } -------------------------------------------------------------------------------- /test_data/nested_objects.yaml: -------------------------------------------------------------------------------- 1 | aaa: string 2 | bbb: 3 | 111: 1-1-1 4 | ccc: 5 | 222: # Null value 6 | 223: xxx 7 | -------------------------------------------------------------------------------- /test_data/json/z_ex_05.json: -------------------------------------------------------------------------------- 1 | [ 2 | ["name", "hr", "avg"], 3 | ["Mark McGwire", 65, 0.278], 4 | ["Sammy Sosa", 63, 0.288] 5 | ] 6 | -------------------------------------------------------------------------------- /test_data/z_ex_04.yaml: -------------------------------------------------------------------------------- 1 | - 2 | name: Mark McGwire 3 | hr: 65 4 | avg: 0.278 5 | - 6 | name: Sammy Sosa 7 | hr: 63 8 | avg: 0.288 -------------------------------------------------------------------------------- /test_data/z_ex_20.yaml: -------------------------------------------------------------------------------- 1 | canonical: 1.23015e+3 2 | exponential: 12.3015e+02 3 | fixed: 1230.15 4 | negative infinity: -.inf 5 | not a number: .NaN -------------------------------------------------------------------------------- /test_data/json/z_ex_15.json: -------------------------------------------------------------------------------- 1 | "Sammy Sosa completed another fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!" -------------------------------------------------------------------------------- /test_data/json/z_ex_28.json: -------------------------------------------------------------------------------- 1 | { 2 | "Time": "2001-11-23 15:01:42 -5", 3 | "User": "ed", 4 | "Warning": "This is an error message for the log file" 5 | } -------------------------------------------------------------------------------- /test_data/z_ex_22.yaml: -------------------------------------------------------------------------------- 1 | canonical: 2001-12-15T02:59:43.1Z 2 | iso8601: 2001-12-14t21:59:43.10-05:00 3 | spaced: 2001-12-14 21:59:43.10 -5 4 | date: 2002-12-14 5 | -------------------------------------------------------------------------------- /test_data/json/z_ex_12.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"item": "Super Hoop", "quantity": 1}, 3 | {"item": "Basketball", "quantity": 4}, 4 | {"item": "Big Shoes", "quantity": 1} 5 | ] 6 | -------------------------------------------------------------------------------- /test_data/z_ex_09.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | hr: # 1998 hr ranking 3 | - Mark McGwire 4 | - Sammy Sosa 5 | rbi: 6 | # 1998 rbi ranking 7 | - Sammy Sosa 8 | - Ken Griffey 9 | -------------------------------------------------------------------------------- /test_data/z_ex_15.yaml: -------------------------------------------------------------------------------- 1 | > 2 | Sammy Sosa completed another 3 | fine season with great stats. 4 | 5 | 63 Home Runs 6 | 0.288 Batting Average 7 | 8 | What a year! -------------------------------------------------------------------------------- /test_data/z_ex_10.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | hr: 3 | - Mark McGwire 4 | # Following node labeled SS 5 | - &SS Sammy Sosa 6 | rbi: 7 | - *SS # Subsequent occurrence 8 | - Ken Griffey -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.v linguist-language=V text=auto eol=lf 2 | *.vv linguist-language=V text=auto eol=lf 3 | *.bat text=auto eol=crlf 4 | Dockerfile.* linguist-language=Dockerfile 5 | -------------------------------------------------------------------------------- /test_data/json/z_ex_20.json: -------------------------------------------------------------------------------- 1 | { 2 | "canonical": 1230.15, 3 | "exponential": 1230.15, 4 | "fixed": 1230.15, 5 | "negative infinity": "-.inf", 6 | "not a number": ".NaN" 7 | } -------------------------------------------------------------------------------- /test_data/z_ex_25.yaml: -------------------------------------------------------------------------------- 1 | # Sets are represented as a 2 | # Mapping where each key is 3 | # associated with a null value 4 | --- !!set 5 | ? Mark McGwire 6 | ? Sammy Sosa 7 | ? Ken Griff -------------------------------------------------------------------------------- /test_data/z_ex_16.yaml: -------------------------------------------------------------------------------- 1 | name: Mark McGwire 2 | accomplishment: > 3 | Mark set a major league 4 | home run record in 1998. 5 | stats: | 6 | 65 Home Runs 7 | 0.278 Batting Average 8 | -------------------------------------------------------------------------------- /test_data/anchors.yaml: -------------------------------------------------------------------------------- 1 | base: &base 2 | name: Everyone has same name 3 | 4 | foo: &foo 5 | <<: *base 6 | age: 10 7 | 8 | bar: &bar 9 | <<: *base 10 | age: 20 11 | -------------------------------------------------------------------------------- /test_data/z_ex_08.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | time: 20:03:20 3 | player: Sammy Sosa 4 | action: strike (miss) 5 | ... 6 | --- 7 | time: 20:03:47 8 | player: Sammy Sosa 9 | action: grand slam 10 | ... -------------------------------------------------------------------------------- /test_data/z_ex_12.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Products purchased 3 | - item : Super Hoop 4 | quantity: 1 5 | - item : Basketball 6 | quantity: 4 7 | - item : Big Shoes 8 | quantity: 1 9 | -------------------------------------------------------------------------------- /test_data/z_ex_07.yaml: -------------------------------------------------------------------------------- 1 | # Ranking of 1998 home runs 2 | --- 3 | - Mark McGwire 4 | - Sammy Sosa 5 | - Ken Griffey 6 | 7 | # Team ranking 8 | --- 9 | - Chicago Cubs 10 | - St Louis Cardinals -------------------------------------------------------------------------------- /test_data/z_ex_11.yaml: -------------------------------------------------------------------------------- 1 | ? - Detroit Tigers 2 | - Chicago cubs 3 | : 4 | - 2001-07-23 5 | 6 | ? [ New York Yankees, 7 | Atlanta Braves ] 8 | : [ 2001-07-02, 2001-08-12, 9 | 2001-08-14 ] -------------------------------------------------------------------------------- /test_data/json/z_ex_16.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mark McGwire", 3 | "accomplishment": "Mark set a major league home run record in 1998.", 4 | "stats": "65 Home Runs\n0.278 Batting Average\n" 5 | } 6 | -------------------------------------------------------------------------------- /test_data/json/z_ex_22.json: -------------------------------------------------------------------------------- 1 | { 2 | "canonical": "2001-12-15T02:59:43.1Z", 3 | "iso8601": "2001-12-14t21:59:43.10-05:00", 4 | "spaced": "2001-12-14 21:59:43.10 -5", 5 | "date": "2002-12-14" 6 | } -------------------------------------------------------------------------------- /test_data/json/z_ex_03.json: -------------------------------------------------------------------------------- 1 | { 2 | "american": ["Boston Red Sox", "Detroit Tigers", "New York Yankees"], 3 | "national": ["New York Mets", "Chicago Cubs", "Atlanta Braves"], 4 | "array": [1, 2, 3] 5 | } -------------------------------------------------------------------------------- /test_data/z_ex_26.yaml: -------------------------------------------------------------------------------- 1 | # Ordered maps are represented as 2 | # A sequence of mappings, with 3 | # each mapping having one key 4 | --- !!omap 5 | - Mark McGwire: 65 6 | - Sammy Sosa: 63 7 | - Ken Griffy: 58 8 | -------------------------------------------------------------------------------- /test_data/json/z_ex_04.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Mark McGwire", 4 | "hr": 65, 5 | "avg": 0.278 6 | }, 7 | { 8 | "name": "Sammy Sosa", 9 | "hr": 63, 10 | "avg": 0.288 11 | } 12 | ] -------------------------------------------------------------------------------- /test_data/z_ex_17.yaml: -------------------------------------------------------------------------------- 1 | unicode: "Sosa did fine.\u263A" 2 | control: "\b1998\t1999\t2000\n" 3 | hex esc: "\x0d\x0a is \r\n" 4 | 5 | single: '"Howdy!" he cried.' 6 | quoted: ' # Not a ''comment''.' 7 | tie-fighter: '|\-*-/|' 8 | -------------------------------------------------------------------------------- /test_data/z_ex_03.yaml: -------------------------------------------------------------------------------- 1 | american: 2 | - Boston Red Sox 3 | - Detroit Tigers 4 | - New York Yankees 5 | national: 6 | - New York Mets 7 | - Chicago Cubs 8 | - Atlanta Braves 9 | array: 10 | - 1 11 | - 2 12 | - 3 13 | -------------------------------------------------------------------------------- /test_data/json/z_ex_24.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"center": {"x": 73, "y": 129}, "radius": 7}, 3 | {"start": {"x": 73, "y": 129}, "finish": {"x": 89, "y": 102}}, 4 | {"start": {"x": 73, "y": 129}, "color": 16772795, "text": "Pretty vector drawing."} 5 | ] -------------------------------------------------------------------------------- /v.mod: -------------------------------------------------------------------------------- 1 | Module { 2 | name: 'yaml' 3 | description: 'Native V-lang YAML reader' 4 | version: '0.1.0' 5 | license: 'MIT' 6 | repo_url: 'https://github.com/jdonnerstag/vlang-yaml' 7 | dependencies: [] 8 | tags: ['YAML', 'JSON'] 9 | } 10 | -------------------------------------------------------------------------------- /test_data/json/z_ex_17.json: -------------------------------------------------------------------------------- 1 | { 2 | "unicode": "Sosa did fine.☺", 3 | "control": "\b1998\t1999\t2000\n", 4 | "hex esc": "\r\n is \r\n", 5 | "single": "\"Howdy!\" he cried.", 6 | "quoted": " # Not a 'comment'.", 7 | "tie-fighter": "|\-*-/|" 8 | } -------------------------------------------------------------------------------- /test_data/json/z_ex_23.json: -------------------------------------------------------------------------------- 1 | { 2 | "not-date": "2002-04-28", 3 | "picture": "R0lGODlhDAAMAIQAAP//9/X\n17unp5WZmZgAAAOfn515eXv\nPz7Y6OjuDg4J+fn5OTk6enp\n56enmleECcgggoBADs=\n", 4 | "application specific tag": "The semantics of the tag\nabove may be different for\ndifferent documents.\n" 5 | } -------------------------------------------------------------------------------- /test_data/strings.yaml: -------------------------------------------------------------------------------- 1 | unqouted: string 2 | literal-block: | 3 | This entire block of text will be the value of the 'literal-block' key, 4 | with line breaks being preserved. 5 | folded: > 6 | This entire block of text will be the value of 'folded', but this 7 | time, all newlines will be replaced with a single space. 8 | -------------------------------------------------------------------------------- /test_data/z_ex_23.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | not-date: !!str 2002-04-28 3 | 4 | picture: !!binary | 5 | R0lGODlhDAAMAIQAAP//9/X 6 | 17unp5WZmZgAAAOfn515eXv 7 | Pz7Y6OjuDg4J+fn5OTk6enp 8 | 56enmleECcgggoBADs= 9 | 10 | application specific tag: !something | 11 | The semantics of the tag 12 | above may be different for 13 | different documents. 14 | -------------------------------------------------------------------------------- /test_data/z_ex_24.yaml: -------------------------------------------------------------------------------- 1 | %TAG ! tag:clarkevans.com,2002: 2 | --- !shape 3 | # Use the ! handle for presenting 4 | # tag:clarkevans.com,2002:circle 5 | - !circle 6 | center: &ORIGIN {x: 73, y: 129} 7 | radius: 7 8 | - !line 9 | start: *ORIGIN 10 | finish: { x: 89, y: 102 } 11 | - !label 12 | start: *ORIGIN 13 | color: 0xFFEEBB 14 | text: Pretty vector drawing. -------------------------------------------------------------------------------- /examples/yaml2json.v: -------------------------------------------------------------------------------- 1 | import yaml 2 | import os 3 | 4 | fn main() { 5 | if os.args.len != 2 { 6 | eprintln('Usage: yaml2json ') 7 | exit(1) 8 | } 9 | 10 | fname := os.args[1] 11 | if os.is_file(fname) != true { 12 | eprintln('File not found: $fname') 13 | exit(1) 14 | } 15 | 16 | str := yaml.yaml_to_json(fname, replace_tags: true, debug: 0) ? 17 | println(str) 18 | } 19 | -------------------------------------------------------------------------------- /test_data/json/z_ex_18.json: -------------------------------------------------------------------------------- 1 | { 2 | "plain 1": "This unquoted scalar spans many lines. It has three lines.", 3 | "plain 2": "This is also multi-line", 4 | "plain 3": "The second line is more indented", 5 | "plain 4": "The third line is more indented", 6 | "plain 5": "This is another example that should work", 7 | "plain 6": "The second line\nis more indented", 8 | "quoted": "So does this quoted scalar with.\n" 9 | } -------------------------------------------------------------------------------- /test_data/z_ex_18.yaml: -------------------------------------------------------------------------------- 1 | plain 1: 2 | This unquoted scalar 3 | spans many lines. 4 | It has three lines. 5 | 6 | plain 2: This is also 7 | multi-line 8 | 9 | plain 3: The 10 | second line 11 | is more indented 12 | 13 | plain 4: The 14 | third line 15 | is more indented 16 | 17 | plain 5: This 18 | is another 19 | example 20 | that should work 21 | 22 | plain 6: The 23 | second line 24 | 25 | is more indented 26 | 27 | quoted: "So does this 28 | quoted scalar with.\n" 29 | -------------------------------------------------------------------------------- /test_data/z_ex_28.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | Time: 2001-11-23 15:01:42 -5 3 | User: ed 4 | Warning: 5 | This is an error message 6 | for the log file 7 | --- 8 | Time: 2001-11-23 15:02:31 -5 9 | User: ed 10 | Warning: 11 | A slightly different error 12 | message. 13 | --- 14 | Date: 2001-11-23 15:03:17 -5 15 | User: ed 16 | Fatal: 17 | Unknown variable "bar" 18 | Stack: 19 | - file: TopClass.py 20 | line: 23 21 | code: | 22 | x = MoreObject("345\n") 23 | - file: MoreClass.py 24 | line: 58 25 | code: |- 26 | foo = bar 27 | -------------------------------------------------------------------------------- /edge2_test.v: -------------------------------------------------------------------------------- 1 | module yaml 2 | 3 | const debug = 0 4 | 5 | fn test_array_indent() ? { 6 | content := " 7 | arr_indent: 8 | - 1 9 | - 2 10 | - 3 11 | next: That's cool!! 12 | " 13 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 14 | assert docs.documents.len == 1 15 | x := docs.get(0) 16 | assert x is YamlMapValue 17 | if x is YamlMapValue { 18 | assert x.obj.len == 2 19 | dump(x.obj['arr_indent'] ?) 20 | } 21 | assert (x.get('arr_indent') ? as YamlListValue).ar == [YamlValue(i64(1)), i64(2), i64(3)] 22 | assert x.get('next') ?.string() ? == "That's cool!!" 23 | // assert false 24 | } 25 | -------------------------------------------------------------------------------- /test_data/z_ex_27.yaml: -------------------------------------------------------------------------------- 1 | --- ! 2 | invoice: 34843 3 | date : 2001-01-23 4 | bill-to: &id001 5 | given : Chris 6 | family : Dumars 7 | address: 8 | lines: | 9 | 458 Walkman Dr. 10 | Suite #292 11 | city : Royal Oak 12 | state : MI 13 | postal : 48046 14 | ship-to: *id001 15 | product: 16 | - sku : BL394D 17 | quantity : 4 18 | description : Basketball 19 | price : 450.00 20 | - sku : BL4438H 21 | quantity : 1 22 | description : Super Hoop 23 | price : 2392.00 24 | tax : 251.42 25 | total: 4443.52 26 | comments: 27 | Late afternoon is best. 28 | Backup contact is Nancy 29 | Billsmer @ 338-4338. -------------------------------------------------------------------------------- /test_data/json/z_ex_27.json: -------------------------------------------------------------------------------- 1 | { 2 | "invoice": 34843, 3 | "date": "2001-01-23", 4 | "bill-to": { 5 | "given": "Chris", 6 | "family": "Dumars", 7 | "address": { 8 | "lines": "458 Walkman Dr.\nSuite #292", 9 | "city": "Royal Oak", 10 | "state": "MI", 11 | "postal": 48046 12 | } 13 | }, 14 | "ship-to": { 15 | "given": "Chris", 16 | "family": "Dumars", 17 | "address": { 18 | "lines": "458 Walkman Dr.\nSuite #292", 19 | "city": "Royal Oak", 20 | "state": "MI", 21 | "postal": 48046 22 | } 23 | }, 24 | "product": [ 25 | {"sku": "BL394D", "quantity": 4, "description": "Basketball", "price": 450}, 26 | {"sku": "BL4438H", "quantity": 1, "description": "Super Hoop", "price": 2392.0} 27 | ], 28 | "tax": 251.42, 29 | "total": 4443.52, 30 | "comments": "Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338." 31 | } -------------------------------------------------------------------------------- /test_data/zz_add_01.yaml: -------------------------------------------------------------------------------- 1 | # YAML Reference implementation (test tool) 2 | # https://www.json2yaml.com/ 3 | 4 | plain: 5 | This unquoted scalar 6 | spans many lines. 7 | 8 | quoted: "So does this 9 | quoted scalar with colon space: .\n" 10 | 11 | question 1: 12 | This is a text, with a 13 | colon and space: but no quotes. It's a key / value, and not a text 14 | 15 | question 2: 16 | Same with hypen and space - xxx 17 | But hypen space are only detected at beginning of line 18 | 19 | quoted 2: Same as above 20 | but no quotes 21 | 22 | Are keys allowed 23 | to span multiple lines: answer is no 24 | 25 | # VS Code editor is not recognising it. Is that valid YAML or not? 26 | "Are keys allowed 27 | to span multiple lines": answer is no 28 | 29 | # Hmm. Also not detected by VS Code. But I'm reasonably sure this is valid YAML. 30 | "simple": what this simple one 31 | 32 | quoted 3: # comment 33 | this is a multi- 34 | line text -------------------------------------------------------------------------------- /text_scanner/bom.v: -------------------------------------------------------------------------------- 1 | module text_scanner 2 | 3 | pub enum Encodings { 4 | utf_32be // = "UTF-32BE" 5 | utf_32le // = "UTF-32LE" 6 | utf_16be // = "UTF-16BE" 7 | utf_16le // = "UTF-16LE" 8 | utf_8 // = "UTF-8" 9 | } 10 | 11 | pub fn detect_bom(str string) ?Encodings { 12 | if str.starts_with([u8(0x00), 0x00, 0xfe, 0xff].bytestr()) { 13 | return .utf_32be 14 | } else if str.starts_with([u8(0x00), 0x00, 0x00].bytestr()) { 15 | return .utf_32be 16 | } else if str.starts_with([u8(0xff), 0xfe, 0x00, 0x00].bytestr()) { 17 | return .utf_32le 18 | } else if str[1..].starts_with([u8(0x00), 0x00, 0x00].bytestr()) { 19 | return .utf_32le 20 | } else if str.starts_with([u8(0xfe), 0xff].bytestr()) { 21 | return .utf_16be 22 | } else if str.starts_with([u8(0x00)].bytestr()) { 23 | return .utf_16be 24 | } else if str.starts_with([u8(0xff), 0xfe].bytestr()) { 25 | return .utf_16le 26 | } else if str[1..].starts_with([u8(0x00)].bytestr()) { 27 | return .utf_16le 28 | } else if str.starts_with([u8(0xef), 0xbb, 0xbf].bytestr()) { 29 | return .utf_8 30 | } else { 31 | return none 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Juergen Donnerstag 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore all build binaries 2 | * 3 | !*/ 4 | !*.* 5 | *.exe 6 | *.o 7 | *.so 8 | .*.c 9 | *.tmp.c 10 | *.obj 11 | *.exp 12 | *.ilk 13 | *.pdb 14 | *.dll 15 | *.lib 16 | *.bak 17 | a.out 18 | .noprefix.vrepl_temp 19 | 20 | # ignore temp directories 21 | /temp 22 | /tmp 23 | 24 | # unignore special files without extension 25 | !.github/PULL_REQUEST_TEMPLATE 26 | !.editorconfig 27 | !.gitattributes 28 | !.gitignore 29 | !BSDmakefile 30 | !Dockerfile 31 | !Dockerfile.alpine 32 | !Dockerfile.cross 33 | !LICENSE 34 | !Makefile 35 | 36 | # ignore editor files 37 | .idea 38 | .project 39 | .classpath 40 | .c9 41 | *.launch 42 | .settings/ 43 | *.sublime-workspace 44 | .vscode/ 45 | *.code-workspace 46 | *~ 47 | *.swp 48 | *.swo 49 | *.swn 50 | .env 51 | 52 | # ignore debugger files 53 | cachegrind.out.* 54 | .gdb_history 55 | *.dSYM 56 | *.def 57 | 58 | # ignore system files 59 | .DS_Store 60 | ._* 61 | thumbs.db 62 | /.symlink 63 | /.bin 64 | 65 | _docs 66 | 67 | # ignore vs databases 68 | *.suo 69 | *.VC.db 70 | *.rsp 71 | 72 | # ignore cmd/tools/.disable_autorecompilation, which some package managers use. 73 | cmd/tools/.disable_autorecompilation 74 | 75 | test.bin 76 | 77 | # ignore codespace env 78 | .venv/ 79 | -------------------------------------------------------------------------------- /ystrconv/parse_str_test.v: -------------------------------------------------------------------------------- 1 | module ystrconv 2 | 3 | fn test_int() ? { 4 | assert is_int('1') == true 5 | assert is_int('12') == true 6 | assert is_int('+1') == true 7 | assert is_int('+121') == true 8 | assert is_int('-3') == true 9 | assert is_int('-32') == true 10 | 11 | assert is_int('1a') == false 12 | assert is_int('a') == false 13 | assert is_int('a1') == false 14 | assert is_int('') == false 15 | } 16 | 17 | fn test_float() ? { 18 | assert is_float('1') == true 19 | assert is_float('12') == true 20 | assert is_float('+1') == true 21 | assert is_float('+121') == true 22 | assert is_float('-3') == true 23 | assert is_float('-32') == true 24 | assert is_float('.1') == true 25 | assert is_float('1.1') == true 26 | assert is_float('1.') == true 27 | assert is_float('1.1e2') == true 28 | assert is_float('1.1e+2') == true 29 | assert is_float('1.1e-2') == true 30 | assert is_float('1.1E12') == true 31 | 32 | assert is_float('1a') == false 33 | assert is_float('a') == false 34 | assert is_float('a1') == false 35 | assert is_float('') == false 36 | assert is_float('1,1') == false 37 | assert is_float('1.1a') == false 38 | assert is_float('1.1e3a') == false 39 | assert is_float('1.1e*3') == false 40 | assert is_float('1.1e2.2') == false 41 | } 42 | -------------------------------------------------------------------------------- /edge_test.v: -------------------------------------------------------------------------------- 1 | module yaml 2 | 3 | const debug = 0 4 | 5 | fn test_edge() ? { 6 | content := ' 7 | a: 444***111 8 | b: aiu [ true, false ] # comment 9 | c: 09123112 # comment 10 | octal_in_yaml1.1: 02123112 # comment 11 | octal: 0o2123112 # comment 12 | hex: 0x2123112 # comment 13 | byte: 0b101101 14 | d: 1-2 # comment 15 | ee: [ true, false ] 16 | f: null 17 | e: 1 18 | inv_quote: invalid" 19 | 90: a 20 | arrow: it\'s cool yes -> no 21 | double_quote: | 22 | yes "that" is that that that 23 | double_quote_inline: "{\\"yey\\":true}" 24 | time: 11:30 25 | time2: 23:37 26 | time_inv: 25:30 27 | ' 28 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 29 | assert docs.documents.len == 1 30 | x := docs.get(0) 31 | assert x is YamlMapValue 32 | if x is YamlMapValue { 33 | assert x.obj.len == 19 34 | // eprintln(x.ar) 35 | // dump(x.obj['d'] ?) 36 | // dump(x.obj['e'] ?) 37 | // dump(x.obj['f'] ?) 38 | dump(x.obj['arrow'] ?) 39 | // dump(x.obj['b']) 40 | // assert x.obj['a'] == YamlValue("444***111") 41 | // assert x.obj['b'] == YamlValue("aiu [ true, false ]") 42 | // assert x.obj['c'] == YamlValue("02123112") 43 | } 44 | assert x.get('a') ?.string() ? == '444***111' 45 | assert x.get('b') ?.string() ? == 'aiu [ true, false ]' 46 | assert x.get('c') ?.type_name() == 'string' 47 | assert x.get('c') ?.string() ? == '09123112' 48 | assert x.get('octal') ?.i64() ? == 566858 49 | $if yaml_1_1_octal ? { 50 | assert x.get('octal_') ?.i64() ? == 566858 51 | } $else { 52 | assert x.get('octal_in_yaml1.1') ?.string() ? == '02123112' 53 | } 54 | assert x.get('hex') ?.i64() ? == 34746642 55 | assert x.get('byte') ?.i64() ? == 45 56 | assert x.get('d') ?.type_name() == 'string' 57 | assert x.get('d') ?.string() ? == '1-2' 58 | assert x.get('inv_quote') ?.string() ? == 'invalid"' 59 | assert x.get('time') ?.string() ? == '11:30' 60 | assert x.get('time2') ?.string() ? == '23:37' 61 | assert x.get('time_inv') ?.string() ? == '25:30' 62 | assert x.get('arrow') ?.string() ? == "it's cool yes -> no" 63 | assert x.get('double_quote') ?.string() ? == 'yes "that" is that that that' 64 | assert x.get('double_quote_inline') ?.string() ? == '{"yey":true}' 65 | assert x.get('90') ?.string() ? == 'a' 66 | assert x.get('f') ?.type_name() == 'yaml.Null' 67 | assert x.get('f') ? == YamlValue(null) 68 | // assert x.get('d')?.type_name() == "string" 69 | assert (x.get('ee') ? as YamlListValue).ar == [YamlValue(true), false] 70 | assert x.get('e') ?.i64() ? == 1 71 | assert x.get('f') ? is Null 72 | } 73 | -------------------------------------------------------------------------------- /ystrconv/parse_str.v: -------------------------------------------------------------------------------- 1 | module ystrconv 2 | 3 | // import regex 4 | 5 | /* 6 | const ( 7 | re_int = regex.regex_opt("[+-]?[0-9]+") or { panic("Invalid regex: 're_int'") } 8 | 9 | // V's "simplified" regex implementation is useless. The following simple 10 | // regex doesn't work. 11 | re_float = regex.regex_opt("[-+]?([0-9]*[.])?[0-9]+([eE][-+]?[0-9]+)?") or { panic("Invalid regex: 're_float'") } 12 | ) 13 | */ 14 | 15 | // regex.regex_opt("[+-]?[0-9]+")? 16 | fn p_is_int_(str string, i int) int { 17 | mut pos := i 18 | for pos < str.len { 19 | ch := str[pos] 20 | if ch.is_digit() == false { 21 | break 22 | } 23 | pos++ 24 | } 25 | return pos 26 | } 27 | 28 | fn optional_char(str string, pos int, ch u8) int { 29 | if str.len > 0 && str[pos] == ch { 30 | return pos + 1 31 | } 32 | return pos 33 | } 34 | 35 | fn optional_char_in(str string, pos int, bytes string) int { 36 | if pos < str.len && str[pos] in bytes.bytes() { 37 | return pos + 1 38 | } 39 | return pos 40 | } 41 | 42 | // regex.regex_opt("[+-]?[0-9]+")? 43 | fn p_is_int(str string) int { 44 | if str[0] == `0` && str.len > 1 { 45 | return 0 46 | } 47 | mut pos := optional_char_in(str, 0, '+-') 48 | return p_is_int_(str, pos) 49 | } 50 | 51 | // regex.regex_opt("[-+]?([0-9]*[.])?[0-9]+([eE][-+]?[0-9]+)?")? 52 | fn p_is_float(str string) int { 53 | if str[0] == `0` && str.len > 1 && str[1] != `.` { // TODO: parse `octal` & `hex` 54 | return 0 55 | } 56 | mut pos := optional_char_in(str, 0, '+-') 57 | 58 | { 59 | marker := pos 60 | pos = p_is_int_(str, pos) 61 | if pos < str.len && str[pos] == `.` { 62 | pos++ 63 | } else { 64 | pos = marker 65 | } 66 | } 67 | pos = p_is_int_(str, pos) 68 | 69 | { 70 | pos2 := optional_char_in(str, pos, 'eE') 71 | if pos != pos2 { 72 | pos = optional_char_in(str, pos2, '+-') 73 | } 74 | pos = p_is_int_(str, pos) 75 | } 76 | return pos 77 | } 78 | 79 | pub fn is_int(str string) bool { 80 | return str.len > 0 && p_is_int(str) == str.len 81 | } 82 | 83 | pub fn is_float(str string) bool { 84 | return str.len > 0 && p_is_float(str) == str.len 85 | } 86 | 87 | /* 88 | pub fn is_int(str string) bool { 89 | mut re := re_int 90 | start, stop := re.match_string(str) 91 | return str.len > 0 && start == 0 && stop == str.len 92 | } 93 | 94 | pub fn is_float(str string) bool { 95 | mut re := re_float 96 | start, stop := re.match_string(str) 97 | return str.len > 0 && start == 0 && stop == str.len 98 | } 99 | */ 100 | -------------------------------------------------------------------------------- /yaml_to_json_test.v: -------------------------------------------------------------------------------- 1 | module yaml 2 | 3 | // YAML spec: https://yaml.org/spec/1.2/spec.html 4 | // To test your YAML: https://www.json2yaml.com/ 5 | import os 6 | import regex 7 | import json 8 | 9 | const test_data_dir = os.dir(@FILE) + '/test_data' 10 | 11 | const debug = 0 12 | 13 | fn test_compare_with_json_files() ? { 14 | for f in os.ls('$test_data_dir/json') ? { 15 | if f.ends_with('.json') == false { 16 | continue 17 | } 18 | yf := '$test_data_dir/$f'.replace('.json', '.yaml') 19 | if os.is_file(yf) == false { 20 | continue 21 | } 22 | eprintln('read yaml: $yf') 23 | content := os.read_file(yf) ? 24 | json := yaml_to_json(content, replace_tags: true, debug: debug) ? 25 | 26 | file_content := os.read_file('$test_data_dir/json/$f') ? 27 | 28 | mut re := regex.regex_opt(',[\n\r]+\\s*') ? 29 | mut str := re.replace_simple(file_content, ', ') 30 | 31 | re = regex.regex_opt('[\n\r]+\\s*') ? 32 | str = re.replace_simple(str, '') 33 | 34 | str = str.replace('\\n', '\n') 35 | str = str.replace('\\r', '\r') 36 | str = str.replace('\\b', '\b') 37 | str = str.replace('\\t', '\t') 38 | 39 | eprintln(str) 40 | assert json == str 41 | } 42 | } 43 | 44 | fn read_json_file(fname string, debug int) ?string { 45 | content := os.read_file('$test_data_dir/${fname}.yaml') ? 46 | mut json_data := yaml_to_json(content, replace_tags: true, debug: debug) ? 47 | if json_data.starts_with('[') { 48 | return "{ \"ar\": $json_data }" 49 | } 50 | if json_data.starts_with('{') == false { 51 | return "{ \"val\": $json_data }" 52 | } 53 | return json_data 54 | } 55 | 56 | struct Zex01 { 57 | ar []string 58 | } 59 | 60 | fn test_z_ex_01() ? { 61 | mut json_data := read_json_file('z_ex_01', debug) ? 62 | // eprintln("$json_data") 63 | xj := json.decode(Zex01, json_data) ? 64 | assert xj.ar.len == 3 65 | assert xj.ar[0] == 'Mark McGwire' 66 | assert xj.ar[1] == 'Sammy Sosa' 67 | assert xj.ar[2] == 'Ken Griffey' 68 | } 69 | 70 | struct Zex02 { 71 | hr int 72 | avg f64 73 | rbi int 74 | } 75 | 76 | fn test_z_ex_02() ? { 77 | mut json_data := read_json_file('z_ex_02', debug) ? 78 | // eprintln("$json_data") 79 | 80 | // The V built-in json parser is a little .. 81 | // - No exception if string is provided but the target is an 'int'. Instead 0 is returned. 82 | // - If the target is an 'int', then no quotes are allowed, e.g. "hr": "65" will not work. "hr": 65 does. 83 | xj := json.decode(Zex02, json_data) ? 84 | 85 | assert xj.hr == 65 86 | assert xj.avg == 0.278 87 | assert xj.rbi == 147 88 | } 89 | 90 | struct Zex03 { 91 | american []string 92 | national []string 93 | } 94 | 95 | fn test_z_ex_03() ? { 96 | mut json_data := read_json_file('z_ex_03', debug) ? 97 | // eprintln("$json_data") 98 | xj := json.decode(Zex03, json_data) ? 99 | 100 | assert xj.american.len == 3 101 | assert xj.american[0] == 'Boston Red Sox' 102 | assert xj.american[1] == 'Detroit Tigers' 103 | assert xj.american[2] == 'New York Yankees' 104 | 105 | assert xj.national.len == 3 106 | assert xj.national[0] == 'New York Mets' 107 | assert xj.national[1] == 'Chicago Cubs' 108 | assert xj.national[2] == 'Atlanta Braves' 109 | } 110 | 111 | struct Zex04_inner { 112 | name string 113 | hr int 114 | avg f64 115 | } 116 | 117 | struct Zex04 { 118 | ar []Zex04_inner 119 | } 120 | 121 | fn test_z_ex_04() ? { 122 | json_data := read_json_file('z_ex_04', debug) ? 123 | // eprintln("$json_data") 124 | xj := json.decode(Zex04, json_data) ? 125 | 126 | assert xj.ar.len == 2 127 | } 128 | -------------------------------------------------------------------------------- /varray_jdo_test.v: -------------------------------------------------------------------------------- 1 | module yaml 2 | 3 | // --------------------------------------------------------------- 4 | // Some weird array behavior 5 | // --------------------------------------------------------------- 6 | 7 | type XValue = XListValue | string 8 | 9 | struct XListValue { 10 | pub mut: 11 | ar []int 12 | } 13 | 14 | // I think there is a bug in V's array implementation when it comes 15 | // to pointer elements 16 | fn test_array_append_failing() ? { 17 | mut ar := []&XValue{} // []&XValue{} // A list of XValue pointers 18 | mut x := XListValue{} 19 | mut px := &x // Get a pointer to the 'x' XValue 20 | ar << px // Append the pointer to the list 21 | 22 | x.ar << 9 // Modify 'x'. 23 | 24 | assert x.ar.len == 1 25 | assert x.ar[0] == 9 26 | 27 | assert px.ar.len == 1 28 | assert px.ar[0] == 9 29 | 30 | // eprintln("x: $x") 31 | // eprintln("ar: $ar") // ???? 32 | 33 | a0 := ar.last() // pointer to 'x' 34 | if a0 is XListValue { 35 | /* 36 | assert a0.ar.len == 1 // wrong ??? 37 | assert a0.ar[0] == 9 // wrong ??? 38 | */ 39 | } 40 | } 41 | 42 | fn test_array_append_workaround_sollution() ? { 43 | mut ar := []voidptr{} // []&XValue{} // A list of XValue pointers 44 | mut x := XListValue{} 45 | mut px := &x // Get a pointer to the 'x' XValue 46 | 47 | ar << px // Append the pointer to the list 48 | 49 | x.ar << 9 // Modify 'x'. 50 | 51 | assert x.ar.len == 1 52 | assert x.ar[0] == 9 53 | 54 | assert px.ar.len == 1 55 | assert px.ar[0] == 9 56 | 57 | // eprintln("x: $x") 58 | // eprintln("ar: $ar") 59 | 60 | px = ar.last() 61 | a0 := XValue(*px) 62 | if a0 is XListValue { 63 | assert a0.ar.len == 1 64 | assert a0.ar[0] == 9 65 | } 66 | } 67 | 68 | // --------------------------------------------------------------- 69 | // next() for iterations 70 | // --------------------------------------------------------------- 71 | 72 | struct MyData { 73 | pub mut: 74 | pos int 75 | } 76 | 77 | fn (mut this MyData) next() ?int { 78 | x := this.pos 79 | this.pos++ 80 | return x 81 | } 82 | 83 | fn test_next() { 84 | x := MyData{} 85 | mut j := 0 86 | for i in x { 87 | j = i 88 | if i > 9 { 89 | break 90 | } 91 | } 92 | assert j == 10 93 | } 94 | 95 | // --------------------------------------------------------------- 96 | // What happens to functions in embedded structs 97 | // --------------------------------------------------------------- 98 | 99 | struct Aa { 100 | mut: 101 | a int 102 | } 103 | 104 | fn (a Aa) get_a() int { 105 | return a.a 106 | } 107 | 108 | struct Bb { 109 | Aa 110 | mut: 111 | b string 112 | } 113 | 114 | fn test_embedded_structs() { 115 | // ??? It is not possible to use embedded vars in constructor 116 | // b := Bb{ a: 2, b: "test" } 117 | 118 | mut b := Bb{ 119 | b: 'test' 120 | } 121 | b.a = 2 122 | assert b.a == 2 123 | assert b.b == 'test' 124 | 125 | // It is not mentioned in the documentation, but it seems that also the methods 126 | // defined for the embedded struct can be used. 127 | assert b.get_a() == 2 128 | } 129 | 130 | // --------------------------------------------------------------- 131 | // Issue with array being past 132 | // --------------------------------------------------------------- 133 | 134 | struct MyData1 { 135 | pub mut: 136 | ar []int 137 | } 138 | 139 | fn pass_array_mut(mut ar []int) int { 140 | if ar.len > 0 && ar[ar.len - 1] == 99 { 141 | // if ar.len > 0 && ar.last() == 99 { 142 | return 99 143 | } 144 | return 0 145 | } 146 | 147 | fn test_pass_array() { 148 | mut m := MyData1{} 149 | m.ar << 99 150 | assert pass_array_mut(mut m.ar) == 99 151 | } 152 | -------------------------------------------------------------------------------- /ystrconv/interpolate.v: -------------------------------------------------------------------------------- 1 | module ystrconv 2 | 3 | import math 4 | import strings 5 | 6 | pub fn char_to_base(ch u8, base int) ?int { 7 | mut i := int(ch) 8 | if ch >= `0` && ch < (`0` + math.min(base, 10)) { 9 | i = i - int(`0`) 10 | } else if ch >= `A` && ch < (`A` + base - 10) { 11 | i = i - int(`A`) + 10 12 | } else if ch >= `a` && ch < (`a` + base - 10) { 13 | i = i - int(`a`) + 10 14 | } else { 15 | return error("Invalid digit for number with base $base: '$ch'") 16 | } 17 | return i 18 | } 19 | 20 | pub fn parse_number_fix_length(str string, pos int, len int, base int) ?i64 { 21 | if (pos + len) <= str.len { 22 | mut rtn := i64(0) 23 | for i in pos .. (pos + len) { 24 | ch := str[i] 25 | x := char_to_base(ch, base) ? 26 | rtn = (rtn * i64(base)) + i64(x) 27 | } 28 | return rtn 29 | } 30 | return error("Invalid length. Expected $len more chars: '${str[pos..]}'") 31 | } 32 | 33 | pub fn parse_number_variable_length(str string, pos int, base int) ?i64 { 34 | mut rtn := i64(0) 35 | for ch in str[pos..] { 36 | x := char_to_base(ch, base) ? 37 | rtn = (rtn * i64(base)) + i64(x) 38 | } 39 | return rtn 40 | } 41 | 42 | pub fn int_to_bytes(i i64) []u8 { 43 | if i < 0x0100 { 44 | return [u8(i)] 45 | } 46 | 47 | a := u8((i >> 0) & 0xff) 48 | b := u8((i >> 8) & 0xff) 49 | if i < 0x1_0000 { 50 | return [b, a] 51 | } 52 | 53 | c := u8((i >> 16) & 0xff) 54 | d := u8((i >> 24) & 0xff) 55 | if i < 0x1_0000_0000 { 56 | return [d, c, b, a] 57 | } 58 | 59 | e := u8((i >> 32) & 0xff) 60 | f := u8((i >> 40) & 0xff) 61 | g := u8((i >> 48) & 0xff) 62 | h := u8((i >> 56) & 0xff) 63 | return [h, g, f, e, d, c, b, a] 64 | } 65 | 66 | pub fn interpolate_double_quoted_string(val string) ?string { 67 | if val.contains('\\') == false { 68 | return val 69 | } 70 | 71 | mut str := strings.new_builder(val.len) 72 | mut pos := 0 73 | for pos < val.len { 74 | ch := val[pos] 75 | if ch == `\\` && (pos + 1) < val.len { 76 | x := val[pos + 1] 77 | if x == `a` { str.write_byte(0x07) } 78 | else if x == `b` { str.write_byte(0x08) } 79 | else if x == `e` { str.write_byte(0x1b) } 80 | else if x == `f` { str.write_byte(0x0c) } 81 | else if x == `n` { str.write_byte(0x0a) } 82 | else if x == `r` { str.write_byte(0x0d) } 83 | else if x == `t` { str.write_byte(0x09) } 84 | else if x == `v` { str.write_byte(0x0b) } 85 | else if x == `x` { 86 | str.write_string(int_to_bytes(parse_number_fix_length(val, pos + 2, 2, 87 | 16) ?).bytestr()) 88 | pos += 2 89 | } else if x == `u` { 90 | cp := parse_number_fix_length(val, pos + 2, 4, 16) ? 91 | str.write_rune(rune(u32(cp))) 92 | pos += 4 93 | } else if x == `U` { 94 | cp := parse_number_fix_length(val, pos + 2, 8, 16) ? 95 | str.write_rune(rune(u32(cp))) 96 | pos += 8 97 | } else if x >= `0` && x < `8` { 98 | str.write_string(int_to_bytes(parse_number_fix_length(val, pos + 1, 3, 99 | 8) ?).bytestr()) 100 | pos += 2 101 | } else { 102 | // Has no special meaning 103 | str.write_byte(val[pos + 1]) 104 | } 105 | pos++ 106 | } else { 107 | str.write_byte(val[pos]) 108 | } 109 | pos++ 110 | } 111 | 112 | return str.str() 113 | } 114 | 115 | // interpolate_single_quoted_string In Yaml single quoted strings are used 116 | // when unescaping is not what you want. The only exception being '', which 117 | // will be replaced with a single quote, e.g. 'this is a ''test''' 118 | pub fn interpolate_single_quoted_string(val string) string { 119 | return val.replace("''", "'") 120 | } 121 | 122 | // interpolate_plain_value 123 | // 0x1A 124 | // 0o12 125 | // 0b1100100 126 | pub fn interpolate_plain_value(str string) string { 127 | $if test { 128 | assert str.len > 1 129 | } 130 | mut base := 10 131 | mut prefix_len := 0 132 | if str[0] == `0` { 133 | if str[1] == `x` && str.len > 2 { 134 | base = 16 135 | prefix_len = 2 136 | } else if str[1] == `o` && str.len > 2 { 137 | base = 8 138 | prefix_len = 2 139 | } else if str[1] == `b` && str.len > 2 { 140 | base = 2 141 | prefix_len = 2 142 | } else { 143 | $if yaml_1_1_octal ? { 144 | if str.len > 1 { 145 | for i in 1 .. str.len { 146 | if str[i] < `0` || str[i] > `7` { 147 | return str 148 | } 149 | } 150 | base = 8 151 | prefix_len = 1 152 | } else { 153 | return str 154 | } 155 | } $else { 156 | return str 157 | } 158 | } 159 | 160 | x := parse_number_variable_length(str, prefix_len, base) or { return str } 161 | 162 | return x.str() 163 | } else { 164 | return str 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /yaml_obj_accessor.v: -------------------------------------------------------------------------------- 1 | module yaml 2 | 3 | pub type PathType = int | string 4 | 5 | pub fn (this YamlValue) get(path ...PathType) ?YamlValue { 6 | mut rtn := this 7 | 8 | for p in path { 9 | if p is int { 10 | if mut rtn is YamlListValue { 11 | if p < 0 || p >= rtn.ar.len { 12 | return error("Invalid index to access Yaml list element: '$p'") 13 | } 14 | rtn = rtn.ar[p] 15 | } else { 16 | return error("'int' path element can only be used with Yaml lists") 17 | } 18 | } else if p is string { 19 | if mut rtn is YamlMapValue { 20 | if p in rtn.obj { 21 | rtn = rtn.obj[p] ? 22 | } else { 23 | return error("Key not found in Yaml map element: '$p'") 24 | } 25 | } else { 26 | return error("'string' path element can only be used with Yaml maps") 27 | } 28 | } 29 | } 30 | 31 | return rtn 32 | } 33 | 34 | pub fn (this YamlValue) is_list() bool { 35 | return this is YamlListValue 36 | } 37 | 38 | pub fn (this YamlValue) is_map() bool { 39 | return this is YamlMapValue 40 | } 41 | 42 | pub fn (this YamlValue) is_value() bool { 43 | return match this { 44 | string { true } 45 | i64 { true } 46 | f64 { true } 47 | bool { true } 48 | else { false } 49 | } 50 | } 51 | 52 | pub fn (this YamlValue) len() int { 53 | if this is string { 54 | return this.len 55 | } else if this is YamlListValue { 56 | return this.ar.len 57 | } else if this is YamlMapValue { 58 | return this.obj.len 59 | } else { 60 | panic("Invalid YamlValue to determine 'len': $this") 61 | } 62 | } 63 | 64 | pub fn (this YamlValue) is_empty() bool { 65 | return this.len() == 0 66 | } 67 | 68 | pub fn (this YamlValue) string() ?string { 69 | match this { 70 | string { 71 | return this 72 | } 73 | i64 { 74 | return this.str() 75 | } 76 | f64 { 77 | return this.str() 78 | } 79 | bool { 80 | return this.str() 81 | } 82 | else { 83 | return error("Invalid YamlValue to return a 'string': $this") 84 | } 85 | } 86 | } 87 | 88 | pub fn (this YamlValue) get_str(path ...PathType) ?string { 89 | return this.get(...path) ?.string() 90 | } 91 | 92 | pub fn (this YamlValue) get_int(path ...PathType) ?int { 93 | return this.get(...path) ?.int() 94 | } 95 | 96 | pub fn (this YamlValue) get_float(path ...PathType) ?f64 { 97 | return this.get(...path) ?.f64() 98 | } 99 | 100 | pub fn (this YamlValue) get_bool(path ...PathType) ?bool { 101 | return this.get(...path) ?.bool() 102 | } 103 | 104 | pub fn (this YamlValue) i8() ?i8 { 105 | return i8(this.i64() ?) 106 | } 107 | 108 | pub fn (this YamlValue) i16() ?i16 { 109 | return i16(this.i64() ?) 110 | } 111 | 112 | pub fn (this YamlValue) int() ?int { 113 | return int(this.i64() ?) 114 | } 115 | 116 | pub fn (this YamlValue) i64() ?i64 { 117 | if this is i64 { 118 | return this 119 | } 120 | panic('Unable to determine i64 value: $this') 121 | } 122 | 123 | pub fn (this YamlValue) u8() ?u8 { 124 | return u8(this.i64() ?) 125 | } 126 | 127 | pub fn (this YamlValue) u16() ?u16 { 128 | return u16(this.i64() ?) 129 | } 130 | 131 | pub fn (this YamlValue) u32() ?u32 { 132 | return u32(this.i64() ?) 133 | } 134 | 135 | pub fn (this YamlValue) u64() ?u64 { 136 | return u64(this.i64() ?) 137 | } 138 | 139 | pub fn (this YamlValue) f32() ?f32 { 140 | return f32(this.f64() ?) 141 | } 142 | 143 | pub fn (this YamlValue) f64() ?f64 { 144 | if this is f64 { 145 | return this 146 | } 147 | panic('Unable to determine f64 value: $this') 148 | } 149 | 150 | // TODO Tests are missing?? 151 | pub fn (this YamlValue) bool() ?bool { 152 | if this is bool { 153 | return this 154 | } 155 | if this is i64 { 156 | return if this == 0 { false } else { true } 157 | } 158 | panic('Unable to determine f64 value: $this') 159 | } 160 | 161 | // get_date Return an integer with YYYYMMDD digits 162 | // TODO To be implemented 163 | // TODO How to make YamlValue extendable, so that USERS can extend it with their own converters? 164 | pub fn (this YamlValue) get_date(fmt string) ?i64 { 165 | // TODO Does i64()? panic if not an int ?!?!? 166 | return this.string() ?.i64() 167 | } 168 | 169 | // get_time Return an integer with HHMMSS digits 170 | // TODO To be implemented 171 | // TODO How to make YamlValue extendable, so that USERS can extend it with their own converters? 172 | pub fn (this YamlValue) get_time(fmt string) ?i64 { 173 | return this.string() ?.i64() 174 | } 175 | 176 | // get_millis Return milli seconds since 1970 (UNIX style) 177 | // TODO To be implemented 178 | // TODO How to make YamlValue extendable, so that USERS can extend it with their own converters? 179 | pub fn (this YamlValue) get_millis(fmt string) ?i64 { 180 | return this.string() ?.i64() 181 | } 182 | -------------------------------------------------------------------------------- /step2_tokenizer_test.v: -------------------------------------------------------------------------------- 1 | module yaml 2 | 3 | // YAML spec: https://yaml.org/spec/1.2/spec.html 4 | // To test your YAML: https://www.json2yaml.com/ 5 | import os 6 | 7 | const test_data_dir = os.dir(@FILE) + '/test_data' 8 | 9 | const debug = 0 10 | 11 | fn test_is_quoted() ? { 12 | assert is_quoted('', `'`) == false 13 | assert is_quoted('a', `'`) == false 14 | assert is_quoted('aa', `'`) == false 15 | assert is_quoted('aaa', `'`) == false 16 | assert is_quoted("a'b'", `'`) == false 17 | assert is_quoted("'a'b", `'`) == false 18 | assert is_quoted("'ab'", `'`) == true 19 | assert is_quoted("'a'", `'`) == true 20 | assert is_quoted("''", `'`) == true 21 | assert is_quoted("''", `"`) == false 22 | assert is_quoted('"ab"', `"`) == true 23 | assert is_quoted('"a"', `"`) == true 24 | assert is_quoted('""', `"`) == true 25 | } 26 | 27 | fn test_remove_quotes() ? { 28 | assert remove_quotes('') ? == '' 29 | assert remove_quotes('a') ? == 'a' 30 | assert remove_quotes('aa') ? == 'aa' 31 | assert remove_quotes('aaa') ? == 'aaa' 32 | assert remove_quotes("a'b'") ? == "a'b'" 33 | assert remove_quotes("'a'b") ? == "'a'b" 34 | assert remove_quotes("'ab'") ? == 'ab' 35 | assert remove_quotes("'a'") ? == 'a' 36 | assert remove_quotes("''") ? == '' 37 | assert remove_quotes('"ab"') ? == 'ab' 38 | assert remove_quotes('"a"') ? == 'a' 39 | assert remove_quotes('""') ? == '' 40 | } 41 | 42 | fn test_to_value_type() ? { 43 | assert to_value_type('') ? == YamlTokenValueType('') 44 | assert to_value_type('a') ? == YamlTokenValueType('a') 45 | assert to_value_type('aa') ? == YamlTokenValueType('aa') 46 | assert to_value_type('aaa') ? == YamlTokenValueType('aaa') 47 | assert to_value_type("a'b'") ? == YamlTokenValueType("a'b'") 48 | assert to_value_type("'a'b") ? == YamlTokenValueType("'a'b") 49 | assert to_value_type("'ab'") ? == YamlTokenValueType('ab') 50 | assert to_value_type("'a'") ? == YamlTokenValueType('a') 51 | assert to_value_type("''") ? == YamlTokenValueType('') 52 | assert to_value_type('"ab"') ? == YamlTokenValueType('ab') 53 | assert to_value_type('"a"') ? == YamlTokenValueType('a') 54 | assert to_value_type('""') ? == YamlTokenValueType('') 55 | 56 | assert to_value_type('1') ? == YamlTokenValueType(i64(1)) 57 | assert to_value_type('+1') ? == YamlTokenValueType(i64(1)) 58 | assert to_value_type('-1') ? == YamlTokenValueType(i64(-1)) 59 | assert to_value_type('100') ? == YamlTokenValueType(i64(100)) 60 | 61 | assert to_value_type('1.') ? == YamlTokenValueType(f64(1)) 62 | assert to_value_type('1.1') ? == YamlTokenValueType(f64(1.1)) 63 | assert to_value_type('.1') ? == YamlTokenValueType(f64(0.1)) 64 | assert to_value_type('1e2') ? == YamlTokenValueType(f64(100)) 65 | assert to_value_type('10e+2') ? == YamlTokenValueType(f64(1000)) 66 | assert to_value_type('10e-2') ? == YamlTokenValueType(f64(.1)) 67 | 68 | assert to_value_type('450.00') ? == YamlTokenValueType(f64(450)) 69 | assert to_value_type('2392.00') ? == YamlTokenValueType(f64(2392)) 70 | 71 | assert to_value_type('true') ? == YamlTokenValueType(true) 72 | assert to_value_type('false') ? == YamlTokenValueType(false) 73 | assert to_value_type('yes') ? == YamlTokenValueType(true) 74 | assert to_value_type('no') ? == YamlTokenValueType(false) 75 | assert to_value_type('0') ? == YamlTokenValueType(i64(0)) 76 | assert to_value_type('1') ? == YamlTokenValueType(i64(1)) 77 | } 78 | 79 | fn test_z_ex_10_resolve_tags() ? { 80 | content := os.read_file('$test_data_dir/z_ex_10.yaml') ? 81 | tokenizer := yaml_tokenizer(content, replace_tags: true, debug: debug) ? 82 | assert tokenizer.tags.len == 1 83 | assert tokenizer.tokens.len == 14 84 | assert tokenizer.tokens[5] == tokenizer.tokens[9] // "SS" tag 85 | // for i, tok in tokenizer.tokens { eprintln("$i: $tok") } 86 | } 87 | 88 | fn test_z_ex_24_resolve_tags() ? { 89 | content := os.read_file('$test_data_dir/z_ex_24.yaml') ? 90 | tokenizer := yaml_tokenizer(content, replace_tags: true, debug: debug) ? 91 | assert tokenizer.tags.len == 1 92 | assert tokenizer.tokens.len == 44 93 | // for i, tok in tokenizer.tokens { eprintln("$i: $tok.typ, $tok.val") } 94 | assert tokenizer.tokens[4] == tokenizer.tokens[15] // 1. "ORIGIN" tag 95 | assert tokenizer.tokens[4] == tokenizer.tokens[22] // 2. "ORIGIN" tag 96 | } 97 | 98 | fn test_z_ex_27_resolve_tags() ? { 99 | content := os.read_file('$test_data_dir/z_ex_27.yaml') ? 100 | tokenizer := yaml_tokenizer(content, replace_tags: true, debug: debug) ? 101 | assert tokenizer.tags.len == 1 102 | assert tokenizer.tokens.len == 73 103 | // for i, tok in tokenizer.tokens { eprintln("$i: $tok.typ, $tok.val") } 104 | assert tokenizer.tokens[8] == tokenizer.tokens[26] // "id001" tag 105 | assert tokenizer.tokens[15] == tokenizer.tokens[33] 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Native YAML support for [V-lang](https://vlang.io) 2 | 3 | I think this YAML parser covers many important aspects of the YAML spec, but certainly not everything. I've been using the [YAML spec](https://yaml.org/spec/1.2/spec.html) and [this online tool](https://www.json2yaml.com/) whenever I was in doubt. 4 | 5 | This is an initial version. Please apply some caution and report issues back to me. 6 | 7 | Since this is an initial version, it is not yet performance tested. I'm sure there is room for improvements. 8 | 9 | 10 | ## Key Features 11 | 12 | - Read and parse yaml files (or data blocks); utf-8 only 13 | - Tokenize the YAML file and derive YAML events (tokens) 14 | - Convert YAML events to a JSON string (and leverage V's built-in JSON decoder to load json) 15 | - Load the YAML file into a tree-like structure of dynamic YamlValue's (V is currently lacking the feature to map (YAML) values to struct variables. The respective JSON logic is build into the compiler. It is not possible for 16 | users to define their own [attributes] right now) 17 | - Value types, such as string, int, float and bool are auto-detected and carried forward 18 | - Tag definitions and references are mostly supported. By default references get replaced with their definition. 19 | - Multiple YAML documents within a file is supported (mostly) 20 | - un-escape and interpolate strings, hex numbers, etc. 21 | 22 | 23 | ## Architecture 24 | 25 | ``` 26 | TextScanner: split text into string tokens (generic) 27 | -> Scanner: map into basic yaml-like tokens 28 | -> Tokenizer: derive proper yaml tokens (events) 29 | -> JSON output: convert into JSON string 30 | -> print 31 | -> json.decode() V built-in: load into V struct's 32 | -> Reader: load into dynamic YamlValue structure 33 | -> Accessor: path-like access to tree-like YamlValue structure 34 | ``` 35 | 36 | **TextScanner**: A generic module, not YAML specific, with re-useable functions to detect newline, handle line count, skip whitespace, move to end-of-line, set a marker at a certain position and retrieve the text between the marker and the current postion, read quoted strings, etc. 37 | 38 | **Scanner**: Leverage TextScanner to parse the YAML file into basic YAML tokens, properly determine indentation (very important in YAML), the different multi-line text variants, identify special chars such as `-:{}[],!---...`, etc. The token generated consist of a type, the indentiation level and the associated string. 39 | 40 | **Tokenizer**: What we really want are YAML events such as start-list, close, start-object, key, value, end-of-document, tag-definition, tag-reference, etc.. YAML files very human readable, but not very computer friendly. The Tokenizer creates a nice stream of YAML events that can easily be leveraged for different purposes, such as generate JSON, or create dynamic YamlValue's. 41 | 42 | **JSON output**: Convert the stream of YAML events into a JSON string. This json string can be used by V's build-in decoder to load the YAML data into V struct's. 43 | 44 | **YAML Reader**: User defined [attributes] are not yet supported. Hence the YAML data can only be loaded into V struct's via JSON's built-in JSON decoder. Reader creates a completely dynamic tree-like structure, based on YamlValue's, reflecting lists, objects, key and values, including value types (string, int, float, bool). This is a little bit like in dynamic languages, such as Python. 45 | 46 | **Accessor**: Traversing the tree of YamlValue's that make up a yaml file, is not especially pleasant. Accessor provides getter functions, so that by means of a 'path' the value can be accessed. Additionally type converters are provided, to return i8, i16, int, i64, f32, f64, bool etc. values. 47 | 48 | ## API 49 | 50 | ```v 51 | import yaml 52 | 53 | content := os.read_file("myfile.yaml")? 54 | scanner := yaml.yaml_scanner(content, debug)? 55 | for i, tok in scanner.tokens { .. } 56 | 57 | tokenizer := yaml.yaml_tokenizer(content, replace_tags: true, debug: debug)? 58 | for i, tok in tokenizer.tokens { .. } 59 | 60 | json_data := yaml.yaml_to_json(content, replace_tags: true, debug: debug)? 61 | println(json_data) 62 | 63 | docs := yaml.yaml_reader(content)? 64 | x := docs.get(0) // Most files have only 1 (implict) document 65 | assert x.get("american", 0)?.string()? == "Boston Red Sox" 66 | 67 | // with additional options 68 | docs := yaml.yaml_reader(content, replace_tags: yaml.ReplaceTagsEnum.in_reader, debug: debug)? 69 | 70 | // Path-like getter for YAML values 71 | assert x1.get(0, "center", "x")?.int()? == 73 72 | ``` 73 | 74 | ## Examples 75 | 76 | There is a reasonable amount of test cases for each major component. Probably a good starting point for anybody who want to dig a bit deeper. 77 | 78 | ### Yaml-to-json command line tool 79 | 80 | In the `./examples` folder is a little command line utility that reads a yaml file and prints json to the console 81 | -------------------------------------------------------------------------------- /yaml_to_json.v: -------------------------------------------------------------------------------- 1 | module yaml 2 | 3 | import strings 4 | import os 5 | import json 6 | 7 | // YamlJson This is mostly an internal struct to namespace some functions. 8 | pub struct YamlJson { 9 | tokens []YamlToken 10 | } 11 | 12 | // yaml_to_json convert YAML content into a JSON string 13 | pub fn yaml_to_json(content string, args NewTokenizerParams) ?string { 14 | tokenizer := yaml_tokenizer(content, args) ? 15 | 16 | mut json := YamlJson{ 17 | tokens: tokenizer.tokens 18 | } 19 | 20 | return json.yaml_to_json_root(args.debug) 21 | } 22 | 23 | // yaml_file_to_json Read the yaml file and convert it to JSON string 24 | pub fn yaml_file_to_json(fpath string, args NewTokenizerParams) ?string { 25 | content := os.read_file(fpath) ? 26 | return yaml_to_json(content, args) 27 | } 28 | 29 | // yaml_to_json_root Main entry point for converting the YAML tokens 30 | // into a JSON string 31 | pub fn (mut json YamlJson) yaml_to_json_root(debug int) ?string { 32 | if debug > 1 { 33 | eprintln('-------- yaml_to_json') 34 | } 35 | 36 | mut str := strings.new_builder(4000) 37 | mut pos := 0 38 | for pos < json.tokens.len { 39 | tok := json.tokens[pos] 40 | if debug > 1 { 41 | eprintln("pos: $pos, type: $tok.typ, val: '$tok.val'") 42 | } 43 | if tok.typ == YamlTokenKind.start_list { 44 | pos = json.yaml_to_json_list_parent(pos + 1, mut str, debug) ? 45 | } else if tok.typ == YamlTokenKind.start_object { 46 | pos = json.yaml_to_json_map_parent(pos + 1, mut str, debug) ? 47 | } else if tok.typ == YamlTokenKind.value { 48 | str.write_string(tok.val.format()) 49 | } else if tok.typ == YamlTokenKind.close { 50 | // ignore 51 | } else if tok.typ == YamlTokenKind.end_of_document { 52 | break 53 | } else if tok.typ == YamlTokenKind.new_document { 54 | // ignore 55 | } else if tok.typ == YamlTokenKind.tag_def { 56 | // ignore 57 | } else { 58 | str.write_string(tok.val.format()) 59 | } 60 | 61 | pos += 1 62 | } 63 | 64 | if debug > 1 { 65 | eprintln('-------- yaml_to_json finished') 66 | } 67 | return str.str() 68 | } 69 | 70 | fn (mut json YamlJson) yaml_to_json_list_parent(idx int, mut str strings.Builder, debug int) ?int { 71 | str.write_byte(`[`) 72 | mut pos := idx 73 | for pos < json.tokens.len { 74 | tok := json.tokens[pos] 75 | if debug > 1 { 76 | eprintln("pos: $pos, type: $tok.typ, val: '$tok.val'") 77 | } 78 | if tok.typ == YamlTokenKind.start_list { 79 | if pos > idx { 80 | str.write_string(', ') 81 | } 82 | pos = json.yaml_to_json_list_parent(pos + 1, mut str, debug) ? 83 | } else if tok.typ == YamlTokenKind.start_object { 84 | if pos > idx { 85 | str.write_string(', ') 86 | } 87 | pos = json.yaml_to_json_map_parent(pos + 1, mut str, debug) ? 88 | } else if tok.typ == YamlTokenKind.value { 89 | if pos > idx { 90 | str.write_string(', ') 91 | } 92 | str.write_string(tok.val.format()) 93 | } else if tok.typ == YamlTokenKind.tag_ref { 94 | if str.len > 0 { 95 | str.write_string(', ') 96 | } 97 | x := tok.val.format() 98 | str.write_string('"*') 99 | str.write_string(x[1..]) 100 | } else if tok.typ == YamlTokenKind.close { 101 | str.write_byte(`]`) 102 | break 103 | } else if tok.typ == YamlTokenKind.end_of_document { 104 | break 105 | } else if tok.typ == YamlTokenKind.tag_def { 106 | // ignore 107 | } else { 108 | // ignore 109 | } 110 | 111 | pos += 1 112 | } 113 | 114 | return pos 115 | } 116 | 117 | fn (mut json YamlJson) yaml_to_json_map_parent(idx int, mut str strings.Builder, debug int) ?int { 118 | str.write_byte(`{`) 119 | mut pos := idx 120 | for pos < json.tokens.len { 121 | tok := json.tokens[pos] 122 | if debug > 1 { 123 | eprintln("pos: $pos, type: $tok.typ, val: '$tok.val'") 124 | } 125 | if tok.typ == YamlTokenKind.start_list { 126 | pos = json.yaml_to_json_list_parent(pos + 1, mut str, debug) ? 127 | } else if tok.typ == YamlTokenKind.start_object { 128 | pos = json.yaml_to_json_map_parent(pos + 1, mut str, debug) ? 129 | } else if tok.typ == YamlTokenKind.key { 130 | if pos > idx { 131 | str.write_string(', ') 132 | } 133 | x := tok.val.format() 134 | if x.len > 0 && x[0] in [`"`, `'`] { 135 | str.write_string(x) 136 | str.write_string(': ') 137 | } else { 138 | str.write_byte(`"`) 139 | str.write_string(x) 140 | str.write_string('": ') 141 | } 142 | } else if tok.typ == YamlTokenKind.value { 143 | str.write_string(tok.val.format()) 144 | } else if tok.typ == YamlTokenKind.tag_ref { 145 | x := tok.val.format() 146 | str.write_string('"*') 147 | str.write_string(x[1..]) 148 | } else if tok.typ == YamlTokenKind.close { 149 | str.write_byte(`}`) 150 | break 151 | } else if tok.typ == YamlTokenKind.end_of_document { 152 | break 153 | } else if tok.typ == YamlTokenKind.tag_def { 154 | // ignore 155 | } else { 156 | // ignore 157 | } 158 | 159 | pos += 1 160 | } 161 | 162 | return pos 163 | } 164 | -------------------------------------------------------------------------------- /step1_scanner_test.v: -------------------------------------------------------------------------------- 1 | module yaml 2 | 3 | // YAML spec: https://yaml.org/spec/1.2/spec.html 4 | // To test your YAML: https://www.json2yaml.com/ 5 | import os 6 | 7 | const test_data_dir = os.dir(@FILE) + '/test_data' 8 | 9 | const debug = 0 10 | 11 | const ( 12 | z27 = [ 13 | Token{ column: 1, typ: TokenKind.document, val: '---' }, 14 | Token{ column: 1, typ: TokenKind.tag_url, val: '' }, 15 | Token{ column: 1, typ: TokenKind.newline, val: '' }, 16 | Token{ column: 1, typ: TokenKind.xstr, val: 'invoice' }, 17 | Token{ column: 1, typ: TokenKind.colon, val: ':' }, 18 | Token{ column: 1, typ: TokenKind.xstr, val: '34843' }, 19 | Token{ column: 1, typ: TokenKind.newline, val: '' }, 20 | Token{ column: 1, typ: TokenKind.xstr, val: 'date' }, 21 | Token{ column: 1, typ: TokenKind.colon, val: ':' }, 22 | Token{ column: 1, typ: TokenKind.xstr, val: '2001-01-23' }, 23 | Token{ column: 1, typ: TokenKind.newline, val: '' }, 24 | Token{ column: 1, typ: TokenKind.xstr, val: 'bill-to' }, 25 | Token{ column: 1, typ: TokenKind.colon, val: ':' }, 26 | Token{ column: 1, typ: TokenKind.tag_def, val: 'id001' }, 27 | Token{ column: 1, typ: TokenKind.newline, val: '' }, 28 | Token{ column: 5, typ: TokenKind.xstr, val: 'given' }, 29 | Token{ column: 5, typ: TokenKind.colon, val: ':' }, 30 | Token{ column: 5, typ: TokenKind.xstr, val: 'Chris' }, 31 | Token{ column: 5, typ: TokenKind.newline, val: '' }, 32 | Token{ column: 5, typ: TokenKind.xstr, val: 'family' }, 33 | Token{ column: 5, typ: TokenKind.colon, val: ':' }, 34 | Token{ column: 5, typ: TokenKind.xstr, val: 'Dumars' }, 35 | Token{ column: 5, typ: TokenKind.newline, val: '' }, 36 | Token{ column: 5, typ: TokenKind.xstr, val: 'address' }, 37 | Token{ column: 5, typ: TokenKind.colon, val: ':' }, 38 | Token{ column: 5, typ: TokenKind.newline, val: '' }, 39 | Token{ column: 9, typ: TokenKind.xstr, val: 'lines' }, 40 | Token{ column: 9, typ: TokenKind.colon, val: ':' }, 41 | Token{ column: 9, typ: TokenKind.xstr, val: '458 Walkman Dr.\nSuite #292'}, 42 | Token{ column: 9, typ: TokenKind.newline, val: '' }, 43 | Token{ column: 9, typ: TokenKind.xstr, val: 'city' }, 44 | Token{ column: 9, typ: TokenKind.colon, val: ':' }, 45 | Token{ column: 9, typ: TokenKind.xstr, val: 'Royal Oak' }, 46 | Token{ column: 9, typ: TokenKind.newline, val: '' }, 47 | Token{ column: 9, typ: TokenKind.xstr, val: 'state' }, 48 | Token{ column: 9, typ: TokenKind.colon, val: ':' }, 49 | Token{ column: 9, typ: TokenKind.xstr, val: 'MI' }, 50 | Token{ column: 9, typ: TokenKind.newline, val: '' }, 51 | Token{ column: 9, typ: TokenKind.xstr, val: 'postal' }, 52 | Token{ column: 9, typ: TokenKind.colon, val: ':' }, 53 | Token{ column: 9, typ: TokenKind.xstr, val: '48046' }, 54 | Token{ column: 9, typ: TokenKind.newline, val: '' }, 55 | Token{ column: 1, typ: TokenKind.xstr, val: 'ship-to' }, 56 | Token{ column: 1, typ: TokenKind.colon, val: ':' }, 57 | Token{ column: 1, typ: TokenKind.tag_ref, val: 'id001' }, 58 | Token{ column: 1, typ: TokenKind.newline, val: '' }, 59 | Token{ column: 1, typ: TokenKind.xstr, val: 'product' }, 60 | Token{ column: 1, typ: TokenKind.colon, val: ':' }, 61 | Token{ column: 1, typ: TokenKind.newline, val: '' }, 62 | Token{ column: 6, typ: TokenKind.hyphen, val: '-' }, 63 | Token{ column: 7, typ: TokenKind.xstr, val: 'sku' }, 64 | Token{ column: 7, typ: TokenKind.colon, val: ':' }, 65 | Token{ column: 7, typ: TokenKind.xstr, val: 'BL394D' }, 66 | Token{ column: 7, typ: TokenKind.newline, val: '' }, 67 | Token{ column: 7, typ: TokenKind.xstr, val: 'quantity' }, 68 | Token{ column: 7, typ: TokenKind.colon, val: ':' }, 69 | Token{ column: 7, typ: TokenKind.xstr, val: '4' }, 70 | Token{ column: 7, typ: TokenKind.newline, val: '' }, 71 | Token{ column: 7, typ: TokenKind.xstr, val: 'description' }, 72 | Token{ column: 7, typ: TokenKind.colon, val: ':' }, 73 | Token{ column: 7, typ: TokenKind.xstr, val: 'Basketball' }, 74 | Token{ column: 7, typ: TokenKind.newline, val: '' }, 75 | Token{ column: 7, typ: TokenKind.xstr, val: 'price' }, 76 | Token{ column: 7, typ: TokenKind.colon, val: ':' }, 77 | Token{ column: 7, typ: TokenKind.xstr, val: '450.00' }, 78 | Token{ column: 7, typ: TokenKind.newline, val: '' }, 79 | Token{ column: 6, typ: TokenKind.hyphen, val: '-' }, 80 | Token{ column: 7, typ: TokenKind.xstr, val: 'sku' }, 81 | Token{ column: 7, typ: TokenKind.colon, val: ':' }, 82 | Token{ column: 7, typ: TokenKind.xstr, val: 'BL4438H' }, 83 | Token{ column: 7, typ: TokenKind.newline, val: '' }, 84 | Token{ column: 7, typ: TokenKind.xstr, val: 'quantity' }, 85 | Token{ column: 7, typ: TokenKind.colon, val: ':' }, 86 | Token{ column: 7, typ: TokenKind.xstr, val: '1' }, 87 | Token{ column: 7, typ: TokenKind.newline, val: '' }, 88 | Token{ column: 7, typ: TokenKind.xstr, val: 'description' }, 89 | Token{ column: 7, typ: TokenKind.colon, val: ':' }, 90 | Token{ column: 7, typ: TokenKind.xstr, val: 'Super Hoop' }, 91 | Token{ column: 7, typ: TokenKind.newline, val: '' }, 92 | Token{ column: 7, typ: TokenKind.xstr, val: 'price' }, 93 | Token{ column: 7, typ: TokenKind.colon, val: ':' }, 94 | Token{ column: 7, typ: TokenKind.xstr, val: '2392.00' }, 95 | Token{ column: 7, typ: TokenKind.newline, val: '' }, 96 | Token{ column: 1, typ: TokenKind.xstr, val: 'tax' }, 97 | Token{ column: 1, typ: TokenKind.colon, val: ':' }, 98 | Token{ column: 1, typ: TokenKind.xstr, val: '251.42' }, 99 | Token{ column: 1, typ: TokenKind.newline, val: '' }, 100 | Token{ column: 1, typ: TokenKind.xstr, val: 'total' }, 101 | Token{ column: 1, typ: TokenKind.colon, val: ':' }, 102 | Token{ column: 1, typ: TokenKind.xstr, val: '4443.52' }, 103 | Token{ column: 1, typ: TokenKind.newline, val: '' }, 104 | Token{ column: 1, typ: TokenKind.xstr, val: 'comments' }, 105 | Token{ column: 1, typ: TokenKind.colon, val: ':' }, 106 | Token{ column: 1, typ: TokenKind.newline, val: '' }, 107 | Token{ column: 5, typ: TokenKind.xstr, val: 'Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338.' }, 108 | ] 109 | ) 110 | 111 | fn test_z_ex_27() ? { 112 | content := os.read_file('$test_data_dir/z_ex_27.yaml') ? 113 | scanner := yaml_scanner(content, debug) ? 114 | 115 | for i, tok in scanner.tokens { 116 | assert tok.typ == z27[i].typ 117 | assert tok.column == z27[i].column 118 | assert tok.val == z27[i].val 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /ystrconv/interpolate_test.v: -------------------------------------------------------------------------------- 1 | module ystrconv 2 | 3 | import encoding.utf8 4 | 5 | fn test_char_to_base() ? { 6 | assert char_to_base(`0`, 8) ? == 0 7 | assert char_to_base(`0`, 10) ? == 0 8 | assert char_to_base(`0`, 16) ? == 0 9 | 10 | assert char_to_base(`7`, 8) ? == 7 11 | assert char_to_base(`7`, 10) ? == 7 12 | assert char_to_base(`7`, 16) ? == 7 13 | 14 | if _ := char_to_base(`8`, 8) { assert false } 15 | assert char_to_base(`8`, 10) ? == 8 16 | assert char_to_base(`8`, 16) ? == 8 17 | 18 | if _ := char_to_base(`A`, 8) { assert false } 19 | if _ := char_to_base(`A`, 10) { assert false } 20 | assert char_to_base(`A`, 16) ? == 10 21 | 22 | if _ := char_to_base(`a`, 8) { assert false } 23 | if _ := char_to_base(`a`, 10) { assert false } 24 | assert char_to_base(`a`, 16) ? == 10 25 | 26 | if _ := char_to_base(`F`, 8) { assert false } 27 | if _ := char_to_base(`F`, 10) { assert false } 28 | assert char_to_base(`F`, 16) ? == 15 29 | 30 | if _ := char_to_base(`G`, 8) { assert false } 31 | if _ := char_to_base(`G`, 10) { assert false } 32 | if _ := char_to_base(`G`, 16) { assert false } 33 | } 34 | 35 | fn test_parse_number_fix_length() ? { 36 | assert parse_number_fix_length('\\000', 1, 3, 8) ? == i64(0) 37 | assert parse_number_fix_length('\\001', 1, 3, 8) ? == i64(1) 38 | assert parse_number_fix_length('\\010', 1, 3, 8) ? == i64(8) 39 | assert parse_number_fix_length('\\100', 1, 3, 8) ? == i64(64) 40 | assert parse_number_fix_length('\\111', 1, 3, 8) ? == i64(73) 41 | assert parse_number_fix_length('\\111abc', 1, 3, 8) ? == i64(73) 42 | assert parse_number_fix_length('a\\111', 2, 3, 8) ? == i64(73) 43 | 44 | assert parse_number_fix_length('\\x00', 2, 2, 16) ? == i64(0) 45 | assert parse_number_fix_length('\\x01', 2, 2, 16) ? == i64(1) 46 | assert parse_number_fix_length('\\x10', 2, 2, 16) ? == i64(16) 47 | assert parse_number_fix_length('\\x11', 2, 2, 16) ? == i64(17) 48 | assert parse_number_fix_length('\\xff', 2, 2, 16) ? == i64(255) 49 | assert parse_number_fix_length('\\xFF', 2, 2, 16) ? == i64(255) 50 | 51 | assert parse_number_fix_length('\\x11', 2, 2, 16) ? == i64(17) 52 | assert parse_number_fix_length('\\x11abc', 2, 2, 16) ? == i64(17) 53 | assert parse_number_fix_length('a\\x11', 3, 2, 16) ? == i64(17) 54 | } 55 | 56 | fn test_int_to_bytes() ? { 57 | assert int_to_bytes(0) == [u8(0)] 58 | assert int_to_bytes(1) == [u8(1)] 59 | assert int_to_bytes(0xff) == [u8(0xff)] 60 | assert int_to_bytes(0x100) == [u8(0x1), 0] 61 | assert int_to_bytes(0x1000) == [u8(0x10), 0] 62 | assert int_to_bytes(0x1_0000) == [u8(0), 1, 0, 0] 63 | assert int_to_bytes(0x1000_0000) == [u8(0x10), 0, 0, 0] 64 | assert int_to_bytes(0x1_0000_0000) == [u8(0), 0, 0, 1, 0, 0, 0, 0] 65 | assert int_to_bytes(0x1_0000_0000_0000) == [u8(0), 1, 0, 0, 0, 0, 0, 0] 66 | assert int_to_bytes(0x1000_0000_0000_0000) == [u8(0x10), 0, 0, 0, 0, 0, 0, 0] 67 | } 68 | 69 | fn test_interpolate_double_quoted_string() ? { 70 | assert interpolate_double_quoted_string(r'') ? == '' 71 | assert interpolate_double_quoted_string(r'a') ? == 'a' 72 | assert interpolate_double_quoted_string(r'\n') ? == '\n' 73 | assert interpolate_double_quoted_string(r'\r') ? == '\r' 74 | assert interpolate_double_quoted_string(r'\t') ? == '\x09' 75 | assert interpolate_double_quoted_string(r"\'") ? == "'" 76 | assert interpolate_double_quoted_string(r'\"') ? == '"' 77 | assert interpolate_double_quoted_string(r'\?') ? == '?' 78 | assert interpolate_double_quoted_string(r'\123') ? == 'S' 79 | assert interpolate_double_quoted_string(r'\123\124') ? == 'ST' 80 | assert interpolate_double_quoted_string(r'\123ABC') ? == 'SABC' 81 | assert interpolate_double_quoted_string(r'\x00') ? == '\000' 82 | assert interpolate_double_quoted_string(r'\x40') ? == '@' 83 | assert interpolate_double_quoted_string(r'\x40abc\123') ? == '@abcS' 84 | assert interpolate_double_quoted_string(r'\u0040') ? == '@' 85 | 86 | assert '௵' == '\u0BF5' 87 | assert '௵'.bytes() == [u8(0xe0), 0xaf, 0xb5] 88 | assert '\u0BF5'.bytes() == [u8(0xe0), 0xaf, 0xb5] 89 | assert utf8.get_uchar('௵', 0) == 0x0bf5 90 | assert utf8_char_len('௵'[0]) == 3 91 | assert utf32_to_str(0x0BF5).bytes() == [u8(0xe0), 0xaf, 0xb5] 92 | assert interpolate_double_quoted_string(r'\u0BF5') ? == '௵' 93 | 94 | assert interpolate_double_quoted_string(r"This is a ''comment''") ? == "This is a ''comment''" 95 | } 96 | 97 | fn test_interpolate_single_quoted_string() ? { 98 | assert interpolate_single_quoted_string(r'') == '' 99 | assert interpolate_single_quoted_string(r'a') == 'a' 100 | assert interpolate_single_quoted_string(r'\n') == '\\n' 101 | assert interpolate_single_quoted_string(r'\r') == '\\r' 102 | assert interpolate_single_quoted_string(r'\t') == '\\t' 103 | assert interpolate_single_quoted_string(r"\'") == "\\'" 104 | assert interpolate_single_quoted_string(r'\"') == '\\"' 105 | assert interpolate_single_quoted_string(r'\?') == '\\?' 106 | assert interpolate_single_quoted_string(r'\123') == '\\123' 107 | assert interpolate_single_quoted_string(r'\123\124') == '\\123\\124' 108 | assert interpolate_single_quoted_string(r'\123ABC') == '\\123ABC' 109 | assert interpolate_single_quoted_string(r'\x00') == '\\x00' 110 | assert interpolate_single_quoted_string(r'\x40') == '\\x40' 111 | assert interpolate_single_quoted_string(r'\x40abc\123') == '\\x40abc\\123' 112 | assert interpolate_single_quoted_string(r'\u0040') == '\\u0040' 113 | 114 | assert '௵' == '\u0BF5' 115 | assert '௵'.bytes() == [u8(0xe0), 0xaf, 0xb5] 116 | assert '\u0BF5'.bytes() == [u8(0xe0), 0xaf, 0xb5] 117 | assert utf8.get_uchar('௵', 0) == 0x0bf5 118 | assert utf8_char_len('௵'[0]) == 3 119 | assert utf32_to_str(0x0BF5).bytes() == [u8(0xe0), 0xaf, 0xb5] 120 | assert interpolate_single_quoted_string(r'\u0BF5') == '\\u0BF5' 121 | 122 | // This is the only thing happening 123 | assert interpolate_single_quoted_string(r"This is a ''comment''") == "This is a 'comment'" 124 | } 125 | 126 | fn test_interpolate_plain_value() ? { 127 | assert interpolate_plain_value('abc') == 'abc' 128 | assert interpolate_plain_value('123') == '123' 129 | 130 | assert interpolate_plain_value('0x1') == '1' 131 | assert interpolate_plain_value('0x12') == '18' 132 | assert interpolate_plain_value('0x123') == '291' 133 | assert interpolate_plain_value('ab0x11') == 'ab0x11' 134 | assert interpolate_plain_value('0x11Test') == '0x11Test' 135 | 136 | assert interpolate_plain_value('0o1') == '1' 137 | assert interpolate_plain_value('0o12') == '10' 138 | assert interpolate_plain_value('0o123') == '83' 139 | assert interpolate_plain_value('0o1Test') == '0o1Test' 140 | 141 | assert interpolate_plain_value('0b1') == '1' 142 | assert interpolate_plain_value('0b10') == '2' 143 | assert interpolate_plain_value('0b101') == '5' 144 | assert interpolate_plain_value('0b1Test') == '0b1Test' 145 | } 146 | -------------------------------------------------------------------------------- /step3_reader.v: -------------------------------------------------------------------------------- 1 | module yaml 2 | 3 | import text_scanner as ts 4 | 5 | // Even though the compiler is able to handle this, and smart-cast 6 | // does not raise a compiler error, it is not possible to access the 7 | // array in the if-clause, e.g. if x is []YamlValue { assert x.len == 3 } 8 | // does not work!!! Putting them into a struct is a workaround. 9 | // type YamlValue = map[string]YamlValue | []YamlValue | string 10 | pub type YamlValue = Null | YamlListValue | YamlMapValue | bool | f64 | i64 | string 11 | 12 | pub struct Null {} 13 | 14 | pub const null = Null{} 15 | 16 | pub struct YamlListValue { 17 | pub mut: 18 | ar []YamlValue 19 | } 20 | 21 | pub struct YamlMapValue { 22 | pub mut: 23 | obj map[string]YamlValue 24 | } 25 | 26 | pub struct YamlValues { 27 | pub: 28 | text string // The full yaml document 29 | newline string // The auto-detected newline 30 | encoding ts.Encodings // Currently only utf-8 is supported 31 | pub mut: 32 | documents []YamlValue // The list of YAML documents. Often there is just one. 33 | 34 | tags map[string]YamlValue 35 | } 36 | 37 | [inline] 38 | pub fn (val YamlValues) get(idx int) YamlValue { 39 | return val.documents[idx] 40 | } 41 | 42 | pub enum ReplaceTagsEnum { 43 | do_not 44 | in_tokenizer 45 | in_reader 46 | } 47 | 48 | pub struct NewYamlReaderParams { 49 | debug int // 4 and 8 are good number to print increasingly more debug messages 50 | replace_tags ReplaceTagsEnum = ReplaceTagsEnum.in_reader 51 | } 52 | 53 | // yaml_reader This is the main function to read a YAML file and convert 54 | // the YAML data into 'objects'. In V structs must be defined at compile 55 | // time, and V [attributes] are not user extensible. V's json implementation 56 | // makes use of attributes. Without a better option at hand, the implementation 57 | // dynamically creates a tree structure leveraging a sumtype. 58 | pub fn yaml_reader(content string, args NewYamlReaderParams) ?YamlValues { 59 | replace_tags_in_tokenizer := args.replace_tags == ReplaceTagsEnum.in_tokenizer 60 | tokenizer := yaml_tokenizer(content, replace_tags: replace_tags_in_tokenizer, debug: args.debug) ? 61 | 62 | mut values := YamlValues{ 63 | text: tokenizer.text 64 | newline: tokenizer.newline 65 | encoding: tokenizer.encoding 66 | } 67 | 68 | values.read_root(tokenizer.tokens, args.debug) ? 69 | 70 | return values 71 | } 72 | 73 | // to_yamlvalue Convert a YamlTokenValueTyoe to a YamlValue 74 | fn (v YamlTokenValueType) to_yamlvalue() YamlValue { 75 | /* 76 | TODO There is bug in the V compiler. This one should actually work, but creates invalid C code 77 | return match v { 78 | string { YamlValue(v) } 79 | i64 { YamlValue(v) } 80 | f64 { YamlValue(v) } 81 | } 82 | */ 83 | if v is string { 84 | a := v 85 | if a == 'null' { 86 | return null 87 | } 88 | return YamlValue(a) 89 | } else if v is i64 { 90 | a := v 91 | return YamlValue(a) 92 | } else if v is f64 { 93 | a := v 94 | return YamlValue(a) 95 | } else if v is bool { 96 | a := v 97 | return YamlValue(a) 98 | } 99 | panic('Will not happen') 100 | } 101 | 102 | // read_root Entry point to convert yaml tokens into yam values 103 | fn (mut values YamlValues) read_root(tokens []YamlToken, debug int) ? { 104 | if debug > 1 { 105 | eprintln('-------- yaml_reader') 106 | } 107 | mut pos := 0 108 | for pos < tokens.len { 109 | tok := tokens[pos] 110 | if debug > 1 { 111 | eprintln("pos: $pos, type: $tok.typ, val: '$tok.val'") 112 | } 113 | if tok.typ == YamlTokenKind.start_list { 114 | mut obj := YamlListValue{} 115 | pos += values.read_with_list_parent(tokens[pos + 1..], mut obj, debug) ? 116 | values.documents << obj 117 | } else if tok.typ == YamlTokenKind.start_object { 118 | mut obj := YamlMapValue{} 119 | pos += values.read_with_map_parent(tokens[pos + 1..], mut obj, debug) ? 120 | values.documents << obj 121 | } else if tok.typ == YamlTokenKind.value { 122 | values.documents << tok.val.to_yamlvalue() 123 | } else if tok.typ == YamlTokenKind.end_of_document { 124 | // ignore 125 | } else if tok.typ == YamlTokenKind.new_document { 126 | // ignore 127 | } else if tok.typ == YamlTokenKind.tag_def { 128 | return error("Found tag definition on top level (document): '$tok.val'") 129 | } else { 130 | return error('Unexpected token: $tok') 131 | } 132 | 133 | pos += 1 134 | } 135 | 136 | if debug > 1 { 137 | eprintln('-------- yaml_reader: output generated') 138 | eprintln(values.documents) 139 | eprintln('tags:') 140 | eprintln(values.tags) 141 | } 142 | } 143 | 144 | // read_with_list_parent Assuming the parent is a list, then handle 145 | // now all its elements 146 | fn (mut values YamlValues) read_with_list_parent(tokens []YamlToken, mut parent YamlListValue, debug int) ?int { 147 | mut pos := 0 148 | mut tag := '' 149 | for pos < tokens.len { 150 | tok := tokens[pos] 151 | if debug > 1 { 152 | eprintln("pos: $pos, type: $tok.typ, val: '$tok.val'") 153 | } 154 | if tok.typ == YamlTokenKind.start_list { 155 | mut obj := YamlListValue{} 156 | pos += values.read_with_list_parent(tokens[pos + 1..], mut obj, debug) ? 157 | parent.ar << obj 158 | } else if tok.typ == YamlTokenKind.start_object { 159 | mut obj := YamlMapValue{} 160 | pos += values.read_with_map_parent(tokens[pos + 1..], mut obj, debug) ? 161 | parent.ar << obj 162 | } else if tok.typ == YamlTokenKind.value { 163 | obj := tok.val.to_yamlvalue() 164 | parent.ar << obj 165 | if tag.len > 0 { 166 | values.tags[tag] = obj 167 | tag = '' 168 | } 169 | } else if tok.typ == YamlTokenKind.key { 170 | return error("Did not expected a 'key' in a list context") 171 | } else if tok.typ == YamlTokenKind.close { 172 | break 173 | } else if tok.typ == YamlTokenKind.end_of_document { 174 | break 175 | } else if tok.typ == YamlTokenKind.new_document { 176 | break 177 | } else if tok.typ == YamlTokenKind.tag_def { 178 | tag = tok.val.str() 179 | } else if tok.typ == YamlTokenKind.tag_ref { 180 | obj := values.tags[tok.val.str()] ? 181 | parent.ar << obj 182 | } else { 183 | return error('Unexpected token: $tok') 184 | } 185 | 186 | pos += 1 187 | } 188 | 189 | return pos + 1 190 | } 191 | 192 | fn (mut values YamlValues) add_tag(tag string, obj YamlValue) { 193 | if tag.len > 0 { 194 | values.tags[tag] = obj 195 | } 196 | } 197 | 198 | // read_with_map_parent Assuming the parent is a map, then handle 199 | // now all its elements 200 | fn (mut values YamlValues) read_with_map_parent(tokens []YamlToken, mut parent YamlMapValue, debug int) ?int { 201 | mut key := '' 202 | mut pos := 0 203 | mut tag := '' 204 | for pos < tokens.len { 205 | tok := tokens[pos] 206 | if debug > 1 { 207 | eprintln("pos: $pos, type: $tok.typ, val: '$tok.val'") 208 | } 209 | if tok.typ == YamlTokenKind.start_list { 210 | mut obj := YamlListValue{} 211 | pos += values.read_with_list_parent(tokens[pos + 1..], mut obj, debug) ? 212 | parent.obj[key] = obj 213 | key = '' 214 | values.add_tag(tag, obj) 215 | tag = '' 216 | } else if tok.typ == YamlTokenKind.start_object { 217 | mut obj := YamlMapValue{} 218 | pos += values.read_with_map_parent(tokens[pos + 1..], mut obj, debug) ? 219 | parent.obj[key] = obj 220 | key = '' 221 | values.add_tag(tag, obj) 222 | tag = '' 223 | } else if tok.typ == YamlTokenKind.value { 224 | obj := tok.val.to_yamlvalue() 225 | parent.obj[key] = obj 226 | key = '' 227 | values.add_tag(tag, obj) 228 | tag = '' 229 | } else if tok.typ == YamlTokenKind.key { 230 | key = tok.val.str() 231 | } else if tok.typ == YamlTokenKind.close { 232 | break 233 | } else if tok.typ == YamlTokenKind.end_of_document { 234 | break 235 | } else if tok.typ == YamlTokenKind.new_document { 236 | break 237 | } else if tok.typ == YamlTokenKind.tag_def { 238 | tag = tok.val.str() 239 | } else if tok.typ == YamlTokenKind.tag_ref { 240 | parent.obj[key] = values.tags[tok.val.str()] ? 241 | } else { 242 | return error('Unexpected token: $tok') 243 | } 244 | 245 | pos += 1 246 | } 247 | 248 | if key.len > 0 { 249 | parent.obj[key] = YamlValue('') 250 | } 251 | 252 | return pos + 1 253 | } 254 | -------------------------------------------------------------------------------- /text_scanner/text_scanner.v: -------------------------------------------------------------------------------- 1 | module text_scanner 2 | 3 | // text_scanner No matter whether you want to read CSV, JSON, YAML, etc. 4 | // you often need a basic text scanner that tokenizes the text. 5 | import os 6 | 7 | struct TextScanner { 8 | pub: 9 | text string // The full yaml document 10 | newline string // The auto-detected newline 11 | encoding Encodings // Currently only utf-8 is supported 12 | pub mut: 13 | pos int // The current scanner position within the file 14 | last_pos int // The starting position of the current token 15 | line_no int = 1 // The current line number 16 | column_pos int // Start-of-line position 17 | } 18 | 19 | pub fn read_file(fpath string, debug int) ?TextScanner { 20 | if debug > 2 { 21 | eprintln('YAML file: $fpath') 22 | } 23 | 24 | content := os.read_file(fpath) ? 25 | if debug > 2 { 26 | eprintln("content: \n'$content'") 27 | } 28 | 29 | return new_scanner(content) 30 | } 31 | 32 | pub fn new_scanner(data string) ?TextScanner { 33 | if data.len == 0 { 34 | return error('No YAML content. Data are empty.') 35 | } 36 | 37 | enc := detect_bom(data) or { Encodings.utf_8 } 38 | 39 | if enc != Encodings.utf_8 { 40 | return error("Currently the YAML reader only supports 'UTF-8'") 41 | } 42 | 43 | newline := detect_newline(data) or { '\n' } 44 | 45 | return TextScanner{ 46 | pos: 0 47 | text: data 48 | encoding: enc 49 | newline: newline 50 | } 51 | } 52 | 53 | [inline] 54 | pub fn (s TextScanner) len() int { 55 | return s.text.len 56 | } 57 | 58 | [inline] 59 | pub fn (s TextScanner) is_eof() bool { 60 | return s.pos >= s.text.len 61 | } 62 | 63 | // at I wish V had more conventions, e.g. ar[idx] => ar.at(idx) 64 | [inline] 65 | pub fn (s TextScanner) at(pos int) u8 { 66 | return s.text[pos] 67 | } 68 | 69 | [inline] 70 | pub fn (s TextScanner) at_pos() u8 { 71 | return s.text[s.pos] 72 | } 73 | 74 | [inline] 75 | pub fn (mut s TextScanner) set_pos(pos int) { 76 | s.pos = pos 77 | } 78 | 79 | [inline] 80 | pub fn is_newline(c u8) bool { 81 | return c in [`\n`, `\r`] 82 | } 83 | 84 | // detect_newline Auto-detect newline 85 | pub fn detect_newline(str string) ?string { 86 | for i, c in str { 87 | if is_newline(c) { 88 | // CR LF (Windows), LF (Unix) and CR (Macintosh) 89 | len := if c == `\r` && (i + 1) < str.len && str[i + 1] == `\n` { 2 } else { 1 } 90 | return str[i..(i + len)] 91 | } 92 | } 93 | 94 | return none 95 | } 96 | 97 | pub fn (mut s TextScanner) on_newline() { 98 | s.skip(s.newline.len) 99 | 100 | s.line_no++ 101 | s.column_pos = s.pos 102 | } 103 | 104 | // skip Move the position and mark it as being read 105 | [inline] 106 | pub fn (mut s TextScanner) skip(incr int) { 107 | s.pos += incr 108 | s.last_pos = s.pos 109 | } 110 | 111 | // skip Move the position only (do not mark it as being read) 112 | [inline] 113 | pub fn (mut s TextScanner) move(incr int) { 114 | s.pos += incr 115 | } 116 | 117 | // move_to_eol Move the position to next eol (or eof). 118 | // Do not mark it as being read. 119 | pub fn (mut s TextScanner) move_to_eol() { 120 | for s.pos < s.text.len { 121 | c := s.text[s.pos] 122 | if is_newline(c) { 123 | return 124 | } 125 | s.pos++ 126 | } 127 | } 128 | 129 | // skip_to_eol Move the position to next eol (or eof) 130 | // and mark it as being read. 131 | pub fn (mut s TextScanner) skip_to_eol() { 132 | s.move_to_eol() 133 | s.last_pos = s.pos 134 | } 135 | 136 | // skip_whitespace Skip any whitespace, and mark it as being read. 137 | pub fn (mut s TextScanner) skip_whitespace() { 138 | for s.pos < s.text.len { 139 | c := s.text[s.pos] 140 | if c.is_space() == false { 141 | s.last_pos = s.pos 142 | return 143 | } 144 | s.pos++ 145 | } 146 | } 147 | 148 | // move_to_end_of_word Move the position to the character following 149 | // the current word. A word separator is either space, newline or eof. 150 | // Do not mark the text as being read. 151 | pub fn (mut s TextScanner) move_to_end_of_word() int { 152 | for s.pos < s.text.len { 153 | c := s.text[s.pos] 154 | if c.is_space() || is_newline(c) { 155 | break 156 | } 157 | s.pos++ 158 | } 159 | return s.pos 160 | } 161 | 162 | pub fn (mut s TextScanner) check_text() string { 163 | return s.text[s.last_pos..s.pos] 164 | } 165 | 166 | // get_text Retrieve the text from start_pos to the current position. 167 | // Update last_pos (mark the text as being read) 168 | pub fn (mut s TextScanner) get_text() string { 169 | str := s.text[s.last_pos..s.pos] 170 | s.last_pos = s.pos 171 | return str 172 | } 173 | 174 | pub fn (mut s TextScanner) at_str(len int) string { 175 | to := s.pos + len 176 | if to < s.text.len { 177 | return s.text[s.pos..s.pos + len] 178 | } 179 | return s.text[s.pos..] 180 | } 181 | 182 | // is_followed_by_space_or_eol Return true, if the current char is 183 | // followed by either a space or newline, or eof. 184 | pub fn (s TextScanner) is_followed_by_space_or_eol() bool { 185 | pos := s.pos + 1 186 | if pos >= s.text.len { 187 | return true 188 | } 189 | return s.text[pos] in [` `, `\r`, `\n`] 190 | } 191 | 192 | // is_followed_by Return true, if the current char is followed by char. 193 | pub fn (s TextScanner) is_followed_by(c u8) bool { 194 | pos := s.pos + 1 195 | if pos >= s.text.len { 196 | return false 197 | } 198 | return s.text[pos] == c 199 | } 200 | 201 | // is_followed_by_word Return true if text at position starts with word, 202 | // and is followed by either space, newline or eof. 203 | pub fn (s TextScanner) is_followed_by_word(str string) bool { 204 | if s.text[s.pos..].starts_with(str) { 205 | pos := s.pos + str.len 206 | if pos >= s.text.len { 207 | return true 208 | } 209 | return s.text[pos] in [` `, `\r`, `\n`] 210 | } 211 | return false 212 | } 213 | 214 | // read_line From the current position read the rest of the line. 215 | // Consider the data as being read. 216 | pub fn (mut s TextScanner) read_line() string { 217 | s.move_to_eol() 218 | return s.get_text() 219 | } 220 | 221 | // substr A secure way to get a substring of the text. 222 | // It properly considers lower and upper bound indices. 223 | pub fn (s TextScanner) substr(pos int, len int) string { 224 | p := if pos < 0 { 0 } else { pos } 225 | if p >= s.text.len || len <= 0 { 226 | return '' 227 | } 228 | if (p + len) >= s.text.len { 229 | return s.text[p..] 230 | } 231 | return s.text[p..(p + len)] + '...' 232 | } 233 | 234 | // substr_escaped For printing escape special chars such CR and LF 235 | pub fn (s TextScanner) substr_escaped(pos int, len int) string { 236 | mut str := s.substr(pos, len) 237 | return str_escaped(str) 238 | } 239 | 240 | // str_escaped For printing escape special chars such CR and LF 241 | pub fn str_escaped(x string) string { 242 | mut str := x 243 | str = str.replace('\n', '\\n') 244 | str = str.replace('\r', '\\r') 245 | return str 246 | } 247 | 248 | // replace_nl_space Replace re"[\s\r\n]+" with " " 249 | pub fn replace_nl_space(str string) string { 250 | mut rtn := []u8{cap: str.len + 1} 251 | mut count := 0 252 | for c in str { 253 | if c in [` `, `\n`, `\r`] { 254 | if count == 0 { 255 | rtn << ` ` 256 | count++ 257 | } 258 | } else { 259 | rtn << c 260 | count = 0 261 | } 262 | } 263 | return rtn.bytestr() 264 | } 265 | 266 | // quoted_string_scanner Scan strings quoted with either `"` or `'` 267 | pub fn (mut s TextScanner) quoted_string_scanner(op fn (start_ch u8, str string) bool) ?string { 268 | start_ch := s.text[s.pos] 269 | line_no := s.line_no 270 | 271 | mut start_pos := s.pos 272 | s.skip(1) 273 | for s.pos < s.text.len { 274 | c := s.text[s.pos] 275 | if op(start_ch, s.text[s.pos..]) { 276 | s.move(2) 277 | } else if c == `\\` { // `"\"str"` 278 | s.move(2) 279 | } else if c == start_ch { 280 | s.skip(1) 281 | s.last_pos = start_pos 282 | return s.get_text() // We deliberately keep the quotes 283 | } else if is_newline(c) { 284 | s.on_newline() 285 | } else { 286 | s.move(1) 287 | } 288 | } 289 | 290 | return error("Closing quote `$start_ch.ascii_str()` missing. Starting at line $line_no: '${s.substr_escaped(s.pos - 10, 291 | 20)}'") 292 | } 293 | 294 | // leading_spaces Determine the number of leading spaces (indentation) 295 | pub fn leading_spaces(str string) int { 296 | for i, c in str { 297 | if c.is_space() == false { 298 | return i 299 | } 300 | } 301 | return str.len 302 | } 303 | -------------------------------------------------------------------------------- /step2_tokenizer.v: -------------------------------------------------------------------------------- 1 | module yaml 2 | 3 | import ystrconv 4 | import text_scanner as ts 5 | 6 | // See https://yaml.org/spec/1.2/spec.html for all the nitty gritty details 7 | 8 | // YamlTokenKind The TextScanner tokens are text centric and quite basic. The 9 | // tokenizer reads the text tokens and derives YAML tokens. Each YAML token 10 | // will be of a specific kind. 11 | pub enum YamlTokenKind { 12 | start_list 13 | start_object 14 | close 15 | key 16 | value 17 | new_document 18 | end_of_document 19 | tag_url // Text starting with '!' 20 | tag_def // Text starting with '&' 21 | tag_ref // Text starting with '*' 22 | tag_directive // %TAG 23 | } 24 | 25 | // YamlTokenValueType The value of YamlToken can be any of these types. 26 | // The type is auto-detected. Quoted strings always remain strings (without quotes). 27 | // true / false, yes / no are valid boolean values. 0 / 1 are integers. 28 | pub type YamlTokenValueType = bool | f64 | i64 | string // TODO implement bool 29 | 30 | pub fn (typ YamlTokenValueType) str() string { 31 | return match typ { 32 | string { typ.str() } 33 | i64 { typ.str() } 34 | f64 { typ.str() } 35 | bool { typ.str() } 36 | } 37 | } 38 | 39 | fn fix_float_str(v f64) string { 40 | mut x := v.str() 41 | if x.len > 0 && x[x.len - 1] == `.` { 42 | x += '0' 43 | } 44 | return x 45 | } 46 | 47 | // format A printable string representation of the value. Especially strings will be 48 | // quoted, e.g. "..." 49 | pub fn (typ YamlTokenValueType) format() string { 50 | return match typ { 51 | string { 52 | x := typ.replace('"', '\\"') 53 | "\"$x\"" 54 | } 55 | i64 { 56 | typ.str() 57 | } 58 | f64 { 59 | fix_float_str(typ) 60 | } 61 | bool { 62 | typ.str() 63 | } 64 | } 65 | } 66 | 67 | // YamlToken A YAML token is quite simple. It consists of a token-kind and 68 | // the associated text, if any. 69 | pub struct YamlToken { 70 | typ YamlTokenKind 71 | val YamlTokenValueType 72 | } 73 | 74 | // ParentNode This is internal only. While iterating over the text tokens, 75 | // we need to remember a stack with the current parent objects. 76 | struct ParentNode { 77 | typ string 78 | indent int 79 | block bool 80 | } 81 | 82 | // YamlTokenizer Iterate over the stream of text tokens provided by TextScanner and 83 | // transform it into respective YamlToken tokens. 84 | struct YamlTokenizer { 85 | pub: 86 | text string // The full yaml document 87 | newline string // The auto-detected newline 88 | encoding ts.Encodings // Currently only utf-8 is supported 89 | replace_tags bool // If true, then tags are replaced with the respective sequence of tokens (tag definition) 90 | debug int 91 | pub mut: 92 | tokens []YamlToken // The list of YAML tokens 93 | 94 | tags map[string]int // tag name => token index 95 | } 96 | 97 | // token_followed_by Return true if the token following the one at position 'i', 98 | // is of type 'typ' or a newline token. 99 | fn (scanner Scanner) token_followed_by(i int, typ TokenKind) bool { 100 | j := i + 1 101 | if j < scanner.tokens.len { 102 | return scanner.tokens[j].typ == typ 103 | } 104 | return typ == TokenKind.newline 105 | } 106 | 107 | // is_quoted Test whether 'str' starts and beginns with the quote char provided 108 | [inline] 109 | fn is_quoted(str string, ch u8) bool { 110 | return str.len > 1 && str[0] == ch && str[str.len - 1] == ch 111 | } 112 | 113 | // remove_quotes Remove any optional quotes 114 | fn remove_quotes(str string) ?string { 115 | if is_quoted(str, `"`) { 116 | val := str[1..str.len - 1] 117 | return ystrconv.interpolate_double_quoted_string(val) 118 | } else if is_quoted(str, `'`) { 119 | val := str[1..str.len - 1] 120 | return ystrconv.interpolate_single_quoted_string(val) 121 | } 122 | return str 123 | } 124 | 125 | // cmp_lowercase Compare two strings ignoring the case. str2 must be 126 | // provided lower case. 127 | fn cmp_lowercase(str1 string, str2 string) bool { 128 | if str1.len != str2.len { 129 | return false 130 | } 131 | for i in 0 .. str1.len { 132 | mut ch := str1[i] 133 | if ch >= `A` && ch <= `Z` { 134 | ch += 32 135 | } 136 | if ch != str2[i] { 137 | return false 138 | } 139 | } 140 | return true 141 | } 142 | 143 | // to_value_type Analyse the token string and convert it to the respective type. 144 | // Remove any optionally existing quotes for string types 145 | fn to_value_type(val string) ?YamlTokenValueType { 146 | str := if val.len > 1 { 147 | // Convert 0x.., 0o.. and 0b.. to decimal integers 148 | ystrconv.interpolate_plain_value(val) 149 | } else { 150 | val 151 | } 152 | if val.len > 0 { 153 | if ystrconv.is_int(str) { 154 | return str.i64() 155 | } else if ystrconv.is_float(str) { 156 | return str.f64() 157 | } else if cmp_lowercase(str, 'true') || cmp_lowercase(str, 'yes') { 158 | return true 159 | } else if cmp_lowercase(str, 'false') || cmp_lowercase(str, 'no') { 160 | return false 161 | } 162 | } 163 | 164 | return YamlTokenValueType(remove_quotes(str) ?) 165 | } 166 | 167 | // new_token Create a new token. Dynamically determine the value type. 168 | // See to_value_type(). 169 | [inline] 170 | fn new_token(typ YamlTokenKind, val string) ?YamlToken { 171 | return YamlToken{ 172 | typ: typ 173 | val: to_value_type(val) ? 174 | } 175 | } 176 | 177 | // new_empty_token Many token only have a token type, but the token value 178 | // is irrelevant. 179 | [inline] 180 | fn new_empty_token(typ YamlTokenKind) YamlToken { 181 | return YamlToken{ 182 | typ: typ 183 | val: i64(0) 184 | } 185 | } 186 | 187 | // new_str_token Do not auto-detect the value type, consider the value 188 | // to be a string. Remove any optional quotes. 189 | [inline] 190 | fn new_str_token(typ YamlTokenKind, val string) ?YamlToken { 191 | return YamlToken{ 192 | typ: typ 193 | val: remove_quotes(val) ? 194 | } 195 | } 196 | 197 | pub struct NewTokenizerParams { 198 | pub: 199 | debug int // 4 and 8 are good number to print increasingly more debug messages 200 | replace_tags bool = true 201 | } 202 | 203 | // yaml_tokenizer Iterate over the stream of text tokens provided by TextScanner and 204 | // transform it into respective YamlToken tokens. 205 | // Please note that we have 2 approaches at different levels to replace tags. If you 206 | // enable it in the tokenizer, then the tag reference will be replaced with a copy of 207 | // all tokens associated with the tag reference. 208 | // The 2nd approach is implmented in the yaml reader. 209 | pub fn yaml_tokenizer(content string, args NewTokenizerParams) ?YamlTokenizer { 210 | scanner := yaml_scanner(content, args.debug) ? 211 | 212 | if scanner.tokens.len == 0 { 213 | return error('No YAML tokens found') 214 | } 215 | 216 | if args.debug > 2 { 217 | eprintln('------------- yaml_tokenizer') 218 | } 219 | 220 | mut tokenizer := YamlTokenizer{ 221 | text: scanner.ts.text 222 | newline: scanner.ts.newline 223 | encoding: scanner.ts.encoding 224 | replace_tags: args.replace_tags 225 | debug: args.debug 226 | } 227 | 228 | tokenizer.tokens = tokenizer.text_to_yaml_tokens(scanner, args.debug) ? 229 | 230 | return tokenizer 231 | } 232 | 233 | fn (tokenizer YamlTokenizer) add_tag_tokens(name string, mut tokens []YamlToken, start int) ? { 234 | if tokens[start].typ == YamlTokenKind.value { 235 | tok := tokens[start] 236 | tokens << tok 237 | return 238 | } 239 | 240 | stop := tokens.len 241 | mut count := 0 242 | for i in start .. stop { 243 | tok := tokens[i] 244 | // eprintln("start: $start, stop: $stop, i: $i, count: $count, tok: $tok.typ, $tok.val") 245 | tokens << tok 246 | if tok.typ in [YamlTokenKind.start_list, YamlTokenKind.start_object] { 247 | count++ 248 | } else if tok.typ == YamlTokenKind.close { 249 | count-- 250 | if count <= 0 { 251 | return 252 | } 253 | } 254 | } 255 | return error("Something went wrong with inserting the tag tokens for '$name'") 256 | } 257 | 258 | // text_to_yaml_tokens The main tokenizer function: convert string like tokens 259 | // into proper YAML tokens 260 | fn (mut tokenizer YamlTokenizer) text_to_yaml_tokens(scanner &Scanner, debug int) ?[]YamlToken { 261 | mut tokens := []YamlToken{} 262 | mut parents := []ParentNode{} // This is internal only, during transformation 263 | 264 | for i, t in scanner.tokens { 265 | if debug > 2 { 266 | eprintln("col: $t.column, typ: $t.typ, val: '$t.val'") 267 | } 268 | if parents.len > 0 && parents.last().block == true { 269 | if t.typ == TokenKind.xstr { 270 | if scanner.token_followed_by(i, TokenKind.colon) { 271 | tokens << new_token(YamlTokenKind.key, t.val) ? 272 | } else { 273 | tokens << new_token(YamlTokenKind.value, t.val) ? 274 | } 275 | } else if t.typ == TokenKind.rabr { 276 | parents.pop() 277 | tokens << new_empty_token(YamlTokenKind.close) 278 | } else if t.typ == TokenKind.rcbr { 279 | parents.pop() 280 | tokens << new_empty_token(YamlTokenKind.close) 281 | } else if t.typ == TokenKind.comma && parents.last().typ == 'list' { 282 | // ignore 283 | } 284 | continue 285 | } 286 | 287 | for parents.len > 0 && parents.last().indent > t.column { 288 | parents.pop() 289 | tokens << new_empty_token(YamlTokenKind.close) 290 | } 291 | 292 | if parents.len == 0 || parents.last().indent < t.column { 293 | if t.typ == TokenKind.hyphen { 294 | parents << ParentNode{ 295 | typ: 'list' 296 | indent: t.column 297 | block: false 298 | } 299 | tokens << new_empty_token(YamlTokenKind.start_list) 300 | } else if t.typ == TokenKind.xstr && scanner.token_followed_by(i, TokenKind.colon) { 301 | parents << ParentNode{ 302 | typ: 'object' 303 | indent: t.column 304 | block: false 305 | } 306 | tokens << new_empty_token(YamlTokenKind.start_object) 307 | } 308 | } 309 | 310 | if t.typ == TokenKind.xstr && scanner.token_followed_by(i, TokenKind.newline) { 311 | tokens << new_token(YamlTokenKind.value, t.val) ? 312 | } else if t.typ == TokenKind.xstr && scanner.token_followed_by(i, TokenKind.colon) { 313 | // TODO There is a bug in V. mut ar []int in combination with ar.last() generates wrong C code 314 | // if tokens.len > 0 && tokens.last().typ == YamlTokenKind.key { 315 | if tokens.len > 0 && tokens[tokens.len - 1].typ == YamlTokenKind.key { 316 | if parents.last().indent == t.column { 317 | // Key with null value 318 | tokens << new_str_token(YamlTokenKind.value, '') ? 319 | } 320 | } 321 | tokens << YamlToken{ 322 | typ: YamlTokenKind.key 323 | val: t.val 324 | } 325 | } else if t.typ == TokenKind.labr { 326 | parents << ParentNode{ 327 | typ: 'list' 328 | indent: t.column 329 | block: true 330 | } 331 | tokens << new_empty_token(YamlTokenKind.start_list) 332 | } else if t.typ == TokenKind.lcbr { 333 | parents << ParentNode{ 334 | typ: 'object' 335 | indent: t.column 336 | block: true 337 | } 338 | tokens << new_empty_token(YamlTokenKind.start_object) 339 | } else if t.typ == TokenKind.question_mark { 340 | return error('complex mapping key: NOT SUPPORTED') 341 | } else if t.typ == TokenKind.tag_ref { 342 | name := t.val 343 | if tokenizer.replace_tags == true { 344 | if name !in tokenizer.tags { 345 | return error("Did not find definition for tag: '$name'") 346 | } 347 | tokenizer.add_tag_tokens(name, mut tokens, tokenizer.tags[name]) ? 348 | } else { 349 | tokens << new_token(YamlTokenKind.tag_ref, name) ? 350 | } 351 | } else if t.typ == TokenKind.tag_def { 352 | if tokenizer.replace_tags == true { 353 | tokenizer.tags[t.val] = tokens.len 354 | } else { 355 | tokens << new_token(YamlTokenKind.tag_def, t.val) ? 356 | } 357 | } else if t.typ == TokenKind.end_of_document { 358 | for parents.len > 0 { 359 | parents.pop() 360 | tokens << new_empty_token(YamlTokenKind.close) 361 | } 362 | tokens << new_empty_token(YamlTokenKind.end_of_document) 363 | } else if t.typ == TokenKind.document { 364 | if parents.len > 0 { 365 | for parents.len > 0 { 366 | parents.pop() 367 | tokens << new_empty_token(YamlTokenKind.close) 368 | } 369 | tokens << new_empty_token(YamlTokenKind.end_of_document) 370 | } 371 | tokens << new_empty_token(YamlTokenKind.new_document) 372 | } 373 | } 374 | 375 | if parents.len > 0 { 376 | for parents.len > 0 { 377 | parents.pop() 378 | tokens << new_empty_token(YamlTokenKind.close) 379 | } 380 | tokens << new_empty_token(YamlTokenKind.end_of_document) 381 | } 382 | 383 | return tokens 384 | } 385 | 386 | /* 387 | See ex 2.11: we do not support complex mapping key, such as 388 | 389 | ? - Detroit Tigers 390 | - Chicago cubs 391 | : 392 | - 2001-07-23 393 | 394 | ? [ New York Yankees, 395 | Atlanta Braves ] 396 | : [ 2001-07-02, 2001-08-12, 397 | 2001-08-14 ] 398 | */ 399 | -------------------------------------------------------------------------------- /yaml_obj_accessor_test.v: -------------------------------------------------------------------------------- 1 | module yaml 2 | 3 | // YAML spec: https://yaml.org/spec/1.2/spec.html 4 | // To test your YAML: https://www.json2yaml.com/ 5 | import os 6 | 7 | const test_data_dir = os.dir(@FILE) + '/test_data' 8 | 9 | const debug = 0 10 | 11 | fn test_z_ex_01() ? { 12 | content := os.read_file('$test_data_dir/z_ex_01.yaml') ? 13 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 14 | assert docs.documents.len == 1 15 | 16 | x := docs.get(0) 17 | assert x.get(0) ?.string() ? == 'Mark McGwire' 18 | assert x.get(1) ?.string() ? == 'Sammy Sosa' 19 | assert x.get(2) ?.string() ? == 'Ken Griffey' 20 | 21 | assert x.get() ?.is_list() == true 22 | assert x.get() ?.is_map() == false 23 | assert x.get() ?.is_value() == false 24 | assert x.get() ?.len() == 3 25 | assert x.get() ?.is_empty() == false 26 | } 27 | 28 | fn test_z_ex_02() ? { 29 | content := os.read_file('$test_data_dir/z_ex_02.yaml') ? 30 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 31 | assert docs.documents.len == 1 32 | x := docs.get(0) 33 | assert x.get('hr') ?.string() ? == '65' 34 | assert x.get('hr') ?.int() ? == 65 35 | assert x.get('avg') ?.string() ? == '0.278' 36 | assert x.get('avg') ?.f64() ? == 0.278 37 | assert x.get('rbi') ?.string() ? == '147' 38 | 39 | assert x.get('hr') ?.int() ? == 65 40 | 41 | assert x.get() ?.is_list() == false 42 | assert x.get() ?.is_map() == true 43 | assert x.get() ?.is_value() == false 44 | assert x.get() ?.len() == 3 45 | assert x.get() ?.is_empty() == false 46 | 47 | assert x.get('hr') ?.int() ? == 65 48 | assert x.get('hr') ?.int() ? == 65 49 | 50 | assert x.get('avg') ?.f64() ? == 0.278 51 | assert x.get('rbi') ?.u32() ? == 147 52 | assert x.get('rbi') ?.u16() ? == 147 53 | assert x.get('rbi') ?.u8() ? == 147 54 | } 55 | 56 | fn test_z_ex_03() ? { 57 | content := os.read_file('$test_data_dir/z_ex_03.yaml') ? 58 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 59 | assert docs.documents.len == 1 60 | x := docs.get(0) 61 | assert x.get('american', 0) ?.string() ? == 'Boston Red Sox' 62 | assert x.get('american', 1) ?.string() ? == 'Detroit Tigers' 63 | assert x.get('american', 2) ?.string() ? == 'New York Yankees' 64 | 65 | assert x.get('national', 0) ?.string() ? == 'New York Mets' 66 | assert x.get('national', 1) ?.string() ? == 'Chicago Cubs' 67 | assert x.get('national', 2) ?.string() ? == 'Atlanta Braves' 68 | } 69 | 70 | fn test_z_ex_04() ? { 71 | content := os.read_file('$test_data_dir/z_ex_04.yaml') ? 72 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 73 | assert docs.documents.len == 1 74 | x1 := docs.get(0) 75 | assert x1.get(0, 'name') ?.string() ? == 'Mark McGwire' 76 | assert x1.get(0, 'hr') ?.string() ? == '65' 77 | assert x1.get(0, 'avg') ?.string() ? == '0.278' 78 | 79 | assert x1.get(1, 'name') ?.string() ? == 'Sammy Sosa' 80 | assert x1.get(1, 'hr') ?.string() ? == '63' 81 | assert x1.get(1, 'avg') ?.string() ? == '0.288' 82 | } 83 | 84 | fn test_z_ex_05() ? { 85 | content := os.read_file('$test_data_dir/z_ex_05.yaml') ? 86 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 87 | assert docs.documents.len == 1 88 | x1 := docs.get(0) 89 | 90 | assert x1.get(0, 0) ?.string() ? == 'name' 91 | assert x1.get(0, 1) ?.string() ? == 'hr' 92 | assert x1.get(0, 2) ?.string() ? == 'avg' 93 | 94 | assert x1.get(1, 0) ?.string() ? == 'Mark McGwire' 95 | assert x1.get(1, 1) ?.string() ? == '65' 96 | assert x1.get(1, 2) ?.string() ? == '0.278' 97 | 98 | assert x1.get(2, 0) ?.string() ? == 'Sammy Sosa' 99 | assert x1.get(2, 1) ?.string() ? == '63' 100 | assert x1.get(2, 2) ?.string() ? == '0.288' 101 | } 102 | 103 | fn test_z_ex_06() ? { 104 | content := os.read_file('$test_data_dir/z_ex_06.yaml') ? 105 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 106 | assert docs.documents.len == 1 107 | x1 := docs.get(0) 108 | 109 | assert x1.get('Mark McGwire', 'hr') ?.string() ? == '65' 110 | assert x1.get('Mark McGwire', 'avg') ?.string() ? == '0.278' 111 | 112 | assert x1.get('Sammy Sosa', 'hr') ?.string() ? == '63' 113 | assert x1.get('Sammy Sosa', 'avg') ?.string() ? == '0.288' 114 | } 115 | 116 | fn test_z_ex_07() ? { 117 | content := os.read_file('$test_data_dir/z_ex_07.yaml') ? 118 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 119 | assert docs.documents.len == 2 120 | 121 | x1 := docs.get(0) 122 | assert x1.get(0) ?.string() ? == 'Mark McGwire' 123 | assert x1.get(1) ?.string() ? == 'Sammy Sosa' 124 | assert x1.get(2) ?.string() ? == 'Ken Griffey' 125 | 126 | x2 := docs.get(1) 127 | assert x2.get(0) ?.string() ? == 'Chicago Cubs' 128 | assert x2.get(1) ?.string() ? == 'St Louis Cardinals' 129 | } 130 | 131 | fn test_z_ex_08() ? { 132 | content := os.read_file('$test_data_dir/z_ex_08.yaml') ? 133 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 134 | assert docs.documents.len == 2 135 | 136 | x1 := docs.get(0) 137 | assert x1.get('time') ?.string() ? == '20:03:20' 138 | assert x1.get('player') ?.string() ? == 'Sammy Sosa' 139 | assert x1.get('action') ?.string() ? == 'strike (miss)' 140 | 141 | x2 := docs.get(1) 142 | assert x2.get('time') ?.string() ? == '20:03:47' 143 | assert x2.get('player') ?.string() ? == 'Sammy Sosa' 144 | assert x2.get('action') ?.string() ? == 'grand slam' 145 | } 146 | 147 | fn test_z_ex_09() ? { 148 | content := os.read_file('$test_data_dir/z_ex_09.yaml') ? 149 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 150 | assert docs.documents.len == 1 151 | 152 | x1 := docs.get(0) 153 | assert x1.get('hr', 0) ?.string() ? == 'Mark McGwire' 154 | assert x1.get('hr', 1) ?.string() ? == 'Sammy Sosa' 155 | 156 | assert x1.get('rbi', 0) ?.string() ? == 'Sammy Sosa' 157 | assert x1.get('rbi', 1) ?.string() ? == 'Ken Griffey' 158 | } 159 | 160 | fn test_z_ex_10() ? { 161 | content := os.read_file('$test_data_dir/z_ex_10.yaml') ? 162 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 163 | assert docs.documents.len == 1 164 | 165 | x1 := docs.get(0) 166 | assert x1.get('hr', 0) ?.string() ? == 'Mark McGwire' 167 | assert x1.get('hr', 1) ?.string() ? == 'Sammy Sosa' 168 | 169 | assert x1.get('rbi', 0) ?.string() ? == 'Sammy Sosa' 170 | assert x1.get('rbi', 1) ?.string() ? == 'Ken Griffey' 171 | } 172 | 173 | fn test_z_ex_11() ? { 174 | content := os.read_file('$test_data_dir/z_ex_11.yaml') ? 175 | 176 | // Complex mapping keys are not supported 177 | if _ := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) { 178 | assert false 179 | } 180 | } 181 | 182 | fn test_z_ex_12() ? { 183 | content := os.read_file('$test_data_dir/z_ex_12.yaml') ? 184 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 185 | assert docs.documents.len == 1 186 | 187 | x1 := docs.get(0) 188 | assert x1.get(0, 'item') ?.string() ? == 'Super Hoop' 189 | assert x1.get(0, 'quantity') ?.string() ? == '1' 190 | 191 | assert x1.get(1, 'item') ?.string() ? == 'Basketball' 192 | assert x1.get(1, 'quantity') ?.string() ? == '4' 193 | 194 | assert x1.get(2, 'item') ?.string() ? == 'Big Shoes' 195 | assert x1.get(2, 'quantity') ?.string() ? == '1' 196 | } 197 | 198 | fn test_z_ex_13() ? { 199 | content := os.read_file('$test_data_dir/z_ex_13.yaml') ? 200 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 201 | assert docs.documents.len == 1 202 | 203 | x1 := docs.get(0) 204 | assert x1.get() ?.string() ? == '\\//||\\/||\n// || ||__' 205 | } 206 | 207 | fn test_z_ex_14() ? { 208 | content := os.read_file('$test_data_dir/z_ex_14.yaml') ? 209 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 210 | assert docs.documents.len == 1 211 | 212 | x1 := docs.get(0) 213 | assert x1.get() ?.string() ? == "Mark McGwire's year was crippled by a knee injury." 214 | } 215 | 216 | fn test_z_ex_15() ? { 217 | content := os.read_file('$test_data_dir/z_ex_15.yaml') ? 218 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 219 | assert docs.documents.len == 1 220 | 221 | x1 := docs.get(0) 222 | assert x1.get() ?.string() ? == 'Sammy Sosa completed another fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!' 223 | } 224 | 225 | fn test_z_ex_16() ? { 226 | content := os.read_file('$test_data_dir/z_ex_16.yaml') ? 227 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 228 | assert docs.documents.len == 1 229 | 230 | x1 := docs.get(0) 231 | assert x1.get('name') ?.string() ? == 'Mark McGwire' 232 | assert x1.get('accomplishment') ?.string() ? == 'Mark set a major league home run record in 1998.' 233 | assert x1.get('stats') ?.string() ? == '65 Home Runs\n0.278 Batting Average\n' 234 | } 235 | 236 | fn test_z_ex_17() ? { 237 | content := os.read_file('$test_data_dir/z_ex_17.yaml') ? 238 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 239 | assert docs.documents.len == 1 240 | 241 | x1 := docs.get(0) 242 | assert x1.get('unicode') ?.string() ? == 'Sosa did fine.☺' 243 | assert x1.get('control') ?.string() ? == '\b1998\t1999\t2000\n' 244 | assert x1.get('hex esc') ?.string() ? == '\r\n is \r\n' 245 | assert x1.get('single') ?.string() ? == '"Howdy!" he cried.' 246 | assert x1.get('quoted') ?.string() ? == " # Not a 'comment'." 247 | assert x1.get('tie-fighter') ?.string() ? == '|\\-*-/|' 248 | } 249 | 250 | fn test_z_ex_18() ? { 251 | content := os.read_file('$test_data_dir/z_ex_18.yaml') ? 252 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 253 | assert docs.documents.len == 1 254 | 255 | x1 := docs.get(0) 256 | assert x1.get('plain 1') ?.string() ? == 'This unquoted scalar spans many lines. It has three lines.' 257 | assert x1.get('plain 2') ?.string() ? == 'This is also multi-line' 258 | assert x1.get('plain 3') ?.string() ? == 'The second line is more indented' 259 | assert x1.get('plain 4') ?.string() ? == 'The third line is more indented' 260 | assert x1.get('plain 5') ?.string() ? == 'This is another example that should work' 261 | assert x1.get('plain 6') ?.string() ? == 'The second line\nis more indented' 262 | 263 | assert x1.get('quoted') ?.string() ? == 'So does this quoted scalar with.\n' 264 | } 265 | 266 | fn test_z_ex_19() ? { 267 | content := os.read_file('$test_data_dir/z_ex_19.yaml') ? 268 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 269 | assert docs.documents.len == 1 270 | 271 | x1 := docs.get(0) 272 | assert x1.get('canonical') ?.string() ? == '12345' 273 | assert x1.get('decimal') ?.int() ? == 12345 274 | assert x1.get('octal') ?.int() ? == 12 275 | assert x1.get('hexadecimal') ?.int() ? == 12 276 | } 277 | 278 | fn test_z_ex_20() ? { 279 | content := os.read_file('$test_data_dir/z_ex_20.yaml') ? 280 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 281 | assert docs.documents.len == 1 282 | 283 | x1 := docs.get(0) 284 | assert x1.get('canonical') ?.f64() ? == 1.23015e+3 285 | assert x1.get('exponential') ?.f64() ? == 12.3015e+02 286 | assert x1.get('fixed') ?.f64() ? == 1230.15 287 | assert x1.get('negative infinity') ?.string() ? == '-.inf' 288 | assert x1.get('not a number') ?.string() ? == '.NaN' 289 | } 290 | 291 | fn test_z_ex_21() ? { 292 | content := os.read_file('$test_data_dir/z_ex_21.yaml') ? 293 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 294 | assert docs.documents.len == 1 295 | 296 | x1 := docs.get(0) 297 | assert x1.get('null') ?.string() ? == '' 298 | assert x1.get('string') ?.string() ? == '012345' 299 | assert x1.get('booleans', 0) ?.string() ? == 'true' 300 | assert x1.get('booleans', 1) ?.string() ? == 'false' 301 | } 302 | 303 | fn test_z_ex_22() ? { 304 | content := os.read_file('$test_data_dir/z_ex_22.yaml') ? 305 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 306 | assert docs.documents.len == 1 307 | 308 | x1 := docs.get(0) 309 | assert x1.get('canonical') ?.string() ? == '2001-12-15T02:59:43.1Z' 310 | assert x1.get('iso8601') ?.string() ? == '2001-12-14t21:59:43.10-05:00' 311 | assert x1.get('spaced') ?.string() ? == '2001-12-14 21:59:43.10 -5' 312 | assert x1.get('date') ?.string() ? == '2002-12-14' 313 | } 314 | 315 | fn test_z_ex_23() ? { 316 | content := os.read_file('$test_data_dir/z_ex_23.yaml') ? 317 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 318 | assert docs.documents.len == 1 319 | 320 | x1 := docs.get(0) 321 | assert x1.get('not-date') ?.string() ? == '2002-04-28' 322 | assert x1.get('picture') ?.string() ? == 'R0lGODlhDAAMAIQAAP//9/X\n17unp5WZmZgAAAOfn515eXv\nPz7Y6OjuDg4J+fn5OTk6enp\n56enmleECcgggoBADs=\n' 323 | assert x1.get('application specific tag') ?.string() ? == 'The semantics of the tag\nabove may be different for\ndifferent documents.\n' 324 | } 325 | 326 | fn test_z_ex_24() ? { 327 | content := os.read_file('$test_data_dir/z_ex_24.yaml') ? 328 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 329 | assert docs.documents.len == 1 330 | 331 | x1 := docs.get(0) 332 | assert x1.get(0, 'center', 'x') ?.int() ? == 73 333 | assert x1.get(0, 'center', 'y') ?.int() ? == 129 334 | assert x1.get(0, 'radius') ?.int() ? == 7 335 | 336 | assert x1.get(1, 'start', 'x') ?.string() ? == '73' 337 | assert x1.get(1, 'start', 'y') ?.string() ? == '129' 338 | assert x1.get(1, 'finish', 'x') ?.string() ? == '89' 339 | assert x1.get(1, 'finish', 'y') ?.string() ? == '102' 340 | 341 | assert x1.get(2, 'start', 'x') ?.string() ? == '73' 342 | assert x1.get(2, 'start', 'y') ?.string() ? == '129' 343 | assert x1.get(2, 'color') ?.int() ? == 0xFFEEBB 344 | assert x1.get(2, 'text') ?.string() ? == 'Pretty vector drawing.' 345 | } 346 | 347 | fn test_z_ex_25() ? { 348 | content := os.read_file('$test_data_dir/z_ex_25.yaml') ? 349 | if _ := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) { 350 | assert false 351 | } 352 | // '?' is not yet support 353 | /* 354 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug)? 355 | assert docs.documents.len == 1 356 | x := rtn[0] 357 | assert x is YamlMapValue 358 | if x is YamlMapValue { 359 | assert x.obj.len == 3 360 | assert x.obj["hr"] == YamlValue("65") 361 | assert x.obj["avg"] == YamlValue("0.278") 362 | assert x.obj["rbi"] == YamlValue("147") 363 | } 364 | */ 365 | } 366 | 367 | fn test_z_ex_26() ? { 368 | content := os.read_file('$test_data_dir/z_ex_26.yaml') ? 369 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 370 | assert docs.documents.len == 1 371 | 372 | x1 := docs.get(0) 373 | assert x1.get(0, 'Mark McGwire') ?.string() ? == '65' 374 | assert x1.get(1, 'Sammy Sosa') ?.string() ? == '63' 375 | assert x1.get(2, 'Ken Griffy') ?.string() ? == '58' 376 | } 377 | 378 | fn test_z_ex_27() ? { 379 | content := os.read_file('$test_data_dir/z_ex_27.yaml') ? 380 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 381 | assert docs.documents.len == 1 382 | 383 | x1 := docs.get(0) 384 | assert x1.get('invoice') ?.string() ? == '34843' 385 | } 386 | 387 | fn test_nested_objects() ? { 388 | content := os.read_file('$test_data_dir/nested_objects.yaml') ? 389 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 390 | assert docs.documents.len == 1 391 | 392 | x1 := docs.get(0) 393 | assert x1.get('aaa') ?.string() ? == 'string' 394 | assert x1.get('bbb', '111') ?.string() ? == '1-1-1' 395 | assert x1.get('ccc', '222') ?.string() ? == '' 396 | assert x1.get('ccc', '223') ?.string() ? == 'xxx' 397 | } 398 | 399 | // 400 | -------------------------------------------------------------------------------- /step1_scanner.v: -------------------------------------------------------------------------------- 1 | module yaml 2 | 3 | import text_scanner as ts 4 | 5 | struct Scanner { 6 | pub mut: 7 | // TODO: There is a separate branch for embedded struct. Due to a V-bug, it can not be used right now. 8 | ts ts.TextScanner // A somewhat generic text scanner 9 | beginning_of_line bool = true // True if we are *logically* at the beginning of a line 10 | tokens []Token // YAML Tokens collected TODO Keep for now, but remove ones next() is in place 11 | indent_level []int = [1] // A stack of relevant indent levels 12 | debug int // The larger, the more debug messages. 4 and 9 are reasonable values. 13 | token_buffer []Token 14 | block_levels []u8 // Open block levels '{' or '[' 15 | } 16 | 17 | pub enum TokenKind { 18 | lcbr // `{` 19 | rcbr // `}` 20 | labr // `[` 21 | rabr // `]` 22 | comma // `,` 23 | colon // `:` 24 | hyphen // `-` 25 | question_mark // `?` 26 | pipe // `|` 27 | greater // `>` 28 | newline // `\r`, `\n` 29 | document // `---` 30 | end_of_document // `...` 31 | xstr // anything else 32 | tag_url // Text starting with '!' 33 | tag_def // Text starting with '&' 34 | tag_ref // Text starting with '*' 35 | tag_directive // %TAG 36 | } 37 | 38 | pub struct Token { 39 | typ TokenKind // Token type 40 | column int // Token indent level 41 | val string // Token string value 42 | } 43 | 44 | struct ScannerIter { 45 | mut: 46 | s Scanner 47 | } 48 | 49 | [inline] 50 | pub fn (mut iter ScannerIter) next() ?Token { 51 | return iter.s.next_token() 52 | } 53 | 54 | [inline] 55 | pub fn (s Scanner) scan() ScannerIter { 56 | return ScannerIter{ 57 | s: s 58 | } 59 | } 60 | 61 | // This is basically a file reader and tokenizer. It's key properties are: 62 | // - It splits the (file) content into a series of basic tokens, such as newline, 63 | // "-", ":", "{", "}", "[", etc. 64 | // - It properly handles quoted text, such as ".." and also the variations of 65 | // multi-line text 66 | // - Indentation plays a major role in YAML. Every token provide the indentation 67 | // level (== column) 68 | pub fn yaml_scanner(content string, debug int) ?Scanner { 69 | if debug > 2 { 70 | eprintln("content: \n'$content'") 71 | } 72 | 73 | mut scanner := new_scanner(content, debug) ? 74 | 75 | // TODO Remove ones streaming properly works 76 | for tok in scanner.scan() { 77 | scanner.tokens << tok 78 | } 79 | return scanner 80 | } 81 | 82 | pub fn new_scanner(data string, debug int) ?Scanner { 83 | text_scanner := ts.new_scanner(data) ? 84 | return Scanner{ 85 | ts: text_scanner 86 | debug: debug 87 | } 88 | } 89 | 90 | [inline] 91 | pub fn (s Scanner) len() int { 92 | return s.ts.len() 93 | } 94 | 95 | // TODO rename fn. It no longer adds a token to something 96 | fn (mut s Scanner) add_token(t_type TokenKind, val string) Token { 97 | tok := Token{ 98 | typ: t_type 99 | column: s.indent_level.last() 100 | val: val 101 | } 102 | if s.debug > 7 { 103 | eprintln("new token: line-no: $s.ts.line_no, $s.indent_level, $t_type, '$val'") 104 | } 105 | return tok 106 | } 107 | 108 | fn (mut s Scanner) tokenize(t_type TokenKind, from int, to int) ?Token { 109 | x := if from >= s.ts.len() { '' } else { s.ts.text[from..to].trim_space() } 110 | if t_type != .xstr || x.len > 0 { 111 | return s.add_token(t_type, x) 112 | } 113 | return none 114 | } 115 | 116 | [inline] 117 | fn (mut s Scanner) newline() { 118 | s.ts.on_newline() 119 | s.beginning_of_line = true 120 | } 121 | 122 | fn (mut s Scanner) to_token_kind(c u8) TokenKind { 123 | return match c { 124 | `{` { TokenKind.lcbr } 125 | `}` { TokenKind.rcbr } 126 | `[` { TokenKind.labr } 127 | `]` { TokenKind.rabr } 128 | `,` { TokenKind.comma } 129 | `:` { TokenKind.colon } 130 | `-` { TokenKind.hyphen } 131 | `?` { TokenKind.question_mark } 132 | `>` { TokenKind.greater } 133 | `|` { TokenKind.pipe } 134 | `\n` { TokenKind.newline } 135 | `\r` { TokenKind.newline } 136 | `!` { TokenKind.tag_url } 137 | `&` { TokenKind.tag_def } 138 | `*` { TokenKind.tag_ref } 139 | else { TokenKind.xstr } 140 | } 141 | } 142 | 143 | [inline] 144 | fn (mut s Scanner) catch_up() ?Token { 145 | str := s.ts.get_text().trim_space() 146 | if str.len > 0 { 147 | tok := s.add_token(.xstr, str) 148 | return tok 149 | } 150 | return none 151 | } 152 | 153 | fn (mut s Scanner) new_indent_level_for_list(pos int) { 154 | if pos < 0 { 155 | // Remove all indent levels, except the first one 156 | for s.indent_level.len > 1 { 157 | s.indent_level.pop() 158 | } 159 | return 160 | } 161 | 162 | // Remove all indent levels, until 'column' 163 | col := pos - s.ts.column_pos + 2 // Column 164 | for (s.indent_level.len > 1) && (s.indent_level.last() >= col) { 165 | s.indent_level.pop() 166 | } 167 | 168 | // Add an indent level, if new level is greater then the latest one 169 | if s.indent_level.last() < col { 170 | s.indent_level << col 171 | } 172 | } 173 | 174 | fn (mut s Scanner) new_indent_level(pos int) { 175 | if pos < 0 { 176 | // Remove all indent levels, except the first one 177 | for s.indent_level.len > 1 { 178 | s.indent_level.pop() 179 | } 180 | return 181 | } 182 | 183 | // Remove all indent levels, until 'column' 184 | col := pos - s.ts.column_pos + 1 // Column 185 | for (s.indent_level.len > 1) && (s.indent_level.last() > col) { 186 | s.indent_level.pop() 187 | } 188 | 189 | // Add an indent level, if new level is greater then the latest one 190 | if s.indent_level.last() < col { 191 | s.indent_level << col 192 | } 193 | } 194 | 195 | fn (mut s Scanner) on_newline() Token { 196 | if tok := s.catch_up() { 197 | if tok.typ == .xstr { 198 | return s.plain_multi_line_scanner(s.ts.pos, tok) 199 | } 200 | } 201 | 202 | // Nothing found to catch up 203 | s.newline() 204 | return s.add_token(.newline, '') 205 | } 206 | 207 | [inline] 208 | fn (mut s Scanner) add_to_token_buffer(tok Token) { 209 | s.token_buffer.prepend(tok) 210 | } 211 | 212 | // next_token This the main scanner implementation. 213 | // Get the next token 214 | fn (mut s Scanner) next_token() ?Token { 215 | if s.token_buffer.len > 0 { 216 | return s.token_buffer.pop() 217 | } 218 | 219 | if s.ts.is_eof() { 220 | return none 221 | } 222 | 223 | if s.block_levels.len > 0 { 224 | return s.block_scanner() 225 | } 226 | 227 | for !s.ts.is_eof() { 228 | c := s.ts.at_pos() 229 | if s.debug > 8 { 230 | eprintln("YAML: lineno: $s.ts.line_no, pos: $s.ts.pos, bol: $s.beginning_of_line, indent: $s.indent_level.last() ($s.indent_level.len), str='${s.ts.substr_escaped(s.ts.pos, 231 | 20)}'") 232 | } 233 | 234 | // Some additional tokens/chars are considered only at the beginning of a line. 235 | // "Beginning of line" definition is a bit tricky: It is used to determine the 236 | // indent level. Usually it is the first non-space char, but in case of e.g. 237 | // " - text", the indent level for "-" is 3 and for "text" it is 5. 238 | if s.beginning_of_line { 239 | if c == ` ` { 240 | s.ts.skip(1) 241 | continue 242 | } else if c == `\t` { 243 | return error("Tabs are not allowed for indentation. You must use spaces: '${s.ts.substr_escaped(s.ts.pos - 10, 244 | 20)}'") 245 | } else if c in [`-`, `?`] && s.ts.is_followed_by_space_or_eol() { // list or set 246 | s.new_indent_level_for_list(s.ts.pos) 247 | tok := s.tokenize(s.to_token_kind(c), s.ts.pos, s.ts.pos + 1) ? 248 | s.ts.skip(1) // The next is optionally a space. It could as well be a newline 249 | return tok 250 | } else if s.ts.is_followed_by_word('---') { // beginning of document 251 | tok := s.tokenize(.document, s.ts.pos, s.ts.pos + 3) ? 252 | s.ts.skip(3) 253 | s.new_indent_level(-1) // close any open indent level 254 | return tok 255 | } else if s.ts.is_followed_by_word('...') { // end of document 256 | tok := s.tokenize(.end_of_document, s.ts.pos, s.ts.pos + 3) ? 257 | s.ts.skip(3) 258 | s.new_indent_level(-1) // close any open indent level 259 | return tok 260 | } else if s.ts.is_followed_by_word('%TAG') { // TAG directive 261 | str := s.ts.read_line() 262 | return s.add_token(.tag_directive, str) 263 | } 264 | } 265 | 266 | if c in [`"`, `'`] { // quoted strings 267 | if s.ts.check_text().trim_space().len > 0 { 268 | s.ts.move(1) 269 | continue 270 | } 271 | return s.quoted_string_scanner() 272 | } else if c in [`>`, `|`] { // (multi-line) flow text 273 | if s.ts.check_text().trim_space().len > 0 { 274 | s.ts.move(1) 275 | continue 276 | } 277 | return s.flow_string_scanner() 278 | } else if c == `#` { // Comment 279 | if tok := s.catch_up() { 280 | return tok 281 | } 282 | s.ts.skip_to_eol() 283 | } else if (c == `:`) && s.ts.is_followed_by_space_or_eol() { // key value separator: key: value 284 | if tok := s.catch_up() { 285 | return tok 286 | } 287 | tok := s.tokenize(.colon, s.ts.pos, s.ts.pos + 1) ? 288 | s.ts.skip(1) 289 | return tok 290 | } else if ts.is_newline(c) { 291 | return s.on_newline() 292 | } else if c in [`{`, `[`] { 293 | if s.ts.check_text().trim_space().len > 0 { 294 | s.ts.move(1) 295 | continue 296 | } 297 | return s.open_block(c) 298 | } else if c in [`!`, `&`, `*`] { // Tag related 299 | str0 := s.ts.check_text().trim_space() 300 | if str0.len > 0 { 301 | text := s.ts.read_line().trim_space() 302 | tok := s.add_token(.xstr, text) 303 | return tok 304 | } 305 | 306 | typ := s.to_token_kind(c) 307 | s.ts.skip(1) // Skip the leading char '!', '&', '*' 308 | s.ts.move_to_end_of_word() 309 | str := s.ts.get_text() 310 | tok := s.add_token(typ, str) 311 | return tok 312 | } else if c == `~` { // null 313 | str0 := s.ts.check_text() 314 | if str0.len > 1 { 315 | text := s.ts.read_line().trim_space() 316 | tok := s.add_token(.xstr, text) 317 | return tok 318 | } 319 | } else { 320 | if s.beginning_of_line { 321 | // This is the first non-space character 322 | // If required, adjust the indent level 323 | s.new_indent_level(s.ts.pos) 324 | s.beginning_of_line = false 325 | } 326 | s.ts.move(1) 327 | } 328 | } 329 | 330 | // Execute any outstanding (delayed) activities 331 | // Make sure that we always end with a newline 332 | tok := s.on_newline() 333 | if tok.typ != TokenKind.newline { 334 | s.add_to_token_buffer(s.on_newline()) 335 | } 336 | 337 | return tok 338 | } 339 | 340 | fn (mut s Scanner) open_block(c u8) ?Token { 341 | mut typ := s.to_token_kind(c) 342 | tok := s.tokenize(typ, s.ts.pos, s.ts.pos + 1) ? 343 | 344 | s.block_levels << c 345 | s.ts.skip(1) 346 | 347 | return tok 348 | } 349 | 350 | // block_scanner Scan the '{..}'' and '[..]' sections 351 | fn (mut s Scanner) block_scanner() ?Token { 352 | start_ch := s.block_levels.last() // The opening quote char 353 | 354 | for !s.ts.is_eof() { 355 | c := s.ts.at_pos() 356 | if c in [`{`, `[`] { 357 | return s.open_block(c) 358 | } else if c in [`}`, `]`] { 359 | if start_ch == `[` && c != `]` { 360 | return error('Bracket mismatch: $start_ch .. $c') 361 | } 362 | if start_ch == `{` && c != `}` { 363 | return error('Bracket mismatch: $start_ch .. $c') 364 | } 365 | if tok := s.catch_up() { 366 | return tok 367 | } 368 | s.block_levels.pop() 369 | tok := s.tokenize(s.to_token_kind(c), s.ts.pos, s.ts.pos + 1) ? 370 | s.ts.skip(1) // Position the pointer right after the closing bracket. 371 | return tok 372 | } else if ts.is_newline(c) { 373 | if tok := s.catch_up() { 374 | return tok 375 | } 376 | s.newline() 377 | } else if c in [`,`, `:`] { 378 | if tok := s.catch_up() { 379 | return tok 380 | } 381 | tok := s.tokenize(s.to_token_kind(c), s.ts.pos, s.ts.pos + 1) ? 382 | s.ts.skip(1) 383 | return tok 384 | } else if c in [`"`, `'`] { 385 | return s.quoted_string_scanner() 386 | } else if c == `#` { 387 | if tok := s.catch_up() { 388 | return tok 389 | } 390 | s.ts.skip_to_eol() 391 | } else { 392 | s.ts.move(1) 393 | } 394 | } 395 | return error("Missing closing bracket: '$start_ch'") 396 | } 397 | 398 | // quoted_string_scanner Scan strings quoted with either `"` or `'` 399 | fn (mut s Scanner) quoted_string_scanner() ?Token { 400 | yaml_quoted_escapes := fn (start_ch u8, str string) bool { 401 | return start_ch == `'` && str.starts_with("''") 402 | } 403 | 404 | mut str := s.ts.quoted_string_scanner(yaml_quoted_escapes) ? 405 | str = ts.replace_nl_space(str) 406 | return s.add_token(.xstr, str) 407 | } 408 | 409 | // flow_string_scanner YAML has support for multi line flow text, triggered by 410 | // either '|' or '>'. Multi-line PLAIN text exists as well and has no start 411 | // indicator. Since the rules are quite different, plain text is handled 412 | // elsewhere. 413 | // The scanner position will be at the '|' or '>' char. A newline (or comment) 414 | // must follow the start indicator. 415 | fn (mut s Scanner) flow_string_scanner() ?Token { 416 | t_type := s.to_token_kind(s.ts.at(s.ts.pos)) 417 | 418 | { // Skip to end of line. Only comments are allowed. 419 | s.ts.skip(1) 420 | str := s.ts.read_line().trim_space() 421 | if str.len > 0 && str[0] !in [`#`, `-`] { 422 | return error("'|' and '>' must be followed by newline or a comment: '$str'") 423 | } 424 | } 425 | mut text := '' 426 | mut indent := 0 427 | mut nl := if t_type == TokenKind.greater { ' ' } else { '\n' } 428 | mut start_pos := 0 429 | 430 | for !s.ts.is_eof() { 431 | start_pos = s.ts.pos 432 | s.newline() 433 | 434 | str := s.ts.read_line() 435 | is_empty := str.trim_space().len == 0 436 | 437 | if text.len == 0 && is_empty == true { 438 | return error("First line of multi-line text must not be empty: '${s.ts.substr_escaped(s.ts.pos - 10, 439 | 20)}'") 440 | } 441 | 442 | x := ts.leading_spaces(str) 443 | if indent == 0 && x > 0 { 444 | indent = x 445 | } 446 | 447 | // eprintln("is_empty: $is_empty, indent: $indent, x: $x, str: '$str'") 448 | lstr := if str.len >= indent { str[indent..] } else { '' } 449 | if text.len == 0 { 450 | text = lstr 451 | } else if is_empty == true { 452 | text += '\n' + lstr 453 | nl = '\n' 454 | } else if indent > 0 && x >= indent { 455 | text += nl + lstr 456 | if lstr.len == 0 || lstr[0].is_space() { 457 | nl = '\n' 458 | } else { 459 | nl = if t_type == TokenKind.greater { ' ' } else { '\n' } 460 | } 461 | } else { 462 | break 463 | } 464 | } 465 | 466 | if !s.ts.is_eof() { 467 | s.ts.set_pos(start_pos) 468 | } 469 | s.ts.skip(0) 470 | 471 | return s.add_token(.xstr, text) 472 | } 473 | 474 | // plain_multi_line_scanner This method is invoked at newline and if 475 | // catch_up() returned an .xstr token. This is potentially the first 476 | // line of a plain multi-line text. 477 | // In any case an .xstr token will be returned. Either with the single 478 | // line plain value, or the multi-line plain value. 479 | // Upon return, the scanner position will be at the newline (or eof). 480 | // IMHO YAML has quite some 'rules', which is also true for plain text. 481 | // See https://yaml.org/spec/1.2/spec.html#id2788859 for more details. 482 | fn (mut s Scanner) plain_multi_line_scanner(pos int, tok Token) Token { 483 | mut text := tok.val 484 | mut indent := tok.column - 1 485 | mut nl := ' ' 486 | 487 | for !s.ts.is_eof() { 488 | // Remember the current position if the line is not 489 | // part of the multi-line 490 | start_pos := s.ts.pos 491 | s.newline() 492 | 493 | str := s.ts.read_line() 494 | 495 | trimmed := str.trim_space() 496 | empty := trimmed.len == 0 497 | x := ts.leading_spaces(str) // The indent level of the current line 498 | 499 | // See YAML spec: Empty lines or leading spaces, will trigger newline 500 | if empty == true { 501 | text += '\n' 502 | nl = '' 503 | continue 504 | } else { 505 | text += nl 506 | nl = ' ' 507 | } 508 | 509 | // eprintln("plain: x: $x, indent: $indent, str: '${ts.str_escaped(str)}'") 510 | if x == 0 || x < indent || str.contains_any_substr(['- ', ': ', ' #']) || str.ends_with('-') 511 | || str.ends_with(':') || str.starts_with('#') { 512 | s.ts.pos = start_pos 513 | s.ts.line_no-- 514 | s.ts.skip(0) 515 | break 516 | } else { 517 | text += trimmed 518 | } 519 | } 520 | 521 | // See YAML spec for plain text: leading and trailing spaces are trimmed. 522 | text = text.trim_space() 523 | return Token{ 524 | ...tok 525 | val: text 526 | } 527 | } 528 | -------------------------------------------------------------------------------- /step3_reader_test.v: -------------------------------------------------------------------------------- 1 | module yaml 2 | 3 | // YAML spec: https://yaml.org/spec/1.2/spec.html 4 | // To test your YAML: https://www.json2yaml.com/ 5 | import os 6 | 7 | const test_data_dir = os.dir(@FILE) + '/test_data' 8 | 9 | const debug = 0 10 | 11 | fn test_z_ex_01() ? { 12 | content := os.read_file('$test_data_dir/z_ex_01.yaml') ? 13 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 14 | assert docs.documents.len == 1 15 | x := docs.get(0) 16 | assert x is YamlListValue 17 | if x is YamlListValue { 18 | assert x.ar.len == 3 19 | // eprintln(x.ar) 20 | assert x.ar[0] == YamlValue('Mark McGwire') 21 | assert x.ar[1] == YamlValue('Sammy Sosa') 22 | assert x.ar[2] == YamlValue('Ken Griffey') 23 | } 24 | } 25 | 26 | fn test_z_ex_02() ? { 27 | content := os.read_file('$test_data_dir/z_ex_02.yaml') ? 28 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 29 | assert docs.documents.len == 1 30 | x := docs.get(0) 31 | assert x is YamlMapValue 32 | if x is YamlMapValue { 33 | assert x.obj.len == 3 34 | // eprintln(x.obj) 35 | assert x.obj['hr'] ? == YamlValue(i64(65)) 36 | assert x.obj['avg'] ? == YamlValue(f64(0.278)) 37 | assert x.obj['rbi'] ? == YamlValue(i64(147)) 38 | } 39 | } 40 | 41 | fn test_z_ex_03() ? { 42 | content := os.read_file('$test_data_dir/z_ex_03.yaml') ? 43 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 44 | assert docs.documents.len == 1 45 | x1 := docs.get(0) 46 | assert x1 is YamlMapValue 47 | if x1 is YamlMapValue { 48 | assert x1.obj.len == 3 49 | x2 := x1.obj['american'] ? 50 | assert x2 is YamlListValue 51 | if x2 is YamlListValue { 52 | assert x2.ar.len == 3 53 | assert x2.ar[0] == YamlValue('Boston Red Sox') 54 | assert x2.ar[1] == YamlValue('Detroit Tigers') 55 | assert x2.ar[2] == YamlValue('New York Yankees') 56 | } 57 | x3 := x1.obj['national'] ? 58 | assert x3 is YamlListValue 59 | if x3 is YamlListValue { 60 | assert x3.ar.len == 3 61 | assert x3.ar[0] == YamlValue('New York Mets') 62 | assert x3.ar[1] == YamlValue('Chicago Cubs') 63 | assert x3.ar[2] == YamlValue('Atlanta Braves') 64 | } 65 | x4 := x1.obj["array"]? 66 | assert x4 is YamlListValue 67 | if x4 is YamlListValue { 68 | assert x4.ar.len == 3 69 | assert x4.ar[0] == YamlValue(i64(1)) 70 | assert x4.ar[1] == YamlValue(i64(2)) 71 | assert x4.ar[2] == YamlValue(i64(3)) 72 | } 73 | } 74 | } 75 | 76 | fn test_z_ex_04() ? { 77 | content := os.read_file('$test_data_dir/z_ex_04.yaml') ? 78 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 79 | assert docs.documents.len == 1 80 | x1 := docs.get(0) 81 | assert x1 is YamlListValue 82 | if x1 is YamlListValue { 83 | assert x1.ar.len == 2 84 | x2 := x1.ar[0] 85 | assert x2 is YamlMapValue 86 | if x2 is YamlMapValue { 87 | assert x2.obj.len == 3 88 | assert x2.obj['name'] ? == YamlValue('Mark McGwire') 89 | assert x2.obj['hr'] ? == YamlValue(i64(65)) 90 | assert x2.obj['avg'] ? == YamlValue(f64(0.278)) 91 | } 92 | x3 := x1.ar[1] 93 | assert x3 is YamlMapValue 94 | if x3 is YamlMapValue { 95 | assert x3.obj.len == 3 96 | assert x3.obj['name'] ? == YamlValue('Sammy Sosa') 97 | assert x3.obj['hr'] ? == YamlValue(i64(63)) 98 | assert x3.obj['avg'] ? == YamlValue(f64(0.288)) 99 | } 100 | } 101 | } 102 | 103 | fn test_z_ex_05() ? { 104 | content := os.read_file('$test_data_dir/z_ex_05.yaml') ? 105 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 106 | assert docs.documents.len == 1 107 | x1 := docs.get(0) 108 | assert x1 is YamlListValue 109 | if x1 is YamlListValue { 110 | assert x1.ar.len == 3 111 | x2 := x1.ar[0] 112 | assert x2 is YamlListValue 113 | if x2 is YamlListValue { 114 | assert x2.ar.len == 3 115 | assert x2.ar == [YamlValue('name'), 'hr', 'avg'] 116 | } 117 | x3 := x1.ar[1] 118 | assert x3 is YamlListValue 119 | if x3 is YamlListValue { 120 | assert x3.ar.len == 3 121 | assert x3.ar == [YamlValue('Mark McGwire'), i64(65), f64(0.278)] 122 | } 123 | x4 := x1.ar[2] 124 | assert x4 is YamlListValue 125 | if x4 is YamlListValue { 126 | assert x4.ar.len == 3 127 | assert x4.ar == [YamlValue('Sammy Sosa'), i64(63), f64(0.288)] 128 | } 129 | } 130 | } 131 | 132 | fn test_z_ex_06() ? { 133 | content := os.read_file('$test_data_dir/z_ex_06.yaml') ? 134 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 135 | assert docs.documents.len == 1 136 | x1 := docs.get(0) 137 | assert x1 is YamlMapValue 138 | if x1 is YamlMapValue { 139 | assert x1.obj.len == 2 140 | x2 := x1.obj['Mark McGwire'] ? 141 | assert x2 is YamlMapValue 142 | if x2 is YamlMapValue { 143 | assert x2.obj.len == 2 144 | assert x2.obj['hr'] ? == YamlValue(i64(65)) 145 | assert x2.obj['avg'] ? == YamlValue(f64(0.278)) 146 | } 147 | x3 := x1.obj['Sammy Sosa'] ? 148 | assert x3 is YamlMapValue 149 | if x3 is YamlMapValue { 150 | assert x3.obj.len == 2 151 | assert x3.obj['hr'] ? == YamlValue(i64(63)) 152 | assert x3.obj['avg'] ? == YamlValue(f64(0.288)) 153 | } 154 | } 155 | } 156 | 157 | fn test_z_ex_07() ? { 158 | content := os.read_file('$test_data_dir/z_ex_07.yaml') ? 159 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 160 | assert docs.documents.len == 2 161 | x1 := docs.get(0) 162 | assert x1 is YamlListValue 163 | if x1 is YamlListValue { 164 | assert x1.ar.len == 3 165 | assert x1.ar[0] == YamlValue('Mark McGwire') 166 | assert x1.ar[1] == YamlValue('Sammy Sosa') 167 | assert x1.ar[2] == YamlValue('Ken Griffey') 168 | } 169 | x2 := docs.get(1) 170 | assert x2 is YamlListValue 171 | if x2 is YamlListValue { 172 | assert x2.ar.len == 2 173 | assert x2.ar[0] == YamlValue('Chicago Cubs') 174 | assert x2.ar[1] == YamlValue('St Louis Cardinals') 175 | } 176 | } 177 | 178 | fn test_z_ex_08() ? { 179 | content := os.read_file('$test_data_dir/z_ex_08.yaml') ? 180 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 181 | assert docs.documents.len == 2 182 | x1 := docs.get(0) 183 | assert x1 is YamlMapValue 184 | if x1 is YamlMapValue { 185 | assert x1.obj.len == 3 186 | assert x1.obj['time'] ? == YamlValue('20:03:20') 187 | assert x1.obj['player'] ? == YamlValue('Sammy Sosa') 188 | assert x1.obj['action'] ? == YamlValue('strike (miss)') 189 | } 190 | x2 := docs.get(1) 191 | assert x2 is YamlMapValue 192 | if x2 is YamlMapValue { 193 | assert x2.obj.len == 3 194 | assert x2.obj['time'] ? == YamlValue('20:03:47') 195 | assert x2.obj['player'] ? == YamlValue('Sammy Sosa') 196 | assert x2.obj['action'] ? == YamlValue('grand slam') 197 | } 198 | } 199 | 200 | fn test_z_ex_09() ? { 201 | content := os.read_file('$test_data_dir/z_ex_09.yaml') ? 202 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 203 | assert docs.documents.len == 1 204 | x1 := docs.get(0) 205 | assert x1 is YamlMapValue 206 | if x1 is YamlMapValue { 207 | assert x1.obj.len == 2 208 | x2 := x1.obj['hr'] ? 209 | assert x2 is YamlListValue 210 | if x2 is YamlListValue { 211 | assert x2.ar.len == 2 212 | assert x2.ar[0] == YamlValue('Mark McGwire') 213 | assert x2.ar[1] == YamlValue('Sammy Sosa') 214 | } 215 | x3 := x1.obj['rbi'] ? 216 | assert x3 is YamlListValue 217 | if x3 is YamlListValue { 218 | assert x3.ar.len == 2 219 | assert x3.ar[0] == YamlValue('Sammy Sosa') 220 | assert x3.ar[1] == YamlValue('Ken Griffey') 221 | } 222 | } 223 | } 224 | 225 | fn test_z_ex_10_in_tokenizer() ? { 226 | content := os.read_file('$test_data_dir/z_ex_10.yaml') ? 227 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_tokenizer, debug: debug) ? 228 | assert docs.documents.len == 1 229 | x1 := docs.get(0) 230 | assert x1 is YamlMapValue 231 | if x1 is YamlMapValue { 232 | assert x1.obj.len == 2 233 | x2 := x1.obj['hr'] ? 234 | assert x2 is YamlListValue 235 | if x2 is YamlListValue { 236 | assert x2.ar.len == 2 237 | assert x2.ar[0] == YamlValue('Mark McGwire') 238 | assert x2.ar[1] == YamlValue('Sammy Sosa') 239 | } 240 | x3 := x1.obj['rbi'] ? 241 | assert x3 is YamlListValue 242 | if x3 is YamlListValue { 243 | assert x3.ar.len == 2 244 | assert x3.ar[0] == YamlValue('Sammy Sosa') 245 | assert x3.ar[1] == YamlValue('Ken Griffey') 246 | } 247 | } 248 | 249 | // Since the tags are already replaced in the tokenizer, they are no longer 250 | // visible to the reader. 251 | assert docs.tags.len == 0 252 | } 253 | 254 | fn test_z_ex_10() ? { 255 | content := os.read_file('$test_data_dir/z_ex_10.yaml') ? 256 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 257 | assert docs.documents.len == 1 258 | x1 := docs.get(0) 259 | assert x1 is YamlMapValue 260 | if x1 is YamlMapValue { 261 | assert x1.obj.len == 2 262 | x2 := x1.obj['hr'] ? 263 | assert x2 is YamlListValue 264 | if x2 is YamlListValue { 265 | assert x2.ar.len == 2 266 | assert x2.ar[0] == YamlValue('Mark McGwire') 267 | assert x2.ar[1] == YamlValue('Sammy Sosa') 268 | } 269 | x3 := x1.obj['rbi'] ? 270 | assert x3 is YamlListValue 271 | if x3 is YamlListValue { 272 | assert x3.ar.len == 2 273 | assert x3.ar[0] == YamlValue('Sammy Sosa') 274 | assert x3.ar[1] == YamlValue('Ken Griffey') 275 | } 276 | } 277 | 278 | assert docs.tags.len == 1 279 | assert 'SS' in docs.tags 280 | assert docs.tags['SS'] ? == YamlValue('Sammy Sosa') 281 | } 282 | 283 | fn test_z_ex_11() ? { 284 | content := os.read_file('$test_data_dir/z_ex_11.yaml') ? 285 | 286 | // Complex mapping keys are not supported 287 | if _ := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) { 288 | assert false 289 | } 290 | } 291 | 292 | fn test_z_ex_12() ? { 293 | content := os.read_file('$test_data_dir/z_ex_12.yaml') ? 294 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 295 | assert docs.documents.len == 1 296 | x1 := docs.get(0) 297 | assert x1 is YamlListValue 298 | if x1 is YamlListValue { 299 | assert x1.ar.len == 3 300 | x2 := x1.ar[0] 301 | assert x2 is YamlMapValue 302 | if x2 is YamlMapValue { 303 | assert x2.obj.len == 2 304 | assert x2.obj['item'] ? == YamlValue('Super Hoop') 305 | assert x2.obj['quantity'] ? == YamlValue(i64(1)) 306 | } 307 | x3 := x1.ar[1] 308 | assert x3 is YamlMapValue 309 | if x3 is YamlMapValue { 310 | assert x3.obj.len == 2 311 | assert x3.obj['item'] ? == YamlValue('Basketball') 312 | assert x3.obj['quantity'] ? == YamlValue(i64(4)) 313 | } 314 | x4 := x1.ar[2] 315 | assert x4 is YamlMapValue 316 | if x4 is YamlMapValue { 317 | assert x4.obj.len == 2 318 | assert x4.obj['item'] ? == YamlValue('Big Shoes') 319 | assert x4.obj['quantity'] ? == YamlValue(i64(1)) 320 | } 321 | } 322 | } 323 | 324 | fn test_z_ex_13() ? { 325 | content := os.read_file('$test_data_dir/z_ex_13.yaml') ? 326 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 327 | assert docs.documents.len == 1 328 | x1 := docs.get(0) 329 | assert x1 is string 330 | if x1 is string { 331 | // According to the YAML spec, newline will always be "\n" irrespective of the OS 332 | assert x1 == '\\//||\\/||\n// || ||__' 333 | } 334 | } 335 | 336 | fn test_z_ex_14() ? { 337 | content := os.read_file('$test_data_dir/z_ex_14.yaml') ? 338 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 339 | assert docs.documents.len == 1 340 | x1 := docs.get(0) 341 | assert x1 is string 342 | if x1 is string { 343 | assert x1 == "Mark McGwire's year was crippled by a knee injury." 344 | } 345 | } 346 | 347 | fn test_z_ex_15() ? { 348 | content := os.read_file('$test_data_dir/z_ex_15.yaml') ? 349 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 350 | assert docs.documents.len == 1 351 | x1 := docs.get(0) 352 | assert x1 is string 353 | if x1 is string { 354 | // According to the YAML spec, newline will always be "\n" irrespective of the OS 355 | assert x1 == 'Sammy Sosa completed another fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!' 356 | } 357 | } 358 | 359 | fn test_z_ex_16() ? { 360 | content := os.read_file('$test_data_dir/z_ex_16.yaml') ? 361 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 362 | assert docs.documents.len == 1 363 | x1 := docs.get(0) 364 | assert x1 is YamlMapValue 365 | if x1 is YamlMapValue { 366 | assert x1.obj.len == 3 367 | assert x1.obj['name'] ? == YamlValue('Mark McGwire') 368 | 369 | // According to https://www.json2yaml.com/ the following 2 require a "\n" at the very end. 370 | // However, I don't understand the logic. It is totally inconsistent. 371 | assert x1.obj['accomplishment'] ? == YamlValue('Mark set a major league home run record in 1998.') 372 | assert x1.obj['stats'] ? == YamlValue('65 Home Runs\n0.278 Batting Average\n') 373 | } 374 | } 375 | 376 | fn test_z_ex_17() ? { 377 | content := os.read_file('$test_data_dir/z_ex_17.yaml') ? 378 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 379 | assert docs.documents.len == 1 380 | x1 := docs.get(0) 381 | assert x1 is YamlMapValue 382 | if x1 is YamlMapValue { 383 | assert x1.obj.len == 6 384 | assert x1.obj['unicode'] ? == YamlValue('Sosa did fine.☺') 385 | assert x1.obj['control'] ? == YamlValue('\b1998\t1999\t2000\n') 386 | assert x1.obj['hex esc'] ? == YamlValue('\r\n is \r\n') 387 | assert x1.obj['single'] ? == YamlValue('"Howdy!" he cried.') 388 | assert x1.obj['quoted'] ? == YamlValue(" # Not a 'comment'.") 389 | assert x1.obj['tie-fighter'] ? == YamlValue('|\\-*-/|') 390 | } 391 | } 392 | 393 | fn test_z_ex_18() ? { 394 | content := os.read_file('$test_data_dir/z_ex_18.yaml') ? 395 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 396 | assert docs.documents.len == 1 397 | x1 := docs.get(0) 398 | assert x1 is YamlMapValue 399 | if x1 is YamlMapValue { 400 | assert x1.obj.len == 7 401 | assert x1.obj['plain 1'] ? == YamlValue('This unquoted scalar spans many lines. It has three lines.') 402 | assert x1.obj['plain 2'] ? == YamlValue('This is also multi-line') 403 | assert x1.obj['plain 3'] ? == YamlValue('The second line is more indented') 404 | assert x1.obj['plain 4'] ? == YamlValue('The third line is more indented') 405 | assert x1.obj['plain 5'] ? == YamlValue('This is another example that should work') 406 | assert x1.obj['plain 6'] ? == YamlValue('The second line\nis more indented') 407 | 408 | assert x1.obj['quoted'] ? == YamlValue('So does this quoted scalar with.\n') 409 | } 410 | } 411 | 412 | fn test_z_ex_19() ? { 413 | content := os.read_file('$test_data_dir/z_ex_19.yaml') ? 414 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 415 | assert docs.documents.len == 1 416 | x1 := docs.get(0) 417 | assert x1 is YamlMapValue 418 | if x1 is YamlMapValue { 419 | assert x1.obj.len == 4 420 | assert x1.obj['canonical'] ? == YamlValue(i64(12345)) 421 | assert x1.obj['decimal'] ? == YamlValue(i64(12345)) 422 | assert x1.obj['octal'] ? == YamlValue(i64(12)) 423 | assert x1.obj['hexadecimal'] ? == YamlValue(i64(12)) 424 | } 425 | } 426 | 427 | fn test_z_ex_20() ? { 428 | content := os.read_file('$test_data_dir/z_ex_20.yaml') ? 429 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 430 | assert docs.documents.len == 1 431 | x1 := docs.get(0) 432 | assert x1 is YamlMapValue 433 | if x1 is YamlMapValue { 434 | assert x1.obj.len == 5 435 | assert x1.obj['canonical'] ? == YamlValue(f64(1.23015e+3)) 436 | assert x1.obj['exponential'] ? == YamlValue(f64(12.3015e+02)) 437 | assert x1.obj['fixed'] ? == YamlValue(f64(1230.15)) 438 | assert x1.obj['negative infinity'] ? == YamlValue('-.inf') 439 | assert x1.obj['not a number'] ? == YamlValue('.NaN') 440 | } 441 | } 442 | 443 | fn test_z_ex_21() ? { 444 | content := os.read_file('$test_data_dir/z_ex_21.yaml') ? 445 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 446 | assert docs.documents.len == 1 447 | x1 := docs.get(0) 448 | assert x1 is YamlMapValue 449 | if x1 is YamlMapValue { 450 | assert x1.obj.len == 3 451 | assert x1.obj['null'] ? == YamlValue('') 452 | assert x1.obj['string'] ? == YamlValue('012345') // quoted string are never casted 453 | x2 := x1.obj['booleans'] ? 454 | assert x2 is YamlListValue 455 | if x2 is YamlListValue { 456 | assert x2.ar.len == 2 457 | assert x2.ar[0] == YamlValue(true) 458 | assert x2.ar[1] == YamlValue(false) 459 | } 460 | } 461 | } 462 | 463 | fn test_z_ex_22() ? { 464 | content := os.read_file('$test_data_dir/z_ex_22.yaml') ? 465 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 466 | assert docs.documents.len == 1 467 | x1 := docs.get(0) 468 | assert x1 is YamlMapValue 469 | if x1 is YamlMapValue { 470 | assert x1.obj.len == 4 471 | assert x1.obj['canonical'] ? == YamlValue('2001-12-15T02:59:43.1Z') 472 | assert x1.obj['iso8601'] ? == YamlValue('2001-12-14t21:59:43.10-05:00') 473 | assert x1.obj['spaced'] ? == YamlValue('2001-12-14 21:59:43.10 -5') 474 | assert x1.obj['date'] ? == YamlValue('2002-12-14') 475 | } 476 | } 477 | 478 | fn test_z_ex_23() ? { 479 | content := os.read_file('$test_data_dir/z_ex_23.yaml') ? 480 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 481 | assert docs.documents.len == 1 482 | x1 := docs.get(0) 483 | assert x1 is YamlMapValue 484 | if x1 is YamlMapValue { 485 | assert x1.obj.len == 3 486 | assert x1.obj['not-date'] ? == YamlValue('2002-04-28') 487 | assert x1.obj['picture'] ? == YamlValue('R0lGODlhDAAMAIQAAP//9/X\n17unp5WZmZgAAAOfn515eXv\nPz7Y6OjuDg4J+fn5OTk6enp\n56enmleECcgggoBADs=\n') 488 | assert x1.obj['application specific tag'] ? == YamlValue('The semantics of the tag\nabove may be different for\ndifferent documents.\n') 489 | } 490 | } 491 | 492 | fn test_z_ex_24() ? { 493 | content := os.read_file('$test_data_dir/z_ex_24.yaml') ? 494 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 495 | assert docs.documents.len == 1 496 | x1 := docs.get(0) 497 | assert x1 is YamlListValue 498 | if x1 is YamlListValue { 499 | assert x1.ar.len == 3 500 | x2 := x1.ar[0] 501 | assert x2 is YamlMapValue 502 | if x2 is YamlMapValue { 503 | assert x2.obj.len == 2 504 | x3 := x2.obj['center'] ? 505 | assert x3 is YamlMapValue 506 | if x3 is YamlMapValue { 507 | assert x3.obj.len == 2 508 | assert x3.obj['x'] ? == YamlValue(i64(73)) 509 | assert x3.obj['y'] ? == YamlValue(i64(129)) 510 | } 511 | assert x2.obj['radius'] ? == YamlValue(i64(7)) 512 | } 513 | x4 := x1.ar[1] 514 | assert x4 is YamlMapValue 515 | if x4 is YamlMapValue { 516 | assert x4.obj.len == 2 517 | x4a := x4.obj['start'] ? 518 | assert x4a is YamlMapValue 519 | if x4a is YamlMapValue { 520 | assert x4a.obj.len == 2 521 | assert x4a.obj['x'] ? == YamlValue(i64(73)) 522 | assert x4a.obj['y'] ? == YamlValue(i64(129)) 523 | } 524 | 525 | x5 := x4.obj['finish'] ? 526 | assert x5 is YamlMapValue 527 | if x5 is YamlMapValue { 528 | assert x5.obj.len == 2 529 | assert x5.obj['x'] ? == YamlValue(i64(89)) 530 | assert x5.obj['y'] ? == YamlValue(i64(102)) 531 | } 532 | } 533 | x6 := x1.ar[2] 534 | assert x6 is YamlMapValue 535 | if x6 is YamlMapValue { 536 | assert x6.obj.len == 3 537 | x4a := x6.obj['start'] ? 538 | assert x4a is YamlMapValue 539 | if x4a is YamlMapValue { 540 | assert x4a.obj.len == 2 541 | assert x4a.obj['x'] ? == YamlValue(i64(73)) 542 | assert x4a.obj['y'] ? == YamlValue(i64(129)) 543 | } 544 | 545 | assert x6.obj['color'] ? == YamlValue(i64(0xFFEEBB)) 546 | assert x6.obj['text'] ? == YamlValue('Pretty vector drawing.') 547 | } 548 | } 549 | } 550 | 551 | fn test_z_ex_25() ? { 552 | content := os.read_file('$test_data_dir/z_ex_25.yaml') ? 553 | if _ := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) { 554 | assert false 555 | } 556 | // '?' is not yet support 557 | /* 558 | docs := yaml_reader(fpath, replace_tags: ReplaceTagsEnum.in_reader, debug: debug)? 559 | assert docs.documents.len == 1 560 | x := docs.get(0) 561 | assert x is YamlMapValue 562 | if x is YamlMapValue { 563 | assert x.obj.len == 3 564 | assert x.obj["hr"] == YamlValue("65") 565 | assert x.obj["avg"] == YamlValue("0.278") 566 | assert x.obj["rbi"] == YamlValue("147") 567 | } 568 | */ 569 | } 570 | 571 | fn test_z_ex_26() ? { 572 | content := os.read_file('$test_data_dir/z_ex_26.yaml') ? 573 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 574 | assert docs.documents.len == 1 575 | x1 := docs.get(0) 576 | assert x1 is YamlListValue 577 | if x1 is YamlListValue { 578 | assert x1.ar.len == 3 579 | x2 := x1.ar[0] 580 | assert x2 is YamlMapValue 581 | if x2 is YamlMapValue { 582 | assert x2.obj.len == 1 583 | assert x2.obj['Mark McGwire'] ? == YamlValue(i64(65)) 584 | } 585 | x3 := x1.ar[1] 586 | assert x3 is YamlMapValue 587 | if x3 is YamlMapValue { 588 | assert x3.obj.len == 1 589 | assert x3.obj['Sammy Sosa'] ? == YamlValue(i64(63)) 590 | } 591 | x4 := x1.ar[2] 592 | assert x4 is YamlMapValue 593 | if x4 is YamlMapValue { 594 | assert x4.obj.len == 1 595 | assert x4.obj['Ken Griffy'] ? == YamlValue(i64(58)) 596 | } 597 | } 598 | } 599 | 600 | fn test_z_ex_27() ? { 601 | content := os.read_file('$test_data_dir/z_ex_27.yaml') ? 602 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 603 | assert docs.documents.len == 1 604 | x1 := docs.get(0) 605 | assert x1 is YamlMapValue 606 | if x1 is YamlMapValue { 607 | assert x1.obj.len == 8 608 | assert x1.obj['invoice'] ? == YamlValue(i64(34843)) 609 | } 610 | } 611 | 612 | fn test_nested_objects() ? { 613 | content := os.read_file('$test_data_dir/nested_objects.yaml') ? 614 | docs := yaml_reader(content, replace_tags: ReplaceTagsEnum.in_reader, debug: debug) ? 615 | assert docs.documents.len == 1 616 | x1 := docs.get(0) 617 | assert x1 is YamlMapValue 618 | if x1 is YamlMapValue { 619 | assert x1.obj.len == 3 620 | assert x1.obj['aaa'] ? == YamlValue('string') 621 | x2 := x1.obj['bbb'] ? 622 | assert x2 is YamlMapValue 623 | if x2 is YamlMapValue { 624 | assert x2.obj.len == 1 625 | assert x2.obj['111'] ? == YamlValue('1-1-1') 626 | } 627 | x3 := x1.obj['ccc'] ? 628 | assert x3 is YamlMapValue 629 | if x3 is YamlMapValue { 630 | assert x3.obj.len == 2 631 | assert x3.obj['222'] ? == YamlValue('') 632 | assert x3.obj['223'] ? == YamlValue('xxx') 633 | } 634 | } 635 | } 636 | 637 | // 638 | --------------------------------------------------------------------------------