diff options
author | Greg Ward <gward@python.net> | 2002-06-07 21:43:37 (GMT) |
---|---|---|
committer | Greg Ward <gward@python.net> | 2002-06-07 21:43:37 (GMT) |
commit | 00935824898a826f2dd6c65933667445075cdb2e (patch) | |
tree | 6e1feb74d416163dd35b5d9356911dff8d9bcda1 | |
parent | 1790e65d43dae0cd59f8008b3549a017ff4f16d6 (diff) | |
download | cpython-00935824898a826f2dd6c65933667445075cdb2e.zip cpython-00935824898a826f2dd6c65933667445075cdb2e.tar.gz cpython-00935824898a826f2dd6c65933667445075cdb2e.tar.bz2 |
Initial revision. Currently biased towards English in a fixed-width font,
according to the conventions that I (and Tim Peters) learned in school.
-rw-r--r-- | Lib/textwrap.py | 239 |
1 files changed, 239 insertions, 0 deletions
diff --git a/Lib/textwrap.py b/Lib/textwrap.py new file mode 100644 index 0000000..355c469 --- /dev/null +++ b/Lib/textwrap.py @@ -0,0 +1,239 @@ +""" +Utilities for wrapping text strings and filling text paragraphs. +""" + +__revision__ = "$Id$" + +import string, re + + +# XXX is this going to be implemented properly somewhere in 2.3? +def islower (c): + return c in string.lowercase + + +class TextWrapper: + """ + Object for wrapping/filling text. The public interface consists of + the wrap() and fill() methods; the other methods are just there for + subclasses to override in order to tweak the default behaviour. + If you want to completely replace the main wrapping algorithm, + you'll probably have to override _wrap_chunks(). + + Several instance attributes control various aspects of + wrapping: + expand_tabs + if true (default), tabs in input text will be expanded + to spaces before further processing. Each tab will + become 1 .. 8 spaces, depending on its position in its line. + If false, each tab is treated as a single character. + replace_whitespace + if true (default), all whitespace characters in the input + text are replaced by spaces after tab expansion. Note + that expand_tabs is false and replace_whitespace is true, + every tab will be converted to a single space! + break_long_words + if true (default), words longer than the line width constraint + will be broken. If false, those words will not be broken, + and some lines might be longer than the width constraint. + """ + + whitespace_trans = string.maketrans(string.whitespace, + ' ' * len(string.whitespace)) + + # This funky little regex is just the trick for splitting + # text up into word-wrappable chunks. E.g. + # "Hello there -- you goof-ball, use the -b option!" + # splits into + # Hello/ /there/ /--/ /you/ /goof-/ball,/ /use/ /the/ /-b/ /option! + # (after stripping out empty strings). + wordsep_re = re.compile(r'(\s+|' # any whitespace + r'\w{2,}-(?=\w{2,})|' # hyphenated words + r'(?<=\w)-{2,}(?=\w))') # em-dash + + + def __init__ (self): + self.expand_tabs = 1 + self.replace_whitespace = 1 + self.break_long_words = 1 + + + # -- Private methods ----------------------------------------------- + # (possibly useful for subclasses to override) + + def _munge_whitespace (self, text): + """_munge_whitespace(text : string) -> string + + Munge whitespace in text: expand tabs and convert all other + whitespace characters to spaces. Eg. " foo\tbar\n\nbaz" + becomes " foo bar baz". + """ + if self.expand_tabs: + text = text.expandtabs() + if self.replace_whitespace: + text = text.translate(self.whitespace_trans) + return text + + + def _split (self, text): + """_split(text : string) -> [string] + + Split the text to wrap into indivisible chunks. Chunks are + not quite the same as words; see wrap_chunks() for full + details. As an example, the text + Look, goof-ball -- use the -b option! + breaks into the following chunks: + 'Look,', ' ', 'goof-', 'ball', ' ', '--', ' ', + 'use', ' ', 'the', ' ', '-b', ' ', 'option!' + """ + chunks = self.wordsep_re.split(text) + chunks = filter(None, chunks) + return chunks + + def _fix_sentence_endings (self, chunks): + """_fix_sentence_endings(chunks : [string]) + + Correct for sentence endings buried in 'chunks'. Eg. when the + original text contains "... foo.\nBar ...", munge_whitespace() + and split() will convert that to [..., "foo.", " ", "Bar", ...] + which has one too few spaces; this method simply changes the one + space to two. + """ + i = 0 + while i < len(chunks)-1: + # chunks[i] looks like the last word of a sentence, + # and it's followed by a single space. + if (chunks[i][-1] == "." and + chunks[i+1] == " " and + islower(chunks[i][-2])): + chunks[i+1] = " " + i += 2 + else: + i += 1 + + def _handle_long_word (self, chunks, cur_line, cur_len, width): + """_handle_long_word(chunks : [string], + cur_line : [string], + cur_len : int, width : int) + + Handle a chunk of text (most likely a word, not whitespace) that + is too long to fit in any line. + """ + space_left = width - cur_len + + # If we're allowed to break long words, then do so: put as much + # of the next chunk onto the current line as will fit. + if self.break_long_words: + cur_line.append(chunks[0][0:space_left]) + chunks[0] = chunks[0][space_left:] + + # Otherwise, we have to preserve the long word intact. Only add + # it to the current line if there's nothing already there -- + # that minimizes how much we violate the width constraint. + elif not cur_line: + cur_line.append(chunks.pop(0)) + + # If we're not allowed to break long words, and there's already + # text on the current line, do nothing. Next time through the + # main loop of _wrap_chunks(), we'll wind up here again, but + # cur_len will be zero, so the next line will be entirely + # devoted to the long word that we can't handle right now. + + def _wrap_chunks (self, chunks, width): + """_wrap_chunks(chunks : [string], width : int) -> [string] + + Wrap a sequence of text chunks and return a list of lines of + length 'width' or less. (If 'break_long_words' is false, some + lines may be longer than 'width'.) Chunks correspond roughly to + words and the whitespace between them: each chunk is indivisible + (modulo 'break_long_words'), but a line break can come between + any two chunks. Chunks should not have internal whitespace; + ie. a chunk is either all whitespace or a "word". Whitespace + chunks will be removed from the beginning and end of lines, but + apart from that whitespace is preserved. + """ + lines = [] + + while chunks: + + cur_line = [] # list of chunks (to-be-joined) + cur_len = 0 # length of current line + + # First chunk on line is whitespace -- drop it. + if chunks[0].strip() == '': + del chunks[0] + + while chunks: + l = len(chunks[0]) + + # Can at least squeeze this chunk onto the current line. + if cur_len + l <= width: + cur_line.append(chunks.pop(0)) + cur_len += l + + # Nope, this line is full. + else: + break + + # The current line is full, and the next chunk is too big to + # fit on *any* line (not just this one). + if chunks and len(chunks[0]) > width: + self._handle_long_word(chunks, cur_line, cur_len, width) + + # If the last chunk on this line is all whitespace, drop it. + if cur_line and cur_line[-1].strip() == '': + del cur_line[-1] + + # Convert current line back to a string and store it in list + # of all lines (return value). + if cur_line: + lines.append(''.join(cur_line)) + + return lines + + + # -- Public interface ---------------------------------------------- + + def wrap (self, text, width): + """wrap(text : string, width : int) -> [string] + + Split 'text' into multiple lines of no more than 'width' + characters each, and return the list of strings that results. + Tabs in 'text' are expanded with string.expandtabs(), and all + other whitespace characters (including newline) are converted to + space. + """ + text = self._munge_whitespace(text) + if len(text) <= width: + return [text] + chunks = self._split(text) + self._fix_sentence_endings(chunks) + return self._wrap_chunks(chunks, width) + + def fill (self, text, width, initial_tab="", subsequent_tab=""): + """fill(text : string, + width : int, + initial_tab : string = "", + subsequent_tab : string = "") + -> string + + Reformat the paragraph in 'text' to fit in lines of no more than + 'width' columns. The first line is prefixed with 'initial_tab', + and subsequent lines are prefixed with 'subsequent_tab'; the + lengths of the tab strings are accounted for when wrapping lines + to fit in 'width' columns. + """ + lines = self.wrap(text, width) + sep = "\n" + subsequent_tab + return initial_tab + sep.join(lines) + + +# Convenience interface + +_wrapper = TextWrapper() + +def wrap (text, width): + return _wrapper.wrap(text, width) + +def fill (text, width, initial_tab="", subsequent_tab=""): + return _wrapper.fill(text, width, initial_tab, subsequent_tab) |