From 34d256939f22a5186b32d0628ed399e4b644e798 Mon Sep 17 00:00:00 2001 From: Jeremy Stanley Date: Fri, 25 May 2018 06:10:52 +0000 Subject: [PATCH] Retool word wrapping Better handle CR+LF injection when encountering words longer than the terminal width. If a word is so wide it cannot be wrapped, leave it on a line by itself and allow the terminal to apply its own wrapping rules instead. Fixes a bug where excessive EOL markers would get added in such situations. Also more accurately handles skipping ANSI escape sequences in subsequently wrapped content. Include a word-wrapping test in the selftests to avoid regressing here. --- mudpy/misc.py | 68 +++++++++++++++++++++++++------------------------ mudpy/tests/selftest.py | 6 +++++ 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/mudpy/misc.py b/mudpy/misc.py index 4355586..439e00a 100644 --- a/mudpy/misc.py +++ b/mudpy/misc.py @@ -1116,8 +1116,10 @@ def wrap_ansi_text(text, width): # ignoring color escape sequences rel_pos = 0 - # the absolute position of the most recent whitespace character - last_whitespace = 0 + # the absolute and relative positions of the most recent whitespace + # character + last_abs_whitespace = 0 + last_rel_whitespace = 0 # whether the current character is part of a color escape sequence escape = False @@ -1131,39 +1133,38 @@ def wrap_ansi_text(text, width): # the current character is the escape character if each_character == "\x1b" and not escape: escape = True + rel_pos -= 1 # the current character is within an escape sequence elif escape: - - # the current character is m, which terminates the - # escape sequence + rel_pos -= 1 if each_character == "m": + # the current character is m, which terminates the + # escape sequence escape = False - # the current character is a newline, so reset the relative - # position (start a new line) - elif each_character == "\n": - rel_pos = 0 - last_whitespace = abs_pos - - # the current character meets the requested maximum line width, - # so we need to backtrack and find a space at which to wrap; - # special care is taken to avoid an off-by-one in case the - # current character is a double-width glyph - elif each_character != "\r" and ( - rel_pos >= width or ( - rel_pos >= width - 1 and glyph_columns( - each_character - ) == 2 - ) - ): - - # it's always possible we landed on whitespace - if unicodedata.category(each_character) in ("Cc", "Zs"): - last_whitespace = abs_pos - - # insert an eol in place of the space - text = text[:last_whitespace] + "\r\n" + text[last_whitespace + 1:] + # track the most recent whitespace we've seen + # TODO(fungi) exclude non-breaking spaces (\x0a) + elif unicodedata.category(each_character) in ("Cc", "Zs"): + if each_character == "\n": + # the current character is a newline, so reset the relative + # position too (start a new line) + rel_pos = 0 + if each_character != "\r": + # the current character is not a carriage return, so mark it as + # whitespace (we don't want to break and wrap between CR+LF) + last_abs_whitespace = abs_pos + last_rel_whitespace = rel_pos + + # the current character meets the requested maximum line width, so we + # need to wrap unless the current word is wider than the terminal (in + # which case we let it do the wrapping instead) + if last_rel_whitespace != 0 and (rel_pos > width or ( + rel_pos > width - 1 and glyph_columns(each_character) == 2)): + + # insert an eol in place of the last space + text = (text[:last_abs_whitespace] + "\r\n" + + text[last_abs_whitespace + 1:]) # increase the absolute position because an eol is two # characters but the space it replaced was only one @@ -1171,9 +1172,8 @@ def wrap_ansi_text(text, width): # now we're at the begining of a new line, plus the # number of characters wrapped from the previous line - rel_pos = 0 - for remaining_characters in text[last_whitespace:abs_pos]: - rel_pos += glyph_columns(remaining_characters) + rel_pos -= last_rel_whitespace + last_rel_whitespace = 0 # as long as the character is not a carriage return and the # other above conditions haven't been met, count it as a @@ -1181,7 +1181,9 @@ def wrap_ansi_text(text, width): elif each_character != "\r": rel_pos += glyph_columns(each_character) if unicodedata.category(each_character) in ("Cc", "Zs"): - last_whitespace = abs_pos + # TODO(fungi) exclude non-breaking spaces (\x0a) + last_abs_whitespace = abs_pos + last_rel_whitespace = rel_pos # increase the absolute position for every character abs_pos += 1 diff --git a/mudpy/tests/selftest.py b/mudpy/tests/selftest.py index b859484..bf32434 100644 --- a/mudpy/tests/selftest.py +++ b/mudpy/tests/selftest.py @@ -96,6 +96,11 @@ test_chat_mode = ( (0, r'says, "Now less chatty\."', ""), ) +test_wrapping = ( + (0, '> ', "say " + 100 * "o"), + (1, r'says,\r\n"O[o]+\."', ""), +) + test_movement = ( (0, "> ", "move north"), (0, r"You exit to the north\.", ""), @@ -257,6 +262,7 @@ dialogue = ( (test_typo_replacement, "typo replacement"), (test_sentence_capitalization, "sentence capitalization"), (test_chat_mode, "chat mode"), + (test_wrapping, "wrapping"), (test_movement, "movement"), (test_actor_disappears, "actor spontaneous disappearance"), (test_account1_teardown, "second account teardown"), -- 2.11.0