zope.schema has Text and TextLine. The former is for multiline text, the latter is for a single line, as the name suggests. Zope 3 forms will use a text area for Text fields and an input box for TextLine fields. Display widgets, however, apply no special formatting (other than HTML-quoting of characters like <, > and &), and since newlines are treated the same way as spaces in HTML, your multiline text gets collapsed into a single paragraph.
Here's a pattern I've been using in Zope 3 to display multiline user-entered text as several paragraphs:
import cgi from zope.component import adapts from zope.publisher.browser import BrowserView from zope.publisher.interfaces import IRequest class SplitToParagraphsView(BrowserView): """Splits a string into paragraphs via newlines.""" adapts(None, IRequest) def paragraphs(self): if self.context is None: return  return filter(None, [s.strip() for s in self.context.splitlines()]) def __call__(self): return "".join('<p>%s</p>\n' % cgi.escape(p) for p in self.paragraphs())
<configure xmlns="http://namespaces.zope.org/zope"> <view for="*" name="paragraphs" type="zope.publisher.interfaces.browser.IBrowserRequest" factory=".views.SplitToParagraphsView" permission="zope.Public" /> </configure>
<p tal:replace="structure object/attribute/@@paragraphs" />
Update: The view really ought to be registered twice: once for basestring and once for NoneType. I was too lazy to figure out the dotted names for those (or check if zope.interface has external interface declarations for them), so I registered it for "*". You should know that this makes the view available for arbitrary objects (but won't work for most of them, since they don't have a splitlines method), and that it is, sadly, accessible to users who may try to hack your system by typing things like @@paragraphs in the browser's address bar. Ignas Mikalajūnas offers an alternative solution using TALES path adapters.