markdown.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import re
  2. from textwrap import indent
  3. from typing import Any, Dict, Iterable, cast
  4. from ..core import BaseRenderer, BlockState
  5. from ..util import strip_end
  6. from ._list import render_list
  7. fenced_re = re.compile(r"^[`~]+", re.M)
  8. class MarkdownRenderer(BaseRenderer):
  9. """A renderer to re-format Markdown text."""
  10. NAME = "markdown"
  11. def __call__(self, tokens: Iterable[Dict[str, Any]], state: BlockState) -> str:
  12. out = self.render_tokens(tokens, state)
  13. # special handle for line breaks
  14. out += "\n\n".join(self.render_referrences(state)) + "\n"
  15. return strip_end(out)
  16. def render_referrences(self, state: BlockState) -> Iterable[str]:
  17. ref_links = state.env["ref_links"]
  18. for key in ref_links:
  19. attrs = ref_links[key]
  20. text = "[" + attrs["label"] + "]: " + attrs["url"]
  21. title = attrs.get("title")
  22. if title:
  23. text += ' "' + title + '"'
  24. yield text
  25. def render_children(self, token: Dict[str, Any], state: BlockState) -> str:
  26. children = token["children"]
  27. return self.render_tokens(children, state)
  28. def text(self, token: Dict[str, Any], state: BlockState) -> str:
  29. return cast(str, token["raw"])
  30. def emphasis(self, token: Dict[str, Any], state: BlockState) -> str:
  31. return "*" + self.render_children(token, state) + "*"
  32. def strong(self, token: Dict[str, Any], state: BlockState) -> str:
  33. return "**" + self.render_children(token, state) + "**"
  34. def link(self, token: Dict[str, Any], state: BlockState) -> str:
  35. label = cast(str, token.get("label"))
  36. text = self.render_children(token, state)
  37. out = "[" + text + "]"
  38. if label:
  39. return out + "[" + label + "]"
  40. attrs = token["attrs"]
  41. url: str = attrs["url"]
  42. title = attrs.get("title")
  43. if text == url and not title:
  44. return "<" + text + ">"
  45. elif "mailto:" + text == url and not title:
  46. return "<" + text + ">"
  47. out += "("
  48. if "(" in url or ")" in url:
  49. out += "<" + url + ">"
  50. else:
  51. out += url
  52. if title:
  53. out += ' "' + title + '"'
  54. return out + ")"
  55. def image(self, token: Dict[str, Any], state: BlockState) -> str:
  56. return "!" + self.link(token, state)
  57. def codespan(self, token: Dict[str, Any], state: BlockState) -> str:
  58. return "`" + cast(str, token["raw"]) + "`"
  59. def linebreak(self, token: Dict[str, Any], state: BlockState) -> str:
  60. return " \n"
  61. def softbreak(self, token: Dict[str, Any], state: BlockState) -> str:
  62. return "\n"
  63. def blank_line(self, token: Dict[str, Any], state: BlockState) -> str:
  64. return ""
  65. def inline_html(self, token: Dict[str, Any], state: BlockState) -> str:
  66. return cast(str, token["raw"])
  67. def paragraph(self, token: Dict[str, Any], state: BlockState) -> str:
  68. text = self.render_children(token, state)
  69. return text + "\n\n"
  70. def heading(self, token: Dict[str, Any], state: BlockState) -> str:
  71. level = cast(int, token["attrs"]["level"])
  72. marker = "#" * level
  73. text = self.render_children(token, state)
  74. return marker + " " + text + "\n\n"
  75. def thematic_break(self, token: Dict[str, Any], state: BlockState) -> str:
  76. return "***\n\n"
  77. def block_text(self, token: Dict[str, Any], state: BlockState) -> str:
  78. return self.render_children(token, state) + "\n"
  79. def block_code(self, token: Dict[str, Any], state: BlockState) -> str:
  80. attrs = token.get("attrs", {})
  81. info = cast(str, attrs.get("info", ""))
  82. code = cast(str, token["raw"])
  83. if code and code[-1] != "\n":
  84. code += "\n"
  85. marker = token.get("marker")
  86. if not marker:
  87. marker = _get_fenced_marker(code)
  88. marker2 = cast(str, marker)
  89. return marker2 + info + "\n" + code + marker2 + "\n\n"
  90. def block_quote(self, token: Dict[str, Any], state: BlockState) -> str:
  91. text = indent(self.render_children(token, state), "> ", lambda _: True)
  92. text = text.rstrip("> \n")
  93. return text + "\n\n"
  94. def block_html(self, token: Dict[str, Any], state: BlockState) -> str:
  95. return cast(str, token["raw"]) + "\n\n"
  96. def block_error(self, token: Dict[str, Any], state: BlockState) -> str:
  97. return ""
  98. def list(self, token: Dict[str, Any], state: BlockState) -> str:
  99. return render_list(self, token, state)
  100. def _get_fenced_marker(code: str) -> str:
  101. found = fenced_re.findall(code)
  102. if not found:
  103. return "```"
  104. ticks = [] # `
  105. waves = [] # ~
  106. for s in found:
  107. if s[0] == "`":
  108. ticks.append(len(s))
  109. else:
  110. waves.append(len(s))
  111. if not ticks:
  112. return "```"
  113. if not waves:
  114. return "~~~"
  115. return "`" * (max(ticks) + 1)