superfences-ps1 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.
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) `2026` `Tucker Beck`
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.
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: superfences-ps1
3
+ Version: 0.1.0
4
+ Summary: MkDocs plugin that adds a configurable shell-ps1 code fence with copy-safe prompt character
5
+ Keywords: mkdocs,mkdocs-plugin,plugin,pymdownx,superfences,shell,ps1,pygments,documentation,documentation-tools,mkdocs-material
6
+ Author: Tucker Beck
7
+ Author-email: Tucker Beck <tucker.beck@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE.md
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Framework :: MkDocs
17
+ Classifier: Topic :: Documentation
18
+ Requires-Dist: mkdocs>=1.5.0
19
+ Requires-Dist: pymdown-extensions>=10.0
20
+ Requires-Dist: pygments>=2.16
21
+ Requires-Python: >=3.12
22
+ Project-URL: homepage, https://github.com/dusktreader/superfences-ps1
23
+ Project-URL: source, https://github.com/dusktreader/superfences-ps1
24
+ Project-URL: changelog, https://github.com/dusktreader/superfences-ps1/blob/main/CHANGELOG.md
25
+ Project-URL: issues, https://github.com/dusktreader/superfences-ps1/issues
26
+ Description-Content-Type: text/markdown
27
+
28
+ # superfences-ps1
29
+
30
+ MkDocs plugin that adds a configurable shell-prompt SuperFences fence with copy-safe PS1 prompt characters.
31
+
32
+ The plugin registers a custom [SuperFences](https://facelessuser.github.io/pymdown-extensions/extensions/superfences/)
33
+ fence (default name `shell-ps1`) that prepends a configurable PS1 prompt character to every command line. The prompt is
34
+ rendered visibly in the documentation but is **never copied to the clipboard** and is **never selected by mouse** —
35
+ the plugin injects a `data-copy` attribute with the raw source text so Material for MkDocs' copy button bypasses the
36
+ prompt spans entirely, and `user-select: none` CSS is injected to prevent mouse selection of the prompt.
37
+
38
+
39
+ ## Installation
40
+
41
+ ```shell
42
+ pip install superfences-ps1
43
+ ```
44
+
45
+ Or with uv:
46
+
47
+ ```shell
48
+ uv add superfences-ps1
49
+ ```
50
+
51
+
52
+ ## Requirements
53
+
54
+ - `pymdownx.superfences` must be listed under `markdown_extensions` in `mkdocs.yaml`
55
+
56
+
57
+ ## Usage
58
+
59
+
60
+ ### `mkdocs.yaml` configuration
61
+
62
+ ```yaml
63
+ plugins:
64
+ - superfences-ps1:
65
+ fence_name: shell-ps1 # fence language identifier (default: shell-ps1)
66
+ prompt_char: "$" # PS1 prompt character prepended to each line (default: $)
67
+ prompt_color: "#5fb3b3" # optional CSS color for the prompt character
68
+
69
+ markdown_extensions:
70
+ - pymdownx.superfences
71
+ ```
72
+
73
+
74
+ ### Writing shell fences
75
+
76
+ Use the configured `fence_name` in your markdown:
77
+
78
+ ````markdown
79
+ ```shell-ps1
80
+ echo "Hello, world!"
81
+ ls -la
82
+ ```
83
+ ````
84
+
85
+ The plugin prepends the prompt character to each non-empty line before passing the content to Pygments. The rendered
86
+ output shows the prompt visually, but the copy button and mouse selection only grab the commands themselves.
87
+
88
+
89
+ ## Configuration options
90
+
91
+ | Option | Type | Default | Description |
92
+ |----------------|-----------------|-------------|----------------------------------------------------------|
93
+ | `fence_name` | `str` | `shell-ps1` | The fence language identifier used in markdown |
94
+ | `prompt_char` | `str` | `$` | The PS1 prompt character prepended to each command line |
95
+ | `prompt_color` | `str` or `None` | `None` | Optional CSS color value for the rendered prompt |
96
+
97
+ > [!NOTE]
98
+ > `prompt_char` must be one of `$`, `%`, or `#` — the only characters Pygments' `BashSessionLexer` recognises as
99
+ > prompt markers, emitting them as `Generic.Prompt` (`.gp`) tokens. Any other value will cause a `PluginError` at
100
+ > build time. `>` in particular is the continuation prompt (`_ps2`) and causes the entire line to be tokenised as
101
+ > `Generic.Output` (`.go`), which would treat the command text as output.
102
+
103
+
104
+ ## How it works
105
+
106
+ 1. On `on_config`, the plugin validates that `pymdownx.superfences` is present and injects a custom fence formatter
107
+ into `mdx_configs["pymdownx.superfences"]["custom_fences"]`.
108
+ 2. When MkDocs processes a page containing a fenced code block with the configured `fence_name`, SuperFences calls
109
+ the injected formatter.
110
+ 3. The formatter prepends `<prompt_char> ` to every non-empty line, then passes the result to Pygments using the
111
+ `console` (`BashSessionLexer`) lexer. Pygments wraps the prompt in `<span class="gp">` and the command in
112
+ additional token spans.
113
+ 4. The formatter injects a `data-copy` attribute on the wrapper `<div>` containing the raw (un-prompted) source.
114
+ Material for MkDocs' copy button reads `data-copy` in preference to `innerText`, so the prompt is never included
115
+ in clipboard content.
116
+ 5. On `on_post_page`, the plugin injects a `<style>` block into each page's `<head>` with `user-select: none` on
117
+ `.gp` spans, preventing mouse selection of the prompt. If `prompt_color` is configured, the color rule is
118
+ combined into the same `<style>` block.
119
+
120
+
121
+ ## Development
122
+
123
+ ```shell
124
+ git clone https://github.com/dusktreader/superfences-ps1
125
+ cd superfences-ps1
126
+ uv sync
127
+ make qa/full
128
+ ```
129
+
130
+
131
+ ## License
132
+
133
+ MIT — see [LICENSE.md](LICENSE.md).
@@ -0,0 +1,106 @@
1
+ # superfences-ps1
2
+
3
+ MkDocs plugin that adds a configurable shell-prompt SuperFences fence with copy-safe PS1 prompt characters.
4
+
5
+ The plugin registers a custom [SuperFences](https://facelessuser.github.io/pymdown-extensions/extensions/superfences/)
6
+ fence (default name `shell-ps1`) that prepends a configurable PS1 prompt character to every command line. The prompt is
7
+ rendered visibly in the documentation but is **never copied to the clipboard** and is **never selected by mouse** —
8
+ the plugin injects a `data-copy` attribute with the raw source text so Material for MkDocs' copy button bypasses the
9
+ prompt spans entirely, and `user-select: none` CSS is injected to prevent mouse selection of the prompt.
10
+
11
+
12
+ ## Installation
13
+
14
+ ```shell
15
+ pip install superfences-ps1
16
+ ```
17
+
18
+ Or with uv:
19
+
20
+ ```shell
21
+ uv add superfences-ps1
22
+ ```
23
+
24
+
25
+ ## Requirements
26
+
27
+ - `pymdownx.superfences` must be listed under `markdown_extensions` in `mkdocs.yaml`
28
+
29
+
30
+ ## Usage
31
+
32
+
33
+ ### `mkdocs.yaml` configuration
34
+
35
+ ```yaml
36
+ plugins:
37
+ - superfences-ps1:
38
+ fence_name: shell-ps1 # fence language identifier (default: shell-ps1)
39
+ prompt_char: "$" # PS1 prompt character prepended to each line (default: $)
40
+ prompt_color: "#5fb3b3" # optional CSS color for the prompt character
41
+
42
+ markdown_extensions:
43
+ - pymdownx.superfences
44
+ ```
45
+
46
+
47
+ ### Writing shell fences
48
+
49
+ Use the configured `fence_name` in your markdown:
50
+
51
+ ````markdown
52
+ ```shell-ps1
53
+ echo "Hello, world!"
54
+ ls -la
55
+ ```
56
+ ````
57
+
58
+ The plugin prepends the prompt character to each non-empty line before passing the content to Pygments. The rendered
59
+ output shows the prompt visually, but the copy button and mouse selection only grab the commands themselves.
60
+
61
+
62
+ ## Configuration options
63
+
64
+ | Option | Type | Default | Description |
65
+ |----------------|-----------------|-------------|----------------------------------------------------------|
66
+ | `fence_name` | `str` | `shell-ps1` | The fence language identifier used in markdown |
67
+ | `prompt_char` | `str` | `$` | The PS1 prompt character prepended to each command line |
68
+ | `prompt_color` | `str` or `None` | `None` | Optional CSS color value for the rendered prompt |
69
+
70
+ > [!NOTE]
71
+ > `prompt_char` must be one of `$`, `%`, or `#` — the only characters Pygments' `BashSessionLexer` recognises as
72
+ > prompt markers, emitting them as `Generic.Prompt` (`.gp`) tokens. Any other value will cause a `PluginError` at
73
+ > build time. `>` in particular is the continuation prompt (`_ps2`) and causes the entire line to be tokenised as
74
+ > `Generic.Output` (`.go`), which would treat the command text as output.
75
+
76
+
77
+ ## How it works
78
+
79
+ 1. On `on_config`, the plugin validates that `pymdownx.superfences` is present and injects a custom fence formatter
80
+ into `mdx_configs["pymdownx.superfences"]["custom_fences"]`.
81
+ 2. When MkDocs processes a page containing a fenced code block with the configured `fence_name`, SuperFences calls
82
+ the injected formatter.
83
+ 3. The formatter prepends `<prompt_char> ` to every non-empty line, then passes the result to Pygments using the
84
+ `console` (`BashSessionLexer`) lexer. Pygments wraps the prompt in `<span class="gp">` and the command in
85
+ additional token spans.
86
+ 4. The formatter injects a `data-copy` attribute on the wrapper `<div>` containing the raw (un-prompted) source.
87
+ Material for MkDocs' copy button reads `data-copy` in preference to `innerText`, so the prompt is never included
88
+ in clipboard content.
89
+ 5. On `on_post_page`, the plugin injects a `<style>` block into each page's `<head>` with `user-select: none` on
90
+ `.gp` spans, preventing mouse selection of the prompt. If `prompt_color` is configured, the color rule is
91
+ combined into the same `<style>` block.
92
+
93
+
94
+ ## Development
95
+
96
+ ```shell
97
+ git clone https://github.com/dusktreader/superfences-ps1
98
+ cd superfences-ps1
99
+ uv sync
100
+ make qa/full
101
+ ```
102
+
103
+
104
+ ## License
105
+
106
+ MIT — see [LICENSE.md](LICENSE.md).
@@ -0,0 +1,93 @@
1
+ [project]
2
+ name = "superfences-ps1"
3
+ version = "0.1.0"
4
+ description = "MkDocs plugin that adds a configurable shell-ps1 code fence with copy-safe prompt character"
5
+ authors = [
6
+ {name = "Tucker Beck", email = "tucker.beck@gmail.com"}
7
+ ]
8
+ readme = "README.md"
9
+ license = "MIT"
10
+ license-files = ["LICENSE.md"]
11
+ keywords = [
12
+ "mkdocs",
13
+ "mkdocs-plugin",
14
+ "plugin",
15
+ "pymdownx",
16
+ "superfences",
17
+ "shell",
18
+ "ps1",
19
+ "pygments",
20
+ "documentation",
21
+ "documentation-tools",
22
+ "mkdocs-material",
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 3 - Alpha",
26
+ "Intended Audience :: Developers",
27
+ "License :: OSI Approved :: MIT License",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Framework :: MkDocs",
32
+ "Topic :: Documentation",
33
+ ]
34
+ requires-python = ">=3.12"
35
+ dependencies = [
36
+ "mkdocs>=1.5.0",
37
+ "pymdown-extensions>=10.0",
38
+ "pygments>=2.16",
39
+ ]
40
+
41
+ [project.entry-points."mkdocs.plugins"]
42
+ superfences-ps1 = "superfences_ps1.plugin:ShellPromptPlugin"
43
+
44
+ [project.urls]
45
+ homepage = "https://github.com/dusktreader/superfences-ps1"
46
+ source = "https://github.com/dusktreader/superfences-ps1"
47
+ changelog = "https://github.com/dusktreader/superfences-ps1/blob/main/CHANGELOG.md"
48
+ issues = "https://github.com/dusktreader/superfences-ps1/issues"
49
+
50
+ [tool.uv]
51
+ package = true
52
+
53
+ [dependency-groups]
54
+ dev = [
55
+ "mkdocs-material~=9.6",
56
+ "pre-commit>=4.5.1",
57
+ "pyclean~=3.1",
58
+ "pygments~=2.19",
59
+ "pytest~=8.3",
60
+ "pytest-cov~=6.0",
61
+ "pytest-pretty~=1.2",
62
+ "pytest-random-order~=1.1",
63
+ "ruff~=0.11",
64
+ "ty>=0.0.0a1",
65
+ "types-markdown>=3.10.2.20260211",
66
+ "typos~=1.31",
67
+ "yamllint>=1.38.0",
68
+ ]
69
+
70
+ [build-system]
71
+ requires = ["uv_build>=0.10.9,<0.11.0"]
72
+ build-backend = "uv_build"
73
+
74
+ [tool.ty.src]
75
+ include = ["src", "tests"]
76
+
77
+ [tool.pytest.ini_options]
78
+ addopts = [
79
+ "--random-order",
80
+ "--cov=src/superfences_ps1",
81
+ "--cov-report=term-missing",
82
+ "--cov-fail-under=85",
83
+ ]
84
+
85
+ [tool.ruff]
86
+ line-length = 120
87
+ src = ["src/superfences_ps1", "tests"]
88
+
89
+ [tool.ruff.format]
90
+ docstring-code-format = true
91
+
92
+ [tool.typos.default]
93
+ extend-ignore-identifiers-re = []
@@ -0,0 +1,7 @@
1
+ """
2
+ superfences-ps1 — MkDocs plugin that registers a SuperFences shell-prompt fence.
3
+
4
+ The fence prepends a configurable prompt character to each command line so that
5
+ Pygments emits a ``Generic.Prompt`` (``.gp``) token. Material for MkDocs strips
6
+ ``.gp`` spans from the clipboard copy, so the prompt is visible but never copied.
7
+ """
@@ -0,0 +1,163 @@
1
+ import html
2
+ from collections.abc import Callable
3
+ from typing import override
4
+
5
+ import markdown
6
+ from mkdocs.config import config_options
7
+ from mkdocs.config.base import Config
8
+ from mkdocs.config.config_options import Optional, Type
9
+ from mkdocs.config.defaults import MkDocsConfig
10
+ from mkdocs.exceptions import PluginError
11
+ from mkdocs.plugins import BasePlugin, get_plugin_logger
12
+ from mkdocs.structure.pages import Page
13
+ from pygments import highlight
14
+ from pygments.formatters.html import HtmlFormatter
15
+ from pygments.lexers import get_lexer_by_name
16
+
17
+ log = get_plugin_logger(__name__)
18
+
19
+ DEFAULT_FENCE_NAME = "shell-ps1"
20
+ DEFAULT_PROMPT_CHAR = "$"
21
+ VALID_PROMPT_CHARS = ("$", "%", "#")
22
+
23
+ _SUPERFENCES_KEY = "pymdownx.superfences"
24
+
25
+ # Type alias for a superfences custom-fence formatter callable.
26
+ # The signature mirrors pymdownx.superfences.fence_code_format.
27
+ FormatterFn = Callable[..., str]
28
+
29
+ # Type alias for the loosely-typed superfences configuration dicts that
30
+ # come back from MkDocs' mdx_configs mapping (no stubs available).
31
+ _FenceDict = dict[str, object]
32
+ _SuperfencesCfg = dict[str, object]
33
+ _MdxConfigs = dict[str, object]
34
+
35
+
36
+ class ShellPromptConfig(Config):
37
+ fence_name: Type[str] = config_options.Type(str, default=DEFAULT_FENCE_NAME)
38
+ prompt_char: Type[str] = config_options.Type(str, default=DEFAULT_PROMPT_CHAR)
39
+ prompt_color: Optional[str] = config_options.Optional(config_options.Type(str))
40
+
41
+
42
+ class ShellPromptPlugin(BasePlugin[ShellPromptConfig]):
43
+ """
44
+ MkDocs plugin that adds a configurable shell-prompt SuperFences fence.
45
+
46
+ The fence prepends `<prompt_char> ` to every non-empty line so that
47
+ Pygments' `BashSessionLexer` (`console`) emits a `Generic.Prompt`
48
+ token (`.gp` CSS class) for the prompt portion. A `data-copy` attribute
49
+ is injected on the wrapper `<div>` containing the raw (un-prompted) source
50
+ text so that Material for MkDocs' copy button uses that value instead of
51
+ reading `innerText`, keeping the prompt out of the clipboard.
52
+
53
+ Requires `pymdownx.superfences` in `markdown_extensions`.
54
+
55
+ Configuration (`mkdocs.yaml`):
56
+
57
+ ```yaml
58
+ plugins:
59
+ - superfences-ps1:
60
+ fence_name: shell # fence language identifier (default: shell)
61
+ prompt_char: "$" # prompt character prepended to each line (default: $)
62
+ prompt_color: "#89b0c2" # CSS color for the prompt character (default: unset)
63
+ ```
64
+ """
65
+
66
+ def _make_formatter(self, prompt_char: str) -> FormatterFn:
67
+ """Return a SuperFences-compatible formatter function for the given prompt char."""
68
+
69
+ def formatter(
70
+ source: str,
71
+ _language: str,
72
+ css_class: str,
73
+ _options: dict[str, object],
74
+ _md: markdown.Markdown,
75
+ **_kwargs: object,
76
+ ) -> str:
77
+ prompted = "\n".join(f"{prompt_char} {line}" if line.strip() else line for line in source.splitlines())
78
+ lexer = get_lexer_by_name("console", stripall=False)
79
+ fmt = HtmlFormatter(
80
+ cssclass=f"highlight {css_class}".strip(),
81
+ wrapcode=True,
82
+ )
83
+ rendered: str = highlight(prompted, lexer, fmt)
84
+ # Inject data-copy on the wrapper <div> with the raw (un-prompted) source
85
+ # text. Material for MkDocs' copy button reads this attribute in preference
86
+ # to innerText, so the prompt character is never included in the clipboard.
87
+ copy_text = html.escape(source.rstrip("\n"), quote=True)
88
+ return rendered.replace("<div ", f'<div data-copy="{copy_text}" ', 1)
89
+
90
+ return formatter
91
+
92
+ @override
93
+ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None:
94
+ """Inject the shell-prompt fence into the superfences custom_fences list."""
95
+ fence_name = self.config.fence_name
96
+ prompt_char = self.config.prompt_char
97
+
98
+ ext_names: set[str] = set()
99
+ for ext in config.get("markdown_extensions", []):
100
+ if isinstance(ext, str):
101
+ ext_names.add(ext)
102
+ elif isinstance(ext, dict):
103
+ ext_names.update(ext.keys())
104
+
105
+ if prompt_char not in VALID_PROMPT_CHARS:
106
+ raise PluginError(
107
+ f"[superfences-ps1] Invalid prompt_char {prompt_char!r}. "
108
+ f"Must be one of: {', '.join(repr(c) for c in VALID_PROMPT_CHARS)}. "
109
+ "Other characters are not recognised as prompts by Pygments' BashSessionLexer."
110
+ )
111
+
112
+ if _SUPERFENCES_KEY not in ext_names:
113
+ raise PluginError(
114
+ f"[superfences-ps1] '{_SUPERFENCES_KEY}' must be listed under 'markdown_extensions' in mkdocs.yaml."
115
+ )
116
+
117
+ mdx_configs: _MdxConfigs = config.get("mdx_configs", {})
118
+ superfences_cfg: _SuperfencesCfg = mdx_configs.get(_SUPERFENCES_KEY, {})
119
+ custom_fences: list[_FenceDict] = superfences_cfg.get("custom_fences", [])
120
+
121
+ existing_names = {f["name"] for f in custom_fences}
122
+ if fence_name in existing_names:
123
+ log.warning(
124
+ "Fence name '%s' is already registered in superfences; skipping injection.",
125
+ fence_name,
126
+ )
127
+ return None
128
+
129
+ custom_fences.append(
130
+ {
131
+ "name": fence_name,
132
+ "class": fence_name,
133
+ "format": self._make_formatter(prompt_char),
134
+ }
135
+ )
136
+ superfences_cfg["custom_fences"] = custom_fences
137
+ mdx_configs[_SUPERFENCES_KEY] = superfences_cfg
138
+ config["mdx_configs"] = mdx_configs
139
+
140
+ log.debug(
141
+ "Registered shell-prompt fence '%s' with prompt char '%s'.",
142
+ fence_name,
143
+ prompt_char,
144
+ )
145
+ return None
146
+
147
+ @override
148
+ def on_post_page(self, output: str, /, *, page: Page, config: MkDocsConfig) -> str:
149
+ """
150
+ Inject prompt CSS into each page's <head>.
151
+
152
+ Always injects `user-select: none` on `.gp` so the prompt character is excluded from mouse text selection.
153
+ Also injects a `color` rule when `prompt_color` is configured.
154
+ """
155
+ if "</head>" not in output:
156
+ return output
157
+
158
+ prompt_color = self.config.prompt_color
159
+ rules = "user-select: none;"
160
+ if prompt_color:
161
+ rules += f" color: {prompt_color};"
162
+ style = f"<style>.highlight .gp {{ {rules} }}</style>"
163
+ return output.replace("</head>", f"{style}\n</head>", 1)
File without changes