This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/main by this push:
     new 1e376c7  Remove detected instances of markup in strings
1e376c7 is described below

commit 1e376c7e21694b4d15bd220fc4e0874ffb73ad5d
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Oct 27 19:07:15 2025 +0000

    Remove detected instances of markup in strings
---
 atr/admin/__init__.py    | 55 +++++++++++++++++--------------------
 atr/analysis.py          | 13 ---------
 atr/get/__init__.py      |  4 +--
 atr/get/example_test.py  | 38 --------------------------
 atr/htm.py               | 70 ++++++++++++++++++++++++++++++++++++++++--------
 atr/mail.py              | 51 +++--------------------------------
 atr/post/__init__.py     |  4 +--
 atr/post/example_test.py | 39 ---------------------------
 atr/routes/download.py   | 17 +++++++-----
 atr/routes/finish.py     |  3 +--
 atr/routes/published.py  | 31 +++++++++++----------
 11 files changed, 118 insertions(+), 207 deletions(-)

diff --git a/atr/admin/__init__.py b/atr/admin/__init__.py
index 8edf6f2..676d572 100644
--- a/atr/admin/__init__.py
+++ b/atr/admin/__init__.py
@@ -64,6 +64,10 @@ class BrowseAsUserForm(forms.Typed):
     submit = forms.submit("Browse as this user")
 
 
+class CheckKeysForm(forms.Typed):
+    submit = forms.submit("Check public signing key details")
+
+
 class DeleteCommitteeKeysForm(forms.Typed):
     committee_name = forms.select("Committee")
     confirm_delete = forms.string(
@@ -86,12 +90,20 @@ class DeleteReleaseForm(forms.Typed):
     submit = forms.submit("Delete selected releases permanently")
 
 
+class DeleteTestKeysForm(forms.Typed):
+    submit = forms.submit("Delete all OpenPGP keys for test user")
+
+
 class LdapLookupForm(forms.Typed):
     uid = forms.optional("ASF UID (optional)", placeholder="Enter ASF UID, 
e.g. johnsmith, or * for all")
     email = forms.optional("Email address (optional)", placeholder="Enter 
email address, e.g. [email protected]")
     submit = forms.submit("Lookup")
 
 
+class RegenerateKeysForm(forms.Typed):
+    submit = forms.submit("Regenerate all KEYS files")
+
+
 @admin.get("/all-releases")
 async def all_releases(session: web.Committer) -> str:
     """Display a list of all releases across all phases."""
@@ -298,19 +310,14 @@ async def _delete_test_openpgp_keys(session: 
web.Committer) -> quart.Response |
     test_uid = "test"
 
     if quart.request.method != "POST":
-        empty_form = await forms.Empty.create_form()
-        return quart.Response(
-            f"""
-<form method="post">
-  <button type="submit">Delete all OpenPGP keys for {test_uid} user</button>
-  {empty_form.hidden_tag()}
-</form>
-""",
-            mimetype="text/html",
-        )
+        delete_form = await DeleteTestKeysForm.create_form()
+        rendered_form = forms.render_simple(delete_form, action="")
+        return quart.Response(str(rendered_form), mimetype="text/html")
 
     # This is a POST request
-    await util.validate_empty_form()
+    delete_form = await DeleteTestKeysForm.create_form()
+    if not await delete_form.validate_on_submit():
+        raise base.ASFQuartException("Invalid form submission. Please check 
your input and try again.", errorcode=400)
 
     async with storage.write() as write:
         wafc = write.as_foundation_committer()
@@ -442,16 +449,9 @@ async def keys_check_post(session: web.Committer) -> 
quart.Response:
 async def _keys_check(session: web.Committer) -> quart.Response:
     """Check public signing key details."""
     if quart.request.method != "POST":
-        empty_form = await forms.Empty.create_form()
-        return quart.Response(
-            f"""
-<form method="post">
-  <button type="submit">Check public signing key details</button>
-  {empty_form.hidden_tag()}
-</form>
-""",
-            mimetype="text/html",
-        )
+        check_form = await CheckKeysForm.create_form()
+        rendered_form = forms.render_simple(check_form, action="")
+        return quart.Response(str(rendered_form), mimetype="text/html")
 
     try:
         result = await _check_keys()
@@ -474,16 +474,9 @@ async def keys_regenerate_all_post(session: web.Committer) 
-> quart.Response:
 async def _keys_regenerate_all(session: web.Committer) -> quart.Response:
     """Regenerate the KEYS file for all committees."""
     if quart.request.method != "POST":
-        empty_form = await forms.Empty.create_form()
-        return quart.Response(
-            f"""
-<form method="post">
-  <button type="submit">Regenerate all KEYS files</button>
-  {empty_form.hidden_tag()}
-</form>
-""",
-            mimetype="text/html",
-        )
+        regenerate_form = await RegenerateKeysForm.create_form()
+        rendered_form = forms.render_simple(regenerate_form, action="")
+        return quart.Response(str(rendered_form), mimetype="text/html")
 
     async with db.session() as data:
         committee_names = [c.name for c in await data.committee().all()]
diff --git a/atr/analysis.py b/atr/analysis.py
index d0ef58a..7a86426 100755
--- a/atr/analysis.py
+++ b/atr/analysis.py
@@ -179,19 +179,6 @@ def architecture_pattern() -> str:
     return "(" + "|".join(architectures) + ")(?=[_.-])"
 
 
-def candidate_highlight(path: pathlib.Path) -> str:
-    parts = []
-    for part in path.parts:
-        if ("<" in part) or (">" in part) or ("&" in part):
-            # TODO: Should perhaps check for ' and " too for attribute value 
safety
-            raise ValueError(f"Invalid path segment: {part}")
-        if _CANDIDATE_WHOLE.match(part):
-            parts.append(f"<strong>{part}</strong>")
-            continue
-        parts.append(_CANDIDATE_PARTIAL.sub(r"<strong>\g<0></strong>", part))
-    return str(pathlib.Path(*parts))
-
-
 def candidate_match(segment: str) -> re.Match[str] | None:
     return _CANDIDATE_WHOLE.match(segment) or 
_CANDIDATE_PARTIAL.search(segment)
 
diff --git a/atr/get/__init__.py b/atr/get/__init__.py
index 729f0db..352ab48 100644
--- a/atr/get/__init__.py
+++ b/atr/get/__init__.py
@@ -24,8 +24,6 @@ import atr.get.compose as compose
 import atr.get.distribution as distribution
 import atr.get.vote as vote
 
-from .example_test import respond as example_test
-
 ROUTES_MODULE: Final[Literal[True]] = True
 
-__all__ = ["announce", "candidate", "committees", "compose", "distribution", 
"example_test", "vote"]
+__all__ = ["announce", "candidate", "committees", "compose", "distribution", 
"vote"]
diff --git a/atr/get/example_test.py b/atr/get/example_test.py
deleted file mode 100644
index 2a29a13..0000000
--- a/atr/get/example_test.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-import atr.blueprints.get as get
-import atr.forms as forms
-import atr.post as post
-import atr.util as util
-import atr.web as web
-
-
[email protected]("/example/test")
-async def respond(session: web.Committer) -> str:
-    empty_form = await forms.Empty.create_form()
-    return f"""\
-<h1>Test route (GET)</h1>
-<p>Hello, {session.asf_uid}!</p>
-<p>This is a test GET route for committers only.</p>
-
-<h2>Test POST submission</h2>
-<form method="post" action="{util.as_url(post.example_test)}">
-    {empty_form.hidden_tag()}
-    <button type="submit" class="btn btn-primary">Submit to POST route</button>
-</form>
-"""
diff --git a/atr/htm.py b/atr/htm.py
index ac3587d..9d2e1b9 100644
--- a/atr/htm.py
+++ b/atr/htm.py
@@ -17,6 +17,7 @@
 
 from __future__ import annotations
 
+import contextlib
 from typing import TYPE_CHECKING, Any
 
 import htpy
@@ -24,12 +25,14 @@ import htpy
 from . import log
 
 if TYPE_CHECKING:
-    from collections.abc import Callable
-
+    from collections.abc import Callable, Generator
 
 type Element = htpy.Element
+type VoidElement = htpy.VoidElement
 
 a = htpy.a
+body = htpy.body
+br = htpy.br
 button = htpy.button
 code = htpy.code
 details = htpy.details
@@ -39,6 +42,7 @@ form = htpy.form
 h1 = htpy.h1
 h2 = htpy.h2
 h3 = htpy.h3
+html = htpy.html
 li = htpy.li
 p = htpy.p
 pre = htpy.pre
@@ -46,11 +50,13 @@ script = htpy.script
 span = htpy.span
 strong = htpy.strong
 summary = htpy.summary
+style = htpy.style
 table = htpy.table
 tbody = htpy.tbody
 td = htpy.td
 th = htpy.th
 thead = htpy.thead
+title = htpy.title
 tr = htpy.tr
 ul = htpy.ul
 
@@ -99,6 +105,19 @@ class Block:
     def __repr__(self) -> str:
         return f"{self.element!r}[*{self.elements!r}]"
 
+    def __check_parent(self, child_tag: str, allowed_parent_tags: set[str]) -> 
None:
+        # TODO: We should make this a static check
+        tag_name = self.__tag_name()
+        if self.element is not None:
+            if tag_name not in allowed_parent_tags:
+                permitted = ", ".join(allowed_parent_tags) + f", not 
{tag_name}"
+                raise ValueError(f"{child_tag} can only be used as a child of 
{permitted}")
+
+    def __tag_name(self) -> str:
+        if self.element is None:
+            return "div"
+        return self.element._name
+
     def append(self, eob: Block | Element) -> None:
         match eob:
             case Block():
@@ -107,11 +126,19 @@ class Block:
             case htpy.Element():
                 self.elements.append(eob)
 
-    def collect(self, separator: str | None = None, depth: int = 1) -> Element:
+    @contextlib.contextmanager
+    def block(
+        self, element: Element | None = None, separator: Element | VoidElement 
| str | None = None
+    ) -> Generator[Block, Any, Any]:
+        block = Block(element)
+        yield block
+        self.append(block.collect(separator=separator))
+
+    def collect(self, separator: Element | VoidElement | str | None = None, 
depth: int = 1) -> Element:
         src = log.caller_name(depth=depth)
 
         if separator is not None:
-            separated: list[Element | str] = [separator] * (2 * 
len(self.elements) - 1)
+            separated: list[Element | VoidElement | str] = [separator] * (2 * 
len(self.elements) - 1)
             separated[::2] = self.elements
             elements = separated
         else:
@@ -124,21 +151,24 @@ class Block:
             self.element._name,
             self.element._attrs,
             self.element._children,
-        )
-        # TODO: Check that there are no injection attacks possible here
-        if ' data-src="' not in new_element._attrs:
-            if new_element._attrs:
-                new_element._attrs = new_element._attrs + f' data-src="{src}"'
-            else:
-                new_element._attrs = f' data-src="{src}"'
+        )(data_src=src)
+        # if self.element._name == "html":
+        #     return "<!doctype html>" + new_element[*elements]
         return new_element[*elements]
 
     @property
     def a(self) -> BlockElementCallable:
+        self.__check_parent("a", {"div", "p", "pre"})
         return BlockElementCallable(self, a)
 
+    @property
+    def body(self) -> BlockElementCallable:
+        self.__check_parent("body", {"html"})
+        return BlockElementCallable(self, body)
+
     @property
     def code(self) -> BlockElementCallable:
+        self.__check_parent("code", {"div", "p"})
         return BlockElementCallable(self, code)
 
     @property
@@ -151,26 +181,32 @@ class Block:
 
     @property
     def h1(self) -> BlockElementCallable:
+        self.__check_parent("h1", {"body", "div"})
         return BlockElementCallable(self, h1)
 
     @property
     def h2(self) -> BlockElementCallable:
+        self.__check_parent("h2", {"body", "div"})
         return BlockElementCallable(self, h2)
 
     @property
     def h3(self) -> BlockElementCallable:
+        self.__check_parent("h3", {"body", "div"})
         return BlockElementCallable(self, h3)
 
     @property
     def li(self) -> BlockElementCallable:
+        self.__check_parent("li", {"ul", "ol"})
         return BlockElementCallable(self, li)
 
     @property
     def p(self) -> BlockElementCallable:
+        self.__check_parent("p", {"body", "div"})
         return BlockElementCallable(self, p)
 
     @property
     def pre(self) -> BlockElementCallable:
+        self.__check_parent("pre", {"body", "div"})
         return BlockElementCallable(self, pre)
 
     @property
@@ -181,19 +217,31 @@ class Block:
     def strong(self) -> BlockElementCallable:
         return BlockElementCallable(self, strong)
 
+    @property
+    def style(self) -> BlockElementCallable:
+        self.__check_parent("style", {"html", "head"})
+        return BlockElementCallable(self, style)
+
     @property
     def summary(self) -> BlockElementCallable:
         return BlockElementCallable(self, summary)
 
     @property
     def table(self) -> BlockElementCallable:
+        self.__check_parent("table", {"body", "div"})
         return BlockElementCallable(self, table)
 
     def text(self, text: str) -> None:
         self.elements.append(text)
 
+    @property
+    def title(self) -> BlockElementCallable:
+        self.__check_parent("title", {"head", "html"})
+        return BlockElementCallable(self, title)
+
     @property
     def ul(self) -> BlockElementCallable:
+        self.__check_parent("ul", {"body", "div"})
         return BlockElementCallable(self, ul)
 
 
diff --git a/atr/mail.py b/atr/mail.py
index fd61d62..17fafe4 100644
--- a/atr/mail.py
+++ b/atr/mail.py
@@ -31,11 +31,7 @@ import atr.log as log
 # We could e.g. use uppercase instead of global_
 # It's not always worth identifying globals as globals
 # But in many cases we should do so
-# TODO: Get at least global_dkim_domain from configuration
-# And probably global_dkim_selector too
-# global_dkim_selector: str = "mail"
-global_dkim_domain: str = "apache.org"
-# global_secret_key: str | None = None
+global_domain: str = "apache.org"
 
 _MAIL_RELAY: Final[str] = "mail-relay.apache.org"
 _SMTP_PORT: Final[int] = 587
@@ -55,14 +51,14 @@ async def send(message: Message) -> tuple[str, list[str]]:
     """Send an email notification about an artifact or a vote."""
     log.info(f"Sending email for event: {message}")
     from_addr = message.email_sender
-    if not from_addr.endswith(f"@{global_dkim_domain}"):
-        raise ValueError(f"from_addr must end with @{global_dkim_domain}, got 
{from_addr}")
+    if not from_addr.endswith(f"@{global_domain}"):
+        raise ValueError(f"from_addr must end with @{global_domain}, got 
{from_addr}")
     to_addr = message.email_recipient
     _validate_recipient(to_addr)
 
     # UUID4 is entirely random, with no timestamp nor namespace
     # It does have 6 version and variant bits, so only 122 bits are random
-    mid = f"{uuid.uuid4()}@{global_dkim_domain}"
+    mid = f"{uuid.uuid4()}@{global_domain}"
     headers = [
         f"From: {from_addr}",
         f"To: {to_addr}",
@@ -100,49 +96,10 @@ async def send(message: Message) -> tuple[str, list[str]]:
     return mid, errors
 
 
-# def set_secret_key(key: str) -> None:
-#     """Set the secret key for DKIM signing."""
-#     global global_secret_key
-#     global_secret_key = key
-
-
-# async def set_secret_key_default() -> None:
-#     # TODO: Document this, or improve it
-#     project_root = 
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-#     dkim_path = os.path.join(project_root, "state", "dkim.private")
-
-#     async with aiofiles.open(dkim_path) as f:
-#         dkim_key = await f.read()
-#         set_secret_key(dkim_key.strip())
-#         log.info("DKIM key loaded and set successfully")
-
-
 async def _send_many(from_addr: str, to_addrs: list[str], msg_text: str) -> 
list[str]:
     """Send an email to multiple recipients."""
     message_bytes = bytes(msg_text, "utf-8")
 
-    # if global_secret_key is None:
-    #     # This is a severe configuration error
-    #     # It does not count as a send error to only warn about
-    #     raise ValueError("global_secret_key is not set")
-
-    # # DKIM sign the message
-    # private_key = bytes(global_secret_key, "utf-8")
-
-    # # Create a DKIM signature
-    # sig = dkim.sign(
-    #     message=message_bytes,
-    #     selector=bytes(global_dkim_selector, "utf-8"),
-    #     domain=bytes(global_dkim_domain, "utf-8"),
-    #     privkey=private_key,
-    #     include_headers=[b"From", b"To", b"Subject", b"Date", b"Message-ID"],
-    # )
-
-    # # Prepend the DKIM signature to the message
-    # dkim_msg = sig + message_bytes
-
-    # log.info(f"email_send_many: {dkim_msg}")
-
     errors = []
     for addr in to_addrs:
         try:
diff --git a/atr/post/__init__.py b/atr/post/__init__.py
index 768113b..ca6989d 100644
--- a/atr/post/__init__.py
+++ b/atr/post/__init__.py
@@ -22,8 +22,6 @@ import atr.post.candidate as candidate
 import atr.post.distribution as distribution
 import atr.post.vote as vote
 
-from .example_test import respond as example_test
-
 ROUTES_MODULE: Final[Literal[True]] = True
 
-__all__ = ["announce", "candidate", "distribution", "example_test", "vote"]
+__all__ = ["announce", "candidate", "distribution", "vote"]
diff --git a/atr/post/example_test.py b/atr/post/example_test.py
deleted file mode 100644
index fce4b45..0000000
--- a/atr/post/example_test.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-import quart
-
-import atr.blueprints.post as post
-import atr.get as get
-import atr.util as util
-import atr.web as web
-
-
[email protected]("/example/test")
-async def respond(session: web.Committer) -> quart.Response:
-    await util.validate_empty_form()
-    await quart.flash("POST request successful!", "success")
-
-    return quart.Response(
-        f"""\
-<h1>Test route (POST)</h1>
-<p>Hello, {session.asf_uid}!</p>
-<p>This POST route was successfully called!</p>
-<p><a href="{util.as_url(get.example_test)}">Go back to the GET route</a></p>
-""",
-        mimetype="text/html",
-    )
diff --git a/atr/routes/download.py b/atr/routes/download.py
index 974fc40..8a8614d 100644
--- a/atr/routes/download.py
+++ b/atr/routes/download.py
@@ -24,11 +24,13 @@ import aiofiles
 import aiofiles.os
 import asfquart.base as base
 import quart
+import werkzeug.http
 import werkzeug.wrappers.response as response
 import zipstream
 
 import atr.config as config
 import atr.db as db
+import atr.htm as htm
 import atr.models.sql as sql
 import atr.route as route
 import atr.routes.mapping as mapping
@@ -141,9 +143,10 @@ async def zip_selected(
             yield chunk
 
     headers = {
-        "Content-Disposition": f'attachment; filename="{release.name}.zip"',
+        "Content-Disposition": f"attachment; 
filename={werkzeug.http.quote_header_value(release.name + '.zip')}",
         "Content-Type": "application/zip",
     }
+    # TODO: Write a type safe wrapper for quart.Response that ensures headers 
are encoded correctly
     return quart.Response(stream_zip(files_to_zip), headers=headers, 
mimetype="application/zip")
 
 
@@ -210,7 +213,9 @@ async def _list(
         if is_file or is_dir:
             files.append(file_in_dir)
     files.sort()
-    html = []
+    html = htm.Block(htm.html)
+    html.style["body { margin: 1rem; font: 1.25rem/1.5 serif; }"]
+    div = htm.Block()
 
     # Add link to parent directory if not at root
     if file_path != ".":
@@ -221,7 +226,7 @@ async def _list(
             version_name=version_name,
             file_path=parent_path_str,
         )
-        html.append(f'<a href="{parent_link_url}">../</a>')
+        div.a(href=parent_link_url)["../"]
 
     # List files and directories
     for item_in_dir in files:
@@ -233,7 +238,7 @@ async def _list(
             file_path=relative_path_str,
         )
         display_name = f"{item_in_dir}/" if await 
aiofiles.os.path.isdir(full_path / item_in_dir) else str(item_in_dir)
-        html.append(f'<a href="{link_url}">{display_name}</a>')
-    head = "<style>body { margin: 1rem; font: 1.25rem/1.5 serif; }</style>"
-    response_body = head + "<br>\n".join(html)
+        div.a(href=link_url)[display_name]
+    html.body[div.collect(separator=htm.br)]
+    response_body = html.collect()
     return quart.Response(response_body, mimetype="text/html")
diff --git a/atr/routes/finish.py b/atr/routes/finish.py
index 2388044..3f60489 100644
--- a/atr/routes/finish.py
+++ b/atr/routes/finish.py
@@ -218,8 +218,7 @@ async def _analyse_rc_tags(latest_revision_dir: 
pathlib.Path) -> RCTagAnalysisRe
         if len(r.affected_paths_preview) >= 5:
             # Can't break here, because we need to update the counts
             continue
-        highlighted_preview = analysis.candidate_highlight(p_rel)
-        r.affected_paths_preview.append((highlighted_preview, 
stripped_path_str))
+        r.affected_paths_preview.append((original_path_str, stripped_path_str))
 
     return r
 
diff --git a/atr/routes/published.py b/atr/routes/published.py
index 175ddd7..daed201 100644
--- a/atr/routes/published.py
+++ b/atr/routes/published.py
@@ -22,6 +22,7 @@ from datetime import datetime
 import aiofiles.os
 import quart
 
+import atr.htm as htm
 import atr.route as route
 import atr.util as util
 
@@ -41,21 +42,25 @@ async def root(session: route.CommitterSession) -> 
quart.Response:
 
 
 async def _directory_listing(full_path: pathlib.Path, current_path: str) -> 
quart.Response:
-    html_parts = [
-        "<!doctype html>",
-        f"<title>Index of /{current_path}</title>",
-        "<style>body { margin: 1rem; }</style>",
-        f"<h1>Index of /{current_path}</h1>",
-        "<pre>",
-    ]
+    html = htm.Block(htm.html)
+    html.title[f"Index of /{current_path}"]
+    html.style["body { margin: 1rem; }"]
+    with html.block(htm.body) as body:
+        htm.h1[f"Index of /{current_path}"]
+        with body.block(htm.pre) as pre:
+            await _directory_listing_pre(full_path, current_path, pre)
+    return quart.Response(html.collect(), mimetype="text/html")
 
+
+async def _directory_listing_pre(full_path: pathlib.Path, current_path: str, 
pre: htm.Block) -> None:
     if current_path:
         parent_path = pathlib.Path(current_path).parent
         parent_url_path = str(parent_path) if str(parent_path) != "." else ""
         if parent_url_path:
-            html_parts.append(f'<a href="{util.as_url(path, 
path=parent_url_path)}">../</a>')
+            pre.a(href=util.as_url(path, path=parent_url_path))["../"]
         else:
-            html_parts.append(f'<a href="{util.as_url(root)}">../</a>')
+            pre.a(href=util.as_url(root))["../"]
+        pre.text("\n\n")
 
     entries = []
     dir_contents = await aiofiles.os.listdir(full_path)
@@ -80,11 +85,9 @@ async def _directory_listing(full_path: pathlib.Path, 
current_path: str) -> quar
             mtime = 
datetime.fromtimestamp(stat_info.st_mtime).strftime("%Y-%m-%d %H:%M")
             entry_path = str(pathlib.Path(current_path) / entry["name"])
             display_name = f"{entry['name']}/" if is_dir else entry["name"]
-            link = f'<a href="{util.as_url(path, 
path=entry_path)}">{display_name}</a>'
-            html_parts.append(f"{mode} {nlink} {size} {mtime}  {link}")
-
-    html_parts.append("</pre>")
-    return quart.Response("\n".join(html_parts), mimetype="text/html")
+            pre.text(f"{mode} {nlink} {size} {mtime}  ")
+            pre.a(href=util.as_url(path, path=entry_path))[display_name]
+            pre.text("\n")
 
 
 async def _file_content(full_path: pathlib.Path) -> quart.Response:


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to