"""
A module implementing HTML-related interfaces.
"""
# built-in
from io import StringIO
from typing import Any, Optional
from urllib.parse import parse_qs, urlencode
# third-party
from svgen.element import Element
from svgen.element.html import Html, div
from vcorelib import DEFAULT_ENCODING
from vcorelib.io import IndentedFileWriter
from vcorelib.paths import find_file
# internal
from runtimepy import PKG_NAME
from runtimepy.net.html.bootstrap import (
add_bootstrap_css,
add_bootstrap_js,
icon_str,
)
from runtimepy.net.html.bootstrap.elements import (
bootstrap_button,
centered_markdown,
)
[docs]
def create_app_shell(
parent: Element,
bootstrap_theme: str = "dark",
use_button_column: bool = True,
**kwargs,
) -> tuple[Element, Element]:
"""Create a bootstrap-based application shell."""
container = div(parent=parent, **kwargs)
container.add_class("d-flex", "align-items-start", "bg-body", "h-100")
# Dark theme.
container["data-bs-theme"] = bootstrap_theme
# Buttons.
button_column = div(
id="button-column",
parent=container if use_button_column else None,
head_child=div(
class_str="flex-grow-1 border-bottom "
"bg-gradient-secondary-to-bottom"
),
tail_child=div(
class_str="flex-grow-1 border-top bg-gradient-secondary-to-top"
),
)
button_column.add_class(
"d-flex", "flex-column", "h-100", f"bg-{bootstrap_theme}-subtle"
)
# Dark/light theme switch button.
bootstrap_button(
icon_str("highlights"),
tooltip="Toggle light/dark.",
id="theme-button",
parent=button_column,
title="change theme button",
)
# Fullscreen toggle.
bootstrap_button(
icon_str("fullscreen"),
tooltip="Toggle fullscreen.",
id="fullscreen-button",
parent=button_column,
title="toggle fullscreen",
)
return container, button_column
[docs]
def markdown_page(
parent: Element, markdown: str, **kwargs
) -> tuple[Element, Element]:
"""Compose a landing page."""
parent, button_column = create_app_shell(parent, **kwargs)
container = centered_markdown(parent, markdown, "h-100", "text-body")
container.add_class("overflow-y-auto")
return container, button_column
[docs]
def common_css(document: Html, online: bool = False) -> None:
"""Add common CSS to an HTML document."""
append_kind(document.head, "font", kind="css", tag="style")
add_bootstrap_css(document.head, online)
append_kind(
document.head, "main", "bootstrap_extra", kind="css", tag="style"
)
[docs]
def full_markdown_page(
document: Html,
markdown: str,
uri_query: Optional[str] = None,
online: bool = False,
) -> None:
"""Render a full markdown HTML app."""
common_css(document)
markdown_kwargs: dict[str, Any] = {"id": PKG_NAME}
params: dict[str, str] = {"print": "true"}
if uri_query:
parsed = parse_qs(uri_query)
for key, val in parsed.items():
params[key] = val[-1]
# Handle pages optimized for document creation.
# from vcorelib.python import StrToBool
# if "print" in parsed and any(
# StrToBool.check(x) for x in parsed["print"]
# ):
# markdown_kwargs["bootstrap_theme"] = "light"
# markdown_kwargs["use_button_column"] = False
_, button_column = markdown_page(
document.body, markdown, **markdown_kwargs
)
button_column.add_class("border-end")
bootstrap_button(
icon_str("printer"),
tooltip="Printer-friendly view.",
id="print-button",
title="print-view button",
parent=div(
tag="a",
href=f"?{urlencode(params)}#light-mode",
parent=button_column,
),
)
# JavaScript.
append_kind(document.body, "util", "markdown_page")
add_bootstrap_js(document.body, online)
[docs]
def handle_worker(writer: IndentedFileWriter) -> int:
"""Boilerplate contents for worker thread block."""
# Not currently used.
# return write_found_file(
# writer, kind_url("js", "webgl-debug", subdir="third-party")
# )
del writer
return 0
[docs]
def write_found_file(writer: IndentedFileWriter, *args, **kwargs) -> bool:
"""Write a file's contents to the file-writer's stream."""
result = False
entry = find_file(*args, **kwargs)
if entry is not None:
with entry.open(encoding=DEFAULT_ENCODING) as path_fd:
for line in path_fd:
writer.write(line)
result = True
return result
[docs]
def kind_url(
kind: str, name: str, subdir: str = None, package: str = PKG_NAME
) -> str:
"""Return a URL to find a package resource."""
path = kind
if subdir is not None:
path += "/" + subdir
path += f"/{name}"
return f"package://{package}/{path}.{kind}"
WORKER_TYPE = "text/js-worker"
[docs]
def append_kind(
element: Element,
*names: str,
package: str = PKG_NAME,
kind: str = "js",
tag: str = "script",
subdir: str = None,
worker: bool = False,
) -> Optional[Element]:
"""Append a new script element."""
elem = Element(tag=tag, allow_no_end_tag=False)
with StringIO() as stream:
writer = IndentedFileWriter(stream, per_indent=2)
found_count = 0
for name in names:
if write_found_file(
writer, kind_url(kind, name, subdir=subdir, package=package)
):
found_count += 1
if worker:
found_count += handle_worker(writer)
if found_count:
elem.text = stream.getvalue()
if found_count:
element.children.append(elem)
if worker:
elem["type"] = WORKER_TYPE
return elem if found_count else None