__init__.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. # Copyright 2021-2024 Avaiga Private Limited
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  4. # the License. You may obtain a copy of the License at
  5. #
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. #
  8. # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  9. # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
  10. # specific language governing permissions and limitations under the License.
  11. import re
  12. import typing as t
  13. from abc import ABC, abstractmethod
  14. from os import path
  15. from charset_normalizer import detect
  16. from taipy.logger._taipy_logger import _TaipyLogger
  17. from ..page import Page
  18. from ..utils import _is_in_notebook, _varname_from_content
  19. from ._html import _TaipyHTMLParser
  20. if t.TYPE_CHECKING:
  21. from watchdog.observers.api import BaseObserver
  22. from ..gui import Gui
  23. class _Renderer(Page, ABC):
  24. def __init__(self, **kwargs) -> None:
  25. """NOT DOCUMENTED
  26. Initialize a new _Renderer with the indicated content.
  27. Arguments:
  28. content (str): The text content or the path to the file holding the text to be transformed.
  29. If *content* is a path to a readable file, the file is read entirely as the text template.
  30. """
  31. from ..builder._element import _Element # noqa: F811
  32. super().__init__(**kwargs)
  33. content: t.Optional[t.Union[str, _Element]] = kwargs.get("content", None)
  34. if content is None:
  35. raise ValueError("'content' argument is missing for class '_Renderer'")
  36. self._content = ""
  37. self._base_element: t.Optional[_Element] = None
  38. self._filepath = ""
  39. self._observer: t.Optional["BaseObserver"] = None
  40. self._encoding: t.Optional[str] = kwargs.get("encoding", None)
  41. if isinstance(content, str):
  42. self.__process_content(content)
  43. elif isinstance(content, _Element):
  44. self._base_element = content
  45. else:
  46. raise ValueError(
  47. f"'content' argument has incorrect type '{type(content).__name__}'. This must be a string or an Builder element." # noqa: E501
  48. )
  49. def __process_content(self, content: str) -> None:
  50. relative_file_path = (
  51. None if self._frame is None else path.join(path.dirname(self._frame.f_code.co_filename), content)
  52. )
  53. if relative_file_path is not None and path.exists(relative_file_path) and path.isfile(relative_file_path):
  54. content = relative_file_path
  55. if content == relative_file_path or (path.exists(content) and path.isfile(content)):
  56. self.__parse_file_content(content)
  57. # Watchdog observer: watch for file changes
  58. if _is_in_notebook() and self._observer is None:
  59. self.__observe_file_change(content)
  60. return
  61. self._content = self.__sanitize_content(content)
  62. def __observe_file_change(self, file_path: str):
  63. from watchdog.observers import Observer
  64. from .utils import FileWatchdogHandler
  65. self._observer = Observer()
  66. file_path = path.abspath(file_path)
  67. self._observer.schedule(FileWatchdogHandler(file_path, self), path.dirname(file_path), recursive=False)
  68. self._observer.start()
  69. def __parse_file_content(self, content):
  70. with open(t.cast(str, content), "rb") as f:
  71. file_content = f.read()
  72. encoding = "utf-8"
  73. if self._encoding is not None:
  74. encoding = self._encoding
  75. _TaipyLogger._get_logger().info(f"'{encoding}' encoding was used to decode file '{content}'.")
  76. elif (detected_encoding := detect(file_content)["encoding"]) is not None:
  77. encoding = detected_encoding
  78. _TaipyLogger._get_logger().info(f"Detected '{encoding}' encoding for file '{content}'.")
  79. else:
  80. _TaipyLogger._get_logger().info(f"Using default '{encoding}' encoding for file '{content}'.")
  81. self._content = self.__sanitize_content(file_content.decode(encoding))
  82. # Save file path for error handling
  83. self._filepath = content
  84. def __sanitize_content(self, content: str) -> str:
  85. # replace all CRLF (\r\n) with LF (\n)
  86. text = re.sub(r'\r\n', '\n', content)
  87. # replace all remaining CR (\r) with LF (\n)
  88. text = re.sub(r'\r', '\n', content)
  89. return text
  90. def set_content(self, content: str) -> None:
  91. """Set a new page content.
  92. Reads the new page content and reinitializes the `Page^` instance to reflect the change.
  93. !!! important
  94. This function can only be used in an IPython notebook context.
  95. Arguments:
  96. content (str): The text content or the path to the file holding the text to be transformed.
  97. If *content* is a path to a readable file, the file is read entirely as the text
  98. template.
  99. Exceptions:
  100. RuntimeError: If this method is called outside an IPython notebook context.
  101. """
  102. if not _is_in_notebook():
  103. raise RuntimeError("'set_content()' must be used in an IPython notebook context")
  104. self.__process_content(content)
  105. if self._notebook_gui is not None and self._notebook_page is not None:
  106. if self._notebook_gui._config.root_page is self._notebook_page:
  107. self._notebook_gui._navigate("/", {"tp_reload_all": "true"})
  108. return
  109. self._notebook_gui._navigate(self._notebook_page._route, {"tp_reload_same_route_only": "true"})
  110. def _get_content_detail(self, gui: "Gui") -> str:
  111. if self._filepath:
  112. return f"in file '{self._filepath}'"
  113. if varname := _varname_from_content(gui, self._content):
  114. return f"in variable '{varname}'"
  115. return ""
  116. @abstractmethod
  117. def render(self, gui: "Gui") -> str:
  118. pass
  119. class _EmptyPage(_Renderer):
  120. def __init__(self) -> None:
  121. super().__init__(content="<PageContent />")
  122. def render(self, gui: "Gui") -> str:
  123. return self._content
  124. class Markdown(_Renderer):
  125. """Page generator for *Markdown* text.
  126. Taipy can use Markdown text to create pages that are the base of
  127. user interfaces.
  128. You can find details on the Taipy Markdown-specific syntax and how to add
  129. Taipy Visual Elements in the [section on HTML](../gui/pages/index.md#using-markdown)
  130. of the User Manual.
  131. """
  132. def __init__(self, content: str, **kwargs) -> None:
  133. """Initialize a new `Markdown` page.
  134. Arguments:
  135. content (str): The text content or the path to the file holding the Markdown text
  136. to be transformed.<br/>
  137. If _content_ is a path to a readable file, the file is read as the Markdown
  138. template content.
  139. """
  140. kwargs["content"] = content
  141. super().__init__(**kwargs)
  142. # Generate JSX from Markdown
  143. def render(self, gui: "Gui") -> str:
  144. return gui._markdown.convert(self._content)
  145. class Html(_Renderer):
  146. """Page generator for *HTML* text.
  147. Taipy can use HTML code to create pages that are the base of
  148. user interfaces.
  149. You can find details on HTML-specific constructs and how to add
  150. Taipy Visual Elements in the [section on HTML](../gui/pages/index.md#using-html)
  151. of the User Manual.
  152. """
  153. def __init__(self, content: str, **kwargs) -> None:
  154. """Initialize a new `Html` page.
  155. Arguments:
  156. content (str): The text content or the path to the file holding the HTML text to
  157. be transformed.<br/>
  158. If *content* is a path to a readable file, the file is read as the HTML
  159. template content.
  160. """
  161. kwargs["content"] = content
  162. super().__init__(**kwargs)
  163. self.head = None
  164. # Modify path routes
  165. def modify_taipy_base_url(self, base_url):
  166. self._content = self._content.replace("{{taipy_base_url}}", f"{base_url}")
  167. # Generate JSX from HTML
  168. def render(self, gui: "Gui") -> str:
  169. parser = _TaipyHTMLParser(gui)
  170. parser.feed_data(self._content)
  171. self.head = parser.head
  172. return parser.get_jsx()