image.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import re
  2. from typing import TYPE_CHECKING, Any, Dict, List, Match, Optional
  3. from ..util import escape as escape_text
  4. from ..util import escape_url
  5. from ._base import BaseDirective, DirectivePlugin
  6. if TYPE_CHECKING:
  7. from ..block_parser import BlockParser
  8. from ..core import BlockState
  9. from ..markdown import Markdown
  10. from ..renderers.html import HTMLRenderer
  11. __all__ = ["Image", "Figure"]
  12. _num_re = re.compile(r"^\d+(?:\.\d*)?")
  13. _allowed_aligns = ["top", "middle", "bottom", "left", "center", "right"]
  14. def _parse_attrs(options: Dict[str, Any]) -> Dict[str, Any]:
  15. attrs = {}
  16. if "alt" in options:
  17. attrs["alt"] = options["alt"]
  18. # validate align
  19. align = options.get("align")
  20. if align and align in _allowed_aligns:
  21. attrs["align"] = align
  22. height = options.get("height")
  23. width = options.get("width")
  24. if height and _num_re.match(height):
  25. attrs["height"] = height
  26. if width and _num_re.match(width):
  27. attrs["width"] = width
  28. if "target" in options:
  29. attrs["target"] = escape_url(options["target"])
  30. return attrs
  31. class Image(DirectivePlugin):
  32. NAME = "image"
  33. def parse(self, block: "BlockParser", m: Match[str], state: "BlockState") -> Dict[str, Any]:
  34. options = dict(self.parse_options(m))
  35. attrs = _parse_attrs(options)
  36. attrs["src"] = self.parse_title(m)
  37. return {"type": "block_image", "attrs": attrs}
  38. def __call__(self, directive: "BaseDirective", md: "Markdown") -> None:
  39. directive.register(self.NAME, self.parse)
  40. assert md.renderer is not None
  41. if md.renderer.NAME == "html":
  42. md.renderer.register("block_image", render_block_image)
  43. def render_block_image(
  44. self: "HTMLRenderer",
  45. src: str,
  46. alt: Optional[str] = None,
  47. width: Optional[str] = None,
  48. height: Optional[str] = None,
  49. **attrs: Any,
  50. ) -> str:
  51. img = '<img src="' + escape_text(src) + '"'
  52. style = ""
  53. if alt:
  54. img += ' alt="' + escape_text(alt) + '"'
  55. if width:
  56. if width.isdigit():
  57. img += ' width="' + width + '"'
  58. else:
  59. style += "width:" + width + ";"
  60. if height:
  61. if height.isdigit():
  62. img += ' height="' + height + '"'
  63. else:
  64. style += "height:" + height + ";"
  65. if style:
  66. img += ' style="' + escape_text(style) + '"'
  67. img += " />"
  68. _cls = "block-image"
  69. align = attrs.get("align")
  70. if align:
  71. _cls += " align-" + align
  72. target = attrs.get("target")
  73. if target:
  74. href = self.safe_url(target)
  75. outer = '<a class="' + _cls + '" href="' + href + '">'
  76. return outer + img + "</a>\n"
  77. else:
  78. return '<div class="' + _cls + '">' + img + "</div>\n"
  79. class Figure(DirectivePlugin):
  80. NAME = "figure"
  81. def parse_directive_content(
  82. self, block: "BlockParser", m: Match[str], state: "BlockState"
  83. ) -> Optional[List[Dict[str, Any]]]:
  84. content = self.parse_content(m)
  85. if not content:
  86. return None
  87. tokens = list(self.parse_tokens(block, content, state))
  88. caption = tokens[0]
  89. if caption["type"] == "paragraph":
  90. caption["type"] = "figcaption"
  91. children = [caption]
  92. if len(tokens) > 1:
  93. children.append({"type": "legend", "children": tokens[1:]})
  94. return children
  95. return None
  96. def parse(self, block: "BlockParser", m: Match[str], state: "BlockState") -> Dict[str, Any]:
  97. options = dict(self.parse_options(m))
  98. image_attrs = _parse_attrs(options)
  99. image_attrs["src"] = self.parse_title(m)
  100. align = image_attrs.pop("align", None)
  101. fig_attrs = {}
  102. if align:
  103. fig_attrs["align"] = align
  104. for k in ["figwidth", "figclass"]:
  105. if k in options:
  106. fig_attrs[k] = options[k]
  107. children = [{"type": "block_image", "attrs": image_attrs}]
  108. content = self.parse_directive_content(block, m, state)
  109. if content:
  110. children.extend(content)
  111. return {
  112. "type": "figure",
  113. "attrs": fig_attrs,
  114. "children": children,
  115. }
  116. def __call__(self, directive: "BaseDirective", md: "Markdown") -> None:
  117. directive.register(self.NAME, self.parse)
  118. assert md.renderer is not None
  119. if md.renderer.NAME == "html":
  120. md.renderer.register("figure", render_figure)
  121. md.renderer.register("block_image", render_block_image)
  122. md.renderer.register("figcaption", render_figcaption)
  123. md.renderer.register("legend", render_legend)
  124. def render_figure(
  125. self: Any,
  126. text: str,
  127. align: Optional[str] = None,
  128. figwidth: Optional[str] = None,
  129. figclass: Optional[str] = None,
  130. ) -> str:
  131. _cls = "figure"
  132. if align:
  133. _cls += " align-" + align
  134. if figclass:
  135. _cls += " " + figclass
  136. html = '<figure class="' + _cls + '"'
  137. if figwidth:
  138. html += ' style="width:' + figwidth + '"'
  139. return html + ">\n" + text + "</figure>\n"
  140. def render_figcaption(self: Any, text: str) -> str:
  141. return "<figcaption>" + text + "</figcaption>\n"
  142. def render_legend(self: Any, text: str) -> str:
  143. return '<div class="legend">\n' + text + "</div>\n"