Displaying Paragraphs of Text with ZPT ====================================== To display paragraphs of text in HTML, use fmt:text-to-html. For details, see . Basics ------ >>> from lp.testing import test_tales >>> text = ('This is a paragraph.\n' ... '\n' ... 'This is another paragraph.') >>> test_tales('foo/fmt:text-to-html', foo=text) '

This is a paragraph.

\n

This is another paragraph.

' >>> text = ('This is a line.\n' ... 'This is another line.') >>> test_tales('foo/fmt:text-to-html', foo=text) '

This is a line.
\nThis is another line.

' >>> text = ( ... 'This is a paragraph that has been hard-wrapped by an e-mail' ... ' application.\n' ... 'We used to handle this specially, but we no longer do because it' ... ' was disturbing\n' ... 'the display of backtraces. Expected results:\n' ... '* joy\n' ... '* elation' ... ) >>> print test_tales('foo/fmt:text-to-html', foo=text)

This is a paragraph that has been hard-wrapped by an e-mail application.
We used to handle this specially, but we no longer do because it was disturbing
the display of backtraces. Expected results:
* joy
* elation

>>> text = ( ... " 1. Here's an example\n" ... " 2. where a list is followed by a paragraph.\n" ... " Leading spaces in a line or paragraph are presented, which " ... "means converting them to  . Trailing spaces are passed " ... "through as-is, which means browsers will ignore them, but " ... "that's fine, they're not important anyway.\n" ... ) >>> print test_tales('foo/fmt:text-to-html', foo=text)

 1. Here's an example
 2. where a list is followed by a paragraph.
   Leading spaces in a line or paragraph are presented, which means converting them to  . Trailing spaces are passed through as-is, which means browsers will ignore them, but that's fine, they're not important anyway.

>>> text = ( ... 'This is a little paragraph all by itself. How cute!' ... ) >>> test_tales('foo/fmt:text-to-html', foo=text) '

This is a little paragraph all by itself. How cute!

' >>> text = ( ... 'Here are two paragraphs with lots of whitespace between them.\n' ... '\n' ... '\n' ... '\n' ... '\n' ... 'But they\'re still just two paragraphs.') >>> print test_tales('foo/fmt:text-to-html', foo=text)

Here are two paragraphs with lots of whitespace between them.

But they're still just two paragraphs.

If a line begins with whitespace, it will not be merged with the previous line. This aids in the display of code samples: >>> text = ( ... 'This is a code sample written in Python.\n' ... ' def messageCount(self):\n' ... ' """See IRosettaStats."""\n' ... ' return self.potemplate.messageCount()\n' ... '\n' ... ' def currentCount(self, language=None):\n' ... ' """See IRosettaStats."""\n' ... ' return self.currentCount\n') >>> print test_tales('foo/fmt:text-to-html', foo=text)

This is a code sample written in Python.
    def messageCount(self):
        """See IRosettaStats."""
        return self.potemplate.messageCount()

    def currentCount(self, language=None):
        """See IRosettaStats."""
        return self.currentCount

Testing a bunch of URL links. >>> text = ( ... 'https://launchpad.net/ is the new Launchpad site\n' ... 'http://example.com/something?foo=bar&hum=baz\n' ... 'You can check the PPC md5sums at ' ... 'ftp://ftp.ubuntu.com/ubuntu/dists/breezy/main/installer-powerpc' ... '/current/images/MD5SUMS\n' ... 'irc://irc.freenode.net/#launchpad\n' ... '\n' ... 'I have a Jabber account (jabber:foo@jabber.example.com)\n' ... 'Foo Bar ') >>> print test_tales('foo/fmt:text-to-html', foo=text)

https://launchpad.net/ is the new Launchpad site
http://example.com/something?foo=bar&hum=baz
You can check the PPC md5sums at ftp://ftp.ubuntu.com/ubuntu/dists/breezy/main/installer-powerpc/current/images/MD5SUMS
irc://irc.freenode.net/#launchpad

I have a Jabber account (jabber:foo@jabber.example.com)
Foo Bar <mailto:foo.bar@example.net>

URL linkification ----------------- fmt:text-to-html knows how to linkify URLs: >>> text = ( ... 'http://localhost:8086/bar/baz/foo.html\n' ... 'ftp://localhost:8086/bar/baz/foo.bar.html\n' ... 'sftp://localhost:8086/bar/baz/foo.bar.html.\n' ... 'http://localhost:8086/bar/baz/foo.bar.html;\n' ... 'news://localhost:8086/bar/baz/foo.bar.html:\n' ... 'http://localhost:8086/bar/baz/foo.bar.html?\n' ... 'http://localhost:8086/bar/baz/foo.bar.html,\n' ... '\n' ... ',\n' ... '.\n' ... ';\n' ... ':\n' ... '?\n' ... '(http://localhost:8086/bar/baz/foo.bar.html)\n' ... '(http://localhost:8086/bar/baz/foo.bar.html),\n' ... '(http://localhost:8086/bar/baz/foo.bar.html).\n' ... '(http://localhost:8086/bar/baz/foo.bar.html);\n' ... '(http://localhost:8086/bar/baz/foo.bar.html):\n' ... 'http://localhost/bar/baz/foo.bar.html?a=b&b=a\n' ... 'http://localhost/bar/baz/foo.bar.html?a=b&b=a.\n' ... 'http://localhost/bar/baz/foo.bar.html?a=b&b=a,\n' ... 'http://localhost/bar/baz/foo.bar.html?a=b&b=a;\n' ... 'http://localhost/bar/baz/foo.bar.html?a=b&b=a:\n' ... 'http://localhost/bar/baz/foo.bar.html?' ... 'a=b&b=a:b;c@d_e%f~g#h,j!k-l+m$n*o\'p\n' ... 'http://www.searchtools.com/test/urls/(parens).html\n' ... 'http://www.searchtools.com/test/urls/-dash.html\n' ... 'http://www.searchtools.com/test/urls/_underscore.html\n' ... 'http://www.searchtools.com/test/urls/period.x.html\n' ... 'http://www.searchtools.com/test/urls/!exclamation.html\n' ... 'http://www.searchtools.com/test/urls/~tilde.html\n' ... 'http://www.searchtools.com/test/urls/*asterisk.html\n' ... 'irc://irc.freenode.net/launchpad\n' ... 'irc://irc.freenode.net/%23launchpad,isserver\n' ... 'mailto:noreply@launchpad.net\n' ... 'jabber:noreply@launchpad.net\n' ... 'http://localhost/foo?xxx&\n' ... 'http://localhost?testing=[square-brackets-in-query]\n' ... ) >>> print test_tales('foo/fmt:text-to-html', foo=text)

http://localhost:8086/bar/baz/foo.html
ftp://localhost:8086/bar/baz/foo.bar.html
sftp://localhost:8086/bar/baz/foo.bar.html.
http://localhost:8086/bar/baz/foo.bar.html;
news://localhost:8086/bar/baz/foo.bar.html:
http://localhost:8086/bar/baz/foo.bar.html?
http://localhost:8086/bar/baz/foo.bar.html,
<http://localhost:8086/bar/baz/foo.bar.html>
<http://localhost:8086/bar/baz/foo.bar.html>,
<http://localhost:8086/bar/baz/foo.bar.html>.
<http://localhost:8086/bar/baz/foo.bar.html>;
<http://localhost:8086/bar/baz/foo.bar.html>:
<http://localhost:8086/bar/baz/foo.bar.html>?
(http://localhost:8086/bar/baz/foo.bar.html)
(http://localhost:8086/bar/baz/foo.bar.html),
(http://localhost:8086/bar/baz/foo.bar.html).
(http://localhost:8086/bar/baz/foo.bar.html);
(http://localhost:8086/bar/baz/foo.bar.html):
http://localhost/bar/baz/foo.bar.html?a=b&b=a
http://localhost/bar/baz/foo.bar.html?a=b&b=a.
http://localhost/bar/baz/foo.bar.html?a=b&b=a,
http://localhost/bar/baz/foo.bar.html?a=b&b=a;
http://localhost/bar/baz/foo.bar.html?a=b&b=a:
http://localhost/bar/baz/foo.bar.html?a=b&b=a:b;c@d_e%f~g#h,j!k-l+m$n*o'p
http://www.searchtools.com/test/urls/(parens).html
http://www.searchtools.com/test/urls/-dash.html
http://www.searchtools.com/test/urls/_underscore.html
http://www.searchtools.com/test/urls/period.x.html
http://www.searchtools.com/test/urls/!exclamation.html
http://www.searchtools.com/test/urls/~tilde.html
http://www.searchtools.com/test/urls/*asterisk.html
irc://irc.freenode.net/launchpad
irc://irc.freenode.net/%23launchpad,isserver
mailto:noreply@launchpad.net
jabber:noreply@launchpad.net
http://localhost/foo?xxx&
http://localhost?testing=[square-brackets-in-query]

The fmt:text-to-html formatter leaves a number of non-URIs unlinked: >>> text = ( ... 'nothttp://launchpad.net/\n' ... 'http::No-cache=True\n') >>> print test_tales('foo/fmt:text-to-html', foo=text)

nothttp://launchpad.net/
http::No-cache=True

Bug references -------------- fmt:text-to-html is also smart enough to convert bug references into links: >>> text = ( ... 'bug 123\n' ... 'bug 123\n' ... 'bug #123\n' ... 'bug number 123\n' ... 'bug number. 123\n' ... 'bug num 123\n' ... 'bug num. 123\n' ... 'bug no 123\n' ... 'bug report 123\n' ... 'bug no. 123\n' ... 'bug#123\n' ... 'bug-123\n' ... 'bug-report-123\n' ... 'bug=123\n' ... 'bug\n' ... '#123\n' ... 'debug #52\n') >>> print test_tales('foo/fmt:text-to-html', foo=text)

bug 123
bug 123
bug #123
bug number 123
bug number. 123
bug num 123
bug num. 123
bug no 123
bug report 123
bug no. 123
bug#123
bug-123
bug-report-123
bug=123
bug
#123

debug #52

>>> text = ( ... 'bug 123\n' ... 'bug 123\n') >>> print test_tales('foo/fmt:text-to-html', foo=text)

bug 123
bug 123

>>> text = ( ... 'bug 1234\n' ... 'bug 123\n') >>> print test_tales('foo/fmt:text-to-html', foo=text)

bug 1234
bug 123

>>> text = 'bug 0123\n' >>> print test_tales('foo/fmt:text-to-html', foo=text)

bug 0123

We linkify bugs that are in the Ubuntu convention for referring to bugs in Debian changelogs. >>> text = 'LP: #123.\n' >>> print test_tales('foo/fmt:text-to-html', foo=text)

LP: #123.

Works with multiple bugs: >>> text = 'LP: #123, #2.\n' >>> print test_tales('foo/fmt:text-to-html', foo=text)

LP: #123, #2.

And with lower case 'lp' too: >>> text = 'lp: #123, #2.\n' >>> print test_tales('foo/fmt:text-to-html', foo=text)

lp: #123, #2.

Even line breaks cannot stop the power of bug linking: >>> text = 'LP: #123,\n#2.\n' >>> print test_tales('foo/fmt:text-to-html', foo=text)

LP: #123,
#2.

To check a private bug, we need to log in and set a bug to be private. >>> from zope.component import getUtility >>> from lp.bugs.interfaces.bug import IBugSet >>> bugset = getUtility(IBugSet) >>> firefox_crashes = bugset.get(6) >>> login("test@canonical.com") >>> current_user = getUtility(ILaunchBag).user >>> firefox_crashes.setPrivate(True, current_user) True Bug.setPrivate adds all indirect subscribers to the bug as direct subscribers, but we want to see what the bug looks like if we're not a subscriber. >>> firefox_crashes.unsubscribe(current_user, current_user) A private bug is still linked as no check is made on the actual bug. >>> text = 'bug 6\n' >>> print test_tales('foo/fmt:text-to-html', foo=text)

bug 6

FAQ references -------------- FAQ references are global, and also linkified: >>> text = ( ... 'faq 1\n' ... 'faq #2\n' ... 'faq-2\n' ... 'faq=2\n' ... 'faq item 1\n' ... 'faq number 2\n' ... ) >>> print test_tales('foo/fmt:text-to-html', foo=text)

faq 1
faq #2
faq-2
faq=2
faq item 1
faq number 2

Except, that is, when the FAQ doesn't exist: >>> text = ( ... 'faq 999\n' ... ) >>> print test_tales('foo/fmt:text-to-html', foo=text)

faq 999

Branch references ----------------- Branch references are linkified: >>> text = ( ... 'lp:~foo/bar/baz\n' ... 'lp:~foo/bar/bug-123\n' ... 'lp:~foo/+junk/baz\n' ... 'lp:~foo/ubuntu/jaunty/evolution/baz\n' ... 'lp:foo/bar\n' ... 'lp:foo\n' ... 'lp:foo,\n' ... 'lp:foo/bar.\n' ... 'lp:foo/bar/baz\n' ... 'lp:///foo\n' ... 'lp:/foo\n') >>> print test_tales('foo/fmt:text-to-html', foo=text)

lp:~foo/bar/baz
lp:~foo/bar/bug-123
lp:~foo/+junk/baz
lp:~foo/ubuntu/jaunty/evolution/baz
lp:foo/bar
lp:foo
lp:foo,
lp:foo/bar.
lp:foo/bar/baz
lp:///foo
lp:/foo

Text that looks like a branch reference, but is followed only by digits is treated as a link to a bug. >>> text = 'lp:1234' >>> print test_tales('foo/fmt:text-to-html', foo=text)

lp:1234

We are even smart enough to notice the trailing punctuation gunk and separate that from the link. >>> text = 'lp:1234,' >>> print test_tales('foo/fmt:text-to-html', foo=text)

lp:1234,

OOPS references --------------- fmt:text-to-html is also smart enough to convert OOPS references into links. However, it only does this if the logged in person is a member of the Launchpad Developers team. XXX 2006-08-23 jamesh We explicitly cal set_developer_in_launchbag_before_traversal() here. If this event handler is not called, then the "developer" attribute in the launchbag is not updated. Normally it would be called during the request before traversal, but we aren't doing publication traversal in this test. https://launchpad.net/bugs/30746 When not logged in as a privileged user, no link: >>> from lp.services.webapp.launchbag import ( ... set_developer_in_launchbag_before_traversal) >>> login('test@canonical.com') >>> set_developer_in_launchbag_before_traversal(None) >>> getUtility(ILaunchBag).developer False >>> text = 'OOPS-38C23' >>> print test_tales('foo/fmt:text-to-html', foo=text)

OOPS-38C23

After login, a link: >>> login('foo.bar@canonical.com') >>> set_developer_in_launchbag_before_traversal(None) >>> getUtility(ILaunchBag).developer True >>> print test_tales('foo/fmt:text-to-html', foo=text)

OOPS-38C23

OOPS references can take a number of forms: >>> text = 'OOPS-38C23' >>> print test_tales('foo/fmt:text-to-html', foo=text)

OOPS-38C23

>>> text = 'OOPS-123abcdef' >>> print test_tales('foo/fmt:text-to-html', foo=text)

OOPS-123abcdef

>>> text = 'OOPS-abcdef123' >>> print test_tales('foo/fmt:text-to-html', foo=text)

OOPS-abcdef123

If the configuration value doesn't end with a slash, we won't add one. This lets us configure the URL to use query parameters. >>> from lp.services.config import config >>> oops_root_url = """ ... [launchpad] ... oops_root_url: http://foo/bar ... """ >>> config.push('oops_root_url', oops_root_url) >>> text = 'OOPS-38C23' >>> print test_tales('foo/fmt:text-to-html', foo=text)

OOPS-38C23

>>> config_data = config.pop('oops_root_url') Check against false positives: >>> text = 'OOPS code' >>> print test_tales('foo/fmt:text-to-html', foo=text)

OOPS code

Reset login information. >>> login('test@canonical.com') >>> set_developer_in_launchbag_before_traversal(None) >>> getUtility(ILaunchBag).developer False Regex helper functions ---------------------- The _substitute_matchgroup_for_spaces() static method is part of the fmt:text-to-html code. It is a helper for writing regular expressions where we want to replace a variable number of spaces with the same number of   entities. >>> from lp.app.browser.stringformatter import FormattersAPI >>> import re >>> matchobj = re.match('foo(.*)bar', 'fooX Ybar') >>> matchobj.groups() ('X Y',) >>> FormattersAPI._substitute_matchgroup_for_spaces(matchobj) '   ' The _linkify_substitution() static method is used for converting bug references or URLs into links. It uses the named matchgroups 'bug' and 'bugnum' when it is dealing with bugs, and 'url' when it is dealing with URLs. First, let's try a match of nothing it understands. This is a bug, so we get an AssertionError. >>> matchobj = re.match( ... ('(?Pxxx)?(?Pwww)?(?Pyyy)?(?Pzzz)?' ... '(?Pwww)?(?Pvvv)?'), ... 'fish') >>> sorted(matchobj.groupdict().items()) [('bug', None), ('clbug', None), ('faq', None), ('lpbranchurl', None), ('oops', None), ('url', None)] >>> FormattersAPI._linkify_substitution(matchobj) Traceback (most recent call last): ... AssertionError: Unknown pattern matched. When we have a URL, the URL is made into a link. A quote is added to the url to demonstrate quoting in the HTML attribute. >>> matchobj = re.match('(?Pxxx)?(?Py"y)?', 'y"y') >>> sorted(matchobj.groupdict().items()) [('bug', None), ('url', 'y"y')] >>> FormattersAPI._linkify_substitution(matchobj) 'y"y' When we have a bug reference, the 'bug' group is used as the text of the link, and the 'bugnum' is used to look up the bug. >>> matchobj = re.match( ... '(?Pxxxx)?(?P2)?(?Pyyy)?', 'xxxx2') >>> sorted(matchobj.groupdict().items()) [('bug', 'xxxx'), ('bugnum', '2'), ('url', None)] >>> FormattersAPI._linkify_substitution(matchobj) 'xxxx' When the bugnum doesn't match any bug, we still get a link, but get a message in the link's title. >>> matchobj = re.match( ... '(?Pxxxx)?(?P2000)?(?Pyyy)?', 'xxxx2000') >>> sorted(matchobj.groupdict().items()) [('bug', 'xxxx'), ('bugnum', '2000'), ('url', None)] >>> FormattersAPI._linkify_substitution(matchobj) 'xxxx'