PageRenderTime 3792ms CodeModel.GetById 29ms RepoModel.GetById 1ms app.codeStats 0ms

/src/robot/utils/htmlformatters.py

https://gitlab.com/freedmpure/robotframework
Python | 302 lines | 286 code | 3 blank | 13 comment | 0 complexity | 6e6b10309161a2c74ac5d6a43898c88e MD5 | raw file
  1. # Copyright 2008-2015 Nokia Solutions and Networks
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import re
  15. from functools import partial
  16. from itertools import cycle
  17. class LinkFormatter(object):
  18. _image_exts = ('.jpg', '.jpeg', '.png', '.gif', '.bmp')
  19. _link = re.compile('\[(.+?\|.*?)\]')
  20. _url = re.compile('''
  21. ((^|\ ) ["'\(\[]*) # begin of line or space and opt. any char "'([
  22. ([a-z][\w+-.]*://[^\s|]+?) # url
  23. (?=[\]\)|"'.,!?:;]* ($|\ )) # opt. any char ])"'.,!?:; and end of line or space
  24. ''', re.VERBOSE|re.MULTILINE|re.IGNORECASE)
  25. def format_url(self, text):
  26. return self._format_url(text, format_as_image=False)
  27. def _format_url(self, text, format_as_image=True):
  28. if '://' not in text:
  29. return text
  30. return self._url.sub(partial(self._replace_url, format_as_image), text)
  31. def _replace_url(self, format_as_image, match):
  32. pre = match.group(1)
  33. url = match.group(3)
  34. if format_as_image and self._is_image(url):
  35. return pre + self._get_image(url)
  36. return pre + self._get_link(url)
  37. def _get_image(self, src, title=None):
  38. return '<img src="%s" title="%s">' \
  39. % (self._quot(src), self._quot(title or src))
  40. def _get_link(self, href, content=None):
  41. return '<a href="%s">%s</a>' % (self._quot(href), content or href)
  42. def _quot(self, attr):
  43. return attr if '"' not in attr else attr.replace('"', '&quot;')
  44. def format_link(self, text):
  45. # 2nd, 4th, etc. token contains link, others surrounding content
  46. tokens = self._link.split(text)
  47. formatters = cycle((self._format_url, self._format_link))
  48. return ''.join(f(t) for f, t in zip(formatters, tokens))
  49. def _format_link(self, text):
  50. link, content = [t.strip() for t in text.split('|', 1)]
  51. if self._is_image(content):
  52. content = self._get_image(content, link)
  53. elif self._is_image(link):
  54. return self._get_image(link, content)
  55. return self._get_link(link, content)
  56. def _is_image(self, text):
  57. return text.lower().endswith(self._image_exts)
  58. class LineFormatter(object):
  59. handles = lambda self, line: True
  60. newline = '\n'
  61. _bold = re.compile('''
  62. ( # prefix (group 1)
  63. (^|\ ) # begin of line or space
  64. ["'(]* _? # optionally any char "'( and optional begin of italic
  65. ) #
  66. \* # start of bold
  67. ([^\ ].*?) # no space and then anything (group 3)
  68. \* # end of bold
  69. (?= # start of postfix (non-capturing group)
  70. _? ["').,!?:;]* # optional end of italic and any char "').,!?:;
  71. ($|\ ) # end of line or space
  72. )
  73. ''', re.VERBOSE)
  74. _italic = re.compile('''
  75. ( (^|\ ) ["'(]* ) # begin of line or space and opt. any char "'(
  76. _ # start of italic
  77. ([^\ _].*?) # no space or underline and then anything
  78. _ # end of italic
  79. (?= ["').,!?:;]* ($|\ ) ) # opt. any char "').,!?:; and end of line or space
  80. ''', re.VERBOSE)
  81. _code = re.compile('''
  82. ( (^|\ ) ["'(]* ) # same as above with _ changed to ``
  83. ``
  84. ([^\ `].*?)
  85. ``
  86. (?= ["').,!?:;]* ($|\ ) )
  87. ''', re.VERBOSE)
  88. def __init__(self):
  89. self._formatters = [('*', self._format_bold),
  90. ('_', self._format_italic),
  91. ('``', self._format_code),
  92. ('', LinkFormatter().format_link)]
  93. def format(self, line):
  94. for marker, formatter in self._formatters:
  95. if marker in line:
  96. line = formatter(line)
  97. return line
  98. def _format_bold(self, line):
  99. return self._bold.sub('\\1<b>\\3</b>', line)
  100. def _format_italic(self, line):
  101. return self._italic.sub('\\1<i>\\3</i>', line)
  102. def _format_code(self, line):
  103. return self._code.sub('\\1<code>\\3</code>', line)
  104. class HtmlFormatter(object):
  105. def __init__(self):
  106. self._results = []
  107. self._formatters = [TableFormatter(),
  108. PreformattedFormatter(),
  109. ListFormatter(),
  110. HeaderFormatter(),
  111. RulerFormatter()]
  112. self._formatters.append(ParagraphFormatter(self._formatters[:]))
  113. self._current = None
  114. def format(self, text):
  115. for line in text.splitlines():
  116. self._process_line(line)
  117. self._end_current()
  118. return '\n'.join(self._results)
  119. def _process_line(self, line):
  120. if not line.strip():
  121. self._end_current()
  122. elif self._current and self._current.handles(line):
  123. self._current.add(line)
  124. else:
  125. self._end_current()
  126. self._current = self._find_formatter(line)
  127. self._current.add(line)
  128. def _end_current(self):
  129. if self._current:
  130. self._results.append(self._current.end())
  131. self._current = None
  132. def _find_formatter(self, line):
  133. for formatter in self._formatters:
  134. if formatter.handles(line):
  135. return formatter
  136. class _Formatter(object):
  137. _strip_lines = True
  138. def __init__(self):
  139. self._lines = []
  140. def handles(self, line):
  141. return self._handles(line.strip() if self._strip_lines else line)
  142. def _handles(self, line):
  143. raise NotImplementedError
  144. def add(self, line):
  145. self._lines.append(line.strip() if self._strip_lines else line)
  146. def end(self):
  147. result = self.format(self._lines)
  148. self._lines = []
  149. return result
  150. def format(self, lines):
  151. raise NotImplementedError
  152. class _SingleLineFormatter(_Formatter):
  153. def _handles(self, line):
  154. return not self._lines and self.match(line)
  155. def match(self, line):
  156. raise NotImplementedError
  157. def format(self, lines):
  158. return self.format_line(lines[0])
  159. def format_line(self, line):
  160. raise NotImplementedError
  161. class RulerFormatter(_SingleLineFormatter):
  162. match = re.compile('^-{3,}$').match
  163. def format_line(self, line):
  164. return '<hr>'
  165. class HeaderFormatter(_SingleLineFormatter):
  166. match = re.compile(r'^(={1,3})\s+(\S.*?)\s+\1$').match
  167. def format_line(self, line):
  168. level, text = self.match(line).groups()
  169. level = len(level) + 1
  170. return '<h%d>%s</h%d>' % (level, text, level)
  171. class ParagraphFormatter(_Formatter):
  172. _format_line = LineFormatter().format
  173. def __init__(self, other_formatters):
  174. _Formatter.__init__(self)
  175. self._other_formatters = other_formatters
  176. def _handles(self, line):
  177. return not any(other.handles(line)
  178. for other in self._other_formatters)
  179. def format(self, lines):
  180. return '<p>%s</p>' % self._format_line(' '.join(lines))
  181. class TableFormatter(_Formatter):
  182. _table_line = re.compile('^\| (.* |)\|$')
  183. _line_splitter = re.compile(' \|(?= )')
  184. _format_cell_content = LineFormatter().format
  185. def _handles(self, line):
  186. return self._table_line.match(line) is not None
  187. def format(self, lines):
  188. return self._format_table([self._split_to_cells(l) for l in lines])
  189. def _split_to_cells(self, line):
  190. return [cell.strip() for cell in self._line_splitter.split(line[1:-1])]
  191. def _format_table(self, rows):
  192. maxlen = max(len(row) for row in rows)
  193. table = ['<table border="1">']
  194. for row in rows:
  195. row += [''] * (maxlen - len(row)) # fix ragged tables
  196. table.append('<tr>')
  197. table.extend(self._format_cell(cell) for cell in row)
  198. table.append('</tr>')
  199. table.append('</table>')
  200. return '\n'.join(table)
  201. def _format_cell(self, content):
  202. if content.startswith('=') and content.endswith('='):
  203. tx = 'th'
  204. content = content[1:-1].strip()
  205. else:
  206. tx = 'td'
  207. return '<%s>%s</%s>' % (tx, self._format_cell_content(content), tx)
  208. class PreformattedFormatter(_Formatter):
  209. _format_line = LineFormatter().format
  210. def _handles(self, line):
  211. return line.startswith('| ') or line == '|'
  212. def format(self, lines):
  213. lines = [self._format_line(line[2:]) for line in lines]
  214. return '\n'.join(['<pre>'] + lines + ['</pre>'])
  215. class ListFormatter(_Formatter):
  216. _strip_lines = False
  217. _format_item = LineFormatter().format
  218. def _handles(self, line):
  219. return line.strip().startswith('- ') or \
  220. line.startswith(' ') and self._lines
  221. def format(self, lines):
  222. items = ['<li>%s</li>' % self._format_item(line)
  223. for line in self._combine_lines(lines)]
  224. return '\n'.join(['<ul>'] + items + ['</ul>'])
  225. def _combine_lines(self, lines):
  226. current = []
  227. for line in lines:
  228. line = line.strip()
  229. if not line.startswith('- '):
  230. current.append(line)
  231. continue
  232. if current:
  233. yield ' '.join(current)
  234. current = [line[2:].strip()]
  235. yield ' '.join(current)