example.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import inspect
  2. import re
  3. from pathlib import Path
  4. from typing import Callable, Optional, Union
  5. import docutils.core
  6. import isort
  7. from nicegui import ui
  8. from nicegui.elements.markdown import apply_tailwind, prepare_content
  9. from .intersection_observer import IntersectionObserver as intersection_observer
  10. REGEX_H4 = re.compile(r'<h4.*?>(.*?)</h4>')
  11. SPECIAL_CHARACTERS = re.compile('[^(a-z)(A-Z)(0-9)-]')
  12. PYTHON_BGCOLOR = '#00000010'
  13. PYTHON_COLOR = '#eef5fb'
  14. BASH_BGCOLOR = '#00000010'
  15. BASH_COLOR = '#e8e8e8'
  16. BROWSER_BGCOLOR = '#00000010'
  17. BROWSER_COLOR = '#ffffff'
  18. def remove_prefix(text: str, prefix: str) -> str:
  19. return text[len(prefix):] if text.startswith(prefix) else text
  20. def add_html_with_anchor_link(html: str, menu: Optional[ui.drawer]) -> str:
  21. match = REGEX_H4.search(html)
  22. headline = match.groups()[0].strip()
  23. headline_id = SPECIAL_CHARACTERS.sub('_', headline).lower()
  24. icon = '<span class="material-icons">link</span>'
  25. link = f'<a href="#{headline_id}" class="hover:text-black auto-link" style="color: #ddd">{icon}</a>'
  26. target = f'<div id="{headline_id}" style="position: relative; top: -90px"></div>'
  27. html = html.replace('<h4', f'{target}<h4', 1)
  28. html = html.replace('</h4>', f' {link}</h4>', 1)
  29. ui.html(html).classes('documentation bold-links arrow-links')
  30. if menu:
  31. with menu:
  32. async def click():
  33. if await ui.run_javascript(f'!!document.querySelector("div.q-drawer__backdrop")'):
  34. menu.hide()
  35. ui.open(f'#{headline_id}')
  36. ui.link(headline, f'#{headline_id}').props('data-close-overlay').on('click', click)
  37. class example:
  38. def __init__(self,
  39. content: Union[Callable, type, str],
  40. menu: Optional[ui.drawer],
  41. browser_title: Optional[str] = None,
  42. immediate: bool = False) -> None:
  43. self.content = content
  44. self.menu = menu
  45. self.browser_title = browser_title
  46. self.immediate = immediate
  47. def __call__(self, f: Callable) -> Callable:
  48. with ui.column().classes('w-full mb-8'):
  49. if isinstance(self.content, str):
  50. html = prepare_content(self.content, 'fenced-code-blocks tables')
  51. else:
  52. doc = self.content.__doc__ or self.content.__init__.__doc__
  53. html: str = docutils.core.publish_parts(doc, writer_name='html5_polyglot')['html_body']
  54. html = html.replace('<p>', '<h4>', 1)
  55. html = html.replace('</p>', '</h4>', 1)
  56. html = html.replace('param ', '')
  57. html = apply_tailwind(html)
  58. add_html_with_anchor_link(html, self.menu)
  59. with ui.column().classes('w-full items-stretch gap-8 no-wrap min-[1500px]:flex-row'):
  60. code = inspect.getsource(f).split('# END OF EXAMPLE')[0].strip().splitlines()
  61. while not code[0].startswith(' ' * 8):
  62. del code[0]
  63. code = ['from nicegui import ui'] + [remove_prefix(line[8:], '# ') for line in code]
  64. code = ['' if line == '#' else line for line in code]
  65. if not code[-1].startswith('ui.run('):
  66. code.append('')
  67. code.append('ui.run()')
  68. code = isort.code('\n'.join(code), no_sections=True, lines_after_imports=1)
  69. with python_window(classes='w-full max-w-[44rem]'):
  70. ui.markdown(f'```python\n{code}\n```')
  71. with browser_window(self.browser_title,
  72. classes='w-full max-w-[44rem] min-[1500px]:max-w-[20rem] min-h-[10rem] browser-window'):
  73. if self.immediate:
  74. f()
  75. else:
  76. intersection_observer(on_intersection=f)
  77. def pascal_to_snake(name: str) -> str:
  78. return re.sub(r'(?<!^)(?=[A-Z])', '_', name).lower()
  79. if isinstance(self.content, type):
  80. name = pascal_to_snake(self.content.__name__)
  81. path = Path(__file__).parent / 'more_reference' / f'{name}_reference.py'
  82. if path.exists():
  83. ui.markdown(f'[More examples...](reference/{name})').classes('bold-links')
  84. return f
  85. def _window_header(bgcolor: str) -> ui.row():
  86. return ui.row().classes(f'w-full h-8 p-2 bg-[{bgcolor}]')
  87. def _dots() -> None:
  88. with ui.row().classes('gap-1 relative left-[1px] top-[1px]'):
  89. ui.icon('circle').classes('text-[13px] text-red-400')
  90. ui.icon('circle').classes('text-[13px] text-yellow-400')
  91. ui.icon('circle').classes('text-[13px] text-green-400')
  92. def _title(title: str) -> None:
  93. ui.label(title).classes('text-sm text-gray-600 absolute left-1/2 top-[6px]').style('transform: translateX(-50%)')
  94. def _tab(name: str, color: str, bgcolor: str) -> None:
  95. with ui.row().classes('gap-0'):
  96. with ui.label().classes(f'w-2 h-[24px] bg-[{color}]'):
  97. ui.label().classes(f'w-full h-full bg-[{bgcolor}] rounded-br-[6px]')
  98. ui.label(name).classes(f'text-sm text-gray-600 px-6 py-1 h-[24px] rounded-t-[6px] bg-[{color}]')
  99. with ui.label().classes(f'w-2 h-[24px] bg-[{color}]'):
  100. ui.label().classes(f'w-full h-full bg-[{bgcolor}] rounded-bl-[6px]')
  101. def window(color: str, bgcolor: str, *, title: str = '', tab: str = '', classes: str = '') -> ui.column:
  102. with ui.card().classes(f'no-wrap bg-[{color}] rounded-xl p-0 gap-0 {classes}') \
  103. .style('box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1)'):
  104. with _window_header(bgcolor):
  105. _dots()
  106. if title:
  107. _title(title)
  108. if tab:
  109. _tab(tab, color, bgcolor)
  110. return ui.column().classes('w-full h-full overflow-auto')
  111. def python_window(title: Optional[str] = None, *, classes: str = '') -> ui.card:
  112. return window(PYTHON_COLOR, PYTHON_BGCOLOR, title=title or 'main.py', classes=classes).classes('p-2 python-window')
  113. def bash_window(*, classes: str = '') -> ui.card:
  114. return window(BASH_COLOR, BASH_BGCOLOR, title='bash', classes=classes).classes('p-2 bash-window')
  115. def browser_window(title: Optional[str] = None, *, classes: str = '') -> ui.card:
  116. return window(BROWSER_COLOR, BROWSER_BGCOLOR, tab=title or 'NiceGUI', classes=classes).classes('p-4 browser-window')