123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- """
- 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 `` ``, 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
|