Skip to content

Markdown Phase 2: emit heading prefixes, skip decorative HTML and demo previews - #150

Closed
evnchn wants to merge 13 commits into
mainfrom
mdp2-site-polish
Closed

Markdown Phase 2: emit heading prefixes, skip decorative HTML and demo previews#150
evnchn wants to merge 13 commits into
mainfrom
mdp2-site-polish

Conversation

@evnchn

@evnchn evnchn commented May 13, 2026

Copy link
Copy Markdown
Owner

Drafted by @evnchn with Claude Code (Opus 4.7); diff reviewed before pushing. Approved structural pattern (override_markdown helper) 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:

  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 + ![](/static/loading.gif) 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, 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):

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 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.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.

E2E results

Lint + tests:

$ 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

Smoke-testing python main.py locally was blocked by an unrelated Storage.path regression on current main (from nicegui.testing import Screen in website/documentation/content/screen_documentation.py triggers the sentinel in nicegui/testing/general_fixtures.py:13 that zauberzeug#5960 added). Filed separately if useful. CI runs in a fresh env so this should not affect verification here.

Progress

  • The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc.
  • The implementation is complete.
  • This PR does not address a security issue.
  • Pytests have been added (test_instance_level_render_markdown_override).
  • Documentation is not necessary (underlying agent-facing rendering change; the public markdown=True opt-in introduced in Add Accept: text/markdown content negotiation for NiceGUI pages zauberzeug/nicegui#5889 is unchanged).
  • No breaking changes to the public API — only behavior of Accept: text/markdown responses on the docs site is affected.

falkoschindler and others added 13 commits May 10, 2026 21:07
…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>
@evnchn

evnchn commented May 13, 2026

Copy link
Copy Markdown
Owner Author

Staging clone — now open upstream at zauberzeug#6054. Closing per the "never both" fork-hygiene rule.

@evnchn evnchn closed this May 13, 2026
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` + `![](/static/loading.gif)` 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants