Conversion of User Guide to the SHH stack (#2781)
This commit is contained in:
0
guide/webapp/display/layouts/__init__.py
Normal file
0
guide/webapp/display/layouts/__init__.py
Normal file
25
guide/webapp/display/layouts/base.py
Normal file
25
guide/webapp/display/layouts/base.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Generator
|
||||
|
||||
from html5tagger import Builder
|
||||
from sanic import Request
|
||||
|
||||
|
||||
class BaseLayout:
|
||||
def __init__(self, builder: Builder):
|
||||
self.builder = builder
|
||||
|
||||
@contextmanager
|
||||
def __call__(
|
||||
self, request: Request, full: bool = True
|
||||
) -> Generator[BaseLayout, None, None]:
|
||||
with self.layout(request, full=full):
|
||||
yield self
|
||||
|
||||
@contextmanager
|
||||
def layout(
|
||||
self, request: Request, full: bool = True
|
||||
) -> Generator[None, None, None]:
|
||||
yield
|
||||
0
guide/webapp/display/layouts/elements/__init__.py
Normal file
0
guide/webapp/display/layouts/elements/__init__.py
Normal file
75
guide/webapp/display/layouts/elements/footer.py
Normal file
75
guide/webapp/display/layouts/elements/footer.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from datetime import datetime
|
||||
|
||||
from html5tagger import Builder, E # type: ignore
|
||||
from sanic import Request
|
||||
|
||||
|
||||
def do_footer(builder: Builder, request: Request) -> None:
|
||||
builder.footer(
|
||||
_pagination(request),
|
||||
_content(),
|
||||
class_="footer",
|
||||
)
|
||||
|
||||
|
||||
def _pagination(request: Request) -> Builder:
|
||||
return E.div(
|
||||
_pagination_left(request), _pagination_right(request), class_="level"
|
||||
)
|
||||
|
||||
|
||||
def _pagination_left(request: Request) -> Builder:
|
||||
item = E.div(class_="level-item")
|
||||
if not hasattr(request.ctx, "previous_page"):
|
||||
return E.div(item, class_="level-left")
|
||||
with item:
|
||||
if p := request.ctx.previous_page:
|
||||
path = p.relative_path.with_suffix(".html")
|
||||
item.a(
|
||||
f"← {p.meta.title}",
|
||||
href=f"/{path}",
|
||||
hx_get=f"/{path}",
|
||||
hx_target="#content",
|
||||
hx_swap="innerHTML",
|
||||
hx_push_url="true",
|
||||
class_="button pagination",
|
||||
)
|
||||
return E.div(item, class_="level-left")
|
||||
|
||||
|
||||
def _pagination_right(request: Request) -> Builder:
|
||||
item = E.div(class_="level-item")
|
||||
if not hasattr(request.ctx, "next_page"):
|
||||
return E.div(item, class_="level-right")
|
||||
with item:
|
||||
if p := request.ctx.next_page:
|
||||
path = p.relative_path.with_suffix(".html")
|
||||
item.a(
|
||||
f"{p.meta.title} →",
|
||||
href=f"/{path}",
|
||||
hx_get=f"/{path}",
|
||||
hx_target="#content",
|
||||
hx_swap="innerHTML",
|
||||
hx_push_url="true",
|
||||
class_="button pagination",
|
||||
)
|
||||
return E.div(item, class_="level-right")
|
||||
|
||||
|
||||
def _content() -> Builder:
|
||||
year = datetime.now().year
|
||||
inner = E.p(
|
||||
E.a(
|
||||
"MIT Licensed",
|
||||
href="https://github.com/sanic-org/sanic/blob/master/LICENSE",
|
||||
target="_blank",
|
||||
rel="nofollow noopener noreferrer",
|
||||
).br()(
|
||||
E.small(f"Copyright © 2018-{year} Sanic Community Organization")
|
||||
),
|
||||
)
|
||||
return E.div(
|
||||
inner,
|
||||
E.p("~ Made with ❤️ and ☕️ ~"),
|
||||
class_="content has-text-centered",
|
||||
)
|
||||
68
guide/webapp/display/layouts/elements/navbar.py
Normal file
68
guide/webapp/display/layouts/elements/navbar.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from webapp.display.layouts.models import MenuItem
|
||||
|
||||
from html5tagger import Builder, E # type: ignore
|
||||
from sanic import Request
|
||||
|
||||
|
||||
def do_navbar(builder: Builder, request: Request) -> None:
|
||||
navbar_items = [
|
||||
_render_navbar_item(item, request)
|
||||
for item in request.app.config.NAVBAR
|
||||
]
|
||||
container = E.div(
|
||||
_search_form(request), *navbar_items, class_="navbar-end"
|
||||
)
|
||||
|
||||
builder.nav(
|
||||
E.div(container, class_="navbar-menu"),
|
||||
class_="navbar is-hidden-touch",
|
||||
)
|
||||
|
||||
|
||||
def _search_form(request: Request) -> Builder:
|
||||
return E.div(
|
||||
E.div(
|
||||
E.input(
|
||||
id_="search",
|
||||
type_="text",
|
||||
placeholder="Search",
|
||||
class_="input",
|
||||
value=request.args.get("q", ""),
|
||||
hx_target="#content",
|
||||
hx_swap="innerHTML",
|
||||
hx_push_url="true",
|
||||
hx_trigger="keyup changed delay:500ms",
|
||||
hx_get=f"/{request.ctx.language}/search",
|
||||
hx_params="*",
|
||||
),
|
||||
class_="control",
|
||||
),
|
||||
class_="navbar-item",
|
||||
)
|
||||
|
||||
|
||||
def _render_navbar_item(item: MenuItem, request: Request) -> Builder:
|
||||
if item.items:
|
||||
return E.div(
|
||||
E.a(item.label, class_="navbar-link"),
|
||||
E.div(
|
||||
*(
|
||||
_render_navbar_item(subitem, request)
|
||||
for subitem in item.items
|
||||
),
|
||||
class_="navbar-dropdown",
|
||||
),
|
||||
class_="navbar-item has-dropdown is-hoverable",
|
||||
)
|
||||
|
||||
kwargs = {
|
||||
"class_": "navbar-item",
|
||||
}
|
||||
if item.href:
|
||||
kwargs["href"] = item.href
|
||||
kwargs["target"] = "_blank"
|
||||
kwargs["rel"] = "nofollow noopener noreferrer"
|
||||
elif item.path:
|
||||
kwargs["href"] = f"/{request.ctx.language}/{item.path}"
|
||||
internal = [item.label]
|
||||
return E.a(*internal, **kwargs)
|
||||
118
guide/webapp/display/layouts/elements/sidebar.py
Normal file
118
guide/webapp/display/layouts/elements/sidebar.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from webapp.display.layouts.models import MenuItem
|
||||
from webapp.display.text import slugify
|
||||
|
||||
from html5tagger import Builder, E # type: ignore
|
||||
from sanic import Request
|
||||
|
||||
|
||||
def do_sidebar(builder: Builder, request: Request) -> None:
|
||||
builder.a(class_="burger")(E.span().span().span().span())
|
||||
builder.aside(*_menu_items(request), class_="menu")
|
||||
|
||||
|
||||
def _menu_items(request: Request) -> list[Builder]:
|
||||
return [
|
||||
_sanic_logo(request),
|
||||
*_sidebar_items(request),
|
||||
E.hr(),
|
||||
E.p("Current with version ").strong(
|
||||
request.app.config.GENERAL.current_version
|
||||
),
|
||||
E.hr(),
|
||||
E.p("Want more? ").a(
|
||||
"sanicbook.com", href="https://sanicbook.com", target="_blank"
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def _sanic_logo(request: Request) -> Builder:
|
||||
return E.a(
|
||||
class_="navbar-item sanic-simple-logo",
|
||||
href=f"https://sanic.dev/{request.ctx.language}/",
|
||||
)(
|
||||
E.img(
|
||||
src="/assets/images/sanic-framework-logo-simple-400x97.png", # noqa: E501
|
||||
alt="Sanic Framework",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _sidebar_items(request: Request) -> list[Builder]:
|
||||
return [
|
||||
builder
|
||||
for item in request.app.config.SIDEBAR
|
||||
for builder in _render_sidebar_item(item, request, True)
|
||||
]
|
||||
|
||||
|
||||
def _render_sidebar_item(
|
||||
item: MenuItem, request: Request, root: bool = False
|
||||
) -> list[Builder]:
|
||||
builders: list[Builder] = []
|
||||
if root:
|
||||
builders.append(E.p(class_="menu-label")(item.label))
|
||||
else:
|
||||
builders.append(_single_sidebar_item(item, request))
|
||||
|
||||
if item.items:
|
||||
ul = E.ul(class_="menu-list")
|
||||
with ul:
|
||||
for subitem in item.items:
|
||||
sub_builders = _render_sidebar_item(subitem, request)
|
||||
ul(*sub_builders)
|
||||
builders.append(ul)
|
||||
|
||||
return builders
|
||||
|
||||
|
||||
def _single_sidebar_item(item: MenuItem, request: Request) -> Builder:
|
||||
if item.path and item.path.startswith("/"):
|
||||
path = item.path
|
||||
else:
|
||||
path = f"/{request.ctx.language}/{item.path}" if item.path else ""
|
||||
kwargs = {}
|
||||
classes: list[str] = []
|
||||
li_classes = "menu-item"
|
||||
_, page, _ = request.app.ctx.get_page(
|
||||
request.ctx.language, item.path or ""
|
||||
)
|
||||
if request.path == path:
|
||||
classes.append("is-active")
|
||||
if item.href:
|
||||
kwargs["href"] = item.href
|
||||
kwargs["target"] = "_blank"
|
||||
kwargs["rel"] = "nofollow noopener noreferrer"
|
||||
elif not path:
|
||||
li_classes += " is-group"
|
||||
if _is_open_item(item, request.ctx.language, request.path):
|
||||
classes.append("is-open")
|
||||
else:
|
||||
kwargs.update(
|
||||
{
|
||||
"href": path,
|
||||
"hx-get": path,
|
||||
"hx-target": "#content",
|
||||
"hx-swap": "innerHTML",
|
||||
"hx-push-url": "true",
|
||||
}
|
||||
)
|
||||
kwargs["class_"] = " ".join(classes)
|
||||
inner = E().a(item.label, **kwargs)
|
||||
if page and page.anchors:
|
||||
with inner.ul(class_="anchor-list"):
|
||||
for anchor in page.anchors:
|
||||
inner.li(
|
||||
E.a(anchor.strip("`"), href=f"{path}#{slugify(anchor)}"),
|
||||
class_="is-anchor",
|
||||
)
|
||||
return E.li(inner, class_=li_classes)
|
||||
|
||||
|
||||
def _is_open_item(item: MenuItem, language: str, current_path: str) -> bool:
|
||||
path = f"/{language}/{item.path}" if item.path else ""
|
||||
if current_path == path:
|
||||
return True
|
||||
for subitem in item.items:
|
||||
if _is_open_item(subitem, language, current_path):
|
||||
return True
|
||||
return False
|
||||
50
guide/webapp/display/layouts/home.py
Normal file
50
guide/webapp/display/layouts/home.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Generator
|
||||
|
||||
from html5tagger import Builder, E
|
||||
from sanic import Request
|
||||
|
||||
from .base import BaseLayout
|
||||
|
||||
|
||||
class HomeLayout(BaseLayout):
|
||||
@contextmanager
|
||||
def layout(
|
||||
self, request: Request, full: bool = True
|
||||
) -> Generator[None, None, None]:
|
||||
self._hero(request.ctx.language)
|
||||
with self.builder.div(class_="container"):
|
||||
yield
|
||||
|
||||
def _hero(self, language: str) -> None:
|
||||
with self.builder.section(class_="hero is-large has-text-centered"):
|
||||
self.builder.div(
|
||||
E.h1(E.span("Sanic"), class_="title"),
|
||||
E.h2(class_="subtitle")("Build fast. Run fast."),
|
||||
E.h3(class_="tagline")("Accelerate your web app development"),
|
||||
self._do_buttons(language),
|
||||
class_="hero-body",
|
||||
)
|
||||
|
||||
def _do_buttons(self, language: str) -> Builder:
|
||||
builder = E.div(class_="buttons is-centered")
|
||||
with builder:
|
||||
builder.a(
|
||||
"Get Started",
|
||||
class_="button is-primary",
|
||||
href=f"/{language}/guide/getting-started.html",
|
||||
)
|
||||
builder.a(
|
||||
"Help",
|
||||
class_="button is-outlined",
|
||||
href=f"/{language}/help.html",
|
||||
)
|
||||
builder.a(
|
||||
"GitHub",
|
||||
class_="button is-outlined",
|
||||
href="https://github.com/sanic-org/sanic",
|
||||
target="_blank",
|
||||
)
|
||||
return builder
|
||||
45
guide/webapp/display/layouts/main.py
Normal file
45
guide/webapp/display/layouts/main.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from contextlib import contextmanager
|
||||
from typing import Generator
|
||||
|
||||
from webapp.display.layouts.elements.footer import do_footer
|
||||
from webapp.display.layouts.elements.navbar import do_navbar
|
||||
from webapp.display.layouts.elements.sidebar import do_sidebar
|
||||
|
||||
from sanic import Request
|
||||
|
||||
from .base import BaseLayout
|
||||
|
||||
|
||||
class MainLayout(BaseLayout):
|
||||
@contextmanager
|
||||
def layout(
|
||||
self, request: Request, full: bool = True
|
||||
) -> Generator[None, None, None]:
|
||||
if full:
|
||||
with self.builder.div(class_="is-flex"):
|
||||
self._sidebar(request)
|
||||
with self.builder.main(class_="is-flex-grow-1"):
|
||||
self._navbar(request)
|
||||
with self.builder.div(class_="container", id="content"):
|
||||
with self._content_wrapper():
|
||||
yield
|
||||
self._footer(request)
|
||||
else:
|
||||
with self._content_wrapper():
|
||||
yield
|
||||
self._footer(request)
|
||||
|
||||
@contextmanager
|
||||
def _content_wrapper(self) -> Generator[None, None, None]:
|
||||
with self.builder.section(class_="section"):
|
||||
with self.builder.article():
|
||||
yield
|
||||
|
||||
def _navbar(self, request: Request) -> None:
|
||||
do_navbar(self.builder, request)
|
||||
|
||||
def _sidebar(self, request: Request) -> None:
|
||||
do_sidebar(self.builder, request)
|
||||
|
||||
def _footer(self, request: Request) -> None:
|
||||
do_footer(self.builder, request)
|
||||
14
guide/webapp/display/layouts/models.py
Normal file
14
guide/webapp/display/layouts/models.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from msgspec import Struct, field
|
||||
|
||||
|
||||
class MenuItem(Struct, kw_only=False, omit_defaults=True):
|
||||
label: str
|
||||
path: str | None = None
|
||||
href: str | None = None
|
||||
items: list[MenuItem] = field(default_factory=list)
|
||||
|
||||
|
||||
class GeneralConfig(Struct, kw_only=False):
|
||||
current_version: str
|
||||
Reference in New Issue
Block a user