Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pygments/formatters/rtf.py: 68%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

160 statements  

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')