webifier-extensions 1.0.1__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.
Files changed (52) hide show
  1. webifier_extensions/_resources.py +11 -0
  2. webifier_extensions/analytics/google/extension.py +41 -0
  3. webifier_extensions/chapters/extension.py +11 -0
  4. webifier_extensions/chapters/renderer.py +50 -0
  5. webifier_extensions/comments/extension.py +11 -0
  6. webifier_extensions/comments/renderer.py +35 -0
  7. webifier_extensions/markdown/extension.py +18 -0
  8. webifier_extensions/markdown/renderer.py +27 -0
  9. webifier_extensions/notebook/converter.py +63 -0
  10. webifier_extensions/notebook/extension.py +54 -0
  11. webifier_extensions/people/extension.py +11 -0
  12. webifier_extensions/people/renderer.py +78 -0
  13. webifier_extensions/registry.py +25 -0
  14. webifier_extensions/resume/assets/css/resume.css +476 -0
  15. webifier_extensions/resume/assets/js/resume.js +29 -0
  16. webifier_extensions/resume/experience.html +246 -0
  17. webifier_extensions/resume/extension.py +54 -0
  18. webifier_extensions/resume/publications.html +36 -0
  19. webifier_extensions/resume/renderer.py +21 -0
  20. webifier_extensions/search/extension.py +19 -0
  21. webifier_extensions/standard/assets/css/codehilite.css +74 -0
  22. webifier_extensions/standard/assets/css/main.css +115 -0
  23. webifier_extensions/standard/assets/images/colab-badge.svg +1 -0
  24. webifier_extensions/standard/content_page.py +28 -0
  25. webifier_extensions/standard/extension.py +26 -0
  26. webifier_extensions/standard/freeform.py +39 -0
  27. webifier_extensions/standard/links.py +24 -0
  28. webifier_extensions/standard/page.py +46 -0
  29. webifier_extensions/standard/section.py +69 -0
  30. webifier_extensions/standard/templates/content.html +47 -0
  31. webifier_extensions/standard/templates/macros/chapters.html +40 -0
  32. webifier_extensions/standard/templates/macros/comments.html +21 -0
  33. webifier_extensions/standard/templates/macros/footer.html +33 -0
  34. webifier_extensions/standard/templates/macros/head.html +49 -0
  35. webifier_extensions/standard/templates/macros/header.html +35 -0
  36. webifier_extensions/standard/templates/macros/link.html +58 -0
  37. webifier_extensions/standard/templates/macros/links.html +18 -0
  38. webifier_extensions/standard/templates/macros/meta.html +12 -0
  39. webifier_extensions/standard/templates/macros/nav.html +175 -0
  40. webifier_extensions/standard/templates/macros/page_navigation.html +29 -0
  41. webifier_extensions/standard/templates/macros/people.html +23 -0
  42. webifier_extensions/standard/templates/macros/person.html +84 -0
  43. webifier_extensions/standard/templates/page.html +53 -0
  44. webifier_extensions/standard/templates/section.html +76 -0
  45. webifier_extensions/theme/assets/css/theme.css +220 -0
  46. webifier_extensions/theme/assets/js/theme.js +110 -0
  47. webifier_extensions/theme/extension.py +52 -0
  48. webifier_extensions-1.0.1.dist-info/METADATA +47 -0
  49. webifier_extensions-1.0.1.dist-info/RECORD +52 -0
  50. webifier_extensions-1.0.1.dist-info/WHEEL +4 -0
  51. webifier_extensions-1.0.1.dist-info/entry_points.txt +11 -0
  52. webifier_extensions-1.0.1.dist-info/licenses/LICENSE +21 -0
@@ -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
+ }