feat(booth): view .md (rendered) and .txt/.log in-booth without downloading

Loose .md/.txt/.log files rendered as forced-download links in the gallery
and downloaded (or showed raw) when opened. Now they open in a readable
in-booth page via the existing /b/<name>/view route:
  - .md  -> rendered HTML (Python-Markdown: fenced code, tables, sane lists),
           styled in an Australis .markdown-body with the viewer chrome;
  - .txt/.log -> preformatted <pre> text view.
The gallery links docs to the viewer (📄) instead of a download; the view
page keeps a ⬇ (?dl=1) for saving. Files over 2 MB hand back raw. New
markdown dep (optional-import: degrades .md to text view if absent).
booth_image_view -> booth_view_file (now handles image + doc + raw-fallback).
9 new tests, suite 44 passing; deployed + verified live on nh3-dev :8090.
This commit is contained in:
vh
2026-08-05 01:13:38 -07:00
parent b5ae9365ff
commit 315faac4b5
5 changed files with 157 additions and 20 deletions
+62 -4
View File
@@ -10,9 +10,11 @@ from booth.app import (
build_gallery,
classify,
create_app,
doc_kind,
generate_pickup_id,
human_dur,
is_expired,
render_doc,
safe_upload_name,
sweep_once,
wrap_verbatim_html,
@@ -353,12 +355,12 @@ def test_image_view_traversal_404(client):
assert c.get("/b/run1/view", params={"f": "../../etc/passwd"}).status_code == 404
def test_image_view_nonimage_redirects_to_raw(client):
def test_view_nonviewable_redirects_to_raw(client):
c, data = client
_touch(data / "run1" / "notes.txt")
r = c.get("/b/run1/view", params={"f": "notes.txt"}, follow_redirects=False)
_touch(data / "run1" / "data.bin") # not image/md/txt -> nothing to render, hand back raw
r = c.get("/b/run1/view", params={"f": "data.bin"}, follow_redirects=False)
assert r.status_code == 307
assert r.headers["location"] == "/b/run1/notes.txt"
assert r.headers["location"] == "/b/run1/data.bin"
# ---- verbatim-index.html wrapper --------------------------------------------
@@ -435,3 +437,59 @@ def test_verbatim_index_raw_file_route_unwrapped(client):
r = c.get("/b/brief/index.html")
assert r.status_code == 200
assert "booth-nav-home" not in r.text
# ---- .md / .txt in-booth doc viewer -----------------------------------------
def test_doc_kind():
assert doc_kind("notes.md") == "markdown"
assert doc_kind("a.markdown") == "markdown"
assert doc_kind("log.txt") == "text"
assert doc_kind("run.log") == "text"
assert doc_kind("shot.png") is None
assert doc_kind("data.bin") is None
def test_render_doc_markdown():
html, is_html = render_doc("# Title\n\n- a\n- b\n", "markdown")
assert is_html is True
assert "<h1>" in html and "Title" in html
assert "<li>" in html
def test_render_doc_text_is_verbatim():
body, is_html = render_doc("plain\ntext", "text")
assert is_html is False and body == "plain\ntext"
def test_view_markdown_renders(client):
c, data = client
d = data / "run1"; d.mkdir()
(d / "notes.md").write_text("# Heading\n\nsome **bold** text\n")
r = c.get("/b/run1/view", params={"f": "notes.md"})
assert r.status_code == 200
assert "<h1>" in r.text and "Heading" in r.text # rendered, not raw markdown
assert "<strong>bold</strong>" in r.text
assert "attachment" not in r.headers.get("content-disposition", "") # viewed, not downloaded
def test_view_text_shows_preformatted(client):
c, data = client
d = data / "run1"; d.mkdir()
(d / "out.txt").write_text("line one\nline two")
r = c.get("/b/run1/view", params={"f": "out.txt"})
assert r.status_code == 200
assert "<pre" in r.text and "line one" in r.text
assert "attachment" not in r.headers.get("content-disposition", "")
def test_gallery_links_docs_to_view(client):
c, data = client
d = data / "run1"; d.mkdir()
(d / "readme.md").write_text("# hi")
(d / "notes.txt").write_text("hello") # loose txt (no media partner) -> own item
page = c.get("/b/run1/")
assert "view?f=readme.md" in page.text # md -> viewer
assert "view?f=notes.txt" in page.text # txt -> viewer
assert 'href="readme.md" download' not in page.text # not a forced download