sphinx-dynamic-command-builder 0.1.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.
- sphinx_dynamic_command_builder/__init__.py +33 -0
- sphinx_dynamic_command_builder/directive.py +147 -0
- sphinx_dynamic_command_builder/static/sphinx-dynamic-command-builder.css +105 -0
- sphinx_dynamic_command_builder/static/sphinx-dynamic-command-builder.js +147 -0
- sphinx_dynamic_command_builder-0.1.0.dist-info/METADATA +110 -0
- sphinx_dynamic_command_builder-0.1.0.dist-info/RECORD +8 -0
- sphinx_dynamic_command_builder-0.1.0.dist-info/WHEEL +4 -0
- sphinx_dynamic_command_builder-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from sphinx.util.fileutil import copy_asset
|
|
6
|
+
|
|
7
|
+
from .directive import DynamicCommandDirective
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
STATIC_DIR = Path(__file__).parent / "static"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _copy_static_assets(app, exc):
|
|
15
|
+
if exc is not None or app.builder.format != "html":
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
static_out = Path(app.outdir) / "_static"
|
|
19
|
+
copy_asset(str(STATIC_DIR / "sphinx-dynamic-command-builder.css"), str(static_out))
|
|
20
|
+
copy_asset(str(STATIC_DIR / "sphinx-dynamic-command-builder.js"), str(static_out))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def setup(app):
|
|
24
|
+
app.add_directive("dynamic-command", DynamicCommandDirective)
|
|
25
|
+
app.add_css_file("sphinx-dynamic-command-builder.css")
|
|
26
|
+
app.add_js_file("sphinx-dynamic-command-builder.js")
|
|
27
|
+
app.connect("build-finished", _copy_static_assets)
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
"version": __version__,
|
|
31
|
+
"parallel_read_safe": True,
|
|
32
|
+
"parallel_write_safe": True,
|
|
33
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from html import escape
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from docutils import nodes
|
|
7
|
+
from docutils.parsers.rst import Directive
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _as_str(value: Any, field: str) -> str:
|
|
12
|
+
if value is None:
|
|
13
|
+
return ""
|
|
14
|
+
if not isinstance(value, str):
|
|
15
|
+
raise ValueError(f"{field} must be a string")
|
|
16
|
+
return value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _choice_button(group_key: str, group_default: str, choice: dict[str, Any]) -> str:
|
|
20
|
+
label = _as_str(choice.get("label"), "choice.label")
|
|
21
|
+
value = _as_str(choice.get("value"), "choice.value")
|
|
22
|
+
if not label:
|
|
23
|
+
raise ValueError("choice.label is required")
|
|
24
|
+
if not value:
|
|
25
|
+
raise ValueError("choice.value is required")
|
|
26
|
+
|
|
27
|
+
attrs = {
|
|
28
|
+
"class": "sdc-button",
|
|
29
|
+
"type": "button",
|
|
30
|
+
"data-sdc-option": group_key,
|
|
31
|
+
"data-sdc-value": value,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
command_base = _as_str(choice.get("base"), "choice.base")
|
|
35
|
+
env = _as_str(choice.get("env"), "choice.env")
|
|
36
|
+
args = _as_str(choice.get("args"), "choice.args")
|
|
37
|
+
if command_base:
|
|
38
|
+
attrs["data-sdc-base"] = command_base
|
|
39
|
+
if env:
|
|
40
|
+
attrs["data-sdc-env"] = env
|
|
41
|
+
if args:
|
|
42
|
+
attrs["data-sdc-args"] = args
|
|
43
|
+
if value == group_default:
|
|
44
|
+
attrs["data-sdc-default"] = "true"
|
|
45
|
+
|
|
46
|
+
rendered_attrs = " ".join(
|
|
47
|
+
f'{escape(name)}="{escape(attr_value, quote=True)}"'
|
|
48
|
+
for name, attr_value in attrs.items()
|
|
49
|
+
)
|
|
50
|
+
return f"<button {rendered_attrs}>{escape(label)}</button>"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _option_group(group: dict[str, Any]) -> str:
|
|
54
|
+
label = _as_str(group.get("label"), "options.label")
|
|
55
|
+
key = _as_str(group.get("key"), "options.key")
|
|
56
|
+
default = _as_str(group.get("default"), "options.default")
|
|
57
|
+
choices = group.get("choices")
|
|
58
|
+
|
|
59
|
+
if not label:
|
|
60
|
+
raise ValueError("options.label is required")
|
|
61
|
+
if not key:
|
|
62
|
+
raise ValueError("options.key is required")
|
|
63
|
+
if not isinstance(choices, list) or not choices:
|
|
64
|
+
raise ValueError(f"options.{key}.choices must be a non-empty list")
|
|
65
|
+
|
|
66
|
+
if not default:
|
|
67
|
+
default = _as_str(choices[0].get("value"), "choice.value")
|
|
68
|
+
|
|
69
|
+
buttons = "\n".join(_choice_button(key, default, choice) for choice in choices)
|
|
70
|
+
return f"""
|
|
71
|
+
<div class="sdc-group">
|
|
72
|
+
<div class="sdc-label">{escape(label)}</div>
|
|
73
|
+
<div class="sdc-buttons">
|
|
74
|
+
{buttons}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _format_attrs(config: dict[str, Any]) -> dict[str, str]:
|
|
81
|
+
format_config = config.get("format", {})
|
|
82
|
+
if format_config is None:
|
|
83
|
+
format_config = {}
|
|
84
|
+
if not isinstance(format_config, dict):
|
|
85
|
+
raise ValueError("format must be a YAML mapping")
|
|
86
|
+
|
|
87
|
+
line_break = _as_str(format_config.get("line_break", "options"), "format.line_break")
|
|
88
|
+
if line_break not in {"options", "none"}:
|
|
89
|
+
raise ValueError("format.line_break must be one of: options, none")
|
|
90
|
+
|
|
91
|
+
indent = _as_str(format_config.get("indent", " "), "format.indent")
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
"data-sdc-line-break": line_break,
|
|
95
|
+
"data-sdc-indent": indent,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class DynamicCommandDirective(Directive):
|
|
100
|
+
"""Render a selector-driven command generator from YAML content."""
|
|
101
|
+
|
|
102
|
+
has_content = True
|
|
103
|
+
|
|
104
|
+
def run(self) -> list[nodes.Node]:
|
|
105
|
+
try:
|
|
106
|
+
config = yaml.safe_load("\n".join(self.content)) or {}
|
|
107
|
+
if not isinstance(config, dict):
|
|
108
|
+
raise ValueError("dynamic-command content must be a YAML mapping")
|
|
109
|
+
|
|
110
|
+
base = _as_str(config.get("base"), "base")
|
|
111
|
+
if not base:
|
|
112
|
+
raise ValueError("base is required")
|
|
113
|
+
|
|
114
|
+
groups = config.get("options")
|
|
115
|
+
if not isinstance(groups, list) or not groups:
|
|
116
|
+
raise ValueError("options must be a non-empty list")
|
|
117
|
+
|
|
118
|
+
command_label = _as_str(
|
|
119
|
+
config.get("command_label", "Generated command"), "command_label"
|
|
120
|
+
)
|
|
121
|
+
format_attrs = _format_attrs(config)
|
|
122
|
+
rendered_format_attrs = " ".join(
|
|
123
|
+
f'{escape(name)}="{escape(value, quote=True)}"'
|
|
124
|
+
for name, value in format_attrs.items()
|
|
125
|
+
)
|
|
126
|
+
rendered_groups = "\n".join(_option_group(group) for group in groups)
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
error = self.state_machine.reporter.error(
|
|
129
|
+
f"dynamic-command: {exc}",
|
|
130
|
+
line=self.lineno,
|
|
131
|
+
)
|
|
132
|
+
return [error]
|
|
133
|
+
|
|
134
|
+
html = f"""
|
|
135
|
+
<div class="sdc-card">
|
|
136
|
+
<div data-sdc>
|
|
137
|
+
<div class="sdc-controls">
|
|
138
|
+
{rendered_groups}
|
|
139
|
+
</div>
|
|
140
|
+
<div class="sdc-command">
|
|
141
|
+
<div class="sdc-command-label">{escape(command_label)}</div>
|
|
142
|
+
<pre><code class="language-bash" data-sdc-output data-sdc-base="{escape(base, quote=True)}" {rendered_format_attrs}></code></pre>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
"""
|
|
147
|
+
return [nodes.raw("", html, format="html")]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
.sdc-card {
|
|
2
|
+
background: var(--pst-color-background, #fff);
|
|
3
|
+
border: 1px solid var(--pst-color-border, #d8dee4);
|
|
4
|
+
border-radius: 8px;
|
|
5
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 6%);
|
|
6
|
+
margin: 1.5rem 0;
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.sdc-controls {
|
|
11
|
+
background: color-mix(in srgb, var(--pst-color-surface, #f6f8fa) 96%, var(--pst-color-primary, #0969da) 4%);
|
|
12
|
+
border-bottom: 1px solid var(--pst-color-border, #d8dee4);
|
|
13
|
+
display: grid;
|
|
14
|
+
gap: 0;
|
|
15
|
+
padding: 0.35rem 1rem;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.sdc-group {
|
|
19
|
+
align-items: center;
|
|
20
|
+
border-bottom: 1px solid var(--pst-color-border, #d8dee4);
|
|
21
|
+
display: grid;
|
|
22
|
+
gap: 1rem;
|
|
23
|
+
grid-template-columns: minmax(120px, 180px) 1fr;
|
|
24
|
+
padding: 0.65rem 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.sdc-group:last-child {
|
|
28
|
+
border-bottom: 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.sdc-label,
|
|
32
|
+
.sdc-command-label {
|
|
33
|
+
color: var(--pst-color-text-muted, #57606a);
|
|
34
|
+
font-size: 0.75rem;
|
|
35
|
+
font-weight: 600;
|
|
36
|
+
letter-spacing: 0;
|
|
37
|
+
line-height: 1.2;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.sdc-buttons {
|
|
41
|
+
background: var(--pst-color-background, #fff);
|
|
42
|
+
border: 1px solid var(--pst-color-border, #d8dee4);
|
|
43
|
+
border-radius: 7px;
|
|
44
|
+
display: inline-flex;
|
|
45
|
+
flex-wrap: wrap;
|
|
46
|
+
gap: 0.25rem;
|
|
47
|
+
max-width: 100%;
|
|
48
|
+
padding: 0.25rem;
|
|
49
|
+
width: max-content;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.sdc-button {
|
|
53
|
+
background: transparent;
|
|
54
|
+
border: 0;
|
|
55
|
+
border-radius: 5px;
|
|
56
|
+
color: var(--pst-color-text-base, #24292f);
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
font: inherit;
|
|
59
|
+
font-size: 0.9rem;
|
|
60
|
+
line-height: 1.2;
|
|
61
|
+
padding: 0.42rem 0.68rem;
|
|
62
|
+
transition: background-color 120ms ease, color 120ms ease, box-shadow 120ms ease;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.sdc-button.is-selected {
|
|
66
|
+
background: var(--pst-color-primary, #0969da);
|
|
67
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 12%);
|
|
68
|
+
color: var(--pst-color-on-primary, #fff);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.sdc-command {
|
|
72
|
+
background: var(--pst-color-background, #fff);
|
|
73
|
+
padding: 1rem;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.sdc-command-label {
|
|
77
|
+
margin-bottom: 0.5rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.sdc-command pre {
|
|
81
|
+
border: 1px solid var(--pst-color-border, #d8dee4);
|
|
82
|
+
border-radius: 7px;
|
|
83
|
+
margin: 0;
|
|
84
|
+
overflow-x: auto;
|
|
85
|
+
white-space: pre;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.sdc-command code {
|
|
89
|
+
font-size: 0.86rem;
|
|
90
|
+
line-height: 1.45;
|
|
91
|
+
white-space: pre;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@media (max-width: 640px) {
|
|
95
|
+
.sdc-group {
|
|
96
|
+
align-items: start;
|
|
97
|
+
gap: 0.45rem;
|
|
98
|
+
grid-template-columns: 1fr;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.sdc-buttons {
|
|
102
|
+
width: 100%;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
function tokenizeCommand(command) {
|
|
3
|
+
return command.match(/"[^"]*"|'[^']*'|\S+/g) || [];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function splitCommandParts(command) {
|
|
7
|
+
const tokens = tokenizeCommand(command);
|
|
8
|
+
const firstOption = tokens.findIndex((token) => token.startsWith("--"));
|
|
9
|
+
|
|
10
|
+
if (firstOption === -1) {
|
|
11
|
+
return {
|
|
12
|
+
prefix: tokens.join(" "),
|
|
13
|
+
options: [],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
prefix: tokens.slice(0, firstOption).join(" "),
|
|
19
|
+
options: groupOptionTokens(tokens.slice(firstOption)),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function groupOptionTokens(tokens) {
|
|
24
|
+
const groups = [];
|
|
25
|
+
let group = [];
|
|
26
|
+
|
|
27
|
+
tokens.forEach((token) => {
|
|
28
|
+
if (token.startsWith("--") && group.length) {
|
|
29
|
+
groups.push(group.join(" "));
|
|
30
|
+
group = [token];
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
group.push(token);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (group.length) {
|
|
38
|
+
groups.push(group.join(" "));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return groups;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatCommand(env, command, args, config) {
|
|
45
|
+
if (config.lineBreak === "none") {
|
|
46
|
+
return [...env, command, ...args].filter(Boolean).join(" ");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const commandParts = splitCommandParts(command);
|
|
50
|
+
const lines = [
|
|
51
|
+
[...env, commandParts.prefix].filter(Boolean).join(" "),
|
|
52
|
+
...commandParts.options,
|
|
53
|
+
...args.flatMap((arg) => groupOptionTokens(tokenizeCommand(arg))),
|
|
54
|
+
].filter(Boolean);
|
|
55
|
+
|
|
56
|
+
return lines
|
|
57
|
+
.map((line, index) => {
|
|
58
|
+
const continuation = index < lines.length - 1 ? " \\" : "";
|
|
59
|
+
const indent = index === 0 ? "" : config.indent;
|
|
60
|
+
return `${indent}${line}${continuation}`;
|
|
61
|
+
})
|
|
62
|
+
.join("\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readState(panel) {
|
|
66
|
+
const keys = Array.from(
|
|
67
|
+
new Set(
|
|
68
|
+
Array.from(panel.querySelectorAll("[data-sdc-option]")).map((option) =>
|
|
69
|
+
option.getAttribute("data-sdc-option")
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return keys.reduce((state, key) => {
|
|
75
|
+
const selected = panel.querySelector(
|
|
76
|
+
`[data-sdc-option="${key}"][data-sdc-default="true"]`
|
|
77
|
+
);
|
|
78
|
+
const first = panel.querySelector(`[data-sdc-option="${key}"]`);
|
|
79
|
+
state[key] = (selected || first)?.getAttribute("data-sdc-value") || "";
|
|
80
|
+
return state;
|
|
81
|
+
}, {});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function updatePanel(panel, state) {
|
|
85
|
+
panel.querySelectorAll("[data-sdc-option]").forEach((option) => {
|
|
86
|
+
const key = option.getAttribute("data-sdc-option");
|
|
87
|
+
const value = option.getAttribute("data-sdc-value");
|
|
88
|
+
const isSelected = state[key] === value;
|
|
89
|
+
|
|
90
|
+
option.classList.toggle("is-selected", isSelected);
|
|
91
|
+
option.setAttribute("aria-pressed", isSelected ? "true" : "false");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
panel.querySelectorAll("[data-sdc-output]").forEach((output) => {
|
|
95
|
+
const env = [];
|
|
96
|
+
const args = [];
|
|
97
|
+
let command = output.getAttribute("data-sdc-base") || "";
|
|
98
|
+
|
|
99
|
+
Object.entries(state).forEach(([key, value]) => {
|
|
100
|
+
const selected = Array.from(
|
|
101
|
+
panel.querySelectorAll(`[data-sdc-option="${key}"]`)
|
|
102
|
+
).find((option) => option.getAttribute("data-sdc-value") === value);
|
|
103
|
+
|
|
104
|
+
if (!selected) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const nextBase = selected.getAttribute("data-sdc-base");
|
|
109
|
+
const nextEnv = selected.getAttribute("data-sdc-env");
|
|
110
|
+
const nextArgs = selected.getAttribute("data-sdc-args");
|
|
111
|
+
|
|
112
|
+
if (nextBase) {
|
|
113
|
+
command = nextBase;
|
|
114
|
+
}
|
|
115
|
+
if (nextEnv) {
|
|
116
|
+
env.push(nextEnv);
|
|
117
|
+
}
|
|
118
|
+
if (nextArgs) {
|
|
119
|
+
args.push(nextArgs);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
output.textContent = formatCommand(env, command, args, {
|
|
124
|
+
indent: output.getAttribute("data-sdc-indent") || " ",
|
|
125
|
+
lineBreak: output.getAttribute("data-sdc-line-break") || "options",
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function setupPanel(panel) {
|
|
131
|
+
const state = readState(panel);
|
|
132
|
+
|
|
133
|
+
panel.querySelectorAll("[data-sdc-option]").forEach((option) => {
|
|
134
|
+
option.addEventListener("click", () => {
|
|
135
|
+
state[option.getAttribute("data-sdc-option")] =
|
|
136
|
+
option.getAttribute("data-sdc-value");
|
|
137
|
+
updatePanel(panel, state);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
updatePanel(panel, state);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
145
|
+
document.querySelectorAll("[data-sdc]").forEach(setupPanel);
|
|
146
|
+
});
|
|
147
|
+
})();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sphinx-dynamic-command-builder
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Interactive command builders for Sphinx documentation
|
|
5
|
+
Project-URL: Homepage, https://github.com/Aionw/sphinx-dynamic-command-builder
|
|
6
|
+
Project-URL: Repository, https://github.com/Aionw/sphinx-dynamic-command-builder
|
|
7
|
+
Project-URL: Issues, https://github.com/Aionw/sphinx-dynamic-command-builder/issues
|
|
8
|
+
Author: sphinx-dynamic-command-builder contributors
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: command,directive,documentation,interactive,sphinx
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: Sphinx :: Extension
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Documentation
|
|
24
|
+
Classifier: Topic :: Documentation :: Sphinx
|
|
25
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Requires-Dist: pyyaml>=6
|
|
28
|
+
Requires-Dist: sphinx>=5
|
|
29
|
+
Provides-Extra: docs
|
|
30
|
+
Requires-Dist: myst-parser>=2; extra == 'docs'
|
|
31
|
+
Requires-Dist: sphinx-book-theme>=1; extra == 'docs'
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# sphinx-dynamic-command-builder
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/sphinx-dynamic-command-builder/)
|
|
39
|
+
|
|
40
|
+
Interactive command builders for Sphinx documentation.
|
|
41
|
+
|
|
42
|
+
`sphinx-dynamic-command-builder` adds a `dynamic-command` directive that renders a small selector UI from YAML and updates a generated command in the browser. It is useful for docs that need to show command-line examples assembled from several independent choices.
|
|
43
|
+
|
|
44
|
+
Demo: [GitHub Pages](https://aionw.github.io/sphinx-dynamic-command-builder/)
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install sphinx-dynamic-command-builder
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then enable the extension in `conf.py`:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
extensions = [
|
|
56
|
+
"sphinx_dynamic_command_builder",
|
|
57
|
+
]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
````md
|
|
63
|
+
```{dynamic-command}
|
|
64
|
+
base: python -m sglang.launch_server --model-path [model_path]
|
|
65
|
+
format:
|
|
66
|
+
line_break: options
|
|
67
|
+
indent: " "
|
|
68
|
+
options:
|
|
69
|
+
- label: Topology
|
|
70
|
+
key: nodes
|
|
71
|
+
default: single
|
|
72
|
+
choices:
|
|
73
|
+
- label: Single node
|
|
74
|
+
value: single
|
|
75
|
+
args: --host 0.0.0.0 --port 30000
|
|
76
|
+
- label: Multi node
|
|
77
|
+
value: multi
|
|
78
|
+
args: --host 0.0.0.0 --port 30000 --disaggregation-ib-device mlx5_1
|
|
79
|
+
```
|
|
80
|
+
````
|
|
81
|
+
|
|
82
|
+
Each option group is rendered as one selector row. Selecting a choice updates the generated command.
|
|
83
|
+
|
|
84
|
+
## YAML schema
|
|
85
|
+
|
|
86
|
+
See [Configuration](docs/configuration.md) for the full field reference and formatting rules.
|
|
87
|
+
|
|
88
|
+
- `base`: base command string.
|
|
89
|
+
- `command_label`: optional output label. Defaults to `Generated command`.
|
|
90
|
+
- `format.line_break`: optional command wrapping mode. Use `options` to put each `--option` group on its own shell-continuation line, or `none` to render a single line. Defaults to `options`.
|
|
91
|
+
- `format.indent`: optional indentation for continuation lines. Defaults to two spaces.
|
|
92
|
+
- `options`: list of option groups.
|
|
93
|
+
- `options[].label`: visible group label.
|
|
94
|
+
- `options[].key`: stable group key.
|
|
95
|
+
- `options[].default`: optional default choice value. Defaults to the first choice.
|
|
96
|
+
- `options[].choices`: list of choices.
|
|
97
|
+
- `choices[].label`: visible choice label.
|
|
98
|
+
- `choices[].value`: stable choice value.
|
|
99
|
+
- `choices[].env`: optional text prepended before the base command.
|
|
100
|
+
- `choices[].args`: optional text appended after the base command.
|
|
101
|
+
- `choices[].base`: optional replacement base command for this choice.
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
uv venv
|
|
107
|
+
uv pip install -e ".[test,docs]"
|
|
108
|
+
pytest
|
|
109
|
+
sphinx-build -M html docs docs/_build
|
|
110
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
sphinx_dynamic_command_builder/__init__.py,sha256=ctjvd-HhxRzbEljl1EEte7B7n65bfVKBqbqke6vXHms,934
|
|
2
|
+
sphinx_dynamic_command_builder/directive.py,sha256=jxetw9bhL0nn3HmHTBWPGuxwRdjZ08wbILNuK8ZpjAw,4782
|
|
3
|
+
sphinx_dynamic_command_builder/static/sphinx-dynamic-command-builder.css,sha256=U0P1jYKcaGIdk7WRsCckPkD1-6jbjIx_XbtQM6L17_k,2154
|
|
4
|
+
sphinx_dynamic_command_builder/static/sphinx-dynamic-command-builder.js,sha256=T-wQQgVhUxYccURU1_xG2sjqbu40_nVeNfHe_aaAX28,4183
|
|
5
|
+
sphinx_dynamic_command_builder-0.1.0.dist-info/METADATA,sha256=fQoxaSXPnjQk2c5US_gTIbUB_fovP7oPKFGzyaIwYG0,3959
|
|
6
|
+
sphinx_dynamic_command_builder-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
7
|
+
sphinx_dynamic_command_builder-0.1.0.dist-info/licenses/LICENSE,sha256=ZJcV9uUBseT13kS5cA9M3EqVprlFoJ2Fnm5qj_E80FA,1100
|
|
8
|
+
sphinx_dynamic_command_builder-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sphinx-dynamic-command-builder contributors
|
|
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.
|