slash-command 0.2.0__py3-none-any.whl
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.
slash_command/_parser.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
_NAME_RE = re.compile(r"[a-zA-Z][a-zA-Z0-9_-]*")
|
|
7
|
+
_WS = frozenset(" \t\n\r")
|
|
8
|
+
_HWS = frozenset(" \t") # horizontal whitespace only
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Text:
|
|
13
|
+
content: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Command:
|
|
18
|
+
name: str
|
|
19
|
+
args: list[str] = field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Document = list[Text | Command]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _word_boundary(text: str, pos: int) -> bool:
|
|
26
|
+
return pos == 0 or text[pos - 1] in _WS
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _parse_args(text: str, pos: int) -> tuple[list[str], int]:
|
|
30
|
+
"""Return (args, new_pos) starting right after the command name."""
|
|
31
|
+
n = len(text)
|
|
32
|
+
|
|
33
|
+
# Must have at least one horizontal space before args.
|
|
34
|
+
if pos >= n or text[pos] not in _HWS:
|
|
35
|
+
return [], pos
|
|
36
|
+
|
|
37
|
+
# Skip horizontal whitespace.
|
|
38
|
+
while pos < n and text[pos] in _HWS:
|
|
39
|
+
pos += 1
|
|
40
|
+
|
|
41
|
+
if pos >= n or text[pos] == "\n":
|
|
42
|
+
return [], pos
|
|
43
|
+
|
|
44
|
+
# Quoted form: first non-space char is a quote.
|
|
45
|
+
if text[pos] in "\"'":
|
|
46
|
+
args: list[str] = []
|
|
47
|
+
start = pos # save for unterminated-quote backtrack
|
|
48
|
+
while pos < n and text[pos] in "\"'":
|
|
49
|
+
quote = text[pos]
|
|
50
|
+
pos += 1
|
|
51
|
+
end = text.find(quote, pos)
|
|
52
|
+
if end == -1:
|
|
53
|
+
# Unterminated quote — backtrack, emit command with no args.
|
|
54
|
+
return [], start
|
|
55
|
+
args.append(text[pos:end])
|
|
56
|
+
pos = end + 1
|
|
57
|
+
while pos < n and text[pos] in _HWS:
|
|
58
|
+
pos += 1
|
|
59
|
+
return args, pos
|
|
60
|
+
|
|
61
|
+
# Raw form: scan to EOL, stop early at the next command trigger.
|
|
62
|
+
raw_start = pos
|
|
63
|
+
while pos < n and text[pos] != "\n":
|
|
64
|
+
if text[pos] == "/" and _word_boundary(text, pos):
|
|
65
|
+
m = _NAME_RE.match(text, pos + 1)
|
|
66
|
+
if m:
|
|
67
|
+
break
|
|
68
|
+
pos += 1
|
|
69
|
+
|
|
70
|
+
raw = text[raw_start:pos].rstrip()
|
|
71
|
+
if raw:
|
|
72
|
+
return [raw], pos # pos is EOL, EOF, or start of next command trigger
|
|
73
|
+
return [], pos
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _try_command(text: str, pos: int) -> tuple[Command, int] | None:
|
|
77
|
+
"""Try to parse a command at pos (text[pos] must be '/')."""
|
|
78
|
+
m = _NAME_RE.match(text, pos + 1)
|
|
79
|
+
if not m:
|
|
80
|
+
return None
|
|
81
|
+
name = m.group()
|
|
82
|
+
args, end = _parse_args(text, m.end())
|
|
83
|
+
return Command(name, args), end
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def parse(text: str) -> Document:
|
|
87
|
+
"""Parse *text* into an ordered sequence of Text and Command segments."""
|
|
88
|
+
segments: Document = []
|
|
89
|
+
pos = 0
|
|
90
|
+
text_start = 0
|
|
91
|
+
n = len(text)
|
|
92
|
+
|
|
93
|
+
while pos < n:
|
|
94
|
+
if text[pos] == "/" and _word_boundary(text, pos):
|
|
95
|
+
result = _try_command(text, pos)
|
|
96
|
+
if result is not None:
|
|
97
|
+
cmd, end = result
|
|
98
|
+
if pos > text_start:
|
|
99
|
+
segments.append(Text(text[text_start:pos]))
|
|
100
|
+
segments.append(cmd)
|
|
101
|
+
text_start = pos = end
|
|
102
|
+
continue
|
|
103
|
+
pos += 1
|
|
104
|
+
|
|
105
|
+
if text_start < n:
|
|
106
|
+
segments.append(Text(text[text_start:]))
|
|
107
|
+
|
|
108
|
+
return segments
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: slash_command
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Slash Command Language (SCL) — Python implementation
|
|
5
|
+
Project-URL: Homepage, https://github.com/monperrus/slash_command
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/monperrus/slash_command/issues
|
|
7
|
+
Author-email: Martin Monperrus <martin2020+claude@monperrus.net>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: command,island-parsing,llm,parser,slash
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Text Processing
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# slash_command
|
|
28
|
+
|
|
29
|
+
Python implementation of the **Slash Command Language (SCL)** — a minimal
|
|
30
|
+
syntax for embedding executable commands within natural-language text.
|
|
31
|
+
|
|
32
|
+
See the [SCL specification](https://github.com/monperrus/slash_command) for the full
|
|
33
|
+
grammar, normative algorithm, and conformance criteria.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install slash_command
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from slash_command import parse, Text, Command
|
|
45
|
+
|
|
46
|
+
doc = parse('Please /search "quantum computing" and then /exit')
|
|
47
|
+
|
|
48
|
+
for segment in doc:
|
|
49
|
+
if isinstance(segment, Command):
|
|
50
|
+
print(f"command: {segment.name!r}, args: {segment.args}")
|
|
51
|
+
else:
|
|
52
|
+
print(f"text: {segment.content!r}")
|
|
53
|
+
```
|
|
54
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
slash_command/__init__.py,sha256=-V2dToZinaMfAcUhjEnLwzMWloFgwh8yXACco2jg-oI,163
|
|
2
|
+
slash_command/_parser.py,sha256=BWA6gQM86RhCf3CgZGlliwi669mdSM6ZQUQmnqwQCws,2973
|
|
3
|
+
slash_command-0.2.0.dist-info/METADATA,sha256=rLfuU9wWSXd9p0GDOVflx_k0ON-R-dIYDzcYUc4E8JA,1701
|
|
4
|
+
slash_command-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
5
|
+
slash_command-0.2.0.dist-info/licenses/LICENSE,sha256=cZcVt0H9cnb7yEUhuaOjEY44tOenoE73-XlnbE3fTG8,1073
|
|
6
|
+
slash_command-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Martin Monperrus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|