quizcomp.parser.renderer.tex
1import logging 2import typing 3 4import markdown_it 5 6import quizcomp.model.constants 7import quizcomp.parser.ast 8import quizcomp.parser.common 9import quizcomp.parser.renderer.base 10import quizcomp.parser.style 11 12_logger = logging.getLogger(__name__) 13 14TEX_REPLACEMENTS: typing.Dict[str, str] = { 15 # Specially handle braces and slashes to avoid clobbering other replacements. 16 '{': 'ZZZzzz OPEN BRACE REPLACEMENT zzzZZZ', 17 '}': 'ZZZzzz CLOSE BRACE REPLACEMENT zzzZZZ', 18 '\\': 'ZZZzzz BACKSLASH REPLACEMENT zzzZZZ', 19 20 '|': '\\textbar{}', 21 '$': '\\$', 22 '#': '\\#', 23 '%': '\\%', 24 '^': '\\^', 25 '_': '\\_', 26 'π': '$\\pi$', 27 '`': '\\`{}', 28 29 'ZZZzzz OPEN BRACE REPLACEMENT zzzZZZ': '\\{', 30 'ZZZzzz CLOSE BRACE REPLACEMENT zzzZZZ': '\\}', 31 'ZZZzzz BACKSLASH REPLACEMENT zzzZZZ': '\\textbackslash{}', 32} 33 34VERB_CHARACTERS: typing.List[str] = ['|', '!', '@', '#', '$', '^', '&', '-', '_', '=', '+'] 35 36TEX_TEXT_TABLE_ALIGNMENT: typing.Dict[str, str] = { 37 quizcomp.parser.style.ALLOWED_VALUES_ALIGNMENT_LEFT: 'l', 38 quizcomp.parser.style.ALLOWED_VALUES_ALIGNMENT_CENTER: 'c', 39 quizcomp.parser.style.ALLOWED_VALUES_ALIGNMENT_RIGHT: 'r', 40} 41 42# HTML and CommonMark support six levels of headings, 43# we will use these analogues for TeX. 44# The only standard TeX heading we are not using is \part{}, 45# which is before \chapter{}. 46HEADINGS: typing.List[str] = [ 47 'chapter', 48 'section', 49 'subsection', 50 'subsubsection', 51 'paragraph', 52 'subparagraph', 53] 54 55class QuizComposerRendererTex(quizcomp.parser.renderer.base.QuizComposerRendererBase): 56 """ A renderer for TeX. """ 57 58 __output__ = quizcomp.model.constants.Format.TEX.value 59 60 def _container_block(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 61 # Pull any style attatched to this block and put it in a copy of the context. 62 context, block_style = quizcomp.parser.common.handle_block_style(node.attributes, context) 63 64 # Compute fixes using different styles depending on if this block is root. 65 # If we are root, then we need to use all style. 66 # If we are not root, then earlier blocks would have already applied other style, 67 # and we only need the block style. 68 active_style = block_style 69 if (node.get(quizcomp.parser.common.TOKEN_META_KEY_ROOT, False)): 70 active_style = context.style 71 72 prefixes, suffixes = quizcomp.parser.style.compute_tex_fixes(active_style) 73 child_content = [self._render_node(child, context) for child in node.children] 74 75 content = prefixes + child_content + list(reversed(suffixes)) 76 77 return "\n\n".join(content) 78 79 def _text(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 80 return tex_escape(node.text) 81 82 def _softbreak(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 83 return "\n" 84 85 def _hardbreak(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 86 return '~\\newline\n' 87 88 def _em(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 89 content = ''.join([self._render_node(child, context) for child in node.children]) 90 return r"\textit{%s}" % (content) 91 92 def _strong(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 93 content = ''.join([self._render_node(child, context) for child in node.children]) 94 return r"\textbf{%s}" % (content) 95 96 def _fence(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 97 language_string = '' 98 99 info = node.get('info', None) 100 if ((info is not None) and (len(info) > 0)): 101 language_string = f"[language={info}]" 102 103 return "\\begin{lstlisting}%s\n%s\n\\end{lstlisting}" % (language_string, node.text.rstrip()) 104 105 def _code_block(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 106 """ 107 This token is poorly named, it is actually an indented code block. 108 Treat it like a fence with no info string. 109 """ 110 111 return self._fence(node, context) 112 113 def _code_inline(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 114 text = node.text 115 116 delim = None 117 for char in VERB_CHARACTERS: 118 if (char not in text): 119 delim = char 120 break 121 122 if (delim is None): 123 raise ValueError("Could not find a delimiter to use with TeX's `\verb'.") 124 125 return f"\\verb{delim}{text}{delim}" 126 127 def _math_block(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 128 text = node.text.strip() 129 return f"$$\n{text}\n$$" 130 131 def _math_inline(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 132 text = node.text.strip() 133 return f"$ {text} $" 134 135 def _image(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 136 src = node.get('src', None) 137 if (src is None): 138 raise ValueError('No image source provided.') 139 140 width_float = quizcomp.parser.style.get_image_width(context.style) 141 142 return r"\includegraphics[width=%0.2f\textwidth]{%s}" % (width_float, str(src)) 143 144 def _link(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 145 text = ''.join([self._render_node(child, context) for child in node.children]).strip() 146 url = node.get('href', '') 147 148 if (len(text) == 0): 149 return r"\url{%s}" % (url) 150 151 return r"\href{%s}{%s}" % (url, text) 152 153 def _placeholder(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 154 text = tex_escape(node.text) 155 return r"\textsc{<%s>}" % (text) 156 157 def _table(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 158 border_table = quizcomp.parser.style.get_boolean_style_key( 159 context.style, 160 quizcomp.parser.style.KEY_TABLE_BORDER_TABLE, 161 quizcomp.parser.style.DEFAULT_TABLE_BORDER_TABLE, 162 ) 163 164 border_cells = quizcomp.parser.style.get_boolean_style_key( 165 context.style, 166 quizcomp.parser.style.KEY_TABLE_BORDER_CELLS, 167 quizcomp.parser.style.DEFAULT_TABLE_BORDER_CELLS, 168 ) 169 170 default_alignment = quizcomp.parser.style.get_alignment( 171 context.style, 172 quizcomp.parser.style.KEY_TEXT_ALIGN, 173 quizcomp.parser.style.ALLOWED_VALUES_ALIGNMENT_CENTER, 174 ) 175 176 column_infos = _discover_column_info(node) 177 178 column_join = '' 179 if (border_cells): 180 column_join = '|' 181 182 # Build column specifiers. 183 column_specifiers = [] 184 for column_info in column_infos: 185 raw_alignment = str(column_info.get('text-align', default_alignment)) 186 column_specifiers.append(TEX_TEXT_TABLE_ALIGNMENT.get(raw_alignment, quizcomp.parser.style.ALLOWED_VALUES_ALIGNMENT_LEFT)) 187 column_spec = column_join.join(column_specifiers) 188 189 if (border_table): 190 column_spec = '|' + column_spec + '|' 191 192 lines = [ 193 r'\begin{tabular}{ ' + column_spec + ' }', 194 ] 195 196 if (border_table): 197 lines.append(r'\hline') 198 199 lines.append("\n".join([self._render_node(child, context) for child in node.children])) 200 201 if (border_table): 202 lines.append(r'\hline') 203 204 lines += [ 205 r'\end{tabular}', 206 ] 207 208 lines = _apply_tex_table_style(context.style, lines) 209 210 return "\n".join(lines) 211 212 def _thead(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 213 head_rule = quizcomp.parser.style.get_boolean_style_key( 214 context.style, 215 quizcomp.parser.style.KEY_TABLE_HEAD_RULE, 216 quizcomp.parser.style.DEFAULT_TABLE_HEAD_RULE, 217 ) 218 219 content = "\n".join([self._render_node(child, context) for child in node.children]) 220 221 if (head_rule): 222 content += '\n\\hline' 223 224 return content 225 226 def _tbody(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 227 border_cells = quizcomp.parser.style.get_boolean_style_key( 228 context.style, 229 quizcomp.parser.style.KEY_TABLE_BORDER_CELLS, 230 quizcomp.parser.style.DEFAULT_TABLE_BORDER_CELLS, 231 ) 232 233 delim = "\n" 234 if (border_cells): 235 delim = "\n\\hline\n" 236 237 content = delim.join([self._render_node(child, context) for child in node.children]) 238 return content 239 240 def _tr(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 241 content = ' & '.join([self._render_node(child, context) for child in node.children]) 242 return content + ' \\\\' 243 244 def _th(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 245 bold = quizcomp.parser.style.get_boolean_style_key( 246 context.style, 247 quizcomp.parser.style.KEY_TABLE_HEAD_BOLD, 248 quizcomp.parser.style.DEFAULT_TABLE_HEAD_BOLD, 249 ) 250 251 content = ''.join([self._render_node(child, context) for child in node.children]) 252 253 if (bold): 254 content = "\\textbf{%s}" % (content) 255 256 return content 257 258 def _td(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 259 content = ''.join([self._render_node(child, context) for child in node.children]) 260 return content 261 262 def _bullet_list(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 263 items = '\n'.join([self._render_node(child, context) for child in node.children]) 264 return "\\begin{itemize}\n" + items + "\n\\end{itemize}" 265 266 def _ordered_list(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 267 items = '\n'.join([self._render_node(child, context) for child in node.children]) 268 return "\\begin{enumerate}\n" + items + "\n\\end{enumerate}" 269 270 def _list_item(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 271 content = ''.join([self._render_node(child, context) for child in node.children]) 272 return " \\item " + content 273 274 def _hr(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 275 return "\\hrulefill" 276 277 def _heading(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 278 level = self.parse_heading_level(node) 279 280 index = level - 1 281 if ((index < 0) or (index >= len(HEADINGS))): 282 raise ValueError(f"Heading index is out of range: {index}.") 283 284 heading = HEADINGS[index] 285 content = ''.join([self._render_node(child, context) for child in node.children]) 286 287 return "\\%s{%s}" % (heading, content) 288 289 def _blockquote(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 290 content = ''.join([self._render_node(child, context) for child in node.children]) 291 return "\\begin{quote}\n%s\n\\end{quote}" % (content) 292 293def tex_escape(text: str) -> str: 294 """ 295 Prepare normal text for tex. 296 """ 297 298 for key, value in TEX_REPLACEMENTS.items(): 299 text = text.replace(key, value) 300 301 return text 302 303def _apply_tex_table_style(style: typing.Dict[str, typing.Any], lines: typing.List[str]) -> typing.List[str]: 304 """ Build the style for a table. """ 305 306 # Height 307 height = max(1.0, float(style.get(quizcomp.parser.style.KEY_TABLE_CELL_HEIGHT, quizcomp.parser.style.DEFAULT_TABLE_CELL_HEIGHT))) 308 prefix = [ 309 r'\begingroup', 310 r'\renewcommand{\arraystretch}{%0.2f}' % (height), 311 ] 312 lines = prefix + lines 313 lines.append(r'\endgroup') 314 315 # Width 316 width = max(1.0, float(style.get(quizcomp.parser.style.KEY_TABLE_CELL_WIDTH, quizcomp.parser.style.DEFAULT_TABLE_CELL_WIDTH))) 317 prefix = [ 318 r'\begingroup', 319 r'\setlength{\tabcolsep}{%0.2fem}' % (width), 320 ] 321 lines = prefix + lines 322 lines.append(r'\endgroup') 323 324 return lines 325 326def _discover_column_info( 327 node: quizcomp.parser.ast.ASTNode, 328 info: typing.Union[typing.List[typing.Dict[str, typing.Any]], None] = None, 329 ) -> typing.List[typing.Dict[str, typing.Any]]: 330 """ 331 Descend into a table to figure out how many columns there are and if a column has alignment information. 332 We will not make assumptions about symmetric rows or existence of headers. 333 The largest row will determine width, and the last instance of styling information in a column will determine alignment. 334 """ 335 336 if (info is None): 337 info = [] 338 339 type = node.type 340 if (type not in {'table', 'thead', 'tbody', 'tr', 'td', 'th'}): 341 return info 342 343 children = node.children 344 345 if (type in {'table', 'thead', 'tbody'}): 346 for child in children: 347 info = _discover_column_info(child, info = info) 348 return info 349 350 if (type == 'tr'): 351 for (i, child) in enumerate(children): 352 if (i >= len(info)): 353 info.append({}) 354 355 info[i] = _discover_column_info_cells(child, style = info[i]) 356 357 return info 358 359 _logger.warning("Unexpected table node type '%s'.", type) 360 return info 361 362def _discover_column_info_cells( 363 node: quizcomp.parser.ast.ASTNode, 364 style: typing.Union[typing.Dict[str, typing.Any], None] = None, 365 ) -> typing.Dict[str, typing.Any]: 366 """ Get the style for the table cells in this node. """ 367 368 if (style is None): 369 style = {} 370 371 type = node.type 372 if (type not in {'td', 'th'}): 373 return style 374 375 raw_style = node.get('style', '').strip() 376 if (len(raw_style) == 0): 377 return style 378 379 rules = [part.strip() for part in raw_style.split(';')] 380 for raw_rule in rules: 381 key, value = [part.strip() for part in raw_rule.split(':', maxsplit = 1)] 382 style[key] = value 383 384 return style 385 386def get_renderer(options: markdown_it.utils.OptionsDict) -> QuizComposerRendererTex: 387 """ Get this renderer and options. """ 388 389 return QuizComposerRendererTex()
TEX_REPLACEMENTS: Dict[str, str] =
{'{': 'ZZZzzz OPEN BRACE REPLACEMENT zzzZZZ', '}': 'ZZZzzz CLOSE BRACE REPLACEMENT zzzZZZ', '\\': 'ZZZzzz BACKSLASH REPLACEMENT zzzZZZ', '|': '\\textbar{}', '$': '\\$', '#': '\\#', '%': '\\%', '^': '\\^', '_': '\\_', 'π': '$\\pi$', '`': '\\`{}', 'ZZZzzz OPEN BRACE REPLACEMENT zzzZZZ': '\\{', 'ZZZzzz CLOSE BRACE REPLACEMENT zzzZZZ': '\\}', 'ZZZzzz BACKSLASH REPLACEMENT zzzZZZ': '\\textbackslash{}'}
VERB_CHARACTERS: List[str] =
['|', '!', '@', '#', '$', '^', '&', '-', '_', '=', '+']
TEX_TEXT_TABLE_ALIGNMENT: Dict[str, str] =
{'left': 'l', 'center': 'c', 'right': 'r'}
HEADINGS: List[str] =
['chapter', 'section', 'subsection', 'subsubsection', 'paragraph', 'subparagraph']
56class QuizComposerRendererTex(quizcomp.parser.renderer.base.QuizComposerRendererBase): 57 """ A renderer for TeX. """ 58 59 __output__ = quizcomp.model.constants.Format.TEX.value 60 61 def _container_block(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 62 # Pull any style attatched to this block and put it in a copy of the context. 63 context, block_style = quizcomp.parser.common.handle_block_style(node.attributes, context) 64 65 # Compute fixes using different styles depending on if this block is root. 66 # If we are root, then we need to use all style. 67 # If we are not root, then earlier blocks would have already applied other style, 68 # and we only need the block style. 69 active_style = block_style 70 if (node.get(quizcomp.parser.common.TOKEN_META_KEY_ROOT, False)): 71 active_style = context.style 72 73 prefixes, suffixes = quizcomp.parser.style.compute_tex_fixes(active_style) 74 child_content = [self._render_node(child, context) for child in node.children] 75 76 content = prefixes + child_content + list(reversed(suffixes)) 77 78 return "\n\n".join(content) 79 80 def _text(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 81 return tex_escape(node.text) 82 83 def _softbreak(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 84 return "\n" 85 86 def _hardbreak(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 87 return '~\\newline\n' 88 89 def _em(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 90 content = ''.join([self._render_node(child, context) for child in node.children]) 91 return r"\textit{%s}" % (content) 92 93 def _strong(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 94 content = ''.join([self._render_node(child, context) for child in node.children]) 95 return r"\textbf{%s}" % (content) 96 97 def _fence(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 98 language_string = '' 99 100 info = node.get('info', None) 101 if ((info is not None) and (len(info) > 0)): 102 language_string = f"[language={info}]" 103 104 return "\\begin{lstlisting}%s\n%s\n\\end{lstlisting}" % (language_string, node.text.rstrip()) 105 106 def _code_block(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 107 """ 108 This token is poorly named, it is actually an indented code block. 109 Treat it like a fence with no info string. 110 """ 111 112 return self._fence(node, context) 113 114 def _code_inline(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 115 text = node.text 116 117 delim = None 118 for char in VERB_CHARACTERS: 119 if (char not in text): 120 delim = char 121 break 122 123 if (delim is None): 124 raise ValueError("Could not find a delimiter to use with TeX's `\verb'.") 125 126 return f"\\verb{delim}{text}{delim}" 127 128 def _math_block(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 129 text = node.text.strip() 130 return f"$$\n{text}\n$$" 131 132 def _math_inline(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 133 text = node.text.strip() 134 return f"$ {text} $" 135 136 def _image(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 137 src = node.get('src', None) 138 if (src is None): 139 raise ValueError('No image source provided.') 140 141 width_float = quizcomp.parser.style.get_image_width(context.style) 142 143 return r"\includegraphics[width=%0.2f\textwidth]{%s}" % (width_float, str(src)) 144 145 def _link(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 146 text = ''.join([self._render_node(child, context) for child in node.children]).strip() 147 url = node.get('href', '') 148 149 if (len(text) == 0): 150 return r"\url{%s}" % (url) 151 152 return r"\href{%s}{%s}" % (url, text) 153 154 def _placeholder(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 155 text = tex_escape(node.text) 156 return r"\textsc{<%s>}" % (text) 157 158 def _table(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 159 border_table = quizcomp.parser.style.get_boolean_style_key( 160 context.style, 161 quizcomp.parser.style.KEY_TABLE_BORDER_TABLE, 162 quizcomp.parser.style.DEFAULT_TABLE_BORDER_TABLE, 163 ) 164 165 border_cells = quizcomp.parser.style.get_boolean_style_key( 166 context.style, 167 quizcomp.parser.style.KEY_TABLE_BORDER_CELLS, 168 quizcomp.parser.style.DEFAULT_TABLE_BORDER_CELLS, 169 ) 170 171 default_alignment = quizcomp.parser.style.get_alignment( 172 context.style, 173 quizcomp.parser.style.KEY_TEXT_ALIGN, 174 quizcomp.parser.style.ALLOWED_VALUES_ALIGNMENT_CENTER, 175 ) 176 177 column_infos = _discover_column_info(node) 178 179 column_join = '' 180 if (border_cells): 181 column_join = '|' 182 183 # Build column specifiers. 184 column_specifiers = [] 185 for column_info in column_infos: 186 raw_alignment = str(column_info.get('text-align', default_alignment)) 187 column_specifiers.append(TEX_TEXT_TABLE_ALIGNMENT.get(raw_alignment, quizcomp.parser.style.ALLOWED_VALUES_ALIGNMENT_LEFT)) 188 column_spec = column_join.join(column_specifiers) 189 190 if (border_table): 191 column_spec = '|' + column_spec + '|' 192 193 lines = [ 194 r'\begin{tabular}{ ' + column_spec + ' }', 195 ] 196 197 if (border_table): 198 lines.append(r'\hline') 199 200 lines.append("\n".join([self._render_node(child, context) for child in node.children])) 201 202 if (border_table): 203 lines.append(r'\hline') 204 205 lines += [ 206 r'\end{tabular}', 207 ] 208 209 lines = _apply_tex_table_style(context.style, lines) 210 211 return "\n".join(lines) 212 213 def _thead(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 214 head_rule = quizcomp.parser.style.get_boolean_style_key( 215 context.style, 216 quizcomp.parser.style.KEY_TABLE_HEAD_RULE, 217 quizcomp.parser.style.DEFAULT_TABLE_HEAD_RULE, 218 ) 219 220 content = "\n".join([self._render_node(child, context) for child in node.children]) 221 222 if (head_rule): 223 content += '\n\\hline' 224 225 return content 226 227 def _tbody(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 228 border_cells = quizcomp.parser.style.get_boolean_style_key( 229 context.style, 230 quizcomp.parser.style.KEY_TABLE_BORDER_CELLS, 231 quizcomp.parser.style.DEFAULT_TABLE_BORDER_CELLS, 232 ) 233 234 delim = "\n" 235 if (border_cells): 236 delim = "\n\\hline\n" 237 238 content = delim.join([self._render_node(child, context) for child in node.children]) 239 return content 240 241 def _tr(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 242 content = ' & '.join([self._render_node(child, context) for child in node.children]) 243 return content + ' \\\\' 244 245 def _th(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 246 bold = quizcomp.parser.style.get_boolean_style_key( 247 context.style, 248 quizcomp.parser.style.KEY_TABLE_HEAD_BOLD, 249 quizcomp.parser.style.DEFAULT_TABLE_HEAD_BOLD, 250 ) 251 252 content = ''.join([self._render_node(child, context) for child in node.children]) 253 254 if (bold): 255 content = "\\textbf{%s}" % (content) 256 257 return content 258 259 def _td(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 260 content = ''.join([self._render_node(child, context) for child in node.children]) 261 return content 262 263 def _bullet_list(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 264 items = '\n'.join([self._render_node(child, context) for child in node.children]) 265 return "\\begin{itemize}\n" + items + "\n\\end{itemize}" 266 267 def _ordered_list(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 268 items = '\n'.join([self._render_node(child, context) for child in node.children]) 269 return "\\begin{enumerate}\n" + items + "\n\\end{enumerate}" 270 271 def _list_item(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 272 content = ''.join([self._render_node(child, context) for child in node.children]) 273 return " \\item " + content 274 275 def _hr(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 276 return "\\hrulefill" 277 278 def _heading(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 279 level = self.parse_heading_level(node) 280 281 index = level - 1 282 if ((index < 0) or (index >= len(HEADINGS))): 283 raise ValueError(f"Heading index is out of range: {index}.") 284 285 heading = HEADINGS[index] 286 content = ''.join([self._render_node(child, context) for child in node.children]) 287 288 return "\\%s{%s}" % (heading, content) 289 290 def _blockquote(self, node: quizcomp.parser.ast.ASTNode, context: quizcomp.parser.common.RenderContext) -> str: 291 content = ''.join([self._render_node(child, context) for child in node.children]) 292 return "\\begin{quote}\n%s\n\\end{quote}" % (content)
A renderer for TeX.
def
tex_escape(text: str) -> str:
294def tex_escape(text: str) -> str: 295 """ 296 Prepare normal text for tex. 297 """ 298 299 for key, value in TEX_REPLACEMENTS.items(): 300 text = text.replace(key, value) 301 302 return text
Prepare normal text for tex.
387def get_renderer(options: markdown_it.utils.OptionsDict) -> QuizComposerRendererTex: 388 """ Get this renderer and options. """ 389 390 return QuizComposerRendererTex()
Get this renderer and options.