table.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import re
  2. from typing import (
  3. TYPE_CHECKING,
  4. Any,
  5. Dict,
  6. List,
  7. Match,
  8. Optional,
  9. Tuple,
  10. Union,
  11. )
  12. from ..helpers import PREVENT_BACKSLASH
  13. if TYPE_CHECKING:
  14. from ..block_parser import BlockParser
  15. from ..core import BaseRenderer, BlockState
  16. from ..markdown import Markdown
  17. # https://michelf.ca/projects/php-markdown/extra/#table
  18. __all__ = ["table", "table_in_quote", "table_in_list"]
  19. TABLE_PATTERN = (
  20. r"^ {0,3}\|(?P<table_head>.+)\|[ \t]*\n"
  21. r" {0,3}\|(?P<table_align> *[-:]+[-| :]*)\|[ \t]*\n"
  22. r"(?P<table_body>(?: {0,3}\|.*\|[ \t]*(?:\n|$))*)\n*"
  23. )
  24. NP_TABLE_PATTERN = (
  25. r"^ {0,3}(?P<nptable_head>\S.*\|.*)\n"
  26. r" {0,3}(?P<nptable_align>[-:]+ *\|[-| :]*)\n"
  27. r"(?P<nptable_body>(?:.*\|.*(?:\n|$))*)\n*"
  28. )
  29. TABLE_CELL = re.compile(r"^ {0,3}\|(.+)\|[ \t]*$")
  30. CELL_SPLIT = re.compile(r" *" + PREVENT_BACKSLASH + r"\| *")
  31. ALIGN_CENTER = re.compile(r"^ *:-+: *$")
  32. ALIGN_LEFT = re.compile(r"^ *:-+ *$")
  33. ALIGN_RIGHT = re.compile(r"^ *-+: *$")
  34. def parse_table(block: "BlockParser", m: Match[str], state: "BlockState") -> Optional[int]:
  35. pos = m.end()
  36. header = m.group("table_head")
  37. align = m.group("table_align")
  38. thead, aligns = _process_thead(header, align)
  39. if not thead:
  40. return None
  41. assert aligns is not None
  42. rows = []
  43. body = m.group("table_body")
  44. for text in body.splitlines():
  45. m2 = TABLE_CELL.match(text)
  46. if not m2: # pragma: no cover
  47. return None
  48. row = _process_row(m2.group(1), aligns)
  49. if not row:
  50. return None
  51. rows.append(row)
  52. children = [thead, {"type": "table_body", "children": rows}]
  53. state.append_token({"type": "table", "children": children})
  54. return pos
  55. def parse_nptable(block: "BlockParser", m: Match[str], state: "BlockState") -> Optional[int]:
  56. header = m.group("nptable_head")
  57. align = m.group("nptable_align")
  58. thead, aligns = _process_thead(header, align)
  59. if not thead:
  60. return None
  61. assert aligns is not None
  62. rows = []
  63. body = m.group("nptable_body")
  64. for text in body.splitlines():
  65. row = _process_row(text, aligns)
  66. if not row:
  67. return None
  68. rows.append(row)
  69. children = [thead, {"type": "table_body", "children": rows}]
  70. state.append_token({"type": "table", "children": children})
  71. return m.end()
  72. def _process_thead(header: str, align: str) -> Union[Tuple[None, None], Tuple[Dict[str, Any], List[str]]]:
  73. headers = CELL_SPLIT.split(header)
  74. aligns = CELL_SPLIT.split(align)
  75. if len(headers) != len(aligns):
  76. return None, None
  77. for i, v in enumerate(aligns):
  78. if ALIGN_CENTER.match(v):
  79. aligns[i] = "center"
  80. elif ALIGN_LEFT.match(v):
  81. aligns[i] = "left"
  82. elif ALIGN_RIGHT.match(v):
  83. aligns[i] = "right"
  84. else:
  85. aligns[i] = None
  86. children = [
  87. {"type": "table_cell", "text": text.strip(), "attrs": {"align": aligns[i], "head": True}}
  88. for i, text in enumerate(headers)
  89. ]
  90. thead = {"type": "table_head", "children": children}
  91. return thead, aligns
  92. def _process_row(text: str, aligns: List[str]) -> Optional[Dict[str, Any]]:
  93. cells = CELL_SPLIT.split(text)
  94. if len(cells) != len(aligns):
  95. return None
  96. children = [
  97. {"type": "table_cell", "text": text.strip(), "attrs": {"align": aligns[i], "head": False}}
  98. for i, text in enumerate(cells)
  99. ]
  100. return {"type": "table_row", "children": children}
  101. def render_table(renderer: "BaseRenderer", text: str) -> str:
  102. return "<table>\n" + text + "</table>\n"
  103. def render_table_head(renderer: "BaseRenderer", text: str) -> str:
  104. return "<thead>\n<tr>\n" + text + "</tr>\n</thead>\n"
  105. def render_table_body(renderer: "BaseRenderer", text: str) -> str:
  106. return "<tbody>\n" + text + "</tbody>\n"
  107. def render_table_row(renderer: "BaseRenderer", text: str) -> str:
  108. return "<tr>\n" + text + "</tr>\n"
  109. def render_table_cell(renderer: "BaseRenderer", text: str, align: Optional[str] = None, head: bool = False) -> str:
  110. if head:
  111. tag = "th"
  112. else:
  113. tag = "td"
  114. html = " <" + tag
  115. if align:
  116. html += ' style="text-align:' + align + '"'
  117. return html + ">" + text + "</" + tag + ">\n"
  118. def table(md: "Markdown") -> None:
  119. """A mistune plugin to support table, spec defined at
  120. https://michelf.ca/projects/php-markdown/extra/#table
  121. Here is an example:
  122. .. code-block:: text
  123. First Header | Second Header
  124. ------------- | -------------
  125. Content Cell | Content Cell
  126. Content Cell | Content Cell
  127. :param md: Markdown instance
  128. """
  129. md.block.register("table", TABLE_PATTERN, parse_table, before="paragraph")
  130. md.block.register("nptable", NP_TABLE_PATTERN, parse_nptable, before="paragraph")
  131. if md.renderer and md.renderer.NAME == "html":
  132. md.renderer.register("table", render_table)
  133. md.renderer.register("table_head", render_table_head)
  134. md.renderer.register("table_body", render_table_body)
  135. md.renderer.register("table_row", render_table_row)
  136. md.renderer.register("table_cell", render_table_cell)
  137. def table_in_quote(md: "Markdown") -> None:
  138. """Enable table plugin in block quotes."""
  139. md.block.insert_rule(md.block.block_quote_rules, "table", before="paragraph")
  140. md.block.insert_rule(md.block.block_quote_rules, "nptable", before="paragraph")
  141. def table_in_list(md: "Markdown") -> None:
  142. """Enable table plugin in list."""
  143. md.block.insert_rule(md.block.list_rules, "table", before="paragraph")
  144. md.block.insert_rule(md.block.list_rules, "nptable", before="paragraph")