diff --git a/docs/app/reflex.lock/bun.lock b/docs/app/reflex.lock/bun.lock index 0606ca10649..c5c50b701f5 100644 --- a/docs/app/reflex.lock/bun.lock +++ b/docs/app/reflex.lock/bun.lock @@ -43,7 +43,6 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "19.2.6", - "react-dropzone": "15.0.0", "react-fast-marquee": "1.6.5", "react-helmet": "6.1.0", "react-leaflet": "5.0.0", @@ -864,8 +863,6 @@ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - "attr-accept": ["attr-accept@2.2.5", "", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="], - "autoprefixer": ["autoprefixer@10.5.0", "", { "dependencies": { "browserslist": "^4.28.2", "caniuse-lite": "^1.0.30001787", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong=="], "babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="], @@ -1180,8 +1177,6 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "file-selector": ["file-selector@2.1.2", "", { "dependencies": { "tslib": "^2.7.0" } }, "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig=="], - "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="], "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], @@ -1746,8 +1741,6 @@ "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], - "react-dropzone": ["react-dropzone@15.0.0", "", { "dependencies": { "attr-accept": "^2.2.4", "file-selector": "^2.1.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8 || 18.0.0" } }, "sha512-lGjYV/EoqEjEWPnmiSvH4v5IoIAwQM2W4Z1C0Q/Pw2xD0eVzKPS359BQTUMum+1fa0kH2nrKjuavmTPOGhpLPg=="], - "react-easy-swipe": ["react-easy-swipe@0.0.21", "", { "dependencies": { "prop-types": "^15.5.8" } }, "sha512-OeR2jAxdoqUMHIn/nS9fgreI5hSpgGoL5ezdal4+oO7YSSgJR8ga+PkYGJrSrJ9MKlPcQjMQXnketrD7WNmNsg=="], "react-error-boundary": ["react-error-boundary@6.1.1", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w=="], diff --git a/docs/app/reflex.lock/package.json b/docs/app/reflex.lock/package.json index 4988a1c737b..c510989b352 100644 --- a/docs/app/reflex.lock/package.json +++ b/docs/app/reflex.lock/package.json @@ -44,7 +44,6 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "19.2.6", - "react-dropzone": "15.0.0", "react-fast-marquee": "1.6.5", "react-helmet": "6.1.0", "react-leaflet": "5.0.0", diff --git a/packages/reflex-docgen/src/reflex_docgen/markdown/__init__.py b/packages/reflex-docgen/src/reflex_docgen/markdown/__init__.py index f113841b683..07cfe9b8c97 100644 --- a/packages/reflex-docgen/src/reflex_docgen/markdown/__init__.py +++ b/packages/reflex-docgen/src/reflex_docgen/markdown/__init__.py @@ -1,5 +1,7 @@ """Markdown parsing and types for Reflex documentation.""" +from __future__ import annotations + from reflex_docgen.markdown import transformer as transformer from reflex_docgen.markdown._parser import parse_document as parse_document from reflex_docgen.markdown._types import Block as Block diff --git a/packages/reflex-docgen/src/reflex_docgen/markdown/_parser.py b/packages/reflex-docgen/src/reflex_docgen/markdown/_parser.py index d9f1a782ced..0cfc6a836e7 100644 --- a/packages/reflex-docgen/src/reflex_docgen/markdown/_parser.py +++ b/packages/reflex-docgen/src/reflex_docgen/markdown/_parser.py @@ -91,6 +91,7 @@ def _extract_frontmatter(source: str) -> tuple[FrontMatter | None, str]: only_low_level=only_low_level, title=title, component_previews=tuple(previews), + metadata=data, ), source[m.end() :], ) @@ -254,6 +255,7 @@ def _convert_block(token: BlockToken) -> Block | None: name=flags[0], args=flags[1:], children=_parse_blocks(content), + content=content, ) return CodeBlock(language=language, flags=flags, content=content) diff --git a/packages/reflex-docgen/src/reflex_docgen/markdown/_types.py b/packages/reflex-docgen/src/reflex_docgen/markdown/_types.py index d54b786a2f2..b7ae9d29b06 100644 --- a/packages/reflex-docgen/src/reflex_docgen/markdown/_types.py +++ b/packages/reflex-docgen/src/reflex_docgen/markdown/_types.py @@ -2,7 +2,8 @@ from __future__ import annotations -from dataclasses import dataclass +from collections.abc import Mapping +from dataclasses import dataclass, field # --------------------------------------------------------------------------- # Span types — inline content without exposing mistletoe @@ -141,12 +142,18 @@ class FrontMatter: only_low_level: Whether to show only low-level component variants. title: An optional page title. component_previews: Preview lambdas keyed by component class name. + metadata: The full raw frontmatter mapping, including the keys modeled + by the fields above plus any arbitrary keys a site defines (e.g. + ``author``, ``tags``, ``order``). """ components: tuple[str, ...] only_low_level: bool title: str | None component_previews: tuple[ComponentPreview, ...] + metadata: Mapping[str, object] = field( + default_factory=dict, hash=False, compare=False + ) @dataclass(frozen=True, slots=True, kw_only=True) @@ -174,11 +181,16 @@ class DirectiveBlock: name: The directive name (e.g. "alert", "video", "definition", "section"). args: Additional arguments after the name (e.g. ("info",) or ("https://...",)). children: The parsed block-level content inside the directive. + content: The raw (unparsed) inner text. Directives whose body is + line-oriented rather than markdown (e.g. a ``quote`` block's + ``- name:``/``- role:`` lines) should read this instead of + ``children`` to avoid CommonMark reflowing the lines. """ name: str args: tuple[str, ...] children: tuple[Block, ...] + content: str = "" @dataclass(frozen=True, slots=True, kw_only=True) @@ -310,6 +322,11 @@ class Document: frontmatter: FrontMatter | None blocks: tuple[Block, ...] + @property + def metadata(self) -> Mapping[str, object]: + """Return the raw frontmatter mapping, or an empty mapping if absent.""" + return self.frontmatter.metadata if self.frontmatter is not None else {} + @property def headings(self) -> tuple[HeadingBlock, ...]: """Return all headings in the document.""" diff --git a/packages/reflex-docgen/src/reflex_docgen/markdown/transformer/__init__.py b/packages/reflex-docgen/src/reflex_docgen/markdown/transformer/__init__.py index b4681621e8b..9e16aa4516e 100644 --- a/packages/reflex-docgen/src/reflex_docgen/markdown/transformer/__init__.py +++ b/packages/reflex-docgen/src/reflex_docgen/markdown/transformer/__init__.py @@ -1,4 +1,12 @@ -"""Document transformers for converting parsed markdown into other formats.""" +"""Document transformers for converting parsed markdown into other formats. + +The reflex-dependent :class:`~reflex_docgen.markdown.transformer.reflex.ReflexComponentTransformer` +lives in the :mod:`reflex_docgen.markdown.transformer.reflex` submodule so that +importing this package (and the markdown-string transformer) stays free of a +hard ``reflex`` dependency until the component transformer is actually used. +""" + +from __future__ import annotations from reflex_docgen.markdown.transformer._base import ( DocumentTransformer as DocumentTransformer, diff --git a/packages/reflex-docgen/src/reflex_docgen/markdown/transformer/_markdown.py b/packages/reflex-docgen/src/reflex_docgen/markdown/transformer/_markdown.py index 2660d14a0b2..7912170b4c9 100644 --- a/packages/reflex-docgen/src/reflex_docgen/markdown/transformer/_markdown.py +++ b/packages/reflex-docgen/src/reflex_docgen/markdown/transformer/_markdown.py @@ -113,15 +113,24 @@ def line_break(self, span: LineBreakSpan) -> str: def frontmatter(self, block: FrontMatter) -> str: import yaml - data: dict[str, object] = {} - if block.components: - data["components"] = list(block.components) - if block.only_low_level: - data["only_low_level"] = [True] - if block.title is not None: - data["title"] = block.title - for preview in block.component_previews: - data[preview.name] = preview.source + if block.metadata: + # The raw mapping is the faithful source of truth for a parsed + # document: render it verbatim so a parse -> render round-trip + # preserves key order, falsy values (e.g. ``only_low_level: false``), + # and preview source text exactly. + data: dict[str, object] = dict(block.metadata) + else: + # Programmatically constructed frontmatter carries no raw mapping; + # rebuild one from the typed fields and previews. + data = {} + if block.components: + data["components"] = list(block.components) + if block.only_low_level: + data["only_low_level"] = [True] + if block.title is not None: + data["title"] = block.title + for preview in block.component_previews: + data[preview.name] = preview.source return f"---\n{yaml.dump(data, default_flow_style=False, sort_keys=False).rstrip()}\n---" def code_block(self, block: CodeBlock) -> str: diff --git a/packages/reflex-docgen/src/reflex_docgen/markdown/transformer/reflex.py b/packages/reflex-docgen/src/reflex_docgen/markdown/transformer/reflex.py new file mode 100644 index 00000000000..1391e97010d --- /dev/null +++ b/packages/reflex-docgen/src/reflex_docgen/markdown/transformer/reflex.py @@ -0,0 +1,414 @@ +"""Reflex-component transformer — renders a Document to ``rx.Component`` trees. + +This is the default way to turn parsed markdown into Reflex components. It +covers the lean element set the content sites actually use and exposes two +customization layers that together replace the old flexdown ``component_map``: + +* **Subclassing** — override any block/span handler (``heading``, ``link``, …) + to fully control how a node renders. This is the primary mechanism. +* **Overrides dict** — pass ``overrides={element_name: builder}`` to swap a + single element's builder without subclassing, giving near drop-in parity with + the old ``component_map``. + +Builder signatures (the value side of ``overrides``) receive the node's +already-transformed children plus any scalar attributes, so they only need to +assemble the container: + +============ =========================================================== +element key builder signature +============ =========================================================== +``h1``-``h6`` ``(level: int, children: tuple[rx.Component, ...])`` +``p`` ``(children: tuple[rx.Component, ...])`` +``ul``/``ol`` ``(items: list[rx.Component])`` +``blockquote````(children: tuple[rx.Component, ...])`` +``pre`` ``(content: str, language: str | None)`` +``table`` ``(thead: rx.Component, tbody: rx.Component)`` +``hr`` ``()`` +``span`` ``(text: str)`` +``strong`` ``(children: tuple[rx.Component, ...])`` +``em`` ``(children: tuple[rx.Component, ...])`` +``s`` ``(children: tuple[rx.Component, ...])`` +``code`` ``(code: str)`` +``a`` ``(children: tuple[rx.Component, ...], href: str)`` +``img`` ``(src: str, alt: str)`` +``br`` ``()`` +============ =========================================================== + +Directive blocks are keyed by their directive name (e.g. ``"alert"``) and the +builder receives the :class:`DirectiveBlock` itself — read ``block.content`` +for line-oriented (non-markdown) bodies (e.g. ``md quote``'s ``- name:``/ +``- role:`` lines) to avoid CommonMark reflowing them, or call +``self.transform_blocks(block.children)`` to render the parsed children. +Without an override a directive falls back to a plain ``
``. +""" + +from __future__ import annotations + +from collections.abc import Callable + +import reflex as rx +from reflex_docgen.markdown._types import ( + BoldSpan, + CodeBlock, + CodeSpan, + DirectiveBlock, + Document, + FrontMatter, + HeadingBlock, + ImageSpan, + ItalicSpan, + LineBreakSpan, + LinkSpan, + ListBlock, + ListItem, + QuoteBlock, + Span, + StrikethroughSpan, + TableBlock, + TableCell, + TableRow, + TextBlock, + TextSpan, + ThematicBreakBlock, +) +from reflex_docgen.markdown.transformer._base import DocumentTransformer + +__all__ = ["ReflexComponentTransformer"] + + +def _plain_text(spans: tuple[Span, ...]) -> str: + """Flatten inline spans into their plain text content. + + Used for attributes that must be strings (e.g. an image's ``alt``). + + Args: + spans: The inline spans to flatten. + + Returns: + The concatenated text of the spans, ignoring formatting. + """ + parts: list[str] = [] + for span in spans: + if isinstance(span, TextSpan): + parts.append(span.text) + elif isinstance(span, CodeSpan): + parts.append(span.code) + elif isinstance( + span, BoldSpan | ItalicSpan | StrikethroughSpan | LinkSpan | ImageSpan + ): + parts.append(_plain_text(span.children)) + elif isinstance(span, LineBreakSpan): + parts.append(" ") + return "".join(parts) + + +class ReflexComponentTransformer(DocumentTransformer["rx.Component"]): + """Transforms a :class:`Document` into a tree of Reflex components. + + Every handler renders with plain ``rx.el.*`` elements by default. Pass + ``overrides`` to replace individual element builders, or subclass and + override handlers for full control. See the module docstring for the + builder signatures keyed by element name. + """ + + def __init__(self, overrides: dict[str, Callable[..., rx.Component]] | None = None): + """Initialize the transformer. + + Args: + overrides: Element-name -> builder map (the ``component_map`` + analog). Each builder is consulted at the top of the matching + handler; absent keys fall through to the default ``rx.el.*`` + rendering. + """ + self._overrides = overrides or {} + + def transform(self, document: Document) -> rx.Component: + """Render a full document to a fragment of its blocks. + + Frontmatter is not part of ``document.blocks`` and renders nothing. + + Args: + document: The parsed document. + + Returns: + A fragment wrapping the transformed blocks. + """ + return rx.fragment(*self.transform_blocks(document.blocks)) + + def frontmatter(self, block: FrontMatter) -> rx.Component: + """Render frontmatter as nothing (an empty fragment). + + Args: + block: The frontmatter block. + + Returns: + An empty fragment. + """ + return rx.fragment() + + def heading(self, block: HeadingBlock) -> rx.Component: + """Render a heading as an ``

``-``

`` element. + + Args: + block: The heading block. + + Returns: + The heading component. + """ + level = min(max(block.level, 1), 6) + children = self.transform_spans(block.children) + if builder := self._overrides.get(f"h{level}"): + return builder(level, children) + return getattr(rx.el, f"h{level}")(*children) + + def text_block(self, block: TextBlock) -> rx.Component: + """Render a paragraph as a ``

`` element. + + Args: + block: The text block. + + Returns: + The paragraph component. + """ + children = self.transform_spans(block.children) + if builder := self._overrides.get("p"): + return builder(children) + return rx.el.p(*children) + + def list_block(self, block: ListBlock) -> rx.Component: + """Render a list as a ``