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.
- docdo-0.0.2/.github/workflows/pypi.yml +44 -0
- docdo-0.0.2/.gitignore +10 -0
- docdo-0.0.2/.python-version +1 -0
- docdo-0.0.2/LICENSE +21 -0
- docdo-0.0.2/PKG-INFO +54 -0
- docdo-0.0.2/README.md +42 -0
- docdo-0.0.2/main.py +4 -0
- docdo-0.0.2/pyproject.toml +25 -0
- docdo-0.0.2/pyrightconfig.json +12 -0
- docdo-0.0.2/src/docdo/__init__.py +5 -0
- docdo-0.0.2/src/docdo/cli.py +113 -0
- docdo-0.0.2/src/docdo/converter.py +38 -0
- docdo-0.0.2/src/docdo/pdf.py +45 -0
- docdo-0.0.2/src/docdo/renderer.py +90 -0
- docdo-0.0.2/src/docdo/themes/__init__.py +5 -0
- docdo-0.0.2/src/docdo/themes/default.css +193 -0
- docdo-0.0.2/src/docdo/themes/dracula.css +216 -0
- docdo-0.0.2/src/docdo/themes/resolver.py +57 -0
- docdo-0.0.2/uv.lock +207 -0
|
@@ -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 @@
|
|
|
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,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,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,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
|
+
]
|