"""
pygments.formatters.svg
~~~~~~~~~~~~~~~~~~~~~~~
Formatter
for SVG output.
:copyright: Copyright 2006-2024 by the Pygments team, see AUTHORS.
:license: BSD, see LICENSE
for details.
"""
from pygments.formatter
import Formatter
from pygments.token
import Comment
from pygments.util
import get_bool_opt, get_int_opt
__all__ = [
'SvgFormatter']
def escape_html(text):
"""Escape &, <, > as well as single and double quotes for HTML."""
return text.replace(
'&',
'&'). \
replace(
'<',
'<'). \
replace(
'>',
'>'). \
replace(
'"',
'"'). \
replace(
"'",
''')
class2style = {}
class SvgFormatter(Formatter):
"""
Format tokens
as an SVG graphics file. This formatter
is still experimental.
Each line of code
is a ``<text>`` element
with explicit ``x``
and ``y``
coordinates containing ``<tspan>`` elements
with the individual token styles.
By default, this formatter outputs a full SVG document including doctype
declaration
and the ``<svg>`` root element.
.. versionadded:: 0.9
Additional options accepted:
`nowrap`
Don
't wrap the SVG ``<text>`` elements in ``<svg><g>`` elements and
don
't add a XML declaration and a doctype. If true, the `fontfamily`
and `fontsize` options are ignored. Defaults to ``
False``.
`fontfamily`
The value to give the wrapping ``<g>`` element
's ``font-family``
attribute, defaults to ``
"monospace"``.
`fontsize`
The value to give the wrapping ``<g>`` element
's ``font-size``
attribute, defaults to ``
"14px"``.
`linenos`
If ``
True``, add line numbers (default: ``
False``).
`linenostart`
The line number
for the first line (default: ``1``).
`linenostep`
If set to a number n > 1, only every nth line number
is printed.
`linenowidth`
Maximum width devoted to line numbers (default: ``3*ystep``, sufficient
for up to 4-digit line numbers. Increase width
for longer code blocks).
`xoffset`
Starting offset
in X direction, defaults to ``0``.
`yoffset`
Starting offset
in Y direction, defaults to the font size
if it
is given
in pixels,
or ``20``
else. (This
is necessary since text coordinates
refer to the text baseline,
not the top edge.)
`ystep`
Offset to add to the Y coordinate
for each subsequent line. This should
roughly be the text size plus 5. It defaults to that value
if the text
size
is given
in pixels,
or ``25``
else.
`spacehack`
Convert spaces
in the source to ``&
#160;``, which are non-breaking
spaces. SVG provides the ``xml:space`` attribute to control how
whitespace inside tags
is handled,
in theory, the ``preserve`` value
could be used to keep all whitespace as-is. However, many current SVG
viewers don
't obey that rule, so this option is provided as a workaround
and defaults to ``
True``.
"""
name =
'SVG'
aliases = [
'svg']
filenames = [
'*.svg']
def __init__(self, **options):
Formatter.__init__(self, **options)
self.nowrap = get_bool_opt(options,
'nowrap',
False)
self.fontfamily = options.get(
'fontfamily',
'monospace')
self.fontsize = options.get(
'fontsize',
'14px')
self.xoffset = get_int_opt(options,
'xoffset', 0)
fs = self.fontsize.strip()
if fs.endswith(
'px'):
fs = fs[:-2].strip()
try:
int_fs = int(fs)
except ValueError:
int_fs = 20
self.yoffset = get_int_opt(options,
'yoffset', int_fs)
self.ystep = get_int_opt(options,
'ystep', int_fs + 5)
self.spacehack = get_bool_opt(options,
'spacehack',
True)
self.linenos = get_bool_opt(options,
'linenos',
False)
self.linenostart = get_int_opt(options,
'linenostart',1)
self.linenostep = get_int_opt(options,
'linenostep',1)
self.linenowidth = get_int_opt(options,
'linenowidth', 3*self.ystep)
self._stylecache = {}
def format_unencoded(self, tokensource, outfile):
"""
Format ``tokensource``, an iterable of ``(tokentype, tokenstring)``
tuples
and write it into ``outfile``.
For our implementation we put all lines
in their own
'line group'.
"""
x = self.xoffset
y = self.yoffset
if not self.nowrap:
if self.encoding:
outfile.write(f
'<?xml version="1.0" encoding="{self.encoding}"?>\n')
else:
outfile.write(
'<?xml version="1.0"?>\n')
outfile.write(
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" '
'"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/'
'svg10.dtd">\n')
outfile.write(
'<svg xmlns="http://www.w3.org/2000/svg">\n')
outfile.write(f
'<g font-family="{self.fontfamily}" font-size="{self.fontsize}">\n')
counter = self.linenostart
counter_step = self.linenostep
counter_style = self._get_style(Comment)
line_x = x
if self.linenos:
if counter % counter_step == 0:
outfile.write(f
'<text x="{x+self.linenowidth}" y="{y}" {counter_style} text-anchor="end">{counter}</text>')
line_x += self.linenowidth + self.ystep
counter += 1
outfile.write(f
'<text x="{line_x}" y="{y}" xml:space="preserve">')
for ttype, value
in tokensource:
style = self._get_style(ttype)
tspan = style
and '<tspan' + style +
'>' or ''
tspanend = tspan
and '</tspan>' or ''
value = escape_html(value)
if self.spacehack:
value = value.expandtabs().replace(
' ',
' ')
parts = value.split(
'\n')
for part
in parts[:-1]:
outfile.write(tspan + part + tspanend)
y += self.ystep
outfile.write(
'</text>\n')
if self.linenos
and counter % counter_step == 0:
outfile.write(f
'<text x="{x+self.linenowidth}" y="{y}" text-anchor="end" {counter_style}>{counter}</text>')
counter += 1
outfile.write(f
'<text x="{line_x}" y="{y}" ' 'xml:space="preserve">')
outfile.write(tspan + parts[-1] + tspanend)
outfile.write(
'</text>')
if not self.nowrap:
outfile.write(
'</g></svg>\n')
def _get_style(self, tokentype):
if tokentype
in self._stylecache:
return self._stylecache[tokentype]
otokentype = tokentype
while not self.style.styles_token(tokentype):
tokentype = tokentype.parent
value = self.style.style_for_token(tokentype)
result =
''
if value[
'color']:
result =
' fill="#' + value['color'] + '"'
if value[
'bold']:
result +=
' font-weight="bold"'
if value[
'italic']:
result +=
' font-style="italic"'
self._stylecache[otokentype] = result
return result