preproc.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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 typing import Any, List, Tuple
  14. from markdown.preprocessors import Preprocessor as MdPreprocessor
  15. from ..._warnings import _warn
  16. from ..builder import _Builder
  17. from .factory import _MarkdownFactory
  18. if t.TYPE_CHECKING:
  19. from ...gui import Gui
  20. class _Preprocessor(MdPreprocessor):
  21. # ----------------------------------------------------------------------
  22. # Finds, in the Markdown text, control declaration constructs:
  23. # <|<some value>|>
  24. # or
  25. # <|<some value>|<control_type>|>
  26. # or
  27. # <|<some value>|<control_type>|<prop_name[=propvalue]>>
  28. # or
  29. # <|<control_type>|<prop_name[=propvalue]>>
  30. #
  31. # These constructs are converted a fragment that the ControlPattern
  32. # processes to create the components that get generated.
  33. # <control_type> prop_name="prop_value" ...
  34. # Note that if a value is provided before the control_type, it is set
  35. # as the default property value for that control type.
  36. # The default control type is 'text'.
  37. # ----------------------------------------------------------------------
  38. # Control in Markdown
  39. __CONTROL_RE = re.compile(r"<\|(.*?)\|>")
  40. # Opening tag
  41. __OPENING_TAG_RE = re.compile(r"<([0-9a-zA-Z\_\.]*)\|((?:(?!\|>).)*)\s*$")
  42. # Closing tag
  43. __CLOSING_TAG_RE = re.compile(r"^\s*\|([0-9a-zA-Z\_\.]*)>")
  44. # Link in Markdown
  45. __LINK_RE = re.compile(r"(\[[^\]]*?\]\([^\)]*?\))")
  46. # Split properties and control type
  47. __SPLIT_RE = re.compile(r"(?<!\\\\)\|")
  48. # Property syntax: '<prop_name>[=<prop_value>]'
  49. # If <prop_value> is omitted:
  50. # '<prop_name>' is equivalent to '<prop_name>=true'
  51. # 'not <prop_name>' is equivalent to '<prop_name>=false'
  52. # 'not', 'dont', 'don't' are equivalent in this context
  53. # Note 1: 'not <prop_name>=<prop_value>' is an invalid syntax
  54. # Note 2: Space characters after the equal sign are significative
  55. __PROPERTY_RE = re.compile(r"((?:don'?t|not)\s+)?([a-zA-Z][\.a-zA-Z_$0-9]*(?:\[(?:.*?)\])?)\s*(?:=(.*))?$")
  56. # Error syntax detection regex
  57. __MISSING_LEADING_PIPE_RE = re.compile(r"<[^|](.*?)\|>")
  58. _gui: "Gui"
  59. @staticmethod
  60. def extend(md, gui, priority):
  61. instance = _Preprocessor(md)
  62. md.preprocessors.register(instance, "taipy", priority)
  63. instance._gui = gui
  64. def _make_prop_pair(self, prop_name: str, prop_value: str) -> Tuple[str, str]:
  65. # Un-escape pipe character in property value
  66. return (prop_name, prop_value.replace("\\|", "|"))
  67. def _validate_line(self, line: str, line_count: int) -> bool:
  68. if _Preprocessor.__MISSING_LEADING_PIPE_RE.search(line) is not None:
  69. _warn(f"Missing leading pipe '|' in opening tag line {line_count}: '{line}'.")
  70. return False
  71. return True
  72. def run(self, lines: List[str]) -> List[str]:
  73. new_lines = []
  74. tag_stack = []
  75. for line_count, line in enumerate(lines, start=1):
  76. if not self._validate_line(line, line_count):
  77. continue
  78. new_line = ""
  79. last_index = 0
  80. # Opening tags
  81. m = _Preprocessor.__OPENING_TAG_RE.search(line)
  82. if m is not None:
  83. tag = "part"
  84. properties: List[Tuple[str, str]] = []
  85. if m.group(2):
  86. tag, properties = self._process_control(m.group(2), line_count, tag)
  87. if tag in _MarkdownFactory._TAIPY_BLOCK_TAGS:
  88. tag_stack.append((tag, line_count, m.group(1) or None))
  89. new_line_delimeter = "\n" if line.startswith("<|") else "\n\n"
  90. line = (
  91. line[: m.start()]
  92. + new_line_delimeter
  93. + _MarkdownFactory._TAIPY_START
  94. + tag
  95. + _MarkdownFactory._START_SUFFIX
  96. )
  97. for property in properties:
  98. prop_value = property[1].replace('"', '\\"')
  99. line += f' {property[0]}="{prop_value}"'
  100. line += _MarkdownFactory._TAIPY_END + new_line_delimeter
  101. else:
  102. _warn(f"Failed to recognized block tag '{tag}' in line {line_count}. Check that you are closing the tag properly with '|>' if it is a control element.") # noqa: E501
  103. # Other controls
  104. for m in _Preprocessor.__CONTROL_RE.finditer(line):
  105. control_name, properties = self._process_control(m.group(1), line_count)
  106. new_line += line[last_index : m.start()]
  107. control_text = _MarkdownFactory._TAIPY_START + control_name
  108. for property in properties:
  109. prop_value = property[1].replace('"', '\\"')
  110. control_text += f' {property[0]}="{prop_value}"'
  111. control_text += _MarkdownFactory._TAIPY_END
  112. new_line += control_text
  113. last_index = m.end()
  114. new_line = line if last_index == 0 else new_line + line[last_index:]
  115. # Add key attribute to links
  116. line = new_line
  117. new_line = ""
  118. last_index = 0
  119. for m in _Preprocessor.__LINK_RE.finditer(line):
  120. new_line += line[last_index : m.end()]
  121. new_line += "{: key=" + _Builder._get_key("link") + "}"
  122. last_index = m.end()
  123. new_line = line if last_index == 0 else new_line + line[last_index:]
  124. # Look for a closing tag
  125. m = _Preprocessor.__CLOSING_TAG_RE.search(new_line)
  126. if m is not None:
  127. if len(tag_stack):
  128. open_tag, open_tag_line_count, open_tag_identifier = tag_stack.pop()
  129. close_tag_identifier = m.group(1)
  130. if close_tag_identifier and not open_tag_identifier:
  131. _warn(
  132. f"Missing opening '{open_tag}' tag identifier '{close_tag_identifier}' in line {open_tag_line_count}." # noqa: E501
  133. )
  134. if open_tag_identifier and not close_tag_identifier:
  135. _warn(
  136. f"Missing closing '{open_tag}' tag identifier '{open_tag_identifier}' in line {line_count}."
  137. )
  138. if close_tag_identifier and open_tag_identifier and close_tag_identifier != open_tag_identifier:
  139. _warn(
  140. f"Unmatched '{open_tag}' tag identifier in line {open_tag_line_count} and line {line_count}." # noqa: E501
  141. )
  142. new_line = (
  143. new_line[: m.start()]
  144. + _MarkdownFactory._TAIPY_START
  145. + open_tag
  146. + _MarkdownFactory._END_SUFFIX
  147. + _MarkdownFactory._TAIPY_END
  148. + "\n"
  149. + new_line[m.end() :]
  150. )
  151. else:
  152. new_line = (
  153. new_line[: m.start()]
  154. + f"<div>No matching opened tag on line {line_count}</div>"
  155. + new_line[m.end() :]
  156. )
  157. _warn(f"Line {line_count} has an unmatched closing tag.")
  158. # append the new line
  159. new_lines.append(new_line)
  160. # Issue #337: add an empty string at the beginning of new_lines list if there is not one
  161. # so that markdown extension would be able to render properly
  162. if new_lines and new_lines[0] != "":
  163. new_lines.insert(0, "")
  164. # Check for tags left unclosed (but close them anyway)
  165. for tag, line_no, _ in tag_stack:
  166. new_lines.append(
  167. _MarkdownFactory._TAIPY_START + tag + _MarkdownFactory._END_SUFFIX + _MarkdownFactory._TAIPY_END
  168. )
  169. _warn(f"Opened tag {tag} in line {line_no} is not closed.")
  170. return new_lines
  171. def _process_control(
  172. self, prop_string: str, line_count: int, default_control_name: str = _MarkdownFactory.DEFAULT_CONTROL
  173. ) -> Tuple[str, List[Tuple[str, str]]]:
  174. fragments = [f for f in _Preprocessor.__SPLIT_RE.split(prop_string) if f]
  175. control_name = None
  176. default_prop_name = None
  177. default_prop_value = None
  178. properties: List[Tuple[str, Any]] = []
  179. for fragment in fragments:
  180. if control_name is None and _MarkdownFactory.get_default_property_name(fragment):
  181. control_name = fragment
  182. elif control_name is None and default_prop_value is None:
  183. default_prop_value = fragment
  184. elif prop_match := _Preprocessor.__PROPERTY_RE.match(fragment):
  185. not_prefix = prop_match.group(1)
  186. prop_name = prop_match.group(2)
  187. val = prop_match.group(3)
  188. if not_prefix and val:
  189. _warn(f"Negated property {prop_name} value ignored at {line_count}.")
  190. prop_value = "True"
  191. if not_prefix:
  192. prop_value = "False"
  193. elif val:
  194. prop_value = val
  195. properties.append(self._make_prop_pair(prop_name, prop_value))
  196. elif len(fragment) > 1 and fragment[0] == "{" and fragment[-1] == "}":
  197. properties.append(self._make_prop_pair(fragment[1:-1], fragment))
  198. else:
  199. _warn(f"Bad Taipy property format at line {line_count}: '{fragment}'.")
  200. if control_name is None:
  201. if properties and all(attribute != properties[0][0] for attribute in _MarkdownFactory._TEXT_ATTRIBUTES):
  202. control_name = properties[0][0]
  203. properties = properties[1:]
  204. _warn(f'Unrecognized control {control_name} at line {line_count}: "<|{prop_string}|>".')
  205. else:
  206. control_name = default_control_name
  207. if default_prop_value is not None:
  208. default_prop_name = _MarkdownFactory.get_default_property_name(control_name)
  209. # Set property only if it is not already defined
  210. if default_prop_name and default_prop_name not in [x[0] for x in properties]:
  211. properties.insert(0, self._make_prop_pair(default_prop_name, default_prop_value))
  212. return control_name, properties