Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/mdit_py_plugins/amsmath/__init__.py: 47%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

51 statements  

1"""An extension to capture amsmath latex environments.""" 

2 

3from __future__ import annotations 

4 

5import re 

6from typing import TYPE_CHECKING, Callable, Sequence 

7 

8from markdown_it import MarkdownIt 

9from markdown_it.common.utils import escapeHtml 

10from markdown_it.rules_block import StateBlock 

11 

12from mdit_py_plugins.utils import is_code_block 

13 

14if TYPE_CHECKING: 

15 from markdown_it.renderer import RendererProtocol 

16 from markdown_it.token import Token 

17 from markdown_it.utils import EnvType, OptionsDict 

18 

19# Taken from amsmath version 2.1 

20# http://anorien.csc.warwick.ac.uk/mirrors/CTAN/macros/latex/required/amsmath/amsldoc.pdf 

21ENVIRONMENTS = [ 

22 # 3.2 single equation with an automatically gen-erated number 

23 "equation", 

24 # 3.3 variation equation, used for equations that dont fit on a single line 

25 "multline", 

26 # 3.5 a group of consecutive equations when there is no alignment desired among them 

27 "gather", 

28 # 3.6 Used for two or more equations when vertical alignment is desired 

29 "align", 

30 # allows the horizontal space between equationsto be explicitly specified. 

31 "alignat", 

32 # stretches the space betweenthe equation columns to the maximum possible width 

33 "flalign", 

34 # 4.1 The pmatrix, bmatrix, Bmatrix, vmatrix and Vmatrix have (respectively) 

35 # (),[],{},||,and ‖‖ delimiters built in. 

36 "matrix", 

37 "pmatrix", 

38 "bmatrix", 

39 "Bmatrix", 

40 "vmatrix", 

41 "Vmatrix", 

42 # eqnarray is another math environment, it is not part of amsmath, 

43 # and note that it is better to use align or equation+split instead 

44 "eqnarray", 

45] 

46# other "non-top-level" environments: 

47 

48# 3.4 the split environment is for single equations that are too long to fit on one line 

49# and hence must be split into multiple lines, 

50# it is intended for use only inside some other displayed equation structure, 

51# usually an equation, align, or gather environment 

52 

53# 3.7 variants gathered, aligned,and alignedat are provided 

54# whose total width is the actual width of the contents; 

55# thus they can be used as a component in a containing expression 

56 

57RE_OPEN = r"\\begin\{(" + "|".join(ENVIRONMENTS) + r")([\*]?)\}" 

58 

59 

60def amsmath_plugin( 

61 md: MarkdownIt, *, renderer: Callable[[str], str] | None = None 

62) -> None: 

63 """Parses TeX math equations, without any surrounding delimiters, 

64 only for top-level `amsmath <https://ctan.org/pkg/amsmath>`__ environments: 

65 

66 .. code-block:: latex 

67 

68 \\begin{gather*} 

69 a_1=b_1+c_1\\\\ 

70 a_2=b_2+c_2-d_2+e_2 

71 \\end{gather*} 

72 

73 :param renderer: Function to render content, by default escapes HTML 

74 

75 """ 

76 md.block.ruler.before( 

77 "blockquote", 

78 "amsmath", 

79 amsmath_block, 

80 {"alt": ["paragraph", "reference", "blockquote", "list", "footnote_def"]}, 

81 ) 

82 

83 _renderer = (lambda content: escapeHtml(content)) if renderer is None else renderer 

84 

85 def render_amsmath_block( 

86 self: RendererProtocol, 

87 tokens: Sequence[Token], 

88 idx: int, 

89 options: OptionsDict, 

90 env: EnvType, 

91 ) -> str: 

92 content = _renderer(str(tokens[idx].content)) 

93 return f'<div class="math amsmath">\n{content}\n</div>\n' 

94 

95 md.add_render_rule("amsmath", render_amsmath_block) 

96 

97 

98def amsmath_block( 

99 state: StateBlock, startLine: int, endLine: int, silent: bool 

100) -> bool: 

101 # note the code principally follows the logic in markdown_it/rules_block/fence.py, 

102 # except that: 

103 # (a) it allows for closing tag on same line as opening tag 

104 # (b) it does not allow for opening tag without closing tag (i.e. no auto-closing) 

105 

106 if is_code_block(state, startLine): 

107 return False 

108 

109 # does the first line contain the beginning of an amsmath environment 

110 first_start = state.bMarks[startLine] + state.tShift[startLine] 

111 first_end = state.eMarks[startLine] 

112 first_text = state.src[first_start:first_end] 

113 

114 if not (match_open := re.match(RE_OPEN, first_text)): 

115 return False 

116 

117 # construct the closing tag 

118 environment = match_open.group(1) 

119 numbered = match_open.group(2) 

120 closing = rf"\end{{{match_open.group(1)}{match_open.group(2)}}}" 

121 

122 # start looking for the closing tag, including the current line 

123 nextLine = startLine - 1 

124 

125 while True: 

126 nextLine += 1 

127 if nextLine >= endLine: 

128 # reached the end of the block without finding the closing tag 

129 return False 

130 

131 next_start = state.bMarks[nextLine] + state.tShift[nextLine] 

132 next_end = state.eMarks[nextLine] 

133 if next_start < first_end and state.sCount[nextLine] < state.blkIndent: 

134 # non-empty line with negative indent should stop the list: 

135 # - \begin{align} 

136 # test 

137 return False 

138 

139 if state.src[next_start:next_end].rstrip().endswith(closing): 

140 # found the closing tag 

141 break 

142 

143 state.line = nextLine + 1 

144 

145 if not silent: 

146 token = state.push("amsmath", "math", 0) 

147 token.block = True 

148 token.content = state.getLines( 

149 startLine, state.line, state.sCount[startLine], False 

150 ) 

151 token.meta = {"environment": environment, "numbered": numbered} 

152 token.map = [startLine, nextLine] 

153 

154 return True