markdown.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. """Markdown component."""
  2. import textwrap
  3. from typing import Callable, Dict, List, Union
  4. from reflex.compiler import utils
  5. from reflex.components.component import Component
  6. from reflex.components.datadisplay.list import ListItem, OrderedList, UnorderedList
  7. from reflex.components.navigation import Link
  8. from reflex.components.typography.heading import Heading
  9. from reflex.components.typography.text import Text
  10. from reflex.style import Style
  11. from reflex.utils import types
  12. from reflex.vars import BaseVar, ImportVar, Var
  13. # Mapping from markdown tags to components.
  14. components_by_tag: Dict[str, Callable] = {
  15. "h1": Heading,
  16. "h2": Heading,
  17. "h3": Heading,
  18. "h4": Heading,
  19. "h5": Heading,
  20. "h6": Heading,
  21. "p": Text,
  22. "ul": UnorderedList,
  23. "ol": OrderedList,
  24. "li": ListItem,
  25. "a": Link,
  26. }
  27. class Markdown(Component):
  28. """A markdown component."""
  29. library = "react-markdown@^8.0.7"
  30. lib_dependencies: List[str] = [
  31. "rehype-katex@^6.0.3",
  32. "remark-math@^5.1.1",
  33. "rehype-raw@^6.1.1",
  34. "remark-gfm@^3.0.1",
  35. ]
  36. tag = "ReactMarkdown"
  37. is_default = True
  38. # Custom defined styles for the markdown elements.
  39. custom_styles: Dict[str, Style] = {
  40. k: Style(v)
  41. for k, v in {
  42. "h1": {
  43. "as_": "h1",
  44. "size": "2xl",
  45. },
  46. "h2": {
  47. "as_": "h2",
  48. "size": "xl",
  49. },
  50. "h3": {
  51. "as_": "h3",
  52. "size": "lg",
  53. },
  54. "h4": {
  55. "as_": "h4",
  56. "size": "md",
  57. },
  58. "h5": {
  59. "as_": "h5",
  60. "size": "sm",
  61. },
  62. "h6": {
  63. "as_": "h6",
  64. "size": "xs",
  65. },
  66. }.items()
  67. }
  68. @classmethod
  69. def create(cls, *children, **props) -> Component:
  70. """Create a markdown component.
  71. Args:
  72. children: The children of the component.
  73. props: The properties of the component.
  74. Returns:
  75. The markdown component.
  76. """
  77. assert len(children) == 1 and types._isinstance(
  78. children[0], Union[str, Var]
  79. ), "Markdown component must have exactly one child containing the markdown source."
  80. # Get the markdown source.
  81. src = children[0]
  82. if isinstance(src, str):
  83. src = textwrap.dedent(src)
  84. return super().create(src, **props)
  85. def _get_imports(self):
  86. from reflex.components.datadisplay.code import Code, CodeBlock
  87. imports = super()._get_imports()
  88. # Special markdown imports.
  89. imports["remark-math"] = {ImportVar(tag="remarkMath", is_default=True)}
  90. imports["remark-gfm"] = {ImportVar(tag="remarkGfm", is_default=True)}
  91. imports["rehype-katex"] = {ImportVar(tag="rehypeKatex", is_default=True)}
  92. imports["rehype-raw"] = {ImportVar(tag="rehypeRaw", is_default=True)}
  93. imports[""] = {ImportVar(tag="katex/dist/katex.min.css")}
  94. # Get the imports for each component.
  95. for component in components_by_tag.values():
  96. imports = utils.merge_imports(imports, component()._get_imports())
  97. # Get the imports for the code components.
  98. imports = utils.merge_imports(
  99. imports, CodeBlock.create(theme="light")._get_imports()
  100. )
  101. imports = utils.merge_imports(imports, Code.create()._get_imports())
  102. return imports
  103. def _render(self):
  104. from reflex.components.tags.tag import Tag
  105. components = {
  106. tag: f"{{({{node, ...props}}) => <{(component().tag)} {{...props}} {''.join(Tag(name='', props=Style(self.custom_styles.get(tag, {}))).format_props())} />}}"
  107. for tag, component in components_by_tag.items()
  108. }
  109. components[
  110. "code"
  111. ] = """{({node, inline, className, children, ...props}) =>
  112. {
  113. const match = (className || '').match(/language-(?<lang>.*)/);
  114. return !inline ? (
  115. <Prism
  116. children={String(children).replace(/\n$/, '')}
  117. language={match ? match[1] : ''}
  118. theme={light}
  119. {...props}
  120. />
  121. ) : (
  122. <Code {...props}>
  123. {children}
  124. </Code>
  125. );
  126. }}""".replace(
  127. "\n", " "
  128. )
  129. return (
  130. super()
  131. ._render()
  132. .add_props(
  133. components=components,
  134. remark_plugins=BaseVar(name="[remarkMath, remarkGfm]", type_=List[str]),
  135. rehype_plugins=BaseVar(
  136. name="[rehypeKatex, rehypeRaw]", type_=List[str]
  137. ),
  138. )
  139. .remove_props("custom_components")
  140. )