diff --git a/__snapshots__/packages/core/test-out/parser/util.spec.js b/__snapshots__/packages/core/test-out/parser/util.spec.js index 66806b622..55a444c2f 100644 --- a/__snapshots__/packages/core/test-out/parser/util.spec.js +++ b/__snapshots__/packages/core/test-out/parser/util.spec.js @@ -93,6 +93,206 @@ exports['any() Parse "qux" with "foo | bar" 1'] = { "errors": [] } +exports['concatOnTrailingBackslash() Parse "true" 1'] = { + "node": { + "type": "boolean", + "range": { + "start": 0, + "end": 4 + }, + "value": true + }, + "errors": [] +} + +exports['concatOnTrailingBackslash() Parse "true↓" 1'] = { + "node": { + "type": "boolean", + "range": { + "start": 0, + "end": 4 + }, + "value": true + }, + "errors": [] +} + +exports['concatOnTrailingBackslash() Parse "tru⧵ ↓ " 1'] = { + "node": { + "type": "boolean", + "range": { + "start": 0, + "end": 0 + } + }, + "errors": [ + { + "range": { + "start": 0, + "end": 0 + }, + "message": "Expected “false” or “true”", + "severity": 3 + } + ] +} + +exports['concatOnTrailingBackslash() Parse "tru⧵ ↓ e" 1'] = { + "node": { + "type": "boolean", + "range": { + "start": 0, + "end": 8 + }, + "value": true + }, + "errors": [] +} + +exports['concatOnTrailingBackslash() Parse "tru⧵ ↓ ⧵↓ e" 1'] = { + "node": { + "type": "boolean", + "range": { + "start": 0, + "end": 11 + }, + "value": true + }, + "errors": [] +} + +exports['concatOnTrailingBackslash() Parse "tru⧵ ↓" 1'] = { + "node": { + "type": "boolean", + "range": { + "start": 0, + "end": 0 + } + }, + "errors": [ + { + "range": { + "start": 3, + "end": 6 + }, + "message": "A line continuation cannot be the end of the file", + "severity": 3 + }, + { + "range": { + "start": 0, + "end": 0 + }, + "message": "Expected “false” or “true”", + "severity": 3 + } + ] +} + +exports['concatOnTrailingBackslash() Parse "tru⧵ ↓e" 1'] = { + "node": { + "type": "boolean", + "range": { + "start": 0, + "end": 7 + }, + "value": true + }, + "errors": [] +} + +exports['concatOnTrailingBackslash() Parse "tru⧵" 1'] = { + "node": { + "type": "boolean", + "range": { + "start": 0, + "end": 0 + } + }, + "errors": [ + { + "range": { + "start": 3, + "end": 4 + }, + "message": "A line continuation cannot be the end of the file", + "severity": 3 + }, + { + "range": { + "start": 0, + "end": 0 + }, + "message": "Expected “false” or “true”", + "severity": 3 + } + ] +} + +exports['concatOnTrailingBackslash() Parse "tru⧵e ⧵ ↓ e" 1'] = { + "node": { + "type": "boolean", + "range": { + "start": 0, + "end": 0 + } + }, + "errors": [ + { + "range": { + "start": 0, + "end": 0 + }, + "message": "Expected “false” or “true”", + "severity": 3 + } + ] +} + +exports['concatOnTrailingBackslash() Parse "tru⧵↓ e" 1'] = { + "node": { + "type": "boolean", + "range": { + "start": 0, + "end": 7 + }, + "value": true + }, + "errors": [] +} + +exports['concatOnTrailingBackslash() Parse "tru⧵↓e" 1'] = { + "node": { + "type": "boolean", + "range": { + "start": 0, + "end": 6 + }, + "value": true + }, + "errors": [] +} + +exports['concatOnTrailingBackslash() Parse "tru⧵↓↓e" 1'] = { + "node": { + "type": "boolean", + "range": { + "start": 0, + "end": 0 + } + }, + "errors": [ + { + "range": { + "start": 0, + "end": 0 + }, + "message": "Expected “false” or “true”", + "severity": 3 + } + ] +} + exports['dumpErrors() should not output errors when wrapped with `dumpErrors()` 1'] = { "node": { "type": "boolean", diff --git a/__snapshots__/packages/mcfunction/test-out/parser/entry.spec.js b/__snapshots__/packages/mcfunction/test-out/parser/entry.spec.js new file mode 100644 index 000000000..38648cb50 --- /dev/null +++ b/__snapshots__/packages/mcfunction/test-out/parser/entry.spec.js @@ -0,0 +1,1401 @@ +exports['mcfunction parser entry() Parse "" 1'] = { + "node": { + "type": "mcfunction:entry", + "range": { + "start": 0, + "end": 0 + }, + "children": [] + }, + "errors": [] +} + +exports['mcfunction parser entry() Parse "# this is a comment ⧵ ↓ still a comment" 1'] = { + "node": { + "type": "mcfunction:entry", + "range": { + "start": 0, + "end": 39 + }, + "children": [ + { + "type": "comment", + "range": { + "start": 0, + "end": 39 + }, + "comment": " this is a comment still a comment" + } + ] + }, + "errors": [] +} + +exports['mcfunction parser entry() Parse "# this is a comment" 1'] = { + "node": { + "type": "mcfunction:entry", + "range": { + "start": 0, + "end": 19 + }, + "children": [ + { + "type": "comment", + "range": { + "start": 0, + "end": 19 + }, + "comment": " this is a comment" + } + ] + }, + "errors": [] +} + +exports['mcfunction parser entry() Parse "# this is a comment↓say hi↓$this is a $(macro) ↓" 1'] = { + "node": { + "type": "mcfunction:entry", + "range": { + "start": 0, + "end": 48 + }, + "children": [ + { + "type": "comment", + "range": { + "start": 0, + "end": 19 + }, + "comment": " this is a comment" + }, + { + "type": "mcfunction:command", + "range": { + "start": 20, + "end": 26 + }, + "children": [ + { + "type": "mcfunction:command_child", + "range": { + "start": 20, + "end": 23 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 20, + "end": 23 + }, + "value": "say" + } + ], + "path": [ + "say" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 24, + "end": 26 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 24, + "end": 26 + }, + "value": "hi" + } + ], + "path": [ + "say", + "hi" + ] + } + ] + }, + { + "type": "mcfunction:macro", + "range": { + "start": 27, + "end": 47 + }, + "children": [ + { + "type": "mcfunction:macro/other", + "range": { + "start": 28, + "end": 38 + }, + "value": "this is a " + }, + { + "type": "mcfunction:macro/argument", + "range": { + "start": 38, + "end": 46 + }, + "value": "macro" + }, + { + "type": "mcfunction:macro/other", + "range": { + "start": 46, + "end": 47 + }, + "value": " " + } + ] + } + ] + }, + "errors": [] +} + +exports['mcfunction parser entry() Parse "$ this is a $(macro) command" 1'] = { + "node": { + "type": "mcfunction:entry", + "range": { + "start": 0, + "end": 28 + }, + "children": [ + { + "type": "mcfunction:macro", + "range": { + "start": 0, + "end": 28 + }, + "children": [ + { + "type": "mcfunction:macro/other", + "range": { + "start": 1, + "end": 12 + }, + "value": " this is a " + }, + { + "type": "mcfunction:macro/argument", + "range": { + "start": 12, + "end": 20 + }, + "value": "macro" + }, + { + "type": "mcfunction:macro/other", + "range": { + "start": 20, + "end": 28 + }, + "value": " command" + } + ] + } + ] + }, + "errors": [] +} + +exports['mcfunction parser entry() Parse "$ this is a $(macro) ⧵ ↓ this is $(still) a macro" 1'] = { + "node": { + "type": "mcfunction:entry", + "range": { + "start": 0, + "end": 49 + }, + "children": [ + { + "type": "mcfunction:macro", + "range": { + "start": 0, + "end": 49 + }, + "children": [ + { + "type": "mcfunction:macro/other", + "range": { + "start": 1, + "end": 12 + }, + "value": " this is a " + }, + { + "type": "mcfunction:macro/argument", + "range": { + "start": 12, + "end": 20 + }, + "value": "macro" + }, + { + "type": "mcfunction:macro/other", + "range": { + "start": 20, + "end": 33 + }, + "value": " this is " + }, + { + "type": "mcfunction:macro/argument", + "range": { + "start": 33, + "end": 41 + }, + "value": "still" + }, + { + "type": "mcfunction:macro/other", + "range": { + "start": 41, + "end": 49 + }, + "value": " a macro" + } + ] + } + ] + }, + "errors": [] +} + +exports['mcfunction parser entry() Parse "execute if true if true run say hi" 1'] = { + "node": { + "type": "mcfunction:entry", + "range": { + "start": 0, + "end": 34 + }, + "children": [ + { + "type": "mcfunction:command", + "range": { + "start": 0, + "end": 34 + }, + "children": [ + { + "type": "mcfunction:command_child", + "range": { + "start": 0, + "end": 7 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 0, + "end": 7 + }, + "value": "execute" + } + ], + "path": [ + "execute" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 8, + "end": 10 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 8, + "end": 10 + }, + "value": "if" + } + ], + "path": [ + "execute", + "if" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 11, + "end": 15 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 11, + "end": 15 + }, + "value": "true" + } + ], + "path": [ + "execute", + "if", + "true" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 16, + "end": 18 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 16, + "end": 18 + }, + "value": "if" + } + ], + "path": [ + "execute", + "if" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 19, + "end": 23 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 19, + "end": 23 + }, + "value": "true" + } + ], + "path": [ + "execute", + "if", + "true" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 24, + "end": 27 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 24, + "end": 27 + }, + "value": "run" + } + ], + "path": [ + "execute", + "run" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 28, + "end": 31 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 28, + "end": 31 + }, + "value": "say" + } + ], + "path": [ + "say" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 32, + "end": 34 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 32, + "end": 34 + }, + "value": "hi" + } + ], + "path": [ + "say", + "hi" + ] + } + ] + } + ] + }, + "errors": [] +} + +exports['mcfunction parser entry() Parse "say hi" 1'] = { + "node": { + "type": "mcfunction:entry", + "range": { + "start": 0, + "end": 6 + }, + "children": [ + { + "type": "mcfunction:command", + "range": { + "start": 0, + "end": 6 + }, + "children": [ + { + "type": "mcfunction:command_child", + "range": { + "start": 0, + "end": 3 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 0, + "end": 3 + }, + "value": "say" + } + ], + "path": [ + "say" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 4, + "end": 6 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 4, + "end": 6 + }, + "value": "hi" + } + ], + "path": [ + "say", + "hi" + ] + } + ] + } + ] + }, + "errors": [] +} + +exports['mcfunction parser entry() Parse "say hi" without backslash continuation 1'] = { + "node": { + "type": "mcfunction:entry", + "range": { + "start": 0, + "end": 6 + }, + "children": [ + { + "type": "mcfunction:command", + "range": { + "start": 0, + "end": 6 + }, + "children": [ + { + "type": "mcfunction:command_child", + "range": { + "start": 0, + "end": 3 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 0, + "end": 3 + }, + "value": "say" + } + ], + "path": [ + "say" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 4, + "end": 6 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 4, + "end": 6 + }, + "value": "hi" + } + ], + "path": [ + "say", + "hi" + ] + } + ] + } + ] + }, + "errors": [] +} + +exports['mcfunction parser entry() Parse "say hi↓say hi" 1'] = { + "node": { + "type": "mcfunction:entry", + "range": { + "start": 0, + "end": 13 + }, + "children": [ + { + "type": "mcfunction:command", + "range": { + "start": 0, + "end": 6 + }, + "children": [ + { + "type": "mcfunction:command_child", + "range": { + "start": 0, + "end": 3 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 0, + "end": 3 + }, + "value": "say" + } + ], + "path": [ + "say" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 4, + "end": 6 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 4, + "end": 6 + }, + "value": "hi" + } + ], + "path": [ + "say", + "hi" + ] + } + ] + }, + { + "type": "mcfunction:command", + "range": { + "start": 7, + "end": 13 + }, + "children": [ + { + "type": "mcfunction:command_child", + "range": { + "start": 7, + "end": 10 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 7, + "end": 10 + }, + "value": "say" + } + ], + "path": [ + "say" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 11, + "end": 13 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 11, + "end": 13 + }, + "value": "hi" + } + ], + "path": [ + "say", + "hi" + ] + } + ] + } + ] + }, + "errors": [] +} + +exports['mcfunction parser entry() Parse "say hi↓say hi" without backslash continuation 1'] = { + "node": { + "type": "mcfunction:entry", + "range": { + "start": 0, + "end": 13 + }, + "children": [ + { + "type": "mcfunction:command", + "range": { + "start": 0, + "end": 6 + }, + "children": [ + { + "type": "mcfunction:command_child", + "range": { + "start": 0, + "end": 3 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 0, + "end": 3 + }, + "value": "say" + } + ], + "path": [ + "say" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 4, + "end": 6 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 4, + "end": 6 + }, + "value": "hi" + } + ], + "path": [ + "say", + "hi" + ] + } + ] + }, + { + "type": "mcfunction:command", + "range": { + "start": 7, + "end": 13 + }, + "children": [ + { + "type": "mcfunction:command_child", + "range": { + "start": 7, + "end": 10 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 7, + "end": 10 + }, + "value": "say" + } + ], + "path": [ + "say" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 11, + "end": 13 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 11, + "end": 13 + }, + "value": "hi" + } + ], + "path": [ + "say", + "hi" + ] + } + ] + } + ] + }, + "errors": [] +} + +exports['mcfunction parser entry() Parse "say trailing ⧵↓ data" 1'] = { + "node": { + "type": "mcfunction:entry", + "range": { + "start": 0, + "end": 20 + }, + "children": [ + { + "type": "mcfunction:command", + "range": { + "start": 0, + "end": 20 + }, + "children": [ + { + "type": "mcfunction:command_child", + "range": { + "start": 0, + "end": 3 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 0, + "end": 3 + }, + "value": "say" + } + ], + "path": [ + "say" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 4, + "end": 12 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 4, + "end": 12 + }, + "value": "trailing" + } + ], + "path": [ + "say", + "trailing" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 12, + "end": 20 + }, + "children": [ + { + "type": "mcfunction:command_child/trailing", + "range": { + "start": 12, + "end": 20 + }, + "value": " data" + } + ], + "path": [] + } + ] + } + ] + }, + "errors": [ + { + "range": { + "start": 4, + "end": 12 + }, + "message": "Expected “hi”", + "severity": 3 + }, + { + "range": { + "start": 12, + "end": 20 + }, + "message": "Trailing data encountered: “ data”", + "severity": 3 + } + ] +} + +exports['mcfunction parser entry() Parse "say trailing ⧵↓ data" without backslash continuation 1'] = { + "node": { + "type": "mcfunction:entry", + "range": { + "start": 0, + "end": 20 + }, + "children": [ + { + "type": "mcfunction:command", + "range": { + "start": 0, + "end": 14 + }, + "children": [ + { + "type": "mcfunction:command_child", + "range": { + "start": 0, + "end": 3 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 0, + "end": 3 + }, + "value": "say" + } + ], + "path": [ + "say" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 4, + "end": 12 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 4, + "end": 12 + }, + "value": "trailing" + } + ], + "path": [ + "say", + "trailing" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 12, + "end": 14 + }, + "children": [ + { + "type": "mcfunction:command_child/trailing", + "range": { + "start": 12, + "end": 14 + }, + "value": " \\" + } + ], + "path": [] + } + ] + }, + { + "type": "mcfunction:command", + "range": { + "start": 16, + "end": 20 + }, + "children": [ + { + "type": "mcfunction:command_child", + "range": { + "start": 16, + "end": 20 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 16, + "end": 20 + }, + "value": "data" + } + ], + "path": [ + "data" + ] + } + ] + } + ] + }, + "errors": [ + { + "range": { + "start": 4, + "end": 12 + }, + "message": "Expected “hi”", + "severity": 3 + }, + { + "range": { + "start": 12, + "end": 14 + }, + "message": "Trailing data encountered: “ \\”", + "severity": 3 + }, + { + "range": { + "start": 16, + "end": 20 + }, + "message": "Expected “execute” or “say”", + "severity": 3 + } + ] +} + +exports['mcfunction parser entry() Parse "say ⧵↓ hi ↓ # comment start ⧵↓ end ↓ say hi" 1'] = { + "node": { + "type": "mcfunction:entry", + "range": { + "start": 0, + "end": 43 + }, + "children": [ + { + "type": "mcfunction:command", + "range": { + "start": 0, + "end": 10 + }, + "children": [ + { + "type": "mcfunction:command_child", + "range": { + "start": 0, + "end": 3 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 0, + "end": 3 + }, + "value": "say" + } + ], + "path": [ + "say" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 7, + "end": 9 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 7, + "end": 9 + }, + "value": "hi" + } + ], + "path": [ + "say", + "hi" + ] + } + ] + }, + { + "type": "comment", + "range": { + "start": 12, + "end": 35 + }, + "comment": " comment start end " + }, + { + "type": "mcfunction:command", + "range": { + "start": 37, + "end": 43 + }, + "children": [ + { + "type": "mcfunction:command_child", + "range": { + "start": 37, + "end": 40 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 37, + "end": 40 + }, + "value": "say" + } + ], + "path": [ + "say" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 41, + "end": 43 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 41, + "end": 43 + }, + "value": "hi" + } + ], + "path": [ + "say", + "hi" + ] + } + ] + } + ] + }, + "errors": [] +} + +exports['mcfunction parser entry() Parse "sa⧵ ↓ y ⧵ ↓ hi" 1'] = { + "node": { + "type": "mcfunction:entry", + "range": { + "start": 0, + "end": 16 + }, + "children": [ + { + "type": "mcfunction:command", + "range": { + "start": 0, + "end": 16 + }, + "children": [ + { + "type": "mcfunction:command_child", + "range": { + "start": 0, + "end": 9 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 0, + "end": 9 + }, + "value": "say" + } + ], + "path": [ + "say" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 14, + "end": 16 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 14, + "end": 16 + }, + "value": "hi" + } + ], + "path": [ + "say", + "hi" + ] + } + ] + } + ] + }, + "errors": [] +} + +exports['mcfunction parser entry() Parse "sa⧵ ↓ y ⧵ ↓ hi" without backslash continuation 1'] = { + "node": { + "type": "mcfunction:entry", + "range": { + "start": 0, + "end": 16 + }, + "children": [ + { + "type": "mcfunction:command", + "range": { + "start": 0, + "end": 5 + }, + "children": [ + { + "type": "mcfunction:command_child", + "range": { + "start": 0, + "end": 3 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 0, + "end": 3 + }, + "value": "sa\\" + } + ], + "path": [ + "sa\\" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 3, + "end": 5 + }, + "children": [ + { + "type": "mcfunction:command_child/trailing", + "range": { + "start": 3, + "end": 5 + }, + "value": " " + } + ], + "path": [] + } + ] + }, + { + "type": "mcfunction:command", + "range": { + "start": 8, + "end": 12 + }, + "children": [ + { + "type": "mcfunction:command_child", + "range": { + "start": 8, + "end": 9 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 8, + "end": 9 + }, + "value": "y" + } + ], + "path": [ + "y" + ] + }, + { + "type": "mcfunction:command_child", + "range": { + "start": 9, + "end": 12 + }, + "children": [ + { + "type": "mcfunction:command_child/trailing", + "range": { + "start": 9, + "end": 12 + }, + "value": " \\ " + } + ], + "path": [] + } + ] + }, + { + "type": "mcfunction:command", + "range": { + "start": 14, + "end": 16 + }, + "children": [ + { + "type": "mcfunction:command_child", + "range": { + "start": 14, + "end": 16 + }, + "children": [ + { + "type": "mcfunction:command_child/literal", + "range": { + "start": 14, + "end": 16 + }, + "value": "hi" + } + ], + "path": [ + "hi" + ] + } + ] + } + ] + }, + "errors": [ + { + "range": { + "start": 0, + "end": 3 + }, + "message": "Expected “execute” or “say”", + "severity": 3 + }, + { + "range": { + "start": 3, + "end": 5 + }, + "message": "Trailing data encountered: “ ”", + "severity": 3 + }, + { + "range": { + "start": 8, + "end": 9 + }, + "message": "Expected “execute” or “say”", + "severity": 3 + }, + { + "range": { + "start": 9, + "end": 12 + }, + "message": "Trailing data encountered: “ \\ ”", + "severity": 3 + }, + { + "range": { + "start": 14, + "end": 16 + }, + "message": "Expected “execute” or “say”", + "severity": 3 + } + ] +} diff --git a/packages/core/src/parser/util.ts b/packages/core/src/parser/util.ts index ae589f85a..4dd2616d5 100644 --- a/packages/core/src/parser/util.ts +++ b/packages/core/src/parser/util.ts @@ -1,9 +1,10 @@ -import type { AstNode } from '../node/index.js' +import { localize } from '@spyglassmc/locales' +import type { AstNode, ErrorNode } from '../node/index.js' import { SequenceUtil, SequenceUtilDiscriminator } from '../node/index.js' import type { ParserContext } from '../service/index.js' import { ErrorReporter } from '../service/index.js' -import type { ErrorSeverity, ReadonlySource, Source } from '../source/index.js' -import { Range } from '../source/index.js' +import type { ErrorSeverity, ReadonlySource } from '../source/index.js' +import { IndexMap, Range, Source } from '../source/index.js' import type { InfallibleParser, Parser, Result, Returnable } from './Parser.js' import { Failure } from './Parser.js' @@ -478,17 +479,17 @@ export function validate( */ export function stopBefore( parser: InfallibleParser, - ...teminators: (string | readonly string[])[] + ...terminators: (string | readonly string[])[] ): InfallibleParser export function stopBefore( parser: Parser, - ...teminators: (string | readonly string[])[] + ...terminators: (string | readonly string[])[] ): Parser export function stopBefore( parser: Parser, - ...teminators: (string | readonly string[])[] + ...terminators: (string | readonly string[])[] ): Parser { - const flatTerminators = teminators.flat() + const flatTerminators = terminators.flat() return (src, ctx): Result => { const tmpSrc = src.clone() // Cut tmpSrc.string before the nearest terminator. @@ -506,6 +507,77 @@ export function stopBefore( } } +/** + * @returns A parser that is based on the passed-in `parser`, but concatenates lines + * together when we reach, in order: + * - a backslash + * - whitespace (optional) + * - a newline + * - whitespace (optional) + */ +export function concatOnTrailingBackslash( + parser: InfallibleParser, +): InfallibleParser +export function concatOnTrailingBackslash( + parser: Parser, +): Parser +export function concatOnTrailingBackslash( + parser: Parser, +): Parser { + return (src, ctx): Result => { + let wrappedStr = src.sliceToCursor(0) + const wrappedSrcCursor = wrappedStr.length + const indexMap: IndexMap = [] + + while (src.canRead()) { + wrappedStr += src.readUntil('\\') + if (!src.canRead()) { + break + } + + // If we get here, then `src.cursor` is at a backslash + if (src.hasNonSpaceAheadInLine(1)) { + wrappedStr += src.read() + continue + } + + // Create an index map that skips from the trailing backslash to the + // next line's first non-whitespace character + const from = src.getCharRange() + src.nextLine() + + // Minecraft raises a `Line continuation at end of file` if a backslash + // (+ optional whitespace to the next line) is right before the end of the file + if (!src.canRead()) { + const ans: ErrorNode = { + type: 'error', + range: Range.span(from, src), + } + ctx.err.report( + localize('parser.line-continuation-end-of-file'), + ans, + ) + } + + src.skipSpace() + const to = src.getCharRange(-1) + indexMap.push({ + inner: Range.create(wrappedStr.length), + outer: Range.span(from, to), + }) + } + + const wrappedSrc = new Source( + wrappedStr, + IndexMap.merge(src.indexMap, indexMap), + ) + wrappedSrc.innerCursor = wrappedSrcCursor + const ans = parser(wrappedSrc, ctx) + src.cursor = wrappedSrc.cursor + return ans + } +} + /** * @returns A parser that is based on the passed-in `parser`, but will only read the acceptable characters. */ diff --git a/packages/core/src/source/Source.ts b/packages/core/src/source/Source.ts index 126126b01..9c9e2590c 100644 --- a/packages/core/src/source/Source.ts +++ b/packages/core/src/source/Source.ts @@ -103,9 +103,14 @@ export class ReadonlySource { return regex.test(this.peekRemaining()) } - hasNonSpaceAheadInLine(): boolean { + /** + * If there is a non-space character between `cursor + offset` (inclusive) and the next newline, returns `true`. Otherwise returns `false`. + * + * @param offset Defaults to 0. + */ + hasNonSpaceAheadInLine(offset = 0): boolean { for ( - let cursor = this.innerCursor; + let cursor = this.innerCursor + offset; cursor < this.string.length; cursor++ ) { diff --git a/packages/core/test/parser/util.spec.ts b/packages/core/test/parser/util.spec.ts index adb115871..bb3d6b99c 100644 --- a/packages/core/test/parser/util.spec.ts +++ b/packages/core/test/parser/util.spec.ts @@ -8,7 +8,14 @@ import type { Result, Source, } from '../../lib/index.js' -import { any, boolean, dumpErrors, Failure, Range } from '../../lib/index.js' +import { + any, + boolean, + concatOnTrailingBackslash, + dumpErrors, + Failure, + Range, +} from '../../lib/index.js' import { showWhitespaceGlyph, testParser } from '../utils.js' interface LiteralNode extends AstNode { @@ -119,3 +126,41 @@ describe('dumpErrors()', () => { }) } }) + +describe('concatOnTrailingBackslash()', () => { + const parsers: { + parser: Parser + suites: Array<{ + content: string + }> + }[] = [ + { + parser: boolean, + suites: [ + { content: 'true' }, + { content: 'true\n' }, + { content: 'tru\\\ne' }, + { content: 'tru\\ \ne' }, + { content: 'tru\\\n e' }, + { content: 'tru\\ \n e' }, + { content: 'tru\\ \n \\\n e' }, + { content: 'tru\\e \\ \n e' }, + { content: 'tru\\\n\ne' }, + { content: 'tru\\' }, + { content: 'tru\\ \n' }, + { content: 'tru\\ \n ' }, + ], + }, + ] + for (const { parser, suites } of parsers) { + for (const { content } of suites) { + it( + `Parse "${showWhitespaceGlyph(content)}"`, + () => { + const wrappedParser = concatOnTrailingBackslash(parser) + snapshot(testParser(wrappedParser, content)) + }, + ) + } + } +}) diff --git a/packages/core/test/source/Source.spec.ts b/packages/core/test/source/Source.spec.ts index d2d80391b..a4d152f68 100644 --- a/packages/core/test/source/Source.spec.ts +++ b/packages/core/test/source/Source.spec.ts @@ -522,6 +522,34 @@ describe('Source', () => { ) } }) + describe('hasNonSpaceAheadInLine', () => { + const suites: { + string: string + cursor: number + offset?: number + expected: boolean + }[] = [ + { string: 'foo', cursor: 0, expected: true }, + { string: 'foo', cursor: 2, expected: true }, + { string: 'foo', cursor: 3, expected: false }, + { string: 'foo ', cursor: 3, expected: false }, + { string: 'fooo ', cursor: 3, expected: true }, + { string: 'foooo ', cursor: 3, offset: 1, expected: true }, + { string: 'foooo ', cursor: 3, offset: 2, expected: false }, + ] + for (const { string, cursor, offset, expected } of suites) { + it( + `Should return ${expected} for from ${ + markOffsetInString(string, cursor + (offset ?? 0)) + }}`, + () => { + const src = new Source(string) + src.cursor = cursor + assert.strictEqual(src.hasNonSpaceAheadInLine(offset), expected) + }, + ) + } + }) describe('static', () => { describe('isBrigadierQuote()', () => { const suites: { c: string; expected: boolean }[] = [ diff --git a/packages/java-edition/src/mcfunction/index.ts b/packages/java-edition/src/mcfunction/index.ts index 1cd93606a..d5a26344d 100644 --- a/packages/java-edition/src/mcfunction/index.ts +++ b/packages/java-edition/src/mcfunction/index.ts @@ -32,7 +32,7 @@ export const initialize = ( meta.registerLanguage('mcfunction', { extensions: ['.mcfunction'], - parser: mcf.entry(tree, parser.argument), + parser: mcf.entry(tree, parser.argument, true), completer: mcf.completer.entry(tree, completer.getMockNodes), triggerCharacters: [ ' ', diff --git a/packages/java-edition/test/mcfunction/parser/argument.spec.ts b/packages/java-edition/test/mcfunction/parser/argument.spec.ts index ccd79ae85..3aa7cdee8 100644 --- a/packages/java-edition/test/mcfunction/parser/argument.spec.ts +++ b/packages/java-edition/test/mcfunction/parser/argument.spec.ts @@ -73,7 +73,9 @@ const Suites: Partial< 'minecraft:color': [{ content: ['red', 'green'] }], 'minecraft:column_pos': [{ content: ['0 0', '~ ~', '~1 ~-2'] }], 'minecraft:component': [ - { content: ['"hello world"', '""', '{"text":"hello world"}', '[""]'] }, + { + content: ['"hello world"', '""', '{"text":"hello world"}', '[""]'], + }, ], 'minecraft:dimension': [ { content: ['minecraft:overworld', 'minecraft:the_nether'] }, diff --git a/packages/locales/src/locales/en.json b/packages/locales/src/locales/en.json index 5284e923b..6d3ca1bdd 100644 --- a/packages/locales/src/locales/en.json +++ b/packages/locales/src/locales/en.json @@ -155,6 +155,7 @@ "objective-not-following-convention": "Invalid objective %0% which doesn't follow %1% convention", "parser.float.illegal": "Illegal float numeral that doesn't follow %0%", "parser.integer.illegal": "Illegal integer that doesn't follow %0%", + "parser.line-continuation-end-of-file": "A line continuation cannot be the end of the file", "parser.list.value": "a value", "parser.list.trailing-sep": "Trailing separation", "parser.long.illegal": "Illegal long numeral that doesn't follow %0%", diff --git a/packages/mcfunction/src/parser/entry.ts b/packages/mcfunction/src/parser/entry.ts index 059ddcacf..624eda15d 100644 --- a/packages/mcfunction/src/parser/entry.ts +++ b/packages/mcfunction/src/parser/entry.ts @@ -8,7 +8,7 @@ import { macro } from './macro.js' /** * @throws When there's no command tree associated with `commandTreeName`. */ -export function entry( +function mcfunction( commandTree: RootTreeNode, argument: ArgumentParserGetter, ): core.Parser { @@ -44,3 +44,18 @@ export function entry( const comment = core.comment({ singleLinePrefixes: new Set(['#']), }) + +/** + * @param supportsBackslashContinuation Whether or not to concatenate lines together on trailing backslashes. + * Disabled by default. + */ +export const entry = ( + commandTree: RootTreeNode, + argument: ArgumentParserGetter, + supportsBackslashContinuation = false, +) => { + const parser = mcfunction(commandTree, argument) + return supportsBackslashContinuation + ? core.concatOnTrailingBackslash(parser) + : parser +} diff --git a/packages/mcfunction/test/parser/command.spec.ts b/packages/mcfunction/test/parser/command.spec.ts index 59a47afe7..e572bbea9 100644 --- a/packages/mcfunction/test/parser/command.spec.ts +++ b/packages/mcfunction/test/parser/command.spec.ts @@ -6,41 +6,9 @@ import { fail } from 'assert' import { describe, it } from 'mocha' import snapshot from 'snap-shot-it' import { command } from '../../lib/parser/index.js' -import type { RootTreeNode } from '../../lib/tree/index.js' +import { tree } from './utils.js' describe('mcfunction parser command()', () => { - const tree: RootTreeNode = { - type: 'root', - children: { - execute: { - type: 'literal', - children: { - if: { - type: 'literal', - children: { - true: { - type: 'literal', - executable: true, - redirect: ['execute'], - }, - }, - }, - run: { - type: 'literal', - }, - }, - }, - say: { - type: 'literal', - children: { - hi: { - type: 'literal', - executable: true, - }, - }, - }, - }, - } const cases: { content: string }[] = [ { content: '' }, { content: 's' }, diff --git a/packages/mcfunction/test/parser/entry.spec.ts b/packages/mcfunction/test/parser/entry.spec.ts new file mode 100644 index 000000000..63aa5ccef --- /dev/null +++ b/packages/mcfunction/test/parser/entry.spec.ts @@ -0,0 +1,45 @@ +import { + mockProjectData, + showWhitespaceGlyph, + testParser, +} from '@spyglassmc/core/test-out/utils.js' +import { describe, it } from 'mocha' +import snapshot from 'snap-shot-it' +import { entry } from '../../lib/parser/index.js' +import { tree } from './utils.js' + +describe('mcfunction parser entry()', () => { + const cases: { content: string }[] = [ + { content: '' }, + { content: 'say hi' }, + { content: 'say hi\nsay hi' }, + { content: '# this is a comment' }, + { content: '$ this is a $(macro) command' }, + { content: '# this is a comment\nsay hi\n$this is a $(macro) \n' }, + { content: 'execute if true if true run say hi' }, + { content: '# this is a comment \\ \n still a comment' }, + { content: '$ this is a $(macro) \\ \n this is $(still) a macro' }, + { content: 'sa\\ \n y \\ \n hi' }, + { content: 'say trailing \\\n data' }, + { content: 'say \\\n hi \n # comment start \\\n end \n say hi' }, + ] + for (const { content } of cases) { + it(`Parse "${showWhitespaceGlyph(content)}"`, () => { + const parser = entry(tree, () => undefined, true) + snapshot(testParser(parser, content)) + }) + } + + const casesWithoutBackslashContinuation: { content: string }[] = [ + { content: 'say hi' }, + { content: 'say hi\nsay hi' }, + { content: 'sa\\ \n y \\ \n hi' }, + { content: 'say trailing \\\n data' }, + ] + for (const { content } of casesWithoutBackslashContinuation) { + it(`Parse "${showWhitespaceGlyph(content)}" without backslash continuation`, () => { + const parser = entry(tree, () => undefined) + snapshot(testParser(parser, content)) + }) + } +}) diff --git a/packages/mcfunction/test/parser/utils.ts b/packages/mcfunction/test/parser/utils.ts new file mode 100644 index 000000000..2f39aa69f --- /dev/null +++ b/packages/mcfunction/test/parser/utils.ts @@ -0,0 +1,34 @@ +import type { RootTreeNode } from '../../lib/index.js' + +export const tree: RootTreeNode = { + type: 'root', + children: { + execute: { + type: 'literal', + children: { + if: { + type: 'literal', + children: { + true: { + type: 'literal', + executable: true, + redirect: ['execute'], + }, + }, + }, + run: { + type: 'literal', + }, + }, + }, + say: { + type: 'literal', + children: { + hi: { + type: 'literal', + executable: true, + }, + }, + }, + }, +}