├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── archieml ├── __init__.py ├── archieml.py ├── cli.py └── parser.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py └── test_archieml.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | 4 | parser.out 5 | parsetab.py 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | install: "pip install -r requirements.txt" 6 | script: nosetests 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 Kevin Schaul and contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # archieml-python 2 | 3 | [![Build Status](https://travis-ci.org/kevinschaul/archieml-python.svg?branch=master)](https://travis-ci.org/kevinschaul/archieml-python) 4 | 5 | Parse Archie Markup Language (ArchieML) documents into Python dicts. 6 | 7 | Read about the ArchieML specification at [archieml.org](http://archieml.org). 8 | 9 | A work in progress. Run the test for details on how for along this 10 | implementation is. 11 | 12 | Pull requests always welcome. 13 | 14 | ## Usage 15 | 16 | ### Tests 17 | 18 | To run all tests: 19 | 20 | nosetests 21 | 22 | To run a specific test: 23 | 24 | nosetests tests/test_archieml.py:TestParser.test_dotNotation 25 | 26 | -------------------------------------------------------------------------------- /archieml/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinschaul/archieml-python/b7b969c2e16cb5402d41e15067987bd767fdc385/archieml/__init__.py -------------------------------------------------------------------------------- /archieml/archieml.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | import os 5 | import sys 6 | 7 | import cli 8 | 9 | import parser 10 | 11 | class ArchieML(object): 12 | """ 13 | An ArchieML parser 14 | """ 15 | 16 | def __init__(self, args=None): 17 | """ 18 | Get the options from cli. 19 | """ 20 | self.cli = cli.CLI() 21 | self.args = self.cli.parse_arguments(args) 22 | self.parser = parser.Parser(debug=self.args.debug) 23 | 24 | def main(self): 25 | """ 26 | TODO 27 | """ 28 | archieml = """ 29 | key: value 30 | More value 31 | :end 32 | """ 33 | tokens = self.parser.tokenize(archieml) 34 | if (self.args.debug): 35 | print('') 36 | for t in tokens: 37 | print(t) 38 | print('') 39 | parsed = self.parser.parse(archieml) 40 | print(json.dumps(parsed)) 41 | 42 | def launch_new_instance(): 43 | """ 44 | Launch an instance of the ArchieML parser. 45 | 46 | This is the entry function of the command-line tool `archieml`. 47 | """ 48 | archieml = ArchieML() 49 | archieml.main() 50 | 51 | if __name__ == '__main__': 52 | launch_new_instance() 53 | -------------------------------------------------------------------------------- /archieml/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | 5 | class CLI(object): 6 | """ 7 | Handles command-line interface options 8 | """ 9 | 10 | def parse_arguments(self, args=None): 11 | """ 12 | Implement command-line arguments 13 | """ 14 | self.parser = argparse.ArgumentParser() 15 | #self.parser.add_argument('infile', help='The ArchieML file to parse.') 16 | self.parser.add_argument('--debug', action='store_true', \ 17 | dest='debug', help='Print debug output from lex/yacc') 18 | return self.parser.parse_args(args) 19 | -------------------------------------------------------------------------------- /archieml/parser.py: -------------------------------------------------------------------------------- 1 | import ply.lex as lex 2 | import ply.yacc as yacc 3 | 4 | class Parser(object): 5 | """ 6 | TODO 7 | """ 8 | states = ( 9 | ('value', 'inclusive'), 10 | ) 11 | 12 | tokens = ( 13 | 'NEWLINE', 14 | 'COLON', 15 | 'ASTERISK', 16 | 'PERIOD', 17 | 'BACKSLASH', 18 | 'OPEN_CBRACKET', 19 | 'CLOSE_CBRACKET', 20 | 'OPEN_SBRACKET', 21 | 'CLOSE_SBRACKET', 22 | 'OPEN_SKIP', 23 | 'CLOSE_SKIP', 24 | 'OPEN_IGNORE', 25 | 'IDENTIFIER', 26 | 'TEXT', 27 | 'MULTILINE_TEXT', 28 | ) 29 | 30 | precedence = () 31 | 32 | t_COLON = r':' 33 | t_BACKSLASH = r'\\' 34 | t_OPEN_SKIP = r':skip' 35 | t_CLOSE_SKIP = r':endskip' 36 | t_OPEN_IGNORE = r':ignore' 37 | 38 | t_ignore = ' \t' 39 | 40 | def __init__(self, debug=False): 41 | self.debug = debug 42 | 43 | # Top-level key storage 44 | self.keys = {} 45 | 46 | # Store the current object (if any) here, e.g. when entering a block 47 | # defined by {key}. 48 | self.current_object = False 49 | 50 | lex.lex(module=self, debug=debug) 51 | yacc.yacc(module=self, debug=debug) 52 | 53 | def t_NEWLINE(self, t): 54 | r'[\n]+' 55 | pass 56 | 57 | def t_OPEN_CBRACKET(self, t): 58 | r'\{' 59 | return t 60 | 61 | def t_CLOSE_CBRACKET(self, t): 62 | r'\}' 63 | t.lexer.begin('INITIAL') 64 | return t 65 | 66 | def t_OPEN_SBRACKET(self, t): 67 | r'\[' 68 | return t 69 | 70 | def t_CLOSE_SBRACKET(self, t): 71 | r'\]' 72 | t.lexer.begin('INITIAL') 73 | return t 74 | 75 | def t_ASTERISK(self, t): 76 | r'\*' 77 | t.lexer.begin('value') 78 | return t 79 | 80 | def t_IDENTIFIER(self, t): 81 | r'([a-zA-Z0-9-_]+)' 82 | t.lexer.begin('value') 83 | # Strip whitespace surrounding IDENTIFIER 84 | t.value = t.value.strip() 85 | return t 86 | 87 | def t_PERIOD(self, t): 88 | r'\.' 89 | t.lexer.begin('INITIAL') 90 | return t 91 | 92 | def t_value_MULTILINE_TEXT(self, t): 93 | # TODO Why do we have to ignore tokens that are already defined? 94 | r'(?s)[^:\*\.\\{}\[\]]+?:end' 95 | t.lexer.begin('INITIAL') 96 | # Strip off trailing ":end" and whitespace 97 | t.value = t.value[:-4].strip() 98 | return t 99 | 100 | def t_value_TEXT(self, t): 101 | # TODO Why do we have to ignore tokens that are already defined? 102 | r'[^:\*\.\\{}\[\]\n]+' 103 | t.lexer.begin('INITIAL') 104 | # Strip whitespace surrounding TEXT 105 | t.value = t.value.strip() 106 | return t 107 | 108 | def t_error(self, t): 109 | if self.debug: 110 | print(t) 111 | print('Illegal character \'%s\'' % t.value[0]) 112 | t.lexer.skip(1) 113 | 114 | def p_document(self, p): 115 | """ 116 | document : statement 117 | """ 118 | pass 119 | 120 | def p_statement_multiple(self, p): 121 | """ 122 | statement : statement statement 123 | """ 124 | pass 125 | 126 | def p_statement_assign(self, p): 127 | """ 128 | statement : key COLON value 129 | """ 130 | self.assignValue(p[1], p[3]) 131 | 132 | def p_statement_object(self, p): 133 | """ 134 | statement : object 135 | """ 136 | pass 137 | 138 | def p_statement_array(self, p): 139 | """ 140 | statement : array 141 | """ 142 | pass 143 | 144 | def p_statement_array_assign(self, p): 145 | """ 146 | statement : ASTERISK value 147 | """ 148 | if hasattr(self, 'current_array'): 149 | self.current_array.append(p[2]) 150 | 151 | def p_key_multiple(self, p): 152 | """ 153 | key : key PERIOD key 154 | """ 155 | p[0] = p[1] + p[3] 156 | 157 | def p_key(self, p): 158 | """ 159 | key : IDENTIFIER 160 | """ 161 | p[0] = (p[1],) 162 | 163 | def p_value(self, p): 164 | """ 165 | value : TEXT 166 | | MULTILINE_TEXT 167 | """ 168 | p[0] = p[1] 169 | 170 | def p_object_begin(self, p): 171 | """ 172 | object : OPEN_CBRACKET key CLOSE_CBRACKET 173 | """ 174 | self.current_object = p[2] 175 | 176 | def p_object_end(self, p): 177 | """ 178 | object : OPEN_CBRACKET CLOSE_CBRACKET 179 | """ 180 | self.current_object = False 181 | 182 | def p_array_begin(self, p): 183 | """ 184 | array : OPEN_SBRACKET key CLOSE_SBRACKET 185 | """ 186 | key = p[2][0] 187 | self.current_array = self.keys.get(key, []) 188 | self.keys[key] = self.current_array 189 | 190 | def p_array_end(self, p): 191 | """ 192 | array : OPEN_SBRACKET CLOSE_SBRACKET 193 | """ 194 | self.current_array = False 195 | 196 | def p_error(self, p): 197 | if self.debug: 198 | if p: 199 | print(p) 200 | print('Syntax error at \'%s\'' % p.value) 201 | else: 202 | print('Syntax error at EOF') 203 | 204 | def assignValue(self, _key, _value): 205 | """ 206 | `_key` is a tuple containing the nested structure of the key. 207 | e.g. 208 | colors.red -> ('colors', 'red',) 209 | 210 | Set `root` to the extent of the current object (or to the top-level 211 | store `self.keys` if there isn't one). 212 | 213 | e.g. 214 | If `current_object` == ('colors', 'reds'), then set `root` to 215 | `self.keys.colors.reds`. 216 | """ 217 | root = self.keys 218 | if self.current_object: 219 | for i, key in enumerate(self.current_object): 220 | root = root.get(key, {}) 221 | 222 | # Follow the current key down its structure, and assign its value to 223 | # `root`. 224 | num_keys = len(_key) 225 | stored = root 226 | # If this key is not a dict, reassign it. This happens when redefining 227 | # a value as a new object. 228 | if not isinstance(stored, dict): 229 | stored = {} 230 | for i, key in enumerate(_key): 231 | if i == num_keys - 1: 232 | stored[key] = _value 233 | else: 234 | stored[key] = stored.get(key, {}) 235 | stored = stored[key] 236 | 237 | # Unwind the object if it exists, setting the value to `self.keys` 238 | if (self.current_object): 239 | num_keys = len(self.current_object) 240 | stored = self.keys 241 | for i, key in enumerate(self.current_object): 242 | if i == num_keys - 1: 243 | stored[key] = root 244 | else: 245 | stored[key] = stored.get(key, {}) 246 | stored = stored[key] 247 | 248 | def tokenize(self, s): 249 | tokens = [] 250 | lex.input(s) 251 | while True: 252 | token = lex.token() 253 | if not token: 254 | break 255 | tokens.append(token) 256 | return tokens 257 | 258 | def parse(self, s): 259 | yacc.parse(s) 260 | return self.keys 261 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nose==1.3.6 2 | ply==3.4 3 | wsgiref==0.1.2 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name='archieml', 7 | version='0.0.0', 8 | author='Kevin Schaul', 9 | author_email='kevin.schaul@gmail.com', 10 | url='http://kevin.schaul.io', 11 | description='A parser for ArchieML.', 12 | packages=[ 13 | 'archieml', 14 | ], 15 | entry_points = { 16 | 'console_scripts': [ 17 | 'archieml = archieml.archieml:launch_new_instance', 18 | ], 19 | }, 20 | test_suite = 'tests.test_archieml', 21 | ) 22 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinschaul/archieml-python/b7b969c2e16cb5402d41e15067987bd767fdc385/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_archieml.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from archieml.parser import Parser 4 | 5 | class TestLexer(unittest.TestCase): 6 | """ 7 | Test lexing. This is an intermediate step to actually converting an 8 | ArchieML string to json. 9 | """ 10 | def setUp(self): 11 | self.parser = Parser() 12 | 13 | def test_keyValue(self): 14 | # TODO Figure out a better way to test than a string comparison. 15 | # Not immediately sure how to import `IDENTIFIER`, etc. from parser.py 16 | archieml = 'key: This is a value' 17 | expected = "[LexToken(IDENTIFIER,'key',1,0), LexToken(COLON,':',1,3), LexToken(TEXT,'This is a value',1,5)]" 18 | output = str(self.parser.tokenize(archieml)) 19 | self.assertEqual(expected, output) 20 | 21 | 22 | class TestParser(unittest.TestCase): 23 | """ 24 | Test the full conversion from an ArchieML string to json. 25 | 26 | These test cases are pulled from the interactive ArchieML example page: 27 | http://archieml.org/ 28 | """ 29 | def setUp(self): 30 | self.parser = Parser() 31 | 32 | def test_keyValue(self): 33 | archieml = 'key: This is a value' 34 | expected = { 35 | 'key': 'This is a value' 36 | } 37 | output = self.parser.parse(archieml) 38 | self.assertEqual(expected, output) 39 | 40 | def test_surroundingWhitespace(self): 41 | archieml = """ 42 | 1: value 43 | 2:value 44 | 3 : value 45 | 4: value 46 | 5: value 47 | 48 | a: lowercase a 49 | A: uppercase A 50 | """ 51 | expected = { 52 | "1": "value", 53 | "2": "value", 54 | "3": "value", 55 | "4": "value", 56 | "5": "value", 57 | "a": "lowercase a", 58 | "A": "uppercase A" 59 | } 60 | output = self.parser.parse(archieml) 61 | self.assertEqual(expected, output) 62 | 63 | def test_ignoreNonKeys(self): 64 | archieml = """ 65 | This is a key: 66 | 67 | key: value 68 | 69 | It's a nice key! 70 | """ 71 | expected = { 72 | "key": "value" 73 | } 74 | output = self.parser.parse(archieml) 75 | self.assertEqual(expected, output) 76 | 77 | def test_dotNotation(self): 78 | archieml = """ 79 | colors.red: #f00 80 | colors.green: #0f0 81 | colors.blue: #00f 82 | """ 83 | expected = { 84 | "colors": { 85 | "red": "#f00", 86 | "green": "#0f0", 87 | "blue": "#00f" 88 | } 89 | } 90 | output = self.parser.parse(archieml) 91 | self.assertEqual(expected, output) 92 | 93 | def test_objectBlocks(self): 94 | archieml = """ 95 | {colors} 96 | red: #f00 97 | green: #0f0 98 | blue: #00f 99 | 100 | {numbers} 101 | one: 1 102 | ten: 10 103 | one-hundred: 100 104 | {} 105 | 106 | key: value 107 | """ 108 | expected = { 109 | "colors": { 110 | "red": "#f00", 111 | "green": "#0f0", 112 | "blue": "#00f" 113 | }, 114 | "numbers": { 115 | "one": "1", 116 | "ten": "10", 117 | "one-hundred": "100" 118 | }, 119 | "key": "value" 120 | } 121 | output = self.parser.parse(archieml) 122 | self.assertEqual(expected, output) 123 | 124 | def test_dotNotationObjectNamespaces(self): 125 | archieml = """ 126 | {colors.reds} 127 | crimson: #dc143c 128 | darkred: #8b0000 129 | 130 | {colors.blues} 131 | cornflowerblue: #6495ed 132 | darkblue: #00008b 133 | """ 134 | expected = { 135 | "colors": { 136 | "reds": { 137 | "crimson": "#dc143c", 138 | "darkred": "#8b0000" 139 | }, 140 | "blues": { 141 | "cornflowerblue": "#6495ed", 142 | "darkblue": "#00008b" 143 | } 144 | } 145 | } 146 | output = self.parser.parse(archieml) 147 | self.assertEqual(expected, output) 148 | 149 | def test_array(self): 150 | archieml = """ 151 | [scope.array] 152 | [] 153 | """ 154 | expected = { 155 | "scope": { 156 | "array": [] 157 | } 158 | } 159 | output = self.parser.parse(archieml) 160 | self.assertEqual(expected, output) 161 | 162 | def test_newObjectUponRepeatedFirstKey(self): 163 | archieml = """ 164 | [arrayName] 165 | 166 | Jeremy spoke with her on Friday, follow-up scheduled for next week 167 | name: Amanda 168 | age: 26 169 | 170 | # Contact: 434-555-1234 171 | name: Tessa 172 | age: 30 173 | 174 | [] 175 | """ 176 | expected = { 177 | "arrayName": [ 178 | { 179 | "name": "Amanda", 180 | "age": "26" 181 | }, { 182 | "name": "Tessa", 183 | "age": "30" 184 | } 185 | ] 186 | } 187 | output = self.parser.parse(archieml) 188 | self.assertEqual(expected, output) 189 | 190 | def test_arrayOfStrings(self): 191 | archieml = """ 192 | [days] 193 | * Sunday 194 | note: holiday! 195 | * Monday 196 | * Tuesday 197 | 198 | Whitespace is still fine around the '*' 199 | * Wednesday 200 | 201 | * Thursday 202 | 203 | Friday! 204 | * Friday 205 | * Saturday 206 | [] 207 | """ 208 | expected = { 209 | "days": [ 210 | "Sunday", 211 | "Monday", 212 | "Tuesday", 213 | "Wednesday", 214 | "Thursday", 215 | "Friday", 216 | "Saturday" 217 | ] 218 | } 219 | output = self.parser.parse(archieml) 220 | self.assertEqual(expected, output) 221 | 222 | def test_multiLineValues(self): 223 | archieml = """ 224 | key: value 225 | More value 226 | 227 | Even more value 228 | :end 229 | """ 230 | expected = { 231 | "key": "value\n More value\n\nEven more value" 232 | } 233 | output = self.parser.parse(archieml) 234 | self.assertEqual(expected, output) 235 | 236 | def test_multiLineValuesWithinArraysObjects(self): 237 | archieml = """ 238 | [arrays.complex] 239 | key: value 240 | more value 241 | :end 242 | 243 | [arrays.simple] 244 | * value 245 | more value 246 | :end 247 | """ 248 | expected = { 249 | "arrays": { 250 | "complex": [ 251 | { 252 | "key": "value\nmore value" 253 | } 254 | ], 255 | "simple": [ 256 | "value\nmore value" 257 | ] 258 | } 259 | } 260 | output = self.parser.parse(archieml) 261 | self.assertEqual(expected, output) 262 | 263 | def test_backslashEnd(self): 264 | archieml = """ 265 | key: value 266 | \:end 267 | :end 268 | """ 269 | expected = { 270 | "key": "value\n:end" 271 | } 272 | output = self.parser.parse(archieml) 273 | self.assertEqual(expected, output) 274 | 275 | def test_withoutBackslashEnd(self): 276 | archieml = """ 277 | key: value 278 | :end 279 | :end 280 | """ 281 | expected = { 282 | "key": "value" 283 | } 284 | output = self.parser.parse(archieml) 285 | self.assertEqual(expected, output) 286 | 287 | def test_backslashValueEnd(self): 288 | archieml = """ 289 | key: value 290 | \more: value 291 | :end 292 | """ 293 | expected = { 294 | "key": "value\nmore: value" 295 | } 296 | output = self.parser.parse(archieml) 297 | self.assertEqual(expected, output) 298 | 299 | def test_withoutBackslashValueEnd(self): 300 | archieml = """ 301 | key: value 302 | more: value 303 | :end 304 | """ 305 | expected = { 306 | "key": "value", 307 | "more": "value" 308 | } 309 | output = self.parser.parse(archieml) 310 | self.assertEqual(expected, output) 311 | 312 | def test_backslashArray(self): 313 | archieml = """ 314 | key: value 315 | [escaping *s is not necessary if we're not inside an array, but will still be removed] 316 | \* value 317 | :end 318 | """ 319 | expected = { 320 | "key": "value\n\n* value" 321 | } 322 | output = self.parser.parse(archieml) 323 | self.assertEqual(expected, output) 324 | 325 | def test_backslashArray(self): 326 | archieml = """ 327 | key: value 328 | [escaping *s is not necessary if we're not inside an array, but will still be removed] 329 | \* value 330 | :end 331 | """ 332 | expected = { 333 | "key": "value\n\n* value" 334 | } 335 | output = self.parser.parse(archieml) 336 | self.assertEqual(expected, output) 337 | 338 | def test_withoutBackslashArray(self): 339 | archieml = """ 340 | key: value 341 | [escaping *s is not necessary if we're not inside an array, but will still be removed] 342 | * value 343 | :end 344 | """ 345 | expected = { 346 | "key": "value\n\n* value" 347 | } 348 | output = self.parser.parse(archieml) 349 | self.assertEqual(expected, output) 350 | 351 | def test_backslashKeywords(self): 352 | archieml = """ 353 | key: value 354 | \:ignore 355 | \:skip 356 | \:endskip 357 | :end 358 | """ 359 | expected = { 360 | "key": "value\n:ignore\n:skip\n:endskip" 361 | } 362 | output = self.parser.parse(archieml) 363 | self.assertEqual(expected, output) 364 | 365 | def test_withoutBackslashKeywords(self): 366 | archieml = """ 367 | key: value 368 | \:ignore 369 | \:skip 370 | \:endskip 371 | :end 372 | """ 373 | expected = { 374 | "key": "value" 375 | } 376 | output = self.parser.parse(archieml) 377 | self.assertEqual(expected, output) 378 | 379 | def test_inlineComment(self): 380 | archieml = """ 381 | title: Lorem ipsum [IGNORED] dolor sit amet 382 | """ 383 | expected = { 384 | "title": "Lorem ipsum dolor sit amet" 385 | } 386 | output = self.parser.parse(archieml) 387 | self.assertEqual(expected, output) 388 | 389 | def test_preserveInlineComment(self): 390 | archieml = """ 391 | title: Lorem ipsum [[PRESERVED]] dolor sit amet 392 | """ 393 | expected = { 394 | "title": "Lorem ipsum [PRESERVED] dolor sit amet" 395 | } 396 | output = self.parser.parse(archieml) 397 | self.assertEqual(expected, output) 398 | 399 | def test_blockComment(self): 400 | archieml = """ 401 | :skip 402 | this: text 403 | will: be 404 | ignored 405 | :endskip 406 | """ 407 | expected = {} 408 | output = self.parser.parse(archieml) 409 | self.assertEqual(expected, output) 410 | 411 | def test_ignore(self): 412 | archieml = """ 413 | key: value 414 | :ignore 415 | 416 | [array] 417 | * Blah 418 | [] 419 | 420 | other-key: other value 421 | """ 422 | expected = { 423 | "key": "value" 424 | } 425 | output = self.parser.parse(archieml) 426 | self.assertEqual(expected, output) 427 | 428 | def test_ignoreWithinSkip(self): 429 | archieml = """ 430 | key: value 431 | :skip 432 | :ignore 433 | :endskip 434 | 435 | [array] 436 | * Blah 437 | [] 438 | 439 | other-key: other value 440 | """ 441 | expected = { 442 | "key": "value" 443 | } 444 | output = self.parser.parse(archieml) 445 | self.assertEqual(expected, output) 446 | 447 | --------------------------------------------------------------------------------