kpress 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kpress/__init__.py +55 -0
- kpress/_version.py +14 -0
- kpress/cli.py +371 -0
- kpress/contract.py +396 -0
- kpress/errors.py +27 -0
- kpress/format/__init__.py +56 -0
- kpress/format/assets.py +301 -0
- kpress/format/markdown.py +993 -0
- kpress/format/model.py +267 -0
- kpress/format/pdf.py +152 -0
- kpress/format/render.py +614 -0
- kpress/format/sanitize.py +509 -0
- kpress/format/static/css/components.css +1840 -0
- kpress/format/static/css/document.css +459 -0
- kpress/format/static/css/page-reset.css +12 -0
- kpress/format/static/css/print.css +351 -0
- kpress/format/static/css/style-tokens.css +454 -0
- kpress/format/static/css/syntax.css +263 -0
- kpress/format/static/css/theme-dark.css +11 -0
- kpress/format/static/css/theme-light.css +14 -0
- kpress/format/static/fonts/pt-serif-latin-400-italic.woff2 +0 -0
- kpress/format/static/fonts/pt-serif-latin-400-normal.woff2 +0 -0
- kpress/format/static/fonts/pt-serif-latin-700-italic.woff2 +0 -0
- kpress/format/static/fonts/pt-serif-latin-700-normal.woff2 +0 -0
- kpress/format/static/fonts/source-sans-3-latin-wght-italic.woff2 +0 -0
- kpress/format/static/fonts/source-sans-3-latin-wght-normal.woff2 +0 -0
- kpress/format/static/icons/icons.svg +63 -0
- kpress/format/static/js/code-copy.js +60 -0
- kpress/format/static/js/diagrams.js +109 -0
- kpress/format/static/js/host.js +246 -0
- kpress/format/static/js/icons.js +15 -0
- kpress/format/static/js/menu.js +73 -0
- kpress/format/static/js/overlay.js +165 -0
- kpress/format/static/js/runtime.js +505 -0
- kpress/format/static/js/settings-widget.js +184 -0
- kpress/format/static/js/tables.js +45 -0
- kpress/format/static/js/tabs.js +175 -0
- kpress/format/static/js/theme-bootstrap.js +43 -0
- kpress/format/static/js/theme.js +148 -0
- kpress/format/static/js/toc.js +310 -0
- kpress/format/static/js/tooltips.js +795 -0
- kpress/format/static/js/video-popover.js +116 -0
- kpress/format/static/js/viewport.js +163 -0
- kpress/format/static/katex/VERSION +1 -0
- kpress/format/static/katex/auto-render.min.js +1 -0
- kpress/format/static/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- kpress/format/static/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- kpress/format/static/katex/katex-init.js +44 -0
- kpress/format/static/katex/katex.min.css +1 -0
- kpress/format/static/katex/katex.min.js +1 -0
- kpress/format/templates/page.html.jinja +33 -0
- kpress/format/templating.py +47 -0
- kpress/format/theme.py +49 -0
- kpress/format/validate.py +22 -0
- kpress/licenses/katex.txt +21 -0
- kpress/licenses/lucide.txt +59 -0
- kpress/licenses/pt-serif.txt +93 -0
- kpress/licenses/source-sans-3.txt +94 -0
- kpress/models.py +82 -0
- kpress/output.py +31 -0
- kpress/publish/__init__.py +43 -0
- kpress/publish/assets/__init__.py +20 -0
- kpress/publish/assets/copy.py +164 -0
- kpress/publish/build.py +758 -0
- kpress/publish/cache.py +77 -0
- kpress/publish/capability.py +189 -0
- kpress/publish/config.py +591 -0
- kpress/publish/discover.py +89 -0
- kpress/publish/frontmatter.py +108 -0
- kpress/publish/manifest.py +135 -0
- kpress/publish/optimize.py +466 -0
- kpress/publish/optimizer_tool/package-lock.json +650 -0
- kpress/publish/optimizer_tool/package.json +8 -0
- kpress/publish/routes.py +190 -0
- kpress/publish/site_files.py +72 -0
- kpress/py.typed +1 -0
- kpress/runtime.py +443 -0
- kpress/workflow/__init__.py +18 -0
- kpress/workflow/convert.py +66 -0
- kpress/workflow/export.py +37 -0
- kpress/workflow/format.py +49 -0
- kpress/workflow/paste.py +25 -0
- kpress/workflow/workspace.py +40 -0
- kpress-0.1.0.dist-info/METADATA +177 -0
- kpress-0.1.0.dist-info/RECORD +107 -0
- kpress-0.1.0.dist-info/WHEEL +4 -0
- kpress-0.1.0.dist-info/entry_points.txt +2 -0
- kpress-0.1.0.dist-info/licenses/LICENSE +235 -0
- kpress-0.1.0.dist-info/licenses/NOTICE.md +36 -0
kpress/__init__.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""KPress document rendering and publishing package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from kpress._version import __version__
|
|
6
|
+
from kpress.contract import (
|
|
7
|
+
ASSET_MANIFEST_SCHEMA_VERSION,
|
|
8
|
+
BUILD_MANIFEST_SCHEMA_VERSION,
|
|
9
|
+
CONTRACT_VERSION,
|
|
10
|
+
)
|
|
11
|
+
from kpress.errors import (
|
|
12
|
+
KPressAssetNotFoundError,
|
|
13
|
+
KPressInvalidRequestError,
|
|
14
|
+
KPressMissingOptionalDependencyError,
|
|
15
|
+
KPressOptimizerError,
|
|
16
|
+
KPressPublishError,
|
|
17
|
+
KPressRenderError,
|
|
18
|
+
)
|
|
19
|
+
from kpress.models import (
|
|
20
|
+
KPressAsset,
|
|
21
|
+
KPressExportRequest,
|
|
22
|
+
KPressRenderRequest,
|
|
23
|
+
PrintProfile,
|
|
24
|
+
ThemeMode,
|
|
25
|
+
)
|
|
26
|
+
from kpress.runtime import (
|
|
27
|
+
clear_render_cache,
|
|
28
|
+
export_document,
|
|
29
|
+
get_static_asset,
|
|
30
|
+
render_view,
|
|
31
|
+
set_static_root_for_tests,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"ASSET_MANIFEST_SCHEMA_VERSION",
|
|
36
|
+
"BUILD_MANIFEST_SCHEMA_VERSION",
|
|
37
|
+
"CONTRACT_VERSION",
|
|
38
|
+
"KPressAsset",
|
|
39
|
+
"KPressAssetNotFoundError",
|
|
40
|
+
"KPressExportRequest",
|
|
41
|
+
"KPressInvalidRequestError",
|
|
42
|
+
"KPressMissingOptionalDependencyError",
|
|
43
|
+
"KPressOptimizerError",
|
|
44
|
+
"KPressPublishError",
|
|
45
|
+
"KPressRenderError",
|
|
46
|
+
"KPressRenderRequest",
|
|
47
|
+
"PrintProfile",
|
|
48
|
+
"ThemeMode",
|
|
49
|
+
"__version__",
|
|
50
|
+
"clear_render_cache",
|
|
51
|
+
"export_document",
|
|
52
|
+
"get_static_asset",
|
|
53
|
+
"render_view",
|
|
54
|
+
"set_static_root_for_tests",
|
|
55
|
+
]
|
kpress/_version.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""KPress package version.
|
|
2
|
+
|
|
3
|
+
The canonical version comes from the git tag via uv-dynamic-versioning at
|
|
4
|
+
build time; at runtime it is read from the installed package metadata.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
__version__ = version("kpress")
|
|
13
|
+
except PackageNotFoundError: # Source tree without an installed distribution.
|
|
14
|
+
__version__ = "0.0.0+unknown"
|
kpress/cli.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""KPress command line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Callable, Sequence
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import cast
|
|
12
|
+
|
|
13
|
+
from kpress._version import __version__
|
|
14
|
+
from kpress.errors import KPressMissingOptionalDependencyError
|
|
15
|
+
from kpress.format import DocumentInput, RenderOptions, render_page
|
|
16
|
+
from kpress.output import write_text_atomic
|
|
17
|
+
from kpress.publish import BuildOptions, build_site
|
|
18
|
+
from kpress.publish.build import STANDALONE_ASSET_PREFIX, emit_standalone_assets
|
|
19
|
+
from kpress.publish.capability import CAPABILITY_NAMES, platform_info, probe_all
|
|
20
|
+
from kpress.publish.config import load_config
|
|
21
|
+
from kpress.publish.frontmatter import read_document_source
|
|
22
|
+
from kpress.publish.optimize import optimize_text
|
|
23
|
+
from kpress.workflow import convert_document, export_document, format_document, paste_document
|
|
24
|
+
from kpress.workflow.workspace import WORK_ROOT_MARKER, WorkflowResult, prepare_work_root
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _print_result(result: WorkflowResult) -> int:
|
|
28
|
+
print(json.dumps(result.as_dict(), indent=2, sort_keys=True))
|
|
29
|
+
return 0 if not result.diagnostics else 2
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _print_missing_extra(command: str, exc: KPressMissingOptionalDependencyError) -> int:
|
|
33
|
+
print(
|
|
34
|
+
json.dumps(
|
|
35
|
+
{
|
|
36
|
+
"command": command,
|
|
37
|
+
"diagnostics": [str(exc)],
|
|
38
|
+
"error": type(exc).__name__,
|
|
39
|
+
},
|
|
40
|
+
indent=2,
|
|
41
|
+
sort_keys=True,
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
return 2
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _cmd_init(args: argparse.Namespace) -> int:
|
|
48
|
+
root = prepare_work_root(args.work_root)
|
|
49
|
+
config = Path(args.config)
|
|
50
|
+
if not config.exists():
|
|
51
|
+
write_text_atomic(
|
|
52
|
+
config,
|
|
53
|
+
"sources:\n - path: .\n\npublish:\n output_dir: public\n",
|
|
54
|
+
)
|
|
55
|
+
return _print_result(WorkflowResult(command="init", work_root=root, outputs=[config]))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _cmd_convert(args: argparse.Namespace) -> int:
|
|
59
|
+
return _print_result(convert_document(args.input, output=args.output, work_root=args.work_root))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _cmd_format(args: argparse.Namespace) -> int:
|
|
63
|
+
return _print_result(
|
|
64
|
+
format_document(
|
|
65
|
+
args.input,
|
|
66
|
+
output_dir=args.output_dir,
|
|
67
|
+
work_root=args.work_root,
|
|
68
|
+
asset_mode=args.asset_mode,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _cmd_render(args: argparse.Namespace) -> int:
|
|
74
|
+
"""Render one Markdown input to a single KPress HTML file at ``--output``."""
|
|
75
|
+
|
|
76
|
+
root = prepare_work_root(args.work_root)
|
|
77
|
+
source = Path(args.input)
|
|
78
|
+
doc = read_document_source(source)
|
|
79
|
+
title = doc.metadata.get("title") or source.stem.replace("-", " ").title()
|
|
80
|
+
page = render_page(
|
|
81
|
+
DocumentInput(
|
|
82
|
+
title=str(title),
|
|
83
|
+
source_text=doc.body,
|
|
84
|
+
body_markdown=doc.body,
|
|
85
|
+
source_path=str(source),
|
|
86
|
+
frontmatter=doc.metadata,
|
|
87
|
+
sidematter=doc.sidematter,
|
|
88
|
+
),
|
|
89
|
+
RenderOptions(asset_mode=args.asset_mode, asset_url_prefix=STANDALONE_ASSET_PREFIX),
|
|
90
|
+
)
|
|
91
|
+
output = Path(args.output) if args.output else root / "exports" / f"{source.stem}.html"
|
|
92
|
+
write_text_atomic(output, page.html)
|
|
93
|
+
asset_files = emit_standalone_assets(
|
|
94
|
+
output, asset_mode=args.asset_mode, page=page, source=source
|
|
95
|
+
)
|
|
96
|
+
return _print_result(
|
|
97
|
+
WorkflowResult(command="render", work_root=root, outputs=[output, *asset_files])
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _cmd_paste(args: argparse.Namespace) -> int:
|
|
102
|
+
return _print_result(paste_document(title=args.title, text=args.text, work_root=args.work_root))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _cmd_files(args: argparse.Namespace) -> int:
|
|
106
|
+
return _print_result(
|
|
107
|
+
WorkflowResult(
|
|
108
|
+
command="files",
|
|
109
|
+
work_root=prepare_work_root(args.work_root),
|
|
110
|
+
outputs=sorted(Path(args.work_root).rglob("*")),
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _cmd_export(args: argparse.Namespace) -> int:
|
|
116
|
+
return _print_result(
|
|
117
|
+
export_document(args.input, html=args.html, pdf=args.pdf, work_root=args.work_root)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _cmd_clean(args: argparse.Namespace) -> int:
|
|
122
|
+
"""Remove the build output directory and the work root.
|
|
123
|
+
|
|
124
|
+
The output directory resolves relative to the config file (exactly like
|
|
125
|
+
``build_site``) and is only removed when it carries the KPress build
|
|
126
|
+
manifest (or is empty). The work root is only removed when it has the
|
|
127
|
+
default ``.kpress`` name or carries the KPress work-root marker. Anything
|
|
128
|
+
else is refused so a misconfigured path can never delete unrelated files.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
removed: list[Path] = []
|
|
132
|
+
diagnostics: list[str] = []
|
|
133
|
+
|
|
134
|
+
config_path = Path(args.config)
|
|
135
|
+
if config_path.exists():
|
|
136
|
+
config = load_config(str(config_path))
|
|
137
|
+
base = (config.config_path.parent if config.config_path else Path.cwd()).resolve()
|
|
138
|
+
output_dir = base / config.publish.output_dir
|
|
139
|
+
if output_dir.is_dir():
|
|
140
|
+
manifest = output_dir / "_kpress" / "kpress-manifest.json"
|
|
141
|
+
if manifest.exists() or not any(output_dir.iterdir()):
|
|
142
|
+
shutil.rmtree(output_dir)
|
|
143
|
+
removed.append(output_dir)
|
|
144
|
+
else:
|
|
145
|
+
diagnostics.append(
|
|
146
|
+
f"Refusing to remove {output_dir}: it has no KPress build manifest "
|
|
147
|
+
"(_kpress/kpress-manifest.json). Remove it manually if intended."
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
work_root = Path(args.work_root)
|
|
151
|
+
if work_root.is_dir():
|
|
152
|
+
if work_root.name == ".kpress" or (work_root / WORK_ROOT_MARKER).exists():
|
|
153
|
+
shutil.rmtree(work_root)
|
|
154
|
+
removed.append(work_root)
|
|
155
|
+
else:
|
|
156
|
+
diagnostics.append(
|
|
157
|
+
f"Refusing to remove work root {work_root}: it is not named .kpress and "
|
|
158
|
+
f"has no {WORK_ROOT_MARKER} marker. Remove it manually if intended."
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
print(
|
|
162
|
+
json.dumps(
|
|
163
|
+
{
|
|
164
|
+
"command": "clean",
|
|
165
|
+
"diagnostics": diagnostics,
|
|
166
|
+
"removed": [str(path) for path in removed],
|
|
167
|
+
},
|
|
168
|
+
indent=2,
|
|
169
|
+
sort_keys=True,
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
return 2 if diagnostics else 0
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
176
|
+
parser = argparse.ArgumentParser(
|
|
177
|
+
prog="kpress",
|
|
178
|
+
description="Render Markdown into polished HTML documents and publish static sites.",
|
|
179
|
+
)
|
|
180
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
181
|
+
parser.add_argument("--work-root", default=".kpress", help="Workspace directory for outputs.")
|
|
182
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
183
|
+
|
|
184
|
+
init = sub.add_parser("init", help="Write a starter kpress.yml config.")
|
|
185
|
+
init.add_argument("--config", default="kpress.yml")
|
|
186
|
+
init.set_defaults(func=_cmd_init)
|
|
187
|
+
|
|
188
|
+
convert = sub.add_parser("convert", help="Copy a Markdown-compatible input into the workspace.")
|
|
189
|
+
convert.add_argument("input")
|
|
190
|
+
convert.add_argument("--output")
|
|
191
|
+
convert.set_defaults(func=_cmd_convert)
|
|
192
|
+
|
|
193
|
+
# `inline` is deliberately not offered: it is not yet self-contained.
|
|
194
|
+
asset_mode_choices = ["hosted", "linked", "hashed"]
|
|
195
|
+
|
|
196
|
+
fmt = sub.add_parser(
|
|
197
|
+
"format", help="Render a Markdown file to paired Markdown and HTML outputs."
|
|
198
|
+
)
|
|
199
|
+
fmt.add_argument("input")
|
|
200
|
+
fmt.add_argument("--output-dir")
|
|
201
|
+
fmt.add_argument("--asset-mode", choices=asset_mode_choices, default="linked")
|
|
202
|
+
fmt.set_defaults(func=_cmd_format)
|
|
203
|
+
|
|
204
|
+
render = sub.add_parser("render", help="Render one Markdown input to a single HTML file.")
|
|
205
|
+
render.add_argument("input")
|
|
206
|
+
render.add_argument("--output")
|
|
207
|
+
render.add_argument("--asset-mode", choices=asset_mode_choices, default="linked")
|
|
208
|
+
render.set_defaults(func=_cmd_render)
|
|
209
|
+
|
|
210
|
+
paste = sub.add_parser("paste", help="Create a document from pasted or piped text.")
|
|
211
|
+
paste.add_argument("--title", default="Pasted Document")
|
|
212
|
+
paste.add_argument("--text")
|
|
213
|
+
paste.set_defaults(func=_cmd_paste)
|
|
214
|
+
|
|
215
|
+
files = sub.add_parser("files", help="List files in the work root.")
|
|
216
|
+
files.set_defaults(func=_cmd_files)
|
|
217
|
+
|
|
218
|
+
export = sub.add_parser("export", help="Export a document to HTML and optional PDF.")
|
|
219
|
+
export.add_argument("input")
|
|
220
|
+
export.add_argument("--html")
|
|
221
|
+
export.add_argument("--pdf")
|
|
222
|
+
export.set_defaults(func=_cmd_export)
|
|
223
|
+
|
|
224
|
+
clean = sub.add_parser("clean", help="Remove the build output directory and work root.")
|
|
225
|
+
clean.add_argument("--config", default="kpress.yml")
|
|
226
|
+
clean.set_defaults(func=_cmd_clean)
|
|
227
|
+
|
|
228
|
+
build = sub.add_parser("build", help="Build the static site from kpress.yml.")
|
|
229
|
+
build.add_argument("--config", default="kpress.yml")
|
|
230
|
+
build.add_argument("--output-dir")
|
|
231
|
+
build.add_argument("--asset-mode", choices=asset_mode_choices)
|
|
232
|
+
build.add_argument("--optimizer", choices=["none", "full"])
|
|
233
|
+
build.add_argument("--precompress", action="append", choices=["gzip", "br"])
|
|
234
|
+
build.set_defaults(func=_cmd_build)
|
|
235
|
+
|
|
236
|
+
optimize = sub.add_parser("optimize", help="Minify an HTML/CSS/JS file.")
|
|
237
|
+
optimize.add_argument("input")
|
|
238
|
+
optimize.add_argument("--output")
|
|
239
|
+
optimize.add_argument("--kind", choices=["html", "css", "js"], default="html")
|
|
240
|
+
optimize.add_argument("--backend", choices=["none", "full"], default="none")
|
|
241
|
+
optimize.set_defaults(func=_cmd_optimize)
|
|
242
|
+
|
|
243
|
+
doctor = sub.add_parser(
|
|
244
|
+
"doctor", help="Probe optional capabilities (render, publish, optimizer, PDF)."
|
|
245
|
+
)
|
|
246
|
+
doctor.add_argument("--profile", choices=["render", "publish", "optimize", "pdf"])
|
|
247
|
+
doctor.add_argument("--config")
|
|
248
|
+
doctor.add_argument("--allow-network", action="store_true")
|
|
249
|
+
doctor.add_argument("--json", action="store_true", dest="as_json")
|
|
250
|
+
doctor.set_defaults(func=_cmd_doctor)
|
|
251
|
+
|
|
252
|
+
return parser
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _cmd_build(args: argparse.Namespace) -> int:
|
|
256
|
+
try:
|
|
257
|
+
report = build_site(
|
|
258
|
+
args.config,
|
|
259
|
+
BuildOptions(
|
|
260
|
+
output_dir=Path(args.output_dir) if args.output_dir else None,
|
|
261
|
+
asset_mode=args.asset_mode,
|
|
262
|
+
optimizer=args.optimizer,
|
|
263
|
+
precompress=args.precompress,
|
|
264
|
+
),
|
|
265
|
+
)
|
|
266
|
+
except KPressMissingOptionalDependencyError as exc:
|
|
267
|
+
return _print_missing_extra("build", exc)
|
|
268
|
+
print(json.dumps(report.as_dict(), indent=2, sort_keys=True))
|
|
269
|
+
return 0
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _cmd_optimize(args: argparse.Namespace) -> int:
|
|
273
|
+
source = Path(args.input)
|
|
274
|
+
output = Path(args.output) if args.output else source
|
|
275
|
+
try:
|
|
276
|
+
optimized = optimize_text(
|
|
277
|
+
source.read_text(encoding="utf-8"),
|
|
278
|
+
kind=args.kind,
|
|
279
|
+
backend=args.backend,
|
|
280
|
+
)
|
|
281
|
+
except KPressMissingOptionalDependencyError as exc:
|
|
282
|
+
return _print_missing_extra("optimize", exc)
|
|
283
|
+
write_text_atomic(output, optimized)
|
|
284
|
+
print(
|
|
285
|
+
json.dumps(
|
|
286
|
+
{
|
|
287
|
+
"backend": args.backend,
|
|
288
|
+
"kind": args.kind,
|
|
289
|
+
"output": str(output),
|
|
290
|
+
},
|
|
291
|
+
indent=2,
|
|
292
|
+
sort_keys=True,
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
return 0
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
_DOCTOR_LABELS = {
|
|
299
|
+
"render": "Core render",
|
|
300
|
+
"publish": "Static publish",
|
|
301
|
+
"optimizer_full": "Optimizer full",
|
|
302
|
+
"precompress_brotli": "Brotli precompress",
|
|
303
|
+
"pdf_browser": "PDF browser",
|
|
304
|
+
}
|
|
305
|
+
_PROFILE_CAPABILITY = {
|
|
306
|
+
"render": "render",
|
|
307
|
+
"publish": "publish",
|
|
308
|
+
"optimize": "optimizer_full",
|
|
309
|
+
"pdf": "pdf_browser",
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _required_capabilities(config_path: str) -> set[str]:
|
|
314
|
+
"""Capabilities the resolved config actually exercises."""
|
|
315
|
+
|
|
316
|
+
config = load_config(config_path)
|
|
317
|
+
required = {"render", "publish"}
|
|
318
|
+
if config.optimizer.mode == "full":
|
|
319
|
+
required.add("optimizer_full")
|
|
320
|
+
if "br" in config.optimizer.precompress:
|
|
321
|
+
required.add("precompress_brotli")
|
|
322
|
+
if config.pdf.enabled:
|
|
323
|
+
required.add("pdf_browser")
|
|
324
|
+
return required
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _cmd_doctor(args: argparse.Namespace) -> int:
|
|
328
|
+
results = probe_all(allow_network=args.allow_network)
|
|
329
|
+
|
|
330
|
+
required: set[str]
|
|
331
|
+
if args.profile:
|
|
332
|
+
required = {_PROFILE_CAPABILITY[args.profile]}
|
|
333
|
+
elif args.config:
|
|
334
|
+
required = _required_capabilities(args.config)
|
|
335
|
+
else:
|
|
336
|
+
required = set() # bare discovery never fails
|
|
337
|
+
|
|
338
|
+
if args.as_json:
|
|
339
|
+
payload = {
|
|
340
|
+
**platform_info(),
|
|
341
|
+
"capabilities": {
|
|
342
|
+
name: (
|
|
343
|
+
{"status": r.status, "reason": r.reason} if r.reason else {"status": r.status}
|
|
344
|
+
)
|
|
345
|
+
for name, r in results.items()
|
|
346
|
+
},
|
|
347
|
+
}
|
|
348
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
349
|
+
else:
|
|
350
|
+
for name in CAPABILITY_NAMES:
|
|
351
|
+
r = results[name]
|
|
352
|
+
detail = f" - {r.reason}" if r.reason else ""
|
|
353
|
+
print(f"{_DOCTOR_LABELS[name] + ':':<20}{r.status}{detail}")
|
|
354
|
+
|
|
355
|
+
failed = [n for n in required if results[n].status != "ok"]
|
|
356
|
+
return 2 if failed else 0
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
360
|
+
parser = build_parser()
|
|
361
|
+
args = parser.parse_args(argv)
|
|
362
|
+
handler = cast(Callable[[argparse.Namespace], int], args.func)
|
|
363
|
+
try:
|
|
364
|
+
return handler(args)
|
|
365
|
+
except KeyboardInterrupt:
|
|
366
|
+
print("Interrupted.", file=sys.stderr)
|
|
367
|
+
return 130
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
if __name__ == "__main__":
|
|
371
|
+
raise SystemExit(main())
|