Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/api-reference/chart-factories.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ from xy_docs.api_reference import chart_containers_api, chart_factories_api
chart_factories_api()
~~~

Annotation-only compositions use the neutral `chart()` container, so that
factory appears under Annotations. It also appears beside `facet_chart()` in
Facets and Layers because layered marks use the same neutral container.
Annotation-only and layered compositions use the neutral `chart()` container,
so that factory appears once under Annotations and Layers. `facet_chart()` has
its own Facets group.

## Shared Chart Props
## Chart Container Props

~~~python eval
chart_containers_api()
Expand Down
61 changes: 60 additions & 1 deletion docs/app/scripts/check_html_routes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Validate human-facing documentation routes."""

import re
from collections import Counter
from html.parser import HTMLParser
from pathlib import Path

from reflex_site_shared.docs import discover_docs
Expand All @@ -18,6 +20,52 @@
LLMS_DIRECTIVE = "For AI agents: the complete XY documentation index is at"


class _ElementIdParser(HTMLParser):
"""Collect element IDs from prerendered HTML, including inline SVG."""

def __init__(self) -> None:
super().__init__()
self.ids: list[str] = []

def _collect_ids(self, attrs: list[tuple[str, str | None]]) -> None:
"""Record non-empty IDs from one start tag."""
self.ids.extend(value for name, value in attrs if name == "id" and value)

def handle_starttag(
self,
_tag: str,
attrs: list[tuple[str, str | None]],
) -> None:
"""Collect IDs from a normal start tag."""
self._collect_ids(attrs)

def handle_startendtag(
self,
_tag: str,
attrs: list[tuple[str, str | None]],
) -> None:
"""Collect IDs from a self-closing start tag."""
self._collect_ids(attrs)


def duplicate_html_ids(source: str) -> tuple[str, ...]:
"""Return duplicate element IDs from a complete prerendered document."""
parser = _ElementIdParser()
parser.feed(source)
parser.close()
Comment on lines +50 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Duplicate-ID gate lacks focused tests

The new parser and build-failing validation path have no focused positive and negative tests, so changes to empty-ID handling, inline SVG parsing, or duplicate reporting can either reject valid documentation output or allow ambiguous anchors without a targeted failure identifying the regression.

Knowledge Base Used: Testing and Benchmarks

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

return tuple(element_id for element_id, count in Counter(parser.ids).items() if count > 1)


def validate_unique_html_ids(page_route: str, html_path: Path, source: str) -> None:
"""Reject repeated IDs anywhere in a complete prerendered route."""
duplicates = duplicate_html_ids(source)
if duplicates:
msg = (
f"Prerendered route has duplicate element IDs {duplicates}: {page_route} ({html_path})"
)
raise RuntimeError(msg)


def route_html_paths(route: str) -> tuple[Path, ...]:
"""Return the canonical trailing-slash HTML path for a route.

Expand Down Expand Up @@ -115,7 +163,11 @@ def main() -> None:
msg = f"Missing prerendered documentation route: {html_paths!r}"
raise RuntimeError(msg)
for html_path in html_paths:
if html_path.is_file() and LLMS_DIRECTIVE not in html_path.read_text(encoding="utf-8"):
if not html_path.is_file():
continue
source = html_path.read_text(encoding="utf-8")
validate_unique_html_ids(page.route, html_path, source)
if LLMS_DIRECTIVE not in source:
msg = f"Prerendered route omits the llms.txt directive: {html_path}"
raise RuntimeError(msg)

Expand All @@ -128,6 +180,13 @@ def main() -> None:
if not any(path.is_file() for path in html_paths):
msg = f"Missing prerendered redirect route: {html_paths!r}"
raise RuntimeError(msg)
for html_path in html_paths:
if html_path.is_file():
validate_unique_html_ids(
route,
html_path,
html_path.read_text(encoding="utf-8"),
)


if __name__ == "__main__":
Expand Down
4 changes: 2 additions & 2 deletions docs/app/xy_docs/api_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@
xy.triangle_mesh_chart,
),
),
("Annotations", (xy.chart,)),
("Facets and Layers", (xy.chart, xy.facet_chart)),
("Annotations and Layers", (xy.chart,)),
("Facets", (xy.facet_chart,)),
)
_CHART_FACTORY_COMPONENTS = frozenset(
factory for _group_name, factories in CHART_FACTORY_GROUPS for factory in factories
Expand Down
4 changes: 2 additions & 2 deletions docs/styling/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ built — one renderer never silently ignores what another draws.
| `wedge-gap` | xy | `bar`, `column`, `hist`, `histogram` | full | full | full | shipped |
| `marker-shape` | xy | `scatter` | full | full | full | shipped |

### Notes
### Mark style notes

- **`opacity`** — Multiplies the mark's own alpha in every renderer.
- **`fill`** — A plain color compiles to the mark's paint; a `linear-gradient(...)` compiles to a gradient and is accepted only by area and rect kinds, which are the ones with a gradient program.
Expand Down Expand Up @@ -86,7 +86,7 @@ token bag or in mark and axis `style=`, which every renderer reads.
| `axis_title` | full | partial | partial |
| `annotation_label` | full | none | none |

### Notes
### Chrome slot notes

- **`root`** (via `chart style=`) — `styles={'root': ...}` is browser-only, but the chart-level `style=` token bag targets the same element and every renderer reads it (`spec['dom']['style']`). Prefer it for anything that must survive export.
- **`title`** (via `styles={'title': ...}`) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (`fill`, or `color`). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.
Expand Down
10 changes: 5 additions & 5 deletions scripts/gen_capability_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def render() -> str:
"",
]
lines += caps.markdown_mark_property_table()
lines += ["", "### Notes", ""]
lines += ["", "### Mark style notes", ""]
for prop in caps.MARK_STYLE_PROPERTIES:
if prop.notes:
lines.append(f"- **`{prop.id}`** — {prop.notes}")
Expand All @@ -98,7 +98,7 @@ def render() -> str:
"",
]
lines += caps.markdown_slot_table()
lines += ["", "### Notes", ""]
lines += ["", "### Chrome slot notes", ""]
for slot in caps.CHART_SLOTS:
if slot.notes:
lines.append(f"- **`{slot.id}`** (via `{slot.channel}`) — {slot.notes}")
Expand All @@ -110,7 +110,7 @@ def render() -> str:
"",
]
lines += caps.markdown_extension_table()
lines += ["", "### Notes", ""]
lines += ["", "### Extension point notes", ""]
for point in caps.EXTENSION_POINTS:
lines.append(f"- **{point.id}** — {point.notes}")
lines += [
Expand Down Expand Up @@ -179,7 +179,7 @@ def render_public() -> str:
"",
]
lines += caps.markdown_mark_property_table()
lines += ["", "### Notes", ""]
lines += ["", "### Mark style notes", ""]
for prop in caps.MARK_STYLE_PROPERTIES:
if prop.notes:
lines.append(f"- **`{prop.id}`** — {prop.notes}")
Expand All @@ -194,7 +194,7 @@ def render_public() -> str:
"",
]
lines += caps.markdown_slot_table()
lines += ["", "### Notes", ""]
lines += ["", "### Chrome slot notes", ""]
for slot in caps.CHART_SLOTS:
if slot.notes:
lines.append(f"- **`{slot.id}`** (via `{slot.channel}`) — {slot.notes}")
Expand Down
6 changes: 3 additions & 3 deletions spec/api/capability-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ one honors.
| `wedge-gap` | xy | `bar`, `column`, `hist`, `histogram` | full | full | full | shipped |
| `marker-shape` | xy | `scatter` | full | full | full | shipped |

### Notes
### Mark style notes

- **`opacity`** — Multiplies the mark's own alpha in every renderer.
- **`fill`** — A plain color compiles to the mark's paint; a `linear-gradient(...)` compiles to a gradient and is accepted only by area and rect kinds, which are the ones with a gradient program.
Expand Down Expand Up @@ -94,7 +94,7 @@ contracted in [export.md](export.md) §9 and pinned by
| `axis_title` | full | partial | partial |
| `annotation_label` | full | none | none |

### Notes
### Chrome slot notes

- **`root`** (via `chart style=`) — `styles={'root': ...}` is browser-only, but the chart-level `style=` token bag targets the same element and every renderer reads it (`spec['dom']['style']`). Prefer it for anything that must survive export.
- **`title`** (via `styles={'title': ...}`) — Vector (SVG, PDF) honors font-size, font-weight, font-style, font-family, letter-spacing, opacity and the text paint (`fill`, or `color`). The raster writer's glyph primitive takes a size and one RGBA paint and nothing else, so it honors font-size and the paint only — font-weight, font-style, font-family, letter-spacing and opacity are vector-only rather than silently approximated. Properties outside the subset stay browser-only.
Expand All @@ -117,7 +117,7 @@ Ways to add behavior the core does not ship, without forking it.
| mark_plugin_shader | planned | `—` | — |
| custom_renderer | planned | `—` | — |

### Notes
### Extension point notes

- **mark_plugin_composition** — A calc over declared columns plus a build that returns built-in marks. Its output is ordinary traces, so it reuses the built-in rendering, picking, and export paths rather than reimplementing them.
- **mark_plugin_shader** — §24's WGSL/GLSL snippet pair. Deferred: a plugin with its own shader reuses none of the built-in rendering, picking, or export paths and would have to reimplement them.
Expand Down
Loading