Skip to content

Commit

Permalink
Add initial markdown handling
Browse files Browse the repository at this point in the history
  • Loading branch information
vkottler committed Oct 20, 2024
1 parent 2e4f1ca commit 685c342
Show file tree
Hide file tree
Showing 23 changed files with 239 additions and 189 deletions.
File renamed without changes.
163 changes: 163 additions & 0 deletions runtimepy/net/html/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
"""
A module implementing HTML-related interfaces.
"""

# built-in
from io import StringIO
from typing import Optional

# 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,
)


def create_app_shell(parent: Element, **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")

# Dark theme.
container["data-bs-theme"] = "dark"

# Buttons.
button_column = div(parent=container)
button_column.add_class("d-flex", "flex-column", "h-100", "bg-dark-subtle")

# Dark/light theme switch button.
bootstrap_button(
icon_str("lightbulb"),
tooltip=" Toggle light/dark.",
id="theme-button",
parent=button_column,
)

return container, button_column


def markdown_page(parent: Element, markdown: str, **kwargs) -> None:
"""Compose a landing page."""

container = centered_markdown(
create_app_shell(parent, **kwargs)[0], markdown, "h-100", "text-body"
)
container.add_class("overflow-y-auto")


def common_css(document: Html) -> None:
"""Add common CSS to an HTML document."""

append_kind(document.head, "font", kind="css", tag="style")
add_bootstrap_css(document.head)
append_kind(
document.head, "main", "bootstrap_extra", kind="css", tag="style"
)


def full_markdown_page(document: Html, markdown: str) -> None:
"""Render a full markdown HTML app."""

common_css(document)
markdown_page(document.body, markdown, id=PKG_NAME)

# JavaScript.
append_kind(document.body, "markdown_page")
add_bootstrap_js(document.body)


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


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


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"


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
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from vcorelib.io.file_writer import IndentedFileWriter

# internal
from runtimepy.net.server.app.bootstrap import icon_str
from runtimepy.net.html.bootstrap import icon_str

TEXT = "font-monospace"
BOOTSTRAP_BUTTON = f"rounded-0 {TEXT} button-bodge text-nowrap"
Expand Down Expand Up @@ -167,7 +167,7 @@ def slider(

def centered_markdown(
parent: Element, markdown: str, *container_classes: str
) -> None:
) -> Element:
"""Add centered markdown."""

container = div(parent=parent)
Expand Down Expand Up @@ -198,3 +198,5 @@ def centered_markdown(
div(parent=horiz_container)

div(parent=container)

return container
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@

# internal
from runtimepy import PKG_NAME
from runtimepy.net.server.app.bootstrap import icon_str
from runtimepy.net.server.app.bootstrap.elements import (
from runtimepy.net.html import create_app_shell
from runtimepy.net.html.bootstrap.elements import (
BOOTSTRAP_BUTTON,
bootstrap_button,
collapse_button,
flex,
toggle_button,
Expand Down Expand Up @@ -66,30 +65,6 @@ def create_nav_container(
return content


def create_app_shell(parent: Element, **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")

# Dark theme.
container["data-bs-theme"] = "dark"

# Buttons.
button_column = div(parent=container)
button_column.add_class("d-flex", "flex-column", "h-100", "bg-dark-subtle")

# Dark/light theme switch button.
bootstrap_button(
icon_str("lightbulb"),
tooltip=" Toggle light/dark.",
id="theme-button",
parent=button_column,
)

return container, button_column


class TabbedContent:
"""A tabbed-content container."""

Expand Down
44 changes: 37 additions & 7 deletions runtimepy/net/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,19 @@
from typing import Any, Optional, TextIO, Union

# third-party
from vcorelib.io import JsonObject
import aiofiles
from vcorelib import DEFAULT_ENCODING
from vcorelib.io import IndentedFileWriter, JsonObject
from vcorelib.paths import Pathlike, find_file, normalize

# internal
from runtimepy import DEFAULT_EXT, PKG_NAME
from runtimepy.channel.environment.command import GLOBAL
from runtimepy.net.html import full_markdown_page
from runtimepy.net.http.header import RequestHeader
from runtimepy.net.http.request_target import PathMaybeQuery
from runtimepy.net.http.response import ResponseHeader
from runtimepy.net.server.html import HtmlApp, HtmlApps, html_handler
from runtimepy.net.server.html import HtmlApp, HtmlApps, get_html, html_handler
from runtimepy.net.server.json import encode_json, json_handler
from runtimepy.net.tcp.http import HttpConnection
from runtimepy.util import normalize_root, path_has_part
Expand Down Expand Up @@ -106,7 +109,7 @@ def init(self) -> None:
with favicon.open("rb") as favicon_fd:
type(self).favicon_data = favicon_fd.read()

def try_redirect(
async def try_redirect(
self, path: PathMaybeQuery, response: ResponseHeader
) -> Optional[bytes]:
"""Try handling any HTTP redirect rules."""
Expand All @@ -123,7 +126,26 @@ def try_redirect(

return result

def try_file(
async def render_markdown(
self, path: Path, response: ResponseHeader, **kwargs
) -> bytes:
"""Render a markdown file as HTML and return the result."""

document = get_html()

async with aiofiles.open(path, mode="r") as path_fd:
with IndentedFileWriter.string() as writer:
writer.write_markdown(await path_fd.read(), **kwargs)
full_markdown_page(
document,
writer.stream.getvalue(), # type: ignore
)

response["Content-Type"] = f"text/html; charset={DEFAULT_ENCODING}"

return document.encode_str().encode()

async def try_file(
self, path: PathMaybeQuery, response: ResponseHeader
) -> Optional[bytes]:
"""Try serving this path as a file directly from the file-system."""
Expand All @@ -133,6 +155,12 @@ def try_file(
# Try serving the path as a file.
for search in self.paths:
candidate = search.joinpath(path[0][1:])

# Handle markdown sources.
md_candidate = candidate.with_suffix(".md")
if md_candidate.is_file():
return await self.render_markdown(md_candidate, response)

if candidate.is_file():
mime, encoding = mimetypes.guess_type(candidate, strict=False)

Expand All @@ -146,8 +174,8 @@ def try_file(
self.logger.info("Serving '%s' (MIME: %s)", candidate, mime)

# Return the file data.
with candidate.open("rb") as path_fd:
result = path_fd.read()
async with aiofiles.open(candidate, mode="rb") as path_fd:
result = await path_fd.read()

break

Expand Down Expand Up @@ -223,7 +251,9 @@ async def get_handler(

# Try serving a file and handling redirects.
for handler in [self.try_redirect, self.try_file]:
result = handler(request.target.origin_form, response)
result = await handler(
request.target.origin_form, response
)
if result is not None:
return result

Expand Down
7 changes: 3 additions & 4 deletions runtimepy/net/server/app/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@
# internal
from runtimepy import PKG_NAME
from runtimepy.net.arbiter.info import AppInfo
from runtimepy.net.server.app.bootstrap import add_bootstrap_js
from runtimepy.net.server.app.bootstrap.tabs import TabbedContent
from runtimepy.net.server.app.css import common_css
from runtimepy.net.server.app.files import append_kind
from runtimepy.net.html import append_kind, common_css
from runtimepy.net.html.bootstrap import add_bootstrap_js
from runtimepy.net.html.bootstrap.tabs import TabbedContent

TabPopulater = Callable[[TabbedContent], None]

Expand Down
2 changes: 1 addition & 1 deletion runtimepy/net/server/app/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@

# internal
from runtimepy.net.arbiter.info import AppInfo
from runtimepy.net.html.bootstrap.tabs import TabbedContent
from runtimepy.net.http.header import RequestHeader
from runtimepy.net.http.response import ResponseHeader
from runtimepy.net.server.app.base import WebApplication
from runtimepy.net.server.app.bootstrap.tabs import TabbedContent
from runtimepy.net.server.html import HtmlApp

DOCUMENTS: dict[str, Html] = {}
Expand Down
20 changes: 0 additions & 20 deletions runtimepy/net/server/app/css.py

This file was deleted.

7 changes: 2 additions & 5 deletions runtimepy/net/server/app/env/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,8 @@
# internal
from runtimepy import PKG_NAME
from runtimepy.net.arbiter.info import AppInfo
from runtimepy.net.server.app.bootstrap.elements import (
centered_markdown,
input_box,
)
from runtimepy.net.server.app.bootstrap.tabs import TabbedContent
from runtimepy.net.html.bootstrap.elements import centered_markdown, input_box
from runtimepy.net.html.bootstrap.tabs import TabbedContent
from runtimepy.net.server.app.env.modal import Modal
from runtimepy.net.server.app.env.settings import plot_settings
from runtimepy.net.server.app.env.tab import ChannelEnvironmentTab
Expand Down
4 changes: 2 additions & 2 deletions runtimepy/net/server/app/env/modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

# internal
from runtimepy import PKG_NAME
from runtimepy.net.server.app.bootstrap.elements import TEXT
from runtimepy.net.server.app.bootstrap.tabs import TabbedContent
from runtimepy.net.html.bootstrap.elements import TEXT
from runtimepy.net.html.bootstrap.tabs import TabbedContent


class Modal:
Expand Down
Loading

0 comments on commit 685c342

Please sign in to comment.