1"""
2 pygments.formatters.svg
3 ~~~~~~~~~~~~~~~~~~~~~~~
4
5 Formatter for SVG output.
6
7 :copyright: Copyright 2006-present by the Pygments team, see AUTHORS.
8 :license: BSD, see LICENSE for details.
9"""
10
11from pygments.formatter import Formatter
12from pygments.token import Comment
13from pygments.util import get_bool_opt, get_int_opt, html_escape
14
15__all__ = ['SvgFormatter']
16
17
18class2style = {}
19
20class SvgFormatter(Formatter):
21 """
22 Format tokens as an SVG graphics file. This formatter is still experimental.
23 Each line of code is a ``<text>`` element with explicit ``x`` and ``y``
24 coordinates containing ``<tspan>`` elements with the individual token styles.
25
26 By default, this formatter outputs a full SVG document including doctype
27 declaration and the ``<svg>`` root element.
28
29 .. versionadded:: 0.9
30
31 Additional options accepted:
32
33 `nowrap`
34 Don't wrap the SVG ``<text>`` elements in ``<svg><g>`` elements and
35 don't add a XML declaration and a doctype. If true, the `fontfamily`
36 and `fontsize` options are ignored. Defaults to ``False``.
37
38 `fontfamily`
39 The value to give the wrapping ``<g>`` element's ``font-family``
40 attribute, defaults to ``"monospace"``.
41
42 `fontsize`
43 The value to give the wrapping ``<g>`` element's ``font-size``
44 attribute, defaults to ``"14px"``.
45
46 `linenos`
47 If ``True``, add line numbers (default: ``False``).
48
49 `linenostart`
50 The line number for the first line (default: ``1``).
51
52 `linenostep`
53 If set to a number n > 1, only every nth line number is printed.
54
55 `linenowidth`
56 Maximum width devoted to line numbers (default: ``3*ystep``, sufficient
57 for up to 4-digit line numbers. Increase width for longer code blocks).
58
59 `xoffset`
60 Starting offset in X direction, defaults to ``0``.
61
62 `yoffset`
63 Starting offset in Y direction, defaults to the font size if it is given
64 in pixels, or ``20`` else. (This is necessary since text coordinates
65 refer to the text baseline, not the top edge.)
66
67 `ystep`
68 Offset to add to the Y coordinate for each subsequent line. This should
69 roughly be the text size plus 5. It defaults to that value if the text
70 size is given in pixels, or ``25`` else.
71
72 `spacehack`
73 Convert spaces in the source to `` ``, which are non-breaking
74 spaces. SVG provides the ``xml:space`` attribute to control how
75 whitespace inside tags is handled, in theory, the ``preserve`` value
76 could be used to keep all whitespace as-is. However, many current SVG
77 viewers don't obey that rule, so this option is provided as a workaround
78 and defaults to ``True``.
79 """
80 name = 'SVG'
81 aliases = ['svg']
82 filenames = ['*.svg']
83
84 def __init__(self, **options):
85 Formatter.__init__(self, **options)
86 self.nowrap = get_bool_opt(options, 'nowrap', False)
87 self.fontfamily = options.get('fontfamily', 'monospace')
88 self.fontsize = options.get('fontsize', '14px')
89 self.xoffset = get_int_opt(options, 'xoffset', 0)
90 fs = self.fontsize.strip()
91 if fs.endswith('px'):
92 fs = fs[:-2].strip()
93 try:
94 int_fs = int(fs)
95 except ValueError:
96 int_fs = 20
97 self.yoffset = get_int_opt(options, 'yoffset', int_fs)
98 self.ystep = get_int_opt(options, 'ystep', int_fs + 5)
99 self.spacehack = get_bool_opt(options, 'spacehack', True)
100 self.linenos = get_bool_opt(options,'linenos',False)
101 self.linenostart = get_int_opt(options,'linenostart',1)
102 self.linenostep = get_int_opt(options,'linenostep',1)
103 self.linenowidth = get_int_opt(options,'linenowidth', 3*self.ystep)
104 self._stylecache = {}
105
106 def format_unencoded(self, tokensource, outfile):
107 """
108 Format ``tokensource``, an iterable of ``(tokentype, tokenstring)``
109 tuples and write it into ``outfile``.
110
111 For our implementation we put all lines in their own 'line group'.
112 """
113 x = self.xoffset
114 y = self.yoffset
115 if not self.nowrap:
116 if self.encoding:
117 outfile.write(f'<?xml version="1.0" encoding="{self.encoding}"?>\n')
118 else:
119 outfile.write('<?xml version="1.0"?>\n')
120 outfile.write('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" '
121 '"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/'
122 'svg10.dtd">\n')
123 outfile.write('<svg xmlns="http://www.w3.org/2000/svg">\n')
124 outfile.write(f'<g font-family="{self.fontfamily}" font-size="{self.fontsize}">\n')
125
126 counter = self.linenostart
127 counter_step = self.linenostep
128 counter_style = self._get_style(Comment)
129 line_x = x
130
131 if self.linenos:
132 if counter % counter_step == 0:
133 outfile.write(f'<text x="{x+self.linenowidth}" y="{y}" {counter_style} text-anchor="end">{counter}</text>')
134 line_x += self.linenowidth + self.ystep
135 counter += 1
136
137 outfile.write(f'<text x="{line_x}" y="{y}" xml:space="preserve">')
138 for ttype, value in tokensource:
139 style = self._get_style(ttype)
140 tspan = style and '<tspan' + style + '>' or ''
141 tspanend = tspan and '</tspan>' or ''
142 value = html_escape(value)
143 if self.spacehack:
144 value = value.expandtabs().replace(' ', ' ')
145 parts = value.split('\n')
146 for part in parts[:-1]:
147 outfile.write(tspan + part + tspanend)
148 y += self.ystep
149 outfile.write('</text>\n')
150 if self.linenos and counter % counter_step == 0:
151 outfile.write(f'<text x="{x+self.linenowidth}" y="{y}" text-anchor="end" {counter_style}>{counter}</text>')
152
153 counter += 1
154 outfile.write(f'<text x="{line_x}" y="{y}" ' 'xml:space="preserve">')
155 outfile.write(tspan + parts[-1] + tspanend)
156 outfile.write('</text>')
157
158 if not self.nowrap:
159 outfile.write('</g></svg>\n')
160
161 def _get_style(self, tokentype):
162 if tokentype in self._stylecache:
163 return self._stylecache[tokentype]
164 otokentype = tokentype
165 while not self.style.styles_token(tokentype):
166 tokentype = tokentype.parent
167 value = self.style.style_for_token(tokentype)
168 result = ''
169 if value['color']:
170 result = ' fill="#' + value['color'] + '"'
171 if value['bold']:
172 result += ' font-weight="bold"'
173 if value['italic']:
174 result += ' font-style="italic"'
175 self._stylecache[otokentype] = result
176 return result