123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349 |
- """
- pygments.formatters.rtf
- ~~~~~~~~~~~~~~~~~~~~~~~
- A formatter that generates RTF files.
- :copyright: Copyright 2006-2024 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':
- # style color is the css value 'inherit'
- # default to ansi bright-black
- self.lineno_color = _ansimap['ansibrightblack']
- else:
- # style color is assumed to be a hex triplet as other
- # colors in pygments/style.py
- 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):
- # empty strings, should give a small performance improvement
- if not text:
- return ''
- # escape text
- text = self._escape(text)
- buf = []
- for c in text:
- cn = ord(c)
- if cn < (2**7):
- # ASCII character
- buf.append(str(c))
- elif (2**7) <= cn < (2**16):
- # single unicode escape sequence
- buf.append('{\\u%d}' % cn)
- elif (2**16) <= cn:
- # RTF limits unicode to 16 bits.
- # Force surrogate pairs
- 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 = []
- # rtf 1.8 header
- lines.append('{\\rtf1\\ansi\\uc0\\deff0'
- '{\\fonttbl{\\f0\\fmodern\\fprq1\\fcharset0%s;}}'
- % (self.fontface and ' '
- + self._escape(self.fontface) or ''))
- # color table
- lines.append('{\\colortbl;')
- for color, _ in self.color_mapping.items():
- lines.append(self.hex_to_rtf_color(color))
- lines.append('}')
- # font and fontsize
- lines.append('\\f0\\sa0')
- if self.fontsize:
- lines.append('\\fs%d' % self.fontsize)
- # ensure Libre Office Writer imports and renders consecutive
- # space characters the same width, needed for line numbering.
- # https://bugs.documentfoundation.org/show_bug.cgi?id=144050
- 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)
- # first pass of tokens to count lines, needed for line numbering
- if self.linenos:
- line_count = 0
- tokens = [] # for copying the token source generator
- for ttype, value in tokensource:
- tokens.append((ttype, value))
- if value.endswith("\n"):
- line_count += 1
- # width of line number strings (for padding with spaces)
- linenos_width = len(str(line_count+self.linenostart-1))
- tokensource = tokens
- # highlight stream
- 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
- # complete line of input
- if value.endswith("\n"):
- # close line highlighting
- if lineno in self.hl_lines:
- outfile.write('}')
- # newline in RTF file after closing }
- outfile.write("\n")
- start_new_line = True
- lineno += 1
- outfile.write('}\n')
|