diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 8d5b8a2b..7f243ae9 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -77,7 +77,7 @@ jobs: - run: | mk python-release owner=vkottler \ - repo=runtimepy version=5.7.3 + repo=runtimepy version=5.7.4 if: | matrix.python-version == '3.12' && matrix.system == 'ubuntu-latest' diff --git a/README.md b/README.md index 9741a426..629fb7fa 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ ===================================== generator=datazen version=3.1.4 - hash=ef917873929a5dc6a70367fde7f1447f + hash=fbb71900eae64b008d2a2df1b7dde92b ===================================== --> -# runtimepy ([5.7.3](https://pypi.org/project/runtimepy/)) +# runtimepy ([5.7.4](https://pypi.org/project/runtimepy/)) [![python](https://img.shields.io/pypi/pyversions/runtimepy.svg)](https://pypi.org/project/runtimepy/) ![Build Status](https://github.com/vkottler/runtimepy/workflows/Python%20Package/badge.svg) diff --git a/local/variables/package.yaml b/local/variables/package.yaml index f5b58ba7..cc1ab4b9 100644 --- a/local/variables/package.yaml +++ b/local/variables/package.yaml @@ -1,5 +1,5 @@ --- major: 5 minor: 7 -patch: 3 +patch: 4 entry: runtimepy diff --git a/pyproject.toml b/pyproject.toml index 1218442f..b6c6328f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta:__legacy__" [project] name = "runtimepy" -version = "5.7.3" +version = "5.7.4" description = "A framework for implementing Python services." readme = "README.md" requires-python = ">=3.11" diff --git a/runtimepy/__init__.py b/runtimepy/__init__.py index 81245040..233823eb 100644 --- a/runtimepy/__init__.py +++ b/runtimepy/__init__.py @@ -1,7 +1,7 @@ # ===================================== # generator=datazen # version=3.1.4 -# hash=37a32f51e0bf04276100ce27658b7f8f +# hash=cb3ed2d01203d78ad794d39500d706f9 # ===================================== """ @@ -10,7 +10,7 @@ DESCRIPTION = "A framework for implementing Python services." PKG_NAME = "runtimepy" -VERSION = "5.7.3" +VERSION = "5.7.4" # runtimepy-specific content. METRICS_NAME = "metrics" diff --git a/runtimepy/data/js/markdown_page.js b/runtimepy/data/js/markdown_page.js new file mode 100644 index 00000000..f787f4a8 --- /dev/null +++ b/runtimepy/data/js/markdown_page.js @@ -0,0 +1,24 @@ +/* Dark is hard-coded initial state (in HTML). */ +let lightMode = false; + +function lightDarkClick(event) { + lightMode = !lightMode; + + document.getElementById("runtimepy") + .setAttribute("data-bs-theme", lightMode ? "light" : "dark"); + + window.location.hash = lightMode ? "#light-mode" : ""; +} + +let lightDarkButton = document.getElementById("theme-button"); +if (lightDarkButton) { + lightDarkButton.addEventListener("click", lightDarkClick); +} + +if (window.location.hash) { + let parts = window.location.hash.slice(1).split(","); + + if (parts.includes("light-mode")) { + lightDarkButton.click(); + } +} diff --git a/runtimepy/net/html/__init__.py b/runtimepy/net/html/__init__.py new file mode 100644 index 00000000..7bb21bc5 --- /dev/null +++ b/runtimepy/net/html/__init__.py @@ -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 diff --git a/runtimepy/net/server/app/bootstrap/__init__.py b/runtimepy/net/html/bootstrap/__init__.py similarity index 100% rename from runtimepy/net/server/app/bootstrap/__init__.py rename to runtimepy/net/html/bootstrap/__init__.py diff --git a/runtimepy/net/server/app/bootstrap/elements.py b/runtimepy/net/html/bootstrap/elements.py similarity index 98% rename from runtimepy/net/server/app/bootstrap/elements.py rename to runtimepy/net/html/bootstrap/elements.py index dc5a209c..4025611e 100644 --- a/runtimepy/net/server/app/bootstrap/elements.py +++ b/runtimepy/net/html/bootstrap/elements.py @@ -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" @@ -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) @@ -198,3 +198,5 @@ def centered_markdown( div(parent=horiz_container) div(parent=container) + + return container diff --git a/runtimepy/net/server/app/bootstrap/tabs.py b/runtimepy/net/html/bootstrap/tabs.py similarity index 82% rename from runtimepy/net/server/app/bootstrap/tabs.py rename to runtimepy/net/html/bootstrap/tabs.py index 5f934d38..cea1d4a1 100644 --- a/runtimepy/net/server/app/bootstrap/tabs.py +++ b/runtimepy/net/html/bootstrap/tabs.py @@ -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, @@ -84,27 +83,7 @@ def __init__(self, name: str, parent: Element) -> None: """Initialize this instance.""" self.name = name - - # Create application container. - self.container = div(parent=parent, id=name) - self.container.add_class("d-flex", "align-items-start", "bg-body") - - # Dark theme. - self.container["data-bs-theme"] = "dark" - - # Buttons. - self.button_column = div(parent=self.container) - self.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=self.button_column, - ) + self.container, self.button_column = create_app_shell(parent, id=name) # Toggle tabs button. self.add_button("Toggle tabs", f"#{PKG_NAME}-tabs", id="tabs-button") diff --git a/runtimepy/net/server/__init__.py b/runtimepy/net/server/__init__.py index 81a5d9af..2ce9e778 100644 --- a/runtimepy/net/server/__init__.py +++ b/runtimepy/net/server/__init__.py @@ -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 @@ -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.""" @@ -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.""" @@ -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) @@ -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 @@ -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 diff --git a/runtimepy/net/server/app/base.py b/runtimepy/net/server/app/base.py index 8f9195d3..17ae34ab 100644 --- a/runtimepy/net/server/app/base.py +++ b/runtimepy/net/server/app/base.py @@ -11,12 +11,9 @@ # internal from runtimepy import PKG_NAME from runtimepy.net.arbiter.info import AppInfo -from runtimepy.net.server.app.bootstrap import ( - add_bootstrap_css, - add_bootstrap_js, -) -from runtimepy.net.server.app.bootstrap.tabs import TabbedContent -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] @@ -53,11 +50,7 @@ def populate(self, document: Html, app: TabPopulater) -> None: """Populate the body element with the application.""" # CSS. - 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" - ) + common_css(document) # Worker code. append_kind( diff --git a/runtimepy/net/server/app/create.py b/runtimepy/net/server/app/create.py index 7d652bed..fde6c6a3 100644 --- a/runtimepy/net/server/app/create.py +++ b/runtimepy/net/server/app/create.py @@ -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] = {} diff --git a/runtimepy/net/server/app/env/__init__.py b/runtimepy/net/server/app/env/__init__.py index b1fe1fec..e619e794 100644 --- a/runtimepy/net/server/app/env/__init__.py +++ b/runtimepy/net/server/app/env/__init__.py @@ -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 diff --git a/runtimepy/net/server/app/env/modal.py b/runtimepy/net/server/app/env/modal.py index d986e9f8..c8946660 100644 --- a/runtimepy/net/server/app/env/modal.py +++ b/runtimepy/net/server/app/env/modal.py @@ -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: diff --git a/runtimepy/net/server/app/env/settings.py b/runtimepy/net/server/app/env/settings.py index 0e0e4e99..24c162ad 100644 --- a/runtimepy/net/server/app/env/settings.py +++ b/runtimepy/net/server/app/env/settings.py @@ -6,8 +6,8 @@ from svgen.element.html import div # internal -from runtimepy.net.server.app.bootstrap.elements import flex, slider -from runtimepy.net.server.app.bootstrap.tabs import TabbedContent +from runtimepy.net.html.bootstrap.elements import flex, slider +from runtimepy.net.html.bootstrap.tabs import TabbedContent from runtimepy.net.server.app.env.modal import Modal from runtimepy.net.server.app.placeholder import under_construction diff --git a/runtimepy/net/server/app/env/tab/base.py b/runtimepy/net/server/app/env/tab/base.py index 833ba794..7affe1ba 100644 --- a/runtimepy/net/server/app/env/tab/base.py +++ b/runtimepy/net/server/app/env/tab/base.py @@ -13,7 +13,7 @@ ChannelCommandProcessor, ) from runtimepy.net.arbiter.info import AppInfo -from runtimepy.net.server.app.bootstrap.tabs import TabbedContent +from runtimepy.net.html.bootstrap.tabs import TabbedContent from runtimepy.net.server.app.tab import Tab diff --git a/runtimepy/net/server/app/env/tab/controls.py b/runtimepy/net/server/app/env/tab/controls.py index 2216168b..e8f645ef 100644 --- a/runtimepy/net/server/app/env/tab/controls.py +++ b/runtimepy/net/server/app/env/tab/controls.py @@ -13,7 +13,7 @@ from runtimepy.channel import AnyChannel from runtimepy.channel.environment import ChannelEnvironment from runtimepy.enum import RuntimeEnum -from runtimepy.net.server.app.bootstrap.elements import slider, toggle_button +from runtimepy.net.html.bootstrap.elements import slider, toggle_button from runtimepy.net.server.app.env.tab.base import ChannelEnvironmentTabBase from runtimepy.net.server.app.env.widgets import ( TABLE_BUTTON_CLASSES, diff --git a/runtimepy/net/server/app/env/tab/html.py b/runtimepy/net/server/app/env/tab/html.py index e4f5ce31..9f3764b1 100644 --- a/runtimepy/net/server/app/env/tab/html.py +++ b/runtimepy/net/server/app/env/tab/html.py @@ -12,7 +12,7 @@ # internal from runtimepy.channel import AnyChannel from runtimepy.enum import RuntimeEnum -from runtimepy.net.server.app.bootstrap.elements import ( +from runtimepy.net.html.bootstrap.elements import ( TEXT, centered_markdown, flex, diff --git a/runtimepy/net/server/app/env/widgets.py b/runtimepy/net/server/app/env/widgets.py index 6617c6b2..47aac2a0 100644 --- a/runtimepy/net/server/app/env/widgets.py +++ b/runtimepy/net/server/app/env/widgets.py @@ -14,7 +14,7 @@ ChannelCommandProcessor, ) from runtimepy.enum import RuntimeEnum -from runtimepy.net.server.app.bootstrap.elements import ( +from runtimepy.net.html.bootstrap.elements import ( flex, input_box, set_tooltip, diff --git a/runtimepy/net/server/app/files.py b/runtimepy/net/server/app/files.py index f294017f..6a020397 100644 --- a/runtimepy/net/server/app/files.py +++ b/runtimepy/net/server/app/files.py @@ -4,32 +4,14 @@ # built-in from io import StringIO -from typing import Optional # third-party from svgen.element import Element -from vcorelib import DEFAULT_ENCODING from vcorelib.io import IndentedFileWriter -from vcorelib.paths import find_file # internal from runtimepy import PKG_NAME - - -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 +from runtimepy.net.html import kind_url, write_found_file def set_text_to_file(element: Element, *args, **kwargs) -> bool: @@ -45,21 +27,6 @@ def set_text_to_file(element: Element, *args, **kwargs) -> bool: 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}" - - def set_text_to_kind( element: Element, kind: str, @@ -72,55 +39,3 @@ def set_text_to_kind( return set_text_to_file( element, kind_url(kind, name, subdir=subdir, package=package) ) - - -WORKER_TYPE = "text/js-worker" - - -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 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 diff --git a/runtimepy/net/server/app/landing_page.py b/runtimepy/net/server/app/landing_page.py index 1ec05483..0f2e781f 100644 --- a/runtimepy/net/server/app/landing_page.py +++ b/runtimepy/net/server/app/landing_page.py @@ -10,6 +10,7 @@ # internal from runtimepy.net.arbiter.info import AppInfo +from runtimepy.net.html import full_markdown_page from runtimepy.net.http.header import RequestHeader from runtimepy.net.http.response import ResponseHeader @@ -28,6 +29,11 @@ def landing_page( del response del request_data - print(app) + full_markdown_page( + document, + app.config_param("landing_page", {}, strict=True).get( # type: ignore + "markdown", "no data" + ), + ) return document diff --git a/runtimepy/net/server/app/placeholder.py b/runtimepy/net/server/app/placeholder.py index 2c08b28a..7c316f78 100644 --- a/runtimepy/net/server/app/placeholder.py +++ b/runtimepy/net/server/app/placeholder.py @@ -8,8 +8,8 @@ # internal from runtimepy.net.arbiter.info import AppInfo -from runtimepy.net.server.app.bootstrap import icon_str -from runtimepy.net.server.app.bootstrap.tabs import TabbedContent +from runtimepy.net.html.bootstrap import icon_str +from runtimepy.net.html.bootstrap.tabs import TabbedContent from runtimepy.net.server.app.tab import Tab diff --git a/runtimepy/net/server/app/sound.py b/runtimepy/net/server/app/sound.py index ab2e8c30..be26619e 100644 --- a/runtimepy/net/server/app/sound.py +++ b/runtimepy/net/server/app/sound.py @@ -7,7 +7,7 @@ from svgen.element.html import div # built-in -from runtimepy.net.server.app.bootstrap.elements import bootstrap_button +from runtimepy.net.html.bootstrap.elements import bootstrap_button from runtimepy.net.server.app.tab import Tab diff --git a/runtimepy/net/server/app/tab.py b/runtimepy/net/server/app/tab.py index b36d4632..55d7f82d 100644 --- a/runtimepy/net/server/app/tab.py +++ b/runtimepy/net/server/app/tab.py @@ -13,9 +13,9 @@ # internal from runtimepy.net.arbiter.info import AppInfo -from runtimepy.net.server.app.bootstrap import icon_str -from runtimepy.net.server.app.bootstrap.tabs import TabbedContent -from runtimepy.net.server.app.files import kind_url, write_found_file +from runtimepy.net.html import kind_url, write_found_file +from runtimepy.net.html.bootstrap import icon_str +from runtimepy.net.html.bootstrap.tabs import TabbedContent class Tab: diff --git a/runtimepy/net/server/html.py b/runtimepy/net/server/html.py index 216e4f0d..597335bd 100644 --- a/runtimepy/net/server/html.py +++ b/runtimepy/net/server/html.py @@ -20,6 +20,12 @@ HtmlApps = dict[str, HtmlApp] +def get_html() -> Html: + """Get a default HTML document.""" + + return Html(HttpConnection.identity) + + async def html_handler( apps: HtmlApps, stream: TextIO, @@ -36,8 +42,4 @@ async def html_handler( # Create the application. app = apps.get(request.target.path, default_app) if app is not None: - ( - await app( - Html(HttpConnection.identity), request, response, request_data - ) - ).render(stream) + (await app(get_html(), request, response, request_data)).render(stream) diff --git a/tasks/dev.yaml b/tasks/dev.yaml index 8863442e..a9517bc8 100644 --- a/tasks/dev.yaml +++ b/tasks/dev.yaml @@ -3,6 +3,7 @@ includes: - package://runtimepy/server.yaml - dev_no_wait.yaml - ../tests/data/valid/connection_arbiter/test_ssl.yaml + - ../tests/data/valid/connection_arbiter/landing_page.yaml port_overrides: runtimepy_https_server: 8443 diff --git a/tests/data/valid/connection_arbiter/landing_page.yaml b/tests/data/valid/connection_arbiter/landing_page.yaml new file mode 100644 index 00000000..e1bad50d --- /dev/null +++ b/tests/data/valid/connection_arbiter/landing_page.yaml @@ -0,0 +1,10 @@ +--- +config: + + # Additional web applications. + landing_page: + paths: [/landing_page.html] + markdown: | + # Sample Markdown + + Sample content. diff --git a/tests/data/valid/connection_arbiter/runtimepy_http.yaml b/tests/data/valid/connection_arbiter/runtimepy_http.yaml index 88324842..29ae4508 100644 --- a/tests/data/valid/connection_arbiter/runtimepy_http.yaml +++ b/tests/data/valid/connection_arbiter/runtimepy_http.yaml @@ -3,6 +3,7 @@ includes_left: - package://runtimepy/server.yaml includes: - test_ssl.yaml + - landing_page.yaml app: - tests.net.stream.runtimepy_http_test @@ -18,10 +19,6 @@ config: foo: bar xdg_fragment: "wave1,hide-tabs,hide-channels/wave1:sin,cos" - # Additional web applications. - landing_page: - paths: [/landing_page.html] - ports: - {name: tftp_server, type: udp} diff --git a/tests/net/server/__init__.py b/tests/net/server/__init__.py index 0953294b..2e2d090f 100644 --- a/tests/net/server/__init__.py +++ b/tests/net/server/__init__.py @@ -71,6 +71,7 @@ async def runtimepy_http_client_server( client.request(RequestHeader(target="/index.html")), client.request(RequestHeader(target="/test_json.html")), client.request(RequestHeader(target="/landing_page.html")), + client.request(RequestHeader(target="/README.md")), # Files from file-system. client.request(RequestHeader(target="/sample.json")), client.request(RequestHeader(target="/manifest.yaml")), diff --git a/tests/net/server/app/test_files.py b/tests/net/server/app/test_files.py index a88abbbf..84f524fe 100644 --- a/tests/net/server/app/test_files.py +++ b/tests/net/server/app/test_files.py @@ -7,7 +7,8 @@ # module under test from runtimepy import PKG_NAME -from runtimepy.net.server.app.files import kind_url, set_text_to_file +from runtimepy.net.html import kind_url +from runtimepy.net.server.app.files import set_text_to_file def test_set_text_to_file_basic():