svg.py 5.7 KB

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