model.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. from __future__ import annotations
  2. import abc
  3. from dataclasses import dataclass
  4. from typing import Callable, Iterator, List, Optional, Union, overload
  5. import docutils.core
  6. from nicegui.dataclasses import KWONLY_SLOTS
  7. from nicegui.elements.markdown import apply_tailwind, remove_indentation
  8. from . import registry
  9. @dataclass(**KWONLY_SLOTS)
  10. class DocumentationPart:
  11. title: Optional[str] = None
  12. description: Optional[str] = None
  13. link: Optional[str] = None
  14. ui: Optional[Callable] = None
  15. demo: Optional[Callable] = None
  16. @property
  17. def link_target(self) -> Optional[str]:
  18. """Return the link target for in-page navigation."""
  19. return self.title.lower().replace(' ', '_') if self.title else None
  20. class Documentation(abc.ABC):
  21. def __init__(self,
  22. route: str, *,
  23. title: str,
  24. subtitle: str,
  25. back_link: Optional[str] = None) -> None:
  26. self.route = route
  27. self.title = title
  28. self.subtitle = subtitle
  29. self.back_link = back_link
  30. self._content: List[DocumentationPart] = []
  31. self.content()
  32. registry.add(self)
  33. def __iter__(self) -> Iterator[DocumentationPart]:
  34. return iter(self._content)
  35. def __len__(self) -> int:
  36. return len(self._content)
  37. def text(self, title: str, description: str) -> None:
  38. """Add a text block to the documentation."""
  39. self._content.append(DocumentationPart(title=title, description=description))
  40. @overload
  41. def demo(self, title: str, description: str, /) -> Callable[[Callable], Callable]: ...
  42. @overload
  43. def demo(self, element: type, /) -> Callable[[Callable], Callable]: ...
  44. @overload
  45. def demo(self, function: Callable, /) -> Callable[[Callable], Callable]: ...
  46. def demo(self, *args) -> Callable[[Callable], Callable]:
  47. """Add a demo section to the documentation."""
  48. if len(args) == 2:
  49. title, description = args
  50. elif isinstance(args[0], type):
  51. title, description = args[0].__init__.__doc__.split('\n', 1) # type: ignore
  52. elif callable(args[0]):
  53. title, description = args[0].__doc__.split('\n', 1)
  54. else:
  55. raise ValueError('Invalid arguments')
  56. description = remove_indentation(description).replace('param ', '')
  57. html = apply_tailwind(docutils.core.publish_parts(description, writer_name='html5_polyglot')['html_body'])
  58. def decorator(function: Callable) -> Callable:
  59. self._content.append(DocumentationPart(title=title, description=html, demo=function))
  60. return function
  61. return decorator
  62. def ui(self, function: Callable) -> Callable:
  63. """Add arbitrary UI to the documentation."""
  64. self._content.append(DocumentationPart(ui=function))
  65. return function
  66. def intro(self, documentation: Documentation) -> None:
  67. """Add an element intro section to the documentation."""
  68. documentation.back_link = self.route
  69. part = documentation._content[0] # pylint: disable=protected-access
  70. part.link = documentation.route
  71. self._content.append(part)
  72. @abc.abstractmethod
  73. def content(self) -> None:
  74. """Add documentation content here."""
  75. class SectionDocumentation(Documentation):
  76. _title: str
  77. _route: str
  78. def __init_subclass__(cls, title: str, name: str) -> None:
  79. cls._title = title
  80. cls._route = f'/documentation/section_{name}'
  81. return super().__init_subclass__()
  82. def __init__(self) -> None:
  83. super().__init__(self._route, subtitle='Documentation', title=self._title, back_link='/documentation')
  84. class DetailDocumentation(Documentation):
  85. _title: str
  86. _route: str
  87. def __init_subclass__(cls, title: str, name: str) -> None:
  88. cls._title = title
  89. cls._route = f'/documentation/{name}'
  90. return super().__init_subclass__()
  91. def __init__(self) -> None:
  92. super().__init__(self._route, subtitle='Documentation', title=self._title)
  93. class UiElementDocumentation(Documentation):
  94. _element: Union[type, Callable]
  95. def __init_subclass__(cls, element: Union[type, Callable]) -> None:
  96. cls._element = element
  97. return super().__init_subclass__()
  98. def __init__(self) -> None:
  99. self.element = self._element
  100. name = self.element.__name__.lower()
  101. super().__init__(f'/documentation/{name}', subtitle='Documentation', title=f'ui.*{name}*')
  102. def main_demo(self) -> None:
  103. """Add a demo for the element here."""
  104. def more(self) -> None:
  105. """Add more demos for the element here."""
  106. def content(self) -> None:
  107. self.demo(self.element)(self.main_demo) # pylint: disable=not-callable
  108. self.more()