aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/Pygments/py3/pygments/formatters/rtf.py
blob: 7f8b7e4cd7daa4ede3033261bd25af7623a0674e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
"""
    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')