django-app-help 0.1.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 (34) hide show
  1. django_app_help-0.1.0/MANIFEST.in +4 -0
  2. django_app_help-0.1.0/PKG-INFO +203 -0
  3. django_app_help-0.1.0/README.md +191 -0
  4. django_app_help-0.1.0/app_help/__init__.py +7 -0
  5. django_app_help-0.1.0/app_help/apps.py +5 -0
  6. django_app_help-0.1.0/app_help/conf.py +31 -0
  7. django_app_help-0.1.0/app_help/engine.py +180 -0
  8. django_app_help-0.1.0/app_help/exceptions.py +50 -0
  9. django_app_help-0.1.0/app_help/migrations/__init__.py +1 -0
  10. django_app_help-0.1.0/app_help/models.py +59 -0
  11. django_app_help-0.1.0/app_help/parsing.py +192 -0
  12. django_app_help-0.1.0/app_help/views.py +131 -0
  13. django_app_help-0.1.0/demo/demo/__init__.py +0 -0
  14. django_app_help-0.1.0/demo/demo/asgi.py +16 -0
  15. django_app_help-0.1.0/demo/demo/core/__init__.py +0 -0
  16. django_app_help-0.1.0/demo/demo/core/admin.py +3 -0
  17. django_app_help-0.1.0/demo/demo/core/apps.py +8 -0
  18. django_app_help-0.1.0/demo/demo/core/migrations/__init__.py +0 -0
  19. django_app_help-0.1.0/demo/demo/core/models.py +3 -0
  20. django_app_help-0.1.0/demo/demo/core/tests.py +42 -0
  21. django_app_help-0.1.0/demo/demo/core/views.py +188 -0
  22. django_app_help-0.1.0/demo/demo/settings.py +132 -0
  23. django_app_help-0.1.0/demo/demo/urls.py +56 -0
  24. django_app_help-0.1.0/demo/demo/wsgi.py +16 -0
  25. django_app_help-0.1.0/demo/manage.py +22 -0
  26. django_app_help-0.1.0/django_app_help.egg-info/PKG-INFO +203 -0
  27. django_app_help-0.1.0/django_app_help.egg-info/SOURCES.txt +32 -0
  28. django_app_help-0.1.0/django_app_help.egg-info/dependency_links.txt +1 -0
  29. django_app_help-0.1.0/django_app_help.egg-info/requires.txt +5 -0
  30. django_app_help-0.1.0/django_app_help.egg-info/top_level.txt +5 -0
  31. django_app_help-0.1.0/pyproject.toml +53 -0
  32. django_app_help-0.1.0/setup.cfg +4 -0
  33. django_app_help-0.1.0/tests/test_engine.py +99 -0
  34. django_app_help-0.1.0/tests/test_views.py +165 -0
@@ -0,0 +1,4 @@
1
+ include LICENSE
2
+ include README.md
3
+ recursive-include app_help/static *
4
+ recursive-include app_help/templates *
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-app-help
3
+ Version: 0.1.0
4
+ Author-email: Caltech IMSS ADS <imss-ads-staff@caltech.edu>
5
+ Requires-Python: >=3.14
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: Django>=5.2
8
+ Requires-Dist: PyYAML>=6.0
9
+ Requires-Dist: django-markdownify>=0.9.7
10
+ Requires-Dist: django-wildewidgets>=1.7.2
11
+ Requires-Dist: pytest>=9.1.1
12
+
13
+ # Django App Help
14
+
15
+ Filesystem-backed Markdown help for Django applications. Help content lives in your Git repository, deploys read-only with the app, and renders in the browser through [django-wildewidgets](https://github.com/caltechads/django-wildewidgets).
16
+
17
+ Use it for in-app guidance, workflow documentation, field explanations, and operational notes — not as a full CMS.
18
+
19
+ ## Features
20
+
21
+ - Canonical Markdown pages under `help/pages/`
22
+ - Audience-specific **books** defined in YAML under `help/books/`
23
+ - Reusable snippets via `::include` directives
24
+ - Cross-page links with `help:` URLs and asset references with `asset:` URLs
25
+ - `HelpEngine` for loading, rendering, and validating content
26
+ - `HelpOffcanvasMixin` to attach a help panel to Wildewidgets views
27
+
28
+ ## Installation
29
+
30
+ Install the package and its runtime dependencies:
31
+
32
+ ```bash
33
+ pip install django-app-help
34
+ ```
35
+
36
+ Or:
37
+
38
+ ```bash
39
+ uv add django-app-help
40
+ ```
41
+
42
+ Or, from a checkout of this repository:
43
+
44
+ ```bash
45
+ uv sync
46
+ ```
47
+
48
+ Dependencies include Django, PyYAML, django-markdownify, and django-wildewidgets.
49
+
50
+ ## Help content layout
51
+
52
+ Point Django at a help root directory with this structure:
53
+
54
+ ```text
55
+ help/
56
+ ├── books/
57
+ │ ├── user.yaml
58
+ │ └── admin.yaml
59
+ ├── pages/
60
+ │ ├── getting-started/
61
+ │ │ └── welcome.md
62
+ │ └── topics/
63
+ │ └── pages.md
64
+ ├── snippets/
65
+ │ └── support-contact.md
66
+ └── assets/
67
+ └── images/
68
+ ```
69
+
70
+ - **Page IDs** are paths under `pages/` without `.md` (for example `getting-started/welcome`).
71
+ - **Books** list page IDs in sections for different audiences. The same page can appear in multiple books.
72
+ - **Snippets** are included in pages with a whole-line directive:
73
+
74
+ ```markdown
75
+ ::include snippets/support-contact.md
76
+ ```
77
+
78
+ - **Links** reference other pages and assets:
79
+
80
+ ```markdown
81
+ [Navigation](help:getting-started/navigation)
82
+ ![Diagram](asset:images/help-diagram.png)
83
+ ```
84
+
85
+ Pages may include YAML front matter (`title`, `summary`, `audience`, and so on). The engine strips front matter before rendering.
86
+
87
+ ## Django setup
88
+
89
+ ### Settings
90
+
91
+ Add the app (optional — there are no database models, but this keeps the integration explicit):
92
+
93
+ ```python
94
+ INSTALLED_APPS = [
95
+ # ...
96
+ "markdownify",
97
+ "wildewidgets",
98
+ "app_help",
99
+ ]
100
+ ```
101
+
102
+ Set the filesystem help root and import the recommended Markdownify settings:
103
+
104
+ ```python
105
+ from pathlib import Path
106
+
107
+ from app_help.conf import MARKDOWNIFY # noqa: F401
108
+
109
+ APP_HELP_ROOT = Path(__file__).resolve().parent / "myapp" / "help"
110
+ ```
111
+
112
+ `app_help.conf.MARKDOWNIFY` whitelists the HTML tags help pages need when rendered through django-markdownify.
113
+
114
+ ### Wildewidgets views
115
+
116
+ Use `HelpOffcanvasMixin` on a Wildewidgets view. List it **before** `StandardWidgetMixin` so cooperative `get_context_data()` can build page content first, then wrap it with the help offcanvas:
117
+
118
+ ```python
119
+ from app_help.views import HelpOffcanvasMixin
120
+ from wildewidgets import MenuMixin, StandardWidgetMixin
121
+ from django.views.generic import TemplateView
122
+
123
+
124
+ class MyPageView(HelpOffcanvasMixin, MenuMixin, StandardWidgetMixin, TemplateView):
125
+ help_page_id = "getting-started/welcome"
126
+ help_book_slug = "user" # optional; omit to skip book membership checks
127
+ help_offcanvas_title = "Help"
128
+
129
+ def get_content(self):
130
+ # return your Wildewidgets layout
131
+ ...
132
+ ```
133
+
134
+ Configure help per view with class attributes:
135
+
136
+ | Attribute | Purpose |
137
+ |-----------|---------|
138
+ | `help_root` | Override `settings.APP_HELP_ROOT` |
139
+ | `help_page_id` | Page to render (required) |
140
+ | `help_book_slug` | Require the page to be listed in this book |
141
+ | `help_offcanvas_id` | DOM id for the offcanvas (default `help-offcanvas`) |
142
+ | `help_offcanvas_title` | Panel title (default `Help`) |
143
+
144
+ Override `get_help_root()`, `get_help_page_id()`, or `get_help_book_slug()` when the active page depends on the request.
145
+
146
+ ### Programmatic use
147
+
148
+ Render or validate content without a view:
149
+
150
+ ```python
151
+ from app_help import HelpEngine
152
+
153
+ engine = HelpEngine("/path/to/help")
154
+
155
+ markdown = engine.render_page("getting-started/welcome", book_slug="user")
156
+ page = engine.load_page("getting-started/welcome")
157
+ book = engine.load_book("user")
158
+
159
+ engine.validate_page("getting-started/welcome")
160
+ engine.validate_book("user")
161
+ ```
162
+
163
+ `render_page()` expands snippet includes, strips front matter, and optionally verifies that the page belongs to the requested book.
164
+
165
+ ## Demo
166
+
167
+ The `demo/` directory is a small Django project that shows help wired into Academy-themed Wildewidgets pages. It installs this package in editable mode and serves sample content from `demo/demo/core/help/`.
168
+
169
+ ### Run the demo
170
+
171
+ From the repository root:
172
+
173
+ ```bash
174
+ uv sync
175
+ cd demo
176
+ uv sync
177
+ uv run python manage.py runserver
178
+ ```
179
+
180
+ Open http://127.0.0.1:8000/. Each sidebar route renders a static demo page with a help offcanvas:
181
+
182
+ | Route | Help book | Help page |
183
+ |-------|-----------|-----------|
184
+ | `/` | `user` | `getting-started/welcome` |
185
+ | `/components/` | `user` | `topics/pages` |
186
+ | `/workflow/` | `developer` | `authoring/pages` |
187
+ | `/status/` | `admin` | `admin/validation` |
188
+
189
+ Browse the Markdown under `demo/demo/core/help/` to see books, pages, snippets, includes, and link patterns in context. `demo/demo/core/views.py` shows how `HelpOffcanvasMixin` composes with `StandardWidgetMixin`.
190
+
191
+ ## Development
192
+
193
+ Run the library test suite from the repository root:
194
+
195
+ ```bash
196
+ make pytest
197
+ ```
198
+
199
+ Pass extra pytest arguments after the target:
200
+
201
+ ```bash
202
+ make pytest ARGS="tests/test_engine.py -v"
203
+ ```
@@ -0,0 +1,191 @@
1
+ # Django App Help
2
+
3
+ Filesystem-backed Markdown help for Django applications. Help content lives in your Git repository, deploys read-only with the app, and renders in the browser through [django-wildewidgets](https://github.com/caltechads/django-wildewidgets).
4
+
5
+ Use it for in-app guidance, workflow documentation, field explanations, and operational notes — not as a full CMS.
6
+
7
+ ## Features
8
+
9
+ - Canonical Markdown pages under `help/pages/`
10
+ - Audience-specific **books** defined in YAML under `help/books/`
11
+ - Reusable snippets via `::include` directives
12
+ - Cross-page links with `help:` URLs and asset references with `asset:` URLs
13
+ - `HelpEngine` for loading, rendering, and validating content
14
+ - `HelpOffcanvasMixin` to attach a help panel to Wildewidgets views
15
+
16
+ ## Installation
17
+
18
+ Install the package and its runtime dependencies:
19
+
20
+ ```bash
21
+ pip install django-app-help
22
+ ```
23
+
24
+ Or:
25
+
26
+ ```bash
27
+ uv add django-app-help
28
+ ```
29
+
30
+ Or, from a checkout of this repository:
31
+
32
+ ```bash
33
+ uv sync
34
+ ```
35
+
36
+ Dependencies include Django, PyYAML, django-markdownify, and django-wildewidgets.
37
+
38
+ ## Help content layout
39
+
40
+ Point Django at a help root directory with this structure:
41
+
42
+ ```text
43
+ help/
44
+ ├── books/
45
+ │ ├── user.yaml
46
+ │ └── admin.yaml
47
+ ├── pages/
48
+ │ ├── getting-started/
49
+ │ │ └── welcome.md
50
+ │ └── topics/
51
+ │ └── pages.md
52
+ ├── snippets/
53
+ │ └── support-contact.md
54
+ └── assets/
55
+ └── images/
56
+ ```
57
+
58
+ - **Page IDs** are paths under `pages/` without `.md` (for example `getting-started/welcome`).
59
+ - **Books** list page IDs in sections for different audiences. The same page can appear in multiple books.
60
+ - **Snippets** are included in pages with a whole-line directive:
61
+
62
+ ```markdown
63
+ ::include snippets/support-contact.md
64
+ ```
65
+
66
+ - **Links** reference other pages and assets:
67
+
68
+ ```markdown
69
+ [Navigation](help:getting-started/navigation)
70
+ ![Diagram](asset:images/help-diagram.png)
71
+ ```
72
+
73
+ Pages may include YAML front matter (`title`, `summary`, `audience`, and so on). The engine strips front matter before rendering.
74
+
75
+ ## Django setup
76
+
77
+ ### Settings
78
+
79
+ Add the app (optional — there are no database models, but this keeps the integration explicit):
80
+
81
+ ```python
82
+ INSTALLED_APPS = [
83
+ # ...
84
+ "markdownify",
85
+ "wildewidgets",
86
+ "app_help",
87
+ ]
88
+ ```
89
+
90
+ Set the filesystem help root and import the recommended Markdownify settings:
91
+
92
+ ```python
93
+ from pathlib import Path
94
+
95
+ from app_help.conf import MARKDOWNIFY # noqa: F401
96
+
97
+ APP_HELP_ROOT = Path(__file__).resolve().parent / "myapp" / "help"
98
+ ```
99
+
100
+ `app_help.conf.MARKDOWNIFY` whitelists the HTML tags help pages need when rendered through django-markdownify.
101
+
102
+ ### Wildewidgets views
103
+
104
+ Use `HelpOffcanvasMixin` on a Wildewidgets view. List it **before** `StandardWidgetMixin` so cooperative `get_context_data()` can build page content first, then wrap it with the help offcanvas:
105
+
106
+ ```python
107
+ from app_help.views import HelpOffcanvasMixin
108
+ from wildewidgets import MenuMixin, StandardWidgetMixin
109
+ from django.views.generic import TemplateView
110
+
111
+
112
+ class MyPageView(HelpOffcanvasMixin, MenuMixin, StandardWidgetMixin, TemplateView):
113
+ help_page_id = "getting-started/welcome"
114
+ help_book_slug = "user" # optional; omit to skip book membership checks
115
+ help_offcanvas_title = "Help"
116
+
117
+ def get_content(self):
118
+ # return your Wildewidgets layout
119
+ ...
120
+ ```
121
+
122
+ Configure help per view with class attributes:
123
+
124
+ | Attribute | Purpose |
125
+ |-----------|---------|
126
+ | `help_root` | Override `settings.APP_HELP_ROOT` |
127
+ | `help_page_id` | Page to render (required) |
128
+ | `help_book_slug` | Require the page to be listed in this book |
129
+ | `help_offcanvas_id` | DOM id for the offcanvas (default `help-offcanvas`) |
130
+ | `help_offcanvas_title` | Panel title (default `Help`) |
131
+
132
+ Override `get_help_root()`, `get_help_page_id()`, or `get_help_book_slug()` when the active page depends on the request.
133
+
134
+ ### Programmatic use
135
+
136
+ Render or validate content without a view:
137
+
138
+ ```python
139
+ from app_help import HelpEngine
140
+
141
+ engine = HelpEngine("/path/to/help")
142
+
143
+ markdown = engine.render_page("getting-started/welcome", book_slug="user")
144
+ page = engine.load_page("getting-started/welcome")
145
+ book = engine.load_book("user")
146
+
147
+ engine.validate_page("getting-started/welcome")
148
+ engine.validate_book("user")
149
+ ```
150
+
151
+ `render_page()` expands snippet includes, strips front matter, and optionally verifies that the page belongs to the requested book.
152
+
153
+ ## Demo
154
+
155
+ The `demo/` directory is a small Django project that shows help wired into Academy-themed Wildewidgets pages. It installs this package in editable mode and serves sample content from `demo/demo/core/help/`.
156
+
157
+ ### Run the demo
158
+
159
+ From the repository root:
160
+
161
+ ```bash
162
+ uv sync
163
+ cd demo
164
+ uv sync
165
+ uv run python manage.py runserver
166
+ ```
167
+
168
+ Open http://127.0.0.1:8000/. Each sidebar route renders a static demo page with a help offcanvas:
169
+
170
+ | Route | Help book | Help page |
171
+ |-------|-----------|-----------|
172
+ | `/` | `user` | `getting-started/welcome` |
173
+ | `/components/` | `user` | `topics/pages` |
174
+ | `/workflow/` | `developer` | `authoring/pages` |
175
+ | `/status/` | `admin` | `admin/validation` |
176
+
177
+ Browse the Markdown under `demo/demo/core/help/` to see books, pages, snippets, includes, and link patterns in context. `demo/demo/core/views.py` shows how `HelpOffcanvasMixin` composes with `StandardWidgetMixin`.
178
+
179
+ ## Development
180
+
181
+ Run the library test suite from the repository root:
182
+
183
+ ```bash
184
+ make pytest
185
+ ```
186
+
187
+ Pass extra pytest arguments after the target:
188
+
189
+ ```bash
190
+ make pytest ARGS="tests/test_engine.py -v"
191
+ ```
@@ -0,0 +1,7 @@
1
+ """Filesystem-backed Markdown help engine."""
2
+
3
+ from app_help.engine import HelpEngine
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["HelpEngine"]
@@ -0,0 +1,5 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DjangoAppHelpConfig(AppConfig):
5
+ name = 'app_help'
@@ -0,0 +1,31 @@
1
+ """Recommended Django settings for django-app-help integrations."""
2
+
3
+ import bleach
4
+
5
+ #: Bleach tag whitelist for Markdown help content rendered via django-markdownify.
6
+ _MARKDOWN_HELP_TAGS = {
7
+ *bleach.sanitizer.ALLOWED_TAGS,
8
+ "h1",
9
+ "h2",
10
+ "h3",
11
+ "h4",
12
+ "h5",
13
+ "h6",
14
+ "hr",
15
+ "p",
16
+ "pre",
17
+ }
18
+
19
+ #: Recommended ``MARKDOWNIFY`` settings for help pages rendered with Wildewidgets.
20
+ MARKDOWNIFY = {
21
+ "default": {
22
+ "WHITELIST_TAGS": sorted(_MARKDOWN_HELP_TAGS),
23
+ "MARKDOWN_EXTENSIONS": [
24
+ "markdown.extensions.fenced_code",
25
+ ],
26
+ "LINKIFY_TEXT": {
27
+ "PARSE_URLS": True,
28
+ "SKIP_TAGS": ["pre", "code"],
29
+ },
30
+ },
31
+ }
@@ -0,0 +1,180 @@
1
+ """Public Markdown rendering engine for filesystem-backed help content."""
2
+
3
+ from pathlib import Path, PurePosixPath
4
+
5
+ from app_help.exceptions import HelpBookNotFoundError, HelpPageNotFoundError, PageNotInBookError
6
+ from app_help.models import Book, Page
7
+ from app_help.parsing import expand_includes, parse_book, parse_front_matter, validate_asset_links, validate_help_links
8
+
9
+
10
+ class HelpEngine:
11
+ """Load help books and render canonical Markdown pages from disk.
12
+
13
+ Args:
14
+ root_path: Help root containing ``books``, ``pages``, ``snippets``, and ``assets`` directories.
15
+ max_include_depth: Maximum nested snippet include depth.
16
+ """
17
+
18
+ def __init__(self, root_path: str | Path, max_include_depth: int = 5) -> None:
19
+ #: Help root containing content directories.
20
+ self.root_path = Path(root_path)
21
+ #: Maximum nested snippet include depth.
22
+ self.max_include_depth = max_include_depth
23
+ #: Directory containing book YAML files.
24
+ self.books_path = self.root_path / "books"
25
+ #: Directory containing canonical Markdown pages.
26
+ self.pages_path = self.root_path / "pages"
27
+ #: Directory containing help assets.
28
+ self.assets_path = self.root_path / "assets"
29
+
30
+ def render_page(self, page_id: str, book_slug: str | None = None) -> str:
31
+ """Return expanded Markdown for a page.
32
+
33
+ Args:
34
+ page_id: Logical page ID, such as ``billing/overview``.
35
+ book_slug: Optional book slug requiring the page to be listed in that book.
36
+
37
+ Raises:
38
+ HelpBookNotFoundError: If ``book_slug`` is provided and missing.
39
+ HelpPageNotFoundError: If the page file is missing.
40
+ PageNotInBookError: If the book does not reference the page.
41
+
42
+ Returns:
43
+ Markdown with front matter removed and snippets expanded.
44
+ """
45
+ if book_slug is not None:
46
+ book = self.load_book(book_slug)
47
+ if not book.contains_page(page_id):
48
+ raise PageNotInBookError(f"{page_id} is not listed in book {book_slug}")
49
+
50
+ page = self.load_page(page_id)
51
+ markdown = expand_includes(page.markdown, self.root_path, self.max_include_depth)
52
+ return markdown.rstrip() + "\n"
53
+
54
+ def load_page(self, page_id: str) -> Page:
55
+ """Load a canonical page from disk.
56
+
57
+ Args:
58
+ page_id: Logical page ID.
59
+
60
+ Raises:
61
+ HelpPageNotFoundError: If the page file is missing.
62
+
63
+ Returns:
64
+ Parsed page.
65
+ """
66
+ path = self._page_path(page_id)
67
+ if not path.is_file():
68
+ raise HelpPageNotFoundError(f"page not found: {page_id}")
69
+
70
+ metadata, markdown = parse_front_matter(path.read_text(encoding="utf-8"), path)
71
+ return Page(page_id=page_id, path=path, metadata=metadata, markdown=markdown)
72
+
73
+ def load_book(self, slug: str) -> Book:
74
+ """Load a book by slug.
75
+
76
+ Args:
77
+ slug: Book slug.
78
+
79
+ Raises:
80
+ HelpBookNotFoundError: If the book file is missing.
81
+
82
+ Returns:
83
+ Parsed book.
84
+ """
85
+ path = self._book_path(slug)
86
+ if not path.is_file():
87
+ raise HelpBookNotFoundError(f"book not found: {slug}")
88
+ return parse_book(path)
89
+
90
+ def validate_page(self, page_id: str) -> None:
91
+ """Validate includes, help links, and asset links for one page.
92
+
93
+ Args:
94
+ page_id: Logical page ID.
95
+
96
+ Raises:
97
+ HelpAssetNotFoundError: If an asset reference is missing.
98
+ HelpLinkNotFoundError: If a help link points to a missing page.
99
+ HelpPageNotFoundError: If the page file is missing.
100
+ """
101
+ markdown = self.render_page(page_id)
102
+ validate_help_links(markdown, self._page_exists)
103
+ validate_asset_links(markdown, self.assets_path)
104
+
105
+ def validate_book(self, slug: str) -> None:
106
+ """Validate that all pages referenced by a book exist and pass page validation.
107
+
108
+ Args:
109
+ slug: Book slug.
110
+
111
+ Raises:
112
+ HelpBookNotFoundError: If the book file is missing.
113
+ HelpPageNotFoundError: If a referenced page is missing.
114
+ """
115
+ book = self.load_book(slug)
116
+ for page_id in book.pages:
117
+ self.validate_page(page_id)
118
+
119
+ def _page_path(self, page_id: str) -> Path:
120
+ """Return the filesystem path for a safe page ID.
121
+
122
+ Args:
123
+ page_id: Logical page ID.
124
+
125
+ Raises:
126
+ HelpPageNotFoundError: If the page ID is unsafe.
127
+
128
+ Returns:
129
+ Markdown file path.
130
+ """
131
+ rel = self._safe_relative(page_id, HelpPageNotFoundError)
132
+ return self.pages_path / rel.with_suffix(".md")
133
+
134
+ def _book_path(self, slug: str) -> Path:
135
+ """Return the filesystem path for a safe book slug.
136
+
137
+ Args:
138
+ slug: Book slug.
139
+
140
+ Raises:
141
+ HelpBookNotFoundError: If the slug is unsafe.
142
+
143
+ Returns:
144
+ YAML file path.
145
+ """
146
+ rel = self._safe_relative(slug, HelpBookNotFoundError)
147
+ return self.books_path / rel.with_suffix(".yaml")
148
+
149
+ def _page_exists(self, page_id: str) -> bool:
150
+ """Return whether a page ID resolves to an existing file.
151
+
152
+ Args:
153
+ page_id: Logical page ID.
154
+
155
+ Returns:
156
+ True when the page exists.
157
+ """
158
+ try:
159
+ return self._page_path(page_id).is_file()
160
+ except HelpPageNotFoundError:
161
+ return False
162
+
163
+ @staticmethod
164
+ def _safe_relative(value: str, error_type: type[Exception]) -> Path:
165
+ """Convert a logical ID into a safe relative filesystem path.
166
+
167
+ Args:
168
+ value: Logical ID or slug.
169
+ error_type: Exception class to raise for unsafe values.
170
+
171
+ Raises:
172
+ Exception: The supplied ``error_type`` when ``value`` is unsafe.
173
+
174
+ Returns:
175
+ Relative path.
176
+ """
177
+ rel = PurePosixPath(value)
178
+ if rel.is_absolute() or ".." in rel.parts or not rel.parts or any(part == "" for part in rel.parts):
179
+ raise error_type(f"unsafe help path: {value}")
180
+ return Path(*rel.parts)
@@ -0,0 +1,50 @@
1
+ """Exceptions raised by the file-based help engine."""
2
+
3
+
4
+ class HelpError(Exception):
5
+ """Base class for help engine errors."""
6
+
7
+
8
+ class HelpPageNotFoundError(HelpError):
9
+ """Raised when a requested help page does not exist."""
10
+
11
+
12
+ class HelpBookNotFoundError(HelpError):
13
+ """Raised when a requested help book does not exist."""
14
+
15
+
16
+ class PageNotInBookError(HelpError):
17
+ """Raised when a book-scoped page request is not listed in the book."""
18
+
19
+
20
+ class InvalidIncludeError(HelpError):
21
+ """Raised when an include directive points outside allowed snippets."""
22
+
23
+
24
+ class IncludeNotFoundError(HelpError):
25
+ """Raised when an include directive points to a missing snippet."""
26
+
27
+
28
+ class CircularIncludeError(HelpError):
29
+ """Raised when snippet includes form a cycle."""
30
+
31
+
32
+ class IncludeDepthError(HelpError):
33
+ """Raised when snippet includes exceed the configured depth."""
34
+
35
+
36
+ class InvalidBookError(HelpError):
37
+ """Raised when a book YAML file is missing required structure."""
38
+
39
+
40
+ class InvalidFrontMatterError(HelpError):
41
+ """Raised when page front matter is not valid YAML metadata."""
42
+
43
+
44
+ class HelpLinkNotFoundError(HelpError):
45
+ """Raised when a rendered page links to a missing help page."""
46
+
47
+
48
+ class HelpAssetNotFoundError(HelpError):
49
+ """Raised when a rendered page references a missing asset."""
50
+
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"