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.
- webifier_extensions-1.0.1/.github/workflows/python-publish.yml +49 -0
- webifier_extensions-1.0.1/.gitignore +9 -0
- webifier_extensions-1.0.1/LICENSE +21 -0
- webifier_extensions-1.0.1/PKG-INFO +47 -0
- webifier_extensions-1.0.1/README.md +31 -0
- webifier_extensions-1.0.1/pyproject.toml +50 -0
- webifier_extensions-1.0.1/webifier_extensions/_resources.py +11 -0
- webifier_extensions-1.0.1/webifier_extensions/analytics/google/extension.py +41 -0
- webifier_extensions-1.0.1/webifier_extensions/chapters/extension.py +11 -0
- webifier_extensions-1.0.1/webifier_extensions/chapters/renderer.py +50 -0
- webifier_extensions-1.0.1/webifier_extensions/comments/extension.py +11 -0
- webifier_extensions-1.0.1/webifier_extensions/comments/renderer.py +35 -0
- webifier_extensions-1.0.1/webifier_extensions/markdown/extension.py +18 -0
- webifier_extensions-1.0.1/webifier_extensions/markdown/renderer.py +27 -0
- webifier_extensions-1.0.1/webifier_extensions/notebook/converter.py +63 -0
- webifier_extensions-1.0.1/webifier_extensions/notebook/extension.py +54 -0
- webifier_extensions-1.0.1/webifier_extensions/people/extension.py +11 -0
- webifier_extensions-1.0.1/webifier_extensions/people/renderer.py +78 -0
- webifier_extensions-1.0.1/webifier_extensions/registry.py +25 -0
- webifier_extensions-1.0.1/webifier_extensions/resume/assets/css/resume.css +476 -0
- webifier_extensions-1.0.1/webifier_extensions/resume/assets/js/resume.js +29 -0
- webifier_extensions-1.0.1/webifier_extensions/resume/experience.html +246 -0
- webifier_extensions-1.0.1/webifier_extensions/resume/extension.py +54 -0
- webifier_extensions-1.0.1/webifier_extensions/resume/publications.html +36 -0
- webifier_extensions-1.0.1/webifier_extensions/resume/renderer.py +21 -0
- webifier_extensions-1.0.1/webifier_extensions/search/extension.py +19 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/assets/css/codehilite.css +74 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/assets/css/main.css +115 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/assets/images/colab-badge.svg +1 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/content_page.py +28 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/extension.py +26 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/freeform.py +39 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/links.py +24 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/page.py +46 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/section.py +69 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/templates/content.html +47 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/chapters.html +40 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/comments.html +21 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/footer.html +33 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/head.html +49 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/header.html +35 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/link.html +58 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/links.html +18 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/meta.html +12 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/nav.html +175 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/page_navigation.html +29 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/people.html +23 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/templates/macros/person.html +84 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/templates/page.html +53 -0
- webifier_extensions-1.0.1/webifier_extensions/standard/templates/section.html +76 -0
- webifier_extensions-1.0.1/webifier_extensions/theme/assets/css/theme.css +220 -0
- webifier_extensions-1.0.1/webifier_extensions/theme/assets/js/theme.js +110 -0
- 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,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
|
+
}
|