webifier-extensions 1.0.1__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 (53) hide show
  1. webifier_extensions-1.0.1/.github/workflows/python-publish.yml +49 -0
  2. webifier_extensions-1.0.1/.gitignore +9 -0
  3. webifier_extensions-1.0.1/LICENSE +21 -0
  4. webifier_extensions-1.0.1/PKG-INFO +47 -0
  5. webifier_extensions-1.0.1/README.md +31 -0
  6. webifier_extensions-1.0.1/pyproject.toml +50 -0
  7. webifier_extensions-1.0.1/webifier_extensions/_resources.py +11 -0
  8. webifier_extensions-1.0.1/webifier_extensions/analytics/google/extension.py +41 -0
  9. webifier_extensions-1.0.1/webifier_extensions/chapters/extension.py +11 -0
  10. webifier_extensions-1.0.1/webifier_extensions/chapters/renderer.py +50 -0
  11. webifier_extensions-1.0.1/webifier_extensions/comments/extension.py +11 -0
  12. webifier_extensions-1.0.1/webifier_extensions/comments/renderer.py +35 -0
  13. webifier_extensions-1.0.1/webifier_extensions/markdown/extension.py +18 -0
  14. webifier_extensions-1.0.1/webifier_extensions/markdown/renderer.py +27 -0
  15. webifier_extensions-1.0.1/webifier_extensions/notebook/converter.py +63 -0
  16. webifier_extensions-1.0.1/webifier_extensions/notebook/extension.py +54 -0
  17. webifier_extensions-1.0.1/webifier_extensions/people/extension.py +11 -0
  18. webifier_extensions-1.0.1/webifier_extensions/people/renderer.py +78 -0
  19. webifier_extensions-1.0.1/webifier_extensions/registry.py +25 -0
  20. webifier_extensions-1.0.1/webifier_extensions/resume/assets/css/resume.css +476 -0
  21. webifier_extensions-1.0.1/webifier_extensions/resume/assets/js/resume.js +29 -0
  22. webifier_extensions-1.0.1/webifier_extensions/resume/experience.html +246 -0
  23. webifier_extensions-1.0.1/webifier_extensions/resume/extension.py +54 -0
  24. webifier_extensions-1.0.1/webifier_extensions/resume/publications.html +36 -0
  25. webifier_extensions-1.0.1/webifier_extensions/resume/renderer.py +21 -0
  26. webifier_extensions-1.0.1/webifier_extensions/search/extension.py +19 -0
  27. webifier_extensions-1.0.1/webifier_extensions/standard/assets/css/codehilite.css +74 -0
  28. webifier_extensions-1.0.1/webifier_extensions/standard/assets/css/main.css +115 -0
  29. webifier_extensions-1.0.1/webifier_extensions/standard/assets/images/colab-badge.svg +1 -0
  30. webifier_extensions-1.0.1/webifier_extensions/standard/content_page.py +28 -0
  31. webifier_extensions-1.0.1/webifier_extensions/standard/extension.py +26 -0
  32. webifier_extensions-1.0.1/webifier_extensions/standard/freeform.py +39 -0
  33. webifier_extensions-1.0.1/webifier_extensions/standard/links.py +24 -0
  34. webifier_extensions-1.0.1/webifier_extensions/standard/page.py +46 -0
  35. webifier_extensions-1.0.1/webifier_extensions/standard/section.py +69 -0
  36. webifier_extensions-1.0.1/webifier_extensions/standard/templates/content.html +47 -0
  37. webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/chapters.html +40 -0
  38. webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/comments.html +21 -0
  39. webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/footer.html +33 -0
  40. webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/head.html +49 -0
  41. webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/header.html +35 -0
  42. webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/link.html +58 -0
  43. webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/links.html +18 -0
  44. webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/meta.html +12 -0
  45. webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/nav.html +175 -0
  46. webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/page_navigation.html +29 -0
  47. webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/people.html +23 -0
  48. webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/person.html +84 -0
  49. webifier_extensions-1.0.1/webifier_extensions/standard/templates/page.html +53 -0
  50. webifier_extensions-1.0.1/webifier_extensions/standard/templates/section.html +76 -0
  51. webifier_extensions-1.0.1/webifier_extensions/theme/assets/css/theme.css +220 -0
  52. webifier_extensions-1.0.1/webifier_extensions/theme/assets/js/theme.js +110 -0
  53. webifier_extensions-1.0.1/webifier_extensions/theme/extension.py +52 -0
@@ -0,0 +1,49 @@
1
+ name: Upload Python Package
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ release:
8
+ types: [created]
9
+ workflow_dispatch:
10
+
11
+ jobs:
12
+ deploy:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ fetch-depth: 0
18
+
19
+ - name: Set up Python
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+
24
+ - name: Install dependencies
25
+ run: |
26
+ python -m pip install --upgrade pip
27
+ python -m pip install build twine
28
+
29
+ - name: Verify release tag
30
+ run: |
31
+ TAG="$(git describe --tags --exact-match)"
32
+ case "$TAG" in
33
+ v[0-9]*)
34
+ echo "Publishing $TAG"
35
+ ;;
36
+ *)
37
+ echo "Refusing to publish from non-release tag: $TAG"
38
+ exit 1
39
+ ;;
40
+ esac
41
+
42
+ - name: Build and publish
43
+ env:
44
+ TWINE_USERNAME: __token__
45
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
46
+ run: |
47
+ python -m build --sdist --wheel
48
+ python -m twine check dist/*
49
+ python -m twine upload dist/*
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .DS_Store
7
+ **/.DS_Store
8
+ dist/
9
+ build/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vahid Zehtab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: webifier-extensions
3
+ Version: 1.0.1
4
+ Summary: First-party extensions for Webifier static sites.
5
+ Author-email: Vahid Zehtab <vahid98zee@gmail.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: beautifulsoup4
10
+ Requires-Dist: markupsafe
11
+ Requires-Dist: nbconvert<8,>=7
12
+ Requires-Dist: nbformat
13
+ Provides-Extra: dev
14
+ Requires-Dist: ruff; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Webifier Extensions
18
+
19
+ First-party extensions for Webifier.
20
+
21
+ This distribution is installed automatically when users install `webifier`:
22
+
23
+ ```shell
24
+ pip install webifier
25
+ ```
26
+
27
+ For local extension development, install this package in the same environment as
28
+ the local Webifier core.
29
+
30
+ Enable named instances in a site configuration:
31
+
32
+ ```yaml
33
+ config:
34
+ webifier:
35
+ extensions:
36
+ site:
37
+ uses: webifier.standard
38
+ markdown:
39
+ uses: webifier.markdown
40
+ search:
41
+ uses: webifier.search
42
+ ```
43
+
44
+ Extensions can register renderers, content renderers, templates, themes, assets,
45
+ resolvers, format loaders, hooks, and config defaults. Hooks are page-aware, so a
46
+ `head` hook can inspect the current page data or page-local config and inject
47
+ JavaScript only on pages that need it.
@@ -0,0 +1,31 @@
1
+ # Webifier Extensions
2
+
3
+ First-party extensions for Webifier.
4
+
5
+ This distribution is installed automatically when users install `webifier`:
6
+
7
+ ```shell
8
+ pip install webifier
9
+ ```
10
+
11
+ For local extension development, install this package in the same environment as
12
+ the local Webifier core.
13
+
14
+ Enable named instances in a site configuration:
15
+
16
+ ```yaml
17
+ config:
18
+ webifier:
19
+ extensions:
20
+ site:
21
+ uses: webifier.standard
22
+ markdown:
23
+ uses: webifier.markdown
24
+ search:
25
+ uses: webifier.search
26
+ ```
27
+
28
+ Extensions can register renderers, content renderers, templates, themes, assets,
29
+ resolvers, format loaders, hooks, and config defaults. Hooks are page-aware, so a
30
+ `head` hook can inspect the current page data or page-local config and inject
31
+ JavaScript only on pages that need it.
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "webifier-extensions"
7
+ version = "1.0.1"
8
+ description = "First-party extensions for Webifier static sites."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Vahid Zehtab", email = "vahid98zee@gmail.com" },
14
+ ]
15
+ dependencies = [
16
+ "beautifulsoup4",
17
+ "markupsafe",
18
+ "nbconvert>=7,<8",
19
+ "nbformat",
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ dev = [
24
+ "ruff",
25
+ ]
26
+
27
+ [project.entry-points."webifier.extensions"]
28
+ "webifier.standard" = "webifier_extensions.standard.extension:StandardExtension"
29
+ "webifier.markdown" = "webifier_extensions.markdown.extension:MarkdownExtension"
30
+ "webifier.notebook" = "webifier_extensions.notebook.extension:NotebookExtension"
31
+ "webifier.search" = "webifier_extensions.search.extension:SearchExtension"
32
+ "webifier.theme" = "webifier_extensions.theme.extension:ThemeExtension"
33
+ "webifier.analytics.google" = "webifier_extensions.analytics.google.extension:GoogleAnalyticsExtension"
34
+ "webifier.comments" = "webifier_extensions.comments.extension:CommentsExtension"
35
+ "webifier.people" = "webifier_extensions.people.extension:PeopleExtension"
36
+ "webifier.chapters" = "webifier_extensions.chapters.extension:ChaptersExtension"
37
+ "webifier.resume" = "webifier_extensions.resume.extension:ResumeExtension"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["webifier_extensions"]
41
+
42
+ [tool.ruff]
43
+ target-version = "py310"
44
+ line-length = 120
45
+
46
+ [tool.ruff.lint]
47
+ select = ["E", "F", "I", "UP", "B", "SIM"]
48
+
49
+ [tool.ruff.lint.isort]
50
+ known-first-party = ["webifier_extensions"]
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import os
5
+
6
+
7
+ def package_path(package: str, *parts: str) -> str:
8
+ spec = importlib.util.find_spec(package)
9
+ if not spec or not spec.submodule_search_locations:
10
+ raise ValueError(f"Cannot locate package resources for {package!r}.")
11
+ return os.path.join(next(iter(spec.submodule_search_locations)), *parts)
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from markupsafe import Markup
4
+ from webifier.core.extensions import Extension, ExtensionContext
5
+
6
+
7
+ class GoogleAnalyticsExtension(Extension):
8
+ id = "webifier.analytics.google"
9
+ config_key = "google_analytics"
10
+
11
+ def register(self, ctx: ExtensionContext) -> None:
12
+ super().register(ctx)
13
+ ctx.add_hook("head", self.render_head)
14
+
15
+ def render_head(self, builder, *, config=None, **_kwargs) -> str:
16
+ analytics = {}
17
+ if isinstance(config, dict):
18
+ analytics = config.get("google_analytics", {})
19
+ if analytics is False or (
20
+ isinstance(analytics, dict) and analytics.get("enabled") is False
21
+ ):
22
+ return ""
23
+ if not isinstance(analytics, dict):
24
+ analytics = {}
25
+ measurement_id = analytics.get("measurement_id") or analytics.get("id")
26
+ if not measurement_id:
27
+ return ""
28
+ return Markup(
29
+ "\n".join(
30
+ [
31
+ "<!-- Global site tag (gtag.js) - Google Analytics -->",
32
+ f'<script async src="https://www.googletagmanager.com/gtag/js?id={measurement_id}"></script>',
33
+ "<script>",
34
+ " window.dataLayer = window.dataLayer || [];",
35
+ " function gtag(){dataLayer.push(arguments);}",
36
+ " gtag('js', new Date());",
37
+ f" gtag('config', '{measurement_id}');",
38
+ "</script>",
39
+ ]
40
+ )
41
+ )
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from webifier.core.extensions import Extension
4
+
5
+
6
+ class ChaptersExtension(Extension):
7
+ id = "webifier.chapters"
8
+ dependencies = ("webifier.standard",)
9
+ renderers = {
10
+ "chapters": "webifier_extensions.chapters.renderer.ChaptersRenderer",
11
+ }
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, ClassVar
4
+
5
+ from webifier.core.base import NodeContext, RendererModule
6
+
7
+
8
+ class ChaptersRenderer(RendererModule):
9
+ """Render chapters as a Bootstrap accordion."""
10
+
11
+ template: ClassVar[str] = "macros/chapters.html"
12
+ META_KEYS: ClassVar[frozenset[str]] = frozenset(
13
+ {
14
+ "kind",
15
+ "template",
16
+ "label",
17
+ "background",
18
+ "style",
19
+ "content",
20
+ }
21
+ )
22
+
23
+ def process(self, data: dict[str, Any], ctx: NodeContext, builder) -> dict[str, Any]:
24
+ """Process each chapter's content recursively."""
25
+ processed = dict(data)
26
+ if "content" in processed and isinstance(processed["content"], list):
27
+ chapters = []
28
+ for i, chapter in enumerate(processed["content"]):
29
+ if isinstance(chapter, dict):
30
+ chapter_processed = {}
31
+ for key, value in chapter.items():
32
+ if key in ("title",):
33
+ chapter_processed[key] = value
34
+ else:
35
+ chapter_processed[key] = builder.process_node(
36
+ value, ctx.child(f"chapter-{i}")
37
+ )
38
+ chapters.append(chapter_processed)
39
+ else:
40
+ chapters.append(chapter)
41
+ processed["content"] = chapters
42
+ return processed
43
+
44
+ def render(self, data: dict[str, Any], ctx: NodeContext, builder) -> str:
45
+ template = builder.jinja_env.get_template(self.template)
46
+ return template.module.render_chapters(
47
+ data,
48
+ data.get("content", []),
49
+ ctx.depth,
50
+ )
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from webifier.core.extensions import Extension
4
+
5
+
6
+ class CommentsExtension(Extension):
7
+ id = "webifier.comments"
8
+ config_key = "comments"
9
+ renderers = {
10
+ "comments": "webifier_extensions.comments.renderer.CommentsRenderer",
11
+ }
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, ClassVar
4
+
5
+ from webifier.core.base import NodeContext, RendererModule
6
+
7
+
8
+ class CommentsRenderer(RendererModule):
9
+ """Render an Utterances comment widget."""
10
+
11
+ template: ClassVar[str] = "macros/comments.html"
12
+ META_KEYS: ClassVar[frozenset[str]] = frozenset(
13
+ {
14
+ "kind",
15
+ "template",
16
+ "label",
17
+ "background",
18
+ "style",
19
+ "repo",
20
+ "issue_term",
21
+ "issue_label",
22
+ "theme",
23
+ "crossorigin",
24
+ }
25
+ )
26
+
27
+ def process(self, data: dict[str, Any], ctx: NodeContext, builder) -> dict[str, Any]:
28
+ """No children to process — just pass config through."""
29
+ # Inject global config for repo fallback
30
+ data["_config"] = builder.config
31
+ return data
32
+
33
+ def render(self, data: dict[str, Any], ctx: NodeContext, builder) -> str:
34
+ template = builder.jinja_env.get_template(self.template)
35
+ return template.module.render_comments(data, data.get("_config"))
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from webifier.core.extensions import Extension, ExtensionContext
4
+
5
+
6
+ class MarkdownExtension(Extension):
7
+ id = "webifier.markdown"
8
+ renderers = {
9
+ "markdown": "webifier_extensions.markdown.renderer.MarkdownRenderer",
10
+ }
11
+
12
+ def register(self, ctx: ExtensionContext) -> None:
13
+ super().register(ctx)
14
+ for key in (".md", ".markdown", "md", "markdown"):
15
+ ctx.register_content_renderer(key, self.build_markdown_page)
16
+
17
+ def build_markdown_page(self, builder, src: str, ctx):
18
+ return builder._build_markdown_page(src, ctx)
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, ClassVar
4
+
5
+ from webifier.core.base import NodeContext, RendererModule
6
+
7
+
8
+ class MarkdownRenderer(RendererModule):
9
+ """Render a markdown string to HTML."""
10
+
11
+ template: ClassVar[str] = "" # No template — direct render
12
+ META_KEYS: ClassVar[frozenset[str]] = frozenset({"kind", "template"})
13
+
14
+ def process(self, data: dict[str, Any], ctx: NodeContext, builder) -> dict[str, Any]:
15
+ return data
16
+
17
+ def render(self, data: dict[str, Any], ctx: NodeContext, builder) -> str:
18
+ """Render markdown content to HTML string."""
19
+ raw = data.get("content", "")
20
+ if not raw:
21
+ return ""
22
+ return builder.render_markdown(
23
+ raw,
24
+ assets_src_dir=ctx.assets_src_dir,
25
+ assets_target_dir=ctx.assets_target_dir,
26
+ search_links=ctx.search_links,
27
+ )
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from webifier.core.frontmatter import split_yaml_front_matter
6
+
7
+
8
+ def convert_notebook(builder, src: str, assets_dir: str) -> tuple[str, dict]:
9
+ """Convert a Jupyter notebook to HTML body content.
10
+
11
+ Uses nbconvert to export, then extracts the notebook container
12
+ and post-processes HTML for asset resolution.
13
+ """
14
+ from bs4 import BeautifulSoup
15
+
16
+ try:
17
+ from nbconvert import HTMLExporter
18
+ except ImportError as exc:
19
+ raise ImportError(
20
+ "nbconvert is required for notebook conversion. Install it with: pip install nbconvert"
21
+ ) from exc
22
+
23
+ notebook, metadata = read_notebook_with_metadata(src)
24
+
25
+ exporter = HTMLExporter()
26
+ body, _ = exporter.from_notebook_node(notebook)
27
+
28
+ # Extract just the notebook content
29
+ soup = BeautifulSoup(body, "html.parser")
30
+ container = soup.find(id="notebook-container") or soup.find("body") or soup
31
+ content = str(container)
32
+
33
+ # Post-process HTML for asset resolution
34
+ from webifier.core.html import process_html
35
+
36
+ content = process_html(
37
+ builder,
38
+ content,
39
+ assets_src_dir=os.path.dirname(src),
40
+ assets_target_dir=assets_dir,
41
+ )
42
+
43
+ return content, metadata
44
+
45
+
46
+ def read_notebook_with_metadata(src: str) -> tuple[object, dict]:
47
+ import nbformat
48
+
49
+ with open(src) as f:
50
+ notebook = nbformat.read(f, as_version=4)
51
+
52
+ cells = notebook.get("cells", [])
53
+ if not cells or cells[0].get("cell_type") != "markdown":
54
+ return notebook, {}
55
+
56
+ metadata, first_cell_body = split_yaml_front_matter(cells[0].get("source", ""))
57
+ if not metadata:
58
+ return notebook, {}
59
+ if first_cell_body.strip():
60
+ cells[0]["source"] = first_cell_body.lstrip("\n")
61
+ else:
62
+ notebook["cells"] = cells[1:]
63
+ return notebook, metadata
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from webifier.core.base import resolve_renderer
6
+ from webifier.core.extensions import Extension, ExtensionContext
7
+ from webifier.interface.io import prepend_baseurl, strip_suffixes
8
+
9
+ from .converter import convert_notebook
10
+
11
+
12
+ class NotebookExtension(Extension):
13
+ id = "webifier.notebook"
14
+ dependencies = ("webifier.standard",)
15
+
16
+ def register(self, ctx: ExtensionContext) -> None:
17
+ super().register(ctx)
18
+ for key in (".ipynb", "notebook"):
19
+ ctx.register_content_renderer(key, self.build_notebook_page)
20
+
21
+ def build_notebook_page(self, builder, src: str, ctx):
22
+ if not os.path.isfile(src):
23
+ print(f" Warning: notebook file not found: {src}")
24
+ return None
25
+
26
+ try:
27
+ body_html, metadata = convert_notebook(builder, src, builder.assets_dir)
28
+ except Exception as exc:
29
+ print(f" Warning: notebook conversion failed for {src}: {exc}")
30
+ return None
31
+
32
+ renderer = resolve_renderer("content-page", jinja_env=builder.jinja_env)
33
+ page_data = {
34
+ "content": body_html,
35
+ "metadata": metadata,
36
+ "title": metadata.get("title", os.path.basename(src)),
37
+ "page_url": prepend_baseurl(
38
+ strip_suffixes(src, builder._content_suffixes()),
39
+ builder.base_url,
40
+ ),
41
+ "source_path": src,
42
+ }
43
+ if builder.root_data:
44
+ page_data["nav"] = builder.root_data.get("nav")
45
+ page_data["footer"] = builder.root_data.get("footer")
46
+ page_data["config"] = builder.config
47
+ if builder.repo_full_name:
48
+ nb_dir = os.path.dirname(src)
49
+ nb_name = strip_suffixes(os.path.basename(src), [".ipynb"])
50
+ page_data["colab"] = (
51
+ f"https://colab.research.google.com/github/"
52
+ f"{builder.repo_full_name}/blob/master/{nb_dir}/{nb_name}.ipynb"
53
+ )
54
+ return renderer.render(page_data, ctx, builder)
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from webifier.core.extensions import Extension
4
+
5
+
6
+ class PeopleExtension(Extension):
7
+ id = "webifier.people"
8
+ dependencies = ("webifier.standard",)
9
+ renderers = {
10
+ "people": "webifier_extensions.people.renderer.PeopleRenderer",
11
+ }
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, ClassVar
4
+
5
+ from webifier.core.base import NodeContext, RendererModule
6
+
7
+
8
+ class PeopleRenderer(RendererModule):
9
+ """Render a grid of people cards."""
10
+
11
+ template: ClassVar[str] = "macros/people.html"
12
+ META_KEYS: ClassVar[frozenset[str]] = frozenset(
13
+ {
14
+ "kind",
15
+ "template",
16
+ "label",
17
+ "background",
18
+ "style",
19
+ "content",
20
+ }
21
+ )
22
+
23
+ def process(self, data: dict[str, Any], ctx: NodeContext, builder) -> dict[str, Any]:
24
+ """Process each person entry."""
25
+ processed = dict(data)
26
+ if "content" in processed and isinstance(processed["content"], list):
27
+ people = []
28
+ for person in processed["content"]:
29
+ if isinstance(person, dict):
30
+ person = _process_person(person, ctx, builder)
31
+ people.append(person)
32
+ processed["content"] = people
33
+ return processed
34
+
35
+ def render(self, data: dict[str, Any], ctx: NodeContext, builder) -> str:
36
+ template = builder.jinja_env.get_template(self.template)
37
+ return template.module.render_people(data)
38
+
39
+
40
+ def _process_person(person: dict, ctx: NodeContext, builder) -> dict:
41
+ """Process a single person dict — resolve images, render bio."""
42
+ # Resolve profile image
43
+ if "image" in person:
44
+ img = person["image"]
45
+ if isinstance(img, str) and not img.startswith(("http", "data:")):
46
+ new_path = builder.files.copy_file(
47
+ img,
48
+ img,
49
+ src_dir=ctx.assets_src_dir,
50
+ target_dir=ctx.assets_target_dir or builder.assets_dir,
51
+ )
52
+ if new_path:
53
+ person["image"] = new_path
54
+ elif "github" in person:
55
+ person["image"] = f"https://github.com/{person['github']}.png"
56
+
57
+ # GitHub-derived image fallback
58
+ if "image" not in person and "github" in person:
59
+ person["image"] = f"https://github.com/{person['github']}.png"
60
+
61
+ # Render bio as markdown
62
+ if "bio" in person and isinstance(person["bio"], str):
63
+ person["bio"] = builder.render_markdown(
64
+ person["bio"],
65
+ assets_src_dir=ctx.assets_src_dir,
66
+ assets_target_dir=ctx.assets_target_dir,
67
+ )
68
+
69
+ # Process contact links
70
+ if "contact" in person and isinstance(person["contact"], list):
71
+ processed_links = []
72
+ for link in person["contact"]:
73
+ if isinstance(link, dict):
74
+ link = builder._process_link(link, ctx)
75
+ processed_links.append(link)
76
+ person["contact"] = processed_links
77
+
78
+ return person
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from .analytics.google.extension import GoogleAnalyticsExtension
4
+ from .chapters.extension import ChaptersExtension
5
+ from .comments.extension import CommentsExtension
6
+ from .markdown.extension import MarkdownExtension
7
+ from .notebook.extension import NotebookExtension
8
+ from .people.extension import PeopleExtension
9
+ from .resume.extension import ResumeExtension
10
+ from .search.extension import SearchExtension
11
+ from .standard.extension import StandardExtension
12
+ from .theme.extension import ThemeExtension
13
+
14
+ EXTENSIONS = {
15
+ "webifier.standard": StandardExtension,
16
+ "webifier.markdown": MarkdownExtension,
17
+ "webifier.notebook": NotebookExtension,
18
+ "webifier.search": SearchExtension,
19
+ "webifier.theme": ThemeExtension,
20
+ "webifier.analytics.google": GoogleAnalyticsExtension,
21
+ "webifier.comments": CommentsExtension,
22
+ "webifier.people": PeopleExtension,
23
+ "webifier.chapters": ChaptersExtension,
24
+ "webifier.resume": ResumeExtension,
25
+ }