Skip to content

Emit heading prefixes, skip decorative HTML and demo previews in markdown stream - #6054

Merged
falkoschindler merged 4 commits into
zauberzeug:mainfrom
evnchn:mdp2-site-polish
Jun 5, 2026
Merged

Emit heading prefixes, skip decorative HTML and demo previews in markdown stream#6054
falkoschindler merged 4 commits into
zauberzeug:mainfrom
evnchn:mdp2-site-polish

Conversation

@evnchn

@evnchn evnchn commented May 13, 2026

Copy link
Copy Markdown
Collaborator

Drafted by @evnchn with Claude Code (Opus 4.7); diff reviewed before pushing. Implements the structural pattern (override_markdown helper) Falko proposed in the #6007 triage.

Motivation

Phase 2 polish for #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 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):

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 #6052

This PR is the companion referenced in #6052's body. #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

  • 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 #5889 is unchanged).
  • No breaking changes to the public API — only behavior of Accept: text/markdown responses on the docs site is affected.

…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>
Bound the TypeVar to ui.element directly (dropping the nicegui.element
import), move it next to its only use, trim docstrings, and chain
.classes()/.style() inside the override_markdown() calls.
The docs left-drawer lazy-loads its navigation tree behind an
intersection observer; the `/static/loading.gif` placeholder is visible
server-side, so it leaked `![](/static/loading.gif)` before the content
on every docs page. Wrap it with override_markdown(..., '').

@falkoschindler falkoschindler left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The override_markdown consolidation is the right shape — one shared helper instead of five private subclasses + a monkey-patch, with both the type: ignore and pylint: disable confined to it.

Verified the markdown stream live (Accept: text/markdown on /documentation/button and /documentation/section_text_elements):

  • Heading hierarchy ✓ — H1 (# ui.*button*) from section_heading, H2s from subheading in both branches (## Icons, ## [Label](/documentation/label)).
  • Demo previews suppressed ✓ — no localhost:8080 chrome, no demo spinner; the demo code block still survives via python_window.
  • Decorative HTML gone ✓ — no raw <i class="ph-…">, anchor <div>s, or empty [](#anchor) icon-links in the content.

A couple of small things I folded in on top while verifying:

  • Helper tidy-up — bound the TypeVar to ui.element directly (drops the nicegui.element import) and chained .classes()/.style() inside the override_markdown(...) calls so the with lines read as "build element, then wrap".
  • One more placeholder leak — the docs left-drawer lazy-loads its nav tree behind an intersection observer, and its /static/loading.gif placeholder was leaking ![](…) before the content on every docs page. Same family as the browser_window case, so wrapped it with override_markdown(..., '') in main.py. Now 0 occurrences in the stream.

The remaining header/nav and copy-button noise is genuinely separate scope (copy buttons being what #6052 cleans up). 👍

@falkoschindler
falkoschindler enabled auto-merge June 5, 2026 18:07
@falkoschindler
falkoschindler added this pull request to the merge queue Jun 5, 2026
Merged via the queue into zauberzeug:main with commit 1807391 Jun 5, 2026
7 checks passed
falkoschindler added a commit to evnchn/nicegui that referenced this pull request Jun 8, 2026
Re-add the `.strip()` dropped in b62aef4 so a whitespace-only child
falls back to `[Button]` instead of `[Button:  ]`, and document why the
guard only surfaces a single plain line of child content.

Add a test pinning that a decorative child whose markdown is suppressed
(mirroring the website's `override_markdown(..., '')` from zauberzeug#6054) leaves
the button rendering as `[Button]`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pull Bot pushed a commit to codingwatching/nicegui that referenced this pull request Jun 8, 2026
…g#6052)

### Motivation

Phase 2 polish for zauberzeug#5889 (markdown content negotiation).
`Button._render_markdown` only inspected the `label` and `icon` props,
so a button that carried its meaning through an `aria-label` or a child
element — e.g. `ui.button().props('aria-label="Save"')`, or a button
wrapping a `ui.label` — collapsed to a bare `[Button]`, dropping
information a reading agent could have used.

This PR surfaces that information, while **keeping `[Button]` as the
representation for genuinely empty / decorative buttons** (such as the
copy buttons whose only child is a phosphor icon). Dropping the
`[Button]` token itself for decorative buttons was considered (per
[zauberzeug#6007 finding
4](zauberzeug#6007)) but not done
here: the placeholder is a useful structural signal in the stream, and
zauberzeug#6054 already stops the decorative child markup from leaking.

### Implementation

`nicegui/elements/button.py:_render_markdown()` now resolves, in order:

1. `label` prop → `[Button: <label>]` (existing)
2. `aria-label` prop → `[Button: <aria-label>]` (new)
3. `icon` prop → `[Button: icon:<icon>]` (existing)
4. A single plain line of child markdown (no newlines, no `[`/`]`) →
`[Button: <child>]` (new)
5. Otherwise → `[Button]` (unchanged)

The child fallback is deliberately conservative: multi-child, bracketed
(e.g. nested links), or empty/decorative children fall through to
`[Button]` rather than garbling the `[Button: ...]` wrapper. Children
whose markdown is suppressed via `override_markdown(..., '')` (zauberzeug#6054)
render empty and are skipped, so a copy button stays `[Button]` without
exposing raw `<i class="ph-...">` HTML.

Tests in `tests/test_markdown_response.py` cover the `aria-label`
priority (below `label`, above `icon`), single-line child content, and
children that fall back to `[Button]` (a nested-link child, a
multi-child button, and a whitespace-only child).

### 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/updated.
- [x] Documentation is not necessary (agent-facing rendering change).
- [x] No breaking changes to the public API — empty `ui.button()` still
renders `[Button]`; only buttons carrying an `aria-label` or a
single-line child gain a label. No Python API signature changes.

---------

Co-authored-by: evnchn <evnchn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Falko Schindler <falko@zauberzeug.com>
@evnchn
evnchn deleted the mdp2-site-polish branch June 17, 2026 03:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Type/scope: Documentation, examples and website review Status: PR is open and needs review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants