Coverage for fpdf2_textindex / md_emphasis.py: 91.80%
53 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 15:45 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-24 15:45 +0000
1"""Markdown Emphasis."""
3from __future__ import annotations
5import enum
6import re
7from typing import TYPE_CHECKING
9import fpdf
10from typing_extensions import Self
13class MDEmphasis(enum.IntFlag):
14 """Markdown Emphasis."""
16 if TYPE_CHECKING:
17 MARKER_PATTERN: str
18 MARKERS: dict[MDEmphasis, str]
19 MD_PATTERN: re.Pattern[str]
20 NONE: MDEmphasis
22 BOLD = 1
23 """Bold."""
25 ITALICS = 2
26 """Italics."""
28 UNDERLINE = 4
29 """Underline."""
31 STRIKETHROUGH = 8
32 """Strikethrough."""
34 @property
35 def font_style(self) -> str:
36 """The corresponding [fpdf.FPDF.font_style](https://py-pdf.github.io/fpdf2/fpdf/graphics_state.html#fpdf.graphics_state.GraphicsState.font_style)."""
37 return "".join(
38 str(mde.name)[0] for mde in type(self) if mde.value & self
39 )
41 @property
42 def marker(self) -> str:
43 """The marker."""
44 return "".join(
45 type(self).MARKERS[mde] for mde in type(self) if mde.value & self
46 )
48 @property
49 def text_emphasis(self) -> fpdf.enums.TextEmphasis:
50 """The corresponding [fpdf.enums.TextEmphasis](https://py-pdf.github.io/fpdf2/fpdf/enums.html#fpdf.enums.TextEmphasis)."""
51 return fpdf.enums.TextEmphasis.coerce(self.font_style)
53 def format(self, text: str) -> str:
54 """Formats a text according to this :py:class:`MDEmphasis`.
56 Args:
57 text: The text to format.
59 Returns:
60 The formatted text.
61 """
62 prefix = "".join(mde.marker for mde in type(self) if mde.value & self)
63 suffix = prefix[::-1]
64 return f"{prefix:s}{text:s}{suffix:s}"
66 @classmethod
67 def parse(cls, text: str) -> tuple[str, Self]:
68 """Parses a text and returns the "inner", unformatted text and the
69 corresponding :py:class:`MDEmphasis`.
71 Args:
72 text: The text to parse.
74 Returns:
75 The "inner", unformatted text and the corresponding
76 :py:class:`MDEmphasis`.
78 Raises:
79 ValueError: If the end emphasis does not correspond to the mirrored
80 start emphasis.
81 """
82 label_emphasis = MDEmphasis.NONE
83 match = cls.MD_PATTERN.match(text)
84 if not match: 84 ↛ 85line 84 didn't jump to line 85 because the condition on line 84 was never true
85 return text, label_emphasis
86 start_emph = match.group("md_start")
87 end_emph = match.group("md_end")
88 if start_emph != end_emph[::-1]: 88 ↛ 89line 88 didn't jump to line 89 because the condition on line 88 was never true
89 msg = f"invalid (not-mirrored) emphasis: {match.group(0)!r:s}"
90 raise ValueError(msg)
91 for mde in MDEmphasis:
92 if mde.marker in start_emph:
93 label_emphasis |= mde
94 text = match.group("text")
95 return text, label_emphasis
97 @classmethod
98 def remove(cls, text: str) -> str:
99 """Removes the markdown emphasis markers from a text.
101 Args:
102 text: The text to remove the markers from.
104 Returns:
105 The "inner", unformatted text.
106 """
107 return cls.parse(text)[0]
110MDEmphasis.NONE = MDEmphasis(0)
111"""None."""
113# Add markers and patterns for formatting and parsing
114MDEmphasis.MARKERS = {MDEmphasis.NONE: ""}
115MDEmphasis.MARKERS.update(
116 {
117 mde: getattr(fpdf.FPDF, f"MARKDOWN_{mde.name:s}_MARKER")
118 for mde in MDEmphasis
119 }
120)
121MDEmphasis.MARKER_PATTERN = (
122 r"(?<!\\)(?P<{name:s}>"
123 rf"(?:{'|'.join(re.escape(mde.marker) for mde in MDEmphasis):s})"
124 r"{{0,4}})"
125)
126MDEmphasis.MD_PATTERN = re.compile(
127 rf"{MDEmphasis.MARKER_PATTERN.format(name='md_start'):s}"
128 rf"(?!\*|~|_|-)(?P<text>.*)(?<!\*|~|_|-)"
129 rf"{MDEmphasis.MARKER_PATTERN.format(name='md_end'):s}"
130)