diff --git a/.gitignore b/.gitignore index c5d7e5a..b2ec6f6 100644 --- a/.gitignore +++ b/.gitignore @@ -72,6 +72,7 @@ instance/ # pdoc documentation /doc +/docusaurus # PyBuilder .pybuilder/ diff --git a/docker-compose-test.yaml b/docker-compose-test.yaml index 0452d2f..a8e8136 100644 --- a/docker-compose-test.yaml +++ b/docker-compose-test.yaml @@ -1,6 +1,6 @@ services: fishjam: - image: "ghcr.io/fishjam-cloud/fishjam:${TAG:-edge}" + image: "ghcr.io/fishjam-cloud/fishjam:${TAG:-edge-88258de-1764324482}" container_name: fishjam restart: on-failure healthcheck: diff --git a/pyproject.toml b/pyproject.toml index bd5bc29..7b65907 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ format_check = "scripts:run_format_check" lint = "scripts:run_linter" fix_lint = "scripts:run_linter_fix" generate_docs = "scripts:generate_docs" +generate_docusaurus = "scripts:generate_docusaurus" update_client = "scripts:update_client" room_manager = "scripts:start_room_manager" diff --git a/scripts.py b/scripts.py index ac4ed57..e9a265e 100644 --- a/scripts.py +++ b/scripts.py @@ -1,4 +1,5 @@ import os +import re import shutil import subprocess import sys @@ -55,6 +56,12 @@ def run_linter_fix(): def generate_docs(): + here = Path(__file__).parent + input = here / "doc" + + if input.exists(): + shutil.rmtree(input) + check_exit_code( "pdoc \ --include-undocumented \ @@ -64,8 +71,6 @@ def generate_docs(): -o doc \ fishjam" ) - here = Path(__file__).parent - input = here / "doc" input_images = here / "images" out = here / "docs" / "api" out_images = here / "docs" / "api" / "images" @@ -81,6 +86,72 @@ def generate_docs(): f.rename(f.with_suffix(".md")) +def clean_mdx_content(content: str) -> str: + parts = re.split(r"((?:```[\s\S]*?```|`[^`\n]+`))", content) + + # example: convert `fishjam._openapi_client.models.peer.Peer` into `Peer` + internal_path_pattern = r"fishjam\.(?:[\w.]+\.)?_[\w.]+\." + + cleaned_parts = [] + for part in parts: + if part.startswith("`"): + text = ( + part.replace("<", "<") + .replace(">", ">") + .replace("'", "'") + .replace("builtins.", "") + ) + + text = re.sub(internal_path_pattern, "", text) + + cleaned_parts.append(text) + else: + text = part.replace("{", "\\{").replace("}", "\\}").replace("<", "<") + + cleaned_parts.append(text) + + return "".join(cleaned_parts) + + +def generate_docusaurus(): + here = Path(__file__).parent + input = here / "doc" + out = here / "docusaurus" + + if input.exists(): + shutil.rmtree(input) + if out.exists(): + shutil.rmtree(out) + + check_exit_code( + "pdoc \ + --include-undocumented \ + -t templates/docusaurus \ + -o doc \ + fishjam" + ) + try: + os.remove(input / "index.html") + except FileNotFoundError: + pass + + out.mkdir(parents=True, exist_ok=True) + + for f in input.glob("**/*.html"): + content = f.read_text(encoding="utf-8") + safe_content = clean_mdx_content(content) + rel_path = f.relative_to(input) + dest_path = out / rel_path.with_suffix(".md") + dest_path.parent.mkdir(parents=True, exist_ok=True) + + dest_path.write_text(safe_content, encoding="utf-8") + + fishjam_dir = out / "fishjam" + submodules_dir = out / "submodules" + + fishjam_dir.rename(submodules_dir) + + def update_client(): if len(sys.argv) < 2: raise RuntimeError("Missing fishjam openapi.yaml raw url positional argument") diff --git a/templates/docusaurus/frame.html.jinja2 b/templates/docusaurus/frame.html.jinja2 new file mode 100644 index 0000000..a6fc348 --- /dev/null +++ b/templates/docusaurus/frame.html.jinja2 @@ -0,0 +1,7 @@ +--- +title: {{ module.name }} +sidebar_label: {{ module.name.split(".")[-1] }} +custom_edit_url: null +--- + +{% block content %}{% endblock %} \ No newline at end of file diff --git a/templates/docusaurus/module.html.jinja2 b/templates/docusaurus/module.html.jinja2 new file mode 100644 index 0000000..d5cb97c --- /dev/null +++ b/templates/docusaurus/module.html.jinja2 @@ -0,0 +1,144 @@ +{% extends "frame.html.jinja2" %} + +{% block content %} +# {{ module.name }} + +{% block module_info %} +{% if module.docstring and ".. include::" not in module.docstring %} +{{ module.docstring | safe }} +{% endif %} +{% endblock %} + +{% block nav_submodules %} +{% if module.submodules %} +## Submodules +{% for submodule in module.submodules if is_public(submodule) | trim %} +- [{{ submodule.name }}](submodules/{{ submodule.name }}) +{% endfor %} +{% endif %} +{% endblock %} + +{% block module_contents %} +{% for m in module.flattened_own_members if is_public(m) | trim %} +## {{ m.name }} +{{ member(m) }} +{% if m.kind == "class" %} +{% for cm in m.own_members if cm.kind != "class" and is_public(cm) | trim %} +### {{ cm.name }} +{{ member(cm) }} +{% endfor %} +{% set inherited_members = inherited(m) | trim %} +{% if inherited_members %} +#### Inherited Members +{{ inherited_members }} +{% endif %} +{% endif %} +--- +{% endfor %} +{% endblock %} +{% endblock content %} + +{# + ========================================================================= + HELPER MACROS (Markdown Version) + ========================================================================= +#} + +{% defaultmacro default_value(var) -%} +{% if var.default_value_str and "object object" not in var.default_value_str %} + = {{ var.default_value_str | safe }} +{% endif %} +{% enddefaultmacro %} + +{% defaultmacro annotation(var) %} +{% if var.annotation_str %}{{ var.annotation_str }}{% endif %} +{% enddefaultmacro %} + +{% defaultmacro function(fn) -%} +{% if fn.name == "__init__" %} +def {{ fn.qualname.split(".")[-1] }}{{ fn.signature_without_self | string | safe }} +{% else %} +def {{ fn.name }}{{ fn.signature | string | safe }} +{% endif %} +{% enddefaultmacro %} + +{% defaultmacro variable(var) -%} +{{ var.name }}{{ annotation(var) }}{{ default_value(var) }} +{% enddefaultmacro %} + +{% defaultmacro class_bases(cls) %} + {%- if cls.bases -%} + ( + {%- for base in cls.bases -%} + {%- if base is mapping or base is iterable and base is not string -%} + {{ base[-1] }} + {%- else -%} + {{ base }} + {%- endif -%} + {%- if loop.nextitem %}, {% endif %} + {%- endfor -%} + ) + {%- endif -%} +{% enddefaultmacro %} + +{% defaultmacro class(cls) -%} +class {{ cls.qualname }}{{ class_bases(cls) }}: +{% enddefaultmacro %} + +{% defaultmacro member(doc) %} +```python +{% if doc.kind == "class" %}{{ class(doc) }}{% elif doc.kind == "function" %}{{ function(doc) }}{% else %}{{ variable(doc) }}{% endif %} +``` +{{ doc.docstring | safe }} +{% enddefaultmacro %} + +{% defaultmacro is_public(doc) %} +{% if not include_undocumented and not doc.docstring %} +{% elif doc.docstring and "@private" in doc.docstring %} +{% elif doc.name == "__init__" and (doc.docstring or (doc.kind == "function" and doc.signature_without_self.parameters)) %} +true +{% elif doc.name == "__doc__" %} +{% elif doc.kind == "variable" and doc.is_typevar and not doc.docstring %} +{% elif doc.kind == "module" and doc.fullname not in all_modules %} +{% elif (doc.qualname or doc.name) is in(module.obj.__all__ or []) %} +true +{% elif not doc.name.startswith("_") %} +true +{% endif %} +{% enddefaultmacro %} + +{% defaultmacro inherited(cls) %} +{% set ignored_bases = ["str", "object", "int", "float", "bool", "list", "dict", "tuple", "set", "exception", "baseexception"] %} +{% for base, members in cls.inherited_members.items() %} + {% set base_name = base.name if base.name is defined else base %} + {% if base_name is mapping or base_name is iterable and base_name is not string %} + {% set base_name = base_name[-1] %} + {% endif %} + + {% if base_name | lower not in ignored_bases %} + + {% set member_list %} + {% for m in members if is_public(m) | trim %} + * `{{ m.name }}` + {% endfor %} + {% endset %} + + {% if member_list | trim %} +* **{{ base_name }}**: +{{ member_list }} + {% endif %} + + {% endif %} +{% endfor %} +{% enddefaultmacro %} + +{# Empty macros to prevent errors from unused calls in default logic #} +{% defaultmacro bases(cls) %}{% enddefaultmacro %} +{% defaultmacro decorators(doc) %}{% enddefaultmacro %} +{% defaultmacro submodule(mod) %}{% enddefaultmacro %} +{% defaultmacro docstring(var) %}{% enddefaultmacro %} +{% defaultmacro nav_members(members) %}{% enddefaultmacro %} +{% defaultmacro view_source_state(doc) %}{% enddefaultmacro %} +{% defaultmacro view_source_button(doc) %}{% enddefaultmacro %} +{% defaultmacro view_source_code(doc) %}{% enddefaultmacro %} +{% defaultmacro module_name() %}{% enddefaultmacro %} diff --git a/uv.lock b/uv.lock index 9403492..d6ac914 100644 --- a/uv.lock +++ b/uv.lock @@ -1893,6 +1893,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/fa/3234f913fe9a6525a7b97c6dad1f51e72b917e6872e051a5e2ffd8b16fbb/ruamel.yaml.clib-0.2.14-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:70eda7703b8126f5e52fcf276e6c0f40b0d314674f896fc58c47b0aef2b9ae83", size = 137970, upload-time = "2025-09-22T19:51:09.472Z" }, { url = "https://files.pythonhosted.org/packages/ef/ec/4edbf17ac2c87fa0845dd366ef8d5852b96eb58fcd65fc1ecf5fe27b4641/ruamel.yaml.clib-0.2.14-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a0cb71ccc6ef9ce36eecb6272c81afdc2f565950cdcec33ae8e6cd8f7fc86f27", size = 739639, upload-time = "2025-09-22T19:51:10.566Z" }, { url = "https://files.pythonhosted.org/packages/15/18/b0e1fafe59051de9e79cdd431863b03593ecfa8341c110affad7c8121efc/ruamel.yaml.clib-0.2.14-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7cb9ad1d525d40f7d87b6df7c0ff916a66bc52cb61b66ac1b2a16d0c1b07640", size = 764456, upload-time = "2025-09-22T19:51:11.736Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cd/150fdb96b8fab27fe08d8a59fe67554568727981806e6bc2677a16081ec7/ruamel_yaml_clib-0.2.14-cp314-cp314-win32.whl", hash = "sha256:9b4104bf43ca0cd4e6f738cb86326a3b2f6eef00f417bd1e7efb7bdffe74c539", size = 102394, upload-time = "2025-11-14T21:57:36.703Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e6/a3fa40084558c7e1dc9546385f22a93949c890a8b2e445b2ba43935f51da/ruamel_yaml_clib-0.2.14-cp314-cp314-win_amd64.whl", hash = "sha256:13997d7d354a9890ea1ec5937a219817464e5cc344805b37671562a401ca3008", size = 122673, upload-time = "2025-11-14T21:57:38.177Z" }, ] [[package]]