diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index fc2b6ad1700..06ae79a0914 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -34,5 +34,3 @@ jobs: fetch-tags: true fetch-depth: 0 - run: uv run pre-commit run --all-files --show-diff-on-failure - - name: Run docs app pre-commit - run: uv run pre-commit run --all-files --show-diff-on-failure --config docs/app/.pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2c1ffba8b48..45bc3b9e76b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,11 +4,13 @@ repos: - args: - reflex - tests + - docs/app + - packages id: ruff-format - args: - --fix - --exit-non-zero-on-fix - exclude: ^(integration/benchmarks/|docs/app/) + exclude: ^integration/benchmarks/ id: ruff-check repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.6 diff --git a/docs/app/.pre-commit-config.yaml b/docs/app/.pre-commit-config.yaml deleted file mode 100644 index 09f522519ed..00000000000 --- a/docs/app/.pre-commit-config.yaml +++ /dev/null @@ -1,12 +0,0 @@ -fail_fast: true - -repos: - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.14.0 - hooks: - - id: ruff-format - args: ["--config", "docs/app/pyproject.toml"] - files: ^docs/app/ - - id: ruff-check - args: ["--config", "docs/app/pyproject.toml", "--fix", "--exit-non-zero-on-fix", "--no-unsafe-fixes"] - files: ^docs/app/ diff --git a/docs/app/CLAUDE.md b/docs/app/CLAUDE.md index 16607350f86..88eb1a30eed 100644 --- a/docs/app/CLAUDE.md +++ b/docs/app/CLAUDE.md @@ -23,7 +23,7 @@ reflex_docs/ # Main application code components/ # Reusable UI components views/ # Shared view components (navbar, footer, cta) templates/ # Page templates (docpage, mainpage) -docs/ # Markdown documentation (flexdown) +docs/ # Markdown documentation (reflex_docgen) tests/ # Pytest + Playwright tests ``` @@ -52,5 +52,5 @@ tests/ # Pytest + Playwright tests ## Key conventions -- Docs: flexdown in `docs/` +- Docs: reflex_docgen in `docs/` - Before committing: `uv run reflex compile` and `uv run pre-commit run --all-files` diff --git a/docs/app/pyproject.toml b/docs/app/pyproject.toml index ac8ba7dbe5d..479983561bc 100644 --- a/docs/app/pyproject.toml +++ b/docs/app/pyproject.toml @@ -6,9 +6,7 @@ requires-python = ">=3.10" dependencies = [ "email-validator", "fastapi", - "flexdown", "googletrans-py", - "mistletoe", "openai", "orjson", "pandas", diff --git a/docs/app/reflex_docs/components/button.py b/docs/app/reflex_docs/components/button.py index 54a8b2f6845..2e7f8de4d78 100644 --- a/docs/app/reflex_docs/components/button.py +++ b/docs/app/reflex_docs/components/button.py @@ -1,8 +1,7 @@ from typing import Callable, Literal -from reflex_ui_shared.components.icons import get_icon - import reflex as rx +from reflex_ui_shared.components.icons import get_icon LiteralButtonVariant = Literal[ "primary", "success", "destructive", "secondary", "muted" diff --git a/docs/app/reflex_docs/components/docpage/navbar/buttons/sidebar.py b/docs/app/reflex_docs/components/docpage/navbar/buttons/sidebar.py index ebb2a66ec15..7fc5678720f 100644 --- a/docs/app/reflex_docs/components/docpage/navbar/buttons/sidebar.py +++ b/docs/app/reflex_docs/components/docpage/navbar/buttons/sidebar.py @@ -1,12 +1,11 @@ +import reflex as rx import reflex_ui as ui +from reflex.style import toggle_color_mode from reflex_ui_shared.components.icons import get_icon from reflex_ui_shared.components.marketing_button import button from reflex_ui_shared.constants import DISCORD_URL, GITHUB_URL, TWITTER_URL from reflex_ui_shared.views.hosting_banner import HostingBannerState -import reflex as rx -from reflex.style import toggle_color_mode - _DRAWER_LINKS_DOCS = "/docs" _DRAWER_LINKS_TEMPLATES = "/templates" _DRAWER_LINKS_BLOG = "/blog" diff --git a/docs/app/reflex_docs/docgen_pipeline.py b/docs/app/reflex_docs/docgen_pipeline.py index 6b35e89a949..87fd1dbf1e3 100644 --- a/docs/app/reflex_docs/docgen_pipeline.py +++ b/docs/app/reflex_docs/docgen_pipeline.py @@ -4,6 +4,7 @@ import types from pathlib import Path +import reflex as rx from reflex_base.constants.colors import ColorType from reflex_docgen.markdown import ( Block, @@ -52,15 +53,14 @@ ) from reflex_ui_shared.constants import REFLEX_ASSETS_CDN -import reflex as rx - # --------------------------------------------------------------------------- -# Exec environment — mirrors flexdown's module-based exec mechanism +# Exec environment — mirrors reflex_docgen's module-based exec mechanism # --------------------------------------------------------------------------- # One in-memory module per file — all exec blocks within a doc accumulate # into the same namespace, so later definitions shadow earlier ones cleanly. _file_modules: dict[str, types.ModuleType] = {} +_executed_blocks: set[tuple[str, str]] = set() # Register the parent package so pickle can resolve child modules. _PARENT_PKG = "_docgen_exec" @@ -83,8 +83,16 @@ def _exec_code(content: str, env: dict, filename: str) -> None: """Execute a ``python exec`` code block via an in-memory module. All exec blocks within the same file share one module so that State - subclass redefinitions shadow correctly. + subclass redefinitions shadow correctly. When the same block is + encountered a second time (e.g. the frontend is evaluated twice — + once for compilation and once on the backend), skip re-execution and + just populate *env* from the cached module namespace. """ + key = (filename, content) + if key in _executed_blocks: + env.update(_file_modules[filename].__dict__) + return + if filename not in _file_modules: mod_name = _make_module_name(filename) module = types.ModuleType(mod_name) @@ -99,6 +107,7 @@ def _exec_code(content: str, env: dict, filename: str) -> None: exec(compile(content, filename or "", "exec"), module.__dict__) env.update(module.__dict__) + _executed_blocks.add(key) # --------------------------------------------------------------------------- @@ -165,7 +174,7 @@ def _spans_to_plaintext(spans: tuple[Span, ...]) -> str: class ReflexDocTransformer(DocumentTransformer[rx.Component]): """Transforms a reflex_docgen Document into Reflex components. - Mirrors the rendering that the flexdown pipeline produces, so docs from + Mirrors the rendering that the reflex_docgen pipeline produces, so docs from the parent docs directory look identical to the locally-authored ones. """ @@ -503,27 +512,34 @@ def title_comp() -> rx.Component: ), ] - if children: - # Has body content — render as collapsible accordion. - if title_spans: - trigger.append(title_comp()) - body = rx.accordion.content( - self._render_children(children), - padding="0px", - margin_top="16px", - ) - else: - trigger.append( - rx.box( - self._render_children(children), - class_name="font-[475] !text-secondary-11", - ), - ) - body = rx.fragment() + if children and title_spans: + # Has heading + body — render as collapsible accordion. + trigger.append(title_comp()) + body = rx.accordion.content( + self._render_children(children), + padding="0px", + margin_top="16px", + ) return collapsible_box(trigger, body, color) - # Title only, no body — simple box. - trigger.append(title_comp()) + # Title only, or text-only (no heading) — simple non-collapsible box. + if title_spans: + trigger.append(title_comp()) + elif children: + # Render inline spans directly — avoid text_block's mb-4 margin. + spans: list[rx.Component | str] = [] + for child in children: + if isinstance(child, TextBlock): + spans.extend(_render_spans(child.children)) + else: + spans.append(self.transform_block(child)) + trigger.append( + rx.box( + *spans, + class_name="font-[475]", + color=f"{rx.color(color, 11)}", + ), + ) return rx.vstack( rx.hstack( *trigger, @@ -694,6 +710,13 @@ def render_docgen_document( def get_docgen_toc(filepath: str | Path) -> list[tuple[int, str]]: - """Extract TOC headings as (level, text) tuples — same format as flexdown's get_toc.""" + """Extract TOC headings as (level, text) tuples — same format as reflex_docgen's get_toc.""" doc = _parse_doc(filepath) return [(h.level, _spans_to_plaintext(h.children)) for h in doc.headings] + + +def render_markdown(text: str) -> rx.Component: + """Render a plain markdown text string into Reflex components.""" + doc = parse_document(text) + transformer = ReflexDocTransformer() + return transformer.transform(doc) diff --git a/docs/app/reflex_docs/pages/docs/__init__.py b/docs/app/reflex_docs/pages/docs/__init__.py index 5863cab6a56..48bd188d4bf 100644 --- a/docs/app/reflex_docs/pages/docs/__init__.py +++ b/docs/app/reflex_docs/pages/docs/__init__.py @@ -1,20 +1,15 @@ import os -import sys from collections import defaultdict, namedtuple from pathlib import Path from types import SimpleNamespace -import flexdown -from flexdown.document import Document +import reflex as rx +from reflex_docgen.markdown import parse_document # External Components from reflex_pyplot import pyplot as pyplot -from reflex_ui_shared.components.blocks.flexdown import xd -from reflex_ui_shared.constants import REFLEX_ASSETS_CDN from reflex_ui_shared.route import Route -from reflex_ui_shared.utils.docpage import get_toc -import reflex as rx from reflex_docs.docgen_pipeline import get_docgen_toc, render_docgen_document from reflex_docs.pages.docs.component import multi_docs from reflex_docs.pages.library_previews import components_previews_pages @@ -29,22 +24,6 @@ from .recipes_overview import overview -def should_skip_compile(doc: flexdown.Document): - """Skip compilation if the markdown file has not been modified since the last compilation.""" - if not os.environ.get("REFLEX_PERSIST_WEB_DIR", False): - return False - - # Check if the doc has been compiled already. - compiled_output = f".web/pages/{doc.replace('.md', '.js')}" - # Get the timestamp of the compiled file. - compiled_time = ( - os.path.getmtime(compiled_output) if os.path.exists(compiled_output) else 0 - ) - # Get the timestamp of the source file. - source_time = os.path.getmtime(doc) - return compiled_time > source_time - - def to_title_case(text: str) -> str: return " ".join(word.capitalize() for word in text.split("_")) @@ -78,10 +57,14 @@ def build_nested_namespace( return parent_namespace -def get_components_from_metadata(current_doc): +def get_components_from_frontmatter(filepath: str) -> list: + """Extract component tuples from a doc's frontmatter.""" + source = Path(filepath).read_text(encoding="utf-8") + doc = parse_document(source) + if doc.frontmatter is None: + return [] components = [] - - for comp_str in current_doc.metadata.get("components", []): + for comp_str in doc.frontmatter.components: component = eval(comp_str) if isinstance(component, type): components.append((component, comp_str)) @@ -91,16 +74,31 @@ def get_components_from_metadata(current_doc): components.append((component.__call__.__self__, comp_str)) else: raise ValueError(f"Invalid component: {component}") - return components +def get_previews_from_frontmatter(filepath: str) -> dict[str, str]: + """Extract component preview sources from a doc's frontmatter.""" + source = Path(filepath).read_text(encoding="utf-8") + doc = parse_document(source) + if doc.frontmatter is None: + return {} + return {p.name: p.source for p in doc.frontmatter.component_previews} + + # --------------------------------------------------------------------------- -# Local docs — processed via flexdown +# Discover all docs — single pipeline via reflex_docgen # --------------------------------------------------------------------------- -flexdown_docs = [ - str(doc).replace("\\", "/") for doc in flexdown.utils.get_flexdown_files("docs/") -] +_app_root = Path(__file__).resolve().parent.parent.parent.parent # …/app/ +_docs_dir = _app_root.parent # …/docs/ (parent of app/) + +all_docs: dict[str, str] = {} # virtual_path → actual_path +for _md_file in sorted(_docs_dir.rglob("*.md")): + # Skip anything inside the app/ subdirectory. + if _md_file.is_relative_to(_app_root): + continue + _virtual = "docs/" + str(_md_file.relative_to(_docs_dir)).replace("\\", "/") + all_docs[_virtual] = str(_md_file) # Add integration docs from the installed package doc_path_mapping: dict[str, str] = {} @@ -112,34 +110,11 @@ def get_components_from_metadata(current_doc): if integration_doc.name in ("snowflake.md", "overview.md"): continue virtual_path = f"docs/ai_builder/integrations/{integration_doc.name}" + vp = virtual_path.replace("\\", "/") actual_path = str(integration_doc).replace("\\", "/") - if virtual_path.replace("\\", "/") not in flexdown_docs: - doc_path_mapping[virtual_path.replace("\\", "/")] = actual_path - flexdown_docs.append(virtual_path.replace("\\", "/")) - -# --------------------------------------------------------------------------- -# Reflex-shipped docs (installed in site-packages/docs/) — processed via -# reflex_docgen.markdown pipeline (no flexdown). -# --------------------------------------------------------------------------- -# Maps virtual path (e.g. "docs/getting_started/basics.md") → absolute path. -docgen_docs: dict[str, str] = {} -_app_root = Path(__file__).resolve().parent.parent.parent.parent # …/app/ -_reflex_docs_dir = _app_root.parent # …/reflex/docs/ (parent of app/) -# Add parent of docs dir to sys.path so exec blocks can `from docs.X import Y`. -_docs_parent = str(_reflex_docs_dir.parent) -if _docs_parent not in sys.path: - sys.path.insert(0, _docs_parent) -if _reflex_docs_dir.is_dir(): - for _pkg_doc in sorted(_reflex_docs_dir.rglob("*.md")): - # Skip anything inside the app/ subdirectory. - if _pkg_doc.is_relative_to(_app_root): - continue - _virtual = "docs/" + str(_pkg_doc.relative_to(_reflex_docs_dir)).replace( - "\\", "/" - ) - # Only add if not already provided locally (local overrides package). - if _virtual not in flexdown_docs: - docgen_docs[_virtual] = str(_pkg_doc) + if vp not in all_docs: + doc_path_mapping[vp] = actual_path + all_docs[vp] = actual_path graphing_components = defaultdict(list) component_list = defaultdict(list) @@ -149,22 +124,6 @@ def get_components_from_metadata(current_doc): doc_markdown_sources: dict[str, str] = {} -def exec_blocks(doc, href): - """Execute the exec and demo blocks in the document.""" - source = doc.content - env = doc.metadata.copy() - env["__xd"] = xd - env["__exec"] = True - blocks = xd.get_blocks(source, href) - # Get only the exec and demo blocks. - blocks = [b for b in blocks if b.__class__.__name__ in ["ExecBlock", "DemoBlock"]] - for block in blocks: - block.render(env) - - -outblocks = [] - - manual_titles = { "docs/database/overview.md": "Database Overview", "docs/custom-components/overview.md": "Custom Components Overview", @@ -225,13 +184,6 @@ def make_docpage(route: str, title: str, doc_virtual: str, render_fn): return docpage(set_path=route, t=title)(render_fn) -def load_flexdown_doc(actual_path: str) -> Document: - """Load a flexdown Document and inject standard metadata.""" - d = Document.from_file(actual_path) - d.metadata["REFLEX_ASSETS_CDN"] = REFLEX_ASSETS_CDN - return d - - def handle_library_doc( doc: str, actual_path: str, @@ -239,53 +191,28 @@ def handle_library_doc( resolved: ResolvedDoc, ): """Handle docs/library/** docs — component API reference via multi_docs.""" - d = load_flexdown_doc(actual_path) - clist = [title, *get_components_from_metadata(d)] + clist = [title, *get_components_from_frontmatter(actual_path)] + previews = get_previews_from_frontmatter(actual_path) if doc.startswith("docs/library/graphing"): graphing_components[resolved.category].append(clist) else: component_list[resolved.category].append(clist) - if should_skip_compile(actual_path): - outblocks.append((d, resolved.route)) - return None return multi_docs( path=resolved.route, - comp=d, + virtual_path=doc, + actual_path=actual_path, + previews=previews, component_list=clist, title=resolved.display_title, ) -def get_component(doc: str, title: str): - """Build a page component for a local (flexdown) doc.""" - resolved = resolve_doc_route(doc, title) - if resolved is None: - return None - - actual_doc_path = doc_path_mapping.get(doc, doc) - - if doc.startswith("docs/library"): - return handle_library_doc(doc, actual_doc_path, title, resolved) - - if should_skip_compile(actual_doc_path): - outblocks.append((load_flexdown_doc(actual_doc_path), resolved.route)) - return None - - d = load_flexdown_doc(actual_doc_path) - - def comp(): - return (get_toc(d, actual_doc_path), xd.render(d, actual_doc_path)) - - return make_docpage(resolved.route, resolved.display_title, doc, comp) - - def get_component_docgen(virtual_doc: str, actual_path: str, title: str): - """Build a page component for a reflex-package doc via reflex_docgen.""" + """Build a page component for a doc via reflex_docgen.""" resolved = resolve_doc_route(virtual_doc, title) if resolved is None: return None - # Library docs still need component introspection via multi_docs (flexdown-based). if virtual_doc.startswith("docs/library"): return handle_library_doc(virtual_doc, actual_path, title, resolved) @@ -300,20 +227,14 @@ def comp(_actual=actual_path, _virtual=virtual_doc): return make_docpage(resolved.route, resolved.display_title, virtual_doc, comp) -for fd in flexdown_docs: - if fd.endswith("-style.md") or fd.endswith("-ll.md"): +# Build doc_markdown_sources mapping +for _virtual, _actual in all_docs.items(): + if _virtual.endswith("-style.md") or _virtual.endswith("-ll.md"): continue - route = doc_route_from_path(fd) - if not _check_whitelisted_path(route): + _route = doc_route_from_path(_virtual) + if not _check_whitelisted_path(_route): continue - doc_markdown_sources[route] = doc_path_mapping.get(fd, fd) -for virtual_doc, actual_path in docgen_docs.items(): - if virtual_doc.endswith("-style.md") or virtual_doc.endswith("-ll.md"): - continue - route = doc_route_from_path(virtual_doc) - if not _check_whitelisted_path(route): - continue - doc_markdown_sources[route] = actual_path + doc_markdown_sources[_route] = _actual doc_routes = [ library, @@ -364,16 +285,12 @@ def register_doc(virtual_doc: str, comp): library_: Route = library # type: ignore[assignment] -# Process local docs (flexdown pipeline). -for _doc in sorted(flexdown_docs): - register_doc(_doc, get_component(_doc, doc_title_from_path(_doc))) - -# Process reflex-package docs (reflex_docgen pipeline). -for _virtual, _actual in sorted(docgen_docs.items()): +# Process all docs via reflex_docgen pipeline. +for _virtual, _actual in sorted(all_docs.items()): register_doc( _virtual, get_component_docgen(_virtual, _actual, doc_title_from_path(_virtual)), ) for name, ns in docs_ns.__dict__.items(): - locals()[name] = ns + globals()[name] = ns diff --git a/docs/app/reflex_docs/pages/docs/api_reference/plugins.py b/docs/app/reflex_docs/pages/docs/api_reference/plugins.py index 18a1bf00590..7e76be1ecd4 100644 --- a/docs/app/reflex_docs/pages/docs/api_reference/plugins.py +++ b/docs/app/reflex_docs/pages/docs/api_reference/plugins.py @@ -1,9 +1,9 @@ """Plugins API reference page.""" -from reflex_docs.templates.docpage import docpage - import reflex as rx +from reflex_docs.templates.docpage import docpage + @docpage("/docs/api-reference/plugins/") def plugins(): diff --git a/docs/app/reflex_docs/pages/docs/api_reference/utils.py b/docs/app/reflex_docs/pages/docs/api_reference/utils.py index 6f7cf038e55..437557e3675 100644 --- a/docs/app/reflex_docs/pages/docs/api_reference/utils.py +++ b/docs/app/reflex_docs/pages/docs/api_reference/utils.py @@ -1,9 +1,9 @@ """Utils API reference page.""" -from reflex_docs.templates.docpage import docpage - import reflex as rx +from reflex_docs.templates.docpage import docpage + @docpage("/docs/api-reference/utils/") def utils(): diff --git a/docs/app/reflex_docs/pages/docs/apiref.py b/docs/app/reflex_docs/pages/docs/apiref.py index 902abfdd7ef..9f458f9a9c9 100644 --- a/docs/app/reflex_docs/pages/docs/apiref.py +++ b/docs/app/reflex_docs/pages/docs/apiref.py @@ -1,8 +1,8 @@ -from reflex_docgen import generate_class_documentation - import reflex as rx from reflex.istate.manager import StateManager from reflex.utils.imports import ImportVar +from reflex_docgen import generate_class_documentation + from reflex_docs.templates.docpage import docpage from .source import generate_docs diff --git a/docs/app/reflex_docs/pages/docs/cloud_cliref.py b/docs/app/reflex_docs/pages/docs/cloud_cliref.py index 485eb384fcf..7b838990932 100644 --- a/docs/app/reflex_docs/pages/docs/cloud_cliref.py +++ b/docs/app/reflex_docs/pages/docs/cloud_cliref.py @@ -6,10 +6,10 @@ from typing import Any, TypedDict import click -from reflex_ui_shared.components.blocks.flexdown import markdown - import reflex as rx from reflex.reflex import cli + +from reflex_docs.docgen_pipeline import render_markdown from reflex_docs.templates.docpage import docpage @@ -344,7 +344,7 @@ def prefix( def generate_docs(source: str): return rx.box( - markdown(text=source), + render_markdown(text=source), ) diff --git a/docs/app/reflex_docs/pages/docs/component.py b/docs/app/reflex_docs/pages/docs/component.py index 2edffe3b19b..0f4e1f17a2e 100644 --- a/docs/app/reflex_docs/pages/docs/component.py +++ b/docs/app/reflex_docs/pages/docs/component.py @@ -3,11 +3,13 @@ import hashlib import os import textwrap +from pathlib import Path from types import UnionType from typing import Literal, Union, _GenericAlias, get_args, get_origin -from flexdown.document import Document +import reflex as rx from reflex.components.base.fragment import Fragment +from reflex.components.component import Component from reflex.components.radix.primitives.base import RadixPrimitiveComponent from reflex.components.radix.themes.base import RadixThemesComponent from reflex_docgen import ( @@ -15,12 +17,12 @@ PropDocumentation, generate_documentation, ) -from reflex_ui_shared.components.blocks.flexdown import markdown, xd -from reflex_ui_shared.constants import REFLEX_ASSETS_CDN -from reflex_ui_shared.utils.docpage import get_toc -import reflex as rx -from reflex.components.component import Component +from reflex_docs.docgen_pipeline import ( + get_docgen_toc, + render_docgen_document, + render_markdown, +) from reflex_docs.templates.docpage import docdemobox, docpage, h1_comp, h2_comp @@ -442,7 +444,7 @@ def prop_docs( def generate_props( props: tuple[PropDocumentation, ...], component: type[Component], - comp: Document | rx.Component, + previews: dict[str, str], ) -> rx.Component: prop_list = list(props) if len(prop_list) == 0: @@ -484,9 +486,10 @@ def generate_props( class_name="bg-slate-2", ) + comp: rx.Component try: - if f"{component.__name__}" in comp.metadata: - comp = eval(comp.metadata[component.__name__])(**prop_dict) + if component.__name__ in previews: + comp = eval(previews[component.__name__])(**prop_dict) elif not is_interactive: comp = rx.fragment() @@ -643,12 +646,12 @@ def generate_valid_children(comp: type[Component]) -> rx.Component: def component_docs( - component_tuple: tuple[type[Component], str], comp: Document + component_tuple: tuple[type[Component], str], previews: dict[str, str] ) -> rx.Component: """Generates documentation for a given component.""" component = component_tuple[0] doc = generate_documentation(component) - props = generate_props(doc.props, component, comp) + props = generate_props(doc.props, component, previews) triggers = generate_event_triggers(doc.event_handlers) children = generate_valid_children(component) @@ -663,7 +666,9 @@ def component_docs( return rx.box( h2_comp(text=comp_display_name), - rx.box(markdown(textwrap.dedent(doc.description or "")), class_name="pb-2"), + rx.box( + render_markdown(textwrap.dedent(doc.description or "")), class_name="pb-2" + ), props, children, triggers, @@ -671,9 +676,17 @@ def component_docs( ) -def multi_docs(path: str, comp: Document, component_list: list, title: str): +def multi_docs( + path: str, + virtual_path: str, + actual_path: str, + previews: dict[str, str], + component_list: list, + title: str, +): components = [ - component_docs(component_tuple, comp) for component_tuple in component_list[1:] + component_docs(component_tuple, previews) + for component_tuple in component_list[1:] ] fname = path.strip("/") + ".md" ll_doc_exists = os.path.exists(fname.replace(".md", "-ll.md")) @@ -728,10 +741,18 @@ def links(current_page, ll_doc_exists, path): @docpage(set_path=path, t=title) def out(): - toc = get_toc(comp, fname, component_list) - return toc, rx.box( + toc = get_docgen_toc(actual_path) + doc_content = Path(actual_path).read_text(encoding="utf-8") + # Append API Reference headings for the component list + if component_list: + toc.append((1, "API Reference")) + for component_tuple in component_list[1:]: + toc.append((2, component_tuple[1])) + return (toc, doc_content), rx.box( links("hl", ll_doc_exists, path), - xd.render(comp, filename=fname), + render_docgen_document( + virtual_filepath=virtual_path, actual_filepath=actual_path + ), h1_comp(text="API Reference"), rx.box(*components, class_name="flex flex-col"), class_name="flex flex-col w-full", @@ -739,14 +760,19 @@ def out(): @docpage(set_path=path + "low", t=title + " (Low Level)") def ll(): - nonlocal fname - fname = fname.replace(".md", "-ll.md") - d2 = Document.from_file(fname) - d2.metadata["REFLEX_ASSETS_CDN"] = REFLEX_ASSETS_CDN - toc = get_toc(d2, fname, component_list) - return toc, rx.box( + ll_actual = fname.replace(".md", "-ll.md") + ll_virtual = virtual_path.replace(".md", "-ll.md") + toc = get_docgen_toc(ll_actual) + doc_content = Path(ll_actual).read_text(encoding="utf-8") + if component_list: + toc.append((1, "API Reference")) + for component_tuple in component_list[1:]: + toc.append((2, component_tuple[1])) + return (toc, doc_content), rx.box( links("ll", ll_doc_exists, path), - xd.render(d2, fname), + render_docgen_document( + virtual_filepath=ll_virtual, actual_filepath=ll_actual + ), h1_comp(text="API Reference"), rx.box(*components, class_name="flex flex-col"), class_name="flex flex-col w-full", diff --git a/docs/app/reflex_docs/pages/docs/custom_components.py b/docs/app/reflex_docs/pages/docs/custom_components.py index d7e482cfdb6..12414e2e8c9 100644 --- a/docs/app/reflex_docs/pages/docs/custom_components.py +++ b/docs/app/reflex_docs/pages/docs/custom_components.py @@ -2,12 +2,12 @@ import os import httpx +import reflex as rx from reflex_ui_shared.components.icons import get_icon from reflex_ui_shared.styles.colors import c_color from reflex_ui_shared.styles.fonts import base from reflex_ui_shared.styles.shadows import shadows -import reflex as rx from reflex_docs.templates.docpage import docpage, h1_comp, text_comp_2 SORTING_CRITERIA = { diff --git a/docs/app/reflex_docs/pages/docs/env_vars.py b/docs/app/reflex_docs/pages/docs/env_vars.py index 02bae540eb2..b9f95939449 100644 --- a/docs/app/reflex_docs/pages/docs/env_vars.py +++ b/docs/app/reflex_docs/pages/docs/env_vars.py @@ -5,10 +5,10 @@ import inspect from typing import Any, List, Optional, Tuple -from reflex_ui_shared.components.blocks.flexdown import markdown - import reflex as rx from reflex.config import EnvironmentVariables + +from reflex_docs.docgen_pipeline import render_markdown from reflex_docs.templates.docpage import docpage, h1_comp, h2_comp @@ -119,7 +119,7 @@ def generate_env_var_table(cls, include_internal: bool = False) -> rx.Component: class_name="w-[15%]", ), rx.table.cell( - markdown(cls.get_env_var_docstring(name) or ""), + render_markdown(cls.get_env_var_docstring(name) or ""), class_name="font-small text-slate-11 w-[50%]", ), ) @@ -145,7 +145,7 @@ def env_vars_page(): "reflex.config.EnvironmentVariables", class_name="code-style text-[18px]" ), rx.divider(), - markdown( + render_markdown( """ Reflex provides a number of environment variables that can be used to configure the behavior of your application. These environment variables can be set in your shell environment or in a `.env` file. diff --git a/docs/app/reflex_docs/pages/docs/guide.py b/docs/app/reflex_docs/pages/docs/guide.py index bea6267851b..da5989ee612 100644 --- a/docs/app/reflex_docs/pages/docs/guide.py +++ b/docs/app/reflex_docs/pages/docs/guide.py @@ -1,6 +1,5 @@ -from reflex_ui_shared.templates.webpage import webpage - import reflex as rx +from reflex_ui_shared.templates.webpage import webpage @webpage(path="/flexdown-guide", title="Flexdown Guide") diff --git a/docs/app/reflex_docs/pages/docs/library.py b/docs/app/reflex_docs/pages/docs/library.py index 608bfd0f193..07882662eb2 100644 --- a/docs/app/reflex_docs/pages/docs/library.py +++ b/docs/app/reflex_docs/pages/docs/library.py @@ -1,7 +1,7 @@ -from reflex_ui_shared.components.icons import get_icon - import reflex as rx from reflex.utils.format import to_snake_case, to_title_case +from reflex_ui_shared.components.icons import get_icon + from reflex_docs.templates.docpage import docpage, h1_comp, text_comp_2 diff --git a/docs/app/reflex_docs/pages/docs/markdown_api.py b/docs/app/reflex_docs/pages/docs/markdown_api.py index a6f2fd6174c..58d7abf599f 100644 --- a/docs/app/reflex_docs/pages/docs/markdown_api.py +++ b/docs/app/reflex_docs/pages/docs/markdown_api.py @@ -3,6 +3,7 @@ from pathlib import Path from reflex.constants import Dirs + from reflex_docs.pages.docs import doc_markdown_sources PUBLIC_DIR = Path.cwd() / Dirs.WEB / Dirs.PUBLIC diff --git a/docs/app/reflex_docs/pages/docs/recipes_overview.py b/docs/app/reflex_docs/pages/docs/recipes_overview.py index 148105c841f..37f5223aa61 100644 --- a/docs/app/reflex_docs/pages/docs/recipes_overview.py +++ b/docs/app/reflex_docs/pages/docs/recipes_overview.py @@ -1,4 +1,5 @@ import reflex as rx + from reflex_docs.templates.docpage import docpage, h1_comp, h2_comp, text_comp_2 diff --git a/docs/app/reflex_docs/pages/docs/source.py b/docs/app/reflex_docs/pages/docs/source.py index 55d7c73d0cc..74c18cc63b0 100644 --- a/docs/app/reflex_docs/pages/docs/source.py +++ b/docs/app/reflex_docs/pages/docs/source.py @@ -1,11 +1,11 @@ +import reflex as rx from reflex_docgen import ( FieldDocumentation, MethodDocumentation, generate_class_documentation, ) -from reflex_ui_shared.components.blocks.flexdown import markdown -import reflex as rx +from reflex_docs.docgen_pipeline import render_markdown from reflex_docs.templates.docpage import h1_comp, h2_comp table_header_class_name = ( @@ -44,7 +44,7 @@ def format_fields( format_field(field), ), rx.table.cell( - markdown(field.description or ""), + render_markdown(field.description or ""), class_name="font-small text-slate-11", ), ) @@ -102,7 +102,7 @@ def generate_docs( h1_comp(text=title.title()), rx.code(doc.name, class_name="code-style text-[18px]"), rx.divider(), - markdown(doc.description or ""), + render_markdown(doc.description or ""), ( rx.box( h2_comp(text="Class Fields"), diff --git a/docs/app/reflex_docs/pages/docs_landing/__init__.py b/docs/app/reflex_docs/pages/docs_landing/__init__.py index bda52736445..012709f7df3 100644 --- a/docs/app/reflex_docs/pages/docs_landing/__init__.py +++ b/docs/app/reflex_docs/pages/docs_landing/__init__.py @@ -1,9 +1,9 @@ +import reflex as rx from reflex_ui_shared.constants import REFLEX_ASSETS_CDN from reflex_ui_shared.meta.meta import create_meta_tags from reflex_ui_shared.views.cta_card import cta_card from reflex_ui_shared.views.footer import footer_index -import reflex as rx from reflex_docs.pages.docs_landing.views import ( ai_builder_section, divider, diff --git a/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py b/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py index cfc8addca47..6330612a182 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/ai_builder.py @@ -1,11 +1,11 @@ import os import frontmatter +import reflex as rx import reflex_ui as ui from reflex_ui_shared.components.marquee import marquee from reflex_ui_shared.constants import INTEGRATIONS_IMAGES_URL, REFLEX_ASSETS_CDN -import reflex as rx from reflex_docs.pages.docs import ai_builder as ai_builder_pages diff --git a/docs/app/reflex_docs/pages/docs_landing/views/divider.py b/docs/app/reflex_docs/pages/docs_landing/views/divider.py index 590bf1ed319..f092fc06930 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/divider.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/divider.py @@ -1,6 +1,5 @@ -import reflex_ui as ui - import reflex as rx +import reflex_ui as ui def divider(class_name: str = "") -> rx.Component: diff --git a/docs/app/reflex_docs/pages/docs_landing/views/enterprise.py b/docs/app/reflex_docs/pages/docs_landing/views/enterprise.py index 0ca667fef30..419fce7237c 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/enterprise.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/enterprise.py @@ -1,4 +1,5 @@ import reflex as rx + from reflex_docs.pages.docs import enterprise as enterprise_page from reflex_docs.pages.docs_landing.views.link_item import faded_borders, link_item diff --git a/docs/app/reflex_docs/pages/docs_landing/views/framework.py b/docs/app/reflex_docs/pages/docs_landing/views/framework.py index 29e076a9672..53bdae6ae70 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/framework.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/framework.py @@ -1,8 +1,8 @@ +import reflex as rx import reflex_ui as ui from reflex_ui_shared.components.marketing_button import button from reflex_ui_shared.constants import REFLEX_ASSETS_CDN -import reflex as rx from reflex_docs.pages.docs import authentication, database, getting_started from reflex_docs.pages.docs.library import library from reflex_docs.pages.library_previews import core_components_dict diff --git a/docs/app/reflex_docs/pages/docs_landing/views/hero.py b/docs/app/reflex_docs/pages/docs_landing/views/hero.py index 118e8e289bb..d86746fe316 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/hero.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/hero.py @@ -1,9 +1,9 @@ +import reflex as rx import reflex_ui as ui from reflex_ui_shared.components.marketing_button import button from reflex_ui_shared.constants import REFLEX_ASSETS_CDN from reflex_ui_shared.views.hosting_banner import HostingBannerState -import reflex as rx from reflex_docs.pages.docs import getting_started diff --git a/docs/app/reflex_docs/pages/docs_landing/views/hosting.py b/docs/app/reflex_docs/pages/docs_landing/views/hosting.py index a145a097ed5..a8aac6a0ee5 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/hosting.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/hosting.py @@ -1,4 +1,5 @@ import reflex as rx + from reflex_docs.pages.docs import hosting as hosting_page from reflex_docs.pages.docs_landing.views.link_item import faded_borders, link_item diff --git a/docs/app/reflex_docs/pages/docs_landing/views/link_item.py b/docs/app/reflex_docs/pages/docs_landing/views/link_item.py index 14e5e44f67d..303f05c19aa 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/link_item.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/link_item.py @@ -1,6 +1,5 @@ -import reflex_ui as ui - import reflex as rx +import reflex_ui as ui def faded_borders() -> rx.Component: diff --git a/docs/app/reflex_docs/pages/docs_landing/views/other.py b/docs/app/reflex_docs/pages/docs_landing/views/other.py index 8dc03b042c1..39a5709260e 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/other.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/other.py @@ -1,6 +1,6 @@ +import reflex as rx from reflex_ui_shared.constants import CONTRIBUTING_URL -import reflex as rx from reflex_docs.pages.docs.custom_components import custom_components from reflex_docs.pages.docs_landing.views.link_item import faded_borders, link_item diff --git a/docs/app/reflex_docs/pages/docs_landing/views/self_hosting.py b/docs/app/reflex_docs/pages/docs_landing/views/self_hosting.py index ebaceadc016..da8ddba27e4 100644 --- a/docs/app/reflex_docs/pages/docs_landing/views/self_hosting.py +++ b/docs/app/reflex_docs/pages/docs_landing/views/self_hosting.py @@ -1,4 +1,5 @@ import reflex as rx + from reflex_docs.pages.docs import hosting as hosting_page from reflex_docs.pages.docs_landing.views.link_item import faded_borders, link_item diff --git a/docs/app/reflex_docs/pages/integrations/integration_gallery.py b/docs/app/reflex_docs/pages/integrations/integration_gallery.py index b3f3f965c28..a5ae4c71f39 100644 --- a/docs/app/reflex_docs/pages/integrations/integration_gallery.py +++ b/docs/app/reflex_docs/pages/integrations/integration_gallery.py @@ -1,8 +1,7 @@ -import reflex_ui as ui -from reflex_ui_shared.constants import INTEGRATIONS_IMAGES_URL - import reflex as rx +import reflex_ui as ui from reflex.experimental import ClientStateVar +from reflex_ui_shared.constants import INTEGRATIONS_IMAGES_URL from .integration_list import get_integration_path from .integration_request import request_integration_dialog diff --git a/docs/app/reflex_docs/pages/integrations/integration_request.py b/docs/app/reflex_docs/pages/integrations/integration_request.py index b866d198457..51acf7b61b4 100644 --- a/docs/app/reflex_docs/pages/integrations/integration_request.py +++ b/docs/app/reflex_docs/pages/integrations/integration_request.py @@ -1,7 +1,7 @@ +import reflex as rx import reflex_ui as ui -from reflex_docs.templates.docpage.feedback_state import FeedbackState -import reflex as rx +from reflex_docs.templates.docpage.feedback_state import FeedbackState def request_integration_dialog() -> rx.Component: diff --git a/docs/app/reflex_docs/pages/library_previews.py b/docs/app/reflex_docs/pages/library_previews.py index 87e07f59d27..7daeef71ca6 100644 --- a/docs/app/reflex_docs/pages/library_previews.py +++ b/docs/app/reflex_docs/pages/library_previews.py @@ -1,7 +1,7 @@ -from reflex_ui_shared.constants import REFLEX_ASSETS_CDN - import reflex as rx from reflex.utils.format import to_snake_case, to_title_case +from reflex_ui_shared.constants import REFLEX_ASSETS_CDN + from reflex_docs.templates.docpage import docpage, h1_comp, text_comp_2 diff --git a/docs/app/reflex_docs/reflex_docs.py b/docs/app/reflex_docs/reflex_docs.py index 8506ee9cfc0..5191073d14b 100644 --- a/docs/app/reflex_docs/reflex_docs.py +++ b/docs/app/reflex_docs/reflex_docs.py @@ -3,16 +3,15 @@ import os import sys +import reflex as rx import reflex_enterprise as rxe +from reflex.utils.exec import is_prod_mode from reflex_ui_shared import styles from reflex_ui_shared.constants import REFLEX_ASSETS_CDN from reflex_ui_shared.meta.meta import favicons_links, to_cdn_image_url from reflex_ui_shared.telemetry import get_pixel_website_trackers -import reflex as rx -from reflex.utils.exec import is_prod_mode from reflex_docs.pages import page404, routes -from reflex_docs.pages.docs import exec_blocks, outblocks from reflex_docs.pages.docs.markdown_api import generate_markdown_files from reflex_docs.whitelist import _check_whitelisted_path @@ -20,10 +19,6 @@ # higher and the prod build fails with EMFILE error. WINDOWS_MAX_ROUTES = int(os.environ.get("REFLEX_WEB_WINDOWS_MAX_ROUTES", "100")) -# Execute all the exec blocks in the documents. -for doc, href in outblocks: - exec_blocks(doc, href) - generate_markdown_files() # Create the app. app = rxe.App( diff --git a/docs/app/reflex_docs/templates/docpage/docpage.py b/docs/app/reflex_docs/templates/docpage/docpage.py index 67838a553b6..90c886e9169 100644 --- a/docs/app/reflex_docs/templates/docpage/docpage.py +++ b/docs/app/reflex_docs/templates/docpage/docpage.py @@ -4,8 +4,11 @@ from datetime import datetime from typing import Callable +import reflex as rx import reflex_ui as ui from reflex.components.radix.themes.base import LiteralAccentColor +from reflex.experimental.client_state import ClientStateVar +from reflex.utils.format import to_snake_case, to_title_case from reflex_ui_shared.components.blocks.code import * from reflex_ui_shared.components.blocks.demo import * from reflex_ui_shared.components.blocks.headings import * @@ -17,10 +20,6 @@ from reflex_ui_shared.utils.docpage import right_sidebar_item_highlight from reflex_ui_shared.views.footer import dark_mode_toggle -import reflex as rx -from reflex.experimental.client_state import ClientStateVar -from reflex.utils.format import to_snake_case, to_title_case - class FeedbackState(rx.State): """Minimal stub for feedback buttons (full implementation removed).""" diff --git a/docs/app/reflex_docs/templates/docpage/feedback_state.py b/docs/app/reflex_docs/templates/docpage/feedback_state.py index 7925c6eda1e..ca5b3859a9d 100644 --- a/docs/app/reflex_docs/templates/docpage/feedback_state.py +++ b/docs/app/reflex_docs/templates/docpage/feedback_state.py @@ -4,11 +4,10 @@ from typing import Optional import httpx +import reflex as rx from httpx import Response from reflex_ui_shared.constants import REFLEX_DEV_WEB_GENERAL_FORM_FEEDBACK_WEBHOOK_URL -import reflex as rx - class FeedbackState(rx.State): """The state for feedback components.""" diff --git a/docs/app/reflex_docs/templates/docpage/sidebar/sidebar.py b/docs/app/reflex_docs/templates/docpage/sidebar/sidebar.py index 8594e0059f3..b5cc2e648c7 100644 --- a/docs/app/reflex_docs/templates/docpage/sidebar/sidebar.py +++ b/docs/app/reflex_docs/templates/docpage/sidebar/sidebar.py @@ -2,10 +2,10 @@ from __future__ import annotations +import reflex as rx import reflex_ui as ui from reflex_ui_shared.styles.colors import c_color -import reflex as rx from reflex_docs.templates.docpage.state import NavbarState from .sidebar_items.ai import ( diff --git a/docs/app/reflex_docs/views/bottom_section/get_started.py b/docs/app/reflex_docs/views/bottom_section/get_started.py index e1705b8b997..941f259196c 100644 --- a/docs/app/reflex_docs/views/bottom_section/get_started.py +++ b/docs/app/reflex_docs/views/bottom_section/get_started.py @@ -1,8 +1,8 @@ +import reflex as rx import reflex_ui as ui -from reflex_docs.components.hint import hint from reflex_ui_shared.components.icons import get_icon -import reflex as rx +from reflex_docs.components.hint import hint def code_block() -> rx.Component: diff --git a/docs/app/reflex_docs/views/bottom_section/newsletter.py b/docs/app/reflex_docs/views/bottom_section/newsletter.py index 6b0cd7bf848..30464e88580 100644 --- a/docs/app/reflex_docs/views/bottom_section/newsletter.py +++ b/docs/app/reflex_docs/views/bottom_section/newsletter.py @@ -1,7 +1,7 @@ -from reflex_docs.components.button import button +import reflex as rx from reflex_ui_shared.backend.signup import IndexState -import reflex as rx +from reflex_docs.components.button import button def newsletter_input() -> rx.Component: diff --git a/docs/app/reflex_docs/views/docs_navbar.py b/docs/app/reflex_docs/views/docs_navbar.py index 074a981b334..4718bde7ddf 100644 --- a/docs/app/reflex_docs/views/docs_navbar.py +++ b/docs/app/reflex_docs/views/docs_navbar.py @@ -1,12 +1,12 @@ +import reflex as rx import reflex_ui as ui -from reflex_docs.components.docpage.navbar.buttons.sidebar import navbar_sidebar_button -from reflex_docs.pages.docs import ai_builder, getting_started, hosting -from reflex_docs.views.search import search_bar from reflex_ui.blocks.demo_form import demo_form_dialog from reflex_ui_shared.components.marketing_button import button from reflex_ui_shared.constants import REFLEX_ASSETS_CDN -import reflex as rx +from reflex_docs.components.docpage.navbar.buttons.sidebar import navbar_sidebar_button +from reflex_docs.pages.docs import ai_builder, getting_started, hosting +from reflex_docs.views.search import search_bar def logo() -> rx.Component: diff --git a/docs/app/tests/conftest.py b/docs/app/tests/conftest.py index 2214bdb332d..8d143eb7b05 100644 --- a/docs/app/tests/conftest.py +++ b/docs/app/tests/conftest.py @@ -2,7 +2,6 @@ from pathlib import Path import pytest - from reflex.testing import AppHarness # Add tests directory to Python path for absolute imports diff --git a/docs/library/tables-and-data-grids/table.md b/docs/library/tables-and-data-grids/table.md index 26f8bc5bf3a..828522797be 100644 --- a/docs/library/tables-and-data-grids/table.md +++ b/docs/library/tables-and-data-grids/table.md @@ -489,7 +489,7 @@ class Customer(rx.Model, table=True): ``` ```python exec -class DatabaseTableState(rx.State): +class LoadingDataTableState(rx.State): users: list[dict] = [] @rx.event @@ -543,8 +543,8 @@ def loading_data_table_example(): rx.table.column_header_cell("Address"), ), ), - rx.table.body(rx.foreach(DatabaseTableState.users, show_customer)), - on_mount=DatabaseTableState.load_entries, + rx.table.body(rx.foreach(LoadingDataTableState.users, show_customer)), + on_mount=LoadingDataTableState.load_entries, width="100%", margin_bottom="1em", ) diff --git a/packages/reflex-ui-shared/pyproject.toml b/packages/reflex-ui-shared/pyproject.toml index 7f3fbb74b74..75f819619c1 100644 --- a/packages/reflex-ui-shared/pyproject.toml +++ b/packages/reflex-ui-shared/pyproject.toml @@ -32,8 +32,6 @@ dependencies = [ "reflex-components-sonner", "httpx", "email-validator", - "flexdown", - "mistletoe", "ruff-format", ] diff --git a/packages/reflex-ui-shared/src/reflex_ui_shared/components/blocks/flexdown.py b/packages/reflex-ui-shared/src/reflex_ui_shared/components/blocks/flexdown.py index f70c08625a8..e3ddc64c8b9 100644 --- a/packages/reflex-ui-shared/src/reflex_ui_shared/components/blocks/flexdown.py +++ b/packages/reflex-ui-shared/src/reflex_ui_shared/components/blocks/flexdown.py @@ -1,8 +1,6 @@ -"""Flexdown module.""" +"""Flexdown module — component maps and markdown helpers.""" # pyright: reportAttributeAccessIssue=false -import flexdown -import reflex_ui as ui from reflex_base.constants.colors import ColorType import reflex as rx @@ -10,8 +8,6 @@ code_block_markdown, code_block_markdown_dark, ) -from reflex_ui_shared.components.blocks.collapsible import collapsible_box -from reflex_ui_shared.components.blocks.demo import docdemo, docdemobox, docgraphing from reflex_ui_shared.components.blocks.headings import ( h1_comp_xd, h2_comp_xd, @@ -21,15 +17,12 @@ ) from reflex_ui_shared.components.blocks.typography import ( code_comp, - definition, doclink2, list_comp, ordered_list_comp, text_comp, unordered_list_comp, ) -from reflex_ui_shared.constants import REFLEX_ASSETS_CDN -from reflex_ui_shared.styles.colors import c_color from reflex_ui_shared.styles.fonts import base, code @@ -52,577 +45,6 @@ def get_code_style(color: ColorType): } -class AlertBlock(flexdown.blocks.MarkdownBlock): - """A block that displays a component along with its code.""" - - starting_indicator = "```md alert" - ending_indicator = "```" - - include_indicators = True - - def render(self, env: dict) -> rx.Component: - """Render. - - Returns: - The component. - """ - lines = self.get_lines(env) - - args = lines[0].removeprefix(self.starting_indicator).split() - - if len(args) == 0: - args = ["info"] - status = args[0] - - if lines[1].startswith("#"): - title = lines[1].strip("#").strip() - content = "\n".join(lines[2:-1]) - else: - title = "" - content = "\n".join(lines[1:-1]) - - colors: dict[str, ColorType] = { - "info": "accent", - "success": "grass", - "warning": "amber", - "error": "red", - } - - color: ColorType = colors.get(status, "blue") - - has_content = bool(content.strip()) - - icon = rx.box( - rx.match( - status, - ("info", rx.icon(tag="info", size=18, margin_right=".5em")), - ("success", rx.icon(tag="circle_check", size=18, margin_right=".5em")), - ( - "warning", - rx.icon(tag="triangle_alert", size=18, margin_right=".5em"), - ), - ("error", rx.icon(tag="ban", size=18, margin_right=".5em")), - ), - color=f"{rx.color(color, 11)}", - ) - title_comp = ( - markdown_with_shiki( - title, - margin_y="0px", - style=get_code_style(color), - ) - if title - else self.render_fn(content=content) - ) - - if has_content: - body = ( - rx.accordion.content( - markdown(content), padding="0px", margin_top="16px" - ) - if title - else rx.fragment() - ) - return collapsible_box([icon, title_comp], body, color) - - return rx.vstack( - rx.hstack( - icon, - markdown_with_shiki( - title, - color=f"{rx.color(color, 11)}", - margin_y="0px", - style=get_code_style(color), - ), - align_items="center", - width="100%", - spacing="1", - padding=["16px", "24px"], - ), - border=f"1px solid {rx.color(color, 4)}", - background_color=f"{rx.color(color, 3)}", - border_radius="12px", - margin_bottom="16px", - margin_top="16px", - width="100%", - ) - - -class SectionBlock(flexdown.blocks.Block): - """A block that displays a component along with its code.""" - - starting_indicator = "```md section" - ending_indicator = "```" - - def render(self, env: dict) -> rx.Component: - """Render. - - Returns: - The component. - """ - lines = self.get_lines(env) - - # Split up content into sections based on markdown headers. - header_indices = [i for i, line in enumerate(lines) if line.startswith("#")] - header_indices.append(len(lines)) - sections = [ - ( - lines[header_indices[i]].strip("#"), - "\n".join(lines[header_indices[i] + 1 : header_indices[i + 1]]), - ) - for i in range(len(header_indices) - 1) - ] - - return rx.box( - rx.vstack( - *[ - rx.fragment( - rx.text( - rx.text.span( - header, - font_weight="bold", - ), - width="100%", - ), - rx.box( - markdown(section), - width="100%", - ), - ) - for header, section in sections - ], - text_align="left", - margin_y="1em", - width="100%", - ), - border_left=f"1.5px {c_color('slate', 4)} solid", - padding_left="1em", - width="100%", - align_items="center", - ) - - -class DefinitionBlock(flexdown.blocks.Block): - """DefinitionBlock.""" - - starting_indicator = "```md definition" - ending_indicator = "```" - - def render(self, env: dict) -> rx.Component: - """Render. - - Returns: - The component. - """ - lines = self.get_lines(env) - - # Split up content into sections based on markdown headers. - header_indices = [i for i, line in enumerate(lines) if line.startswith("#")] - header_indices.append(len(lines)) - sections = [ - ( - lines[header_indices[i]].removeprefix("#"), - "\n".join(lines[header_indices[i] + 1 : header_indices[i + 1]]), - ) - for i in range(len(header_indices) - 1) - ] - - defs = [definition(title, content) for title, content in sections] - - return rx.fragment( - rx.mobile_only(rx.vstack(*defs)), - rx.tablet_and_desktop( - rx.grid( - *[rx.box(d) for d in defs], - columns="2", - width="100%", - gap="1rem", - margin_bottom="1em", - ) - ), - ) - - -class DemoOnly(flexdown.blocks.Block): - """A block that displays only a component demo without showing the code.""" - - starting_indicator = "```python demo-only" - ending_indicator = "```" - include_indicators = True - theme: str | None = None - - def render(self, env: dict) -> rx.Component: - """Render. - - Returns: - The component. - """ - lines = self.get_lines(env) - code = "\n".join(lines[1:-1]) - - args = lines[0].removeprefix(self.starting_indicator).split() - - exec_mode = env.get("__exec", False) - comp: rx.Component = rx.fragment() - - for arg in args: - if arg.startswith("id="): - comp_id = arg.rsplit("id=")[-1] - break - else: - comp_id = None - - if "exec" in args: - env["__xd"].exec(code, env, self.filename) - if not exec_mode: - comp = env[list(env.keys())[-1]]() - elif "graphing" in args: - env["__xd"].exec(code, env, self.filename) - if not exec_mode: - comp = env[list(env.keys())[-1]]() - # Get all the code before the final "def". - parts = code.rpartition("def") - data, code = parts[0], parts[1] + parts[2] - return docgraphing(code, comp=comp, data=data) - elif exec_mode: - return comp - elif "box" in args: - comp = eval(code, env, env) - return rx.box(comp, margin_bottom="1em", id=comp_id) - else: - comp = eval(code, env, env) - - # Return only the component without any code display - return rx.box(comp, margin_bottom="1em", id=comp_id) - - -class DemoBlock(flexdown.blocks.Block): - """A block that displays a component along with its code.""" - - starting_indicator = "```python demo" - ending_indicator = "```" - include_indicators = True - theme: str | None = None - - def render(self, env: dict) -> rx.Component: - """Render. - - Returns: - The component. - """ - lines = self.get_lines(env) - code = "\n".join(lines[1:-1]) - - args = lines[0].removeprefix(self.starting_indicator).split() - - exec_mode = env.get("__exec", False) - comp: rx.Component = rx.fragment() - - for arg in args: - if arg.startswith("id="): - comp_id = arg.rsplit("id=")[-1] - break - else: - comp_id = None - - if "exec" in args: - env["__xd"].exec(code, env, self.filename) - if not exec_mode: - comp = env[list(env.keys())[-1]]() - elif "graphing" in args: - env["__xd"].exec(code, env, self.filename) - if not exec_mode: - comp = env[list(env.keys())[-1]]() - # Get all the code before the final "def". - parts = code.rpartition("def") - data, code = parts[0], parts[1] + parts[2] - return docgraphing(code, comp=comp, data=data) - elif exec_mode: - return comp - elif "box" in args: - comp = eval(code, env, env) - return rx.box(docdemobox(comp), margin_bottom="1em", id=comp_id) - else: - comp = eval(code, env, env) - - # Sweep up additional CSS-like props to apply to the demobox itself - demobox_props = {} - for arg in args: - prop, equals, value = arg.partition("=") - if equals: - demobox_props[prop] = value - - if "toggle" in args: - demobox_props["toggle"] = True - - return docdemo( - code, comp=comp, demobox_props=demobox_props, theme=self.theme, id=comp_id - ) - - -class DemoBlockDark(DemoBlock): - """DemoBlockDark.""" - - theme = "dark" - - -class DemoBlockNestedMarkdown(DemoBlock): - """Used when the block contains literal markdown with triple backticks.""" - - starting_indicator = "````python demo" - ending_indicator = "````" - - -class DemoBlockNestedMarkdownDark(DemoBlockNestedMarkdown): - """DemoBlockNestedMarkdownDark.""" - - theme = "dark" - - -class VideoBlock(flexdown.blocks.MarkdownBlock): - """A block that displays a video.""" - - starting_indicator = "```md video" - ending_indicator = "```" - - include_indicators = True - - def render(self, env: dict) -> rx.Component: - """Render. - - Returns: - The component. - """ - lines = self.get_lines(env) - - args = lines[0].removeprefix(self.starting_indicator).split() - - if len(args) == 0: - args = ["info"] - url = args[0] - - title = lines[1].strip("#").strip() if lines[1].startswith("#") else "" - - color: ColorType = "blue" - - trigger = [ - markdown_with_shiki( - title or "Video Description", - margin_y="0px", - style=get_code_style(color), - ), - ] - body = rx.accordion.content( - rx.video( - src=url, - width="100%", - height="500px", - border_radius="10px", - overflow="hidden", - ), - margin_top="16px", - padding="0px", - ) - return collapsible_box(trigger, body, color, item_border_radius="0px") - - -class QuoteBlock(flexdown.blocks.MarkdownBlock): - """A block that displays a quote.""" - - starting_indicator = "```md quote" - ending_indicator = "```" - - include_indicators = True - - def _parse(self, env: dict) -> dict[str, str]: - lines = self.get_lines(env) - quote_content = [] - data = { - "name": "", - "role": "", - "image": "", - "variant": "small", - } - - for line in lines[1:-1]: # Skip the first and last lines (indicators) - if line.startswith("- name:"): - data["name"] = line.split(":", 1)[1].strip() - elif line.startswith("- role:"): - data["role"] = line.split(":", 1)[1].strip() - elif line.startswith("- image:"): - data["image"] = line.split(":", 1)[1].strip() - elif line.startswith("- variant:"): - data["variant"] = line.split(":", 1)[1].strip().lower() - else: - quote_content.append(line) - - data["quote_text"] = "\n".join(quote_content).strip() - return data - - def _author(self, name: str, role: str, class_name: str = "") -> rx.Component: - return rx.el.div( - rx.el.span( - name, - class_name="text-xs font-mono uppercase font-[415] text-secondary-12", - ), - rx.el.span( - role, - class_name="text-xs font-mono font-[415] text-secondary-11 uppercase", - ), - class_name=ui.cn("flex flex-col gap-0.5", class_name), - ) - - def _avatar( - self, name: str, image: str, class_name: str = "" - ) -> rx.Component | None: - if not image: - return None - avatar_class = ui.cn("rounded-full object-cover aspect-square", class_name) - return rx.image( - src=f"{REFLEX_ASSETS_CDN}case_studies/people/{image}", - alt=f"{name} profile picture", - class_name=avatar_class, - ) - - def _render_medium(self, data: dict[str, str]) -> rx.Component: - return rx.el.div( - rx.el.div( - self._avatar(data["name"], data["image"], class_name="size-6"), - class_name="p-4 shrink-0 lg:border-r border-secondary-8 border-dashed max-lg:border-b bg-secondary-1", - ), - rx.el.span( - f'"{data["quote_text"]}"', - class_name="text-secondary-12 text-base font-[575] p-4 bg-white-1 w-full", - ), - class_name="flex lg:flex-row flex-col border border-dashed border-secondary-8 mt-2 mb-6 rounded-lg overflow-hidden box-border bg-white-1", - ) - - def _render_small(self, data: dict[str, str]) -> rx.Component: - return rx.el.div( - rx.el.span( - f'"{data["quote_text"]}"', - class_name="text-secondary-12 text-lg font-[575] p-6 lg:border-r border-secondary-8 border-dashed max-lg:border-b bg-white-1", - ), - rx.el.div( - rx.el.div( - self._author(data["name"], data["role"]), - class_name="text-end text-nowrap", - ), - self._avatar(data["name"], data["image"], class_name="size-14"), - class_name="flex flex-row gap-6 items-center p-6 shrink-0 bg-secondary-1", - ), - class_name="flex lg:flex-row flex-col border border-dashed border-secondary-8 mt-2 mb-6 rounded-lg overflow-hidden box-border bg-white-1", - ) - - def _render_big(self, data: dict[str, str]) -> rx.Component: - return rx.el.div( - rx.el.div( - rx.el.span( - f"{data['quote_text']}", - class_name="text-secondary-12 text-2xl font-[575]", - ), - rx.el.div( - self._avatar(data["name"], data["image"], class_name="size-6"), - self._author( - data["name"], - data["role"], - class_name="flex-row gap-3.5 items-center", - ), - class_name="flex flex-row gap-3.5 items-center", - ), - class_name="flex flex-col gap-12 pr-[12.5rem] relative z-10", - ), - rx.image( - src=f"{REFLEX_ASSETS_CDN}common/{rx.color_mode_cond('light', 'dark')}/quote_squares.svg", - loading="lazy", - alt="Quote icon", - class_name="absolute right-0 inset-y-0 h-[calc(100%)] min-h-full w-auto origin-right pointer-events-none object-contain object-right", - ), - class_name="flex flex-col dark:border bg-white-1 dark:border-secondary-4 mt-2 mb-6 overflow-hidden shadow-[0_0_0_1px_rgba(0,0,0,0.12)_inset,0_6px_12px_0_rgba(0,0,0,0.06),0_1px_1px_0_rgba(0,0,0,0.01),0_4px_6px_0_rgba(0,0,0,0.02)] rounded-xl py-8 px-8 relative", - ) - - def render(self, env: dict) -> rx.Component: - """Render. - - Returns: - The component. - """ - data = self._parse(env) - renderers = { - "small": self._render_small, - "medium": self._render_medium, - "big": self._render_big, - } - renderer = renderers.get(data["variant"], self._render_small) - return renderer(data) - - -class TabsBlock(flexdown.blocks.Block): - """A block that displays content in tabs.""" - - starting_indicator = "---md tabs" - ending_indicator = "---" - - def render(self, env: dict) -> rx.Component: - """Render. - - Returns: - The component. - """ - lines = self.get_lines(env) - - tab_sections = [] - current_section = [] - current_title = "" - - for line in lines[1:-1]: # Skip the first and last lines (indicators) - stripped_line = line.strip() - - if stripped_line.startswith("--tab "): - if current_title: - tab_sections.append((current_title, "\n".join(current_section))) - current_title = stripped_line[6:].strip() - current_section = [] - elif stripped_line == "--": - if current_title: - tab_sections.append((current_title, "\n".join(current_section))) - current_title = "" - current_section = [] - else: - current_section.append(line) - - # Add the last section if there's content - if current_title and current_section: - tab_sections.append((current_title, "\n".join(current_section))) - - # Create tab components - triggers = [] - contents = [] - - for i, (title, content) in enumerate(tab_sections): - value = f"tab{i + 1}" - triggers.append( - rx.tabs.trigger( - title, - value=value, - class_name="tab-style font-base font-semibold text-[1.25rem]", - ) - ) - - # Render the tab content - tab_content = [] - for block in env["__xd"].get_blocks(content, self.filename): - if isinstance(block, flexdown.blocks.MarkdownBlock): - block.render_fn = env["__xd"].flexdown_memo - tab_content.append(block.render(env=env)) - - contents.append(rx.tabs.content(rx.fragment(*tab_content), value=value)) - - return rx.tabs.root( - rx.tabs.list(*triggers, class_name="mt-4"), *contents, default_value="tab1" - ) - - def _markdown_table(*children, **props) -> rx.Component: return rx.box( rx.el.table( @@ -702,44 +124,13 @@ def _markdown_td(*children, **props) -> rx.Component: comp2["ol"] = lambda items: ordered_list_comp(items=items) -xd = flexdown.Flexdown( - block_types=[ - DemoOnly, - DemoBlock, - DemoBlockNestedMarkdown, - AlertBlock, - DefinitionBlock, - SectionBlock, - VideoBlock, - TabsBlock, - QuoteBlock, - ], - component_map=component_map, -) -xd.clear_modules() -xd2 = flexdown.Flexdown( - block_types=[ - DemoBlockDark, - DemoBlockNestedMarkdownDark, - AlertBlock, - DefinitionBlock, - SectionBlock, - VideoBlock, - TabsBlock, - QuoteBlock, - ], - component_map=comp2, -) -xd2.clear_modules() - - def markdown(text: str): """Markdown. Returns: The component. """ - return xd.get_default_block().render_fn(content=text) + return rx.markdown(text, component_map=component_map) def markdown_codeblock(value: str, **props: object) -> rx.Component: diff --git a/packages/reflex-ui-shared/src/reflex_ui_shared/components/docs.py b/packages/reflex-ui-shared/src/reflex_ui_shared/components/docs.py index 6b940ab0c65..cbc127c9078 100644 --- a/packages/reflex-ui-shared/src/reflex_ui_shared/components/docs.py +++ b/packages/reflex-ui-shared/src/reflex_ui_shared/components/docs.py @@ -1,10 +1,5 @@ """Template for documentation pages.""" -from typing import Any - -import flexdown -import mistletoe - from .blocks import * @@ -125,76 +120,3 @@ def right_sidebar_item_highlight(): // Run the function when the page loads setupTableOfContentsHighlight(); """ - - -def get_headings(comp: Any): - """Get the strings from markdown component. - - Returns: - The component. - """ - if isinstance(comp, mistletoe.block_token.Heading): - heading_text = "".join( - token.content for token in comp.children if hasattr(token, "content") - ) - return [(comp.level, heading_text)] - - # Recursively get the strings from the children. - if not hasattr(comp, "children") or comp.children is None: - return [] - - headings = [] - for child in comp.children: - headings.extend(get_headings(child)) - return headings - - -def get_toc(source: Any, href: str, component_list: list | None = None): - """Get toc. - - Returns: - The component. - """ - from reflex_ui_shared.flexdown import xd - - component_list = component_list or [] - component_list = component_list[1:] - - # Generate the TOC - # The environment used for execing and evaling code. - from reflex_ui_shared.constants import REFLEX_ASSETS_CDN - - env = source.metadata - env["__xd"] = xd - env["REFLEX_ASSETS_CDN"] = REFLEX_ASSETS_CDN - - # Get the content of the document. - doc_content = source.content - - # Get the blocks in the source code. - # Note: we must use reflex-web's special flexdown instance xd here - it knows about all custom block types (like DemoBlock) - blocks = xd.get_blocks(doc_content, href) - - content_pieces = [] - for block in blocks: - if ( - not isinstance(block, flexdown.blocks.MarkdownBlock) - or len(block.lines) == 0 - or not block.lines[0].startswith("#") - ): - continue - # Now we should have all the env entries we need - content = block.get_content(env) - content_pieces.append(content) - - content = "\n".join(content_pieces) - doc = mistletoe.Document(content) - - # Parse the markdown headers. - headings = get_headings(doc) - - if len(component_list): - headings.append((1, "API Reference")) - for component_tuple in component_list: - headings.append((2, component_tuple[1])) - return headings, doc_content diff --git a/packages/reflex-ui-shared/src/reflex_ui_shared/gallery/apps.py b/packages/reflex-ui-shared/src/reflex_ui_shared/gallery/apps.py index 0c911a1a34b..691e12a26e3 100644 --- a/packages/reflex-ui-shared/src/reflex_ui_shared/gallery/apps.py +++ b/packages/reflex-ui-shared/src/reflex_ui_shared/gallery/apps.py @@ -3,15 +3,15 @@ import copy import re -import flexdown import reflex_ui as ui from reflex_ui.blocks.demo_form import demo_form_dialog import reflex as rx -from reflex_ui_shared.components.blocks.flexdown import xd +from reflex_ui_shared.components.blocks.flexdown import markdown from reflex_ui_shared.components.code_card import gallery_app_card from reflex_ui_shared.components.icons import get_icon from reflex_ui_shared.constants import REFLEX_ASSETS_CDN, SCREENSHOT_BUCKET +from reflex_ui_shared.gallery.common import MarkdownDocument from reflex_ui_shared.gallery.gallery import integrations_stack from reflex_ui_shared.templates.gallery_app_page import gallery_app_page @@ -54,11 +54,13 @@ def load_all_gallery_apps(): Returns: The component. """ - gallery_apps = {} + from reflex_ui_shared.utils.md import MarkdownDocument, get_md_files + + gallery_apps: dict[tuple[str, str], MarkdownDocument] = {} for folder, _ in GALLERY_APP_SOURCES: - paths = flexdown.utils.get_flexdown_files(folder) + paths = get_md_files(folder) for path in sorted(paths, reverse=True): - document = flexdown.Document.from_file(path) # This has metadata + document = MarkdownDocument.from_file(path) document.metadata["title"] = document.metadata.get("title", "Untitled") clean_path = str(path).replace(".md", "/") gallery_apps[clean_path, folder] = document @@ -136,7 +138,7 @@ def more_posts(current_post: dict) -> rx.Component: ) -def page(document: flexdown.Document, is_reflex_template: bool) -> rx.Component: +def page(document: MarkdownDocument, is_reflex_template: bool) -> rx.Component: """Render a detailed app page based on source type. Returns: @@ -264,7 +266,7 @@ def page(document: flexdown.Document, is_reflex_template: bool) -> rx.Component: class_name="grid grid-cols-1 lg:grid-cols-2 divide-y lg:divide-y-0 lg:divide-x divide-slate-3 border-b border-slate-3", ), rx.box( - xd.render(document, "blog.md"), + markdown(document.content), class_name="flex flex-col gap-4 w-full p-8", ), more_posts(meta) if not is_reflex_template else rx.fragment(), diff --git a/packages/reflex-ui-shared/src/reflex_ui_shared/gallery/common.py b/packages/reflex-ui-shared/src/reflex_ui_shared/gallery/common.py index 78dc3cb512f..52f8d74f0b0 100644 --- a/packages/reflex-ui-shared/src/reflex_ui_shared/gallery/common.py +++ b/packages/reflex-ui-shared/src/reflex_ui_shared/gallery/common.py @@ -2,12 +2,12 @@ import re -import flexdown import reflex_ui as ui import reflex as rx from reflex_ui_shared.constants import INTEGRATIONS_IMAGES_URL, REFLEX_ASSETS_CDN from reflex_ui_shared.gallery.r_svg_loader import r_svg_loader +from reflex_ui_shared.utils.md import MarkdownDocument, get_md_files REFLEX_BUILD_TEMPLATES_PATH = "reflex_build_templates/" REFLEX_BUILD_TEMPLATES_IMAGES = "reflex_build_template_images/" @@ -21,13 +21,13 @@ def get_templatey_apps(paths: list): """ gallery_apps = {} for path in sorted(paths, reverse=True): - document = flexdown.Document.from_file(path) # This has metadata + document = MarkdownDocument.from_file(path) key = str(path).replace(".md", "/") gallery_apps[key] = document return gallery_apps -paths = flexdown.utils.get_flexdown_files(REFLEX_BUILD_TEMPLATES_PATH) +paths = get_md_files(REFLEX_BUILD_TEMPLATES_PATH) template_apps_data = get_templatey_apps(paths) diff --git a/packages/reflex-ui-shared/src/reflex_ui_shared/gallery/gallery.py b/packages/reflex-ui-shared/src/reflex_ui_shared/gallery/gallery.py index 4b6e95e8bed..10fd855acc6 100644 --- a/packages/reflex-ui-shared/src/reflex_ui_shared/gallery/gallery.py +++ b/packages/reflex-ui-shared/src/reflex_ui_shared/gallery/gallery.py @@ -2,13 +2,13 @@ import re -import flexdown import reflex_ui as ui import reflex as rx from reflex_ui_shared.constants import INTEGRATIONS_IMAGES_URL, REFLEX_ASSETS_CDN from reflex_ui_shared.gallery.r_svg_loader import r_svg_loader from reflex_ui_shared.templates.webpage import webpage +from reflex_ui_shared.utils.md import MarkdownDocument, get_md_files REFLEX_BUILD_TEMPLATES_PATH = "reflex_build_templates/" REFLEX_BUILD_TEMPLATES_IMAGES = "reflex_build_template_images/" @@ -22,13 +22,13 @@ def get_templatey_apps(paths: list): """ gallery_apps = {} for path in sorted(paths, reverse=True): - document = flexdown.Document.from_file(path) # This has metadata + document = MarkdownDocument.from_file(path) key = str(path).replace(".md", "/") gallery_apps[key] = document return gallery_apps -paths = flexdown.utils.get_flexdown_files(REFLEX_BUILD_TEMPLATES_PATH) +paths = get_md_files(REFLEX_BUILD_TEMPLATES_PATH) template_apps_data = get_templatey_apps(paths) diff --git a/packages/reflex-ui-shared/src/reflex_ui_shared/utils/docpage.py b/packages/reflex-ui-shared/src/reflex_ui_shared/utils/docpage.py index 9e2b8bfb59a..f997566670f 100644 --- a/packages/reflex-ui-shared/src/reflex_ui_shared/utils/docpage.py +++ b/packages/reflex-ui-shared/src/reflex_ui_shared/utils/docpage.py @@ -1,7 +1,4 @@ -"""Docpage utilities: TOC generation and sidebar highlight.""" - -import flexdown -import mistletoe +"""Docpage utilities: sidebar highlight.""" def right_sidebar_item_highlight(): @@ -121,66 +118,3 @@ def right_sidebar_item_highlight(): // Run the function when the page loads setupTableOfContentsHighlight(); """ - - -def get_headings(comp: mistletoe.block_token.BlockToken): - """Get the strings from markdown component. - - Returns: - The component. - """ - if isinstance(comp, mistletoe.block_token.Heading): - heading_text = "".join( - token.content for token in comp.children if hasattr(token, "content") - ) - return [(comp.level, heading_text)] - - if not hasattr(comp, "children") or comp.children is None: - return [] - - headings = [] - for child in comp.children: - headings.extend(get_headings(child)) - return headings - - -def get_toc(source: flexdown.Document, href: str, component_list: list | None = None): - """Get toc. - - Returns: - The component. - """ - from reflex_ui_shared.components.blocks.flexdown import xd - from reflex_ui_shared.constants import REFLEX_ASSETS_CDN - - component_list = component_list or [] - component_list = component_list[1:] - - env = source.metadata - env["__xd"] = xd - env["REFLEX_ASSETS_CDN"] = REFLEX_ASSETS_CDN - - doc_content = source.content - blocks = xd.get_blocks(doc_content, href) - - content_pieces = [] - for block in blocks: - if ( - not isinstance(block, flexdown.blocks.MarkdownBlock) - or len(block.lines) == 0 - or not block.lines[0].startswith("#") - ): - continue - content = block.get_content(env) - content_pieces.append(content) - - content = "\n".join(content_pieces) - doc = mistletoe.Document(content) - - headings = get_headings(doc) - - if len(component_list): - headings.append((1, "API Reference")) - for component_tuple in component_list: - headings.append((2, component_tuple[1])) - return headings, doc_content diff --git a/packages/reflex-ui-shared/src/reflex_ui_shared/utils/md.py b/packages/reflex-ui-shared/src/reflex_ui_shared/utils/md.py new file mode 100644 index 00000000000..6a777bb961e --- /dev/null +++ b/packages/reflex-ui-shared/src/reflex_ui_shared/utils/md.py @@ -0,0 +1,52 @@ +"""Markdown document utilities — YAML frontmatter parsing and file discovery.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +import yaml + +_FRONT_MATTER_RE = re.compile(r"^---\s*\n(.+?)\n---\s*\n(.*)$", re.DOTALL) + + +@dataclass(kw_only=True) +class MarkdownDocument: + """A markdown document with YAML frontmatter.""" + + metadata: dict[str, Any] = field(default_factory=dict) + content: str + + @classmethod + def from_source(cls, source: str) -> MarkdownDocument: + """Parse a markdown source string with optional YAML frontmatter. + + Returns: + The parsed document. + """ + match = re.match(_FRONT_MATTER_RE, source) + if not match: + return cls(content=source) + front_matter = yaml.safe_load(match.group(1)) or {} + return cls(metadata=front_matter, content=match.group(2)) + + @classmethod + def from_file(cls, path: str | Path) -> MarkdownDocument: + """Load a markdown document from a file. + + Returns: + The parsed document. + """ + text = Path(path).read_text(encoding="utf-8") + return cls.from_source(text) + + +def get_md_files(directory: str | Path) -> list[Path]: + """Recursively find all .md files in a directory. + + Returns: + Sorted list of .md file paths. + """ + return sorted(Path(directory).rglob("*.md")) diff --git a/uv.lock b/uv.lock index 5cbdebe87e1..035b35344e5 100644 --- a/uv.lock +++ b/uv.lock @@ -950,21 +950,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, ] -[[package]] -name = "flexdown" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mistletoe" }, - { name = "pyyaml" }, - { name = "reflex" }, - { name = "ruff-format" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d4/72/1468206a948500ab05443ff3b289fc86effc75bfc083e4a9eeb55fd91973/flexdown-0.3.0.tar.gz", hash = "sha256:7bb818e9772926b42f9cbd70a3319e848a8f9eb8e4b56d1a65a1d8cb3f360d88", size = 101531, upload-time = "2026-03-27T17:25:02.27Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/c3/0752c56ccbdb270f13fe9f89fd3cb68b2e4d8a3b39b09f15a476b2130158/flexdown-0.3.0-py3-none-any.whl", hash = "sha256:174e0ab611f6c7fdc61e84cc35b3c29971f23d86931ac9a14917ab797977dc71", size = 25304, upload-time = "2026-03-27T17:25:01.4Z" }, -] - [[package]] name = "fonttools" version = "4.62.1" @@ -3741,9 +3726,7 @@ dependencies = [ { name = "alembic" }, { name = "email-validator" }, { name = "fastapi" }, - { name = "flexdown" }, { name = "googletrans-py" }, - { name = "mistletoe" }, { name = "openai" }, { name = "orjson" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -3781,9 +3764,7 @@ requires-dist = [ { name = "alembic" }, { name = "email-validator" }, { name = "fastapi" }, - { name = "flexdown" }, { name = "googletrans-py" }, - { name = "mistletoe" }, { name = "openai" }, { name = "orjson" }, { name = "pandas" },