Skip to content

Commit b920166

Browse files
authored
Libdoc: Fix shortdoc when doc format is HTML
Fixes robotframework#3701
1 parent 65b018c commit b920166

File tree

5 files changed

+314
-9
lines changed

5 files changed

+314
-9
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
*** Settings ***
2+
Resource libdoc_resource.robot
3+
Suite Setup Run Libdoc to XML:HTML and to HTML and Parse Model
4+
Test Template Should Be Equal Multiline
5+
6+
*** Keywords ***
7+
Run Libdoc to XML:HTML and to HTML and Parse Model
8+
Run Libdoc And Set Output --format XML:HTML ${TESTDATADIR}/module.py ${OUTXML}
9+
Run Libdoc And Parse Model From HTML ${OUTXML}
10+
11+
*** Comments ***
12+
This test suite will be changed with one of the next Tasks to contain a check for the roundtrip from library into XML then from XML to html.
13+
14+
*** Test Cases ***
15+
Name
16+
${MODEL}[name] module
17+
18+
Documentation
19+
${MODEL}[doc] <p>Module test library.</p>
20+
21+
Version
22+
${MODEL}[version] 0.1-alpha
23+
24+
Generated
25+
[Template] Should Match Regexp
26+
${MODEL}[generated] \\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}
27+
28+
Scope
29+
${MODEL}[scope] GLOBAL
30+
31+
Named Args
32+
[Template] Should Be Equal
33+
${MODEL}[named_args] ${True}
34+
35+
Inits
36+
[Template] Should Be Empty
37+
${MODEL}[inits]
38+
39+
Keyword Names
40+
${MODEL}[keywords][0][name] Get Hello
41+
${MODEL}[keywords][1][name] Keyword
42+
${MODEL}[keywords][13][name] Set Name Using Robot Name Attribute
43+
44+
Keyword Arguments
45+
[Template] Should Be Equal As Strings
46+
${MODEL}[keywords][0][args] []
47+
${MODEL}[keywords][1][args] ['a1=d', '*a2']
48+
${MODEL}[keywords][6][args] ['arg=hyv\\\\xe4']
49+
${MODEL}[keywords][10][args] ['arg=hyvä']
50+
${MODEL}[keywords][12][args] ['a=1', 'b=True', 'c=(1, 2, None)']
51+
${MODEL}[keywords][13][args] ['a', 'b', '*args', '**kwargs']
52+
53+
Embedded Arguments
54+
[Template] NONE
55+
Should Be Equal ${MODEL}[keywords][14][name] Takes \${embedded} \${args}
56+
Should Be Empty ${MODEL}[keywords][14][args]
57+
58+
Keyword Documentation
59+
${MODEL}[keywords][1][doc]
60+
... <p>A keyword.</p>
61+
... <p>See <a href="#Get%20Hello" class="name">get hello</a> for details.</p>
62+
${MODEL}[keywords][0][doc]
63+
... <p>Get hello.</p>
64+
... <p>See <a href="#Importing" class="name">importing</a> for explanation of nothing and <a href="#Introduction" class="name">introduction</a> for no more information.</p>
65+
${MODEL}[keywords][5][doc]
66+
... <p>This is short doc. It can span multiple physical lines.</p>
67+
... <p>This is body. It can naturally also contain multiple lines.</p>
68+
... <p>And paragraphs.</p>
69+
70+
Non-ASCII Keyword Documentation
71+
${MODEL}[keywords][8][doc] <p>Hyvää yötä.</p>
72+
${MODEL}[keywords][11][doc] <p>Hyvää yötä.</p>\n<p>Спасибо!</p>
73+
74+
Keyword Short Doc
75+
${MODEL}[keywords][1][shortdoc] A keyword.
76+
${MODEL}[keywords][0][shortdoc] Get hello.
77+
${MODEL}[keywords][8][shortdoc] Hyvää yötä.
78+
${MODEL}[keywords][11][shortdoc] Hyvää yötä.
79+
80+
Keyword Short Doc Spanning Multiple Physical Lines
81+
${MODEL}[keywords][5][shortdoc] This is short doc. It can span multiple physical lines.
82+
83+
Keyword tags
84+
[Template] Should Be Equal As Strings
85+
${MODEL}[keywords][1][tags] []
86+
${MODEL}[keywords][2][tags] ['1', 'one', 'yksi']
87+
${MODEL}[keywords][3][tags] ['2', 'kaksi', 'two']
88+
${MODEL}[keywords][4][tags] ['tag1', 'tag2']
89+
90+
User keyword documentation formatting
91+
[Setup] Run Libdoc And Parse Model From HTML ${TESTDATADIR}/resource.robot
92+
${MODEL}[keywords][0][doc] <p>$\{CURDIR}</p>
93+
${MODEL}[keywords][1][doc] <p><b>DEPRECATED</b> for some reason.</p>
94+
${MODEL}[keywords][2][doc]
95+
${MODEL}[keywords][10][doc]
96+
... <p>Hyvää yötä.</p>
97+
... <p>Спасибо!</p>
98+
${MODEL}[keywords][8][doc]
99+
... <p>foo bar <a href="#kw" class="name">kw</a>.</p>
100+
... <p>FIRST <span class="name">\${a1}</span> alskdj alskdjlajd askf laskdjf asldkfj alsdkfj alsdkfjasldkfj END</p>
101+
... <p>SECOND askf laskdjf <i>asldkfj</i> alsdkfj alsdkfjasldkfj askf <b>laskdjf</b> END</p>
102+
... <p>THIRD asldkfj <a href="#Introduction" class="name">introduction</a> alsdkfj <a href="http://foo.bar">http://foo.bar</a> END</p>
103+
... <ul>
104+
... <li>aaa</li>
105+
... <li>bbb</li>
106+
... </ul>
107+
... <hr>
108+
... <table border="1">
109+
... <tr>
110+
... <th>first</th>
111+
... <th>second</th>
112+
... </tr>
113+
... <tr>
114+
... <td>foo</td>
115+
... <td>bar</td>
116+
... </tr>
117+
... </table>

src/robot/libdocpkg/htmlwriter.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def _convert_keyword(self, kw):
7575
'name': kw.name,
7676
'args': kw.args,
7777
'doc': self._doc_formatter.html(kw.doc),
78-
'shortdoc': ' '.join(kw.shortdoc.splitlines()),
78+
'shortdoc': kw.shortdoc,
7979
'tags': tuple(kw.tags),
8080
'matched': True
8181
}
@@ -142,25 +142,57 @@ def _get_formatter(self, doc_format):
142142
try:
143143
return {'ROBOT': html_format,
144144
'TEXT': self._format_text,
145-
'HTML': self._format_html,
145+
'HTML': lambda doc: doc,
146146
'REST': self._format_rest}[doc_format]
147147
except KeyError:
148148
raise DataError("Invalid documentation format '%s'." % doc_format)
149149

150150
def _format_text(self, doc):
151151
return '<p style="white-space: pre-wrap">%s</p>' % html_escape(doc)
152152

153-
def _format_html(self, doc):
154-
return '<div style="margin: 0">%s</div>' % doc
155-
156153
def _format_rest(self, doc):
157154
try:
158155
from docutils.core import publish_parts
159156
except ImportError:
160157
raise DataError("reST format requires 'docutils' module to be installed.")
161158
parts = publish_parts(doc, writer_name='html',
162159
settings_overrides={'syntax_highlight': 'short'})
163-
return self._format_html(parts['html_body'])
160+
return parts['html_body']
164161

165162
def __call__(self, doc):
166163
return self._formatter(doc)
164+
165+
166+
class HtmlToText(object):
167+
html_tags = {
168+
'b': '*',
169+
'i': '_',
170+
'strong': '*',
171+
'em': '_',
172+
'code': '``',
173+
'div.*?': ''
174+
}
175+
html_chars = {
176+
'<br */?>': '\n',
177+
'&amp;': '&',
178+
'&lt;': '<',
179+
'&gt;': '>',
180+
'&quot;': '"',
181+
'&apos;': "'"
182+
}
183+
184+
def get_shortdoc_from_html(self, doc):
185+
match = re.search(r'<p.*?>(.*?)</?p>', doc, re.DOTALL)
186+
if match:
187+
doc = match.group(1)
188+
doc = self.html_to_plain_text(doc)
189+
return doc
190+
191+
def html_to_plain_text(self, doc):
192+
for tag, repl in self.html_tags.items():
193+
doc = re.sub(r'<%(tag)s>(.*?)</%(tag)s>' % {'tag': tag},
194+
r'%(repl)s\1%(repl)s' % {'repl': repl}, doc,
195+
flags=re.DOTALL)
196+
for html, text in self.html_chars.items():
197+
doc = re.sub(html, text, doc)
198+
return doc

src/robot/libdocpkg/model.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from robot.model import Tags
2020
from robot.utils import getshortdoc, Sortable, setter
2121

22+
from .htmlwriter import HtmlToText
2223
from .writer import LibdocWriter
2324
from .output import LibdocOutput
2425

@@ -62,8 +63,17 @@ def _create_toc(self, doc):
6263
def doc_format(self, format):
6364
return format or 'ROBOT'
6465

66+
@setter
67+
def inits(self, inits):
68+
return self._add_parent(inits)
69+
6570
@setter
6671
def keywords(self, kws):
72+
return self._add_parent(kws)
73+
74+
def _add_parent(self, kws):
75+
for keyword in kws:
76+
keyword.parent = self
6777
return sorted(kws)
6878

6979
@property
@@ -78,17 +88,21 @@ def save(self, output=None, format='HTML'):
7888
class KeywordDoc(Sortable):
7989

8090
def __init__(self, name='', args=(), doc='', tags=(), source=None,
81-
lineno=-1):
91+
lineno=-1, parent=None):
8292
self.name = name
8393
self.args = args
8494
self.doc = doc
8595
self.tags = Tags(tags)
8696
self.source = source
8797
self.lineno = lineno
98+
self.parent = parent
8899

89100
@property
90101
def shortdoc(self):
91-
return getshortdoc(self.doc)
102+
doc = self.doc
103+
if self.parent and self.parent.doc_format == 'HTML':
104+
doc = HtmlToText().get_shortdoc_from_html(doc)
105+
return ' '.join(getshortdoc(doc).splitlines())
92106

93107
@property
94108
def deprecated(self):

utest/libdoc/test_libdoc.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import unittest
2+
3+
from robot.utils.asserts import assert_equal
4+
from robot.libdocpkg.model import LibraryDoc, KeywordDoc
5+
from robot.libdocpkg.htmlwriter import HtmlToText, DocToHtml
6+
7+
get_shortdoc = HtmlToText().get_shortdoc_from_html
8+
get_text = HtmlToText().html_to_plain_text
9+
10+
11+
def verify_shortdoc_output(doc_input, expected):
12+
current = get_shortdoc(doc_input)
13+
assert_equal(current, expected)
14+
15+
16+
def verify_keyword_shortdoc(doc_format, doc_input, expected):
17+
libdoc = LibraryDoc(doc_format=doc_format)
18+
libdoc.keywords = [KeywordDoc(doc=doc_input)]
19+
formatter = DocToHtml(doc_format)
20+
keyword = libdoc.keywords[0]
21+
keyword.doc = formatter(keyword.doc)
22+
libdoc.doc_format = 'HTML'
23+
assert_equal(keyword.shortdoc, expected)
24+
25+
26+
class TestHtmlToDoc(unittest.TestCase):
27+
28+
def test_shortdoc_firstline(self):
29+
doc = """<p>This is the first line</p>
30+
<p>This is the second one</p>"""
31+
exp = "This is the first line"
32+
verify_shortdoc_output(doc, exp)
33+
34+
def test_shortdoc_replace_format(self):
35+
doc = "<p>This is <b>bold</b> or <i>italic</i> or <i><b>italicbold</b></i> and code.</p>"
36+
exp = "This is *bold* or _italic_ or _*italicbold*_ and code."
37+
verify_shortdoc_output(doc, exp)
38+
39+
def test_shortdoc_replace_format_multiline(self):
40+
doc = """<p>This is <b>bold</b>
41+
or <i>italic</i> or <i><b>italic
42+
bold</b></i> and <code>code</code>.</p>"""
43+
exp = """This is *bold*
44+
or _italic_ or _*italic
45+
bold*_ and ``code``."""
46+
verify_shortdoc_output(doc, exp)
47+
48+
def test_shortdoc_unexcape_html(self):
49+
doc = """<p>This &amp; &quot;<b>&lt;b&gt;is&lt;/b&gt;</b>&quot;
50+
&lt;i&gt;the&lt;/i&gt; &lt;/p&gt;&apos;first&apos; line</p>"""
51+
exp = """This & "*<b>is</b>*"
52+
<i>the</i> </p>'first' line"""
53+
verify_shortdoc_output(doc, exp)
54+
55+
56+
class TestKeywordDoc(unittest.TestCase):
57+
58+
def test_shortdoc_with_multiline_plain_text(self):
59+
doc = """Writes the message to the console.
60+
61+
If the ``newline`` argument is ``True``, a newline character is
62+
automatically added to the message.
63+
64+
By default the message is written to the standard output stream.
65+
Using the standard error stream is possibly by giving the ``stream``
66+
argument value ``'stderr'``."""
67+
exp = "Writes the message to the console."
68+
verify_keyword_shortdoc('TEXT', doc, exp)
69+
70+
def test_shortdoc_with_empty_plain_text(self):
71+
doc = ""
72+
exp = ""
73+
verify_keyword_shortdoc('TEXT', doc, exp)
74+
75+
def test_shortdoc_with_multiline_robot_format(self):
76+
doc = """Writes the
77+
*message* to
78+
_the_ ``console``.
79+
80+
If the ``newline`` argument is ``True``, a newline character is
81+
automatically added to the message.
82+
83+
By default the message is written to the standard output stream.
84+
Using the standard error stream is possibly by giving the ``stream``
85+
argument value ``'stderr'``."""
86+
exp = "Writes the *message* to _the_ ``console``."
87+
verify_keyword_shortdoc('ROBOT', doc, exp)
88+
89+
def test_shortdoc_with_empty_robot_format(self):
90+
doc = ""
91+
exp = ""
92+
verify_keyword_shortdoc('ROBOT', doc, exp)
93+
94+
def test_shortdoc_with_multiline_HTML_format(self):
95+
doc = """<p><strong>Writes</strong><br><em>the</em> <b>message</b>
96+
to <i>the</i> <code>console</code>.<br><br>
97+
If the <code>newline</code> argument is <code>True</code>, a newline character is
98+
automatically added to the message.</p>
99+
<p>By default the message is written to the standard output stream.
100+
Using the standard error stream is possibly by giving the <code>stream</code>
101+
argument value ``'stderr'``."""
102+
exp = "*Writes* _the_ *message* to _the_ ``console``."
103+
verify_keyword_shortdoc('HTML', doc, exp)
104+
105+
def test_shortdoc_with_nonclosing_p_HTML_format(self):
106+
doc = """<p><strong>Writes</strong><br><em>the</em> <b>message</b>
107+
to <i>the</i> <code>console</code>.<br><br>
108+
If the <code>newline</code> argument is <code>True</code>, a newline character is
109+
automatically added to the message.
110+
<p>By default the message is written to the standard output stream.
111+
Using the standard error stream is possibly by giving the <code>stream</code>
112+
argument value ``'stderr'``."""
113+
exp = "*Writes* _the_ *message* to _the_ ``console``."
114+
verify_keyword_shortdoc('HTML', doc, exp)
115+
116+
def test_shortdoc_with_empty_HTML_format(self):
117+
doc = ""
118+
exp = ""
119+
verify_keyword_shortdoc('HTML', doc, exp)
120+
121+
try:
122+
from docutils.core import publish_parts
123+
def test_shortdoc_with_multiline_reST_format(self):
124+
125+
doc = """Writes the **message**
126+
to *the* console.
127+
128+
If the ``newline`` argument is ``True``, a newline character is
129+
automatically added to the message.
130+
131+
By default the message is written to the standard output stream.
132+
Using the standard error stream is possibly by giving the ``stream``
133+
argument value ``'stderr'``."""
134+
exp = "Writes the *message* to _the_ console."
135+
verify_keyword_shortdoc('REST', doc, exp)
136+
137+
def test_shortdoc_with_empty_reST_format(self):
138+
doc = ""
139+
exp = ""
140+
verify_keyword_shortdoc('REST', doc, exp)
141+
except ImportError:
142+
pass

utest/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# External Python modules required by unit tests.
2-
2+
docutils >= 0.9; platform_python_implementation != 'IronPython'
33
enum34; python_version < '3.0'

0 commit comments

Comments
 (0)