1"""
2 pygments.formatters.rtf
3 ~~~~~~~~~~~~~~~~~~~~~~~
4
5 A formatter that generates RTF files.
6
7 :copyright: Copyright 2006-present by the Pygments team, see AUTHORS.
8 :license: BSD, see LICENSE for details.
9"""
10
11from collections import OrderedDict
12from pygments.formatter import Formatter
13from pygments.style import _ansimap
14from pygments.util import get_bool_opt, get_int_opt, get_list_opt, surrogatepair
15
16
17__all__ = ['RtfFormatter']
18
19
20class RtfFormatter(Formatter):
21 """
22 Format tokens as RTF markup. This formatter automatically outputs full RTF
23 documents with color information and other useful stuff. Perfect for Copy and
24 Paste into Microsoft(R) Word(R) documents.
25
26 Please note that ``encoding`` and ``outencoding`` options are ignored.
27 The RTF format is ASCII natively, but handles unicode characters correctly
28 thanks to escape sequences.
29
30 .. versionadded:: 0.6
31
32 Additional options accepted:
33
34 `style`
35 The style to use, can be a string or a Style subclass (default:
36 ``'default'``).
37
38 `fontface`
39 The used font family, for example ``Bitstream Vera Sans``. Defaults to
40 some generic font which is supposed to have fixed width.
41
42 `fontsize`
43 Size of the font used. Size is specified in half points. The
44 default is 24 half-points, giving a size 12 font.
45
46 .. versionadded:: 2.0
47
48 `linenos`
49 Turn on line numbering (default: ``False``).
50
51 .. versionadded:: 2.18
52
53 `lineno_fontsize`
54 Font size for line numbers. Size is specified in half points
55 (default: `fontsize`).
56
57 .. versionadded:: 2.18
58
59 `lineno_padding`
60 Number of spaces between the (inline) line numbers and the
61 source code (default: ``2``).
62
63 .. versionadded:: 2.18
64
65 `linenostart`
66 The line number for the first line (default: ``1``).
67
68 .. versionadded:: 2.18
69
70 `linenostep`
71 If set to a number n > 1, only every nth line number is printed.
72
73 .. versionadded:: 2.18
74
75 `lineno_color`
76 Color for line numbers specified as a hex triplet, e.g. ``'5e5e5e'``.
77 Defaults to the style's line number color if it is a hex triplet,
78 otherwise ansi bright black.
79
80 .. versionadded:: 2.18
81
82 `hl_lines`
83 Specify a list of lines to be highlighted, as line numbers separated by
84 spaces, e.g. ``'3 7 8'``. The line numbers are relative to the input
85 (i.e. the first line is line 1) unless `hl_linenostart` is set.
86
87 .. versionadded:: 2.18
88
89 `hl_color`
90 Color for highlighting the lines specified in `hl_lines`, specified as
91 a hex triplet (default: style's `highlight_color`).
92
93 .. versionadded:: 2.18
94
95 `hl_linenostart`
96 If set to ``True`` line numbers in `hl_lines` are specified
97 relative to `linenostart` (default ``False``).
98
99 .. versionadded:: 2.18
100 """
101 name = 'RTF'
102 aliases = ['rtf']
103 filenames = ['*.rtf']
104
105 def __init__(self, **options):
106 r"""
107 Additional options accepted:
108
109 ``fontface``
110 Name of the font used. Could for example be ``'Courier New'``
111 to further specify the default which is ``'\fmodern'``. The RTF
112 specification claims that ``\fmodern`` are "Fixed-pitch serif
113 and sans serif fonts". Hope every RTF implementation thinks
114 the same about modern...
115
116 """
117 Formatter.__init__(self, **options)
118 self.fontface = options.get('fontface') or ''
119 self.fontsize = get_int_opt(options, 'fontsize', 0)
120 self.linenos = get_bool_opt(options, 'linenos', False)
121 self.lineno_fontsize = get_int_opt(options, 'lineno_fontsize',
122 self.fontsize)
123 self.lineno_padding = get_int_opt(options, 'lineno_padding', 2)
124 self.linenostart = abs(get_int_opt(options, 'linenostart', 1))
125 self.linenostep = abs(get_int_opt(options, 'linenostep', 1))
126 self.hl_linenostart = get_bool_opt(options, 'hl_linenostart', False)
127
128 self.hl_color = options.get('hl_color', '')
129 if not self.hl_color:
130 self.hl_color = self.style.highlight_color
131
132 self.hl_lines = []
133 for lineno in get_list_opt(options, 'hl_lines', []):
134 try:
135 lineno = int(lineno)
136 if self.hl_linenostart:
137 lineno = lineno - self.linenostart + 1
138 self.hl_lines.append(lineno)
139 except ValueError:
140 pass
141
142 self.lineno_color = options.get('lineno_color', '')
143 if not self.lineno_color:
144 if self.style.line_number_color == 'inherit':
145 # style color is the css value 'inherit'
146 # default to ansi bright-black
147 self.lineno_color = _ansimap['ansibrightblack']
148 else:
149 # style color is assumed to be a hex triplet as other
150 # colors in pygments/style.py
151 self.lineno_color = self.style.line_number_color
152
153 self.color_mapping = self._create_color_mapping()
154
155 def _escape(self, text):
156 return text.replace('\\', '\\\\') \
157 .replace('{', '\\{') \
158 .replace('}', '\\}')
159
160 def _escape_text(self, text):
161 # empty strings, should give a small performance improvement
162 if not text:
163 return ''
164
165 # escape text
166 text = self._escape(text)
167
168 buf = []
169 for c in text:
170 cn = ord(c)
171 if cn < (2**7):
172 # ASCII character
173 buf.append(str(c))
174 elif (2**7) <= cn < (2**16):
175 # single unicode escape sequence
176 buf.append('{\\u%d}' % cn)
177 elif (2**16) <= cn:
178 # RTF limits unicode to 16 bits.
179 # Force surrogate pairs
180 buf.append('{\\u%d}{\\u%d}' % surrogatepair(cn))
181
182 return ''.join(buf).replace('\n', '\\par')
183
184 @staticmethod
185 def hex_to_rtf_color(hex_color):
186 if hex_color[0] == "#":
187 hex_color = hex_color[1:]
188
189 return '\\red%d\\green%d\\blue%d;' % (
190 int(hex_color[0:2], 16),
191 int(hex_color[2:4], 16),
192 int(hex_color[4:6], 16)
193 )
194
195 def _split_tokens_on_newlines(self, tokensource):
196 """
197 Split tokens containing newline characters into multiple token
198 each representing a line of the input file. Needed for numbering
199 lines of e.g. multiline comments.
200 """
201 for ttype, value in tokensource:
202 if value == '\n':
203 yield (ttype, value)
204 elif "\n" in value:
205 lines = value.split("\n")
206 for line in lines[:-1]:
207 yield (ttype, line+"\n")
208 if lines[-1]:
209 yield (ttype, lines[-1])
210 else:
211 yield (ttype, value)
212
213 def _create_color_mapping(self):
214 """
215 Create a mapping of style hex colors to index/offset in
216 the RTF color table.
217 """
218 color_mapping = OrderedDict()
219 offset = 1
220
221 if self.linenos:
222 color_mapping[self.lineno_color] = offset
223 offset += 1
224
225 if self.hl_lines:
226 color_mapping[self.hl_color] = offset
227 offset += 1
228
229 for _, style in self.style:
230 for color in style['color'], style['bgcolor'], style['border']:
231 if color and color not in color_mapping:
232 color_mapping[color] = offset
233 offset += 1
234
235 return color_mapping
236
237 @property
238 def _lineno_template(self):
239 if self.lineno_fontsize != self.fontsize:
240 return '{{\\fs{} \\cf{} %s{}}}'.format(self.lineno_fontsize,
241 self.color_mapping[self.lineno_color],
242 " " * self.lineno_padding)
243
244 return '{{\\cf{} %s{}}}'.format(self.color_mapping[self.lineno_color],
245 " " * self.lineno_padding)
246
247 @property
248 def _hl_open_str(self):
249 return rf'{{\highlight{self.color_mapping[self.hl_color]} '
250
251 @property
252 def _rtf_header(self):
253 lines = []
254 # rtf 1.8 header
255 lines.append('{\\rtf1\\ansi\\uc0\\deff0'
256 '{\\fonttbl{\\f0\\fmodern\\fprq1\\fcharset0%s;}}'
257 % (self.fontface and ' '
258 + self._escape(self.fontface) or ''))
259
260 # color table
261 lines.append('{\\colortbl;')
262 for color, _ in self.color_mapping.items():
263 lines.append(self.hex_to_rtf_color(color))
264 lines.append('}')
265
266 # font and fontsize
267 lines.append('\\f0\\sa0')
268 if self.fontsize:
269 lines.append('\\fs%d' % self.fontsize)
270
271 # ensure Libre Office Writer imports and renders consecutive
272 # space characters the same width, needed for line numbering.
273 # https://bugs.documentfoundation.org/show_bug.cgi?id=144050
274 lines.append('\\dntblnsbdb')
275
276 return lines
277
278 def format_unencoded(self, tokensource, outfile):
279 for line in self._rtf_header:
280 outfile.write(line + "\n")
281
282 tokensource = self._split_tokens_on_newlines(tokensource)
283
284 # first pass of tokens to count lines, needed for line numbering
285 if self.linenos:
286 line_count = 0
287 tokens = [] # for copying the token source generator
288 for ttype, value in tokensource:
289 tokens.append((ttype, value))
290 if value.endswith("\n"):
291 line_count += 1
292
293 # width of line number strings (for padding with spaces)
294 linenos_width = len(str(line_count+self.linenostart-1))
295
296 tokensource = tokens
297
298 # highlight stream
299 lineno = 1
300 start_new_line = True
301 for ttype, value in tokensource:
302 if start_new_line and lineno in self.hl_lines:
303 outfile.write(self._hl_open_str)
304
305 if start_new_line and self.linenos:
306 if (lineno-self.linenostart+1)%self.linenostep == 0:
307 current_lineno = lineno + self.linenostart - 1
308 lineno_str = str(current_lineno).rjust(linenos_width)
309 else:
310 lineno_str = "".rjust(linenos_width)
311 outfile.write(self._lineno_template % lineno_str)
312
313 while not self.style.styles_token(ttype) and ttype.parent:
314 ttype = ttype.parent
315 style = self.style.style_for_token(ttype)
316 buf = []
317 if style['bgcolor']:
318 buf.append('\\cb%d' % self.color_mapping[style['bgcolor']])
319 if style['color']:
320 buf.append('\\cf%d' % self.color_mapping[style['color']])
321 if style['bold']:
322 buf.append('\\b')
323 if style['italic']:
324 buf.append('\\i')
325 if style['underline']:
326 buf.append('\\ul')
327 if style['border']:
328 buf.append('\\chbrdr\\chcfpat%d' %
329 self.color_mapping[style['border']])
330 start = ''.join(buf)
331 if start:
332 outfile.write(f'{{{start} ')
333 outfile.write(self._escape_text(value))
334 if start:
335 outfile.write('}')
336 start_new_line = False
337
338 # complete line of input
339 if value.endswith("\n"):
340 # close line highlighting
341 if lineno in self.hl_lines:
342 outfile.write('}')
343 # newline in RTF file after closing }
344 outfile.write("\n")
345
346 start_new_line = True
347 lineno += 1
348
349 outfile.write('}\n')