svg.py 6.7 KB

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