| """ |
| pygments.formatters.rtf |
| ~~~~~~~~~~~~~~~~~~~~~~~ |
| |
| A formatter that generates RTF files. |
| |
| :copyright: Copyright 2006-2025 by the Pygments team, see AUTHORS. |
| :license: BSD, see LICENSE for details. |
| """ |
|
|
| from collections import OrderedDict |
| from pygments.formatter import Formatter |
| from pygments.style import _ansimap |
| from pygments.util import get_bool_opt, get_int_opt, get_list_opt, surrogatepair |
|
|
|
|
| __all__ = ['RtfFormatter'] |
|
|
|
|
| class RtfFormatter(Formatter): |
| """ |
| Format tokens as RTF markup. This formatter automatically outputs full RTF |
| documents with color information and other useful stuff. Perfect for Copy and |
| Paste into Microsoft(R) Word(R) documents. |
| |
| Please note that ``encoding`` and ``outencoding`` options are ignored. |
| The RTF format is ASCII natively, but handles unicode characters correctly |
| thanks to escape sequences. |
| |
| .. versionadded:: 0.6 |
| |
| Additional options accepted: |
| |
| `style` |
| The style to use, can be a string or a Style subclass (default: |
| ``'default'``). |
| |
| `fontface` |
| The used font family, for example ``Bitstream Vera Sans``. Defaults to |
| some generic font which is supposed to have fixed width. |
| |
| `fontsize` |
| Size of the font used. Size is specified in half points. The |
| default is 24 half-points, giving a size 12 font. |
| |
| .. versionadded:: 2.0 |
| |
| `linenos` |
| Turn on line numbering (default: ``False``). |
| |
| .. versionadded:: 2.18 |
| |
| `lineno_fontsize` |
| Font size for line numbers. Size is specified in half points |
| (default: `fontsize`). |
| |
| .. versionadded:: 2.18 |
| |
| `lineno_padding` |
| Number of spaces between the (inline) line numbers and the |
| source code (default: ``2``). |
| |
| .. versionadded:: 2.18 |
| |
| `linenostart` |
| The line number for the first line (default: ``1``). |
| |
| .. versionadded:: 2.18 |
| |
| `linenostep` |
| If set to a number n > 1, only every nth line number is printed. |
| |
| .. versionadded:: 2.18 |
| |
| `lineno_color` |
| Color for line numbers specified as a hex triplet, e.g. ``'5e5e5e'``. |
| Defaults to the style's line number color if it is a hex triplet, |
| otherwise ansi bright black. |
| |
| .. versionadded:: 2.18 |
| |
| `hl_lines` |
| Specify a list of lines to be highlighted, as line numbers separated by |
| spaces, e.g. ``'3 7 8'``. The line numbers are relative to the input |
| (i.e. the first line is line 1) unless `hl_linenostart` is set. |
| |
| .. versionadded:: 2.18 |
| |
| `hl_color` |
| Color for highlighting the lines specified in `hl_lines`, specified as |
| a hex triplet (default: style's `highlight_color`). |
| |
| .. versionadded:: 2.18 |
| |
| `hl_linenostart` |
| If set to ``True`` line numbers in `hl_lines` are specified |
| relative to `linenostart` (default ``False``). |
| |
| .. versionadded:: 2.18 |
| """ |
| name = 'RTF' |
| aliases = ['rtf'] |
| filenames = ['*.rtf'] |
|
|
| def __init__(self, **options): |
| r""" |
| Additional options accepted: |
| |
| ``fontface`` |
| Name of the font used. Could for example be ``'Courier New'`` |
| to further specify the default which is ``'\fmodern'``. The RTF |
| specification claims that ``\fmodern`` are "Fixed-pitch serif |
| and sans serif fonts". Hope every RTF implementation thinks |
| the same about modern... |
| |
| """ |
| Formatter.__init__(self, **options) |
| self.fontface = options.get('fontface') or '' |
| self.fontsize = get_int_opt(options, 'fontsize', 0) |
| self.linenos = get_bool_opt(options, 'linenos', False) |
| self.lineno_fontsize = get_int_opt(options, 'lineno_fontsize', |
| self.fontsize) |
| self.lineno_padding = get_int_opt(options, 'lineno_padding', 2) |
| self.linenostart = abs(get_int_opt(options, 'linenostart', 1)) |
| self.linenostep = abs(get_int_opt(options, 'linenostep', 1)) |
| self.hl_linenostart = get_bool_opt(options, 'hl_linenostart', False) |
|
|
| self.hl_color = options.get('hl_color', '') |
| if not self.hl_color: |
| self.hl_color = self.style.highlight_color |
|
|
| self.hl_lines = [] |
| for lineno in get_list_opt(options, 'hl_lines', []): |
| try: |
| lineno = int(lineno) |
| if self.hl_linenostart: |
| lineno = lineno - self.linenostart + 1 |
| self.hl_lines.append(lineno) |
| except ValueError: |
| pass |
|
|
| self.lineno_color = options.get('lineno_color', '') |
| if not self.lineno_color: |
| if self.style.line_number_color == 'inherit': |
| |
| |
| self.lineno_color = _ansimap['ansibrightblack'] |
| else: |
| |
| |
| self.lineno_color = self.style.line_number_color |
|
|
| self.color_mapping = self._create_color_mapping() |
|
|
| def _escape(self, text): |
| return text.replace('\\', '\\\\') \ |
| .replace('{', '\\{') \ |
| .replace('}', '\\}') |
|
|
| def _escape_text(self, text): |
| |
| if not text: |
| return '' |
|
|
| |
| text = self._escape(text) |
|
|
| buf = [] |
| for c in text: |
| cn = ord(c) |
| if cn < (2**7): |
| |
| buf.append(str(c)) |
| elif (2**7) <= cn < (2**16): |
| |
| buf.append('{\\u%d}' % cn) |
| elif (2**16) <= cn: |
| |
| |
| buf.append('{\\u%d}{\\u%d}' % surrogatepair(cn)) |
|
|
| return ''.join(buf).replace('\n', '\\par') |
|
|
| @staticmethod |
| def hex_to_rtf_color(hex_color): |
| if hex_color[0] == "#": |
| hex_color = hex_color[1:] |
|
|
| return '\\red%d\\green%d\\blue%d;' % ( |
| int(hex_color[0:2], 16), |
| int(hex_color[2:4], 16), |
| int(hex_color[4:6], 16) |
| ) |
|
|
| def _split_tokens_on_newlines(self, tokensource): |
| """ |
| Split tokens containing newline characters into multiple token |
| each representing a line of the input file. Needed for numbering |
| lines of e.g. multiline comments. |
| """ |
| for ttype, value in tokensource: |
| if value == '\n': |
| yield (ttype, value) |
| elif "\n" in value: |
| lines = value.split("\n") |
| for line in lines[:-1]: |
| yield (ttype, line+"\n") |
| if lines[-1]: |
| yield (ttype, lines[-1]) |
| else: |
| yield (ttype, value) |
|
|
| def _create_color_mapping(self): |
| """ |
| Create a mapping of style hex colors to index/offset in |
| the RTF color table. |
| """ |
| color_mapping = OrderedDict() |
| offset = 1 |
|
|
| if self.linenos: |
| color_mapping[self.lineno_color] = offset |
| offset += 1 |
|
|
| if self.hl_lines: |
| color_mapping[self.hl_color] = offset |
| offset += 1 |
|
|
| for _, style in self.style: |
| for color in style['color'], style['bgcolor'], style['border']: |
| if color and color not in color_mapping: |
| color_mapping[color] = offset |
| offset += 1 |
|
|
| return color_mapping |
|
|
| @property |
| def _lineno_template(self): |
| if self.lineno_fontsize != self.fontsize: |
| return '{{\\fs{} \\cf{} %s{}}}'.format(self.lineno_fontsize, |
| self.color_mapping[self.lineno_color], |
| " " * self.lineno_padding) |
|
|
| return '{{\\cf{} %s{}}}'.format(self.color_mapping[self.lineno_color], |
| " " * self.lineno_padding) |
|
|
| @property |
| def _hl_open_str(self): |
| return rf'{{\highlight{self.color_mapping[self.hl_color]} ' |
|
|
| @property |
| def _rtf_header(self): |
| lines = [] |
| |
| lines.append('{\\rtf1\\ansi\\uc0\\deff0' |
| '{\\fonttbl{\\f0\\fmodern\\fprq1\\fcharset0%s;}}' |
| % (self.fontface and ' ' |
| + self._escape(self.fontface) or '')) |
|
|
| |
| lines.append('{\\colortbl;') |
| for color, _ in self.color_mapping.items(): |
| lines.append(self.hex_to_rtf_color(color)) |
| lines.append('}') |
|
|
| |
| lines.append('\\f0\\sa0') |
| if self.fontsize: |
| lines.append('\\fs%d' % self.fontsize) |
|
|
| |
| |
| |
| lines.append('\\dntblnsbdb') |
|
|
| return lines |
|
|
| def format_unencoded(self, tokensource, outfile): |
| for line in self._rtf_header: |
| outfile.write(line + "\n") |
|
|
| tokensource = self._split_tokens_on_newlines(tokensource) |
|
|
| |
| if self.linenos: |
| line_count = 0 |
| tokens = [] |
| for ttype, value in tokensource: |
| tokens.append((ttype, value)) |
| if value.endswith("\n"): |
| line_count += 1 |
|
|
| |
| linenos_width = len(str(line_count+self.linenostart-1)) |
|
|
| tokensource = tokens |
|
|
| |
| lineno = 1 |
| start_new_line = True |
| for ttype, value in tokensource: |
| if start_new_line and lineno in self.hl_lines: |
| outfile.write(self._hl_open_str) |
|
|
| if start_new_line and self.linenos: |
| if (lineno-self.linenostart+1)%self.linenostep == 0: |
| current_lineno = lineno + self.linenostart - 1 |
| lineno_str = str(current_lineno).rjust(linenos_width) |
| else: |
| lineno_str = "".rjust(linenos_width) |
| outfile.write(self._lineno_template % lineno_str) |
|
|
| while not self.style.styles_token(ttype) and ttype.parent: |
| ttype = ttype.parent |
| style = self.style.style_for_token(ttype) |
| buf = [] |
| if style['bgcolor']: |
| buf.append('\\cb%d' % self.color_mapping[style['bgcolor']]) |
| if style['color']: |
| buf.append('\\cf%d' % self.color_mapping[style['color']]) |
| if style['bold']: |
| buf.append('\\b') |
| if style['italic']: |
| buf.append('\\i') |
| if style['underline']: |
| buf.append('\\ul') |
| if style['border']: |
| buf.append('\\chbrdr\\chcfpat%d' % |
| self.color_mapping[style['border']]) |
| start = ''.join(buf) |
| if start: |
| outfile.write(f'{{{start} ') |
| outfile.write(self._escape_text(value)) |
| if start: |
| outfile.write('}') |
| start_new_line = False |
|
|
| |
| if value.endswith("\n"): |
| |
| if lineno in self.hl_lines: |
| outfile.write('}') |
| |
| outfile.write("\n") |
|
|
| start_new_line = True |
| lineno += 1 |
|
|
| outfile.write('}\n') |
|
|