docdo 0.0.2__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,44 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types:
6
+ - published
7
+
8
+ jobs:
9
+ build:
10
+ name: Build distributions
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v6
14
+
15
+ - name: Set up uv
16
+ uses: astral-sh/setup-uv@v5
17
+
18
+ - name: Build distributions
19
+ run: uv build
20
+
21
+ - name: Upload distributions
22
+ uses: actions/upload-artifact@v7
23
+ with:
24
+ name: wheels
25
+ path: dist
26
+
27
+ pypi-publish:
28
+ name: Upload release to PyPI
29
+ runs-on: ubuntu-latest
30
+ needs: [build]
31
+ environment:
32
+ name: pypi
33
+ url: https://pypi.org/p/docdo/
34
+ permissions:
35
+ id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
36
+ steps:
37
+ - name: Download distributions
38
+ uses: actions/download-artifact@v8
39
+ with:
40
+ name: wheels
41
+ path: dist
42
+
43
+ - name: Publish package distributions to PyPI
44
+ uses: pypa/gh-action-pypi-publish@release/v1
docdo-0.0.2/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.12
docdo-0.0.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 javi22020
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.
docdo-0.0.2/PKG-INFO ADDED
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: docdo
3
+ Version: 0.0.2
4
+ Summary: A CLI tool to render Markdown files to PDF via HTML templates and Playwright.
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: click>=8.0
8
+ Requires-Dist: markdown-it-py>=3.0
9
+ Requires-Dist: playwright>=1.40
10
+ Requires-Dist: pygments>=2.17
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Docdo
14
+
15
+ CLI tool that converts Markdown files to PDF using Playwright and CSS themes.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ uv tool install docdo
21
+ playwright install chromium
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```bash
27
+ docdo -i README.md # outputs README.pdf next to it
28
+ docdo -i docs/ -o out/ # converts a whole folder
29
+ docdo -i README.md -t dracula # use a built-in theme
30
+ docdo -i README.md -t ./my-style.css # use a custom CSS file
31
+ ```
32
+
33
+ | Flag | Description |
34
+ |------|-------------|
35
+ | `-i`, `--input` | Markdown file or folder (required) |
36
+ | `-o`, `--output` | Output PDF file or folder (defaults to input location) |
37
+ | `-t`, `--theme` | Built-in theme name (`default`, `dracula`) or path to a `.css` file |
38
+
39
+ ## Themes
40
+
41
+ Built-in themes live in `src/docdo/themes/`. To add your own, drop a `.css` file there or just pass any CSS file path with `-t`.
42
+
43
+ ## How it works
44
+
45
+ 1. Parses Markdown with `markdown-it-py` (with `pygments` for syntax highlighting).
46
+ 2. Wraps the HTML with the selected CSS theme.
47
+ 3. Renders to PDF via Playwright (headless Chromium).
48
+
49
+ ## Dev
50
+
51
+ ```bash
52
+ uv sync
53
+ uv run docdo -i README.md
54
+ ```
docdo-0.0.2/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # Docdo
2
+
3
+ CLI tool that converts Markdown files to PDF using Playwright and CSS themes.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ uv tool install docdo
9
+ playwright install chromium
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```bash
15
+ docdo -i README.md # outputs README.pdf next to it
16
+ docdo -i docs/ -o out/ # converts a whole folder
17
+ docdo -i README.md -t dracula # use a built-in theme
18
+ docdo -i README.md -t ./my-style.css # use a custom CSS file
19
+ ```
20
+
21
+ | Flag | Description |
22
+ |------|-------------|
23
+ | `-i`, `--input` | Markdown file or folder (required) |
24
+ | `-o`, `--output` | Output PDF file or folder (defaults to input location) |
25
+ | `-t`, `--theme` | Built-in theme name (`default`, `dracula`) or path to a `.css` file |
26
+
27
+ ## Themes
28
+
29
+ Built-in themes live in `src/docdo/themes/`. To add your own, drop a `.css` file there or just pass any CSS file path with `-t`.
30
+
31
+ ## How it works
32
+
33
+ 1. Parses Markdown with `markdown-it-py` (with `pygments` for syntax highlighting).
34
+ 2. Wraps the HTML with the selected CSS theme.
35
+ 3. Renders to PDF via Playwright (headless Chromium).
36
+
37
+ ## Dev
38
+
39
+ ```bash
40
+ uv sync
41
+ uv run docdo -i README.md
42
+ ```
docdo-0.0.2/main.py ADDED
@@ -0,0 +1,4 @@
1
+ from docdo.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "docdo"
3
+ version = "0.0.2"
4
+ description = "A CLI tool to render Markdown files to PDF via HTML templates and Playwright."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "click>=8.0",
9
+ "playwright>=1.40",
10
+ "markdown-it-py>=3.0",
11
+ "pygments>=2.17",
12
+ ]
13
+
14
+ [project.scripts]
15
+ docdo = "docdo.cli:main"
16
+
17
+ [build-system]
18
+ requires = ["hatchling"]
19
+ build-backend = "hatchling.build"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/docdo"]
23
+
24
+ [dependency-groups]
25
+ dev = ["pytest>=8.0"]
@@ -0,0 +1,12 @@
1
+ {
2
+ "venvPath": ".",
3
+ "venv": ".venv",
4
+ "pythonVersion": "3.12",
5
+ "include": ["src", "main.py"],
6
+ "executionEnvironments": [
7
+ {
8
+ "root": ".",
9
+ "extraPaths": ["src"]
10
+ }
11
+ ]
12
+ }
@@ -0,0 +1,5 @@
1
+ """docdo - Markdown to PDF converter."""
2
+
3
+ from docdo.converter import convert
4
+
5
+ __all__ = ["convert"]
@@ -0,0 +1,113 @@
1
+ """Click CLI entry point for docdo."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ from docdo import themes as _themes
11
+ from docdo.converter import convert as _convert
12
+
13
+
14
+ def _default_output(input_path: Path) -> Path:
15
+ """Return a default PDF path next to the input file."""
16
+ return input_path.with_suffix(".pdf")
17
+
18
+
19
+ @click.command(context_settings={"help_option_names": ["-h", "--help"]})
20
+ @click.option(
21
+ "-i", "--input", "input_path",
22
+ required=True,
23
+ type=click.Path(exists=True, readable=True, path_type=Path),
24
+ help="Input Markdown file or folder.",
25
+ )
26
+ @click.option(
27
+ "-o", "--output", "output_path",
28
+ default=None,
29
+ type=click.Path(path_type=Path),
30
+ help="Output PDF file or folder. Defaults to the input location.",
31
+ )
32
+ @click.option(
33
+ "-t", "--theme",
34
+ default=None,
35
+ metavar="NAME_OR_PATH",
36
+ help=(
37
+ f"Theme name ({', '.join(_themes.list_themes())}) "
38
+ "or path to a .css file. Defaults to 'default'."
39
+ ),
40
+ )
41
+ @click.version_option(package_name="docdo")
42
+ def main(input_path: Path, output_path: Path | None, theme: str | None) -> None:
43
+ """Convert Markdown files to PDF with beautiful CSS themes."""
44
+
45
+ if input_path.is_dir():
46
+ _convert_folder(input_path, output_path, theme)
47
+ else:
48
+ _convert_file(input_path, output_path, theme)
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Helpers
53
+ # ---------------------------------------------------------------------------
54
+
55
+
56
+ def _convert_file(
57
+ input_path: Path,
58
+ output_path: Path | None,
59
+ theme: str | None,
60
+ ) -> None:
61
+ if output_path is None:
62
+ output_path = _default_output(input_path)
63
+ if output_path.is_dir():
64
+ output_path = output_path / input_path.with_suffix(".pdf").name
65
+
66
+ click.echo(f" → {input_path.name} ➜ {output_path}")
67
+ try:
68
+ _convert(input_path, output_path, theme)
69
+ except (FileNotFoundError, ValueError) as exc:
70
+ click.secho(f"Error: {exc}", fg="red", err=True)
71
+ sys.exit(1)
72
+ click.secho(" ✓ Done", fg="green")
73
+
74
+
75
+ def _convert_folder(
76
+ input_folder: Path,
77
+ output_folder: Path | None,
78
+ theme: str | None,
79
+ ) -> None:
80
+ md_files = sorted(input_folder.rglob("*.md"))
81
+ if not md_files:
82
+ click.secho(f"No .md files found in {input_folder}", fg="yellow")
83
+ return
84
+
85
+ if output_folder is None:
86
+ output_folder = input_folder
87
+
88
+ output_folder.mkdir(parents=True, exist_ok=True)
89
+ errors = 0
90
+
91
+ for md_file in md_files:
92
+ # Preserve sub-folder structure relative to input_folder
93
+ relative = md_file.relative_to(input_folder)
94
+ out_file = output_folder / relative.with_suffix(".pdf")
95
+ out_file.parent.mkdir(parents=True, exist_ok=True)
96
+
97
+ click.echo(f" → {relative} ➜ {out_file.relative_to(output_folder)}")
98
+ try:
99
+ _convert(md_file, out_file, theme)
100
+ click.secho(" ✓", fg="green")
101
+ except Exception as exc: # noqa: BLE001
102
+ click.secho(f" ✗ {exc}", fg="red", err=True)
103
+ errors += 1
104
+
105
+ total = len(md_files)
106
+ success = total - errors
107
+ color = "green" if errors == 0 else "yellow"
108
+ click.secho(
109
+ f"\nConverted {success}/{total} file(s).",
110
+ fg=color,
111
+ )
112
+ if errors:
113
+ sys.exit(1)
@@ -0,0 +1,38 @@
1
+ """High-level converter: glues themes, renderer, and PDF exporter together."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from docdo import pdf as _pdf
8
+ from docdo import renderer as _renderer
9
+ from docdo import themes as _themes
10
+
11
+
12
+ def convert(
13
+ input_path: str | Path,
14
+ output_path: str | Path,
15
+ theme: str | None = None,
16
+ ) -> None:
17
+ """Convert a single Markdown file to PDF.
18
+
19
+ Args:
20
+ input_path: Path to the source ``.md`` file.
21
+ output_path: Destination path for the generated ``.pdf`` file.
22
+ theme: Built-in theme name or path to a ``.css`` file.
23
+ Defaults to the ``"default"`` theme.
24
+
25
+ Raises:
26
+ FileNotFoundError: If *input_path* does not exist.
27
+ ValueError: If *theme* is an unknown name and not a valid path.
28
+ """
29
+ input_path = Path(input_path)
30
+ output_path = Path(output_path)
31
+
32
+ if not input_path.is_file():
33
+ raise FileNotFoundError(f"Input file not found: {input_path.resolve()}")
34
+
35
+ markdown_source = input_path.read_text(encoding="utf-8")
36
+ css = _themes.load_theme(theme)
37
+ html = _renderer.render(markdown_source, css)
38
+ _pdf.html_to_pdf(html, output_path)
@@ -0,0 +1,45 @@
1
+ """HTML → PDF exporter via Playwright (headless Chromium)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+ from playwright.sync_api import sync_playwright
9
+
10
+
11
+ def html_to_pdf(html: str, output_path: Path) -> None:
12
+ """Render *html* to a PDF file at *output_path*.
13
+
14
+ Args:
15
+ html: Complete HTML document string.
16
+ output_path: Destination path for the generated PDF (created/overwritten).
17
+
18
+ Raises:
19
+ RuntimeError: If Playwright fails to render the page.
20
+ """
21
+ output_path = Path(output_path)
22
+ output_path.parent.mkdir(parents=True, exist_ok=True)
23
+
24
+ # Write the HTML to a temporary file so that relative paths (images, etc.)
25
+ # resolve correctly from the file's directory.
26
+ with tempfile.NamedTemporaryFile(
27
+ suffix=".html", mode="w", encoding="utf-8", delete=False
28
+ ) as tmp:
29
+ tmp.write(html)
30
+ tmp_path = Path(tmp.name)
31
+
32
+ try:
33
+ with sync_playwright() as p:
34
+ browser = p.chromium.launch()
35
+ page = browser.new_page()
36
+ page.goto(tmp_path.as_uri(), wait_until="networkidle")
37
+ page.pdf(
38
+ path=str(output_path),
39
+ format="A4",
40
+ print_background=True,
41
+ margin={"top": "0", "right": "0", "bottom": "0", "left": "0"},
42
+ )
43
+ browser.close()
44
+ finally:
45
+ tmp_path.unlink(missing_ok=True)
@@ -0,0 +1,90 @@
1
+ """Markdown → styled HTML page renderer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from pygments import highlight
8
+ from pygments.formatters import HtmlFormatter
9
+ from pygments.lexers import get_lexer_by_name, guess_lexer
10
+ from pygments.util import ClassNotFound
11
+
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Syntax highlighter (used as a callback by markdown-it-py)
15
+ # ---------------------------------------------------------------------------
16
+
17
+
18
+ def _highlight_code(code: str, lang: str, _info: str) -> str:
19
+ """Pygments syntax highlighter called by markdown-it-py."""
20
+ try:
21
+ lexer = get_lexer_by_name(lang, stripall=True) if lang else guess_lexer(code)
22
+ except ClassNotFound:
23
+ lexer = guess_lexer(code)
24
+
25
+ formatter = HtmlFormatter(nowrap=True)
26
+ highlighted = highlight(code, lexer, formatter)
27
+ return f'<pre class="highlight"><code>{highlighted}</code></pre>'
28
+
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Markdown parser (created after the highlight callback is defined)
32
+ # ---------------------------------------------------------------------------
33
+
34
+ from markdown_it import MarkdownIt # noqa: E402
35
+
36
+ _md = MarkdownIt("commonmark", {"highlight": _highlight_code})
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # HTML page template
41
+ # ---------------------------------------------------------------------------
42
+
43
+ _HTML_TEMPLATE = """\
44
+ <!DOCTYPE html>
45
+ <html lang="en">
46
+ <head>
47
+ <meta charset="UTF-8" />
48
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
49
+ <title>{title}</title>
50
+ <style>
51
+ {css}
52
+ </style>
53
+ </head>
54
+ <body>
55
+ <div class="md-page">
56
+ {body}
57
+ </div>
58
+ </body>
59
+ </html>
60
+ """
61
+
62
+
63
+ def _derive_title(markdown_source: str) -> str:
64
+ """Extract an H1 title from the Markdown source, falling back to 'Document'."""
65
+ match = re.search(r"^#\s+(.+)$", markdown_source, re.MULTILINE)
66
+ return match.group(1).strip() if match else "Document"
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Public API
71
+ # ---------------------------------------------------------------------------
72
+
73
+
74
+ def render(markdown_source: str, css: str) -> str:
75
+ """Convert *markdown_source* to a full HTML page string styled with *css*.
76
+
77
+ Args:
78
+ markdown_source: Raw Markdown text.
79
+ css: CSS string (already resolved by the themes module).
80
+
81
+ Returns:
82
+ A complete, self-contained HTML document as a string.
83
+ """
84
+ body_html = _md.render(markdown_source)
85
+ title = _derive_title(markdown_source)
86
+ return _HTML_TEMPLATE.format(
87
+ title=title,
88
+ css=css,
89
+ body=body_html,
90
+ )
@@ -0,0 +1,5 @@
1
+ """Built-in themes package — CSS files live here as package data."""
2
+
3
+ from docdo.themes.resolver import list_themes, load_theme
4
+
5
+ __all__ = ["list_themes", "load_theme"]
@@ -0,0 +1,193 @@
1
+ /* docdo: Default (light) theme */
2
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap');
3
+
4
+ :root {
5
+ --color-bg: #ffffff;
6
+ --color-text: #1a1a2e;
7
+ --color-muted: #6b7280;
8
+ --color-border: #e5e7eb;
9
+ --color-accent: #3b82f6;
10
+ --color-code-bg: #f3f4f6;
11
+ --color-code-border: #e5e7eb;
12
+ --color-blockquote-border: #3b82f6;
13
+ --color-blockquote-bg: #eff6ff;
14
+ --color-table-header: #f9fafb;
15
+ --color-table-row-alt: #f9fafb;
16
+ --color-link: #2563eb;
17
+ }
18
+
19
+ * {
20
+ box-sizing: border-box;
21
+ margin: 0;
22
+ padding: 0;
23
+ }
24
+
25
+ body {
26
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
27
+ font-size: 15px;
28
+ line-height: 1.75;
29
+ color: var(--color-text);
30
+ background: var(--color-bg);
31
+ }
32
+
33
+ .md-page {
34
+ max-width: 760px;
35
+ margin: 0 auto;
36
+ padding: 48px 48px;
37
+ }
38
+
39
+ /* ── Headings ─────────────────────────────────────────────── */
40
+ h1, h2, h3, h4, h5, h6 {
41
+ font-weight: 700;
42
+ line-height: 1.25;
43
+ margin-top: 2em;
44
+ margin-bottom: 0.6em;
45
+ }
46
+
47
+ h1 { font-size: 2.2em; border-bottom: 2px solid var(--color-border); padding-bottom: 0.3em; margin-top: 0; }
48
+ h2 { font-size: 1.6em; border-bottom: 1px solid var(--color-border); padding-bottom: 0.25em; }
49
+ h3 { font-size: 1.25em; }
50
+ h4 { font-size: 1.05em; }
51
+
52
+ /* ── Paragraphs & spacing ──────────────────────────────────── */
53
+ p { margin-bottom: 1em; }
54
+
55
+ /* ── Links ─────────────────────────────────────────────────── */
56
+ a { color: var(--color-link); text-decoration: underline; text-underline-offset: 2px; }
57
+
58
+ /* ── Code (inline) ─────────────────────────────────────────── */
59
+ code {
60
+ font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
61
+ font-size: 0.88em;
62
+ background: var(--color-code-bg);
63
+ border: 1px solid var(--color-code-border);
64
+ border-radius: 4px;
65
+ padding: 0.15em 0.45em;
66
+ }
67
+
68
+ /* ── Code (block) ───────────────────────────────────────────── */
69
+ pre {
70
+ background: var(--color-code-bg);
71
+ border: 1px solid var(--color-code-border);
72
+ border-radius: 8px;
73
+ padding: 1.2em 1.5em;
74
+ overflow-x: auto;
75
+ margin: 1.2em 0;
76
+ font-size: 0.87em;
77
+ line-height: 1.6;
78
+ }
79
+
80
+ pre code {
81
+ background: none;
82
+ border: none;
83
+ padding: 0;
84
+ font-size: inherit;
85
+ }
86
+
87
+ /* ── Blockquote ─────────────────────────────────────────────── */
88
+ blockquote {
89
+ border-left: 4px solid var(--color-blockquote-border);
90
+ background: var(--color-blockquote-bg);
91
+ border-radius: 0 6px 6px 0;
92
+ padding: 0.8em 1.2em;
93
+ margin: 1.2em 0;
94
+ color: #374151;
95
+ }
96
+
97
+ blockquote > p:last-child { margin-bottom: 0; }
98
+
99
+ /* ── Lists ──────────────────────────────────────────────────── */
100
+ ul, ol { padding-left: 1.6em; margin-bottom: 1em; }
101
+ li + li { margin-top: 0.25em; }
102
+ li > ul, li > ol { margin-top: 0.25em; margin-bottom: 0; }
103
+
104
+ /* ── Horizontal rule ────────────────────────────────────────── */
105
+ hr { border: none; border-top: 2px solid var(--color-border); margin: 2em 0; }
106
+
107
+ /* ── Tables ─────────────────────────────────────────────────── */
108
+ table {
109
+ width: 100%;
110
+ border-collapse: collapse;
111
+ margin: 1.2em 0;
112
+ font-size: 0.9em;
113
+ }
114
+
115
+ thead th {
116
+ background: var(--color-table-header);
117
+ font-weight: 600;
118
+ text-align: left;
119
+ padding: 0.6em 0.9em;
120
+ border: 1px solid var(--color-border);
121
+ }
122
+
123
+ tbody td {
124
+ padding: 0.55em 0.9em;
125
+ border: 1px solid var(--color-border);
126
+ }
127
+
128
+ tbody tr:nth-child(even) { background: var(--color-table-row-alt); }
129
+
130
+ /* ── Images ─────────────────────────────────────────────────── */
131
+ img { max-width: 100%; border-radius: 6px; display: block; margin: 1em auto; }
132
+
133
+ /* ── Pygments syntax highlighting (monokai light) ───────────── */
134
+ .highlight .hll { background-color: #ffffcc }
135
+ .highlight .c { color: #6a9955; font-style: italic }
136
+ .highlight .k { color: #0000ff; font-weight: bold }
137
+ .highlight .o { color: #666666 }
138
+ .highlight .cm { color: #6a9955; font-style: italic }
139
+ .highlight .cp { color: #bc7a00 }
140
+ .highlight .c1 { color: #6a9955; font-style: italic }
141
+ .highlight .cs { color: #6a9955; font-style: italic }
142
+ .highlight .gd { color: #a61717; background-color: #ffe0e0 }
143
+ .highlight .ge { font-style: italic }
144
+ .highlight .gr { color: #aa0000 }
145
+ .highlight .gh { color: #000080; font-weight: bold }
146
+ .highlight .gi { color: #00a000 }
147
+ .highlight .go { color: #888888 }
148
+ .highlight .gp { color: #000080; font-weight: bold }
149
+ .highlight .gs { font-weight: bold }
150
+ .highlight .gu { color: #800080; font-weight: bold }
151
+ .highlight .gt { color: #0044dd }
152
+ .highlight .kc { color: #0000ff; font-weight: bold }
153
+ .highlight .kd { color: #0000ff; font-weight: bold }
154
+ .highlight .kn { color: #0000ff; font-weight: bold }
155
+ .highlight .kp { color: #0000ff }
156
+ .highlight .kr { color: #0000ff; font-weight: bold }
157
+ .highlight .kt { color: #b00040 }
158
+ .highlight .m { color: #808000 }
159
+ .highlight .s { color: #ba2121 }
160
+ .highlight .na { color: #7d9029 }
161
+ .highlight .nb { color: #008000 }
162
+ .highlight .nc { color: #0000ff; font-weight: bold }
163
+ .highlight .no { color: #880000 }
164
+ .highlight .nd { color: #aa22ff }
165
+ .highlight .ni { color: #999999; font-weight: bold }
166
+ .highlight .ne { color: #d2413a; font-weight: bold }
167
+ .highlight .nf { color: #0000ff }
168
+ .highlight .nl { color: #a0a000 }
169
+ .highlight .nn { color: #0000ff; font-weight: bold }
170
+ .highlight .nt { color: #008000; font-weight: bold }
171
+ .highlight .nv { color: #19177c }
172
+ .highlight .ow { color: #aa22ff; font-weight: bold }
173
+ .highlight .w { color: #bbbbbb }
174
+ .highlight .mf { color: #808000 }
175
+ .highlight .mh { color: #808000 }
176
+ .highlight .mi { color: #808000 }
177
+ .highlight .mo { color: #808000 }
178
+ .highlight .sb { color: #ba2121 }
179
+ .highlight .sc { color: #ba2121 }
180
+ .highlight .sd { color: #ba2121; font-style: italic }
181
+ .highlight .s2 { color: #ba2121 }
182
+ .highlight .se { color: #bb6622; font-weight: bold }
183
+ .highlight .sh { color: #ba2121 }
184
+ .highlight .si { color: #bb6688; font-weight: bold }
185
+ .highlight .sx { color: #008000 }
186
+ .highlight .sr { color: #bb6688 }
187
+ .highlight .s1 { color: #ba2121 }
188
+ .highlight .ss { color: #19177c }
189
+ .highlight .bp { color: #008000 }
190
+ .highlight .vc { color: #19177c }
191
+ .highlight .vg { color: #19177c }
192
+ .highlight .vi { color: #19177c }
193
+ .highlight .il { color: #808000 }
@@ -0,0 +1,216 @@
1
+ /* docdo: Dracula (dark) theme */
2
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap');
3
+
4
+ :root {
5
+ --color-bg: #282a36;
6
+ --color-bg-secondary: #343746;
7
+ --color-text: #f8f8f2;
8
+ --color-muted: #6272a4;
9
+ --color-border: #44475a;
10
+ --color-accent: #bd93f9;
11
+ --color-code-bg: #21222c;
12
+ --color-code-border: #44475a;
13
+ --color-blockquote-border: #bd93f9;
14
+ --color-blockquote-bg: #343746;
15
+ --color-table-header: #343746;
16
+ --color-table-row-alt: #2e303f;
17
+ --color-link: #8be9fd;
18
+ }
19
+
20
+ * {
21
+ box-sizing: border-box;
22
+ margin: 0;
23
+ padding: 0;
24
+ }
25
+
26
+ body {
27
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
28
+ font-size: 15px;
29
+ line-height: 1.75;
30
+ color: var(--color-text);
31
+ background: var(--color-bg);
32
+ }
33
+
34
+ .md-page {
35
+ max-width: 760px;
36
+ margin: 0 auto;
37
+ padding: 48px 48px;
38
+ }
39
+
40
+ /* ── Headings ─────────────────────────────────────────────── */
41
+ h1, h2, h3, h4, h5, h6 {
42
+ font-weight: 700;
43
+ line-height: 1.25;
44
+ margin-top: 2em;
45
+ margin-bottom: 0.6em;
46
+ color: #f1fa8c;
47
+ }
48
+
49
+ h1 { font-size: 2.2em; border-bottom: 2px solid var(--color-border); padding-bottom: 0.3em; margin-top: 0; color: #bd93f9; }
50
+ h2 { font-size: 1.6em; border-bottom: 1px solid var(--color-border); padding-bottom: 0.25em; color: #ff79c6; }
51
+ h3 { font-size: 1.25em; color: #50fa7b; }
52
+ h4 { font-size: 1.05em; color: #ffb86c; }
53
+
54
+ /* ── Paragraphs ─────────────────────────────────────────────── */
55
+ p { margin-bottom: 1em; }
56
+
57
+ /* ── Links ─────────────────────────────────────────────────── */
58
+ a { color: var(--color-link); text-decoration: underline; text-underline-offset: 2px; }
59
+
60
+ /* ── Code (inline) ─────────────────────────────────────────── */
61
+ code {
62
+ font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
63
+ font-size: 0.88em;
64
+ background: var(--color-code-bg);
65
+ border: 1px solid var(--color-code-border);
66
+ border-radius: 4px;
67
+ padding: 0.15em 0.45em;
68
+ color: #ff79c6;
69
+ }
70
+
71
+ /* ── Code (block) ───────────────────────────────────────────── */
72
+ pre {
73
+ background: var(--color-code-bg);
74
+ border: 1px solid var(--color-code-border);
75
+ border-radius: 8px;
76
+ padding: 1.2em 1.5em;
77
+ overflow-x: auto;
78
+ margin: 1.2em 0;
79
+ font-size: 0.87em;
80
+ line-height: 1.6;
81
+ }
82
+
83
+ pre code {
84
+ background: none;
85
+ border: none;
86
+ padding: 0;
87
+ font-size: inherit;
88
+ color: var(--color-text);
89
+ }
90
+
91
+ /* ── Blockquote ─────────────────────────────────────────────── */
92
+ blockquote {
93
+ border-left: 4px solid var(--color-blockquote-border);
94
+ background: var(--color-blockquote-bg);
95
+ border-radius: 0 6px 6px 0;
96
+ padding: 0.8em 1.2em;
97
+ margin: 1.2em 0;
98
+ color: #6272a4;
99
+ }
100
+
101
+ blockquote > p:last-child { margin-bottom: 0; }
102
+
103
+ /* ── Lists ──────────────────────────────────────────────────── */
104
+ ul, ol { padding-left: 1.6em; margin-bottom: 1em; }
105
+ li + li { margin-top: 0.25em; }
106
+ li > ul, li > ol { margin-top: 0.25em; margin-bottom: 0; }
107
+
108
+ /* ── Horizontal rule ────────────────────────────────────────── */
109
+ hr { border: none; border-top: 2px solid var(--color-border); margin: 2em 0; }
110
+
111
+ /* ── Tables ─────────────────────────────────────────────────── */
112
+ table {
113
+ width: 100%;
114
+ border-collapse: collapse;
115
+ margin: 1.2em 0;
116
+ font-size: 0.9em;
117
+ }
118
+
119
+ thead th {
120
+ background: var(--color-table-header);
121
+ font-weight: 600;
122
+ text-align: left;
123
+ padding: 0.6em 0.9em;
124
+ border: 1px solid var(--color-border);
125
+ color: #bd93f9;
126
+ }
127
+
128
+ tbody td {
129
+ padding: 0.55em 0.9em;
130
+ border: 1px solid var(--color-border);
131
+ }
132
+
133
+ tbody tr:nth-child(even) { background: var(--color-table-row-alt); }
134
+
135
+ /* ── Images ─────────────────────────────────────────────────── */
136
+ img { max-width: 100%; border-radius: 6px; display: block; margin: 1em auto; }
137
+
138
+ /* ── Pygments Dracula syntax highlighting ───────────────────── */
139
+ .highlight { background: var(--color-code-bg); }
140
+ .highlight .hll { background-color: #44475a }
141
+ .highlight .c { color: #6272a4; font-style: italic }
142
+ .highlight .err { color: #ff5555 }
143
+ .highlight .g { color: #f8f8f2 }
144
+ .highlight .k { color: #ff79c6; font-weight: bold }
145
+ .highlight .l { color: #f8f8f2 }
146
+ .highlight .n { color: #f8f8f2 }
147
+ .highlight .o { color: #ff79c6 }
148
+ .highlight .x { color: #f8f8f2 }
149
+ .highlight .p { color: #f8f8f2 }
150
+ .highlight .ch { color: #6272a4; font-style: italic }
151
+ .highlight .cm { color: #6272a4; font-style: italic }
152
+ .highlight .cp { color: #ff79c6 }
153
+ .highlight .cpf { color: #6272a4; font-style: italic }
154
+ .highlight .c1 { color: #6272a4; font-style: italic }
155
+ .highlight .cs { color: #6272a4; font-style: italic }
156
+ .highlight .gd { color: #8b080b }
157
+ .highlight .ge { font-style: italic }
158
+ .highlight .gr { color: #ff5555 }
159
+ .highlight .gh { color: #f8f8f2; font-weight: bold }
160
+ .highlight .gi { color: #50fa7b }
161
+ .highlight .go { color: #44475a }
162
+ .highlight .gp { color: #f8f8f2; font-weight: bold }
163
+ .highlight .gs { font-weight: bold }
164
+ .highlight .gu { color: #f8f8f2; font-weight: bold }
165
+ .highlight .gt { color: #ff5555 }
166
+ .highlight .kc { color: #ff79c6; font-weight: bold }
167
+ .highlight .kd { color: #8be9fd; font-style: italic }
168
+ .highlight .kn { color: #ff79c6; font-weight: bold }
169
+ .highlight .kp { color: #ff79c6 }
170
+ .highlight .kr { color: #ff79c6; font-weight: bold }
171
+ .highlight .kt { color: #8be9fd }
172
+ .highlight .ld { color: #f8f8f2 }
173
+ .highlight .m { color: #bd93f9 }
174
+ .highlight .s { color: #f1fa8c }
175
+ .highlight .na { color: #50fa7b }
176
+ .highlight .nb { color: #8be9fd; font-style: italic }
177
+ .highlight .nc { color: #50fa7b }
178
+ .highlight .no { color: #f8f8f2 }
179
+ .highlight .nd { color: #f8f8f2 }
180
+ .highlight .ni { color: #f8f8f2 }
181
+ .highlight .ne { color: #f8f8f2 }
182
+ .highlight .nf { color: #50fa7b }
183
+ .highlight .nl { color: #f8f8f2 }
184
+ .highlight .nn { color: #f8f8f2 }
185
+ .highlight .nx { color: #f8f8f2 }
186
+ .highlight .py { color: #f8f8f2 }
187
+ .highlight .nt { color: #ff79c6; font-weight: bold }
188
+ .highlight .nv { color: #8be9fd; font-style: italic }
189
+ .highlight .ow { color: #ff79c6; font-weight: bold }
190
+ .highlight .pm { color: #f8f8f2 }
191
+ .highlight .w { color: #f8f8f2 }
192
+ .highlight .mb { color: #bd93f9 }
193
+ .highlight .mf { color: #bd93f9 }
194
+ .highlight .mh { color: #bd93f9 }
195
+ .highlight .mi { color: #bd93f9 }
196
+ .highlight .mo { color: #bd93f9 }
197
+ .highlight .sa { color: #f1fa8c }
198
+ .highlight .sb { color: #f1fa8c }
199
+ .highlight .sc { color: #f1fa8c }
200
+ .highlight .dl { color: #f1fa8c }
201
+ .highlight .sd { color: #f1fa8c }
202
+ .highlight .s2 { color: #f1fa8c }
203
+ .highlight .se { color: #f1fa8c }
204
+ .highlight .sh { color: #f1fa8c }
205
+ .highlight .si { color: #f1fa8c }
206
+ .highlight .sx { color: #f1fa8c }
207
+ .highlight .sr { color: #f1fa8c }
208
+ .highlight .s1 { color: #f1fa8c }
209
+ .highlight .ss { color: #f1fa8c }
210
+ .highlight .bp { color: #f8f8f2 }
211
+ .highlight .fm { color: #50fa7b }
212
+ .highlight .vc { color: #8be9fd; font-style: italic }
213
+ .highlight .vg { color: #8be9fd; font-style: italic }
214
+ .highlight .vi { color: #8be9fd; font-style: italic }
215
+ .highlight .vm { color: #8be9fd; font-style: italic }
216
+ .highlight .il { color: #bd93f9 }
@@ -0,0 +1,57 @@
1
+ """Theme resolver: maps a name or CSS file path to a CSS string."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.resources
6
+ from pathlib import Path
7
+
8
+ # Built-in theme registry: name → filename inside the themes/ package directory
9
+ _BUILTIN_THEMES: dict[str, str] = {
10
+ "default": "default.css",
11
+ "dracula": "dracula.css",
12
+ }
13
+
14
+
15
+ def list_themes() -> list[str]:
16
+ """Return the names of all built-in themes."""
17
+ return list(_BUILTIN_THEMES.keys())
18
+
19
+
20
+ def load_theme(theme: str | None) -> str:
21
+ """Resolve *theme* to a CSS string.
22
+
23
+ *theme* may be:
24
+ - ``None`` or ``"default"`` → built-in default theme
25
+ - A registered theme name (e.g. ``"dracula"``)
26
+ - An absolute or relative path to a ``.css`` file
27
+
28
+ Raises:
29
+ FileNotFoundError: if *theme* is a path that does not exist.
30
+ ValueError: if *theme* is an unrecognised name.
31
+ """
32
+ if not theme:
33
+ theme = "default"
34
+
35
+ # Check if it looks like a file path
36
+ path = Path(theme)
37
+ if path.suffix.lower() == ".css":
38
+ if not path.is_file():
39
+ raise FileNotFoundError(
40
+ f"Theme CSS file not found: {path.resolve()}"
41
+ )
42
+ return path.read_text(encoding="utf-8")
43
+
44
+ # Treat as a built-in theme name
45
+ if theme not in _BUILTIN_THEMES:
46
+ choices = ", ".join(sorted(_BUILTIN_THEMES))
47
+ raise ValueError(
48
+ f"Unknown theme '{theme}'. "
49
+ f"Available built-in themes: {choices}. "
50
+ "You can also pass a path to a .css file."
51
+ )
52
+
53
+ filename = _BUILTIN_THEMES[theme]
54
+ # Use importlib.resources for reliable access inside installed packages
55
+ package = importlib.resources.files("docdo.themes")
56
+ css_path = package.joinpath(filename)
57
+ return css_path.read_text(encoding="utf-8")
docdo-0.0.2/uv.lock ADDED
@@ -0,0 +1,207 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.12"
4
+
5
+ [[package]]
6
+ name = "click"
7
+ version = "8.3.1"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ dependencies = [
10
+ { name = "colorama", marker = "sys_platform == 'win32'" },
11
+ ]
12
+ sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
13
+ wheels = [
14
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
15
+ ]
16
+
17
+ [[package]]
18
+ name = "colorama"
19
+ version = "0.4.6"
20
+ source = { registry = "https://pypi.org/simple" }
21
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
22
+ wheels = [
23
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
24
+ ]
25
+
26
+ [[package]]
27
+ name = "docdo"
28
+ version = "0.1.0"
29
+ source = { editable = "." }
30
+ dependencies = [
31
+ { name = "click" },
32
+ { name = "markdown-it-py" },
33
+ { name = "playwright" },
34
+ { name = "pygments" },
35
+ ]
36
+
37
+ [package.dev-dependencies]
38
+ dev = [
39
+ { name = "pytest" },
40
+ ]
41
+
42
+ [package.metadata]
43
+ requires-dist = [
44
+ { name = "click", specifier = ">=8.0" },
45
+ { name = "markdown-it-py", specifier = ">=3.0" },
46
+ { name = "playwright", specifier = ">=1.40" },
47
+ { name = "pygments", specifier = ">=2.17" },
48
+ ]
49
+
50
+ [package.metadata.requires-dev]
51
+ dev = [{ name = "pytest", specifier = ">=8.0" }]
52
+
53
+ [[package]]
54
+ name = "greenlet"
55
+ version = "3.3.2"
56
+ source = { registry = "https://pypi.org/simple" }
57
+ sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" }
58
+ wheels = [
59
+ { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" },
60
+ { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" },
61
+ { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" },
62
+ { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" },
63
+ { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" },
64
+ { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" },
65
+ { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" },
66
+ { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" },
67
+ { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" },
68
+ { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" },
69
+ { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" },
70
+ { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" },
71
+ { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" },
72
+ { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" },
73
+ { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" },
74
+ { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" },
75
+ { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" },
76
+ { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" },
77
+ { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" },
78
+ { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" },
79
+ { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" },
80
+ { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" },
81
+ { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" },
82
+ { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" },
83
+ { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" },
84
+ { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" },
85
+ { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" },
86
+ { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" },
87
+ { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" },
88
+ { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" },
89
+ { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" },
90
+ { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" },
91
+ { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" },
92
+ { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" },
93
+ { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" },
94
+ ]
95
+
96
+ [[package]]
97
+ name = "iniconfig"
98
+ version = "2.3.0"
99
+ source = { registry = "https://pypi.org/simple" }
100
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
101
+ wheels = [
102
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
103
+ ]
104
+
105
+ [[package]]
106
+ name = "markdown-it-py"
107
+ version = "4.0.0"
108
+ source = { registry = "https://pypi.org/simple" }
109
+ dependencies = [
110
+ { name = "mdurl" },
111
+ ]
112
+ sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
113
+ wheels = [
114
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
115
+ ]
116
+
117
+ [[package]]
118
+ name = "mdurl"
119
+ version = "0.1.2"
120
+ source = { registry = "https://pypi.org/simple" }
121
+ sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
122
+ wheels = [
123
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
124
+ ]
125
+
126
+ [[package]]
127
+ name = "packaging"
128
+ version = "26.0"
129
+ source = { registry = "https://pypi.org/simple" }
130
+ sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
131
+ wheels = [
132
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
133
+ ]
134
+
135
+ [[package]]
136
+ name = "playwright"
137
+ version = "1.58.0"
138
+ source = { registry = "https://pypi.org/simple" }
139
+ dependencies = [
140
+ { name = "greenlet" },
141
+ { name = "pyee" },
142
+ ]
143
+ wheels = [
144
+ { url = "https://files.pythonhosted.org/packages/f8/c9/9c6061d5703267f1baae6a4647bfd1862e386fbfdb97d889f6f6ae9e3f64/playwright-1.58.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:96e3204aac292ee639edbfdef6298b4be2ea0a55a16b7068df91adac077cc606", size = 42251098, upload-time = "2026-01-30T15:09:24.028Z" },
145
+ { url = "https://files.pythonhosted.org/packages/e0/40/59d34a756e02f8c670f0fee987d46f7ee53d05447d43cd114ca015cb168c/playwright-1.58.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:70c763694739d28df71ed578b9c8202bb83e8fe8fb9268c04dd13afe36301f71", size = 41039625, upload-time = "2026-01-30T15:09:27.558Z" },
146
+ { url = "https://files.pythonhosted.org/packages/e1/ee/3ce6209c9c74a650aac9028c621f357a34ea5cd4d950700f8e2c4b7fe2c4/playwright-1.58.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:185e0132578733d02802dfddfbbc35f42be23a45ff49ccae5081f25952238117", size = 42251098, upload-time = "2026-01-30T15:09:30.461Z" },
147
+ { url = "https://files.pythonhosted.org/packages/f1/af/009958cbf23fac551a940d34e3206e6c7eed2b8c940d0c3afd1feb0b0589/playwright-1.58.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:c95568ba1eda83812598c1dc9be60b4406dffd60b149bc1536180ad108723d6b", size = 46235268, upload-time = "2026-01-30T15:09:33.787Z" },
148
+ { url = "https://files.pythonhosted.org/packages/d9/a6/0e66ad04b6d3440dae73efb39540c5685c5fc95b17c8b29340b62abbd952/playwright-1.58.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f9999948f1ab541d98812de25e3a8c410776aa516d948807140aff797b4bffa", size = 45964214, upload-time = "2026-01-30T15:09:36.751Z" },
149
+ { url = "https://files.pythonhosted.org/packages/0e/4b/236e60ab9f6d62ed0fd32150d61f1f494cefbf02304c0061e78ed80c1c32/playwright-1.58.0-py3-none-win32.whl", hash = "sha256:1e03be090e75a0fabbdaeab65ce17c308c425d879fa48bb1d7986f96bfad0b99", size = 36815998, upload-time = "2026-01-30T15:09:39.627Z" },
150
+ { url = "https://files.pythonhosted.org/packages/41/f8/5ec599c5e59d2f2f336a05b4f318e733077cd5044f24adb6f86900c3e6a7/playwright-1.58.0-py3-none-win_amd64.whl", hash = "sha256:a2bf639d0ce33b3ba38de777e08697b0d8f3dc07ab6802e4ac53fb65e3907af8", size = 36816005, upload-time = "2026-01-30T15:09:42.449Z" },
151
+ { url = "https://files.pythonhosted.org/packages/c8/c4/cc0229fea55c87d6c9c67fe44a21e2cd28d1d558a5478ed4d617e9fb0c93/playwright-1.58.0-py3-none-win_arm64.whl", hash = "sha256:32ffe5c303901a13a0ecab91d1c3f74baf73b84f4bedbb6b935f5bc11cc98e1b", size = 33085919, upload-time = "2026-01-30T15:09:45.71Z" },
152
+ ]
153
+
154
+ [[package]]
155
+ name = "pluggy"
156
+ version = "1.6.0"
157
+ source = { registry = "https://pypi.org/simple" }
158
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
159
+ wheels = [
160
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
161
+ ]
162
+
163
+ [[package]]
164
+ name = "pyee"
165
+ version = "13.0.1"
166
+ source = { registry = "https://pypi.org/simple" }
167
+ dependencies = [
168
+ { name = "typing-extensions" },
169
+ ]
170
+ sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" }
171
+ wheels = [
172
+ { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" },
173
+ ]
174
+
175
+ [[package]]
176
+ name = "pygments"
177
+ version = "2.19.2"
178
+ source = { registry = "https://pypi.org/simple" }
179
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
180
+ wheels = [
181
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
182
+ ]
183
+
184
+ [[package]]
185
+ name = "pytest"
186
+ version = "9.0.2"
187
+ source = { registry = "https://pypi.org/simple" }
188
+ dependencies = [
189
+ { name = "colorama", marker = "sys_platform == 'win32'" },
190
+ { name = "iniconfig" },
191
+ { name = "packaging" },
192
+ { name = "pluggy" },
193
+ { name = "pygments" },
194
+ ]
195
+ sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
196
+ wheels = [
197
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
198
+ ]
199
+
200
+ [[package]]
201
+ name = "typing-extensions"
202
+ version = "4.15.0"
203
+ source = { registry = "https://pypi.org/simple" }
204
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
205
+ wheels = [
206
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
207
+ ]