bolt-anchors 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bolt_anchors-0.1.0/PKG-INFO +96 -0
- bolt_anchors-0.1.0/README.md +86 -0
- bolt_anchors-0.1.0/pyproject.toml +36 -0
- bolt_anchors-0.1.0/pyproject.toml.orig +38 -0
- bolt_anchors-0.1.0/src/bolt_anchors/__init__.py +4 -0
- bolt_anchors-0.1.0/src/bolt_anchors/ast.py +20 -0
- bolt_anchors-0.1.0/src/bolt_anchors/parse.py +331 -0
- bolt_anchors-0.1.0/src/bolt_anchors/plugin.py +73 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: bolt-anchors
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Name resource locations and build nested paths in bolt
|
|
5
|
+
Requires-Dist: beet>=0.119.0
|
|
6
|
+
Requires-Dist: bolt>=0.51.0
|
|
7
|
+
Requires-Dist: mecha>=0.106.0
|
|
8
|
+
Requires-Python: >=3.14
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# Bolt Anchors
|
|
12
|
+
|
|
13
|
+
> Name resource locations and build nested paths in [bolt](https://github.com/mcbeet/beet/tree/main/packages/bolt).
|
|
14
|
+
|
|
15
|
+
Bolt anchors let you bind a resource declaration to an identifier with the `as`
|
|
16
|
+
clause. The anchor can then be used as a value or extended with `/` to build
|
|
17
|
+
nested resource locations.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install bolt-anchors
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
Add the plugin to your `beet.yml`/`beet.json` after `bolt`.
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
pipeline:
|
|
31
|
+
- mecha
|
|
32
|
+
|
|
33
|
+
require:
|
|
34
|
+
- bolt
|
|
35
|
+
- bolt_anchors
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
Bind an anchor when declaring any resource. The anchor holds the fully resolved
|
|
41
|
+
resource location of the declaration.
|
|
42
|
+
|
|
43
|
+
```mcfunction
|
|
44
|
+
append function ~/foo as foo:
|
|
45
|
+
print(foo)
|
|
46
|
+
function foo / "bar":
|
|
47
|
+
say foo bar
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`foo` resolves to `namespace:foo/foo` (relative to the current file and the
|
|
51
|
+
enclosing declarations) and `foo / "bar"` extends it into a nested resource
|
|
52
|
+
location. Anchors work for any resource type and any path depth.
|
|
53
|
+
|
|
54
|
+
```mcfunction
|
|
55
|
+
loot_table ./technical/loot as LOOT {}
|
|
56
|
+
print(LOOT)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Anchors can be used as values, interpolated into strings, and reused when
|
|
60
|
+
declaring or calling other resources.
|
|
61
|
+
|
|
62
|
+
```mcfunction
|
|
63
|
+
function ~/utils as utils:
|
|
64
|
+
function utils / "math" / "add":
|
|
65
|
+
scoreboard players add @s points 1
|
|
66
|
+
|
|
67
|
+
advancement ./tech/root as ROOT {}
|
|
68
|
+
predicate ROOT / "check" {}
|
|
69
|
+
say f"prefix is {utils}"
|
|
70
|
+
function utils / "math" / "add"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Moving the path scope
|
|
74
|
+
|
|
75
|
+
The `anchor` command changes the current path scope without creating a resource.
|
|
76
|
+
Nested locations and anchors inside its block resolve against the new location.
|
|
77
|
+
|
|
78
|
+
```mcfunction
|
|
79
|
+
anchor demo:foo as foo:
|
|
80
|
+
function ~/bar as bar:
|
|
81
|
+
print(bar) # demo:foo/bar
|
|
82
|
+
function foo / "baz" as baz:
|
|
83
|
+
print(baz) # demo:foo/baz
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The block itself emits no file. Commands and statements inside an `anchor` block
|
|
87
|
+
are treated like regular top-level code and are inlined into the current file,
|
|
88
|
+
while any resource declarations create files relative to the new scope.
|
|
89
|
+
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
uv sync --dev
|
|
94
|
+
uv run pytest
|
|
95
|
+
uv run ruff check
|
|
96
|
+
```
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Bolt Anchors
|
|
2
|
+
|
|
3
|
+
> Name resource locations and build nested paths in [bolt](https://github.com/mcbeet/beet/tree/main/packages/bolt).
|
|
4
|
+
|
|
5
|
+
Bolt anchors let you bind a resource declaration to an identifier with the `as`
|
|
6
|
+
clause. The anchor can then be used as a value or extended with `/` to build
|
|
7
|
+
nested resource locations.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install bolt-anchors
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Configuration
|
|
16
|
+
|
|
17
|
+
Add the plugin to your `beet.yml`/`beet.json` after `bolt`.
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
pipeline:
|
|
21
|
+
- mecha
|
|
22
|
+
|
|
23
|
+
require:
|
|
24
|
+
- bolt
|
|
25
|
+
- bolt_anchors
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
Bind an anchor when declaring any resource. The anchor holds the fully resolved
|
|
31
|
+
resource location of the declaration.
|
|
32
|
+
|
|
33
|
+
```mcfunction
|
|
34
|
+
append function ~/foo as foo:
|
|
35
|
+
print(foo)
|
|
36
|
+
function foo / "bar":
|
|
37
|
+
say foo bar
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`foo` resolves to `namespace:foo/foo` (relative to the current file and the
|
|
41
|
+
enclosing declarations) and `foo / "bar"` extends it into a nested resource
|
|
42
|
+
location. Anchors work for any resource type and any path depth.
|
|
43
|
+
|
|
44
|
+
```mcfunction
|
|
45
|
+
loot_table ./technical/loot as LOOT {}
|
|
46
|
+
print(LOOT)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Anchors can be used as values, interpolated into strings, and reused when
|
|
50
|
+
declaring or calling other resources.
|
|
51
|
+
|
|
52
|
+
```mcfunction
|
|
53
|
+
function ~/utils as utils:
|
|
54
|
+
function utils / "math" / "add":
|
|
55
|
+
scoreboard players add @s points 1
|
|
56
|
+
|
|
57
|
+
advancement ./tech/root as ROOT {}
|
|
58
|
+
predicate ROOT / "check" {}
|
|
59
|
+
say f"prefix is {utils}"
|
|
60
|
+
function utils / "math" / "add"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Moving the path scope
|
|
64
|
+
|
|
65
|
+
The `anchor` command changes the current path scope without creating a resource.
|
|
66
|
+
Nested locations and anchors inside its block resolve against the new location.
|
|
67
|
+
|
|
68
|
+
```mcfunction
|
|
69
|
+
anchor demo:foo as foo:
|
|
70
|
+
function ~/bar as bar:
|
|
71
|
+
print(bar) # demo:foo/bar
|
|
72
|
+
function foo / "baz" as baz:
|
|
73
|
+
print(baz) # demo:foo/baz
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The block itself emits no file. Commands and statements inside an `anchor` block
|
|
77
|
+
are treated like regular top-level code and are inlined into the current file,
|
|
78
|
+
while any resource declarations create files relative to the new scope.
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
uv sync --dev
|
|
84
|
+
uv run pytest
|
|
85
|
+
uv run ruff check
|
|
86
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bolt-anchors"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Name resource locations and build nested paths in bolt"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"beet>=0.119.0",
|
|
9
|
+
"bolt>=0.51.0",
|
|
10
|
+
"mecha>=0.106.0",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[dependency-groups]
|
|
14
|
+
dev = [
|
|
15
|
+
"lectern>=0.35.0",
|
|
16
|
+
"pytest>=9.0.2",
|
|
17
|
+
"pytest-insta>=0.4.1",
|
|
18
|
+
"python-semantic-release>=9.21.0",
|
|
19
|
+
"ruff>=0.14.13",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[tool.semantic_release]
|
|
23
|
+
major_on_zero = false
|
|
24
|
+
allow_zero_version = true
|
|
25
|
+
build_command = "uv build"
|
|
26
|
+
version_variables = ["src/bolt_anchors/__init__.py:__version__"]
|
|
27
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
28
|
+
commit_author = "github-actions <action@github.com>"
|
|
29
|
+
|
|
30
|
+
[tool.semantic_release.publish]
|
|
31
|
+
dist_glob_patterns = ["dist/*"]
|
|
32
|
+
upload_to_vcs_release = true
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["uv_build>=0.9.26,<0.10.0"]
|
|
36
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bolt-anchors"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Name resource locations and build nested paths in bolt"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"beet>=0.119.0",
|
|
9
|
+
"bolt>=0.51.0",
|
|
10
|
+
"mecha>=0.106.0",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[dependency-groups]
|
|
14
|
+
dev = [
|
|
15
|
+
"lectern>=0.35.0",
|
|
16
|
+
"pytest>=9.0.2",
|
|
17
|
+
"pytest-insta>=0.4.1",
|
|
18
|
+
"python-semantic-release>=9.21.0",
|
|
19
|
+
"ruff>=0.14.13",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[tool.semantic_release]
|
|
23
|
+
major_on_zero = false
|
|
24
|
+
allow_zero_version = true
|
|
25
|
+
build_command = "uv build"
|
|
26
|
+
|
|
27
|
+
version_variables = ["src/bolt_anchors/__init__.py:__version__"]
|
|
28
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
29
|
+
commit_author = "github-actions <action@github.com>"
|
|
30
|
+
|
|
31
|
+
[tool.semantic_release.publish]
|
|
32
|
+
# This ensures it uses the build_command defined above
|
|
33
|
+
dist_glob_patterns = ["dist/*"]
|
|
34
|
+
upload_to_vcs_release = true
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["uv_build>=0.9.26,<0.10.0"]
|
|
38
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"AstAnchor",
|
|
3
|
+
]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from bolt import AstValue
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class AstAnchor(AstValue):
|
|
14
|
+
"""Ast node representing a named resource location anchor.
|
|
15
|
+
|
|
16
|
+
The anchor holds the fully resolved resource location of the declaration it
|
|
17
|
+
was created from. When used as a value it behaves like a plain string.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
value: Any = ""
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"ANCHOR_COMMAND_IDENTIFIER",
|
|
3
|
+
"AnchorBlockParser",
|
|
4
|
+
"AnchorCommandTransformer",
|
|
5
|
+
"AnchorContext",
|
|
6
|
+
"AnchorIdentifierParser",
|
|
7
|
+
"AnchorRootParser",
|
|
8
|
+
"ResourceNameParser",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from collections.abc import Iterator
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
from dataclasses import dataclass, field, replace
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from bolt.parse import IDENTIFIER_PATTERN
|
|
19
|
+
from bolt.pattern import STRING_PATTERN
|
|
20
|
+
from mecha import (
|
|
21
|
+
AstChildren,
|
|
22
|
+
AstCommand,
|
|
23
|
+
AstNode,
|
|
24
|
+
AstResourceLocation,
|
|
25
|
+
AstRoot,
|
|
26
|
+
CommandSpec,
|
|
27
|
+
CompilationDatabase,
|
|
28
|
+
MutatingReducer,
|
|
29
|
+
Parser,
|
|
30
|
+
get_stream_scope,
|
|
31
|
+
rule,
|
|
32
|
+
)
|
|
33
|
+
from mecha.contrib.nested_location import AstNestedLocation
|
|
34
|
+
from mecha.contrib.relative_location import resolve_relative_location
|
|
35
|
+
from mecha.utils import JsonQuoteHelper
|
|
36
|
+
from tokenstream import Token, TokenStream, set_location
|
|
37
|
+
|
|
38
|
+
from .ast import AstAnchor
|
|
39
|
+
|
|
40
|
+
ANCHOR_COMMAND_IDENTIFIER = "anchor:name:commands"
|
|
41
|
+
|
|
42
|
+
IDENTIFIER_REGEX = re.compile(IDENTIFIER_PATTERN)
|
|
43
|
+
|
|
44
|
+
# Children that indicate that the current resource location argument introduces
|
|
45
|
+
# a resource declaration with a body.
|
|
46
|
+
DECLARATION_CHILDREN = {"commands", "content"}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AnchorContext:
|
|
50
|
+
"""Tracks anchors and declaration roots while parsing a compilation unit."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, database: CompilationDatabase):
|
|
53
|
+
self.database = database
|
|
54
|
+
self._unit: Any = None
|
|
55
|
+
self._bases: list[str] = []
|
|
56
|
+
self._anchored: list[bool] = []
|
|
57
|
+
self._scopes: list[dict[str, AstAnchor]] = [{}]
|
|
58
|
+
self._pending: tuple[tuple[str, ...], str, bool] | None = None
|
|
59
|
+
|
|
60
|
+
def reset_if_needed(self):
|
|
61
|
+
unit = self.database.current
|
|
62
|
+
if unit is not self._unit:
|
|
63
|
+
self._unit = unit
|
|
64
|
+
self._bases.clear()
|
|
65
|
+
self._anchored.clear()
|
|
66
|
+
self._scopes = [{}]
|
|
67
|
+
self._pending = None
|
|
68
|
+
|
|
69
|
+
def file_root(self) -> str | None:
|
|
70
|
+
unit = self.database.current
|
|
71
|
+
if unit is None:
|
|
72
|
+
return None
|
|
73
|
+
return self.database[unit].resource_location
|
|
74
|
+
|
|
75
|
+
def current_root(self) -> str | None:
|
|
76
|
+
if self._bases:
|
|
77
|
+
return self._bases[-1]
|
|
78
|
+
return self.file_root()
|
|
79
|
+
|
|
80
|
+
@contextmanager
|
|
81
|
+
def scope(self) -> Iterator[dict[str, AstAnchor]]:
|
|
82
|
+
self._scopes.append({})
|
|
83
|
+
try:
|
|
84
|
+
yield self._scopes[-1]
|
|
85
|
+
finally:
|
|
86
|
+
self._scopes.pop()
|
|
87
|
+
|
|
88
|
+
@contextmanager
|
|
89
|
+
def declaration(self, base: str, anchored: bool = False) -> Iterator[None]:
|
|
90
|
+
with self.scope():
|
|
91
|
+
self._bases.append(base)
|
|
92
|
+
self._anchored.append(anchored or self.is_anchored())
|
|
93
|
+
try:
|
|
94
|
+
yield
|
|
95
|
+
finally:
|
|
96
|
+
self._anchored.pop()
|
|
97
|
+
self._bases.pop()
|
|
98
|
+
|
|
99
|
+
def is_anchored(self) -> bool:
|
|
100
|
+
return any(self._anchored)
|
|
101
|
+
|
|
102
|
+
def bind(self, identifier: str, anchor: AstAnchor):
|
|
103
|
+
self._scopes[-1][identifier] = anchor
|
|
104
|
+
|
|
105
|
+
def lookup(self, identifier: str) -> AstAnchor | None:
|
|
106
|
+
for scope in reversed(self._scopes):
|
|
107
|
+
if anchor := scope.get(identifier):
|
|
108
|
+
return anchor
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def set_pending(self, name_scope: tuple[str, ...], base: str, anchored: bool):
|
|
112
|
+
self._pending = (name_scope, base, anchored)
|
|
113
|
+
|
|
114
|
+
def take_pending(self, name_scope: tuple[str, ...]) -> tuple[str, bool] | None:
|
|
115
|
+
pending = self._pending
|
|
116
|
+
if pending is None or pending[0] != name_scope:
|
|
117
|
+
return None
|
|
118
|
+
self._pending = None
|
|
119
|
+
return pending[1], pending[2]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def is_declaration_name(spec: CommandSpec, scope: tuple[str, ...]) -> bool:
|
|
123
|
+
"""Return whether the given argument scope introduces a resource declaration."""
|
|
124
|
+
tree = spec.tree.get(scope)
|
|
125
|
+
if tree is None or not tree.children:
|
|
126
|
+
return False
|
|
127
|
+
return any(name in DECLARATION_CHILDREN for name in tree.children)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def resolve_nested_location(
|
|
131
|
+
node: AstNestedLocation, context: AnchorContext
|
|
132
|
+
) -> AstResourceLocation:
|
|
133
|
+
"""Resolve a nested location against the current declaration root."""
|
|
134
|
+
root = context.current_root()
|
|
135
|
+
if root is None:
|
|
136
|
+
raise node.emit_error(
|
|
137
|
+
ValueError("Can't resolve nested location without a root.")
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
namespace, resolved = resolve_relative_location(
|
|
141
|
+
node.path, root, include_root_file=True
|
|
142
|
+
)
|
|
143
|
+
resource_location = AstResourceLocation(
|
|
144
|
+
is_tag=node.is_tag, namespace=namespace, path=resolved
|
|
145
|
+
)
|
|
146
|
+
return set_location(resource_location, node)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def canonical_location(node: AstResourceLocation, context: AnchorContext) -> str:
|
|
150
|
+
"""Return the fully resolved resource location of a name node."""
|
|
151
|
+
if isinstance(node, AstNestedLocation):
|
|
152
|
+
return resolve_nested_location(node, context).get_canonical_value()
|
|
153
|
+
return node.get_canonical_value()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
ANCHOR_SYNTAX = {
|
|
157
|
+
"literal": None,
|
|
158
|
+
"identifier": IDENTIFIER_PATTERN,
|
|
159
|
+
"string": STRING_PATTERN,
|
|
160
|
+
"number": r"(?:0|[1-9]\d*)(?:\.\d+)?",
|
|
161
|
+
"slash": r"/",
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def parse_anchor_path(
|
|
166
|
+
stream: TokenStream,
|
|
167
|
+
context: AnchorContext,
|
|
168
|
+
quote_helper: JsonQuoteHelper,
|
|
169
|
+
) -> tuple[str, Token] | None:
|
|
170
|
+
"""Parse an anchor followed by any number of path segments."""
|
|
171
|
+
with stream.syntax(**ANCHOR_SYNTAX):
|
|
172
|
+
token = stream.peek()
|
|
173
|
+
|
|
174
|
+
if token is None or not isinstance(token.value, str):
|
|
175
|
+
return None
|
|
176
|
+
if not IDENTIFIER_REGEX.fullmatch(token.value):
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
anchor = context.lookup(token.value)
|
|
180
|
+
if anchor is None:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
start = stream.expect()
|
|
184
|
+
value = str(anchor.value)
|
|
185
|
+
|
|
186
|
+
while stream.get(("slash", "/")):
|
|
187
|
+
segment = stream.expect_any("string", "identifier", "number")
|
|
188
|
+
if segment.match("string"):
|
|
189
|
+
text = quote_helper.unquote_string(segment)
|
|
190
|
+
else:
|
|
191
|
+
text = segment.value
|
|
192
|
+
value = f"{value}/{text}"
|
|
193
|
+
|
|
194
|
+
return value, start
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass
|
|
198
|
+
class ResourceNameParser:
|
|
199
|
+
"""Parser for resource declaration names that supports anchors."""
|
|
200
|
+
|
|
201
|
+
parser: Parser
|
|
202
|
+
context: AnchorContext
|
|
203
|
+
spec: CommandSpec
|
|
204
|
+
quote_helper: JsonQuoteHelper = field(default_factory=JsonQuoteHelper)
|
|
205
|
+
|
|
206
|
+
def __call__(self, stream: TokenStream) -> AstNode:
|
|
207
|
+
self.context.reset_if_needed()
|
|
208
|
+
|
|
209
|
+
node = self.parse_anchor(stream)
|
|
210
|
+
if node is None:
|
|
211
|
+
node = self.parser(stream)
|
|
212
|
+
|
|
213
|
+
scope = get_stream_scope(stream)
|
|
214
|
+
if not is_declaration_name(self.spec, scope):
|
|
215
|
+
return node
|
|
216
|
+
|
|
217
|
+
anchored = scope[0] == "anchor"
|
|
218
|
+
|
|
219
|
+
if (anchored or self.context.is_anchored()) and isinstance(
|
|
220
|
+
node, AstNestedLocation
|
|
221
|
+
):
|
|
222
|
+
node = resolve_nested_location(node, self.context)
|
|
223
|
+
|
|
224
|
+
base = canonical_location(node, self.context)
|
|
225
|
+
|
|
226
|
+
with stream.checkpoint() as commit:
|
|
227
|
+
if stream.get(("literal", "as")):
|
|
228
|
+
with stream.syntax(identifier=IDENTIFIER_PATTERN):
|
|
229
|
+
token = stream.expect("identifier")
|
|
230
|
+
self.context.bind(
|
|
231
|
+
token.value, set_location(AstAnchor(value=base), token)
|
|
232
|
+
)
|
|
233
|
+
commit()
|
|
234
|
+
|
|
235
|
+
self.context.set_pending(scope, base, anchored)
|
|
236
|
+
return node
|
|
237
|
+
|
|
238
|
+
def parse_anchor(self, stream: TokenStream) -> AstResourceLocation | None:
|
|
239
|
+
result = parse_anchor_path(stream, self.context, self.quote_helper)
|
|
240
|
+
if result is None:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
value, start = result
|
|
244
|
+
node = AstResourceLocation.from_value(value)
|
|
245
|
+
return set_location(node, start, stream.current)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@dataclass
|
|
249
|
+
class AnchorIdentifierParser:
|
|
250
|
+
"""Parser for identifiers that resolves anchors to their resource location."""
|
|
251
|
+
|
|
252
|
+
parser: Parser
|
|
253
|
+
context: AnchorContext
|
|
254
|
+
quote_helper: JsonQuoteHelper = field(default_factory=JsonQuoteHelper)
|
|
255
|
+
|
|
256
|
+
def __call__(self, stream: TokenStream) -> AstNode:
|
|
257
|
+
self.context.reset_if_needed()
|
|
258
|
+
|
|
259
|
+
result = parse_anchor_path(stream, self.context, self.quote_helper)
|
|
260
|
+
if result is not None:
|
|
261
|
+
value, start = result
|
|
262
|
+
node = AstAnchor(value=value)
|
|
263
|
+
return set_location(node, start, stream.current)
|
|
264
|
+
|
|
265
|
+
return self.parser(stream)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@dataclass
|
|
269
|
+
class AnchorBlockParser:
|
|
270
|
+
"""Parser that manages the anchor scope of a resource declaration body."""
|
|
271
|
+
|
|
272
|
+
parser: Parser
|
|
273
|
+
context: AnchorContext
|
|
274
|
+
|
|
275
|
+
def __call__(self, stream: TokenStream) -> Any:
|
|
276
|
+
self.context.reset_if_needed()
|
|
277
|
+
|
|
278
|
+
scope = get_stream_scope(stream)
|
|
279
|
+
pending = self.context.take_pending(scope[:-1])
|
|
280
|
+
|
|
281
|
+
if pending is None:
|
|
282
|
+
return self.parser(stream)
|
|
283
|
+
|
|
284
|
+
base, anchored = pending
|
|
285
|
+
|
|
286
|
+
with self.context.declaration(base, anchored):
|
|
287
|
+
return self.parser(stream)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@dataclass
|
|
291
|
+
class AnchorCommandTransformer(MutatingReducer):
|
|
292
|
+
"""Inlines the body of `anchor` commands into the surrounding root."""
|
|
293
|
+
|
|
294
|
+
@rule(AstRoot)
|
|
295
|
+
def anchor_commands(self, node: AstRoot) -> AstRoot:
|
|
296
|
+
changed = False
|
|
297
|
+
commands: list[AstCommand] = []
|
|
298
|
+
|
|
299
|
+
for command in node.commands:
|
|
300
|
+
if (
|
|
301
|
+
isinstance(command, AstCommand)
|
|
302
|
+
and command.identifier == ANCHOR_COMMAND_IDENTIFIER
|
|
303
|
+
and command.arguments
|
|
304
|
+
and isinstance(body := command.arguments[-1], AstRoot)
|
|
305
|
+
):
|
|
306
|
+
commands.extend(body.commands)
|
|
307
|
+
changed = True
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
commands.append(command)
|
|
311
|
+
|
|
312
|
+
if changed:
|
|
313
|
+
return replace(node, commands=AstChildren(commands))
|
|
314
|
+
|
|
315
|
+
return node
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@dataclass
|
|
319
|
+
class AnchorRootParser:
|
|
320
|
+
"""Parser that inlines `anchor` commands right after parsing."""
|
|
321
|
+
|
|
322
|
+
parser: Parser
|
|
323
|
+
transformer: AnchorCommandTransformer = field(
|
|
324
|
+
default_factory=AnchorCommandTransformer
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def __call__(self, stream: TokenStream) -> Any:
|
|
328
|
+
node = self.parser(stream)
|
|
329
|
+
if isinstance(node, AstRoot):
|
|
330
|
+
return self.transformer(node)
|
|
331
|
+
return node
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from beet import Context
|
|
2
|
+
from bolt import Runtime
|
|
3
|
+
from mecha import Mecha
|
|
4
|
+
|
|
5
|
+
from .parse import (
|
|
6
|
+
AnchorBlockParser,
|
|
7
|
+
AnchorContext,
|
|
8
|
+
AnchorIdentifierParser,
|
|
9
|
+
AnchorRootParser,
|
|
10
|
+
ResourceNameParser,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = ["beet_default"]
|
|
14
|
+
|
|
15
|
+
RESOURCE_NAME_PARSERS = (
|
|
16
|
+
"command:argument:minecraft:function",
|
|
17
|
+
"command:argument:minecraft:resource_location",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
BLOCK_PARSERS = (
|
|
21
|
+
"command:argument:mecha:nested_root",
|
|
22
|
+
"command:argument:mecha:nested_json",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# `anchor <resource_location> [as <identifier>]:` moves the path scope and can
|
|
26
|
+
# bind the location to an identifier without creating a resource.
|
|
27
|
+
ANCHOR_COMMANDS = {
|
|
28
|
+
"type": "root",
|
|
29
|
+
"children": {
|
|
30
|
+
"anchor": {
|
|
31
|
+
"type": "literal",
|
|
32
|
+
"children": {
|
|
33
|
+
"name": {
|
|
34
|
+
"type": "argument",
|
|
35
|
+
"parser": "minecraft:resource_location",
|
|
36
|
+
"children": {
|
|
37
|
+
"commands": {
|
|
38
|
+
"type": "argument",
|
|
39
|
+
"parser": "mecha:nested_root",
|
|
40
|
+
"executable": True,
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def beet_default(ctx: Context):
|
|
51
|
+
ctx.inject(Runtime)
|
|
52
|
+
mc = ctx.inject(Mecha)
|
|
53
|
+
|
|
54
|
+
context = AnchorContext(mc.database)
|
|
55
|
+
spec = mc.spec
|
|
56
|
+
|
|
57
|
+
spec.add_commands(ANCHOR_COMMANDS)
|
|
58
|
+
|
|
59
|
+
for name in RESOURCE_NAME_PARSERS:
|
|
60
|
+
if parser := spec.parsers.get(name):
|
|
61
|
+
spec.parsers[name] = ResourceNameParser(parser, context, spec)
|
|
62
|
+
|
|
63
|
+
for name in BLOCK_PARSERS:
|
|
64
|
+
if parser := spec.parsers.get(name):
|
|
65
|
+
spec.parsers[name] = AnchorBlockParser(parser, context)
|
|
66
|
+
|
|
67
|
+
if parser := spec.parsers.get("bolt:identifier"):
|
|
68
|
+
spec.parsers["bolt:identifier"] = AnchorIdentifierParser(parser, context)
|
|
69
|
+
|
|
70
|
+
# Inline anchor bodies at parse time so that the cached ast already reflects
|
|
71
|
+
# the moved path scope and statements are evaluated like regular code.
|
|
72
|
+
if parser := spec.parsers.get("root"):
|
|
73
|
+
spec.parsers["root"] = AnchorRootParser(parser)
|