example.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import contextlib
  2. import inspect
  3. import re
  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. class example:
  21. def __init__(self,
  22. content: Union[Callable, type, str],
  23. menu: Optional[ui.element],
  24. browser_title: Optional[str] = None,
  25. immediate: bool = False) -> None:
  26. self.content = content
  27. self.menu = menu
  28. self.browser_title = browser_title
  29. self.immediate = immediate
  30. def __call__(self, f: Callable) -> Callable:
  31. with ui.column().classes('w-full mb-8'):
  32. if isinstance(self.content, str):
  33. html = prepare_content(self.content, 'fenced-code-blocks tables')
  34. else:
  35. doc = self.content.__doc__ or self.content.__init__.__doc__
  36. html: str = docutils.core.publish_parts(doc, writer_name='html5_polyglot')['html_body']
  37. html = html.replace('<p>', '<h4>', 1)
  38. html = html.replace('</p>', '</h4>', 1)
  39. html = html.replace('param ', '')
  40. html = apply_tailwind(html)
  41. match = REGEX_H4.search(html)
  42. headline = match.groups()[0].strip()
  43. headline_id = SPECIAL_CHARACTERS.sub('_', headline).lower()
  44. icon = '<span class="material-icons">link</span>'
  45. link = f'<a href="#{headline_id}" class="hover:text-black auto-link" style="color: #ddd">{icon}</a>'
  46. target = f'<div id="{headline_id}" style="position: relative; top: -90px"></div>'
  47. html = html.replace('<h4', f'{target}<h4', 1)
  48. html = html.replace('</h4>', f' {link}</h4>', 1)
  49. ui.html(html).classes('documentation bold-links arrow-links')
  50. with self.menu or contextlib.nullcontext():
  51. ui.link(headline, f'#{headline_id}')
  52. with ui.column().classes('w-full items-stretch gap-8 no-wrap min-[1500px]:flex-row'):
  53. code = inspect.getsource(f).split('# END OF EXAMPLE')[0].strip().splitlines()
  54. while not code[0].startswith(' ' * 8):
  55. del code[0]
  56. code = ['from nicegui import ui'] + [remove_prefix(line[8:], '# ') for line in code]
  57. code = ['' if line == '#' else line for line in code]
  58. if not code[-1].startswith('ui.run('):
  59. code.append('')
  60. code.append('ui.run()')
  61. code = isort.code('\n'.join(code), no_sections=True, lines_after_imports=1)
  62. with python_window(classes='w-full max-w-[44rem]'):
  63. ui.markdown(f'```python\n{code}\n```')
  64. with browser_window(self.browser_title,
  65. classes='w-full max-w-[44rem] min-[1500px]:max-w-[20rem] min-h-[10rem] browser-window'):
  66. if self.immediate:
  67. f()
  68. else:
  69. intersection_observer(on_intersection=f)
  70. return f
  71. def _window_header(bgcolor: str) -> ui.row():
  72. return ui.row().classes(f'w-full h-8 p-2 bg-[{bgcolor}]')
  73. def _dots() -> None:
  74. with ui.row().classes('gap-1 relative left-[1px] top-[1px]'):
  75. ui.icon('circle').classes('text-[13px] text-red-400')
  76. ui.icon('circle').classes('text-[13px] text-yellow-400')
  77. ui.icon('circle').classes('text-[13px] text-green-400')
  78. def _title(title: str) -> None:
  79. ui.label(title).classes('text-sm text-gray-600 absolute left-1/2 top-[6px]').style('transform: translateX(-50%)')
  80. def _tab(name: str, color: str, bgcolor: str) -> None:
  81. with ui.row().classes('gap-0'):
  82. with ui.label().classes(f'w-2 h-[24px] bg-[{color}]'):
  83. ui.label().classes(f'w-full h-full bg-[{bgcolor}] rounded-br-[6px]')
  84. ui.label(name).classes(f'text-sm text-gray-600 px-6 py-1 h-[24px] rounded-t-[6px] bg-[{color}]')
  85. with ui.label().classes(f'w-2 h-[24px] bg-[{color}]'):
  86. ui.label().classes(f'w-full h-full bg-[{bgcolor}] rounded-bl-[6px]')
  87. def window(color: str, bgcolor: str, *, title: str = '', tab: str = '', classes: str = '') -> ui.column:
  88. with ui.card().classes(f'no-wrap bg-[{color}] rounded-xl p-0 gap-0 {classes}') \
  89. .style('box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1)'):
  90. with _window_header(bgcolor):
  91. _dots()
  92. if title:
  93. _title(title)
  94. if tab:
  95. _tab(tab, color, bgcolor)
  96. return ui.column().classes('w-full h-full overflow-auto')
  97. def python_window(*, classes: str = '') -> ui.card:
  98. return window(PYTHON_COLOR, PYTHON_BGCOLOR, title='main.py', classes=classes).classes('p-2 python-window')
  99. def bash_window(*, classes: str = '') -> ui.card:
  100. return window(BASH_COLOR, BASH_BGCOLOR, title='bash', classes=classes).classes('p-2 bash-window')
  101. def browser_window(title: Optional[str] = None, *, classes: str = '') -> ui.card:
  102. return window(BROWSER_COLOR, BROWSER_BGCOLOR, tab=title or 'NiceGUI', classes=classes).classes('p-4 browser-window')