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']
class QuizComposerRendererTex(quizcomp.parser.renderer.base.QuizComposerRendererBase):
 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.

def get_renderer( options: markdown_it.utils.OptionsDict) -> QuizComposerRendererTex:
387def get_renderer(options: markdown_it.utils.OptionsDict) -> QuizComposerRendererTex:
388    """ Get this renderer and options. """
389
390    return QuizComposerRendererTex()

Get this renderer and options.