├── tests ├── keys.2.aml ├── arrays.6.aml ├── ignore.3.aml ├── scopes.6.aml ├── values.1.aml ├── arrays.1.aml ├── ignore.2.aml ├── scopes.1.aml ├── arrays.2.aml ├── arrays.3.aml ├── ignore.5.aml ├── scopes.2.aml ├── scopes.3.aml ├── ignore.4.aml ├── skip.5.aml ├── values.2.aml ├── values.3.aml ├── values.4.aml ├── values.5.aml ├── arrays.5.aml ├── arrays.7.aml ├── ignore.1.aml ├── scopes.5.aml ├── skip.0.aml ├── skip.1.aml ├── skip.12.aml ├── skip.3.aml ├── arrays.11.aml ├── arrays.4.aml ├── arrays_simple.3.aml ├── ignore.8.aml ├── keys.4.aml ├── multi_line.2.aml ├── scopes.14.aml ├── scopes.4.aml ├── skip.2.aml ├── skip.4.aml ├── values.8.aml ├── arrays.10.aml ├── arrays.9.aml ├── arrays_simple.2.aml ├── ignore.7.aml ├── scopes.12.aml ├── scopes.13.aml ├── scopes.8.aml ├── unicode.4.aml ├── values.10.aml ├── keys.1.aml ├── multi_line.10.aml ├── multi_line.21.aml ├── scopes.7.aml ├── skip.7.aml ├── skip.8.aml ├── arrays.12.aml ├── arrays.13.aml ├── ignore.6.aml ├── multi_line.7.aml ├── multi_line.8.aml ├── scopes.15.aml ├── scopes.16.aml ├── values.6.aml ├── arrays_simple.1.aml ├── arrays_simple.4.aml ├── skip.6.aml ├── values.9.aml ├── arrays_simple.8.aml ├── multi_line.13.aml ├── multi_line.18.aml ├── scopes.9.aml ├── skip.11.aml ├── arrays.14.aml ├── multi_line.1.aml ├── multi_line.12.aml ├── multi_line.16.aml ├── multi_line.23.aml ├── multi_line.4.aml ├── skip.10.aml ├── arrays_complex.1.aml ├── multi_line.6.aml ├── arrays_complex.15.aml ├── multi_line.11.aml ├── multi_line.17.aml ├── arrays_simple.21.aml ├── freeform.1.aml ├── multi_line.22.aml ├── multi_line.9.aml ├── scopes.10.aml ├── scopes.17.aml ├── skip.9.aml ├── unicode.1.aml ├── unicode.2.aml ├── unicode.3.aml ├── arrays_complex.14.aml ├── arrays_complex.2.aml ├── arrays_simple.10.aml ├── arrays_simple.12.aml ├── arrays_simple.5.aml ├── arrays_simple.6.aml ├── multi_line.19.aml ├── multi_line.20.aml ├── multi_line.33.aml ├── arrays_simple.7.aml ├── freeform.2.aml ├── arrays_nested.12.aml ├── arrays_simple.11.aml ├── arrays_simple.20.aml ├── freeform.10.aml ├── multi_line.26.aml ├── multi_line.27.aml ├── multi_line.3.aml ├── arrays_simple.9.aml ├── multi_line.29.aml ├── arrays_complex.13.aml ├── arrays_nested.1.aml ├── arrays_simple.14.aml ├── arrays_simple.15.aml ├── keys.5.aml ├── arrays_complex.6.aml ├── arrays_nested.9.aml ├── arrays_simple.17.aml ├── arrays_simple.18.aml ├── freeform.8.aml ├── skip.13.aml ├── arrays_complex.9.aml ├── arrays_simple.13.aml ├── arrays_simple.19.aml ├── multi_line.28.aml ├── unicode.5.aml ├── arrays.8.aml ├── arrays_complex.12.aml ├── arrays_complex.4.aml ├── arrays_complex.8.aml ├── arrays_nested.2.aml ├── freeform.11.aml ├── freeform.18.aml ├── freeform.6.aml ├── freeform.9.aml ├── arrays_nested.3.aml ├── arrays_simple.16.aml ├── freeform.19.aml ├── arrays_complex.11.aml ├── scopes.11.aml ├── arrays_nested.6.aml ├── freeform.16.aml ├── arrays_complex.10.aml ├── freeform.13.aml ├── freeform.14.aml ├── freeform.15.aml ├── freeform.17.aml ├── arrays_complex.5.aml ├── scopes.18.aml ├── arrays_complex.3.aml ├── arrays_nested.5.aml ├── multi_line.34.aml ├── arrays_complex.7.aml ├── arrays_nested.11.aml ├── freeform.12.aml ├── arrays_nested.10.aml ├── freeform.7.aml ├── arrays_nested.4.aml ├── arrays_nested.8.aml ├── arrays_nested.7.aml ├── freeform.3.aml ├── freeform.4.aml ├── keys.6.aml └── freeform.5.aml ├── package.json ├── index.js ├── test_document.txt ├── tokenizer.js ├── readme.rst ├── test.js ├── parser.js └── assembler.js /tests/keys.2.aml: -------------------------------------------------------------------------------- 1 | test: spaces are not allowed in keys 2 | result: {} 3 | 4 | k ey:value 5 | -------------------------------------------------------------------------------- /tests/arrays.6.aml: -------------------------------------------------------------------------------- 1 | test: ignores text after [array] 2 | result: {"array": []} 3 | 4 | [array]a 5 | -------------------------------------------------------------------------------- /tests/ignore.3.aml: -------------------------------------------------------------------------------- 1 | test: :ignore is case insensitive 2 | result: {} 3 | 4 | :iGnOrE 5 | key:value 6 | -------------------------------------------------------------------------------- /tests/scopes.6.aml: -------------------------------------------------------------------------------- 1 | test: ignores text after {scope} 2 | result: {"scope": {}} 3 | 4 | {scope}a 5 | -------------------------------------------------------------------------------- /tests/values.1.aml: -------------------------------------------------------------------------------- 1 | test: Parses key value pairs 2 | result: {"key": "value"} 3 | 4 | key:value 5 | -------------------------------------------------------------------------------- /tests/arrays.1.aml: -------------------------------------------------------------------------------- 1 | test: [array] creates an empty array at array 2 | result: {"array": []} 3 | 4 | [array] 5 | -------------------------------------------------------------------------------- /tests/ignore.2.aml: -------------------------------------------------------------------------------- 1 | test: text after :ignore should be ignored 2 | result: {} 3 | 4 | :ignore 5 | key:value 6 | -------------------------------------------------------------------------------- /tests/scopes.1.aml: -------------------------------------------------------------------------------- 1 | test: {scope} creates an empty object at scope 2 | result: {"scope": {}} 3 | 4 | {scope} 5 | -------------------------------------------------------------------------------- /tests/arrays.2.aml: -------------------------------------------------------------------------------- 1 | test: ignores spaces on either side of [array] 2 | result: {"array": []} 3 | 4 | [array] 5 | -------------------------------------------------------------------------------- /tests/arrays.3.aml: -------------------------------------------------------------------------------- 1 | test: ignores tabs on either side of [array] 2 | result: {"array": []} 3 | 4 | [array] 5 | -------------------------------------------------------------------------------- /tests/ignore.5.aml: -------------------------------------------------------------------------------- 1 | test: ignores tabs on either side of :ignore 2 | result: {} 3 | 4 | :ignore 5 | key:value 6 | -------------------------------------------------------------------------------- /tests/scopes.2.aml: -------------------------------------------------------------------------------- 1 | test: ignores spaces on either side of {scope} 2 | result: {"scope": {}} 3 | 4 | {scope} 5 | -------------------------------------------------------------------------------- /tests/scopes.3.aml: -------------------------------------------------------------------------------- 1 | test: ignores tabs on either side of {scope} 2 | result: {"scope": {}} 3 | 4 | {scope} 5 | -------------------------------------------------------------------------------- /tests/ignore.4.aml: -------------------------------------------------------------------------------- 1 | test: ignores spaces on either side of :ignore 2 | result: {} 3 | 4 | :ignore 5 | key:value 6 | -------------------------------------------------------------------------------- /tests/skip.5.aml: -------------------------------------------------------------------------------- 1 | test: :skip and :endskip are case insensitive 2 | result: {} 3 | 4 | :sKiP 5 | key:value 6 | :eNdSkIp 7 | -------------------------------------------------------------------------------- /tests/values.2.aml: -------------------------------------------------------------------------------- 1 | test: ignores spaces on either side of the key 2 | result: {"key": "value"} 3 | 4 | key :value 5 | -------------------------------------------------------------------------------- /tests/values.3.aml: -------------------------------------------------------------------------------- 1 | test: ignores tabs on either side of the key 2 | result: {"key": "value"} 3 | 4 | key :value 5 | -------------------------------------------------------------------------------- /tests/values.4.aml: -------------------------------------------------------------------------------- 1 | test: ignores spaces on either side of the value 2 | result: {"key": "value"} 3 | 4 | key: value 5 | -------------------------------------------------------------------------------- /tests/values.5.aml: -------------------------------------------------------------------------------- 1 | test: ignores tabs on either side of the value 2 | result: {"key": "value"} 3 | 4 | key: value 5 | -------------------------------------------------------------------------------- /tests/arrays.5.aml: -------------------------------------------------------------------------------- 1 | test: ignores tabs on either side of [array] variable name 2 | result: {"array": []} 3 | 4 | [ array ] 5 | -------------------------------------------------------------------------------- /tests/arrays.7.aml: -------------------------------------------------------------------------------- 1 | test: arrays can be nested using dot-notaion 2 | result: {"scope": {"array": []}} 3 | 4 | [scope.array] 5 | -------------------------------------------------------------------------------- /tests/ignore.1.aml: -------------------------------------------------------------------------------- 1 | test: text before :ignore should be included 2 | result: {"key": "value"} 3 | 4 | key:value 5 | :ignore 6 | -------------------------------------------------------------------------------- /tests/scopes.5.aml: -------------------------------------------------------------------------------- 1 | test: ignores tabs on either side of {scope} variable name 2 | result: {"scope": {}} 3 | 4 | { scope } 5 | -------------------------------------------------------------------------------- /tests/skip.0.aml: -------------------------------------------------------------------------------- 1 | test: ignores spaces on either side of :skip 2 | result: {} 3 | 4 | :skip 5 | key:value 6 | :endskip 7 | -------------------------------------------------------------------------------- /tests/skip.1.aml: -------------------------------------------------------------------------------- 1 | test: ignores tabs on either side of :skip 2 | result: {} 3 | 4 | :skip 5 | key:value 6 | :endskip 7 | -------------------------------------------------------------------------------- /tests/skip.12.aml: -------------------------------------------------------------------------------- 1 | test: does not parse :end as an :endskip 2 | result: {} 3 | 4 | :skip 5 | :end the above 6 | key:value 7 | -------------------------------------------------------------------------------- /tests/skip.3.aml: -------------------------------------------------------------------------------- 1 | test: ignores tabs on either side of :endskip 2 | result: {} 3 | 4 | :skip 5 | key:value 6 | :endskip 7 | -------------------------------------------------------------------------------- /tests/arrays.11.aml: -------------------------------------------------------------------------------- 1 | test: ignore tabs inside [] 2 | result: {"array": [], "key": "value"} 3 | 4 | [array] 5 | [ ] 6 | key:value 7 | -------------------------------------------------------------------------------- /tests/arrays.4.aml: -------------------------------------------------------------------------------- 1 | test: ignores spaces on either side of [array] variable name 2 | result: {"array": []} 3 | 4 | [ array ] 5 | -------------------------------------------------------------------------------- /tests/arrays_simple.3.aml: -------------------------------------------------------------------------------- 1 | test: ignores tabs on either side of * 2 | result: {"array": ["Value"]} 3 | 4 | [array] 5 | * Value 6 | -------------------------------------------------------------------------------- /tests/ignore.8.aml: -------------------------------------------------------------------------------- 1 | test: ignores all content on line after :ignore + tab 2 | result: {} 3 | 4 | :ignore the below 5 | key:value 6 | -------------------------------------------------------------------------------- /tests/keys.4.aml: -------------------------------------------------------------------------------- 1 | test: keys can be nested using dot-notation 2 | result: {"scope": {"key": "value"}} 3 | 4 | scope.key:value 5 | -------------------------------------------------------------------------------- /tests/multi_line.2.aml: -------------------------------------------------------------------------------- 1 | test: :end is case insensitive 2 | result: {"key": "value\nextra"} 3 | 4 | key:value 5 | extra 6 | :EnD 7 | -------------------------------------------------------------------------------- /tests/scopes.14.aml: -------------------------------------------------------------------------------- 1 | test: ignore tabs inside {} 2 | result: {"scope": {}, "key": "value"} 3 | 4 | {scope} 5 | { } 6 | key:value 7 | -------------------------------------------------------------------------------- /tests/scopes.4.aml: -------------------------------------------------------------------------------- 1 | test: ignores spaces on either side of {scope} variable name 2 | result: {"scope": {}} 3 | 4 | { scope } 5 | -------------------------------------------------------------------------------- /tests/skip.2.aml: -------------------------------------------------------------------------------- 1 | test: ignores spaces on either side of :endskip 2 | result: {} 3 | 4 | :skip 5 | key:value 6 | :endskip 7 | -------------------------------------------------------------------------------- /tests/skip.4.aml: -------------------------------------------------------------------------------- 1 | test: starts parsing again after :endskip 2 | result: {"key": "value"} 3 | 4 | :skip 5 | :endskip 6 | key:value 7 | -------------------------------------------------------------------------------- /tests/values.8.aml: -------------------------------------------------------------------------------- 1 | test: keys are case-sensitive 2 | result: {"key": "value", "Key": "Value"} 3 | 4 | key: value 5 | Key: Value 6 | -------------------------------------------------------------------------------- /tests/arrays.10.aml: -------------------------------------------------------------------------------- 1 | test: ignore spaces inside [] 2 | result: {"array": [], "key": "value"} 3 | 4 | [array] 5 | [ ] 6 | key:value 7 | -------------------------------------------------------------------------------- /tests/arrays.9.aml: -------------------------------------------------------------------------------- 1 | test: [] resets to the global scope 2 | result: {"array": [], "key": "value"} 3 | 4 | [array] 5 | [] 6 | key:value 7 | -------------------------------------------------------------------------------- /tests/arrays_simple.2.aml: -------------------------------------------------------------------------------- 1 | test: ignores spaces on either side of * 2 | result: {"array": ["Value"]} 3 | 4 | [array] 5 | * Value 6 | -------------------------------------------------------------------------------- /tests/ignore.7.aml: -------------------------------------------------------------------------------- 1 | test: ignores all content on line after :ignore + space 2 | result: {} 3 | 4 | :ignore the below 5 | key:value 6 | -------------------------------------------------------------------------------- /tests/scopes.12.aml: -------------------------------------------------------------------------------- 1 | test: {} closes the current scope 2 | result: {"scope": {}, "key": "value"} 3 | 4 | {scope} 5 | {} 6 | key:value 7 | -------------------------------------------------------------------------------- /tests/scopes.13.aml: -------------------------------------------------------------------------------- 1 | test: ignore spaces inside {} 2 | result: {"scope": {}, "key": "value"} 3 | 4 | {scope} 5 | { } 6 | key:value 7 | -------------------------------------------------------------------------------- /tests/scopes.8.aml: -------------------------------------------------------------------------------- 1 | test: items after a {scope} are namespaced 2 | result: {"scope": {"key": "value"}} 3 | 4 | {scope} 5 | key:value 6 | -------------------------------------------------------------------------------- /tests/unicode.4.aml: -------------------------------------------------------------------------------- 1 | test: Arbitrary Unicode can be used along with dot-notation 2 | result: {"🐶": {"🐮": "cow"}} 3 | 4 | 🐶.🐮: cow 5 | -------------------------------------------------------------------------------- /tests/values.10.aml: -------------------------------------------------------------------------------- 1 | test: HTML is allowed in values 2 | result: {"key": "value"} 3 | 4 | key: value 5 | -------------------------------------------------------------------------------- /tests/keys.1.aml: -------------------------------------------------------------------------------- 1 | test: letters, numbers, dashes and underscores are valid key components 2 | result: {"a-_1": "value"} 3 | 4 | a-_1: value 5 | -------------------------------------------------------------------------------- /tests/multi_line.10.aml: -------------------------------------------------------------------------------- 1 | test: does not parse :endskip as an :end 2 | result: {"key": "value"} 3 | 4 | key:value 5 | extra 6 | :endskip 7 | -------------------------------------------------------------------------------- /tests/multi_line.21.aml: -------------------------------------------------------------------------------- 1 | test: allows simple array style lines 2 | result: {"key": "value\n* value"} 3 | 4 | key:value 5 | * value 6 | :end 7 | -------------------------------------------------------------------------------- /tests/scopes.7.aml: -------------------------------------------------------------------------------- 1 | test: items before a {scope} are not namespaced 2 | result: {"key": "value", "scope": {}} 3 | 4 | key:value 5 | {scope} 6 | -------------------------------------------------------------------------------- /tests/skip.7.aml: -------------------------------------------------------------------------------- 1 | test: ignores all content on line after :skip + space 2 | result: {} 3 | 4 | :skip this text 5 | key:value 6 | :endskip 7 | -------------------------------------------------------------------------------- /tests/skip.8.aml: -------------------------------------------------------------------------------- 1 | test: ignores all content on line after :skip + tab 2 | result: {} 3 | 4 | :skip this text 5 | key:value 6 | :endskip 7 | -------------------------------------------------------------------------------- /tests/arrays.12.aml: -------------------------------------------------------------------------------- 1 | test: ignore spaces on either side of [] 2 | result: {"array": [], "key": "value"} 3 | 4 | [array] 5 | [] 6 | key:value 7 | -------------------------------------------------------------------------------- /tests/arrays.13.aml: -------------------------------------------------------------------------------- 1 | test: ignore tabs on either side of [] 2 | result: {"array": [], "key": "value"} 3 | 4 | [array] 5 | [] 6 | key:value 7 | -------------------------------------------------------------------------------- /tests/ignore.6.aml: -------------------------------------------------------------------------------- 1 | test: parses :ignore as a special command even if more is appended to word 2 | result: {} 3 | 4 | :ignorethis 5 | key:value 6 | -------------------------------------------------------------------------------- /tests/multi_line.7.aml: -------------------------------------------------------------------------------- 1 | test: ignores spaces on either side of :end 2 | result: {"key": "value\nextra"} 3 | 4 | key:value 5 | extra 6 | :end 7 | -------------------------------------------------------------------------------- /tests/multi_line.8.aml: -------------------------------------------------------------------------------- 1 | test: ignores tabs on either side of :end 2 | result: {"key": "value\nextra"} 3 | 4 | key:value 5 | extra 6 | :end 7 | -------------------------------------------------------------------------------- /tests/scopes.15.aml: -------------------------------------------------------------------------------- 1 | test: ignore spaces on either side of {} 2 | result: {"scope": {}, "key": "value"} 3 | 4 | {scope} 5 | {} 6 | key:value 7 | -------------------------------------------------------------------------------- /tests/scopes.16.aml: -------------------------------------------------------------------------------- 1 | test: ignore tabs on either side of {} 2 | result: {"scope": {}, "key": "value"} 3 | 4 | {scope} 5 | {} 6 | key:value 7 | -------------------------------------------------------------------------------- /tests/values.6.aml: -------------------------------------------------------------------------------- 1 | test: duplicate keys are assigned to the last given value 2 | result: {"key": "newvalue"} 3 | 4 | key:value 5 | key:newvalue 6 | -------------------------------------------------------------------------------- /tests/arrays_simple.1.aml: -------------------------------------------------------------------------------- 1 | test: creates a simple array when an * is encountered first 2 | result: {"array": ["Value"]} 3 | 4 | [array] 5 | *Value 6 | -------------------------------------------------------------------------------- /tests/arrays_simple.4.aml: -------------------------------------------------------------------------------- 1 | test: adds multiple elements 2 | result: {"array": ["Value 1", "Value 2"]} 3 | 4 | [array] 5 | * Value 1 6 | * Value 2 7 | -------------------------------------------------------------------------------- /tests/skip.6.aml: -------------------------------------------------------------------------------- 1 | test: parse :skip as a special command even if more is appended to word 2 | result: {} 3 | 4 | :skipthis 5 | key:value 6 | :endskip 7 | -------------------------------------------------------------------------------- /tests/values.9.aml: -------------------------------------------------------------------------------- 1 | test: lines without keys don't affect parsing 2 | result: {"key": "value"} 3 | 4 | other stuff 5 | key: value 6 | other stuff 7 | -------------------------------------------------------------------------------- /tests/arrays_simple.8.aml: -------------------------------------------------------------------------------- 1 | test: multi-line values are allowed 2 | result: {"array": ["Value 1\nextra"]} 3 | 4 | [array] 5 | * Value 1 6 | extra 7 | :end 8 | -------------------------------------------------------------------------------- /tests/multi_line.13.aml: -------------------------------------------------------------------------------- 1 | test: ignores all content on line after :end + tab 2 | result: {"key": "value\nextra"} 3 | 4 | key:value 5 | extra 6 | :end this 7 | -------------------------------------------------------------------------------- /tests/multi_line.18.aml: -------------------------------------------------------------------------------- 1 | test: allows escaping commands at the beginning of lines 2 | result: {"key": "value\n:end"} 3 | 4 | key:value 5 | \:end 6 | :end 7 | -------------------------------------------------------------------------------- /tests/scopes.9.aml: -------------------------------------------------------------------------------- 1 | test: scopes can be nested using dot-notaion 2 | result: {"scope": {"scope": {"key": "value"}}} 3 | 4 | {scope.scope} 5 | key:value 6 | -------------------------------------------------------------------------------- /tests/skip.11.aml: -------------------------------------------------------------------------------- 1 | test: ignores all content on line after :endskip + tab 2 | result: {"key": "value"} 3 | 4 | :skip 5 | :endskip the above 6 | key:value 7 | -------------------------------------------------------------------------------- /tests/arrays.14.aml: -------------------------------------------------------------------------------- 1 | test: arrays can be closed with an empty object {} 2 | result: {"array": [], "topkey": "value"} 3 | 4 | [array] 5 | {} 6 | topkey: value 7 | -------------------------------------------------------------------------------- /tests/multi_line.1.aml: -------------------------------------------------------------------------------- 1 | test: adds additional lines to value if followed by an :end 2 | result: {"key": "value\nextra"} 3 | 4 | key:value 5 | extra 6 | :end 7 | -------------------------------------------------------------------------------- /tests/multi_line.12.aml: -------------------------------------------------------------------------------- 1 | test: ignores all content on line after :end + space 2 | result: {"key": "value\nextra"} 3 | 4 | key:value 5 | extra 6 | :end this 7 | -------------------------------------------------------------------------------- /tests/multi_line.16.aml: -------------------------------------------------------------------------------- 1 | test: does not allow escaping colons in keys 2 | result: {"key": "value\nkey2\\:value"} 3 | 4 | key:value 5 | key2\:value 6 | :end 7 | -------------------------------------------------------------------------------- /tests/multi_line.23.aml: -------------------------------------------------------------------------------- 1 | test: allows escaping {scopes} at the beginning of lines 2 | result: {"key": "value\n{scope}"} 3 | 4 | key:value 5 | \{scope} 6 | :end 7 | -------------------------------------------------------------------------------- /tests/multi_line.4.aml: -------------------------------------------------------------------------------- 1 | test: does not preserve whitespace at the end of the key 2 | result: {"key": "value\nextra"} 3 | 4 | key:value 5 | extra 6 | :end 7 | -------------------------------------------------------------------------------- /tests/skip.10.aml: -------------------------------------------------------------------------------- 1 | test: ignores all content on line after :endskip + space 2 | result: {"key": "value"} 3 | 4 | :skip 5 | :endskip the above 6 | key:value 7 | -------------------------------------------------------------------------------- /tests/arrays_complex.1.aml: -------------------------------------------------------------------------------- 1 | test: keys after an [array] are included as items in the array 2 | result: {"array": [{"key": "value"}]} 3 | 4 | [array] 5 | key:value 6 | -------------------------------------------------------------------------------- /tests/multi_line.6.aml: -------------------------------------------------------------------------------- 1 | test: ignores whitespace and newlines before the :end 2 | result: {"key": "value\nextra"} 3 | 4 | key:value 5 | extra 6 | 7 | 8 | :end 9 | -------------------------------------------------------------------------------- /tests/arrays_complex.15.aml: -------------------------------------------------------------------------------- 1 | test: complex ararys overwrite existing keys 2 | result: {"a": {"b": [{"key": "value"}]}} 3 | 4 | a.b:complex value 5 | [a.b] 6 | key:value 7 | -------------------------------------------------------------------------------- /tests/multi_line.11.aml: -------------------------------------------------------------------------------- 1 | test: ordinary text that starts with a colon is included 2 | result: {"key": "value\n:notacommand"} 3 | 4 | key:value 5 | :notacommand 6 | :end 7 | -------------------------------------------------------------------------------- /tests/multi_line.17.aml: -------------------------------------------------------------------------------- 1 | test: allows escaping key lines with a leading backslash 2 | result: {"key": "value\nkey2:value"} 3 | 4 | key:value 5 | \key2:value 6 | :end 7 | -------------------------------------------------------------------------------- /tests/arrays_simple.21.aml: -------------------------------------------------------------------------------- 1 | test: simple arrays overwrite existing keys 2 | result: {"a": {"b": ["simple value"]}} 3 | 4 | a.b: complex value 5 | [a.b] 6 | * simple value 7 | -------------------------------------------------------------------------------- /tests/freeform.1.aml: -------------------------------------------------------------------------------- 1 | test: Strings are converted to objects with key=text 2 | result: {"freeform": [{"type": "text", "value": "Value"}]} 3 | 4 | [+freeform] 5 | Value 6 | [] 7 | -------------------------------------------------------------------------------- /tests/multi_line.22.aml: -------------------------------------------------------------------------------- 1 | test: escapes * within multi-line values when not in a simple array 2 | result: {"key": "value\n* value"} 3 | 4 | key:value 5 | \* value 6 | :end 7 | -------------------------------------------------------------------------------- /tests/multi_line.9.aml: -------------------------------------------------------------------------------- 1 | test: parses :end as a special command even if more is appended to word 2 | result: {"key": "value\nextra"} 3 | 4 | key:value 5 | extra 6 | :endthis 7 | -------------------------------------------------------------------------------- /tests/scopes.10.aml: -------------------------------------------------------------------------------- 1 | test: scopes can be reopened 2 | result: {"scope": {"key": "value", "other": "value"}} 3 | 4 | {scope} 5 | key:value 6 | {} 7 | {scope} 8 | other:value 9 | -------------------------------------------------------------------------------- /tests/scopes.17.aml: -------------------------------------------------------------------------------- 1 | test: key can later be overwriten to become a namespace 2 | result: {"key": {"subkey": "subvalue"}} 3 | 4 | key: value 5 | {key} 6 | subkey: subvalue 7 | -------------------------------------------------------------------------------- /tests/skip.9.aml: -------------------------------------------------------------------------------- 1 | test: parse :endskip as a special command even if more is appended to word 2 | result: {"key": "value"} 3 | 4 | :skip 5 | :endskiptheabove 6 | key:value 7 | -------------------------------------------------------------------------------- /tests/unicode.1.aml: -------------------------------------------------------------------------------- 1 | test: Arbitrary Unicode is allowed in keys 2 | result: {"π": "3.14159", "你好": "你好世界", "🐶🐮": "dogcow"} 3 | 4 | π: 3.14159 5 | 你好: 你好世界 6 | 🐶🐮: dogcow 7 | -------------------------------------------------------------------------------- /tests/unicode.2.aml: -------------------------------------------------------------------------------- 1 | test: Arbitrary Unicode is allowed in keys for scopes 2 | result: {"π": {"value": "3.14159", "name": "Pi"}} 3 | 4 | {π} 5 | value: 3.14159 6 | name: Pi 7 | -------------------------------------------------------------------------------- /tests/unicode.3.aml: -------------------------------------------------------------------------------- 1 | test: Arbitrary Unicode is allowed in keys for arrays 2 | result: {"π": [{"value": "3.14159", "name": "Pi"}]} 3 | 4 | [π] 5 | value: 3.14159 6 | name: Pi 7 | -------------------------------------------------------------------------------- /tests/arrays_complex.14.aml: -------------------------------------------------------------------------------- 1 | test: complex arrays can be redefined as simple arrays 2 | result: {"array": ["Value"]} 3 | 4 | [array] 5 | key:value 6 | [] 7 | [array] 8 | *Value 9 | -------------------------------------------------------------------------------- /tests/arrays_complex.2.aml: -------------------------------------------------------------------------------- 1 | test: array items can have multiple keys 2 | result: {"array": [{"key": "value", "second": "value"}]} 3 | 4 | [array] 5 | key:value 6 | second:value 7 | -------------------------------------------------------------------------------- /tests/arrays_simple.10.aml: -------------------------------------------------------------------------------- 1 | test: allows escaping of command keys within multi-line values 2 | result: {"array": ["Value1\n:end"]} 3 | 4 | [array] 5 | *Value1 6 | \:end 7 | :end 8 | -------------------------------------------------------------------------------- /tests/arrays_simple.12.aml: -------------------------------------------------------------------------------- 1 | test: allows escaping key lines with a leading backslash 2 | result: {"array": ["Value\nkey:value"]} 3 | 4 | [array] 5 | *Value 6 | \key:value 7 | :end 8 | -------------------------------------------------------------------------------- /tests/arrays_simple.5.aml: -------------------------------------------------------------------------------- 1 | test: ignores all other text between elements 2 | result: {"array": ["Value 1", "Value 2"]} 3 | 4 | [array] 5 | * Value 1 6 | Non-element 7 | * Value 2 8 | -------------------------------------------------------------------------------- /tests/arrays_simple.6.aml: -------------------------------------------------------------------------------- 1 | test: ignores key:value pairs between elements 2 | result: {"array": ["Value 1", "Value 2"]} 3 | 4 | [array] 5 | * Value 1 6 | key:value 7 | * Value 2 8 | -------------------------------------------------------------------------------- /tests/multi_line.19.aml: -------------------------------------------------------------------------------- 1 | test: allows escaping commands with extra text at the beginning of lines 2 | result: {"key": "value\n:endthis"} 3 | 4 | key:value 5 | \:endthis 6 | :end 7 | -------------------------------------------------------------------------------- /tests/multi_line.20.aml: -------------------------------------------------------------------------------- 1 | test: allows escaping of non-commands at the beginning of lines 2 | result: {"key": "value\n:notacommand"} 3 | 4 | key:value 5 | \:notacommand 6 | :end 7 | -------------------------------------------------------------------------------- /tests/multi_line.33.aml: -------------------------------------------------------------------------------- 1 | test: does not escape colons after beginning of lines 2 | result: {"key": "value\nLorem key2\\\\:value"} 3 | 4 | key:value 5 | Lorem key2\\:value 6 | :end 7 | -------------------------------------------------------------------------------- /tests/arrays_simple.7.aml: -------------------------------------------------------------------------------- 1 | test: parses key:values normally after an end-array 2 | result: {"array": ["Value 1"], "key": "value"} 3 | 4 | [array] 5 | * Value 1 6 | [] 7 | key:value 8 | -------------------------------------------------------------------------------- /tests/freeform.2.aml: -------------------------------------------------------------------------------- 1 | test: Key-value pairs are turned into key-value objects 2 | result: {"freeform": [{"type": "name", "value": "value"}]} 3 | 4 | [+freeform] 5 | name: value 6 | [] 7 | -------------------------------------------------------------------------------- /tests/arrays_nested.12.aml: -------------------------------------------------------------------------------- 1 | test: subarrays act as top-level arrays when not already inside an array 2 | result: {"subarray": [{"key": "value"}]} 3 | 4 | [.subarray] 5 | key: value 6 | [] 7 | -------------------------------------------------------------------------------- /tests/arrays_simple.11.aml: -------------------------------------------------------------------------------- 1 | test: does not allow escaping of keys within multi-line values 2 | result: {"array": ["Value1\nkey\\:value"]} 3 | 4 | [array] 5 | *Value1 6 | key\:value 7 | :end 8 | -------------------------------------------------------------------------------- /tests/arrays_simple.20.aml: -------------------------------------------------------------------------------- 1 | test: simple arrays can be replaced with complex arrays 2 | result: {"array": [{"key": "value"}]} 3 | 4 | [array] 5 | *Value 6 | [] 7 | [array] 8 | key:value 9 | -------------------------------------------------------------------------------- /tests/freeform.10.aml: -------------------------------------------------------------------------------- 1 | test: Freeforms. nested in scopes 2 | result: {"scope": {"freeform": [{"type": "text", "value": "Value"}]}} 3 | 4 | {scope} 5 | [.+freeform] 6 | Value 7 | [] 8 | {} 9 | -------------------------------------------------------------------------------- /tests/multi_line.26.aml: -------------------------------------------------------------------------------- 1 | test: arrays within a multi-line value breaks up the value 2 | result: {"key": "value", "array": []} 3 | 4 | key:value 5 | text 6 | [array] 7 | more text 8 | :end 9 | -------------------------------------------------------------------------------- /tests/multi_line.27.aml: -------------------------------------------------------------------------------- 1 | test: objects within a multi-line value breaks up the value 2 | result: {"key": "value", "scope": {}} 3 | 4 | key:value 5 | text 6 | {scope} 7 | more text 8 | :end 9 | -------------------------------------------------------------------------------- /tests/multi_line.3.aml: -------------------------------------------------------------------------------- 1 | test: preserves blank lines and whitespace lines in the middle of content 2 | result: {"key": "value\n\n\t \nextra"} 3 | 4 | key:value 5 | 6 | 7 | extra 8 | :end 9 | -------------------------------------------------------------------------------- /tests/arrays_simple.9.aml: -------------------------------------------------------------------------------- 1 | test: allows escaping of * within multi-line values in simple arrays 2 | result: {"array": ["Value 1\n* extra"]} 3 | 4 | [array] 5 | * Value 1 6 | \* extra 7 | :end 8 | -------------------------------------------------------------------------------- /tests/multi_line.29.aml: -------------------------------------------------------------------------------- 1 | test: skips within a multi-line value break up the value 2 | result: {"key": "value"} 3 | 4 | key:value 5 | text 6 | :skip 7 | :endskip 8 | more text 9 | :end 10 | -------------------------------------------------------------------------------- /tests/arrays_complex.13.aml: -------------------------------------------------------------------------------- 1 | test: arrays that are redefined replace the existing array 2 | result: {"array": [{"key": "value 2"}]} 3 | 4 | [array] 5 | key:value 1 6 | [] 7 | [array] 8 | key:value 2 9 | -------------------------------------------------------------------------------- /tests/arrays_nested.1.aml: -------------------------------------------------------------------------------- 1 | test: array keys beginning with [.dots] create complex subarrays 2 | result: {"array": [{"subarray": [{"key": "value"}]}]} 3 | 4 | [array] 5 | [.subarray] 6 | key: value 7 | -------------------------------------------------------------------------------- /tests/arrays_simple.14.aml: -------------------------------------------------------------------------------- 1 | test: arrays within a multi-line value breaks up the value 2 | result: {"array1": ["value"], "array2": []} 3 | 4 | [array1] 5 | * value 6 | [array2] 7 | more text 8 | :end 9 | -------------------------------------------------------------------------------- /tests/arrays_simple.15.aml: -------------------------------------------------------------------------------- 1 | test: objects within a multi-line value break up the value 2 | result: {"array": ["value"], "scope": {}} 3 | 4 | [array] 5 | * value 6 | {scope} 7 | more text 8 | :end 9 | -------------------------------------------------------------------------------- /tests/keys.5.aml: -------------------------------------------------------------------------------- 1 | test: earlier keys within scopes aren't deleted when using dot-notation 2 | result: {"scope": {"key": "value", "otherkey": "value"}} 3 | 4 | scope.key:value 5 | scope.otherkey:value 6 | -------------------------------------------------------------------------------- /tests/arrays_complex.6.aml: -------------------------------------------------------------------------------- 1 | test: duplicate keys must match on dot-notation scope 2 | result: {"array": [{"key": "value", "scope": {"key": "value"}}]} 3 | 4 | [array] 5 | key:value 6 | scope.key:value 7 | -------------------------------------------------------------------------------- /tests/arrays_nested.9.aml: -------------------------------------------------------------------------------- 1 | test: subarrays can server as the item delimiter key 2 | result: {"array": [{"subarray": []}, {"subarray": []}]} 3 | 4 | [array] 5 | [.subarray] 6 | [] 7 | [.subarray] 8 | [] 9 | -------------------------------------------------------------------------------- /tests/arrays_simple.17.aml: -------------------------------------------------------------------------------- 1 | test: bullets within a multi-line value break up the value 2 | result: {"array": ["value", "value\nmore text"]} 3 | 4 | [array] 5 | * value 6 | * value 7 | more text 8 | :end 9 | -------------------------------------------------------------------------------- /tests/arrays_simple.18.aml: -------------------------------------------------------------------------------- 1 | test: skips within a multi-line value break up the value 2 | result: {"array": ["value"]} 3 | 4 | [array] 5 | * value 6 | :skip 7 | :endskip 8 | more text 9 | :end 10 | -------------------------------------------------------------------------------- /tests/freeform.8.aml: -------------------------------------------------------------------------------- 1 | test: Don't parse whitespace lines 2 | result: {"freeform": [{"type": "text", "value": "one"}, {"type": "text", "value": "two"}]} 3 | 4 | [+freeform] 5 | one 6 | 7 | two 8 | [] 9 | -------------------------------------------------------------------------------- /tests/skip.13.aml: -------------------------------------------------------------------------------- 1 | test: ignores keys within a skip block 2 | result: {"key1": "value1", "key2": "value2"} 3 | 4 | key1:value1 5 | :skip 6 | other:value 7 | 8 | :endskip 9 | 10 | key2:value2 11 | -------------------------------------------------------------------------------- /tests/arrays_complex.9.aml: -------------------------------------------------------------------------------- 1 | test: objects within a multi-line value breaks up the value 2 | result: {"array": [{"key": "value"}], "scope": {}} 3 | 4 | [array] 5 | key:value 6 | {scope} 7 | more text 8 | :end 9 | -------------------------------------------------------------------------------- /tests/arrays_simple.13.aml: -------------------------------------------------------------------------------- 1 | test: does not allow escaping of colons not at the beginning of lines 2 | result: {"array": ["Value 1\nword key\\:value"]} 3 | 4 | [array] 5 | * Value 1 6 | word key\:value 7 | :end 8 | -------------------------------------------------------------------------------- /tests/arrays_simple.19.aml: -------------------------------------------------------------------------------- 1 | test: arrays that are reopened replace the existing array 2 | result: {"array": ["Value 2"]} 3 | 4 | [array] 5 | * Value 1 6 | [] 7 | 8 | [array] 9 | * Value 2 10 | [] 11 | -------------------------------------------------------------------------------- /tests/multi_line.28.aml: -------------------------------------------------------------------------------- 1 | test: bullets within a multi-line value do not break up the value 2 | result: {"key": "value\ntext\n* value\nmore text"} 3 | 4 | key:value 5 | text 6 | * value 7 | more text 8 | :end 9 | -------------------------------------------------------------------------------- /tests/unicode.5.aml: -------------------------------------------------------------------------------- 1 | test: Arbitrary Unicode can be used along with freeform arrays 2 | result: {"🐮": [{"type": "text", "value": "This text belongs to a cow."}]} 3 | 4 | [+🐮] 5 | This text belongs to a cow. 6 | -------------------------------------------------------------------------------- /tests/arrays.8.aml: -------------------------------------------------------------------------------- 1 | test: array values can be nested using dot-notaion 2 | result: {"array": [{"scope": {"key": "value"}}, {"scope": {"key": "value"}}]} 3 | 4 | [array] 5 | scope.key: value 6 | scope.key: value 7 | -------------------------------------------------------------------------------- /tests/arrays_complex.12.aml: -------------------------------------------------------------------------------- 1 | test: skips within a multi-line value break up the value 2 | result: {"array": [{"key": "value"}]} 3 | 4 | [array] 5 | key:value 6 | :skip 7 | :endskip 8 | more text 9 | :end 10 | -------------------------------------------------------------------------------- /tests/arrays_complex.4.aml: -------------------------------------------------------------------------------- 1 | test: when a duplicate key is encountered, a new item in the array is started 2 | result: {"array": [{"key": "first"}, {"key": "second"}]} 3 | 4 | [array] 5 | key:first 6 | key:second 7 | -------------------------------------------------------------------------------- /tests/arrays_complex.8.aml: -------------------------------------------------------------------------------- 1 | test: arrays within a multi-line value breaks up the value 2 | result: {"array1": [{"key": "value"}], "array2": []} 3 | 4 | [array1] 5 | key:value 6 | [array2] 7 | more text 8 | :end 9 | -------------------------------------------------------------------------------- /tests/arrays_nested.2.aml: -------------------------------------------------------------------------------- 1 | test: array keys beginning with [.dots] create simple subarrays 2 | result: {"array": [{"subarray": ["Value 1", "Value 2"]}]} 3 | 4 | [array] 5 | [.subarray] 6 | * Value 1 7 | * Value 2 8 | -------------------------------------------------------------------------------- /tests/freeform.11.aml: -------------------------------------------------------------------------------- 1 | test: Freeforms nested in freeforms 2 | result: {"freeform": [{"type": "freeform", "value": [{"type": "text", "value": "Text"}]}]} 3 | 4 | [+freeform] 5 | [.+freeform] 6 | Text 7 | [] 8 | [] 9 | -------------------------------------------------------------------------------- /tests/freeform.18.aml: -------------------------------------------------------------------------------- 1 | test: dot-notation should be become part of the type value within freeforms 2 | result: {"freeform": [{"type": "scope.key", "value": "value"}]} 3 | 4 | [+freeform] 5 | scope.key: value 6 | [] 7 | -------------------------------------------------------------------------------- /tests/freeform.6.aml: -------------------------------------------------------------------------------- 1 | test: Simple arrays nested in freeforms 2 | result: {"freeform": [{"type": "simple", "value": ["Value 1", "Value 2"]}]} 3 | 4 | [+freeform] 5 | [.simple] 6 | * Value 1 7 | * Value 2 8 | [] 9 | -------------------------------------------------------------------------------- /tests/freeform.9.aml: -------------------------------------------------------------------------------- 1 | test: Ignore whitespace on either side of text 2 | result: {"freeform": [{"type": "text", "value": "one"}, {"type": "text", "value": "two"}]} 3 | 4 | [+freeform] 5 | one 6 | two 7 | [] 8 | -------------------------------------------------------------------------------- /tests/arrays_nested.3.aml: -------------------------------------------------------------------------------- 1 | test: subarrays can contain multiple complex values 2 | result: {"array": [{"subarray": [{"key": "value"}, {"key": "value"}]}]} 3 | 4 | [array] 5 | [.subarray] 6 | key: value 7 | key: value 8 | -------------------------------------------------------------------------------- /tests/arrays_simple.16.aml: -------------------------------------------------------------------------------- 1 | test: key/values within a multi-line value do not break up the value 2 | result: {"array": ["value\nkey: value\nmore text"]} 3 | 4 | [array] 5 | * value 6 | key: value 7 | more text 8 | :end 9 | -------------------------------------------------------------------------------- /tests/freeform.19.aml: -------------------------------------------------------------------------------- 1 | test: Bullet strings are converted to objects with key=text, and bullets are not stripped 2 | result: {"freeform": [{"type": "text", "value": "* value"}]} 3 | 4 | [+freeform] 5 | * value 6 | [] 7 | -------------------------------------------------------------------------------- /tests/arrays_complex.11.aml: -------------------------------------------------------------------------------- 1 | test: bullets within a multi-line value do not break up the value 2 | result: {"array": [{"key": "value\n* value\nmore text"}]} 3 | 4 | [array] 5 | key:value 6 | * value 7 | more text 8 | :end 9 | -------------------------------------------------------------------------------- /tests/scopes.11.aml: -------------------------------------------------------------------------------- 1 | test: scopes do not overwrite existing values 2 | result: {"scope": {"scope": {"key": "value"}, "otherscope": {"key": "value"}}} 3 | 4 | {scope.scope} 5 | key:value 6 | {scope.otherscope} 7 | key:value 8 | -------------------------------------------------------------------------------- /tests/arrays_nested.6.aml: -------------------------------------------------------------------------------- 1 | test: subarrays can exist alongside regular keys 2 | result: {"array": [{"parentkey": "value", "subarray": [{"subkey": "value"}]}]} 3 | 4 | [array] 5 | parentkey: value 6 | [.subarray] 7 | subkey: value 8 | -------------------------------------------------------------------------------- /tests/freeform.16.aml: -------------------------------------------------------------------------------- 1 | test: Scoped simple arrays nested in freeforms 2 | result: {"freeform": [{"type": "array.simple", "value": ["value1", "value2"]}]} 3 | 4 | [+freeform] 5 | [.array.simple] 6 | * value1 7 | * value2 8 | [] 9 | -------------------------------------------------------------------------------- /tests/arrays_complex.10.aml: -------------------------------------------------------------------------------- 1 | test: key/values within a multi-line value break up the value 2 | result: {"array": [{"key": "value", "other": "value\nmore text"}]} 3 | 4 | [array] 5 | key:value 6 | other: value 7 | more text 8 | :end 9 | -------------------------------------------------------------------------------- /tests/freeform.13.aml: -------------------------------------------------------------------------------- 1 | test: Complex arrays nested in freeforms 2 | result: {"freeform": [{"type": "complex", "value": [{"key1": "value1", "key2": "value2"}]}]} 3 | 4 | [+freeform] 5 | [.complex] 6 | key1: value1 7 | key2: value2 8 | [] 9 | -------------------------------------------------------------------------------- /tests/freeform.14.aml: -------------------------------------------------------------------------------- 1 | test: {} does not reset to global scope within freeforms 2 | result: {"freeform": [{"type": "scope", "value": {}}, {"type": "key", "value": "value"}]} 3 | 4 | [+freeform] 5 | {.scope} 6 | {} 7 | key: value 8 | [] 9 | -------------------------------------------------------------------------------- /tests/freeform.15.aml: -------------------------------------------------------------------------------- 1 | test: [] does not reset to global scope within freeforms 2 | result: {"freeform": [{"type": "array", "value": []}, {"type": "key", "value": "value"}]} 3 | 4 | [+freeform] 5 | [.array] 6 | [] 7 | key: value 8 | [] 9 | -------------------------------------------------------------------------------- /tests/freeform.17.aml: -------------------------------------------------------------------------------- 1 | test: scoped objects in freeforms 2 | result: {"freeform": [{"type": "object.scope", "value": {"key1": "value1", "key2": "value2"}}]} 3 | 4 | [+freeform] 5 | {.object.scope} 6 | key1: value1 7 | key2: value2 8 | {} 9 | -------------------------------------------------------------------------------- /tests/arrays_complex.5.aml: -------------------------------------------------------------------------------- 1 | test: when a duplicate key is countered, a new item in the array is started 2 | result: {"array": [{"scope": {"key": "first"}}, {"scope": {"key": "second"}}]} 3 | 4 | [array] 5 | scope.key:first 6 | scope.key:second 7 | -------------------------------------------------------------------------------- /tests/scopes.18.aml: -------------------------------------------------------------------------------- 1 | test: {} resets to the global scope 2 | result: {"scope1": {"key1": "value1"}, "scope2": {"key2": "value2"}, "key": "value"} 3 | 4 | {scope1} 5 | key1: value1 6 | {scope2} 7 | key2: value2 8 | {} 9 | key:value 10 | -------------------------------------------------------------------------------- /tests/arrays_complex.3.aml: -------------------------------------------------------------------------------- 1 | test: when a duplicate key is encountered, a new item in the array is started 2 | result: {"array": [{"key": "value", "second": "value"}, {"key": "value"}]} 3 | 4 | [array] 5 | key:value 6 | second:value 7 | key:value 8 | -------------------------------------------------------------------------------- /tests/arrays_nested.5.aml: -------------------------------------------------------------------------------- 1 | test: subarrays can be closed to return to the parent level 2 | result: {"array": [{"parentkey": "value", "subarray": [{"subkey": "value"}]}]} 3 | 4 | [array] 5 | [.subarray] 6 | subkey: value 7 | [] 8 | parentkey: value 9 | -------------------------------------------------------------------------------- /tests/multi_line.34.aml: -------------------------------------------------------------------------------- 1 | test: bullets within a scoped multi-line value do not break up the value 2 | result: {"scope": {"key": "value\ntext\n* value\nmore text"}} 3 | 4 | {scope} 5 | key:value 6 | text 7 | * value 8 | more text 9 | :end 10 | -------------------------------------------------------------------------------- /tests/arrays_complex.7.aml: -------------------------------------------------------------------------------- 1 | test: duplicate keys must match on dot-notation scope 2 | result: {"array": [{"scope": {"key": "value"}, "key": "value", "otherscope": {"key": "value"}}]} 3 | 4 | [array] 5 | scope.key:value 6 | key:value 7 | otherscope.key:value 8 | -------------------------------------------------------------------------------- /tests/arrays_nested.11.aml: -------------------------------------------------------------------------------- 1 | test: subarrays can contain simple subarrays 2 | result: {"array": [{"subarray": [{"subsubarray": ["Value 1", "Value 2"]}]}]} 3 | 4 | [array] 5 | [.subarray] 6 | [.subsubarray] 7 | * Value 1 8 | * Value 2 9 | [] 10 | [] 11 | -------------------------------------------------------------------------------- /tests/freeform.12.aml: -------------------------------------------------------------------------------- 1 | test: Scoped complex arrays nested in freeforms 2 | result: {"freeform": [{"type": "array.complex", "value": [{"key1": "value1", "key2": "value2"}]}]} 3 | 4 | [+freeform] 5 | [.array.complex] 6 | key1: value1 7 | key2: value2 8 | [] 9 | -------------------------------------------------------------------------------- /tests/arrays_nested.10.aml: -------------------------------------------------------------------------------- 1 | test: subarrays can contain complex subarrays 2 | result: {"array": [{"subarray": [{"subsubarray": [{"key1": "Value 1", "key2": "Value 2"}]}]}]} 3 | 4 | [array] 5 | [.subarray] 6 | [.subsubarray] 7 | key1: Value 1 8 | key2: Value 2 9 | [] 10 | [] 11 | -------------------------------------------------------------------------------- /tests/freeform.7.aml: -------------------------------------------------------------------------------- 1 | test: Freeforms nested in arrays 2 | result: {"array": [{"name": "value", "freeform": [{"type": "name", "value": "value"}, {"type": "text", "value": "Text"}]}]} 3 | 4 | [array] 5 | name: value 6 | [.+freeform] 7 | name: value 8 | Text 9 | [] 10 | [] 11 | -------------------------------------------------------------------------------- /tests/arrays_nested.4.aml: -------------------------------------------------------------------------------- 1 | test: subarrays can contain objects with multiple keys 2 | result: {"array": [{"subarray": [{"key1": "value", "key2": "value"}, {"key1": "value", "key2": "value"}]}]} 3 | 4 | [array] 5 | [.subarray] 6 | key1: value 7 | key2: value 8 | key1: value 9 | key2: value 10 | -------------------------------------------------------------------------------- /tests/arrays_nested.8.aml: -------------------------------------------------------------------------------- 1 | test: subarrays do not affect the parent keeping track of the item delimiter key 2 | result: {"array": [{"key": "value", "subarray": [{"subkey": "value"}]}, {"key": "value"}]} 3 | 4 | [array] 5 | key: value 6 | [.subarray] 7 | subkey: value 8 | [] 9 | key: value 10 | -------------------------------------------------------------------------------- /tests/arrays_nested.7.aml: -------------------------------------------------------------------------------- 1 | test: subarrays do not affect the parent keeping track of the item delimiter key 2 | result: {"array": [{"key": "value", "subarray": [{"subkey": "value"}]}, {"key": "value"}]} 3 | 4 | [array] 5 | key: value 6 | [.subarray] 7 | subkey: value 8 | [] 9 | 10 | key: value 11 | -------------------------------------------------------------------------------- /tests/freeform.3.aml: -------------------------------------------------------------------------------- 1 | test: Text and key-value pairs can be combined 2 | result: {"freeform": [{"type": "text", "value": "Value"}, {"type": "name", "value": "value"}, {"type": "text", "value": "Value"}, {"type": "name", "value": "value"}]} 3 | 4 | [+freeform] 5 | Value 6 | name: value 7 | Value 8 | name: value 9 | [] 10 | -------------------------------------------------------------------------------- /tests/freeform.4.aml: -------------------------------------------------------------------------------- 1 | test: Text and key-value pairs can be combined and sequential 2 | result: {"freeform": [{"type": "text", "value": "Value"}, {"type": "text", "value": "Value"}, {"type": "name", "value": "value"}, {"type": "name", "value": "value"}]} 3 | 4 | [+freeform] 5 | Value 6 | Value 7 | name: value 8 | name: value 9 | [] 10 | -------------------------------------------------------------------------------- /tests/keys.6.aml: -------------------------------------------------------------------------------- 1 | test: values are converted between objects and strings as necessary 2 | result: {"string_to_object": {"scope": {"scope": "value"}}, "object_to_string": {"scope": "value"}} 3 | 4 | string_to_object.scope: value 5 | string_to_object.scope.scope: value 6 | 7 | object_to_string.scope.scope: value 8 | object_to_string.scope: value 9 | -------------------------------------------------------------------------------- /tests/freeform.5.aml: -------------------------------------------------------------------------------- 1 | test: Objects nested in freeforms 2 | result: {"freeform": [{"type": "type", "value": "value"}, {"type": "text", "value": "Text"}, {"type": "image", "value": {"name": "map.jpg", "credit": "Photographer"}}]} 3 | 4 | [+freeform] 5 | type: value 6 | Text 7 | {.image} 8 | name: map.jpg 9 | credit: Photographer 10 | {} 11 | [] 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nprapps/betty", 3 | "version": "1.0.11", 4 | "description": "A more exacting ArchieML parser", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/nprapps/betty.git" 12 | }, 13 | "keywords": [], 14 | "author": "Thomas Wilburn", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/nprapps/betty/issues" 18 | }, 19 | "homepage": "https://github.com/nprapps/betty#readme" 20 | } 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var { tokenize } = require("./tokenizer"); 2 | var Parser = require("./parser"); 3 | var Assembler = require("./assembler"); 4 | 5 | var identity = t => t; 6 | 7 | var defaultOptions = { 8 | verbose: false, 9 | onFieldName: identity, 10 | onValue: identity 11 | }; 12 | 13 | var facade = { 14 | parse: function(text, settings) { 15 | var options = Object.assign({}, defaultOptions, settings); 16 | text = text.replace(/\r/g, ""); 17 | var tokens = tokenize(text); 18 | // console.log(tokens); 19 | var parser = new Parser(options); 20 | var instructions = parser.parse(tokens); 21 | var assembler = new Assembler(options); 22 | var output = assembler.assemble(instructions); 23 | // console.log(output); 24 | return structuredClone(output); 25 | }, 26 | tokenize, 27 | Parser, 28 | Assembler 29 | }; 30 | 31 | module.exports = facade; -------------------------------------------------------------------------------- /test_document.txt: -------------------------------------------------------------------------------- 1 | hello: world 2 | timestamp: 2020-02-10T15:00:00.000Z 3 | {options} 4 | test: true 5 | x: false 6 | Longer:: 7 | 8 | this is a block 9 | 10 | It can contain markup 11 | 12 | [and] 13 | it won't care 14 | 15 | this: isn't a field 16 | 17 | ::Longer 18 | 19 | multiline: 20 | 21 | this is a standard multiline value 22 | 23 | :end 24 | 25 | ignored [ test ] line 26 | another ignored:line value 27 | 28 | {.child} 29 | block: true 30 | 31 | {/options} 32 | 33 | not: in options 34 | 35 | [+.free.form] 36 | this is a test block 37 | 38 | key: value 39 | 40 | {.quote} 41 | text: Correctly parses. 42 | {} 43 | 44 | {quote} 45 | error: This should exit the array. 46 | {} 47 | 48 | [] 49 | 50 | [strings] 51 | * test 52 | * a 53 | * b 54 | * longer string goes here: the sequel 55 | 56 | [list] 57 | 58 | a: 1 59 | b: 2 60 | {.c.x} 61 | d: 1 62 | lengthy:: 63 | 64 | deeply nested multiline 65 | 66 | ::lengthy 67 | {} 68 | 69 | a: 3 70 | 71 | a: 4 72 | 73 | [parent] 74 | [.nested] 75 | * one 76 | * two 77 | [/parent] 78 | 79 | closing:out of list 80 | 81 | {named.sub.inner} 82 | prop: This is a named object 83 | {/sub} 84 | outer: Closing only one level 85 | {} -------------------------------------------------------------------------------- /tokenizer.js: -------------------------------------------------------------------------------- 1 | // by default, Betty recognizes several individual characters as tokens for later parsing steps. 2 | var quick = { 3 | "{": "LEFT_BRACE", 4 | "}": "RIGHT_BRACE", 5 | "[": "LEFT_BRACKET", 6 | "]": "RIGHT_BRACKET", 7 | ":": "COLON", 8 | "*": "STAR", 9 | "\n": "TEXT", 10 | "\\": "BACKSLASH", 11 | "/": "SLASH" 12 | } 13 | 14 | module.exports = { 15 | tokenize(text) { 16 | // tokens is the final value, buffer accumulates text during tokenization 17 | var tokens = []; 18 | var buffer = []; 19 | // step through the text, one character at a time 20 | for (var c of text.trim()) { 21 | // if it matches a known token, push the accumulated buffer followed by the token value 22 | if (c in quick) { 23 | if (buffer.length) { 24 | tokens.push({ type: "TEXT", value: buffer.join("") }); 25 | buffer = []; 26 | } 27 | tokens.push({ type: quick[c], value: c }); 28 | } else { 29 | buffer.push(c) 30 | } 31 | } 32 | // add any trailing accumulated text at the end of the file 33 | if (buffer.length) { 34 | tokens.push({ type: "TEXT", value: buffer.join("") }); 35 | } 36 | return tokens; 37 | } 38 | } -------------------------------------------------------------------------------- /readme.rst: -------------------------------------------------------------------------------- 1 | Betty 2 | ===== 3 | 4 | A more specific dialect of `ArchieML `_. While working with editors and reporters, we often found that the format, while "forgiving," can be brittle (especially when combined with CommonMark content). In particular, multiline keys are prone to breaking (either containing no content, or enthusiastically eating the next object in a list). As a result, Betty makes the following changes: 5 | 6 | * Lists will start a new item when they see any redefined key, not just the first key in an object. 7 | * Multiline fields are now less ambiguous: open them with ``key::`` and close with ``::key``. 8 | * If you've opened multiple levels of object, you can jump back out to a specific level by key: ``{/name}`` will close ``{name}``. Note that slash must be flush with the opening brace in this syntax: ``{ /name }`` will not close an object. 9 | * Similarly, you can exit out of a specific named list with ``[/key]`` instead of needing to close individual levels with repeated ``[]`` lines 10 | * You can provide options for behavior: 11 | 12 | * ``verbose`` - set this to be overwhelmed with logging messages 13 | * ``onFieldName`` - provide a callback that accepts a string key for mutation and returns the transformed version. Useful for lower-casing keys when Google Docs tries to capitalize them. 14 | * ``onValue`` - provide a callback that accepts the value and field name, and returns the actual value to add to the object. Useful for automatically casting dates, booleans, and numbers. 15 | 16 | The module exports a single object with a ``parse()`` method, which accepts the text you want to parse and the options object. 17 | 18 | When adding new features or altering the parser, it's useful to make sure that you haven't broken anything. ``npm test`` will run a check against the files from the original specification repo where applicable, as well as a document containing the syntax extensions defined above. Although Betty is not fully-compliant with the ArchieML spec, it should handle existing content reliably. 19 | 20 | Behind the scenes 21 | ----------------- 22 | 23 | When you call ``parse()``, Betty actually runs through three stages before producing a final JSON object. 24 | 25 | 1. A tokenizer breaks the text into a stream of tagged chunks, consisting of either possible syntax characters (such as ``{``, ``}``, and ``:``) or text. 26 | 2. The parser takes the stream of tokens and turns them into higher level instructions for things like "enter an array," "set a value at ``key.path``," or "buffer this text." 27 | 3. The assembler takes those instructions, pre-processes them (merging buffered strings together), then runs through the final stream of operations to actually assemble the object. 28 | 29 | This is much more complex than the baseline ArchieML module. I personally think it's easier this way to reason about the logic for some of the language's "quirks," such as the inconsistent behavior of ``:end`` or ``\`` as an escape. Your mileage may vary. 30 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var fs = require("fs"); 3 | var { parse } = require("./index"); 4 | 5 | var manual = process.argv[2]; 6 | if (manual) { 7 | runSpecTest(manual + ".aml", true); 8 | process.exit(); 9 | } 10 | 11 | function runSpecTest(file, verbose = false) { 12 | var contents = fs.readFileSync(`tests/${file}`, "utf-8"); 13 | var product = parse(contents, { verbose }); 14 | var { test, result } = product; 15 | delete product.test 16 | delete product.result; 17 | result = JSON.parse(result); 18 | console.log(`\n==== ${file} ====`); 19 | console.log("TEST: ", test); 20 | console.log("EXPECTED: ", JSON.stringify(result)); 21 | console.log("FOUND: ", JSON.stringify(product)); 22 | try { 23 | assert.deepStrictEqual(result, product); 24 | console.log(`RESULT: passed`); 25 | passed++; 26 | } catch (err) { 27 | console.error(err.message) 28 | console.log(`RESULT: failed`); 29 | } 30 | } 31 | 32 | // run all archieML tests 33 | var testFiles = fs.readdirSync("tests"); 34 | var passed = 0; 35 | var possible = testFiles.length + 1; 36 | for (var f of testFiles) { 37 | runSpecTest(f); 38 | } 39 | 40 | // syntax extension tests 41 | console.log(`\n==== Betty extensions ====`) 42 | var text = fs.readFileSync("test_document.txt", "utf-8"); 43 | var parsed = parse(text, { 44 | verbose: true, 45 | onFieldName: t => t.toLowerCase(), 46 | onValue: function(value) { 47 | if (value == "true" || value == "false") { 48 | return value == "true"; 49 | } 50 | if (typeof value == "string" && value.match(/^\d{4}-\d{2}-\d{2}T\d{1,2}:\d{2}:\d{2}.\d+Z$/)) { 51 | return Date.parse(value); 52 | } 53 | var attempt = parseFloat(value); 54 | if (!isNaN(attempt)) return attempt; 55 | return value; 56 | } 57 | }); 58 | //console.log(JSON.stringify(parsed, null, 2)); 59 | try { 60 | assert.deepStrictEqual(parsed, { 61 | hello: "world", 62 | options: { 63 | test: true, 64 | x: false, 65 | longer: `this is a block 66 | 67 | It can contain markup 68 | 69 | [and] 70 | it won't care 71 | 72 | this: isn't a field`, 73 | multiline: "this is a standard multiline value", 74 | child: { 75 | block: true 76 | } 77 | }, 78 | not: "in options", 79 | 80 | free: { form: [ 81 | { type: "text", value: "this is a test block" }, 82 | { type: "key", value: "value" }, 83 | { type: "quote", value: { 84 | text: "Correctly parses." 85 | }} 86 | ] }, 87 | quote: { 88 | error: "This should exit the array." 89 | }, 90 | strings: [ 91 | "test", 92 | "a", 93 | "b", 94 | "longer string goes here: the sequel" 95 | ], 96 | 97 | list: [ 98 | { a: 1, b: 2, c: { x: { d: 1, lengthy: "deeply nested multiline" } } }, 99 | { a: 3 }, 100 | { a: 4 } 101 | ], 102 | parent: [ 103 | { 104 | nested: [ 105 | "one", 106 | "two" 107 | ] 108 | } 109 | ], 110 | named: { 111 | sub: { 112 | inner: { 113 | prop: "This is a named object" 114 | } 115 | }, 116 | outer: "Closing only one level" 117 | }, 118 | closing: "out of list", 119 | timestamp: Date.parse("2020-02-10T15:00:00.000Z") 120 | }); 121 | console.log(`RESULT: passed`) 122 | passed++ 123 | } catch (err) { 124 | throw err; 125 | } 126 | 127 | console.log(`\n==== Final result summary ==== 128 | Passed: ${passed} of ${possible}`); -------------------------------------------------------------------------------- /parser.js: -------------------------------------------------------------------------------- 1 | var identity = c => c; 2 | 3 | // take a single stream of tokens and reorganizes it back into lines 4 | // since this is a generator, you can for...of or spread it 5 | var realign = function*(stream) { 6 | var line = []; 7 | for (var i = 0; i < stream.length; i++) { 8 | var token = stream[i]; 9 | line.push(token); 10 | if (token.value == "\n") { 11 | yield line; 12 | line = []; 13 | } 14 | } 15 | yield line; 16 | }; 17 | 18 | var combine = array => array.map(t => t.value).join(""); 19 | 20 | // removes whitespace tokens from the start of a line 21 | var trimStart = function(tokens) { 22 | var line = tokens.slice(); 23 | while (line.length && line[0].value.trim() == "") line.shift(); 24 | return line; 25 | }; 26 | 27 | class Parser { 28 | constructor(options = {}) { 29 | this.options = options; 30 | this.index = 0; 31 | this.instructions = []; 32 | this.lines = []; 33 | } 34 | 35 | /* 36 | parse() processes a stream of tokens and calls methods based on pattern-matching 37 | The result is a list of instructions that are used to assemble the final data object. 38 | */ 39 | 40 | parse(tokens) { 41 | this.index = 0; 42 | this.instructions = []; 43 | this.lines = [...realign(tokens)]; 44 | while (this.index < this.lines.length) { 45 | 46 | // on ignore, quit parsing 47 | if (this.matchValues(":", /^ignore/i) && this.matchTypes("COLON")) { 48 | return this.instructions; 49 | } 50 | 51 | // skip takes precedence 52 | if (this.matchValues(":", /^skip/i) && this.matchTypes("COLON")) { 53 | this.skipCommand(); 54 | continue; 55 | } 56 | 57 | // type-defined grammar 58 | // specifies a parsing function, followed by its token pattern 59 | var typeMatched = [ 60 | [this.multilineValue, "TEXT", "COLON", "COLON", "ANY"], 61 | [this.singleValue, "TEXT", "COLON", "ANY"], 62 | [this.simpleListValue, "STAR", "TEXT"], 63 | [this.objectOpen, "LEFT_BRACE", "TEXT", "RIGHT_BRACE"], 64 | [this.objectClose, "LEFT_BRACE", "RIGHT_BRACE"], 65 | [this.objectCloseNamed, "LEFT_BRACE", "SLASH", "TEXT", "RIGHT_BRACE"], 66 | [this.arrayOpen, "LEFT_BRACKET", "TEXT", "RIGHT_BRACKET"], 67 | [this.arrayClose, "LEFT_BRACKET", "RIGHT_BRACKET"], 68 | [this.arrayCloseNamed, "LEFT_BRACKET", "SLASH", "TEXT", "RIGHT_BRACKET"], 69 | ]; 70 | 71 | // find a matching pattern and call it 72 | var handled = typeMatched.some(([fn, ...types]) => { 73 | if (this.matchTypes(...types)) { 74 | // parse functions can return true to reject the match 75 | var error = fn.call(this); 76 | return !error && true; 77 | } 78 | }); 79 | if (handled) continue; 80 | 81 | // in case of :end 82 | if (this.matchValues(":", /^end(?!skip)/i) && this.matchTypes("COLON")) { 83 | this.flushBuffer(); 84 | continue; 85 | } 86 | 87 | // accumulate text 88 | this.buffer(); 89 | } 90 | return this.instructions; 91 | } 92 | 93 | log(...args) { 94 | if (!this.options.verbose) return; 95 | console.log( 96 | args 97 | .join(" ") 98 | .replace(/\n/g, "\\n") 99 | .replace(/\t/g, "\\t") 100 | ); 101 | } 102 | 103 | // create and output an instruction 104 | addInstruction(type, key, value) { 105 | this.instructions.push({ type, key, value }); 106 | } 107 | 108 | /* 109 | methods for checking and consuming tokens 110 | */ 111 | 112 | // move to the next line 113 | advance() { 114 | var line = this.lines[this.index]; 115 | this.index++; 116 | return line; 117 | } 118 | 119 | // look ahead by offset lines 120 | peek(offset = 0) { 121 | return this.lines[this.index + offset]; 122 | } 123 | 124 | // match against token types 125 | matchTypes(...types) { 126 | var line = trimStart(this.peek()); 127 | return types.every((t, i) => line[i] && (t == "ANY" || t == line[i].type)); 128 | } 129 | 130 | // match against values 131 | // this is useful for things like multiline values, 132 | // where the end tag varies based on the opening key 133 | matchValues(...values) { 134 | var line = trimStart(this.peek()); 135 | return values.every(function(v, i) { 136 | if (i >= line.length) return false; 137 | var token = line[i].value; 138 | if (v instanceof RegExp) { 139 | return token.match(v); 140 | } else { 141 | return token == v; 142 | } 143 | }); 144 | } 145 | 146 | /* 147 | Methods for parsing sets of tokens 148 | Each method examines the current line, and adds the corresponding 149 | instructions to be used by the assembler in the next step 150 | */ 151 | 152 | // skip: through :endskip 153 | skipCommand() { 154 | this.log(`Encountered skip tag`); 155 | while ( 156 | this.index < this.lines.length && 157 | !this.matchValues(":", /^endskip/i) 158 | ) { 159 | var skipped = this.advance(); 160 | this.log(` > Skipping text: "${combine(skipped)}"`); 161 | } 162 | this.addInstruction("skipped"); 163 | } 164 | 165 | // key: value 166 | singleValue() { 167 | // get key and colon 168 | var [key, _, ...values] = trimStart(this.peek()); 169 | var k = key.value.trim(); 170 | // check for valid keys 171 | if (!k.length || k.match(/[\s\?\/="']/)) { 172 | return true; 173 | } 174 | var value = combine(values); 175 | // assign value 176 | this.addInstruction("value", k, value); 177 | this.advance(); 178 | } 179 | 180 | // key:: multiple lines of value ::key 181 | multilineValue() { 182 | var [key, c1, c2, ...values] = trimStart(this.peek()); 183 | var k = key.value.trim(); 184 | // check for valid keys 185 | if (!k.length || k.match(/[\s\?\/="']/)) { 186 | return true; 187 | } 188 | this.advance(); 189 | var next; 190 | var ender = k.toLowerCase(); 191 | while (next = this.peek()) { 192 | if (this.matchValues(":", ":", new RegExp(ender, "i"))) { 193 | break; 194 | } 195 | values.push(...next); 196 | this.advance(); 197 | } 198 | var value = combine(values); 199 | this.addInstruction("value", k, value); 200 | this.advance(); 201 | } 202 | 203 | // * item 204 | simpleListValue() { 205 | // pass the star 206 | var [star, ...values] = trimStart(this.advance()); 207 | this.addInstruction("simple", star.value, combine(values)); 208 | } 209 | 210 | // {objectKey} 211 | objectOpen() { 212 | // ignore the bracket, get the key name 213 | var [_, key] = trimStart(this.peek()); 214 | var k = key.value.trim(); 215 | // handle { }, which closes an object 216 | if (!k) { 217 | return this.objectClose(); 218 | } 219 | // by default, creates a new object at the root 220 | this.addInstruction("object", k); 221 | this.advance(); 222 | } 223 | 224 | objectClose() { 225 | this.addInstruction("closeObject"); 226 | this.advance(); 227 | } 228 | 229 | objectCloseNamed() { 230 | var [brace, slash, key] = trimStart(this.advance()); 231 | this.addInstruction("closeObject", key.value.trim()); 232 | } 233 | 234 | // [arrayKey] 235 | arrayOpen() { 236 | var [_, key] = trimStart(this.peek()); 237 | var k = key.value.trim(); 238 | // handle [ ], where the array should close despite the whitespace 239 | if (!k) { 240 | return this.arrayClose(); 241 | } 242 | // process flags and type to save time later 243 | // unfortunately, + and . prefixes can be in any order 244 | var { path, flags } = k.match(/(?[\+\.]{0,2})(?[^+.].*)/).groups; 245 | var type = flags.includes("+") ? "freeform" : undefined; 246 | if (flags.includes(".")) { 247 | path = "." + path; 248 | } 249 | this.addInstruction("array", path, type); 250 | this.advance(); 251 | } 252 | 253 | arrayClose() { 254 | this.addInstruction("closeArray"); 255 | this.advance(); 256 | } 257 | 258 | arrayCloseNamed() { 259 | var [bracket, slash, key] = trimStart(this.advance()); 260 | this.addInstruction("closeArray", key.value.trim()); 261 | } 262 | 263 | // text is added to a generic buffer, since its use depends on the previous instructions 264 | buffer() { 265 | var values = this.advance(); 266 | this.addInstruction("buffer", null, combine(values)); 267 | } 268 | 269 | // signals that text has finished and should be merged 270 | flushBuffer() { 271 | this.addInstruction("flush"); 272 | this.advance(); 273 | } 274 | } 275 | 276 | module.exports = Parser; 277 | -------------------------------------------------------------------------------- /assembler.js: -------------------------------------------------------------------------------- 1 | 2 | // [TYPE] is used to set metadata on array types 3 | // this lets us use regular JS arrays, but tag them as "freeform" or whatever 4 | var TYPE = Symbol("TYPE"); 5 | 6 | // [NAME] tags objects with their keys, for easy backtracking of {/name} syntax 7 | var NAME = Symbol("NAME"); 8 | 9 | // [PARENT] lets us navigate back up the tree 10 | var PARENT = Symbol("PARENT"); 11 | 12 | var assignSymbol = (a, symbol, value) => 13 | Object.defineProperty(a, symbol, { 14 | value, 15 | enumerable: false, 16 | configurable: false 17 | }); 18 | 19 | /* 20 | The assembler takes a series of instructions from the parser and uses those to build 21 | an output object. For example, it might use "objectOpen: nested" and "singleValue: key,value" 22 | instructions to output { nested: { key: "value" }} 23 | */ 24 | class Assembler { 25 | constructor(options) { 26 | this.options = options; 27 | this.root = {}; 28 | this.stack = [this.root]; 29 | this.instructions = []; 30 | this.index = 0; 31 | } 32 | 33 | log(...args) { 34 | if (!this.options.verbose) return; 35 | console.log( 36 | args 37 | .map(a => typeof a == "object" ? JSON.stringify(a) : a) 38 | .join(" ") 39 | .replace(/\n/g, "\\n") 40 | .replace(/\t/g, "\\t") 41 | ); 42 | } 43 | 44 | // stack manipulation methods 45 | // the assembler maintains a context stack of references for "where" it is 46 | // in the output object tree - e.g., in a nested object inside an array 47 | get top() { 48 | return this.stack[this.stack.length - 1]; 49 | } 50 | 51 | set top(value) { 52 | this.stack[this.stack.length - 1] = value; 53 | } 54 | 55 | pushContext(scope) { 56 | this.stack.push(scope); 57 | } 58 | 59 | popContext() { 60 | var scope = this.stack.pop(); 61 | if (!this.stack.length) this.stack = [this.root]; 62 | return scope || this.root; 63 | } 64 | 65 | // given a key, sets the place in the object where the key should be placed 66 | getTarget(key) { 67 | if (key[0] == ".") { 68 | return this.top; 69 | } 70 | this.reset(); 71 | return this.root; 72 | } 73 | 74 | // on many new keys (outside of lists), jump back to the object root 75 | reset(scope) { 76 | this.stack = [this.root]; 77 | if (scope) this.stack.push(scope); 78 | } 79 | 80 | // turn deep keypaths ("nested.key.string") into a path array 81 | normalizeKeypath(keypath) { 82 | if (typeof keypath == "string") keypath = keypath.split("."); 83 | keypath = keypath.filter(Boolean); 84 | keypath = keypath.map(this.options.onFieldName); 85 | return keypath; 86 | } 87 | 88 | // given a keypath, traverse to that location in the output object 89 | // returns undefined if any step along the keypath fails to exist 90 | getPath(object, keypath) { 91 | keypath = this.normalizeKeypath(keypath); 92 | var terminal = keypath.pop(); 93 | var branch = object; 94 | for (var k of keypath) { 95 | if (!(k in branch)) { 96 | return undefined; 97 | } 98 | branch = branch[k]; 99 | } 100 | return branch && branch[terminal]; 101 | } 102 | 103 | // given a keypath, set the value at that final location 104 | // creates objects along the way for missing keypath segments 105 | setPath(object, keypath, value) { 106 | keypath = this.normalizeKeypath(keypath); 107 | var terminal = keypath.pop(); 108 | var branch = object; 109 | for (var k of keypath) { 110 | if (!k) continue; 111 | if (!(k in branch) || typeof branch[k] != "object") { 112 | branch[k] = {}; 113 | assignSymbol(branch[k], PARENT, branch); 114 | assignSymbol(branch[k], NAME, k); 115 | } 116 | branch = branch[k]; 117 | } 118 | if (typeof value == "string") value = value.trim(); 119 | branch[terminal] = this.options.onValue(value, terminal); 120 | if (typeof branch[terminal] == "object") { 121 | assignSymbol(branch[terminal], PARENT, branch); 122 | } 123 | return branch; 124 | } 125 | 126 | // following the instructions, assemble the final object 127 | assemble(instructions) { 128 | this.log("Raw instructions stream:"); 129 | instructions.forEach(i => this.log(" ", i)); 130 | 131 | // pre-process to combine sequential buffered values into a single instruction 132 | // this initial pass makes it easier to handle blocks of arbitrary text 133 | var processed = []; 134 | var interrupts = new Set(["skipped"]); 135 | var lastValue = null; 136 | var buffer = []; 137 | var mergeBuffer = () => buffer.join("").replace(/^\\/m, ""); 138 | this.log("Preprocessing..."); 139 | for (var i = 0; i < instructions.length; i++) { 140 | var instruction = instructions[i]; 141 | var { type, key, value } = instruction; 142 | switch (type) { 143 | case "simple": 144 | // simple buffers if any other value has been set 145 | if (!lastValue || lastValue.type == "simple") { 146 | buffer = []; 147 | processed.push(instruction); 148 | lastValue = instruction; 149 | break; 150 | } else { 151 | this.log(" Simple item being ignored"); 152 | value = key + value; 153 | } 154 | 155 | case "buffer": 156 | this.log(" Merging buffer instructions..."); 157 | buffer.push(value); 158 | var next = instructions[i + 1]; 159 | while (next && next.type == "buffer") { 160 | i++; 161 | buffer.push(next.value); 162 | next = instructions[i + 1]; 163 | } 164 | break; 165 | 166 | case "value": 167 | if (lastValue && lastValue.type == "simple") { 168 | // buffer this inside of a simple value 169 | buffer.push(key + ":" + value); 170 | break; 171 | } 172 | this.log(` Value encountered: ${key}`); 173 | lastValue = instruction; 174 | var merged = mergeBuffer(); 175 | if (merged.trim()) { 176 | processed.push({ type: "buffer", value: merged }); 177 | } 178 | processed.push(instruction); 179 | buffer = []; 180 | break; 181 | 182 | case "flush": 183 | if (buffer.length) { 184 | var merged = mergeBuffer(); 185 | if (lastValue && merged.trim()) { 186 | lastValue.value += merged; 187 | } else { 188 | processed.push({ type: "buffer", value: merged }); 189 | } 190 | } 191 | buffer = []; 192 | break; 193 | 194 | case "object": 195 | case "array": 196 | case "closeObject": 197 | case "closeArray": 198 | var merged = mergeBuffer(); 199 | if (merged.trim()) { 200 | processed.push({ type: "buffer", value: merged }); 201 | } 202 | this.log(` Encountered ${type}${key ? ` (${key})` : ""}, clearing buffer`); 203 | buffer = []; 204 | 205 | default: 206 | lastValue = null; 207 | processed.push(instruction); 208 | } 209 | } 210 | 211 | // handle leftover garbage in freeform arrays 212 | var merged = mergeBuffer(); 213 | this.log(`Clearing out dangling buffer items...`); 214 | if (merged.trim()) { 215 | processed.push({ type: "buffer", value: merged }); 216 | } 217 | 218 | this.log("Post-process instructions:"); 219 | processed.forEach(i => this.log(" ", i)); 220 | 221 | // now we actually process the final instruction stream 222 | this.log("Assembling result object...") 223 | for (var instruction of processed) { 224 | var { type, key, value } = instruction; 225 | this.log(` ${[type,key,value].filter(d => d).join("/")}`); 226 | // each instruction has a matching method 227 | this[type](key, value); 228 | } 229 | return this.root; 230 | } 231 | 232 | // methods for adding values to arbitrary targets (objects or arrays) 233 | append(target, key, value) { 234 | if (target instanceof Array) { 235 | return this.addToArray(target, key, value); 236 | } else { 237 | if (typeof value == "string") value = value.trim(); 238 | this.setPath(target, key, value); 239 | } 240 | } 241 | 242 | // returns true if the addition was ignored 243 | addToArray(target, key, value) { 244 | switch (target[TYPE]) { 245 | case "freeform": 246 | if (typeof value == "string") value = value.trim(); 247 | key = key.replace(/^\./, ""); 248 | var obj = { type: key, value }; 249 | assignSymbol(obj, PARENT, target); 250 | target.push(obj); 251 | break; 252 | 253 | case "simple": 254 | // simple arrays can't contain keyed values 255 | break; 256 | 257 | default: 258 | if (!target[TYPE]) assignSymbol(target, TYPE, "standard"); 259 | // add to the last object in the array 260 | var last = target[target.length - 1]; 261 | if (!last || this.getPath(last, key)) { 262 | last = {}; 263 | target.push(last); 264 | assignSymbol(last, PARENT, target); 265 | } 266 | if (typeof value == "string") value = value.trim(); 267 | this.setPath(last, key, value); 268 | } 269 | } 270 | 271 | // methods to handle each instruction 272 | // there are relatively few of these, because after parsing, an ArchieML 273 | // document basically just enters object/arrays and adds properties to them 274 | value(key, value) { 275 | var target = this.top; 276 | value = value.trim(); 277 | 278 | if (target instanceof Array) { 279 | switch (target[TYPE]) { 280 | case "simple": 281 | // key value pairs are ignored 282 | break; 283 | 284 | case "freeform": 285 | // you can't add non-dot composite objects to a freeform array 286 | // so we'll exit the array and re-call this 287 | if (typeof value == "object" && key[0] != ".") { 288 | this.closeArray(); 289 | return this.value(key, value); 290 | } 291 | target.push({ type: key, value }); 292 | break; 293 | 294 | default: 295 | this.append(target, key, value); 296 | } 297 | } else { 298 | this.append(target, key, value); 299 | } 300 | } 301 | 302 | simple(key, value) { 303 | if (this.top instanceof Array) { 304 | if (this.top[TYPE] == "simple" || !this.top[TYPE]) { 305 | assignSymbol(this.top, TYPE, "simple"); 306 | this.top.push(value.trim()); 307 | } 308 | 309 | if (this.top[TYPE] == "freeform") { 310 | var obj = { type: "text", value: (key + value).trim() }; 311 | assignSymbol(obj, PARENT, this.top); 312 | this.top.push(obj); 313 | } 314 | } 315 | } 316 | 317 | object(key) { 318 | var target = this.getTarget(key); 319 | var object = this.getPath(target, key); 320 | var path = this.normalizeKeypath(key); 321 | if (typeof object != "object") { 322 | object = {}; 323 | var name = path.at(-1); 324 | assignSymbol(object, NAME, name); 325 | } 326 | this.append(target, key, object); 327 | this.pushContext(object); 328 | } 329 | 330 | closeObject(key) { 331 | // remove the current top scope 332 | var top = this.popContext(); 333 | // navigate via parent up to the named object and push it onto the stack 334 | if (key) { 335 | while (top != this.root && top[NAME] != key) { 336 | top = top[PARENT]; 337 | } 338 | this.pushContext(top[PARENT]); 339 | } 340 | } 341 | 342 | array(key, type) { 343 | var array = []; 344 | var target = this.getTarget(key); 345 | var path = this.normalizeKeypath(key); 346 | var name = path.at(-1); 347 | var [head] = path; 348 | assignSymbol(array, NAME, name); 349 | if (type) { 350 | assignSymbol(array, TYPE, type); 351 | } 352 | this.append(target, key, array); 353 | this.pushContext(array); 354 | } 355 | 356 | closeArray(key) { 357 | if (key) { 358 | // remove the current top scope 359 | var top = this.popContext(); 360 | // navigate via parent up to the named object and push it onto the stack 361 | if (key) { 362 | while (top != this.root && top[NAME] != key) { 363 | top = top[PARENT]; 364 | } 365 | this.pushContext(top[PARENT]); 366 | } 367 | } else { 368 | while (!(this.top instanceof Array) && this.top != this.root) this.popContext(); 369 | if (this.top instanceof Array) this.popContext(); 370 | } 371 | } 372 | 373 | buffer(key, value) { 374 | var target = this.top; 375 | if (target[TYPE] == "freeform") { 376 | var split = value.split("\n").filter(s => s.trim()); 377 | split.forEach(v => target.push({ type: "text", value: v.trim() })); 378 | } 379 | } 380 | 381 | // no-op instructions 382 | // flush and skip are technically handled during pre-processing 383 | flush() {} 384 | skipped() {} 385 | } 386 | 387 | module.exports = Assembler; 388 | --------------------------------------------------------------------------------