diff --git a/CHANGELOG.md b/CHANGELOG.md index b546babc06..d5441d1b0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ avoid inconsistencies with PostgreSQL databases (Griatch) - [Fix][issue3513]: Fixed issue where OnDemandHandler could traceback on an un-pickle-able object and cause an error at server shutdown (Griatch) +- [Fix][issue3649]: The `:j` command in EvEditor would squash empty lines (Griatch) - [Doc][pull3801]: Move Evennia doc build system to latest Sphinx/myST (PowershellNinja, also honorary mention to electroglyph) - [Doc][pull3800]: Describe support for Telnet SSH in HAProxy documentation (holl0wstar) @@ -63,6 +64,7 @@ [pull3733]: https://github.com/evennia/evennia/pull/3853 [issue3858]: https://github.com/evennia/evennia/issues/3858 [issue3813]: https://github.com/evennia/evennia/issues/3513 +[issue3649]: https://github.com/evennia/evennia/issues/3649 ## Evennia 5.0.1 diff --git a/evennia/utils/tests/test_eveditor.py b/evennia/utils/tests/test_eveditor.py index 7fc50861c5..287fc30abe 100644 --- a/evennia/utils/tests/test_eveditor.py +++ b/evennia/utils/tests/test_eveditor.py @@ -338,6 +338,33 @@ class TestEvEditor(BaseEvenniaCommandTest): self.assertEqual(l3, " " * 37 + "l 3" + " " * 38) self.assertEqual(l4, "l" + " " * 76 + "4") + def test_eveditor_COLON_J_preserves_paragraph_breaks(self): + """ + Test to verify fix of issue #3649 (:j command broke input) + """ + eveditor.EvEditor(self.char1) + self.call( + eveditor.CmdEditorGroup(), + "", + raw_string=":", + msg="Line Editor []\n01\n[l:01 w:000 c:0000](:h for help)", + ) + text = ( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n\n" + "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + ) + self.char1.ndb._eveditor.update_buffer(text) + self.call( + eveditor.CmdEditorGroup(), + "=40", + raw_string=":j", + msg="Left-justified lines 1-3.", + ) + lines = self.char1.ndb._eveditor.get_buffer().split("\n") + blank_line_index = lines.index(" " * 40) + self.assertEqual(blank_line_index, 2) + self.assertTrue(lines[blank_line_index + 1].startswith("Sed do eiusmod")) + def test_eveditor_bad_commands(self): eveditor.EvEditor(self.char1) self.call( diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index 0c55c4ad67..98955cbf0d 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -811,6 +811,25 @@ class TestJustify(TestCase): self.assertIn(ANSI_RED, str(result)) + def test_justify_preserves_paragraph_breaks(self): + text = "Para one words here.\n\nPara two words there." + result = utils.justify(text, width=20, align="l") + self.assertEqual( + "Para one words here.\n \nPara two words \nthere. ", + result, + ) + + def test_justify_preserves_paragraph_breaks_with_ansi(self): + from evennia.utils.ansi import ANSI_RED + + text = ANSIString("Para one has |rred|n text.\n\nPara two.") + result = utils.justify(text, width=20, align="l") + clean_lines = result.clean().split("\n") + + self.assertIn(ANSI_RED, str(result)) + self.assertEqual(" " * 20, clean_lines[2]) + self.assertEqual("Para two. ", clean_lines[3]) + class TestAtSearchResult(TestCase): """ diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index b1561aa5a2..8a7daebffa 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -249,13 +249,13 @@ def justify(text, width=None, align="l", indent=0, fillchar=" "): is_ansi = isinstance(text, _ANSISTRING) lb = _ANSISTRING("\n") if is_ansi else "\n" - def _process_line(line): + def _process_line(line, line_word_length, line_gaps): """ helper function that distributes extra spaces between words. The number of gaps is nwords - 1 but must be at least 1 for single-word lines. We distribute odd spaces to one of the gaps. """ - line_rest = width - (wlen + ngaps) + line_rest = width - (line_word_length + line_gaps) gap = _ANSISTRING(" ") if is_ansi else " " @@ -277,8 +277,8 @@ def justify(text, width=None, align="l", indent=0, fillchar=" "): else: line[-1] = line[-1] + pad + sp * (line_rest % 2) else: # align 'f' - gap += sp * (line_rest // max(1, ngaps)) - rest_gap = line_rest % max(1, ngaps) + gap += sp * (line_rest // max(1, line_gaps)) + rest_gap = line_rest % max(1, line_gaps) for i in range(rest_gap): line[i] += sp elif not any(line): @@ -302,49 +302,61 @@ def justify(text, width=None, align="l", indent=0, fillchar=" "): # all other aligns requires splitting into paragraphs and words - # split into paragraphs and words - paragraphs = [text] # re.split("\n\s*?\n", text, re.MULTILINE) - words = [] - for ip, paragraph in enumerate(paragraphs): - if ip > 0: - words.append(("\n", 0)) - words.extend((word, m_len(word)) for word in paragraph.split()) + def _justify_paragraph(words): + ngaps = 0 + wlen = 0 + line = [] + lines = [] - if not words: - # Just whitespace! - return sp * width - - ngaps = 0 - wlen = 0 - line = [] - lines = [] - - while words: - if not line: - # start a new line - word = words.pop(0) - wlen = word[1] - line.append(word[0]) - elif (words[0][1] + wlen + ngaps) >= width: - # next word would exceed word length of line + smallest gaps - lines.append(_process_line(line)) - ngaps, wlen, line = 0, 0, [] - else: - # put a new word on the line - word = words.pop(0) - line.append(word[0]) - if word[1] == 0: - # a new paragraph, process immediately - lines.append(_process_line(line)) + while words: + if not line: + # start a new line + word = words.pop(0) + wlen = word[1] + line.append(word[0]) + elif (words[0][1] + wlen + ngaps) >= width: + # next word would exceed word length of line + smallest gaps + lines.append(_process_line(line, wlen, ngaps)) ngaps, wlen, line = 0, 0, [] else: + # put a new word on the line + word = words.pop(0) + line.append(word[0]) wlen += word[1] ngaps += 1 - if line: # catch any line left behind - lines.append(_process_line(line)) + if line: # catch any line left behind + lines.append(_process_line(line, wlen, ngaps)) + + return lines + + paragraphs = [] + paragraph_words = [] + for input_line in text.split("\n"): + line_words = [(word, m_len(word)) for word in input_line.split()] + if line_words: + paragraph_words.extend(line_words) + else: + if paragraph_words: + paragraphs.append(paragraph_words) + paragraph_words = [] + paragraphs.append(None) + if paragraph_words: + paragraphs.append(paragraph_words) + + if not paragraphs: + # Just whitespace! + return sp * width + + blank_line = _ANSISTRING(sp * width) if is_ansi else sp * width + lines = [] + for paragraph in paragraphs: + if paragraph is None: + lines.append(blank_line) + else: + lines.extend(_justify_paragraph(paragraph[:])) + indentstring = sp * indent - out = lb.join([indentstring + line for line in lines]) return lb.join([indentstring + line for line in lines])