Markdown Phase 2: emit heading prefixes, skip decorative HTML and demo previews - #150
Closed
evnchn wants to merge 13 commits into
Closed
Markdown Phase 2: emit heading prefixes, skip decorative HTML and demo previews#150evnchn wants to merge 13 commits into
evnchn wants to merge 13 commits into
Conversation
…ue)` (zauberzeug#6045) ### Motivation Fixes zauberzeug#1841. `ui.run(native=True, reload=True)` raises `RuntimeError: A SemLock created in a fork context is being shared with a process in a spawn context.` on CPython ≥ 3.11.5. NiceGUI's native-mode `Queue`/`Pipe`/`Event` use the default (fork) context, but uvicorn's `ChangeReload` worker is spawn-context, so when ChangeReload pickles parent state for the worker the fork-context SemLocks fail the cross-context check that landed in [cpython#77377](python/cpython#77377). Older CPython silently corrupted state instead of raising, which is the long tail of "leaked semaphore" / "semaphore released too many times" reports in the same issue. Empirical evidence — distro × backend × Python sweep, exact 3.11.4↔3.11.5 boundary, upstream commit, all in [zauberzeug#1841 (comment)](zauberzeug#1841 (comment)). ### Implementation Switch the three affected primitives to an explicit `multiprocessing.get_context('spawn')`. This is the pattern Python's docs recommend for libraries ([Contexts and start methods](https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods): *"Libraries using multiprocessing should be designed to allow their users to provide their own multiprocessing context"*) and what CPython's own SemLock error message tells you to do (*"Please use the same context to create multiprocessing objects and Process"*). Pywebview already uses the same pattern in [`examples/pystray_icon.py`](https://github.com/r0x0r/pywebview/blob/master/examples/pystray_icon.py). The alternative considered — `multiprocessing.set_start_method('spawn', force=True)` — was rejected because it mutates the user's global default and would break user code that relies on fork (CoW model loading, post-fork file descriptors, ML pipelines). The patch here keeps the user's default untouched and only spawn-types NiceGUI's three IPC primitives. Verified on `python:3.12-slim` Docker against the patched repo via `PYTHONPATH` (no monkeypatch): unpatched fires `SEMLOCK_FIRED`, patched runs to event-loop block. After-import `multiprocessing.get_start_method(allow_none=True)` is unchanged; `mp.get_context('fork').Queue()` in user code still works. ### Progress - [x] The PR title is a short phrase starting with a verb. - [x] The implementation is complete. - [x] This PR does not address a security issue. - [x] Pytests are not necessary. - [x] Documentation has been added/updated or is not necessary. - [x] No breaking changes to the public API or migration steps are described above. --- *Drafted by Claude Code working with @evnchn — empirical investigation across distros / CPython patch versions, then surgical fix matching pywebview's existing pattern.* --------- Co-authored-by: Falko Schindler <falko@zauberzeug.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
### Motivation Mermaid 11.14.0 and earlier are affected by four medium-severity advisories, all reported by @zsxsoft on behalf of @KeenSecurityLab: - [GHSA-ghcm-xqfw-q4vr](GHSA-ghcm-xqfw-q4vr) / CVE-2026-41149 — HTML injection via `classDef` in state diagrams - [GHSA-xcj9-5m2h-648r](GHSA-xcj9-5m2h-648r) / CVE-2026-41148 — CSS injection via `classDefs` - [GHSA-6m6c-36f7-fhxh](GHSA-6m6c-36f7-fhxh) / CVE-2026-41150 — Gantt chart infinite-loop DoS - [GHSA-87f9-hvmw-gh4p](GHSA-87f9-hvmw-gh4p) / CVE-2026-41159 — CSS injection via `fontFamily`, `themeCSS`, `altFontFamily` All four are fixed upstream in Mermaid 11.15.0, raised as Dependabot alerts 260, 261, 262 and 263. ### Implementation Bump `mermaid` from 11.12.2 to 11.15.0 in `nicegui/elements/mermaid/`, rebuild the bundle, and update `DEPENDENCIES.md` to reflect the new minimum version. ### Progress - [x] The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc. - [x] The implementation is complete. - [x] This PR does not address a security issue. (The four CVEs are in the upstream Mermaid library and are already publicly disclosed; this PR only consumes the upstream patch, analogous to zauberzeug#5755.) - [x] Pytests are not necessary. - [x] Documentation has been updated (`DEPENDENCIES.md`). - [x] No breaking changes to the public API.
…erzeug#6021) ## Motivation AI assistants (Claude, Copilot, Cursor, etc.) working on NiceGUI projects repeatedly make the same mistakes: reaching for raw CSS, Quasar props, or JavaScript when a clean Python API exists; using `asyncio.create_task()` instead of `background_tasks.create()`; treating module-level variables as per-user state; or rebuilding entire UI trees instead of updating elements in place. These mistakes happen not because the API docs are lacking, but because LLMs have no compact, opinionated reference that explains both *what* to use and — more importantly — *why*. Without understanding the reasoning behind NiceGUI's design decisions, an LLM will keep producing plausible-looking but non-idiomatic code. This file was developed iteratively: first as a pure API surface reference, then extended with a "Mental Models" section after a real-world test. It was used as context for an AI-assisted code review of [NiceTransfer](https://github.com/joko-zauberzeug/nicetransfer), a NiceGUI-based file transfer application. The review identified and corrected several anti-patterns — direct JS clipboard access instead of `ui.clipboard.write()`, `asyncio.ensure_future()` usage, raw HTML download links instead of `ui.button().props('tag=a')` — and the file was refined based on exactly those findings. ## Implementation A single `llms.md` at the repository root. It is: - **Self-contained** — no external references needed; can be dropped into any NiceGUI project and referenced from `CLAUDE.md` or similar AI context files - **Python-first** — every section and every anti-pattern example reinforces that NiceGUI is a Python abstraction, not a thin wrapper over Quasar/CSS/JS - **"Why"-oriented** — 10 mental models explain the design decisions that matter most: no virtual DOM, shared event loop, pull-based bindings, slot stack mechanics, per-user vs. global state, the Outbox batching model, and more - **Actionable** — do/don't comparison tables, anti-pattern blocks with correct alternatives, and concrete code examples throughout - **LLM-agnostic** — works as context for Claude Code, Copilot, Cursor, or any other AI assistant **Two intended use cases:** 1. **Contributors working on NiceGUI itself** — Falko, if useful: adding `@llms.md` to the repo's `CLAUDE.md` would make Claude Code load it automatically for every session. Happy to add that in a follow-up or here if you prefer. 2. **Developers building NiceGUI projects** — they can reference the file directly from their own project's AI context file: ``` @https://raw.githubusercontent.com/zauberzeug/nicegui/main/llms.md ``` This is already working well in practice for NiceTransfer. ## Progress - [x] Full API surface documentation (all `ui.*` elements, styling system, binding, events, storage, async patterns, pages & routing) - [x] Mental Models section explaining the design rationale behind NiceGUI's key decisions - [x] Anti-patterns with real-world examples sourced from NiceTransfer code review - [x] Iteratively refined based on actual AI-assisted code review results - [x] Tested: integration as AI context in a NiceGUI project led to measurably more idiomatic code and caught patterns that would otherwise have required manual review --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Falko Schindler <falko@zauberzeug.com>
… index (zauberzeug#6049) _Drafted by @evnchn with Claude Code (Opus 4.7); diff and rendered copy reviewed before pushing._ ### Motivation zauberzeug#6021 landed `nicegui/llms.md`, the `/llms.txt` route, and a footer link, but the homepage CTA still only mentions the older [machine-readable JSON `documentation_index`](/documentation/section_configuration_deployment#documentation_index). The two artifacts serve different jobs: - **`llms.txt`** — one condensed Markdown file you paste into a prompt - **`documentation_index`** — full-content JSON for RAG / agent tooling A visitor on the homepage who wants to "let the AI do it" should see both on the discovery surface. ### Implementation One-line copy change to the existing `ui.markdown` block in `website/components/cta_section.py` — no styling, structure, or new files. The new copy frames the two artifacts as complementary (paste-in vs. RAG) and links each to its use case: ```markdown **Or, let your AI do it!** Most LLMs already know NiceGUI. For the rest, paste our [LLM reference](/llms.txt) — a single Markdown drop-in — or point a RAG pipeline at the [documentation index](/documentation/section_configuration_deployment#documentation_index) for the full API as JSON. ``` Link label "LLM reference" matches the existing footer link added in zauberzeug#6021. ### Progress - [x] The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc. - [x] The implementation is complete. - [x] This PR does not address a security issue. - [x] Pytests are not necessary. (Copy-only change to a single `ui.markdown` string; no logic or routes touched.) - [x] Documentation has been added/updated. - [x] No breaking changes to the public API or migration steps are described above. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Falko Schindler <falko@zauberzeug.com>
### Motivation
The current authentication example has a few small problems:
- favicons are showing inconsistently
- png favicons are shown only when logged in, emoji favicons always work
- the username/password page has unexpected (/no) focusing logic
- the widgets are not aligned
### Implementation
- png favicons are accessed via `/favicon.ico` (emojis are inlined)
- => added `"/favicon.ico"` to `unrestricted_page_routes`
- enter on both username and password tried to log in
- => implemented the more usual input focus logic:
- auto-focus username
- enter on username --> focus password
- enter on password --> try to log in
- added `items-stretch` to `ui.card()`
- also, simplified `AuthMiddleware.dispatch` by inverting the logic as
it was a bit hard to understand
In particular the favicon problem is not easy to understand when it
happens. So, having it in the example is probably a good idea. I
imagine, the same problem might trigger elsewhere as well, e.g., for
`robots.txt`. This might warrant a comment somewhere? (where?)
If the `items-stretch` is desired, I can add a new screenshot...
### Progress
- [x] The PR title is a short phrase starting with a verb like "Add
...", "Fix ...", "Update ...", "Remove ...", etc.
- [x] The implementation is complete.
- [x] This PR does not address a security issue.
- [x] Pytests are not necessary.
- [x] Documentation is not necessary.
- [x] No breaking changes.
---------
Co-authored-by: Falko Schindler <falko@zauberzeug.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
publish_parts() was called without disabling file insertion, so attacker-controlled reStructuredText could read local files via .. include::, .. csv-table:: :file:, or .. raw:: :file:. Set file_insertion_enabled=False, raw_enabled=False, and _disable_config=True to block these directives by default. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Reject directory paths on ESM and per-component resource routes (GHSA-pq7c-x8g4-rvp6) Path.exists() returns True for directories, so a trailing slash on /_nicegui/<v>/esm/<key>/ or /_nicegui/<v>/resources/<key>/ resolves filepath to the directory itself and lets FileResponse raise an unhandled RuntimeError, flooding the server log on every request. Use is_file() to reject directories and fall through to a clean 404. * Use is_file() on library and component routes for consistency These routes are not exploitable via the GHSA-pq7c-x8g4-rvp6 vector (the URL parameter only selects a registered entry; the filesystem path comes from server-side registration data, not the request). But is_file() more correctly expresses "serve a regular file, not a directory", so apply it consistently across all four FileResponse guards in this module.
### Motivation
Two `pytest` invocations cannot run simultaneously today: `Screen.PORT`
is hardcoded to `3392`, `Screen.SCREENSHOT_DIR` is a fixed
`'screenshots'`, and NiceGUI's file-based storage resolves to a fixed
`.nicegui/` dir. Running a second process collides on port bind, on
screenshot filenames, and on persistent storage files. This PR makes
those session-unique so two (or more) pytest runs can share a working
directory without stomping each other — including mid-flight overlap
(start session B while session A is already running a long test).
### Implementation
- **`nicegui/helpers/network.py`**
- New public helper `find_free_port()` binds an ephemeral `AF_INET /
SOCK_STREAM` to `('0.0.0.0', 0)`. It binds on `0.0.0.0` deliberately —
`ui.run` defaults to binding on all interfaces, so probing the same
namespace avoids a port-free-on-loopback-but-busy-on-another-interface
race. The docstring notes the inherent TOCTOU caveat (another process
may grab the port between this call and `ui.run` binding).
- **`nicegui/testing/filelock.py`** (new)
- Small cross-platform `FileLock` class: `fcntl.flock` on POSIX,
`msvcrt.locking` on Windows. Exclusive, non-blocking, advisory. The OS
releases the lock automatically on process death — clean exit *or*
SIGKILL/OOM. Used as a process-liveness probe (see screenshot pruning
below).
- **`nicegui/testing/screen_plugin.py`**
- `pytest_configure` sets `Screen.PORT = helpers.find_free_port()`,
`Screen.SCREENSHOT_DIR = Path('screenshots') / str(os.getpid())`, and
`DOWNLOAD_DIR =
Path(tempfile.mkdtemp(prefix='nicegui-test-download-'))`, with an
`atexit.register` as a safety net for aborted sessions.
- The `screen` fixture creates `DOWNLOAD_DIR` before `yield` and rmtrees
it in `finally` (setup/teardown symmetry — Chrome's session-scoped
`download.default_directory` always points at a directory that actually
exists).
- `nicegui_remove_all_screenshots` acquires an exclusive lock on
`screenshots/<pid>/.lock` for the session's lifetime, then prunes
sibling PID-dirs whose lock it can acquire (= owner is gone). This is
immune to PID reuse and handles abnormal exits. The previous
PID-aliveness check with its Windows `ctypes` block is gone.
- **`nicegui/testing/general_fixtures.py`**
- `pytest_configure` creates a session-unique storage dir via
`tempfile.mkdtemp(prefix='nicegui-test-storage-')`, assigns it to
`Storage.path`, and rebuilds `app.storage = Storage()` once so its
internal `FilePersistentDict` picks up the new path (the instance
constructed at `App.__init__` captured the default `.nicegui/`).
- `Storage.path` itself doubles as the sentinel — `None` means "not
configured." `pytest_unconfigure` removes the temp storage dir and
resets the sentinel.
- **`nicegui/testing/plugin.py` / `user_plugin.py`**: re-export the new
`pytest_unconfigure` hook; `plugin.py` exports `screen_plugin`'s
`pytest_configure` (which delegates to `general_fixtures`' before
applying the Screen-specific setup).
- **`tests/test_storage.py`**: replace `Path('.nicegui')` literals with
`Storage.path`, and the hardcoded `http://localhost:3392/status` with
`http://localhost:{Screen.PORT}/status`.
- **`website/documentation/content/screen_documentation.py`**: update
the `PORT` and `SCREENSHOT_DIR` default notes to "automatically
determined free port" and `./screenshots/<pid>`.
### Empirical validation
Two parallel pytest invocations from the same worktree, both using the
Screen fixture:
```
$ uv run pytest tests/test_button.py -v # 4/4 PASSED in 4.10s
$ uv run pytest tests/test_chip.py -v # 2/2 PASSED in 3.99s
```
Both picked different free ports, wrote to different `screenshots/<pid>`
directories, and got their own `tempfile.mkdtemp` download dirs. No
`address in use`, no errors.
### Notes on observable defaults
Three observable defaults change. None are hard breaks — the idiomatic
way to use each surface was always indirect — but worth calling out:
- **`Screen.PORT`** was `3392` and is now a random free port per
process. Tests that hardcoded `3392` (e.g. `http://localhost:3392/...`)
need to read `Screen.PORT`, which has always been the documented-public
way. This PR applies that migration to NiceGUI's own `test_storage.py`.
- **`Screen.SCREENSHOT_DIR`** was `'screenshots'` and is now
`'screenshots/<pid>'`. Same shape as the existing `.failed.png`
convention — a subpath under the screenshots root, transparent to anyone
who reads screenshots via `Screen.SCREENSHOT_DIR` rather than hardcoding
the literal.
- **`Storage.path`** (when the testing plugin is loaded) now points to a
session-unique tempdir instead of `.nicegui/`. Reading `.nicegui/`
directly in test code was never a supported pattern — the `Storage.path`
attribute is the contract. NiceGUI's own `test_storage.py` is migrated
to match.
### Progress
- [x] The PR title is a short phrase starting with a verb like "Add
...", "Fix ...", "Update ...", "Remove ...", etc.
- [x] The implementation is complete.
- [x] This PR does not address a security issue.
- [x] Pytests have been updated (affected `tests/test_storage.py`
exercises storage + port paths).
- [x] Documentation has been updated (`screen_documentation.py`).
- [x] No breaking changes to the public API — observable default values
move, but the documented accessors (`Screen.PORT`,
`Screen.SCREENSHOT_DIR`, `Storage.path`) are unchanged. Migration notes
above.
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Falko Schindler <falko@zauberzeug.com>
Co-authored-by: evnchn <evnchn@users.noreply.github.com>
…o previews Closes findings 1/2/3 from zauberzeug#6007 with a single shared helper. Introduce `website/design.py:override_markdown(element, markdown)` — a thin wrapper that replaces an element's `Accept: text/markdown` rendering with the given string and returns the element. Used here to: 1. Emit `#` / `##` heading prefixes for `section_heading` and `subheading` so docs pages expose their structure in the markdown stream (finding 1). 2. Zero out decorative chrome in `subheading` and `phosphor_icon` — `<div id=...>` anchor targets, `<i class="ph-...">` Phosphor icons, and empty `[](#...)` anchor links no longer leak into markdown (finding 3). 3. Skip the `browser_window` demo placeholder, whose lazy preview never hydrates server-side, so `localhost:8080` / `loading.gif` stop appearing in the markdown stream (finding 2). Per zauberzeug#6007 (Falko's reply), this collapses the previous five private subclasses (`_MarkdownH1`, `_MarkdownH2Link`, `_MarkdownH2Label`, `_DecorativeHtml`, `_DecorativeLink`) and an inline instance-level monkey-patch into one helper. Type-ignore and pylint-disable suppressions live inside the helper only. Adds `test_instance_level_render_markdown_override` to lock in the contract the helper depends on: `_render_markdown` can be replaced per instance without touching class state. Phase 2 of zauberzeug#5889; supersedes fork-only PRs #140, #141, #142. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 tasks
Owner
Author
|
Staging clone — now open upstream at zauberzeug#6054. Closing per the "never both" fork-hygiene rule. |
pull Bot
pushed a commit
to bhardwajRahul/nicegui
that referenced
this pull request
Jun 5, 2026
…down stream (zauberzeug#6054) _Drafted by @evnchn with Claude Code (Opus 4.7); diff reviewed before pushing. Implements the structural pattern (`override_markdown` helper) [Falko proposed in the zauberzeug#6007 triage](zauberzeug#6007 (reply in thread) ### Motivation Phase 2 polish for zauberzeug#5889 (markdown content negotiation, merged 2026-04-24). Closes findings 1–3 from [discussion zauberzeug#6007](zauberzeug#6007): 1. **Heading hierarchy missing on docs pages** — the H1 came through as literal `*Text* Elements`, section H2s as plain links. Agents reading the page had no structural cues. 2. **Demo panel placeholder leaks** — every `browser_window` rendered `localhost:8080` + `` because the lazy preview never hydrates server-side. 3. **Decorative HTML leaks** — Phosphor `<i class="ph-...">` icons, `<div id="...">` anchor targets, and empty `[](#anchor)` link icons appeared as raw HTML / empty links in the stream. Per the [approved triage](zauberzeug#6007 (reply in thread)), the original three triage PRs each introduced private subclasses + per-call-site monkey-patches. Falko's suggestion was a single shared helper. This PR is that consolidation. ### Implementation Introduces `website/design.py:override_markdown(element, markdown)`: ```python def override_markdown(element: _E, markdown: str) -> _E: element._render_markdown = lambda: markdown # type: ignore[method-assign] # pylint: disable=protected-access return element ``` `_E = TypeVar('_E', bound=Element)` preserves call-site element types so chained `.classes()` / `.style()` still type-check. Both suppressions live inside the helper only. Applied in three places: - **`section_heading()`** — wraps the title `ui.markdown` with `f'# {title_}'`. - **`subheading()`** — wraps the anchor `ui.html(<div id=...>)` and the icon-only `ui.link(target=#...)` with `''`; wraps the actual subheading `ui.link` / `ui.label` with `f'## [{text}]({link})'` or `f'## {text}'`. - **`phosphor_icon()`** — wraps its `ui.html(<i class="ph-...">)` with `''`. - **`browser_window()`** in `website/documentation/windows.py` — wraps the whole window with `''` (the chrome and lazy preview are irrelevant to a markdown reader). This collapses what would otherwise be five private subclasses (`_MarkdownH1`, `_MarkdownH2Link`, `_MarkdownH2Label`, `_DecorativeHtml`, `_DecorativeLink`) and one inline instance-patch into a single shared helper. #### Scope / location decision Per Falko's suggestion, the helper stays in `website/design.py` rather than being promoted to `nicegui.helpers` — it's a site-rendering concern, not a public API contract, until a second user-facing case appears. #### Test Adds `test_instance_level_render_markdown_override` to `tests/test_markdown_response.py`, locking in the underlying contract the helper depends on: `_render_markdown` can be replaced per instance without touching class state. ### Composes with zauberzeug#6052 This PR is the companion referenced in zauberzeug#6052's body. zauberzeug#6052 alone surfaces `[Button: <i class="ph-duotone ph-copy"></i>]` for copy buttons because `ui.html._render_markdown()` returns the raw content string and the button now recurses into children. With this PR applied, `phosphor_icon()` returns an element wrapped with `override_markdown(..., '')`, so the child renders to `''`, the button's `_children_to_markdown().strip()` is empty, and the button falls through to `''`. Copy-button noise collapses to zero. Either PR can land first; both are needed for the full effect. ### Local verification ``` $ uv run ruff check website/design.py website/documentation/windows.py tests/test_markdown_response.py All checks passed! $ uv run pylint website/design.py website/documentation/windows.py Your code has been rated at 10.00/10 $ uv run pytest tests/test_markdown_response.py -q 33 passed in 0.95s $ uv run pre-commit run --files website/design.py website/documentation/windows.py tests/test_markdown_response.py ruff check...............................................................Passed autopep8.................................................................Passed trim trailing whitespace.................................................Passed fix end of files.........................................................Passed fix double quoted strings................................................Passed codespell................................................................Passed ``` Same diff is also open at evnchn#150 for fork-side CI verification. ### Progress - [x] The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc. - [x] The implementation is complete. - [x] This PR does not address a security issue. - [x] Pytests have been added (`test_instance_level_render_markdown_override`). - [x] Documentation is not necessary (underlying agent-facing rendering change; the public `markdown=True` opt-in introduced in zauberzeug#5889 is unchanged). - [x] No breaking changes to the public API — only behavior of `Accept: text/markdown` responses on the docs site is affected. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Falko Schindler <falko@zauberzeug.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Drafted by @evnchn with Claude Code (Opus 4.7); diff reviewed before pushing. Approved structural pattern (
override_markdownhelper) from zauberzeug#6007 (comment); supersedes fork-only PRs #140, #141, #142.Motivation
Phase 2 polish for zauberzeug#5889 (markdown content negotiation, merged 2026-04-24). Closes findings 1–3 from discussion #6007:
*Text* Elements, section H2s as plain links. Agents reading the page had no structural cues.browser_windowrenderedlocalhost:8080+because the lazy preview never hydrates server-side.<i class="ph-...">icons,<div id="...">anchor targets, and empty[](#anchor)link icons appeared as raw HTML / empty links in the stream.Per the approved triage, the original three fork PRs each introduced private subclasses + per-call-site monkey-patches. Falko's suggestion was a single shared helper. This PR is that consolidation.
Implementation
Introduces
website/design.py:override_markdown(element, markdown):_E = TypeVar('_E', bound=Element)preserves call-site element types so chained.classes()/.style()still type-check. Both suppressions live inside the helper only.Applied in three places:
section_heading()— wraps the titleui.markdownwithf'# {title_}'.subheading()— wraps the anchorui.html(<div id=...>)and the icon-onlyui.link(target=#...)with''; wraps the actual subheadingui.link/ui.labelwithf'## [{text}]({link})'orf'## {text}'.phosphor_icon()— wraps itsui.html(<i class="ph-...">)with''.browser_window()inwebsite/documentation/windows.py— wraps the whole window with''(the chrome and lazy preview are irrelevant to a markdown reader).This collapses five private subclasses (
_MarkdownH1,_MarkdownH2Link,_MarkdownH2Label,_DecorativeHtml,_DecorativeLink) and one inline instance-patch from the original triage into a single shared helper.Scope / location decision
Per Falko's suggestion, the helper stays in
website/design.pyrather than being promoted tonicegui.helpers— it's a site-rendering concern, not a public API contract, until a second user-facing case appears.Test
Adds
test_instance_level_render_markdown_overridetotests/test_markdown_response.py, locking in the underlying contract the helper depends on:_render_markdowncan be replaced per instance without touching class state.E2E results
Lint + tests:
Smoke-testing
python main.pylocally was blocked by an unrelatedStorage.pathregression on currentmain(from nicegui.testing import Screeninwebsite/documentation/content/screen_documentation.pytriggers the sentinel innicegui/testing/general_fixtures.py:13that zauberzeug#5960 added). Filed separately if useful. CI runs in a fresh env so this should not affect verification here.Progress
test_instance_level_render_markdown_override).markdown=Trueopt-in introduced in AddAccept: text/markdowncontent negotiation for NiceGUI pages zauberzeug/nicegui#5889 is unchanged).Accept: text/markdownresponses on the docs site is affected.