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

1"""Markdown Emphasis.""" 

2 

3from __future__ import annotations 

4 

5import enum 

6import re 

7from typing import TYPE_CHECKING 

8 

9import fpdf 

10from typing_extensions import Self 

11 

12 

13class MDEmphasis(enum.IntFlag): 

14 """Markdown Emphasis.""" 

15 

16 if TYPE_CHECKING: 

17 MARKER_PATTERN: str 

18 MARKERS: dict[MDEmphasis, str] 

19 MD_PATTERN: re.Pattern[str] 

20 NONE: MDEmphasis 

21 

22 BOLD = 1 

23 """Bold.""" 

24 

25 ITALICS = 2 

26 """Italics.""" 

27 

28 UNDERLINE = 4 

29 """Underline.""" 

30 

31 STRIKETHROUGH = 8 

32 """Strikethrough.""" 

33 

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 ) 

40 

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 ) 

47 

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) 

52 

53 def format(self, text: str) -> str: 

54 """Formats a text according to this :py:class:`MDEmphasis`. 

55 

56 Args: 

57 text: The text to format. 

58 

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}" 

65 

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`. 

70 

71 Args: 

72 text: The text to parse. 

73 

74 Returns: 

75 The "inner", unformatted text and the corresponding 

76 :py:class:`MDEmphasis`. 

77 

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 

96 

97 @classmethod 

98 def remove(cls, text: str) -> str: 

99 """Removes the markdown emphasis markers from a text. 

100 

101 Args: 

102 text: The text to remove the markers from. 

103 

104 Returns: 

105 The "inner", unformatted text. 

106 """ 

107 return cls.parse(text)[0] 

108 

109 

110MDEmphasis.NONE = MDEmphasis(0) 

111"""None.""" 

112 

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)