= TALES email formatting = There are many edge-cases when marking up email text for presentation. There is subtle differences in how people quote text that must be handled properly. There are also cases were content may look like quoted text, but it is not. See 'The fmt: namespace to get strings (hiding)' in tales.txt for the common use cases. First, let's bring in a small helper function: >>> from lp.testing import test_tales == Quoting styles == Paragraphs that mix quoted and reply text fold only the quoted lines. >>> mixed_quoted_text = ('Mister X wrote:\n' ... '> This is a quoted line\n' ... 'This is a reply to the line above.\n' ... 'This is a continuation line.' ... '\n') >>> print test_tales('foo/fmt:email-to-html', foo=mixed_quoted_text)

Mister X wrote:
> This is a quoted line
This is a reply to the line above.
This is a continuation line.

A quoted section is folded without affecting the display of the surrounding paragraph, even if there are no blank lines to separate the quoted section from the paragraph. >>> quoted_remark_text = ('Attribution line\n' ... '> quoted_line\n' ... 'Remark line.\n' ... '\n') >>> print test_tales('foo/fmt:email-to-html', foo=quoted_remark_text)

Attribution line
> quoted_line
Remark line.

Multiple quoted paragraphs are treated as a single continuous folded span. >>> quoted_paragraphs = ('Attribution line\n' ... '> First line in the first paragraph.\n' ... '> Second line in the first paragraph.\n' ... '> \n' ... '> First line in the second paragraph.\n' ... '> Second line in the second paragraph.\n' ... '> \n' ... '> First line in the third paragraph.\n' ... '> Second line in the third paragraph.\n' ... '\n') >>> print test_tales('foo/fmt:email-to-html', foo=quoted_paragraphs)

Attribution line
> First line in the first paragraph.
> Second line in the first paragraph.
>
> First line in the second paragraph.
> Second line in the second paragraph.
>
> First line in the third paragraph.
> Second line in the third paragraph.

Paragraphs with nested quoting fold all the quoted lines. There is no distinction between the nested levels of quoting. >>> nested_quoting = ('>>>> four\n' ... '>>> three\n' ... '>> two\n' ... '> one\n') >>> print test_tales('foo/fmt:email-to-html', foo=nested_quoting)

>>>> four
>>> three
>> two
> one

Quoting styles vary between email clients, and how the user starts the quote. Starting runs like '>> ' are as valid as '> ', so they are wrapped in a foldable-quoted span. >>> weird_quoted_text = ('Ms. Y wrote:\n' ... '>> This is a double quoted line\n' ... '>> > This is a triple quoted line.\n' ... '\n') >>> print test_tales('foo/fmt:email-to-html', foo=weird_quoted_text)

Ms. Y wrote:
>> This is a double quoted line
>> > This is a triple quoted line.

== Python interpreter and dpkg handling == The output from the Python interpreter is not quoted text. Passages of text that start with '>>> ' are exempted from the 'foldable-quoted' rules. Note that when '>>> ' occurs inside an existing quoted passage it will be folded because they are a continuation of a quote (see the preceding nested quoting test). # XXX sinzui 2007-08-13 bug=132263: # Passages may be wrongly be interpreted as Python because they start # with '>>> '. The formatter does not check that next and previous # lines of text consistently uses '>>> ' as Python would. >>> python = ('>>> tz = pytz.timezone("Asia/Calcutta")\n' ... '>>> mydate = datetime.datetime(2007, 2, 18, 15, 35)\n' ... '>>> print tz.localize(mydate)\n' ... '2007-02-18 15:35:00+05:30\n' ... '\n') >>> not_python = ('> This line really is a quoted passage.\n' ... '>>> This does not invoke an exception rule.\n' ... '\n') >>> print test_tales('foo/fmt:email-to-html', ... foo='\n'.join([python, not_python]))

>>> tz = pytz.timezone("Asia/Calcutta"... >>> mydate = datetime.datetime(2007, 2, ... 2007-02-18 15:35:00+05:30

> This line really is a quoted ... >>> This does not invoke an exception rule.

Dpkg generates lines that start with a '|' that will be confused with quoted text. Dpkg is common in messages, and when it is, we do not fold lines that start with a '|'. We sometimes receive bad dpkg output where the lines are broken, and we must take care to identify that output and not fold it. >>> bar_quoted_text = ('Someone said sometime ago:\n' ... '| Quote passages are folded.\n' ... '\n') >>> print test_tales('foo/fmt:email-to-html', foo=bar_quoted_text)

Someone said sometime ago:
| Quote passages are folded.

>>> dpkg = ('dpkg -l libdvdread3\n' ... 'Desired=Unknown/Install/Remove/Purge/Hold\n' ... '| Status=Not/Installed/Config-files/Unpacked/Failed-co\n' ... '|/ Err?=(none)/Hold/Reinst-required/X=both-problems\n' ... '||/ Name Version Description\n' ... '+++-==============-==============-====================\n' ... 'ii libdvdread3 0.9.7-2ubuntu1 library for reading DVDs\n' ... '\n') >>> print test_tales('foo/fmt:email-to-html', foo=dpkg)

dpkg -l libdvdread3
Desired=Unknown/Install/... | Status=Not/Installed/Config-... |/ Err?=(none)/Hold/Reinst-required/... ||/ Name Version Description
+++-==============-=========... ii libdvdread3 0.9.7-2ubuntu1 library for reading DVDs

>>> bad_dpkg = ('When dpkg output is in text, possibly tampered with,\n' ... "we must take care to identify '|' quoted passages.\n" ... '$ Desired=Unknown/Install/Remove/Purge/Hold\n' ... '|\n' ... ' Status=Not/Installed/Config-files/Unpacked/Failed-co\n' ... '|/ Err?=(none)/Hold/Reinst-required/X=both-problems\n' ... '||/ Name Version Description\n' ... '+++-==============-==============-==================\n' ... 'ii libdvdread3 0.9.7-2ubuntu1 library for reading DVDs\n' ... '\n') >>> print test_tales('foo/fmt:email-to-html', ... foo='\n'.join([bad_dpkg]))

When dpkg output is in text, possibly tampered with,
we must take care to identify '|' quoted passages.
$ Desired=Unknown/Install/Remove/... |
 Status=Not/Installed/Config-... |/ Err?=(none)/Hold/Reinst-required/... ||/ Name Version Description
+++-==============-=========... ii libdvdread3 0.9.7-2ubuntu1 library for reading DVDs