Robot Framework
htmlformatters.py
Go to the documentation of this file.
1 # Copyright 2008-2015 Nokia Networks
2 # Copyright 2016- Robot Framework Foundation
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 
16 import re
17 from functools import partial
18 from itertools import cycle
19 
20 
22 
25  _image_exts = ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg')
26 
29  _link = re.compile(r'\[(.+?\|.*?)\]')
30 
33  _url = re.compile(r'''
34 ((^|\ ) ["'(\[{]*) # begin of line or space and opt. any char "'([{
35 ([a-z][\w+-.]*://[^\s|]+?) # url
36 (?=[)\]}"'.,!?:;|]* ($|\ )) # opt. any char )]}"'.,!?:;| and eol or space
37 ''', re.VERBOSE|re.MULTILINE|re.IGNORECASE)
38 
39  def format_url(self, text):
40  return self._format_url_format_url(text, format_as_image=False)
41 
42  def _format_url(self, text, format_as_image=True):
43  if '://' not in text:
44  return text
45  return self._url_url.sub(partial(self._replace_url_replace_url, format_as_image), text)
46 
47  def _replace_url(self, format_as_image, match):
48  pre = match.group(1)
49  url = match.group(3)
50  if format_as_image and self._is_image_is_image(url):
51  return pre + self._get_image_get_image(url)
52  return pre + self._get_link_get_link(url)
53 
54  def _get_image(self, src, title=None):
55  return '<img src="%s" title="%s">' \
56  % (self._quot_quot(src), self._quot_quot(title or src))
57 
58  def _get_link(self, href, content=None):
59  return '<a href="%s">%s</a>' % (self._quot_quot(href), content or href)
60 
61  def _quot(self, attr):
62  return attr if '"' not in attr else attr.replace('"', '&quot;')
63 
64  def format_link(self, text):
65  # 2nd, 4th, etc. token contains link, others surrounding content
66  tokens = self._link_link.split(text)
67  formatters = cycle((self._format_url_format_url, self._format_link_format_link))
68  return ''.join(f(t) for f, t in zip(formatters, tokens))
69 
70  def _format_link(self, text):
71  link, content = [t.strip() for t in text.split('|', 1)]
72  if self._is_image_is_image(content):
73  content = self._get_image_get_image(content, link)
74  elif self._is_image_is_image(link):
75  return self._get_image_get_image(link, content)
76  return self._get_link_get_link(link, content)
77 
78  def _is_image(self, text):
79 
80  return (text.startswith('data:image/')
81  or text.lower().endswith(self._image_exts_image_exts))
82 
83 
85  handles = lambda self, line: True
86  newline = '\n'
87 
90  _bold = re.compile(r'''
91 ( # prefix (group 1)
92  (^|\ ) # begin of line or space
93  ["'(]* _? # optionally any char "'( and optional begin of italic
94 ) #
95 \* # start of bold
96 ([^\ ].*?) # no space and then anything (group 3)
97 \* # end of bold
98 (?= # start of postfix (non-capturing group)
99  _? ["').,!?:;]* # optional end of italic and any char "').,!?:;
100  ($|\ ) # end of line or space
101 )
102 ''', re.VERBOSE)
103 
106  _italic = re.compile(r'''
107 ( (^|\ ) ["'(]* ) # begin of line or space and opt. any char "'(
108 _ # start of italic
109 ([^\ _].*?) # no space or underline and then anything
110 _ # end of italic
111 (?= ["').,!?:;]* ($|\ ) ) # opt. any char "').,!?:; and end of line or space
112 ''', re.VERBOSE)
113 
116  _code = re.compile(r'''
117 ( (^|\ ) ["'(]* ) # same as above with _ changed to ``
118 ``
119 ([^\ `].*?)
120 ``
121 (?= ["').,!?:;]* ($|\ ) )
122 ''', re.VERBOSE)
123 
124  def __init__(self):
125  self._formatters_formatters = [('*', self._format_bold_format_bold),
126  ('_', self._format_italic_format_italic),
127  ('``', self._format_code_format_code),
128  ('', LinkFormatter().format_link)]
129 
130  def format(self, line):
131  for marker, formatter in self._formatters_formatters:
132  if marker in line:
133  line = formatter(line)
134  return line
135 
136  def _format_bold(self, line):
137  return self._bold_bold.sub('\\1<b>\\3</b>', line)
138 
139  def _format_italic(self, line):
140  return self._italic_italic.sub('\\1<i>\\3</i>', line)
141 
142  def _format_code(self, line):
143  return self._code_code.sub('\\1<code>\\3</code>', line)
144 
145 
147 
148  def __init__(self):
149  self._formatters_formatters = [TableFormatter(),
151  ListFormatter(),
152  HeaderFormatter(),
153  RulerFormatter()]
154  self._formatters_formatters.append(ParagraphFormatter(self._formatters_formatters[:]))
155  self._current_current = None
156 
157  def format(self, text):
158  results = []
159  for line in text.splitlines():
160  self._process_line_process_line(line, results)
161  self._end_current_end_current(results)
162  return '\n'.join(results)
163 
164  def _process_line(self, line, results):
165  if not line.strip():
166  self._end_current_end_current(results)
167  elif self._current_current and self._current_current.handles(line):
168  self._current_current.add(line)
169  else:
170  self._end_current_end_current(results)
171  self._current_current = self._find_formatter_find_formatter(line)
172  self._current_current.add(line)
173 
174  def _end_current(self, results):
175  if self._current_current:
176  results.append(self._current_current.end())
177  self._current_current = None
178 
179  def _find_formatter(self, line):
180  for formatter in self._formatters_formatters:
181  if formatter.handles(line):
182  return formatter
183 
184 
186 
189  _strip_lines = True
190 
191  def __init__(self):
192  self._lines_lines = []
193 
194  def handles(self, line):
195  return self._handles_handles(line.strip() if self._strip_lines_strip_lines else line)
196 
197  def _handles(self, line):
198  raise NotImplementedError
199 
200  def add(self, line):
201  self._lines_lines.append(line.strip() if self._strip_lines_strip_lines else line)
202 
203  def end(self):
204  result = self.formatformat(self._lines_lines)
205  self._lines_lines = []
206  return result
207 
208  def format(self, lines):
209  raise NotImplementedError
210 
211 
213 
214  def _handles(self, line):
215  return not self._lines_lines and self.matchmatch(line)
216 
217  def match(self, line):
218  raise NotImplementedError
219 
220  def format(self, lines):
221  return self.format_lineformat_line(lines[0])
222 
223  def format_line(self, line):
224  raise NotImplementedError
225 
226 
228  match = re.compile('^-{3,}$').match
229 
230  def format_line(self, line):
231  return '<hr>'
232 
233 
235  match = re.compile(r'^(={1,3})\s+(\S.*?)\s+\1$').match
236 
237  def format_line(self, line):
238  level, text = self.matchmatchmatch(line).groups()
239  level = len(level) + 1
240  return '<h%d>%s</h%d>' % (level, text, level)
241 
242 
244 
247  _format_line = LineFormatter().format
248 
249  def __init__(self, other_formatters):
250  _Formatter.__init__(self)
251  self._other_formatters_other_formatters = other_formatters
252 
253  def _handles(self, line):
254  return not any(other.handles(line)
255  for other in self._other_formatters_other_formatters)
256 
257  def format(self, lines):
258  return '<p>%s</p>' % self._format_line_format_line(' '.join(lines))
259 
260 
262 
265  _table_line = re.compile(r'^\| (.* |)\|$')
266 
269  _line_splitter = re.compile(r' \|(?= )')
270 
273  _format_cell_content = LineFormatter().format
274 
275  def _handles(self, line):
276  return self._table_line_table_line.match(line) is not None
277 
278  def format(self, lines):
279  return self._format_table_format_table([self._split_to_cells_split_to_cells(l) for l in lines])
280 
281  def _split_to_cells(self, line):
282  return [cell.strip() for cell in self._line_splitter_line_splitter.split(line[1:-1])]
283 
284  def _format_table(self, rows):
285  maxlen = max(len(row) for row in rows)
286  table = ['<table border="1">']
287  for row in rows:
288  row += [''] * (maxlen - len(row)) # fix ragged tables
289  table.append('<tr>')
290  table.extend(self._format_cell_format_cell(cell) for cell in row)
291  table.append('</tr>')
292  table.append('</table>')
293  return '\n'.join(table)
294 
295  def _format_cell(self, content):
296  if content.startswith('=') and content.endswith('='):
297  tx = 'th'
298  content = content[1:-1].strip()
299  else:
300  tx = 'td'
301  return '<%s>%s</%s>' % (tx, self._format_cell_content_format_cell_content(content), tx)
302 
303 
305 
308  _format_line = LineFormatter().format
309 
310  def _handles(self, line):
311  return line.startswith('| ') or line == '|'
312 
313  def format(self, lines):
314  lines = [self._format_line_format_line(line[2:]) for line in lines]
315  return '\n'.join(['<pre>'] + lines + ['</pre>'])
316 
317 
319 
322  _strip_lines = False
323 
326  _format_item = LineFormatter().format
327 
328  def _handles(self, line):
329  return line.strip().startswith('- ') or line.startswith(' ') and self._lines_lines
330 
331  def format(self, lines):
332  items = ['<li>%s</li>' % self._format_item_format_item(line)
333  for line in self._combine_lines_combine_lines(lines)]
334  return '\n'.join(['<ul>'] + items + ['</ul>'])
335 
336  def _combine_lines(self, lines):
337  current = []
338  for line in lines:
339  line = line.strip()
340  if not line.startswith('- '):
341  current.append(line)
342  continue
343  if current:
344  yield ' '.join(current)
345  current = [line[2:].strip()]
346  yield ' '.join(current)
def _process_line(self, line, results)
def _format_url(self, text, format_as_image=True)
def _get_image(self, src, title=None)
def _replace_url(self, format_as_image, match)
def _get_link(self, href, content=None)