mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Support 'a' (absolute) justification. Let EvForm accept EvCells as mappings. Resolve #2762
This commit is contained in:
parent
158b9e2e12
commit
9709ecbc57
5 changed files with 207 additions and 51 deletions
|
|
@ -213,6 +213,10 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
|
|||
- Made all id fields BigAutoField for all databases. (owllex)
|
||||
- `EvForm` refactored. New `literals` mapping, for literal mappings into the
|
||||
main template (e.g. for single-character replacements).
|
||||
- `EvForm` `cells` kwarg now accepts `EvCells` with custom formatting options
|
||||
(mainly for custom align/valign). `EvCells` now makes use of `utils.justify`.
|
||||
- `utils.justify` now supports `align="a"` (absolute alignments. This keeps
|
||||
the given left indent but crops/fills to the width. Used in EvCells.
|
||||
|
||||
## Evennia 0.9.5
|
||||
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ import re
|
|||
from copy import copy
|
||||
|
||||
from evennia.utils.ansi import ANSIString
|
||||
from evennia.utils.ansi import raw as ansi_raw
|
||||
from evennia.utils.evtable import EvCell, EvTable
|
||||
from evennia.utils.utils import all_from_module, is_iter, to_str
|
||||
|
||||
|
|
@ -333,6 +334,7 @@ class EvForm:
|
|||
|
||||
"""
|
||||
matrix = EvForm._to_ansi(self.literal_form.split("\n"))
|
||||
|
||||
maxl = max(len(line) for line in matrix)
|
||||
matrix = [line + " " * (maxl - len(line)) for line in matrix]
|
||||
if matrix and not matrix[0].strip():
|
||||
|
|
@ -348,9 +350,8 @@ class EvForm:
|
|||
return obj
|
||||
elif isinstance(obj, str):
|
||||
# since ansi will be parsed twice (here and in the normal ansi send), we have to
|
||||
# escape the |-structure twice. TODO: This is tied to the default color-tag syntax
|
||||
# which is not ideal for those wanting to replace/extend it ...
|
||||
obj = _ANSI_ESCAPE.sub(r"||||", obj)
|
||||
# escape ansi twice.
|
||||
obj = ansi_raw(obj)
|
||||
|
||||
if isinstance(obj, dict):
|
||||
return dict(
|
||||
|
|
@ -370,7 +371,7 @@ class EvForm:
|
|||
"""
|
||||
formchar = self.data["formchar"]
|
||||
tablechar = self.data["tablechar"]
|
||||
form = self.matrix
|
||||
matrix = self.matrix
|
||||
|
||||
cell_options = copy(self.cell_options)
|
||||
cell_options.update(self.options)
|
||||
|
|
@ -378,7 +379,7 @@ class EvForm:
|
|||
table_options = copy(self.table_options)
|
||||
table_options.update(self.options)
|
||||
|
||||
nform = len(form)
|
||||
nmatrix = len(matrix)
|
||||
|
||||
mapping = {}
|
||||
|
||||
|
|
@ -389,7 +390,7 @@ class EvForm:
|
|||
regex = re.compile(rf"{char}+([^{INVALID_FORMCHARS}{char}]+){char}+")
|
||||
|
||||
# find the start/width of rectangles for each line
|
||||
for iy, line in enumerate(EvForm._to_ansi(form, regexable=True)):
|
||||
for iy, line in enumerate(EvForm._to_ansi(matrix, regexable=True)):
|
||||
ix0 = 0
|
||||
while True:
|
||||
match = regex.search(line, ix0)
|
||||
|
|
@ -405,15 +406,15 @@ class EvForm:
|
|||
dy_up = 0
|
||||
if iy > 0:
|
||||
for i in range(1, iy):
|
||||
if all(form[iy - i][ix] == char for ix in range(leftix, rightix)):
|
||||
if all(matrix[iy - i][ix] == char for ix in range(leftix, rightix)):
|
||||
dy_up += 1
|
||||
else:
|
||||
break
|
||||
# find bottom edge of rectangle
|
||||
dy_down = 0
|
||||
if iy < nform - 1:
|
||||
for i in range(1, nform - iy - 1):
|
||||
if all(form[iy + i][ix] == char for ix in range(leftix, rightix)):
|
||||
if iy < nmatrix - 1:
|
||||
for i in range(1, nmatrix - iy - 1):
|
||||
if all(matrix[iy + i][ix] == char for ix in range(leftix, rightix)):
|
||||
dy_down += 1
|
||||
else:
|
||||
break
|
||||
|
|
@ -434,8 +435,22 @@ class EvForm:
|
|||
|
||||
# get data to populate cell
|
||||
data = self.cells_mapping.get(key, "")
|
||||
# generate Cell on the fly
|
||||
cell = EvCell(data, width=width, height=height, **cell_options)
|
||||
if isinstance(data, EvCell):
|
||||
# mapping already provides the cell. We need to override some
|
||||
# of the cell's options to make it work in the evform rectangle.
|
||||
# We retain the align/valign since this may be interesting to
|
||||
# play with within the rectangle.
|
||||
cell = data
|
||||
custom_align = cell.align
|
||||
custom_valign = cell.valign
|
||||
cell.reformat(
|
||||
width=width,
|
||||
height=height,
|
||||
**{**cell_options, **{"align": custom_align, "valign": custom_valign}},
|
||||
)
|
||||
else:
|
||||
# generating cell on the fly
|
||||
cell = EvCell(data, width=width, height=height, **cell_options)
|
||||
|
||||
mapping[key] = (y, x, width, height, cell)
|
||||
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ from textwrap import TextWrapper
|
|||
from django.conf import settings
|
||||
from evennia.utils.ansi import ANSIString
|
||||
from evennia.utils.utils import display_len as d_len
|
||||
from evennia.utils.utils import is_iter
|
||||
from evennia.utils.utils import is_iter, justify
|
||||
|
||||
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
||||
|
||||
|
|
@ -601,29 +601,8 @@ class EvCell:
|
|||
align = self.align
|
||||
hfill_char = self.hfill_char
|
||||
width = self.width
|
||||
if align == "l":
|
||||
lines = [
|
||||
(
|
||||
line.lstrip(" ") + " "
|
||||
if line.startswith(" ") and not line.startswith(" ")
|
||||
else line
|
||||
)
|
||||
+ hfill_char * (width - d_len(line))
|
||||
for line in data
|
||||
]
|
||||
return lines
|
||||
elif align == "r":
|
||||
return [
|
||||
hfill_char * (width - d_len(line))
|
||||
+ (
|
||||
" " + line.rstrip(" ")
|
||||
if line.endswith(" ") and not line.endswith(" ")
|
||||
else line
|
||||
)
|
||||
for line in data
|
||||
]
|
||||
else: # center, 'c'
|
||||
return [self._center(line, self.width, self.hfill_char) for line in data]
|
||||
|
||||
return [justify(line, width, align=align, fillchar=hfill_char) for line in data]
|
||||
|
||||
def _valign(self, data):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
Unit tests for the EvForm text form generator
|
||||
|
||||
"""
|
||||
from unittest import skip
|
||||
|
||||
from django.test import TestCase
|
||||
from evennia.utils import ansi, evform, evtable
|
||||
|
||||
|
|
@ -261,3 +263,137 @@ class TestEvFormParallelTables(TestCase):
|
|||
tables={"2": self.table2, "3": self.table3},
|
||||
)
|
||||
self.assertEqual(ansi.strip_ansi(str(form).strip()), _EXPECTED.strip())
|
||||
|
||||
|
||||
class TestEvFormErrors(TestCase):
|
||||
"""
|
||||
Tests of EvForm errors found for v1.0-dev
|
||||
|
||||
"""
|
||||
|
||||
maxDiff = None
|
||||
|
||||
def _form(self, form, **kwargs):
|
||||
|
||||
formdict = {
|
||||
"form": form,
|
||||
"formchar": "x",
|
||||
"tablechar": "c",
|
||||
}
|
||||
form = evform.EvForm(formdict, **kwargs)
|
||||
# this is necessary since editors/black tend to strip lines spaces
|
||||
# from the end of lines for the comparison strings.
|
||||
form = ansi.strip_ansi(str(form))
|
||||
form = "\n".join(line.rstrip() for line in form.split("\n"))
|
||||
|
||||
return form
|
||||
|
||||
def _validate(self, expected, result):
|
||||
"""easier debug"""
|
||||
err = f"\n{'expected':-^60}\n{expected}\n{'result':-^60}\n{result}\n{'':-^60}"
|
||||
self.assertEqual(expected.lstrip(), result.lstrip(), err)
|
||||
|
||||
@skip("Pending rebuild of markup")
|
||||
def test_2757(self):
|
||||
"""
|
||||
Testing https://github.com/evennia/evennia/issues/2757
|
||||
|
||||
Using || ansi escaping messes with rectangle width
|
||||
|
||||
This should be delayed until refactor of markup.
|
||||
|
||||
"""
|
||||
form = """
|
||||
xxxxxx
|
||||
||---| xx1xxx
|
||||
xxxxxx
|
||||
"""
|
||||
cell_mapping = {1: "Monty"}
|
||||
|
||||
expected = """
|
||||
|
||||
|---| Monty
|
||||
|
||||
"""
|
||||
self._validate(expected, self._form(form, cells=cell_mapping))
|
||||
|
||||
def test_2759(self):
|
||||
"""
|
||||
Testing https://github.com/evennia/evennia/issues/2759
|
||||
|
||||
Leading space in EvCell is stripped
|
||||
|
||||
"""
|
||||
# testing the underlying problem
|
||||
|
||||
cell = evtable.EvCell(" Hi", align="l")
|
||||
self.assertEqual(cell._align(cell.data), [ansi.ANSIString("Hi ")])
|
||||
|
||||
cell = evtable.EvCell(" Hi", align="l")
|
||||
self.assertEqual(cell._align(cell.data), [ansi.ANSIString("Hi ")])
|
||||
|
||||
cell = evtable.EvCell(" Hi", align="a")
|
||||
self.assertEqual(cell._align(cell.data), [ansi.ANSIString(" Hi")])
|
||||
|
||||
form = """
|
||||
.-----------------------.
|
||||
| Test Form |
|
||||
| |
|
||||
|.xxxxx .xxxxxxxxxxxxx |
|
||||
|.xx1xx .xxxxxx2xxxxxx |
|
||||
|.xxxxx .xxxxxxxxxxxxx |
|
||||
| .xxxxxxxxxxxxx |
|
||||
| |
|
||||
-----------------------
|
||||
"""
|
||||
|
||||
cell1 = " Hi."
|
||||
cell2 = " Single space\n Double\n Single again"
|
||||
|
||||
cell_mapping = {1: cell1, 2: cell2}
|
||||
|
||||
# default is left-aligned cells
|
||||
expected = """
|
||||
.-----------------------.
|
||||
| Test Form |
|
||||
| |
|
||||
|.Hi. .Single space |
|
||||
|. .Double |
|
||||
|. .Single again |
|
||||
| . |
|
||||
| |
|
||||
-----------------------
|
||||
"""
|
||||
self._validate(expected, self._form(form, cells=cell_mapping))
|
||||
|
||||
# test with absolute alignment (pass cells directly)
|
||||
cell_mapping = {
|
||||
1: evtable.EvCell(cell1, align="a", valign="t"),
|
||||
2: evtable.EvCell(cell2, align="a", valign="t"),
|
||||
}
|
||||
|
||||
expected = """
|
||||
.-----------------------.
|
||||
| Test Form |
|
||||
| |
|
||||
|. Hi. . Single space |
|
||||
|. . Double |
|
||||
|. . Single again |
|
||||
| . |
|
||||
| |
|
||||
-----------------------
|
||||
"""
|
||||
self._validate(expected, self._form(form, cells=cell_mapping))
|
||||
|
||||
def test_2763(self):
|
||||
"""
|
||||
Testing https://github.com/evennia/evennia/issues/2763
|
||||
|
||||
Duplication of ANSI sequences in evform
|
||||
"""
|
||||
|
||||
formdict = {"form": "|R A |n _ x1xx"}
|
||||
cell_mapping = {1: "test"}
|
||||
expected = f"{ansi.ANSI_RED} A {ansi.ANSI_NORMAL} _ test"
|
||||
form = evform.EvForm(formdict, cells=cell_mapping)
|
||||
self._validate(expected, str(form))
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ def dedent(text, baseline_index=None, indent=None):
|
|||
)
|
||||
|
||||
|
||||
def justify(text, width=None, align="f", indent=0):
|
||||
def justify(text, width=None, align="f", indent=0, fillchar=" "):
|
||||
"""
|
||||
Fully justify a text so that it fits inside `width`. When using
|
||||
full justification (default) this will be done by padding between
|
||||
|
|
@ -224,16 +224,17 @@ def justify(text, width=None, align="f", indent=0):
|
|||
Args:
|
||||
text (str): Text to justify.
|
||||
width (int, optional): The length of each line, in characters.
|
||||
align (str, optional): The alignment, 'l', 'c', 'r' or 'f'
|
||||
for left, center, right or full justification respectively.
|
||||
align (str, optional): The alignment, 'l', 'c', 'r', 'f' or 'a'
|
||||
for left, center, right, full justification. The 'a' stands for
|
||||
'absolute' and means the text will be returned unmodified.
|
||||
indent (int, optional): Number of characters indentation of
|
||||
entire justified text block.
|
||||
fillchar (str): The character to use to fill. Defaults to empty space.
|
||||
|
||||
Returns:
|
||||
justified (str): The justified and indented block of text.
|
||||
|
||||
"""
|
||||
width = width if width else settings.CLIENT_DEFAULT_WIDTH
|
||||
|
||||
def _process_line(line):
|
||||
"""
|
||||
|
|
@ -246,29 +247,46 @@ def justify(text, width=None, align="f", indent=0):
|
|||
if line_rest > 0:
|
||||
if align == "l":
|
||||
if line[-1] == "\n\n":
|
||||
line[-1] = " " * (line_rest - 1) + "\n" + " " * width + "\n" + " " * width
|
||||
line[-1] = sp * (line_rest - 1) + "\n" + sp * width + "\n" + sp * width
|
||||
else:
|
||||
line[-1] += " " * line_rest
|
||||
line[-1] += sp * line_rest
|
||||
elif align == "r":
|
||||
line[0] = " " * line_rest + line[0]
|
||||
line[0] = sp * line_rest + line[0]
|
||||
elif align == "c":
|
||||
pad = " " * (line_rest // 2)
|
||||
pad = sp * (line_rest // 2)
|
||||
line[0] = pad + line[0]
|
||||
if line[-1] == "\n\n":
|
||||
line[-1] += (
|
||||
pad + " " * (line_rest % 2 - 1) + "\n" + " " * width + "\n" + " " * width
|
||||
pad + sp * (line_rest % 2 - 1) + "\n" + sp * width + "\n" + sp * width
|
||||
)
|
||||
else:
|
||||
line[-1] = line[-1] + pad + " " * (line_rest % 2)
|
||||
line[-1] = line[-1] + pad + sp * (line_rest % 2)
|
||||
else: # align 'f'
|
||||
gap += " " * (line_rest // max(1, ngaps))
|
||||
gap += sp * (line_rest // max(1, ngaps))
|
||||
rest_gap = line_rest % max(1, ngaps)
|
||||
for i in range(rest_gap):
|
||||
line[i] += " "
|
||||
line[i] += sp
|
||||
elif not any(line):
|
||||
return [" " * width]
|
||||
return [sp * width]
|
||||
return gap.join(line)
|
||||
|
||||
width = width if width else settings.CLIENT_DEFAULT_WIDTH
|
||||
sp = fillchar
|
||||
|
||||
if align == "a":
|
||||
# absolute mode - just crop or fill to width
|
||||
abs_lines = []
|
||||
for line in text.split("\n"):
|
||||
nlen = len(line)
|
||||
if len(line) < width:
|
||||
line += sp * (width - nlen)
|
||||
else:
|
||||
line = crop(line, width=width, suffix="")
|
||||
abs_lines.append(line)
|
||||
return "\n".join(abs_lines)
|
||||
|
||||
# all other aligns requires splitting into paragraphs and words
|
||||
|
||||
# split into paragraphs and words
|
||||
paragraphs = re.split("\n\s*?\n", text, re.MULTILINE)
|
||||
words = []
|
||||
|
|
@ -303,7 +321,7 @@ def justify(text, width=None, align="f", indent=0):
|
|||
|
||||
if line: # catch any line left behind
|
||||
lines.append(_process_line(line))
|
||||
indentstring = " " * indent
|
||||
indentstring = sp * indent
|
||||
return "\n".join([indentstring + line for line in lines])
|
||||
|
||||
|
||||
|
|
@ -2293,7 +2311,11 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs):
|
|||
# result is a typeclassed entity where `.aliases` is an AliasHandler.
|
||||
aliases = result.aliases.all(return_objs=True)
|
||||
# remove pluralization aliases
|
||||
aliases = [alias for alias in aliases if hasattr(alias, "category") and alias.category not in ("plural_key",)]
|
||||
aliases = [
|
||||
alias
|
||||
for alias in aliases
|
||||
if hasattr(alias, "category") and alias.category not in ("plural_key",)
|
||||
]
|
||||
else:
|
||||
# result is likely a Command, where `.aliases` is a list of strings.
|
||||
aliases = result.aliases
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue