markserv 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- markserv-1.0.0/PKG-INFO +111 -0
- markserv-1.0.0/README.md +94 -0
- markserv-1.0.0/pyproject.toml +85 -0
- markserv-1.0.0/src/markserv/__init__.py +3 -0
- markserv-1.0.0/src/markserv/__main__.py +3 -0
- markserv-1.0.0/src/markserv/app.py +4 -0
- markserv-1.0.0/src/markserv/cli.py +279 -0
- markserv-1.0.0/src/markserv/demo.py +178 -0
- markserv-1.0.0/src/markserv/icons.py +182 -0
- markserv-1.0.0/src/markserv/public/css/app.css +675 -0
- markserv-1.0.0/src/markserv/public/css/github-markdown-dark.css +1124 -0
- markserv-1.0.0/src/markserv/public/css/github-markdown-light.css +1124 -0
- markserv-1.0.0/src/markserv/public/js/clipboard.js +31 -0
- markserv-1.0.0/src/markserv/public/js/dev-reload.js +30 -0
- markserv-1.0.0/src/markserv/public/js/favicon.js +29 -0
- markserv-1.0.0/src/markserv/public/js/sidebar.js +100 -0
- markserv-1.0.0/src/markserv/public/js/theme.js +130 -0
- markserv-1.0.0/src/markserv/public/licenses/github-markdown-css.LICENSE +9 -0
- markserv-1.0.0/src/markserv/public/licenses/htmx.LICENSE +13 -0
- markserv-1.0.0/src/markserv/public/vendor/htmx.min.js +1 -0
- markserv-1.0.0/src/markserv/public/vendor/sse.js +290 -0
- markserv-1.0.0/src/markserv/rendering.py +458 -0
- markserv-1.0.0/src/markserv/site.py +401 -0
- markserv-1.0.0/src/markserv/web.py +313 -0
markserv-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: markserv
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Live-render GitHub-flavored markdown files and folders
|
|
5
|
+
Author: Nathan Gage
|
|
6
|
+
Author-email: Nathan Gage <nathan@sweetspot.so>
|
|
7
|
+
Requires-Dist: cmarkgfm>=2024.11.20
|
|
8
|
+
Requires-Dist: cyclopts>=4.10.1
|
|
9
|
+
Requires-Dist: fasthx[htmy]>=3.1.0
|
|
10
|
+
Requires-Dist: fastapi>=0.115.12
|
|
11
|
+
Requires-Dist: ignoretree>=0.2.0
|
|
12
|
+
Requires-Dist: rich>=14.0.0
|
|
13
|
+
Requires-Dist: uvicorn>=0.34.0
|
|
14
|
+
Requires-Dist: watchfiles>=1.0.5
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# markserv
|
|
19
|
+
|
|
20
|
+
`markserv` is a small FastAPI-based markdown preview server for local docs.
|
|
21
|
+
|
|
22
|
+
It uses:
|
|
23
|
+
|
|
24
|
+
- [`cmarkgfm`](https://github.com/theacodes/cmarkgfm) for GitHub-flavored markdown rendering
|
|
25
|
+
- [`FastAPI`](https://fastapi.tiangolo.com/) for the local web app
|
|
26
|
+
- [`FastHX`](https://github.com/volfpeter/fasthx) for HTMX-aware FastAPI rendering
|
|
27
|
+
- [`htmy`](https://github.com/volfpeter/htmy) for Python component-based HTML rendering
|
|
28
|
+
- [`github-markdown-css`](https://github.com/sindresorhus/github-markdown-css) for GitHub-like styling
|
|
29
|
+
- [`watchfiles`](https://github.com/samuelcolvin/watchfiles) for live reload
|
|
30
|
+
- [`ignoretree`](https://pypi.org/project/ignoretree/) to respect `.gitignore` rules while scanning
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
Once published:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uv tool install markserv
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For local development:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv tool install .
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
Serve a directory of markdown files:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
markserv docs/
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Serve a single markdown file:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
markserv README.md
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Run the built-in synthetic demo site:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
markserv.demo
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Options:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
markserv .
|
|
70
|
+
markserv --host localhost --port 4422 .
|
|
71
|
+
markserv --no-open README.md
|
|
72
|
+
markserv.demo --no-open --port 9001
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Behavior
|
|
76
|
+
|
|
77
|
+
- Renders common markdown extensions like `.md` and `.markdown`
|
|
78
|
+
- Watches markdown files and reloads the browser when content changes
|
|
79
|
+
- Respects `.gitignore` while scanning so ignored trees like `.venv/` are skipped
|
|
80
|
+
- Serves linked local assets like images from the same file tree
|
|
81
|
+
- In directory mode, shows a sidebar for browsing multiple markdown pages
|
|
82
|
+
- Includes a system/light/dark theme control that remembers your choice in browser storage
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
Install dev tooling:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
make install
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Common commands:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
make format
|
|
96
|
+
make format-check
|
|
97
|
+
make lint
|
|
98
|
+
make typecheck
|
|
99
|
+
make test
|
|
100
|
+
make all-ci
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Notes
|
|
104
|
+
|
|
105
|
+
- This is intended for plain markdown / GFM-style docs, not MDX.
|
|
106
|
+
- UI components are rendered with `htmy` from Python.
|
|
107
|
+
- Front-end assets live under `src/markserv/public/`.
|
|
108
|
+
- Bundled CSS comes from `github-markdown-css`.
|
|
109
|
+
- Bundled HTMX assets are used for SSE-driven live updates.
|
|
110
|
+
- The upstream stylesheet license is included at `src/markserv/public/licenses/github-markdown-css.LICENSE`.
|
|
111
|
+
- The bundled HTMX license is included at `src/markserv/public/licenses/htmx.LICENSE`.
|
markserv-1.0.0/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# markserv
|
|
2
|
+
|
|
3
|
+
`markserv` is a small FastAPI-based markdown preview server for local docs.
|
|
4
|
+
|
|
5
|
+
It uses:
|
|
6
|
+
|
|
7
|
+
- [`cmarkgfm`](https://github.com/theacodes/cmarkgfm) for GitHub-flavored markdown rendering
|
|
8
|
+
- [`FastAPI`](https://fastapi.tiangolo.com/) for the local web app
|
|
9
|
+
- [`FastHX`](https://github.com/volfpeter/fasthx) for HTMX-aware FastAPI rendering
|
|
10
|
+
- [`htmy`](https://github.com/volfpeter/htmy) for Python component-based HTML rendering
|
|
11
|
+
- [`github-markdown-css`](https://github.com/sindresorhus/github-markdown-css) for GitHub-like styling
|
|
12
|
+
- [`watchfiles`](https://github.com/samuelcolvin/watchfiles) for live reload
|
|
13
|
+
- [`ignoretree`](https://pypi.org/project/ignoretree/) to respect `.gitignore` rules while scanning
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
Once published:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv tool install markserv
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For local development:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv tool install .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Serve a directory of markdown files:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
markserv docs/
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Serve a single markdown file:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
markserv README.md
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Run the built-in synthetic demo site:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
markserv.demo
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Options:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
markserv .
|
|
53
|
+
markserv --host localhost --port 4422 .
|
|
54
|
+
markserv --no-open README.md
|
|
55
|
+
markserv.demo --no-open --port 9001
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Behavior
|
|
59
|
+
|
|
60
|
+
- Renders common markdown extensions like `.md` and `.markdown`
|
|
61
|
+
- Watches markdown files and reloads the browser when content changes
|
|
62
|
+
- Respects `.gitignore` while scanning so ignored trees like `.venv/` are skipped
|
|
63
|
+
- Serves linked local assets like images from the same file tree
|
|
64
|
+
- In directory mode, shows a sidebar for browsing multiple markdown pages
|
|
65
|
+
- Includes a system/light/dark theme control that remembers your choice in browser storage
|
|
66
|
+
|
|
67
|
+
## Development
|
|
68
|
+
|
|
69
|
+
Install dev tooling:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
make install
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Common commands:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
make format
|
|
79
|
+
make format-check
|
|
80
|
+
make lint
|
|
81
|
+
make typecheck
|
|
82
|
+
make test
|
|
83
|
+
make all-ci
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Notes
|
|
87
|
+
|
|
88
|
+
- This is intended for plain markdown / GFM-style docs, not MDX.
|
|
89
|
+
- UI components are rendered with `htmy` from Python.
|
|
90
|
+
- Front-end assets live under `src/markserv/public/`.
|
|
91
|
+
- Bundled CSS comes from `github-markdown-css`.
|
|
92
|
+
- Bundled HTMX assets are used for SSE-driven live updates.
|
|
93
|
+
- The upstream stylesheet license is included at `src/markserv/public/licenses/github-markdown-css.LICENSE`.
|
|
94
|
+
- The bundled HTMX license is included at `src/markserv/public/licenses/htmx.LICENSE`.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "markserv"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Live-render GitHub-flavored markdown files and folders"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "Nathan Gage", email = "nathan@sweetspot.so" }]
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"cmarkgfm>=2024.11.20",
|
|
10
|
+
"cyclopts>=4.10.1",
|
|
11
|
+
"fasthx[htmy]>=3.1.0",
|
|
12
|
+
"fastapi>=0.115.12",
|
|
13
|
+
"ignoretree>=0.2.0",
|
|
14
|
+
"rich>=14.0.0",
|
|
15
|
+
"uvicorn>=0.34.0",
|
|
16
|
+
"watchfiles>=1.0.5",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[dependency-groups]
|
|
20
|
+
dev = ["httpx>=0.28.1", "pytest>=8.4.0"]
|
|
21
|
+
lint = ["mypy>=1.15.0", "pyright>=1.1.408", "ruff>=0.14.0", "taplo>=0.9.3"]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
markserv = "markserv.cli:main"
|
|
25
|
+
"markserv.demo" = "markserv.demo:main"
|
|
26
|
+
|
|
27
|
+
[tool.uv]
|
|
28
|
+
default-groups = ["dev", "lint"]
|
|
29
|
+
|
|
30
|
+
[tool.uv.build-backend]
|
|
31
|
+
package-data = { "markserv" = [
|
|
32
|
+
"public/**/*.css",
|
|
33
|
+
"public/**/*.js",
|
|
34
|
+
"public/**/*.LICENSE",
|
|
35
|
+
] }
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
line-length = 120
|
|
39
|
+
target-version = "py311"
|
|
40
|
+
include = ["src/**/*.py", "tests/**/*.py"]
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint]
|
|
43
|
+
extend-select = ["B", "FAST", "I", "Q", "RUF100", "UP"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint.flake8-quotes]
|
|
46
|
+
inline-quotes = "double"
|
|
47
|
+
multiline-quotes = "double"
|
|
48
|
+
|
|
49
|
+
[tool.ruff.format]
|
|
50
|
+
docstring-code-format = false
|
|
51
|
+
quote-style = "double"
|
|
52
|
+
|
|
53
|
+
[tool.pyright]
|
|
54
|
+
pythonVersion = "3.11"
|
|
55
|
+
typeCheckingMode = "standard"
|
|
56
|
+
reportMissingTypeStubs = false
|
|
57
|
+
reportUnnecessaryTypeIgnoreComment = true
|
|
58
|
+
include = ["src", "tests"]
|
|
59
|
+
stubPath = "typings"
|
|
60
|
+
venvPath = "."
|
|
61
|
+
venv = ".venv"
|
|
62
|
+
executionEnvironments = [
|
|
63
|
+
{ root = ".", extraPaths = [
|
|
64
|
+
"src",
|
|
65
|
+
] },
|
|
66
|
+
{ root = "tests", extraPaths = [
|
|
67
|
+
"src",
|
|
68
|
+
] },
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.mypy]
|
|
72
|
+
python_version = "3.11"
|
|
73
|
+
files = ["src", "tests"]
|
|
74
|
+
mypy_path = "$MYPY_CONFIG_FILE_DIR/src:$MYPY_CONFIG_FILE_DIR/typings"
|
|
75
|
+
strict = true
|
|
76
|
+
|
|
77
|
+
[tool.pytest.ini_options]
|
|
78
|
+
addopts = "-ra"
|
|
79
|
+
testpaths = ["tests"]
|
|
80
|
+
xfail_strict = true
|
|
81
|
+
filterwarnings = ["error"]
|
|
82
|
+
|
|
83
|
+
[build-system]
|
|
84
|
+
requires = ["uv_build>=0.11.2,<0.12.0"]
|
|
85
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import select
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
import webbrowser
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Annotated, Any, Protocol
|
|
13
|
+
|
|
14
|
+
import uvicorn
|
|
15
|
+
from cyclopts import App, Parameter
|
|
16
|
+
from cyclopts.help import PlainFormatter
|
|
17
|
+
from cyclopts.token import Token
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.logging import RichHandler
|
|
20
|
+
from uvicorn import Config, Server
|
|
21
|
+
|
|
22
|
+
from .app import create_app
|
|
23
|
+
from .site import build_config, build_file_site
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StoppableServer(Protocol):
|
|
27
|
+
should_exit: bool
|
|
28
|
+
|
|
29
|
+
def run(self) -> None: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
console = Console(stderr=True)
|
|
33
|
+
QUIT_KEYS = {"q", "Q", "\x1b"}
|
|
34
|
+
DEFAULT_HOST = "localhost"
|
|
35
|
+
DEFAULT_PORT = 4422
|
|
36
|
+
PYTHON_RELOAD_ENV_VAR = "MARKSERV_PYTHON_RELOAD"
|
|
37
|
+
TARGET_ENV_VAR = "MARKSERV_TARGET"
|
|
38
|
+
PYTHON_RELOAD_DIR = Path(__file__).resolve().parent
|
|
39
|
+
|
|
40
|
+
app = App(
|
|
41
|
+
name="markserv",
|
|
42
|
+
help="Render a folder of GitHub-flavored markdown with live reload.",
|
|
43
|
+
help_formatter=PlainFormatter(),
|
|
44
|
+
result_action="return_value",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _validate_target(_type_: Any, tokens: tuple[Token, ...]) -> Path:
|
|
49
|
+
raw_path = Path(tokens[0].value)
|
|
50
|
+
build_config(raw_path)
|
|
51
|
+
return raw_path
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def configure_logging() -> None:
|
|
55
|
+
logging.basicConfig(
|
|
56
|
+
level=logging.INFO,
|
|
57
|
+
format="%(message)s",
|
|
58
|
+
handlers=[
|
|
59
|
+
RichHandler(
|
|
60
|
+
show_time=False,
|
|
61
|
+
show_level=False,
|
|
62
|
+
show_path=False,
|
|
63
|
+
markup=False,
|
|
64
|
+
rich_tracebacks=True,
|
|
65
|
+
console=console,
|
|
66
|
+
)
|
|
67
|
+
],
|
|
68
|
+
force=True,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def browser_url(host: str, port: int) -> str:
|
|
73
|
+
public_host = "localhost" if host in {"127.0.0.1", "0.0.0.0", "localhost"} else host
|
|
74
|
+
return f"http://{public_host}:{port}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def python_reload_enabled() -> bool:
|
|
78
|
+
value = os.environ.get(PYTHON_RELOAD_ENV_VAR, "")
|
|
79
|
+
return value.lower() in {"1", "true", "yes", "on"}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@contextlib.contextmanager
|
|
83
|
+
def temporary_env(updates: Mapping[str, str]) -> Any:
|
|
84
|
+
previous = {key: os.environ.get(key) for key in updates}
|
|
85
|
+
os.environ.update(updates)
|
|
86
|
+
try:
|
|
87
|
+
yield
|
|
88
|
+
finally:
|
|
89
|
+
for key, value in previous.items():
|
|
90
|
+
if value is None:
|
|
91
|
+
os.environ.pop(key, None)
|
|
92
|
+
else:
|
|
93
|
+
os.environ[key] = value
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def print_startup_banner(*, source: str, root_dir: str, url: str, open_browser: bool, python_reload: bool) -> None:
|
|
97
|
+
quit_hint = (
|
|
98
|
+
"Press Ctrl+C to quit."
|
|
99
|
+
if python_reload
|
|
100
|
+
else "Press Q or Esc to quit."
|
|
101
|
+
if _supports_quit_prompt()
|
|
102
|
+
else "Press Ctrl+C to quit."
|
|
103
|
+
)
|
|
104
|
+
browser_hint = "Browser opens automatically." if open_browser else "Browser auto-open disabled."
|
|
105
|
+
reload_hint = "Python reload enabled via MARKSERV_PYTHON_RELOAD." if python_reload else None
|
|
106
|
+
display_url = url.removeprefix("http://")
|
|
107
|
+
|
|
108
|
+
console.print(f"[bold cyan]markserv[/] serving {source}")
|
|
109
|
+
console.print(f"[cyan]root[/] {root_dir}")
|
|
110
|
+
console.print(f"[cyan]url[/] [link={url}][underline]{display_url}[/underline][/link]")
|
|
111
|
+
console.print(f"[dim]{browser_hint}[/]")
|
|
112
|
+
if reload_hint is not None:
|
|
113
|
+
console.print(f"[dim]{reload_hint}[/]")
|
|
114
|
+
console.print(f"[dim]{quit_hint}[/]")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def create_server(app: Any, *, host: str, port: int) -> Server:
|
|
118
|
+
return Server(
|
|
119
|
+
Config(
|
|
120
|
+
app,
|
|
121
|
+
host=host,
|
|
122
|
+
port=port,
|
|
123
|
+
log_level="warning",
|
|
124
|
+
access_log=False,
|
|
125
|
+
log_config=None,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def run_python_reloading_server(app_factory_import: str, *, host: str, port: int) -> None:
|
|
131
|
+
uvicorn.run(
|
|
132
|
+
app_factory_import,
|
|
133
|
+
factory=True,
|
|
134
|
+
host=host,
|
|
135
|
+
port=port,
|
|
136
|
+
reload=True,
|
|
137
|
+
reload_dirs=[str(PYTHON_RELOAD_DIR)],
|
|
138
|
+
log_level="warning",
|
|
139
|
+
access_log=False,
|
|
140
|
+
log_config=None,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _supports_quit_prompt() -> bool:
|
|
145
|
+
return sys.stdin.isatty() and os.name != "nt"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _request_server_shutdown(server: StoppableServer, stop_event: threading.Event) -> None:
|
|
149
|
+
if stop_event.is_set():
|
|
150
|
+
return
|
|
151
|
+
console.print("[dim]Stopping server...[/dim]")
|
|
152
|
+
server.should_exit = True
|
|
153
|
+
stop_event.set()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _listen_for_quit_keys(server: StoppableServer, stop_event: threading.Event) -> None:
|
|
157
|
+
import termios
|
|
158
|
+
import tty
|
|
159
|
+
|
|
160
|
+
with contextlib.suppress(termios.error, ValueError, OSError):
|
|
161
|
+
fd = sys.stdin.fileno()
|
|
162
|
+
original_attrs = termios.tcgetattr(fd)
|
|
163
|
+
try:
|
|
164
|
+
tty.setcbreak(fd)
|
|
165
|
+
while not stop_event.is_set():
|
|
166
|
+
readable, _writable, _errors = select.select([sys.stdin], [], [], 0.1)
|
|
167
|
+
if not readable:
|
|
168
|
+
continue
|
|
169
|
+
key = sys.stdin.read(1)
|
|
170
|
+
if key in QUIT_KEYS:
|
|
171
|
+
_request_server_shutdown(server, stop_event)
|
|
172
|
+
return
|
|
173
|
+
finally:
|
|
174
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, original_attrs)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def run_server(server: StoppableServer) -> None:
|
|
178
|
+
stop_event = threading.Event()
|
|
179
|
+
listener: threading.Thread | None = None
|
|
180
|
+
|
|
181
|
+
if _supports_quit_prompt():
|
|
182
|
+
listener = threading.Thread(
|
|
183
|
+
target=_listen_for_quit_keys,
|
|
184
|
+
args=(server, stop_event),
|
|
185
|
+
daemon=True,
|
|
186
|
+
name="markserv-quit-listener",
|
|
187
|
+
)
|
|
188
|
+
listener.start()
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
server.run()
|
|
192
|
+
finally:
|
|
193
|
+
stop_event.set()
|
|
194
|
+
if listener is not None:
|
|
195
|
+
listener.join(timeout=0.2)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def serve_application(
|
|
199
|
+
application: Any | None,
|
|
200
|
+
*,
|
|
201
|
+
source: str,
|
|
202
|
+
root_dir: str,
|
|
203
|
+
host: str,
|
|
204
|
+
port: int,
|
|
205
|
+
open_browser: bool,
|
|
206
|
+
app_factory_import: str | None = None,
|
|
207
|
+
env_updates: Mapping[str, str] | None = None,
|
|
208
|
+
) -> None:
|
|
209
|
+
configure_logging()
|
|
210
|
+
url = browser_url(host, port)
|
|
211
|
+
python_reload = python_reload_enabled()
|
|
212
|
+
print_startup_banner(
|
|
213
|
+
source=source,
|
|
214
|
+
root_dir=root_dir,
|
|
215
|
+
url=url,
|
|
216
|
+
open_browser=open_browser,
|
|
217
|
+
python_reload=python_reload,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if open_browser:
|
|
221
|
+
threading.Timer(0.8, lambda: webbrowser.open(url)).start()
|
|
222
|
+
|
|
223
|
+
if python_reload:
|
|
224
|
+
if app_factory_import is None:
|
|
225
|
+
raise ValueError("app_factory_import is required when Python reload is enabled")
|
|
226
|
+
with temporary_env(dict(env_updates or {})):
|
|
227
|
+
run_python_reloading_server(app_factory_import, host=host, port=port)
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
if application is None:
|
|
231
|
+
raise ValueError("application is required when Python reload is disabled")
|
|
232
|
+
|
|
233
|
+
server = create_server(application, host=host, port=port)
|
|
234
|
+
run_server(server)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@app.default
|
|
238
|
+
def serve(
|
|
239
|
+
path: Annotated[
|
|
240
|
+
Path,
|
|
241
|
+
Parameter(
|
|
242
|
+
converter=_validate_target,
|
|
243
|
+
help="Markdown file or directory to serve.",
|
|
244
|
+
),
|
|
245
|
+
] = Path("."),
|
|
246
|
+
/,
|
|
247
|
+
*,
|
|
248
|
+
host: Annotated[str, Parameter(help="Host interface to bind.")] = DEFAULT_HOST,
|
|
249
|
+
port: Annotated[int, Parameter(help="Port to listen on.")] = DEFAULT_PORT,
|
|
250
|
+
open_browser: Annotated[
|
|
251
|
+
bool,
|
|
252
|
+
Parameter(name="--open", help="Open the app in your default browser after the server starts."),
|
|
253
|
+
] = True,
|
|
254
|
+
) -> None:
|
|
255
|
+
"""Serve GitHub-flavored markdown from a file or directory."""
|
|
256
|
+
config = build_config(path)
|
|
257
|
+
site = build_file_site(config)
|
|
258
|
+
serve_application(
|
|
259
|
+
None if python_reload_enabled() else create_app(site),
|
|
260
|
+
source=str(config.source),
|
|
261
|
+
root_dir=str(config.root_dir),
|
|
262
|
+
host=host,
|
|
263
|
+
port=port,
|
|
264
|
+
open_browser=open_browser,
|
|
265
|
+
app_factory_import="markserv.cli:create_app_from_env",
|
|
266
|
+
env_updates={TARGET_ENV_VAR: str(config.source)},
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def create_app_from_env() -> Any:
|
|
271
|
+
target = os.environ.get(TARGET_ENV_VAR)
|
|
272
|
+
if not target:
|
|
273
|
+
raise RuntimeError(f"{TARGET_ENV_VAR} must be set when Python reload is enabled")
|
|
274
|
+
config = build_config(Path(target))
|
|
275
|
+
return create_app(build_file_site(config))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def main(argv: list[str] | None = None) -> None:
|
|
279
|
+
app(tokens=argv)
|