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.
Files changed (28) hide show
  1. podpack_pages-0.2.0/PKG-INFO +69 -0
  2. podpack_pages-0.2.0/README.md +57 -0
  3. podpack_pages-0.2.0/pyproject.toml +37 -0
  4. podpack_pages-0.2.0/src/podpack_pages/__init__.py +16 -0
  5. podpack_pages-0.2.0/src/podpack_pages/content.py +94 -0
  6. podpack_pages-0.2.0/src/podpack_pages/data/html-pages/bsars.html +602 -0
  7. podpack_pages-0.2.0/src/podpack_pages/data/html-pages/bsars1.html +602 -0
  8. podpack_pages-0.2.0/src/podpack_pages/data/html-pages/hastings.html +10 -0
  9. podpack_pages-0.2.0/src/podpack_pages/data/html-pages/parts_demo.html +3 -0
  10. podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/0334111f_e7a455387c495208f5585fce128193599115ba5819e7d648ce2c74f4ba6493fd +0 -0
  11. podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/3420495d_49_Cover_4-1_0-1387x1800.gif +0 -0
  12. podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/39dcba39_f8f8916f17049537000388c18b1ba7d12137364600b07acb052717bbdecfca41 +0 -0
  13. podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/3ca8a5dc_8713311b-1bb9-e372-f477-d79b22a6f716.jpg +0 -0
  14. podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/525ba31b_e7a455387c495208f5585fce128193599115ba5819e7d648ce2c74f4ba6493fd +0 -0
  15. podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/62f94489_cnf-footer-logo.svg +38 -0
  16. podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/85c18542_f8f8916f17049537000388c18b1ba7d12137364600b07acb052717bbdecfca41 +0 -0
  17. podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/a2f6c650_49_Cover_4-1_0-1200x1557.gif +0 -0
  18. podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/b5db9d7f_cnf-header-logo-magazine.svg +27 -0
  19. podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/d381de8d_8713311b-1bb9-e372-f477-d79b22a6f716-629x800.jpg +0 -0
  20. podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/images/dac80d80_sticky-logo-red.svg +12 -0
  21. podpack_pages-0.2.0/src/podpack_pages/data/html-pages/writing/personal-essay-visual-guide.html +194 -0
  22. podpack_pages-0.2.0/src/podpack_pages/data/md-pages/2023_AGM_Minutes.md +64 -0
  23. podpack_pages-0.2.0/src/podpack_pages/data/md-pages/20250321-CarelessPeople.md +19 -0
  24. podpack_pages-0.2.0/src/podpack_pages/data/md-pages/how-site-built.md +11 -0
  25. podpack_pages-0.2.0/src/podpack_pages/data/md-pages/test.md +31 -0
  26. podpack_pages-0.2.0/src/podpack_pages/templates/pages/html.html +4 -0
  27. podpack_pages-0.2.0/src/podpack_pages/templates/pages/markdown.html +16 -0
  28. 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)