mermaidjs_printer.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
  4. """Class to generate files in mermaidjs format."""
  5. from __future__ import annotations
  6. from pylint.pyreverse.printer import EdgeType, NodeProperties, NodeType, Printer
  7. from pylint.pyreverse.utils import get_annotation_label
  8. class MermaidJSPrinter(Printer):
  9. """Printer for MermaidJS diagrams."""
  10. DEFAULT_COLOR = "black"
  11. NODES: dict[NodeType, str] = {
  12. NodeType.CLASS: "class",
  13. NodeType.PACKAGE: "class",
  14. }
  15. ARROWS: dict[EdgeType, str] = {
  16. EdgeType.INHERITS: "--|>",
  17. EdgeType.COMPOSITION: "--*",
  18. EdgeType.ASSOCIATION: "-->",
  19. EdgeType.AGGREGATION: "--o",
  20. EdgeType.USES: "-->",
  21. EdgeType.TYPE_DEPENDENCY: "..>",
  22. }
  23. def _open_graph(self) -> None:
  24. """Emit the header lines."""
  25. self.emit("classDiagram")
  26. self._inc_indent()
  27. def _escape_mermaid_text(self, text: str) -> str:
  28. """Escape characters that conflict with Markdown formatting."""
  29. text = text.replace("__", r"\_\_") # Double underscore → escaped
  30. return text
  31. def emit_node(
  32. self,
  33. name: str,
  34. type_: NodeType,
  35. properties: NodeProperties | None = None,
  36. ) -> None:
  37. """Create a new node.
  38. Nodes can be classes, packages, participants etc.
  39. """
  40. # pylint: disable=duplicate-code
  41. if properties is None:
  42. properties = NodeProperties(label=name)
  43. nodetype = self.NODES[type_]
  44. body = []
  45. if properties.attrs:
  46. # Escape attribute names to prevent Markdown formatting issues
  47. escaped_attrs = [
  48. self._escape_mermaid_text(attr) for attr in properties.attrs
  49. ]
  50. body.extend(escaped_attrs)
  51. if properties.methods:
  52. for func in properties.methods:
  53. args = self._get_method_arguments(func)
  54. # Escape method name and arguments
  55. escaped_method_name = self._escape_mermaid_text(func.name)
  56. escaped_args = [self._escape_mermaid_text(arg) for arg in args]
  57. line = f"{escaped_method_name}({', '.join(escaped_args)})"
  58. line += "*" if func.is_abstract() else ""
  59. if func.returns:
  60. # Escape return type annotation
  61. return_type = get_annotation_label(func.returns)
  62. escaped_return_type = self._escape_mermaid_text(return_type)
  63. line += f" {escaped_return_type}"
  64. body.append(line)
  65. name = name.split(".")[-1]
  66. self.emit(f"{nodetype} {name} {{")
  67. self._inc_indent()
  68. for line in body:
  69. self.emit(line)
  70. self._dec_indent()
  71. self.emit("}")
  72. # apply style for colored output
  73. if properties.color and properties.color != self.DEFAULT_COLOR:
  74. self.emit(f"style {name} fill:{properties.color}")
  75. def emit_edge(
  76. self,
  77. from_node: str,
  78. to_node: str,
  79. type_: EdgeType,
  80. label: str | None = None,
  81. ) -> None:
  82. """Create an edge from one node to another to display relationships."""
  83. from_node = from_node.split(".")[-1]
  84. to_node = to_node.split(".")[-1]
  85. edge = f"{from_node} {self.ARROWS[type_]} {to_node}"
  86. if label:
  87. edge += f" : {self._escape_mermaid_text(label)}"
  88. self.emit(edge)
  89. def _close_graph(self) -> None:
  90. """Emit the lines needed to properly close the graph."""
  91. self._dec_indent()
  92. class HTMLMermaidJSPrinter(MermaidJSPrinter):
  93. """Printer for MermaidJS diagrams wrapped in a html boilerplate."""
  94. HTML_OPEN_BOILERPLATE = """<html>
  95. <body>
  96. <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
  97. <div class="mermaid">
  98. """
  99. HTML_CLOSE_BOILERPLATE = """
  100. </div>
  101. </body>
  102. </html>
  103. """
  104. GRAPH_INDENT_LEVEL = 4
  105. def _open_graph(self) -> None:
  106. self.emit(self.HTML_OPEN_BOILERPLATE)
  107. for _ in range(self.GRAPH_INDENT_LEVEL):
  108. self._inc_indent()
  109. super()._open_graph()
  110. def _close_graph(self) -> None:
  111. for _ in range(self.GRAPH_INDENT_LEVEL):
  112. self._dec_indent()
  113. self.emit(self.HTML_CLOSE_BOILERPLATE)