pbark 1.0.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.
Files changed (86) hide show
  1. pbark-1.0.0/PKG-INFO +71 -0
  2. pbark-1.0.0/README.md +62 -0
  3. pbark-1.0.0/pbark/__init__.py +3 -0
  4. pbark-1.0.0/pbark/__main__.py +4 -0
  5. pbark-1.0.0/pbark/bark.py +109 -0
  6. pbark-1.0.0/pbark/cli/dog_art.py +75 -0
  7. pbark-1.0.0/pbark/cli/version.py +32 -0
  8. pbark-1.0.0/pbark/config/__init__.py +0 -0
  9. pbark-1.0.0/pbark/config/_resources.py +12 -0
  10. pbark-1.0.0/pbark/config/config_loader.py +239 -0
  11. pbark-1.0.0/pbark/config/dog_topics.py +203 -0
  12. pbark-1.0.0/pbark/config/name_hints.py +50 -0
  13. pbark-1.0.0/pbark/config/trait_loader.py +30 -0
  14. pbark-1.0.0/pbark/config/variable_type.py +12 -0
  15. pbark-1.0.0/pbark/errors.py +29 -0
  16. pbark-1.0.0/pbark/interpreter/bark_value.py +62 -0
  17. pbark-1.0.0/pbark/interpreter/comparators.py +45 -0
  18. pbark-1.0.0/pbark/interpreter/dog.py +136 -0
  19. pbark-1.0.0/pbark/interpreter/interpreter.py +823 -0
  20. pbark-1.0.0/pbark/interpreter/loop_signal.py +11 -0
  21. pbark-1.0.0/pbark/interpreter/print_styles.py +39 -0
  22. pbark-1.0.0/pbark/interpreter/prop.py +11 -0
  23. pbark-1.0.0/pbark/interpreter/stdin_reader.py +18 -0
  24. pbark-1.0.0/pbark/lexer.py +184 -0
  25. pbark-1.0.0/pbark/options.py +13 -0
  26. pbark-1.0.0/pbark/parser/__init__.py +0 -0
  27. pbark-1.0.0/pbark/parser/assign/__init__.py +0 -0
  28. pbark-1.0.0/pbark/parser/assign/assign_parser.py +403 -0
  29. pbark-1.0.0/pbark/parser/ast_node.py +209 -0
  30. pbark-1.0.0/pbark/parser/collection/__init__.py +0 -0
  31. pbark-1.0.0/pbark/parser/collection/pile_action_parser.py +125 -0
  32. pbark-1.0.0/pbark/parser/collection/pile_parser.py +77 -0
  33. pbark-1.0.0/pbark/parser/collection/smart_ordinal_parser.py +53 -0
  34. pbark-1.0.0/pbark/parser/collection/stash_parser.py +170 -0
  35. pbark-1.0.0/pbark/parser/collection/stash_spots.py +34 -0
  36. pbark-1.0.0/pbark/parser/collection/take_parser.py +118 -0
  37. pbark-1.0.0/pbark/parser/controlflow/__init__.py +0 -0
  38. pbark-1.0.0/pbark/parser/controlflow/foreach_parser.py +128 -0
  39. pbark-1.0.0/pbark/parser/controlflow/function_parser.py +91 -0
  40. pbark-1.0.0/pbark/parser/controlflow/if_parser.py +139 -0
  41. pbark-1.0.0/pbark/parser/controlflow/inline_body_parser.py +25 -0
  42. pbark-1.0.0/pbark/parser/controlflow/loop_control_parser.py +28 -0
  43. pbark-1.0.0/pbark/parser/controlflow/until_parser.py +84 -0
  44. pbark-1.0.0/pbark/parser/controlflow/while_parser.py +84 -0
  45. pbark-1.0.0/pbark/parser/expression/__init__.py +0 -0
  46. pbark-1.0.0/pbark/parser/expression/comparison_op.py +12 -0
  47. pbark-1.0.0/pbark/parser/expression/condition_parser.py +247 -0
  48. pbark-1.0.0/pbark/parser/expression/value_parser.py +308 -0
  49. pbark-1.0.0/pbark/parser/input/__init__.py +0 -0
  50. pbark-1.0.0/pbark/parser/input/input_parser.py +63 -0
  51. pbark-1.0.0/pbark/parser/keywords/__init__.py +429 -0
  52. pbark-1.0.0/pbark/parser/keywords/attribute_keyword_groups.py +42 -0
  53. pbark-1.0.0/pbark/parser/keywords/collection_keyword_groups.py +41 -0
  54. pbark-1.0.0/pbark/parser/keywords/control_flow_keyword_groups.py +23 -0
  55. pbark-1.0.0/pbark/parser/keywords/keyword_registry.py +18 -0
  56. pbark-1.0.0/pbark/parser/keywords/logic_keyword_groups.py +35 -0
  57. pbark-1.0.0/pbark/parser/keywords/print_keyword_groups.py +26 -0
  58. pbark-1.0.0/pbark/parser/parse_expression.py +184 -0
  59. pbark-1.0.0/pbark/parser/parser.py +299 -0
  60. pbark-1.0.0/pbark/parser/printing/__init__.py +0 -0
  61. pbark-1.0.0/pbark/parser/printing/print_parser.py +434 -0
  62. pbark-1.0.0/pbark/parser/printing/print_verb.py +9 -0
  63. pbark-1.0.0/pbark/parser/share/__init__.py +0 -0
  64. pbark-1.0.0/pbark/parser/share/share_parser.py +142 -0
  65. pbark-1.0.0/pbark/print_style.py +14 -0
  66. pbark-1.0.0/pbark/resources/breeds.txt +182 -0
  67. pbark-1.0.0/pbark/resources/dogfacts.txt +52 -0
  68. pbark-1.0.0/pbark/resources/objects.txt +16 -0
  69. pbark-1.0.0/pbark/resources/piles.txt +5 -0
  70. pbark-1.0.0/pbark/resources/stashes.txt +13 -0
  71. pbark-1.0.0/pbark/resources/traits.json +62 -0
  72. pbark-1.0.0/pbark.egg-info/PKG-INFO +71 -0
  73. pbark-1.0.0/pbark.egg-info/SOURCES.txt +84 -0
  74. pbark-1.0.0/pbark.egg-info/dependency_links.txt +1 -0
  75. pbark-1.0.0/pbark.egg-info/entry_points.txt +2 -0
  76. pbark-1.0.0/pbark.egg-info/requires.txt +3 -0
  77. pbark-1.0.0/pbark.egg-info/top_level.txt +1 -0
  78. pbark-1.0.0/pyproject.toml +27 -0
  79. pbark-1.0.0/setup.cfg +4 -0
  80. pbark-1.0.0/tests/test_feature.py +613 -0
  81. pbark-1.0.0/tests/test_golden.py +86 -0
  82. pbark-1.0.0/tests/test_parser_register.py +45 -0
  83. pbark-1.0.0/tests/test_parser_shape.py +64 -0
  84. pbark-1.0.0/tests/test_parser_stash.py +100 -0
  85. pbark-1.0.0/tests/test_parser_value.py +168 -0
  86. pbark-1.0.0/tests/test_script_mode_parse.py +28 -0
pbark-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,71 @@
1
+ Metadata-Version: 2.4
2
+ Name: pbark
3
+ Version: 1.0.0
4
+ Summary: Python port of the jbark dog-themed story programming language
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Provides-Extra: dev
8
+ Requires-Dist: pytest>=7.0; extra == "dev"
9
+
10
+ # pbark
11
+
12
+ Python port of [jbark](../src/main/java/dev/klomptech/jbark/), the dog-themed story programming language.
13
+
14
+ **Semi-automated port:** pbark was translated from the Java source. It aims for parity with jbark and shares the same test suite. If behaviour diverges, jbark is canonical.
15
+
16
+ ## Release status
17
+
18
+ **Already in this repo:** interpreter, 95 tests, CLI, registry files in `pbark/resources/`, `pip install .` works from a clone.
19
+
20
+ **Still to do (you):** PyPI account → `twine upload` so others can `pip install pbark`. Full steps: [docs/RELEASE-PYTHON.md](../docs/RELEASE-PYTHON.md).
21
+
22
+ When you edit breeds/objects in `src/main/resources/`, refresh the Python copy:
23
+
24
+ ```bash
25
+ ./scripts/sync-pbark-resources.sh # from repo root
26
+ ```
27
+
28
+ ## Install
29
+
30
+ Requires **Python 3.10+**.
31
+
32
+ ```bash
33
+ cd pbark
34
+ python3 -m venv .venv
35
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
36
+ pip install -e .
37
+ ```
38
+
39
+ ## Run
40
+
41
+ From the repo root (after install):
42
+
43
+ ```bash
44
+ ./pbark/bin/pbark examples/woof/goodboy.woof
45
+ ./pbark/bin/pbark --version
46
+ ./pbark/bin/pbark --list-breeds
47
+ ```
48
+
49
+ Or with the module directly:
50
+
51
+ ```bash
52
+ python3 -m pbark examples/woof/bimba.woof
53
+ ```
54
+
55
+ Flags match jbark: `--help`, `--version`, `--strict`, `--quiet`, `--list-breeds`, `--list-objects`.
56
+
57
+ ## Tests
58
+
59
+ ```bash
60
+ pip install -e ".[dev]"
61
+ pytest tests/ -q
62
+ ```
63
+
64
+ The test suite is ported from jbark's JUnit tests. Golden output, parser shape, and feature coverage.
65
+
66
+ ## Docs
67
+
68
+ - [docs/MANUAL.md](../docs/MANUAL.md) - grammar and runtime rules
69
+ - [docs/AUTHOR.md](../docs/AUTHOR.md) - writing stories
70
+ - [docs/RELEASE-PYTHON.md](../docs/RELEASE-PYTHON.md) - what's done vs what you still do for PyPI
71
+ - [examples/](../examples/) - runnable `.woof` programs
pbark-1.0.0/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # pbark
2
+
3
+ Python port of [jbark](../src/main/java/dev/klomptech/jbark/), the dog-themed story programming language.
4
+
5
+ **Semi-automated port:** pbark was translated from the Java source. It aims for parity with jbark and shares the same test suite. If behaviour diverges, jbark is canonical.
6
+
7
+ ## Release status
8
+
9
+ **Already in this repo:** interpreter, 95 tests, CLI, registry files in `pbark/resources/`, `pip install .` works from a clone.
10
+
11
+ **Still to do (you):** PyPI account → `twine upload` so others can `pip install pbark`. Full steps: [docs/RELEASE-PYTHON.md](../docs/RELEASE-PYTHON.md).
12
+
13
+ When you edit breeds/objects in `src/main/resources/`, refresh the Python copy:
14
+
15
+ ```bash
16
+ ./scripts/sync-pbark-resources.sh # from repo root
17
+ ```
18
+
19
+ ## Install
20
+
21
+ Requires **Python 3.10+**.
22
+
23
+ ```bash
24
+ cd pbark
25
+ python3 -m venv .venv
26
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
27
+ pip install -e .
28
+ ```
29
+
30
+ ## Run
31
+
32
+ From the repo root (after install):
33
+
34
+ ```bash
35
+ ./pbark/bin/pbark examples/woof/goodboy.woof
36
+ ./pbark/bin/pbark --version
37
+ ./pbark/bin/pbark --list-breeds
38
+ ```
39
+
40
+ Or with the module directly:
41
+
42
+ ```bash
43
+ python3 -m pbark examples/woof/bimba.woof
44
+ ```
45
+
46
+ Flags match jbark: `--help`, `--version`, `--strict`, `--quiet`, `--list-breeds`, `--list-objects`.
47
+
48
+ ## Tests
49
+
50
+ ```bash
51
+ pip install -e ".[dev]"
52
+ pytest tests/ -q
53
+ ```
54
+
55
+ The test suite is ported from jbark's JUnit tests. Golden output, parser shape, and feature coverage.
56
+
57
+ ## Docs
58
+
59
+ - [docs/MANUAL.md](../docs/MANUAL.md) - grammar and runtime rules
60
+ - [docs/AUTHOR.md](../docs/AUTHOR.md) - writing stories
61
+ - [docs/RELEASE-PYTHON.md](../docs/RELEASE-PYTHON.md) - what's done vs what you still do for PyPI
62
+ - [examples/](../examples/) - runnable `.woof` programs
@@ -0,0 +1,3 @@
1
+ """pbark: Python port of the jbark dog-themed story programming language."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,4 @@
1
+ from pbark.bark import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from pbark.cli import dog_art as DogArt
7
+ from pbark.cli import version as Version
8
+ from pbark.config.config_loader import ConfigLoader
9
+ from pbark.errors import BarkError, ExitCode
10
+ from pbark.interpreter.interpreter import Interpreter
11
+ from pbark.lexer import Lexer
12
+ from pbark.options import BarkOptions
13
+ from pbark.parser.parser import Parser
14
+
15
+
16
+ def print_help() -> None:
17
+ print(
18
+ """pbark: a dog-themed story programming language (Python port of jbark)
19
+
20
+ Usage:
21
+ pbark [script.woof] Run a .woof file
22
+ pbark Read lines from stdin (Ctrl+D to finish)
23
+ pbark --version Show version and a dog fact
24
+ pbark --list-breeds List registered breeds
25
+ pbark --list-objects List registered objects
26
+ pbark --strict script.woof Warn on story-only lines that do nothing
27
+ pbark --quiet script.woof Suppress startup banner and goodbye
28
+
29
+ Docs: docs/AUTHOR.md · docs/MANUAL.md
30
+ """
31
+ )
32
+
33
+
34
+ def _list_names(label: str, names: list[str]) -> None:
35
+ print(f"{label}:")
36
+ for name in names:
37
+ print(f" {name}")
38
+
39
+
40
+ def _is_help_flag(arg: str) -> bool:
41
+ return arg in ("--help", "-h")
42
+
43
+
44
+ def _is_version_flag(arg: str) -> bool:
45
+ return arg in ("--version", "-version")
46
+
47
+
48
+ def run(source: str, options: BarkOptions | None = None) -> int:
49
+ options = options or BarkOptions.defaults()
50
+ try:
51
+ if not options.quiet:
52
+ DogArt.print_banner()
53
+ lexer = Lexer(source)
54
+ parser = Parser(lexer.tokenise(), options)
55
+ program = parser.parse()
56
+ Interpreter().run(program)
57
+ if not options.quiet:
58
+ DogArt.print_goodbye()
59
+ return ExitCode.OK
60
+ except BarkError as e:
61
+ print(e, file=sys.stderr)
62
+ return ExitCode.DATA_ERROR
63
+
64
+
65
+ def main() -> None:
66
+ options = BarkOptions.defaults()
67
+ script_path: str | None = None
68
+ for arg in sys.argv[1:]:
69
+ if _is_help_flag(arg):
70
+ print_help()
71
+ sys.exit(ExitCode.OK)
72
+ if _is_version_flag(arg):
73
+ Version.print_version()
74
+ sys.exit(ExitCode.OK)
75
+ if arg == "--strict":
76
+ options = BarkOptions(True, options.quiet)
77
+ continue
78
+ if arg == "--quiet":
79
+ options = BarkOptions(options.strict, True)
80
+ continue
81
+ if arg == "--list-breeds":
82
+ _list_names("Breeds", ConfigLoader.list_breeds())
83
+ sys.exit(ExitCode.OK)
84
+ if arg == "--list-objects":
85
+ _list_names("Objects", ConfigLoader.list_objects())
86
+ sys.exit(ExitCode.OK)
87
+ if arg.startswith("-"):
88
+ print(f"Unknown flag: {arg}. Try pbark --help", file=sys.stderr)
89
+ sys.exit(ExitCode.USAGE)
90
+ if script_path is not None:
91
+ print("Usage: pbark [flags] [script.woof]. Try pbark --help", file=sys.stderr)
92
+ sys.exit(ExitCode.USAGE)
93
+ script_path = arg
94
+
95
+ try:
96
+ if script_path is not None:
97
+ source = Path(script_path).read_text(encoding="utf-8")
98
+ else:
99
+ print("> ", end="")
100
+ source = sys.stdin.read()
101
+ except OSError as e:
102
+ print(f"Couldn't dig up that file: {e}", file=sys.stderr)
103
+ sys.exit(ExitCode.INVALID_ARGUMENTS)
104
+
105
+ sys.exit(run(source, options))
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+
5
+ from pbark.config._resources import resources_dir
6
+
7
+ EMOJI = "\U0001f415"
8
+ PAW = "\U0001f43e"
9
+ FACT_PREFIX = "Funny fact: "
10
+
11
+ _EMOJIS = [EMOJI, "\U0001f436", "woof."]
12
+
13
+ _BANNERS = [
14
+ """
15
+ ___ __
16
+ /(. .)\\ )
17
+ (*)_____/|
18
+ / |
19
+ / |--\\ |
20
+ (_)(_) (_)
21
+ """,
22
+ """
23
+ / \\__
24
+ ( @\\___
25
+ / O
26
+ / (_____/
27
+ /_____/ U
28
+ woof!
29
+ """,
30
+ """
31
+ __
32
+ (\\,--------'()'--o
33
+ (_ ___ /~"
34
+ (_)_) (_)_)
35
+ """,
36
+ ]
37
+
38
+
39
+ def _load_goodbyes() -> list[str]:
40
+ lines = [
41
+ "The dogs are now chilling on your bed.",
42
+ "After dragging the bed through the house, no more energy to play.",
43
+ "That was a good walk, time to relax.",
44
+ "The dogs had enough of playing, they went upstairs.",
45
+ ]
46
+ path = resources_dir() / "dogfacts.txt"
47
+ if path.is_file():
48
+ for line in path.read_text(encoding="utf-8").splitlines():
49
+ stripped = line.strip()
50
+ if stripped:
51
+ lines.append(FACT_PREFIX + stripped)
52
+ return lines
53
+
54
+
55
+ _GOODBYES = _load_goodbyes()
56
+
57
+
58
+ def print_banner() -> None:
59
+ print(random.choice(_BANNERS))
60
+
61
+
62
+ def random_bare_bark() -> str:
63
+ return random.choice(_EMOJIS)
64
+
65
+
66
+ def print_goodbye() -> None:
67
+ print(random.choice(_GOODBYES))
68
+
69
+
70
+ def paws_art() -> str:
71
+ return f"{PAW} {PAW} {PAW}"
72
+
73
+
74
+ def print_paws() -> None:
75
+ print(paws_art())
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+
5
+ from pbark.cli.dog_art import FACT_PREFIX
6
+ from pbark.config._resources import resources_dir
7
+
8
+ NAME = "pbark"
9
+ NUMBER = "1.0.0"
10
+
11
+
12
+ def _load_facts() -> list[str]:
13
+ path = resources_dir() / "dogfacts.txt"
14
+ if not path.is_file():
15
+ return []
16
+ return [line.strip() for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
17
+
18
+
19
+ _DOG_FACTS = _load_facts()
20
+
21
+
22
+ def print_version() -> None:
23
+ print(f"{NAME} {NUMBER}")
24
+ fact = random_dog_fact()
25
+ if fact is not None:
26
+ print(FACT_PREFIX + fact)
27
+
28
+
29
+ def random_dog_fact() -> str | None:
30
+ if not _DOG_FACTS:
31
+ return None
32
+ return random.choice(_DOG_FACTS)
File without changes
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def resources_dir() -> Path:
7
+ # Bundled copies ship inside the wheel (pip install pbark).
8
+ bundled = Path(__file__).resolve().parent.parent / "resources"
9
+ if (bundled / "breeds.txt").is_file():
10
+ return bundled
11
+ # Git clone dev layout: shared monorepo registry files.
12
+ return Path(__file__).resolve().parent.parent.parent.parent / "src" / "main" / "resources"
@@ -0,0 +1,239 @@
1
+ from __future__ import annotations
2
+
3
+ from pbark.config._resources import resources_dir
4
+ from pbark.config.variable_type import VariableType
5
+ from pbark.errors import BarkError
6
+ from pbark.lexer import Token, TokenType
7
+
8
+
9
+ class ConfigLoader:
10
+ MEMORY = "memory"
11
+ JOURNAL = "journal"
12
+
13
+ _INDEX: dict[str, VariableType] = {}
14
+ _PHRASE_ALIASES: dict[str, str] = {}
15
+ _loaded = False
16
+
17
+ @classmethod
18
+ def _ensure_loaded(cls) -> None:
19
+ if cls._loaded:
20
+ return
21
+ cls._load("/breeds.txt", VariableType.BREED)
22
+ cls._load("/objects.txt", VariableType.OBJECT)
23
+ cls._load("/stashes.txt", VariableType.STASH)
24
+ cls._load("/piles.txt", VariableType.PILE)
25
+ cls._INDEX[cls.MEMORY] = VariableType.STORY_NUMBER
26
+ cls._INDEX[cls.JOURNAL] = VariableType.STORY_TEXT
27
+ cls._build_phrase_aliases()
28
+ cls._loaded = True
29
+
30
+ @classmethod
31
+ def _load(cls, filename: str, vtype: VariableType) -> None:
32
+ path = resources_dir() / filename.lstrip("/")
33
+ if not path.is_file():
34
+ raise IllegalStateError(f"Resource not found: {filename}")
35
+ for line in path.read_text(encoding="utf-8").splitlines():
36
+ word = line.strip()
37
+ if word:
38
+ cls._INDEX[cls.normalise(word)] = vtype
39
+
40
+ @classmethod
41
+ def _build_phrase_aliases(cls) -> None:
42
+ for name in list(cls._INDEX.keys()):
43
+ cls._PHRASE_ALIASES[name] = name
44
+ if "_" in name:
45
+ cls._PHRASE_ALIASES[name.replace("_", " ")] = name
46
+
47
+ @staticmethod
48
+ def normalise(name: str) -> str:
49
+ return name.strip().lower()
50
+
51
+ @classmethod
52
+ def type_of(cls, name: str) -> VariableType | None:
53
+ cls._ensure_loaded()
54
+ return cls._INDEX.get(cls.normalise(name))
55
+
56
+ @classmethod
57
+ def is_valid_name(cls, name: str) -> bool:
58
+ return cls.type_of(name) is not None
59
+
60
+ @classmethod
61
+ def is_breed(cls, name: str) -> bool:
62
+ return cls.type_of(name) == VariableType.BREED
63
+
64
+ @classmethod
65
+ def is_stash(cls, name: str) -> bool:
66
+ return cls.type_of(name) == VariableType.STASH
67
+
68
+ @classmethod
69
+ def is_pile(cls, name: str) -> bool:
70
+ return cls.type_of(name) == VariableType.PILE
71
+
72
+ @classmethod
73
+ def is_story_number_constant(cls, name: str) -> bool:
74
+ return cls.type_of(name) == VariableType.STORY_NUMBER
75
+
76
+ @classmethod
77
+ def is_story_text_constant(cls, name: str) -> bool:
78
+ return cls.type_of(name) == VariableType.STORY_TEXT
79
+
80
+ @classmethod
81
+ def is_story_constant(cls, name: str) -> bool:
82
+ return cls.is_story_number_constant(name)
83
+
84
+ @classmethod
85
+ def memory_name(cls) -> str:
86
+ return cls.MEMORY
87
+
88
+ @classmethod
89
+ def journal_name(cls) -> str:
90
+ return cls.JOURNAL
91
+
92
+ @classmethod
93
+ def token_index_after_resolved_name(
94
+ cls, tokens: list[Token], fr: int, to: int, resolved_name: str
95
+ ) -> int:
96
+ cls._ensure_loaded()
97
+ words: list[str] = []
98
+ positions: list[int] = []
99
+ for i in range(fr, to):
100
+ token = tokens[i]
101
+ if token.is_(TokenType.IDENTIFIER):
102
+ words.append(cls.normalise(token.value))
103
+ positions.append(i)
104
+ for length in range(min(4, len(words)), 0, -1):
105
+ for start in range(len(words) - length + 1):
106
+ phrase = " ".join(words[start : start + length])
107
+ if resolved_name == cls.resolve_name_phrase(phrase):
108
+ return positions[start + length - 1] + 1
109
+ key = cls.normalise(resolved_name)
110
+ for i in range(fr, to):
111
+ token = tokens[i]
112
+ if token.is_(TokenType.IDENTIFIER) and key == cls.normalise(token.value):
113
+ return i + 1
114
+ return -1
115
+
116
+ @classmethod
117
+ def list_names(cls, vtype: VariableType) -> list[str]:
118
+ cls._ensure_loaded()
119
+ return sorted(k for k, v in cls._INDEX.items() if v == vtype)
120
+
121
+ @classmethod
122
+ def list_breeds(cls) -> list[str]:
123
+ return cls.list_names(VariableType.BREED)
124
+
125
+ @classmethod
126
+ def list_objects(cls) -> list[str]:
127
+ return cls.list_names(VariableType.OBJECT)
128
+
129
+ @classmethod
130
+ def list_stashes(cls) -> list[str]:
131
+ return cls.list_names(VariableType.STASH)
132
+
133
+ @classmethod
134
+ def list_piles(cls) -> list[str]:
135
+ return cls.list_names(VariableType.PILE)
136
+
137
+ @classmethod
138
+ def resolve_name_phrase(cls, phrase: str) -> str | None:
139
+ cls._ensure_loaded()
140
+ return cls._PHRASE_ALIASES.get(cls.normalise(phrase))
141
+
142
+ @classmethod
143
+ def resolve_name_from_tokens(cls, tokens: list[Token], fr: int, to: int) -> str | None:
144
+ cls._ensure_loaded()
145
+ words = cls._identifier_words(tokens, fr, to)
146
+ cls._skip_leading_story_noise(words)
147
+ return cls._match_longest_name_phrase(words)
148
+
149
+ @classmethod
150
+ def resolve_collection_from_tokens(
151
+ cls, tokens: list[Token], fr: int, to: int
152
+ ) -> str | None:
153
+ cls._ensure_loaded()
154
+ words = cls._identifier_words(tokens, fr, to)
155
+ cls._skip_leading_story_noise(words)
156
+ best = None
157
+ best_length = 0
158
+ for length in range(min(4, len(words)), 0, -1):
159
+ for start in range(len(words) - length + 1):
160
+ phrase = " ".join(words[start : start + length])
161
+ resolved = cls.resolve_name_phrase(phrase)
162
+ if (
163
+ resolved is not None
164
+ and (cls.is_stash(resolved) or cls.is_pile(resolved))
165
+ and length >= best_length
166
+ ):
167
+ best = resolved
168
+ best_length = length
169
+ return best
170
+
171
+ @classmethod
172
+ def _skip_leading_story_noise(cls, words: list[str]) -> None:
173
+ while words:
174
+ if cls.is_valid_name(words[0]) or cls.resolve_name_phrase(words[0]) is not None:
175
+ break
176
+ name_ahead = False
177
+ for length in range(1, min(4, len(words)) + 1):
178
+ if cls.resolve_name_phrase(" ".join(words[:length])) is not None:
179
+ name_ahead = True
180
+ break
181
+ if name_ahead:
182
+ break
183
+ words.pop(0)
184
+
185
+ @staticmethod
186
+ def _identifier_words(tokens: list[Token], fr: int, to: int) -> list[str]:
187
+ words: list[str] = []
188
+ for i in range(fr, to):
189
+ token = tokens[i]
190
+ if token.is_(TokenType.IDENTIFIER):
191
+ words.append(ConfigLoader.normalise(token.value))
192
+ return words
193
+
194
+ @classmethod
195
+ def _match_longest_name_phrase(cls, words: list[str]) -> str | None:
196
+ for length in range(min(4, len(words)), 0, -1):
197
+ for start in range(len(words) - length + 1):
198
+ phrase = " ".join(words[start : start + length])
199
+ resolved = cls.resolve_name_phrase(phrase)
200
+ if resolved is not None:
201
+ return resolved
202
+ return None
203
+
204
+ @classmethod
205
+ def require_stash(cls, name: str, line: int) -> None:
206
+ if not cls.is_stash(name):
207
+ from pbark.config.name_hints import NameHints
208
+
209
+ raise BarkError(
210
+ line,
211
+ f"'{name}' is not a not something the dogs can get stuff from or bury in."
212
+ + NameHints.hint_phrase(name, cls.list_stashes()),
213
+ )
214
+
215
+ @classmethod
216
+ def require_pile(cls, name: str, line: int) -> None:
217
+ if not cls.is_pile(name):
218
+ from pbark.config.name_hints import NameHints
219
+
220
+ raise BarkError(
221
+ line,
222
+ f"'{name}' is not a registered pile. wrong spot."
223
+ + NameHints.hint_phrase(name, cls.list_piles()),
224
+ )
225
+
226
+ @classmethod
227
+ def require_breed(cls, name: str, line: int) -> None:
228
+ if not cls.is_breed(name):
229
+ from pbark.config.name_hints import NameHints
230
+
231
+ raise BarkError(
232
+ line,
233
+ f'"{name}" isn\'t a breed we know about. Garbage bin mix??'
234
+ + NameHints.hint_phrase(name, cls.list_breeds()),
235
+ )
236
+
237
+
238
+ class IllegalStateError(RuntimeError):
239
+ pass