podpack-pages 0.2.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.
- podpack_pages-0.2.0/PKG-INFO +69 -0
- podpack_pages-0.2.0/README.md +57 -0
- podpack_pages-0.2.0/pyproject.toml +37 -0
- podpack_pages-0.2.0/src/podpack_pages/__init__.py +16 -0
- podpack_pages-0.2.0/src/podpack_pages/content.py +94 -0
- podpack_pages-0.2.0/src/podpack_pages/data/html-pages/bsars.html +602 -0
- podpack_pages-0.2.0/src/podpack_pages/data/html-pages/bsars1.html +602 -0
- podpack_pages-0.2.0/src/podpack_pages/data/html-pages/hastings.html +10 -0
- podpack_pages-0.2.0/src/podpack_pages/data/html-pages/parts_demo.html +3 -0
- podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/0334111f_e7a455387c495208f5585fce128193599115ba5819e7d648ce2c74f4ba6493fd +0 -0
- podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/3420495d_49_Cover_4-1_0-1387x1800.gif +0 -0
- podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/39dcba39_f8f8916f17049537000388c18b1ba7d12137364600b07acb052717bbdecfca41 +0 -0
- podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/3ca8a5dc_8713311b-1bb9-e372-f477-d79b22a6f716.jpg +0 -0
- podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/525ba31b_e7a455387c495208f5585fce128193599115ba5819e7d648ce2c74f4ba6493fd +0 -0
- podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/62f94489_cnf-footer-logo.svg +38 -0
- podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/85c18542_f8f8916f17049537000388c18b1ba7d12137364600b07acb052717bbdecfca41 +0 -0
- podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/a2f6c650_49_Cover_4-1_0-1200x1557.gif +0 -0
- podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/b5db9d7f_cnf-header-logo-magazine.svg +27 -0
- podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/d381de8d_8713311b-1bb9-e372-f477-d79b22a6f716-629x800.jpg +0 -0
- podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/dac80d80_sticky-logo-red.svg +12 -0
- podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/personal-essay-visual-guide.html +194 -0
- podpack_pages-0.2.0/src/podpack_pages/data/md-pages/2023_AGM_Minutes.md +64 -0
- podpack_pages-0.2.0/src/podpack_pages/data/md-pages/20250321-CarelessPeople.md +19 -0
- podpack_pages-0.2.0/src/podpack_pages/data/md-pages/how-site-built.md +11 -0
- podpack_pages-0.2.0/src/podpack_pages/data/md-pages/test.md +31 -0
- podpack_pages-0.2.0/src/podpack_pages/templates/pages/html.html +4 -0
- podpack_pages-0.2.0/src/podpack_pages/templates/pages/markdown.html +16 -0
- podpack_pages-0.2.0/src/podpack_pages/views.py +74 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: podpack-pages
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Content pages for podpack sites: one name space, Markdown first, HTML second
|
|
5
|
+
Author: Steve Holden
|
|
6
|
+
Author-email: Steve Holden <steve@holdenweb.com>
|
|
7
|
+
Requires-Dist: flask>=3.0
|
|
8
|
+
Requires-Dist: markdown>=3.5.2
|
|
9
|
+
Requires-Dist: python-markdown-math>=0.8
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# podpack-pages
|
|
14
|
+
|
|
15
|
+
A podpack app serving a site's prose content — Markdown and HTML files — from
|
|
16
|
+
a **single name space**. `/pages/<name>` searches the Markdown tree
|
|
17
|
+
(`md-pages/<name>.md`) first and the HTML tree (`html-pages/<name>.html`)
|
|
18
|
+
second, so a page's address never says which format it is stored in, and a
|
|
19
|
+
Markdown page shadows an HTML one of the same name.
|
|
20
|
+
|
|
21
|
+
Three spellings, per podpack convention: the distribution is `podpack-pages`,
|
|
22
|
+
the import name (for a site's `apps` list) is `podpack_pages`, and the app
|
|
23
|
+
answers to `pages` — its blueprint's name, which keys `[apps.pages]`,
|
|
24
|
+
`[site.mounts]` and its directories on disk.
|
|
25
|
+
|
|
26
|
+
## Content location
|
|
27
|
+
|
|
28
|
+
Content lives in the app's host data directory:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
<data root>/pages/md-pages/ Markdown, searched first
|
|
32
|
+
<data root>/pages/html-pages/ HTML, searched second; assets live beside pages
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The package ships a starter `data/` tree which podpack seeds to the host on
|
|
36
|
+
first install, so editing a page on the host changes the site with no rebuild.
|
|
37
|
+
|
|
38
|
+
## Routes
|
|
39
|
+
|
|
40
|
+
| Route | What it does |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `/pages/<name>` | serve the page, Markdown space first |
|
|
43
|
+
| `/pages/asset/<path>` | serve an asset (image, stylesheet) belonging to a page |
|
|
44
|
+
|
|
45
|
+
Relative `src`/`href` references inside HTML pages are rewritten server-side
|
|
46
|
+
through `url_for("pages.asset", ...)`, so they follow the app wherever the
|
|
47
|
+
site mounts it. A page named `asset/...` would be shadowed by the asset route;
|
|
48
|
+
don't create one.
|
|
49
|
+
|
|
50
|
+
## Markdown rendering
|
|
51
|
+
|
|
52
|
+
Extensions `mdx_math` and `codehilite` are enabled. A leading `# Heading`
|
|
53
|
+
becomes the page's title and is removed from the body; a page without one
|
|
54
|
+
gets `[apps.pages] default_title`, or "Untitled".
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
```toml
|
|
59
|
+
[apps.pages]
|
|
60
|
+
default_title = "Just another note" # title for Markdown pages with no heading
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Both settings are optional; the app ships defaults.
|
|
64
|
+
|
|
65
|
+
## Requirements of the site's chrome
|
|
66
|
+
|
|
67
|
+
The Markdown template fills `{% block scripts %}` with MathJax. A site chrome
|
|
68
|
+
that does not define that block silently drops it — mathematics then renders
|
|
69
|
+
as raw TeX.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# podpack-pages
|
|
2
|
+
|
|
3
|
+
A podpack app serving a site's prose content — Markdown and HTML files — from
|
|
4
|
+
a **single name space**. `/pages/<name>` searches the Markdown tree
|
|
5
|
+
(`md-pages/<name>.md`) first and the HTML tree (`html-pages/<name>.html`)
|
|
6
|
+
second, so a page's address never says which format it is stored in, and a
|
|
7
|
+
Markdown page shadows an HTML one of the same name.
|
|
8
|
+
|
|
9
|
+
Three spellings, per podpack convention: the distribution is `podpack-pages`,
|
|
10
|
+
the import name (for a site's `apps` list) is `podpack_pages`, and the app
|
|
11
|
+
answers to `pages` — its blueprint's name, which keys `[apps.pages]`,
|
|
12
|
+
`[site.mounts]` and its directories on disk.
|
|
13
|
+
|
|
14
|
+
## Content location
|
|
15
|
+
|
|
16
|
+
Content lives in the app's host data directory:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
<data root>/pages/md-pages/ Markdown, searched first
|
|
20
|
+
<data root>/pages/html-pages/ HTML, searched second; assets live beside pages
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The package ships a starter `data/` tree which podpack seeds to the host on
|
|
24
|
+
first install, so editing a page on the host changes the site with no rebuild.
|
|
25
|
+
|
|
26
|
+
## Routes
|
|
27
|
+
|
|
28
|
+
| Route | What it does |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `/pages/<name>` | serve the page, Markdown space first |
|
|
31
|
+
| `/pages/asset/<path>` | serve an asset (image, stylesheet) belonging to a page |
|
|
32
|
+
|
|
33
|
+
Relative `src`/`href` references inside HTML pages are rewritten server-side
|
|
34
|
+
through `url_for("pages.asset", ...)`, so they follow the app wherever the
|
|
35
|
+
site mounts it. A page named `asset/...` would be shadowed by the asset route;
|
|
36
|
+
don't create one.
|
|
37
|
+
|
|
38
|
+
## Markdown rendering
|
|
39
|
+
|
|
40
|
+
Extensions `mdx_math` and `codehilite` are enabled. A leading `# Heading`
|
|
41
|
+
becomes the page's title and is removed from the body; a page without one
|
|
42
|
+
gets `[apps.pages] default_title`, or "Untitled".
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
```toml
|
|
47
|
+
[apps.pages]
|
|
48
|
+
default_title = "Just another note" # title for Markdown pages with no heading
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Both settings are optional; the app ships defaults.
|
|
52
|
+
|
|
53
|
+
## Requirements of the site's chrome
|
|
54
|
+
|
|
55
|
+
The Markdown template fills `{% block scripts %}` with MathJax. A site chrome
|
|
56
|
+
that does not define that block silently drops it — mathematics then renders
|
|
57
|
+
as raw TeX.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "podpack-pages"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Content pages for podpack sites: one name space, Markdown first, HTML second"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"flask>=3.0",
|
|
9
|
+
"markdown>=3.5.2",
|
|
10
|
+
"python-markdown-math>=0.8",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[[project.authors]]
|
|
14
|
+
name = "Steve Holden"
|
|
15
|
+
email = "steve@holdenweb.com"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["uv_build>=0.8.4,<0.9.0"]
|
|
19
|
+
build-backend = "uv_build"
|
|
20
|
+
|
|
21
|
+
# podpack is deliberately absent from `dependencies`: a real dependency's
|
|
22
|
+
# [tool.uv.sources] entry travels with the distribution, so a site whose own
|
|
23
|
+
# podpack source differs could never lock. A dependency group is dev-only
|
|
24
|
+
# metadata and constrains nobody. See writing-an-app.md rule 1.
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
|
+
"podpack",
|
|
28
|
+
"pytest>=8.0",
|
|
29
|
+
"mypy>=1.11",
|
|
30
|
+
"types-markdown>=3.10.2.20260712",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[tool.uv.sources]
|
|
34
|
+
podpack = { path = "/Users/sholden/sites/podpack", editable = true }
|
|
35
|
+
|
|
36
|
+
[tool.mypy]
|
|
37
|
+
files = ["src", "tests"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""The pages app: a site's prose content, served from one name space.
|
|
2
|
+
|
|
3
|
+
Content is Markdown or HTML files under the app's host data directory. A
|
|
4
|
+
request names a page, not a format: the Markdown space is searched first and
|
|
5
|
+
the HTML space second, so the two kinds of content share one set of addresses
|
|
6
|
+
and a Markdown page shadows an HTML one of the same name.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from podpack import SiteApp
|
|
10
|
+
|
|
11
|
+
from .views import blueprint
|
|
12
|
+
|
|
13
|
+
site_app = SiteApp(
|
|
14
|
+
blueprint=blueprint,
|
|
15
|
+
url_prefix="/pages",
|
|
16
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Locating content on disk, and preparing HTML pages for serving.
|
|
2
|
+
|
|
3
|
+
The app's data directory holds two trees, `md-pages/` and `html-pages/`,
|
|
4
|
+
seeded from the package on first install. Lookup presents them as a single
|
|
5
|
+
name space: `find_page("x")` tries `md-pages/x.md` before `html-pages/x.html`,
|
|
6
|
+
and assets resolve the same way. Which tree a file sits in is a storage
|
|
7
|
+
detail, not part of any page's address.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import mimetypes
|
|
13
|
+
import re
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
MD_TREE = "md-pages"
|
|
18
|
+
HTML_TREE = "html-pages"
|
|
19
|
+
|
|
20
|
+
_ABSOLUTE_PREFIXES = ("/", "http://", "https://", "data:", "#", "mailto:")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ContentNotFound(LookupError):
|
|
24
|
+
"""The requested content does not exist."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _check_relative(path: str) -> None:
|
|
28
|
+
"""Refuse anything that could step outside the content trees."""
|
|
29
|
+
if not path or path.startswith("/"):
|
|
30
|
+
raise ContentNotFound(path)
|
|
31
|
+
for segment in path.split("/"):
|
|
32
|
+
if segment in ("", "..", "."):
|
|
33
|
+
raise ContentNotFound(path)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def find_page(root: Path, name: str) -> tuple[str, str]:
|
|
37
|
+
"""Resolve `name` in the unified space: Markdown first, then HTML.
|
|
38
|
+
|
|
39
|
+
Returns `("markdown", text)` or `("html", text)`.
|
|
40
|
+
"""
|
|
41
|
+
_check_relative(name)
|
|
42
|
+
candidates = (
|
|
43
|
+
("markdown", root / MD_TREE / f"{name}.md"),
|
|
44
|
+
("html", root / HTML_TREE / f"{name}.html"),
|
|
45
|
+
)
|
|
46
|
+
for kind, path in candidates:
|
|
47
|
+
try:
|
|
48
|
+
return kind, path.read_text()
|
|
49
|
+
except OSError:
|
|
50
|
+
continue
|
|
51
|
+
raise ContentNotFound(name)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def find_asset(root: Path, path: str) -> tuple[bytes, str]:
|
|
55
|
+
"""Resolve an asset the same way pages resolve: Markdown tree first."""
|
|
56
|
+
_check_relative(path)
|
|
57
|
+
for tree in (MD_TREE, HTML_TREE):
|
|
58
|
+
fpath = root / tree / path
|
|
59
|
+
try:
|
|
60
|
+
body = fpath.read_bytes()
|
|
61
|
+
except OSError:
|
|
62
|
+
continue
|
|
63
|
+
ctype, _ = mimetypes.guess_type(str(fpath))
|
|
64
|
+
return body, ctype or "application/octet-stream"
|
|
65
|
+
raise ContentNotFound(path)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# src="…" / href="…" with either quoting style. Non-greedy value match so
|
|
69
|
+
# multiple attributes on the same tag don't get swallowed together.
|
|
70
|
+
_ATTR_RE = re.compile(
|
|
71
|
+
r'(?P<attr>src|href)\s*=\s*(?P<q>["\'])(?P<val>[^"\']*)(?P=q)',
|
|
72
|
+
re.IGNORECASE,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def rewrite_asset_urls(html: str, page_dir: str, asset_url: Callable[[str], str]) -> str:
|
|
77
|
+
"""Rewrite relative `src`/`href` values through `asset_url`.
|
|
78
|
+
|
|
79
|
+
`asset_url` maps a path relative to the content trees to a servable URL --
|
|
80
|
+
the view passes `url_for("pages.asset", ...)` so the result follows the app
|
|
81
|
+
wherever the site mounts it. Absolute URLs, anchors, `mailto:` and `data:`
|
|
82
|
+
URIs pass through unchanged. `page_dir` is the directory the page lives in
|
|
83
|
+
relative to its tree, empty at the root.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def _replace(m: re.Match[str]) -> str:
|
|
87
|
+
val = m.group("val")
|
|
88
|
+
lowered = val.lower()
|
|
89
|
+
if any(lowered.startswith(p) for p in _ABSOLUTE_PREFIXES):
|
|
90
|
+
return m.group(0)
|
|
91
|
+
target = f"{page_dir}/{val}" if page_dir else val
|
|
92
|
+
return f'{m.group("attr")}={m.group("q")}{asset_url(target)}{m.group("q")}'
|
|
93
|
+
|
|
94
|
+
return _ATTR_RE.sub(_replace, html)
|