~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/utextwrap.py

  • Committer: INADA Naoki
  • Date: 2011-05-05 03:22:29 UTC
  • mto: This revision was merged to the branch mainline in revision 5874.
  • Revision ID: songofacandy@gmail.com-20110505032229-439iyvma4xv94nvu
utextwrap: Change a way to split between CJK characters.
_split() splits each double width character into a single chunk.
This way is simpler but slower.

Show diffs side-by-side

added added

removed removed

Lines of Context:
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
 
18
 
import osutils
 
18
# UTextWrapper._handle_long_word, UTextWrapper._wrap_chunks,
 
19
# wrap and fill is copied from Python's textwrap module
 
20
# (under PSF license) and modified for support CJK.
 
21
 
19
22
import textwrap
20
23
from unicodedata import east_asian_width as _eawidth
21
24
 
 
25
from bzrlib import osutils
 
26
 
22
27
__all__ = ["UTextWrapper", "fill", "wrap"]
23
28
 
24
29
def _width(s):
38
43
        w += (c in 'FWA' and 2) or 1
39
44
    return w
40
45
 
41
 
def _break_cjkword(word, width):
42
 
    """Split `word` by `width`.
43
 
 
44
 
    Returns a tuple contains 2 strings. First string is head of
45
 
    `word` that's length is less than `width`. Second string is
46
 
    rest of `word`.
47
 
 
48
 
    The border of head and rest is next to double width character.
49
 
    Because spaces is not used as word separator on CJK.
50
 
 
51
 
    When ``_width(word) < width``, returns ``(word, '')``.
52
 
    When can't split anywhere, returns ``('', word)``.
 
46
def _cut(s, width):
 
47
    """Returns head and rest of s. (head+rest == s).
 
48
 
 
49
    Head is large as long as _width(head) <= width.
53
50
    """
 
51
    if isinstance(s, str):
 
52
        return s[:width], s[width:]
 
53
    assert isinstance(s, unicode)
54
54
    w = 0
55
 
    for pos, c in enumerate(word):
56
 
        nw = _width(c)
57
 
        if w + nw > width:
58
 
            break
59
 
        w += nw
60
 
    else:
61
 
        return word, ''
62
 
    if pos>0 and _width(word[pos]) == 2:
63
 
        # "sssDDD" and pos=3 => "sss", "DDD" (D is double width)
64
 
        return word[:pos], word[pos:]
65
 
    # "DDDssss" and pos=4 => "DDD", "ssss"
66
 
    while pos > 0 and _width(word[pos-1]) != 2:
67
 
        pos -= 1
68
 
    if pos == 0:
69
 
        return '', word
70
 
    return word[:pos], word[pos:]
 
55
    for pos, c in enumerate(s):
 
56
        w += (_eawidth(c) in 'FWA' and 2) or 1
 
57
        if w > width:
 
58
            return s[:pos], s[pos:]
 
59
    return s, ''
71
60
 
72
61
 
73
62
class UTextWrapper(textwrap.TextWrapper):
85
74
        textwrap.TextWrapper.__init__(self, width, **kwargs)
86
75
 
87
76
    def _handle_long_word(self, chunks, cur_line, cur_len, width):
88
 
        head, rest = _break_cjkword(chunks[-1], width)
89
 
        if head:
90
 
            chunks.pop()
 
77
        # Figure out when indent is larger than the specified width, and make
 
78
        # sure at least one character is stripped off on every pass
 
79
        if width < 2:
 
80
            space_left = chunks[-1] and _width(chunks[-1][0]) or 1
 
81
        else:
 
82
            space_left = width - cur_len
 
83
 
 
84
        # If we're allowed to break long words, then do so: put as much
 
85
        # of the next chunk onto the current line as will fit.
 
86
        if self.break_long_words:
 
87
            head, rest = _cut(chunks[-1], space_left)
 
88
            cur_line.append(head)
91
89
            if rest:
92
 
                chunks.append(rest)
93
 
            chunks.append(head)
94
 
            return
95
 
        textwrap.TextWrapper._handle_long_word(
96
 
                self, chunks, cur_line, cur_len, width)
 
90
                chunks[-1] = rest
 
91
            else:
 
92
                del chunks[-1]
 
93
 
 
94
        # Otherwise, we have to preserve the long word intact.  Only add
 
95
        # it to the current line if there's nothing already there --
 
96
        # that minimizes how much we violate the width constraint.
 
97
        elif not cur_line:
 
98
            cur_line.append(chunks.pop())
 
99
 
 
100
        # If we're not allowed to break long words, and there's already
 
101
        # text on the current line, do nothing.  Next time through the
 
102
        # main loop of _wrap_chunks(), we'll wind up here again, but
 
103
        # cur_len will be zero, so the next line will be entirely
 
104
        # devoted to the long word that we can't handle right now.
97
105
 
98
106
    def _wrap_chunks(self, chunks):
99
107
        lines = []
137
145
 
138
146
                # Nope, this line is full.
139
147
                else:
140
 
                    # break CJK words
141
 
                    head, rest = _break_cjkword(chunks[-1], width-cur_len)
142
 
                    if head:
143
 
                        cur_line.append(head)
144
 
                        cur_len += _width(head)
145
 
                        assert rest
146
 
                        chunks[-1] = rest
147
148
                    break
148
149
 
149
150
            # The current line is full, and the next chunk is too big to
162
163
 
163
164
        return lines
164
165
 
 
166
    def _split(self, text):
 
167
        chunks = textwrap.TextWrapper._split(self, unicode(text))
 
168
        cjk_split_chunks = []
 
169
        for chunk in chunks:
 
170
            assert chunk # TextWrapper._split removes empty chunk
 
171
            prev_pos = 0
 
172
            for pos, char in enumerate(chunk):
 
173
                # Treats all asian character are line breakable.
 
174
                # But it is not true because line breaking is
 
175
                # prohibited around some characters.
 
176
                # See UAX # 14 "UNICODE LINE BREAKING ALGORITHM"
 
177
                if _eawidth(char) in 'FWA':
 
178
                    if prev_pos < pos:
 
179
                        cjk_split_chunks.append(chunk[prev_pos:pos])
 
180
                    cjk_split_chunks.append(char)
 
181
                    prev_pos = pos+1
 
182
            if prev_pos < len(chunk):
 
183
                cjk_split_chunks.append(chunk[prev_pos:])
 
184
        return cjk_split_chunks
 
185
 
165
186
    def wrap(self, text):
166
187
        # ensure text is unicode
167
188
        return textwrap.TextWrapper.wrap(self, unicode(text))