189 lines
7.1 KiB
Python
189 lines
7.1 KiB
Python
|
"""
|
||
|
pygments.formatters.svg
|
||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||
|
|
||
|
Formatter for SVG output.
|
||
|
|
||
|
:copyright: Copyright 2006-2022 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 `` ``, 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:
|
||
|
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('<?xml version="1.0" encoding="%s"?>\n' %
|
||
|
self.encoding)
|
||
|
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('<g font-family="%s" font-size="%s">\n' %
|
||
|
(self.fontfamily, self.fontsize))
|
||
|
|
||
|
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('<text x="%s" y="%s" %s text-anchor="end">%s</text>' %
|
||
|
(x+self.linenowidth,y,counter_style,counter))
|
||
|
line_x += self.linenowidth + self.ystep
|
||
|
counter += 1
|
||
|
|
||
|
outfile.write('<text x="%s" y="%s" xml:space="preserve">' % (line_x, y))
|
||
|
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('<text x="%s" y="%s" text-anchor="end" %s>%s</text>' %
|
||
|
(x+self.linenowidth,y,counter_style,counter))
|
||
|
|
||
|
counter += 1
|
||
|
outfile.write('<text x="%s" y="%s" ' 'xml:space="preserve">' % (line_x,y))
|
||
|
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
|