documentation_tools.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import importlib
  2. import re
  3. from pathlib import Path
  4. from typing import Callable, Optional, Union
  5. import docutils.core
  6. from nicegui import globals, ui
  7. from nicegui.elements.markdown import apply_tailwind
  8. from .demo import demo
  9. SPECIAL_CHARACTERS = re.compile('[^(a-z)(A-Z)(0-9)-]')
  10. def remove_indentation(text: str) -> str:
  11. """Remove indentation from a multi-line string based on the indentation of the first line."""
  12. lines = text.splitlines()
  13. while lines and not lines[0].strip():
  14. lines.pop(0)
  15. if not lines:
  16. return ''
  17. indentation = len(lines[0]) - len(lines[0].lstrip())
  18. return '\n'.join(line[indentation:] for line in lines)
  19. def pascal_to_snake(name: str) -> str:
  20. return re.sub(r'(?<!^)(?=[A-Z])', '_', name).lower()
  21. def create_anchor_name(text: str) -> str:
  22. return SPECIAL_CHARACTERS.sub('_', text).lower()
  23. def get_menu() -> ui.left_drawer:
  24. return [element for element in globals.get_client().elements.values() if isinstance(element, ui.left_drawer)][0]
  25. def heading(text: str, *, make_menu_entry: bool = True) -> None:
  26. ui.html(f'<em>{text}</em>').classes('mt-8 text-3xl font-weight-500')
  27. if make_menu_entry:
  28. with get_menu():
  29. ui.label(text).classes('font-bold mt-4')
  30. def subheading(text: str, *, make_menu_entry: bool = True) -> None:
  31. name = create_anchor_name(text)
  32. ui.html(f'<div id="{name}"></div>').style('position: relative; top: -90px')
  33. with ui.row().classes('gap-2 items-center'):
  34. ui.label(text).classes('text-2xl')
  35. with ui.link(target=f'#{name}'):
  36. ui.icon('link', size='sm').classes('text-gray-400 hover:text-gray-800')
  37. if make_menu_entry:
  38. with get_menu() as menu:
  39. async def click():
  40. if await ui.run_javascript(f'!!document.querySelector("div.q-drawer__backdrop")'):
  41. menu.hide()
  42. ui.open(f'#{name}')
  43. ui.link(text, target=f'#{name}').props('data-close-overlay').on('click', click)
  44. def markdown(text: str) -> None:
  45. ui.markdown(remove_indentation(text))
  46. class text_demo:
  47. def __init__(self, title: str, explanation: str) -> None:
  48. self.title = title
  49. self.explanation = explanation
  50. self.make_menu_entry = True
  51. def __call__(self, f: Callable) -> Callable:
  52. subheading(self.title, make_menu_entry=self.make_menu_entry)
  53. markdown(self.explanation)
  54. return demo()(f)
  55. class intro_demo(text_demo):
  56. def __init__(self, title: str, explanation: str) -> None:
  57. super().__init__(title, explanation)
  58. self.make_menu_entry = False
  59. class element_demo:
  60. def __init__(self, element_class: Union[Callable, type], browser_title: Optional[str] = None) -> None:
  61. self.element_class = element_class
  62. self.browser_title = browser_title
  63. def __call__(self, f: Callable, *, more_link: Optional[str] = None) -> Callable:
  64. doc = self.element_class.__doc__ or self.element_class.__init__.__doc__
  65. title, documentation = doc.split('\n', 1)
  66. documentation = remove_indentation(documentation)
  67. documentation = documentation.replace('param ', '')
  68. html = docutils.core.publish_parts(documentation, writer_name='html5_polyglot')['html_body']
  69. html = apply_tailwind(html)
  70. with ui.column().classes('w-full mb-8 gap-2'):
  71. subheading(title)
  72. ui.html(html).classes('documentation bold-links arrow-links')
  73. wrapped = demo(browser_title=self.browser_title)(f)
  74. if more_link:
  75. ui.markdown(f'[More...](documentation/{more_link})').classes('bold-links mt-2')
  76. return wrapped
  77. def load_demo(element_class: type) -> None:
  78. name = pascal_to_snake(element_class.__name__)
  79. module = importlib.import_module(f'website.more_documentation.{name}_documentation')
  80. element_demo(element_class)(getattr(module, 'main_demo'), more_link=name)
  81. def generate_class_doc(class_obj: type) -> None:
  82. class_name = pascal_to_snake(class_obj.__name__)
  83. subheading('Methods')
  84. for name, method in class_obj.__dict__.items():
  85. if name.startswith('_'):
  86. continue
  87. if not method.__doc__:
  88. continue
  89. markdown(f'''
  90. ```python
  91. {class_name}.{name}()
  92. ```
  93. ''')
  94. markdown(method.__doc__)