avrae-ls 0.3.1__tar.gz → 0.4.1__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.
- avrae_ls-0.4.1/PKG-INFO +86 -0
- avrae_ls-0.4.1/README.md +67 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/pyproject.toml +1 -1
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls/alias_preview.py +175 -9
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls/api.py +229 -229
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls/argparser.py +16 -3
- avrae_ls-0.4.1/src/avrae_ls/code_actions.py +282 -0
- avrae_ls-0.4.1/src/avrae_ls/codes.py +3 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls/completions.py +489 -78
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls/config.py +61 -2
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls/context.py +62 -1
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls/diagnostics.py +267 -5
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls/parser.py +7 -2
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls/runtime.py +94 -15
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls/server.py +52 -6
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls/signature_help.py +56 -5
- avrae_ls-0.4.1/src/avrae_ls/symbols.py +266 -0
- avrae_ls-0.4.1/src/avrae_ls.egg-info/PKG-INFO +86 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls.egg-info/SOURCES.txt +6 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/tests/test_alias_preview.py +60 -6
- avrae_ls-0.4.1/tests/test_api.py +105 -0
- avrae_ls-0.4.1/tests/test_argparser_unit.py +42 -0
- avrae_ls-0.4.1/tests/test_code_actions.py +153 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/tests/test_completions.py +62 -11
- avrae_ls-0.4.1/tests/test_config_env.py +65 -0
- avrae_ls-0.4.1/tests/test_diagnostics.py +128 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/tests/test_hover.py +17 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/tests/test_runtime.py +125 -2
- avrae_ls-0.4.1/tests/test_signature_help.py +135 -0
- avrae_ls-0.4.1/tests/test_symbols.py +112 -0
- avrae_ls-0.3.1/PKG-INFO +0 -47
- avrae_ls-0.3.1/README.md +0 -28
- avrae_ls-0.3.1/src/avrae_ls/symbols.py +0 -150
- avrae_ls-0.3.1/src/avrae_ls.egg-info/PKG-INFO +0 -47
- avrae_ls-0.3.1/tests/test_api.py +0 -50
- avrae_ls-0.3.1/tests/test_diagnostics.py +0 -157
- avrae_ls-0.3.1/tests/test_symbols.py +0 -21
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/LICENSE +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/setup.cfg +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls/__init__.py +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls/__main__.py +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls/argument_parsing.py +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls/cvars.py +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls/dice.py +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls.egg-info/dependency_links.txt +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls.egg-info/entry_points.txt +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls.egg-info/requires.txt +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/avrae_ls.egg-info/top_level.txt +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/draconic/LICENSE +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/draconic/__init__.py +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/draconic/exceptions.py +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/draconic/helpers.py +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/draconic/interpreter.py +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/draconic/string.py +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/draconic/types.py +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/draconic/utils.py +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/src/draconic/versions.py +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/tests/test_argument_parsing.py +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/tests/test_cvars.py +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/tests/test_gvars.py +0 -0
- {avrae_ls-0.3.1 → avrae_ls-0.4.1}/tests/test_runtime_dice.py +0 -0
avrae_ls-0.4.1/PKG-INFO
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: avrae-ls
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Language server for Avrae draconic aliases
|
|
5
|
+
Author: 1drturtle
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: pygls>=1.3.1
|
|
10
|
+
Requires-Dist: lsprotocol>=2023.0.1
|
|
11
|
+
Requires-Dist: httpx>=0.27
|
|
12
|
+
Requires-Dist: d20>=1.1.2
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=8.3; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-cov>=7.0.0; extra == "dev"
|
|
17
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Avrae Draconic Alias Language Server
|
|
21
|
+
|
|
22
|
+
Language Server Protocol (LSP) implementation targeting Avrae-style draconic aliases. It provides syntax/semantic diagnostics, a mocked execution command, and a thin configuration layer driven by a workspace `.avraels.json` file. Credit to Avrae team for all code yoinked!
|
|
23
|
+
|
|
24
|
+
## Install (released package)
|
|
25
|
+
|
|
26
|
+
- CLI/server via `uv tool` (preferred): `uv tool install avrae-ls` then `avrae-ls --help` to see stdio/TCP options (same as `python -m avrae_ls`). The VS Code extension uses this invocation by default. The draconic interpreter is vendored, so no Git deps are needed.
|
|
27
|
+
|
|
28
|
+
## VS Code extension (released)
|
|
29
|
+
|
|
30
|
+
- Install from VSIX: download `avrae-ls-client.vsix` from the GitHub releases page, then in VS Code run “Extensions: Install from VSIX” and select the file.
|
|
31
|
+
- Open your alias workspace; commands like `Avrae: Show Alias Preview` and `Avrae: Run Alias` will be available.
|
|
32
|
+
|
|
33
|
+
## Developing locally
|
|
34
|
+
|
|
35
|
+
- Prereqs: [uv](https://github.com/astral-sh/uv) and Node.js.
|
|
36
|
+
- Install deps: `uv sync --all-extras` then `make vscode-deps`.
|
|
37
|
+
- Build everything locally: `make package` (wheel + VSIX in `dist/`).
|
|
38
|
+
- Run tests/lint: `make check`.
|
|
39
|
+
- Run via uv tool from source: `uv tool install --from . avrae-ls`.
|
|
40
|
+
- Run diagnostics for a single file (stdout + stderr logs): `avrae-ls --analyze path/to/alias.txt --log-level DEBUG`.
|
|
41
|
+
|
|
42
|
+
## How to test
|
|
43
|
+
|
|
44
|
+
- Quick check (ruff + pytest): `make check` (uses `uv run ruff` and `uv run pytest` under the hood).
|
|
45
|
+
- Lint only: `make lint` or `uv run ruff check src tests`.
|
|
46
|
+
- Tests only (with coverage): `make test` or `uv run pytest tests --cov=src`.
|
|
47
|
+
- CLI smoke test without installing: `uv run python -m avrae_ls --analyze path/to/alias.txt`.
|
|
48
|
+
|
|
49
|
+
## Runtime differences (mock vs. live Avrae)
|
|
50
|
+
|
|
51
|
+
- Mock execution never writes back to Avrae: cvar/uvar/gvar mutations only live for the current run and reset before the next.
|
|
52
|
+
- Network is limited to gvar fetches (when `enableGvarFetch` is true) and `verify_signature`; other Avrae/Discord calls are replaced with mocked context data from `.avraels.json`.
|
|
53
|
+
- `get_gvar`/`using` values are pulled from local var files first; remote fetches go to `https://api.avrae.io/customizations/gvars/<id>` (or your `avraeService.baseUrl`) using `avraeService.token` and are cached for the session.
|
|
54
|
+
- `signature()` returns a mock string (`mock-signature:<int>`). `verify_signature()` POSTs to `/bot/signature/verify`, respects `verifySignatureTimeout`/`verifySignatureRetries`, reuses the last successful response per signature, and includes `avraeService.token` if present.
|
|
55
|
+
|
|
56
|
+
## Troubleshooting gvar fetch / verify_signature
|
|
57
|
+
|
|
58
|
+
- `get_gvar` returns `None` or `using(...)` raises `ModuleNotFoundError`: ensure the workspace `.avraels.json` sets `enableGvarFetch: true`, includes a valid `avraeService.token`, or seed the gvar in a var file referenced by `varFiles`.
|
|
59
|
+
- HTTP 401/403/404 from fetch/verify calls: check the token (401/403) and the gvar/signature id (404). Override `avraeService.baseUrl` if you mirror the API.
|
|
60
|
+
- Slow or flaky calls: tune `verifySignatureTimeout` / `verifySignatureRetries`, or disable remote fetches by flipping `enableGvarFetch` off to rely purely on local vars.
|
|
61
|
+
|
|
62
|
+
## Other editors (stdio)
|
|
63
|
+
|
|
64
|
+
- Any client can launch the server with stdio: `avrae-ls --stdio` (flag accepted for client compatibility) or `python -m avrae_ls`. The server will also auto-discover `.avraels.json` in parent folders.
|
|
65
|
+
- Neovim (nvim-lspconfig example):
|
|
66
|
+
```lua
|
|
67
|
+
require("lspconfig").avraels.setup({
|
|
68
|
+
cmd = { "avrae-ls", "--stdio" },
|
|
69
|
+
filetypes = { "avrae" },
|
|
70
|
+
root_dir = require("lspconfig.util").root_pattern(".avraels.json", ".git"),
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
- Emacs (lsp-mode snippet):
|
|
74
|
+
```elisp
|
|
75
|
+
(lsp-register-client
|
|
76
|
+
(make-lsp-client
|
|
77
|
+
:new-connection (lsp-stdio-connection '("avrae-ls" "--stdio"))
|
|
78
|
+
:major-modes '(fundamental-mode) ;; bind to your Avrae alias mode
|
|
79
|
+
:server-id 'avrae-ls))
|
|
80
|
+
```
|
|
81
|
+
- VS Code commands to mirror: `Avrae: Run Alias (Mock)`, `Avrae: Show Alias Preview`, and `Avrae: Reload Workspace Config` run against the same server binary.
|
|
82
|
+
|
|
83
|
+
## Releasing (maintainers)
|
|
84
|
+
|
|
85
|
+
1. Bump `pyproject.toml` / `package.json`
|
|
86
|
+
2. Create Github release
|
avrae_ls-0.4.1/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Avrae Draconic Alias Language Server
|
|
2
|
+
|
|
3
|
+
Language Server Protocol (LSP) implementation targeting Avrae-style draconic aliases. It provides syntax/semantic diagnostics, a mocked execution command, and a thin configuration layer driven by a workspace `.avraels.json` file. Credit to Avrae team for all code yoinked!
|
|
4
|
+
|
|
5
|
+
## Install (released package)
|
|
6
|
+
|
|
7
|
+
- CLI/server via `uv tool` (preferred): `uv tool install avrae-ls` then `avrae-ls --help` to see stdio/TCP options (same as `python -m avrae_ls`). The VS Code extension uses this invocation by default. The draconic interpreter is vendored, so no Git deps are needed.
|
|
8
|
+
|
|
9
|
+
## VS Code extension (released)
|
|
10
|
+
|
|
11
|
+
- Install from VSIX: download `avrae-ls-client.vsix` from the GitHub releases page, then in VS Code run “Extensions: Install from VSIX” and select the file.
|
|
12
|
+
- Open your alias workspace; commands like `Avrae: Show Alias Preview` and `Avrae: Run Alias` will be available.
|
|
13
|
+
|
|
14
|
+
## Developing locally
|
|
15
|
+
|
|
16
|
+
- Prereqs: [uv](https://github.com/astral-sh/uv) and Node.js.
|
|
17
|
+
- Install deps: `uv sync --all-extras` then `make vscode-deps`.
|
|
18
|
+
- Build everything locally: `make package` (wheel + VSIX in `dist/`).
|
|
19
|
+
- Run tests/lint: `make check`.
|
|
20
|
+
- Run via uv tool from source: `uv tool install --from . avrae-ls`.
|
|
21
|
+
- Run diagnostics for a single file (stdout + stderr logs): `avrae-ls --analyze path/to/alias.txt --log-level DEBUG`.
|
|
22
|
+
|
|
23
|
+
## How to test
|
|
24
|
+
|
|
25
|
+
- Quick check (ruff + pytest): `make check` (uses `uv run ruff` and `uv run pytest` under the hood).
|
|
26
|
+
- Lint only: `make lint` or `uv run ruff check src tests`.
|
|
27
|
+
- Tests only (with coverage): `make test` or `uv run pytest tests --cov=src`.
|
|
28
|
+
- CLI smoke test without installing: `uv run python -m avrae_ls --analyze path/to/alias.txt`.
|
|
29
|
+
|
|
30
|
+
## Runtime differences (mock vs. live Avrae)
|
|
31
|
+
|
|
32
|
+
- Mock execution never writes back to Avrae: cvar/uvar/gvar mutations only live for the current run and reset before the next.
|
|
33
|
+
- Network is limited to gvar fetches (when `enableGvarFetch` is true) and `verify_signature`; other Avrae/Discord calls are replaced with mocked context data from `.avraels.json`.
|
|
34
|
+
- `get_gvar`/`using` values are pulled from local var files first; remote fetches go to `https://api.avrae.io/customizations/gvars/<id>` (or your `avraeService.baseUrl`) using `avraeService.token` and are cached for the session.
|
|
35
|
+
- `signature()` returns a mock string (`mock-signature:<int>`). `verify_signature()` POSTs to `/bot/signature/verify`, respects `verifySignatureTimeout`/`verifySignatureRetries`, reuses the last successful response per signature, and includes `avraeService.token` if present.
|
|
36
|
+
|
|
37
|
+
## Troubleshooting gvar fetch / verify_signature
|
|
38
|
+
|
|
39
|
+
- `get_gvar` returns `None` or `using(...)` raises `ModuleNotFoundError`: ensure the workspace `.avraels.json` sets `enableGvarFetch: true`, includes a valid `avraeService.token`, or seed the gvar in a var file referenced by `varFiles`.
|
|
40
|
+
- HTTP 401/403/404 from fetch/verify calls: check the token (401/403) and the gvar/signature id (404). Override `avraeService.baseUrl` if you mirror the API.
|
|
41
|
+
- Slow or flaky calls: tune `verifySignatureTimeout` / `verifySignatureRetries`, or disable remote fetches by flipping `enableGvarFetch` off to rely purely on local vars.
|
|
42
|
+
|
|
43
|
+
## Other editors (stdio)
|
|
44
|
+
|
|
45
|
+
- Any client can launch the server with stdio: `avrae-ls --stdio` (flag accepted for client compatibility) or `python -m avrae_ls`. The server will also auto-discover `.avraels.json` in parent folders.
|
|
46
|
+
- Neovim (nvim-lspconfig example):
|
|
47
|
+
```lua
|
|
48
|
+
require("lspconfig").avraels.setup({
|
|
49
|
+
cmd = { "avrae-ls", "--stdio" },
|
|
50
|
+
filetypes = { "avrae" },
|
|
51
|
+
root_dir = require("lspconfig.util").root_pattern(".avraels.json", ".git"),
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
- Emacs (lsp-mode snippet):
|
|
55
|
+
```elisp
|
|
56
|
+
(lsp-register-client
|
|
57
|
+
(make-lsp-client
|
|
58
|
+
:new-connection (lsp-stdio-connection '("avrae-ls" "--stdio"))
|
|
59
|
+
:major-modes '(fundamental-mode) ;; bind to your Avrae alias mode
|
|
60
|
+
:server-id 'avrae-ls))
|
|
61
|
+
```
|
|
62
|
+
- VS Code commands to mirror: `Avrae: Run Alias (Mock)`, `Avrae: Show Alias Preview`, and `Avrae: Reload Workspace Config` run against the same server binary.
|
|
63
|
+
|
|
64
|
+
## Releasing (maintainers)
|
|
65
|
+
|
|
66
|
+
1. Bump `pyproject.toml` / `package.json`
|
|
67
|
+
2. Create Github release
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
import shlex
|
|
5
|
-
from dataclasses import dataclass
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
6
|
from typing import Any, Optional, Tuple
|
|
7
7
|
|
|
8
8
|
from .parser import DRACONIC_RE
|
|
@@ -19,6 +19,45 @@ class RenderedAlias:
|
|
|
19
19
|
last_value: Any | None = None
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
@dataclass
|
|
23
|
+
class EmbedFieldPreview:
|
|
24
|
+
name: str
|
|
25
|
+
value: str
|
|
26
|
+
inline: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class EmbedPreview:
|
|
31
|
+
title: str | None = None
|
|
32
|
+
description: str | None = None
|
|
33
|
+
footer: str | None = None
|
|
34
|
+
thumbnail: str | None = None
|
|
35
|
+
image: str | None = None
|
|
36
|
+
color: str | None = None
|
|
37
|
+
timeout: int | None = None
|
|
38
|
+
fields: list[EmbedFieldPreview] = field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> dict[str, Any]:
|
|
41
|
+
return {
|
|
42
|
+
"title": self.title,
|
|
43
|
+
"description": self.description,
|
|
44
|
+
"footer": self.footer,
|
|
45
|
+
"thumbnail": self.thumbnail,
|
|
46
|
+
"image": self.image,
|
|
47
|
+
"color": self.color,
|
|
48
|
+
"timeout": self.timeout,
|
|
49
|
+
"fields": [asdict(f) for f in self.fields],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class SimulatedCommand:
|
|
55
|
+
preview: str | None
|
|
56
|
+
command_name: str | None
|
|
57
|
+
validation_error: str | None
|
|
58
|
+
embed: EmbedPreview | None = None
|
|
59
|
+
|
|
60
|
+
|
|
22
61
|
def _strip_alias_header(text: str) -> str:
|
|
23
62
|
lines = text.splitlines()
|
|
24
63
|
if lines and lines[0].lstrip().startswith("!alias"):
|
|
@@ -82,6 +121,58 @@ def validate_embed_payload(payload: str) -> Tuple[bool, str | None]:
|
|
|
82
121
|
return _validate_embed_flags(text)
|
|
83
122
|
|
|
84
123
|
|
|
124
|
+
def parse_embed_payload(payload: str) -> EmbedPreview:
|
|
125
|
+
"""Parse an embed payload into a structured preview object."""
|
|
126
|
+
tokens = shlex.split(payload.strip())
|
|
127
|
+
preview = EmbedPreview()
|
|
128
|
+
|
|
129
|
+
i = 0
|
|
130
|
+
while i < len(tokens):
|
|
131
|
+
tok = tokens[i]
|
|
132
|
+
if not tok.startswith("-"):
|
|
133
|
+
i += 1
|
|
134
|
+
continue
|
|
135
|
+
key = tok.lower()
|
|
136
|
+
next_val = tokens[i + 1] if i + 1 < len(tokens) else None
|
|
137
|
+
if key == "-title":
|
|
138
|
+
preview.title = next_val or ""
|
|
139
|
+
i += 2
|
|
140
|
+
continue
|
|
141
|
+
if key == "-desc":
|
|
142
|
+
preview.description = next_val or ""
|
|
143
|
+
i += 2
|
|
144
|
+
continue
|
|
145
|
+
if key == "-footer":
|
|
146
|
+
preview.footer = next_val or ""
|
|
147
|
+
i += 2
|
|
148
|
+
continue
|
|
149
|
+
if key == "-thumb":
|
|
150
|
+
preview.thumbnail = next_val or ""
|
|
151
|
+
i += 2
|
|
152
|
+
continue
|
|
153
|
+
if key == "-image":
|
|
154
|
+
preview.image = next_val or ""
|
|
155
|
+
i += 2
|
|
156
|
+
continue
|
|
157
|
+
if key == "-color":
|
|
158
|
+
preview.color = _normalize_color(next_val)
|
|
159
|
+
i += 2 if next_val is not None else 1
|
|
160
|
+
continue
|
|
161
|
+
if key == "-t":
|
|
162
|
+
preview.timeout = _parse_timeout(next_val)
|
|
163
|
+
i += 2
|
|
164
|
+
continue
|
|
165
|
+
if key == "-f":
|
|
166
|
+
field = _parse_field_value(next_val)
|
|
167
|
+
if field:
|
|
168
|
+
preview.fields.append(field)
|
|
169
|
+
i += 2
|
|
170
|
+
continue
|
|
171
|
+
i += 1
|
|
172
|
+
|
|
173
|
+
return preview
|
|
174
|
+
|
|
175
|
+
|
|
85
176
|
def _validate_embed_flags(text: str) -> Tuple[bool, str | None]:
|
|
86
177
|
"""Validate embed flags according to Avrae's help text."""
|
|
87
178
|
if not text:
|
|
@@ -164,17 +255,92 @@ def _validate_timeout_arg(value: str | None) -> Tuple[bool, str | None, int]:
|
|
|
164
255
|
return True, None, consumed
|
|
165
256
|
|
|
166
257
|
|
|
167
|
-
def
|
|
258
|
+
def _parse_timeout(value: str | None) -> int | None:
|
|
259
|
+
if value is None:
|
|
260
|
+
return None
|
|
261
|
+
try:
|
|
262
|
+
return int(value)
|
|
263
|
+
except (TypeError, ValueError):
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _normalize_color(value: str | None) -> str | None:
|
|
268
|
+
if value is None:
|
|
269
|
+
return None
|
|
270
|
+
if not value:
|
|
271
|
+
return None
|
|
272
|
+
match = re.match(r"^(?:#|0x)?([0-9a-fA-F]{6})$", value)
|
|
273
|
+
if not match:
|
|
274
|
+
return value
|
|
275
|
+
return f"#{match.group(1)}"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _parse_field_value(value: str | None) -> EmbedFieldPreview | None:
|
|
279
|
+
if value is None:
|
|
280
|
+
return None
|
|
281
|
+
parts = value.split("|")
|
|
282
|
+
if len(parts) < 2:
|
|
283
|
+
return None
|
|
284
|
+
inline_flag = parts[2].lower() == "inline" if len(parts) == 3 else False
|
|
285
|
+
return EmbedFieldPreview(name=parts[0], value=parts[1], inline=inline_flag)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def simulate_command(command: str) -> SimulatedCommand:
|
|
168
289
|
"""Very small shim to preview common commands."""
|
|
169
|
-
text = command.strip()
|
|
290
|
+
text = _strip_alias_header(command).strip()
|
|
170
291
|
if not text:
|
|
171
|
-
return None, None, None
|
|
172
|
-
head,
|
|
173
|
-
|
|
292
|
+
return SimulatedCommand(None, None, None, None)
|
|
293
|
+
head, payload = _extract_command_head_and_payload(text)
|
|
294
|
+
if not head:
|
|
295
|
+
return SimulatedCommand(None, None, None, None)
|
|
174
296
|
lowered = head.lower()
|
|
175
297
|
if lowered == "echo":
|
|
176
|
-
return payload, "echo", None
|
|
298
|
+
return SimulatedCommand(payload, "echo", None, None)
|
|
177
299
|
if lowered == "embed":
|
|
178
300
|
valid, error = validate_embed_payload(payload)
|
|
179
|
-
|
|
180
|
-
|
|
301
|
+
embed_preview = parse_embed_payload(payload) if valid else None
|
|
302
|
+
return SimulatedCommand(payload, "embed", error, embed_preview)
|
|
303
|
+
if head.startswith("-") and _is_embed_flag(head):
|
|
304
|
+
payload = text
|
|
305
|
+
valid, error = validate_embed_payload(payload)
|
|
306
|
+
embed_preview = parse_embed_payload(payload) if valid else None
|
|
307
|
+
return SimulatedCommand(payload, "embed", error, embed_preview)
|
|
308
|
+
return SimulatedCommand(None, head, None, None)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _extract_command_head_and_payload(text: str) -> tuple[str | None, str]:
|
|
312
|
+
"""Prefer the first non-empty line; fall back to any embed line later."""
|
|
313
|
+
lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
|
|
314
|
+
if not lines:
|
|
315
|
+
return None, ""
|
|
316
|
+
head, payload = _split_head_and_payload_from_line(lines[0])
|
|
317
|
+
if _is_embed_flag(head):
|
|
318
|
+
# Treat the entire payload (including the head line) as embed flags so multiple lines are preserved.
|
|
319
|
+
return head, "\n".join(lines)
|
|
320
|
+
if head and head.lower() in ("embed", "echo"):
|
|
321
|
+
return head, _merge_payload(payload, lines[1:])
|
|
322
|
+
for idx, line in enumerate(lines[1:], start=1):
|
|
323
|
+
possible_head, possible_payload = _split_head_and_payload_from_line(line)
|
|
324
|
+
if possible_head and (possible_head.lower() == "embed" or _is_embed_flag(possible_head)):
|
|
325
|
+
return possible_head, _merge_payload(possible_payload, lines[idx + 1 :])
|
|
326
|
+
return head, _merge_payload(payload, lines[1:])
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _split_head_and_payload_from_line(line: str) -> tuple[str | None, str]:
|
|
330
|
+
if not line:
|
|
331
|
+
return None, ""
|
|
332
|
+
parts = line.split(maxsplit=1)
|
|
333
|
+
head = parts[0]
|
|
334
|
+
payload = parts[1] if len(parts) > 1 else ""
|
|
335
|
+
return head, payload
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _merge_payload(first_payload: str, trailing_lines: list[str]) -> str:
|
|
339
|
+
payload = first_payload
|
|
340
|
+
if trailing_lines:
|
|
341
|
+
payload = (payload + "\n" if payload else "") + "\n".join(trailing_lines)
|
|
342
|
+
return payload
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _is_embed_flag(flag: str) -> bool:
|
|
346
|
+
return flag.lower() in {"-title", "-desc", "-thumb", "-image", "-footer", "-f", "-color", "-t"}
|