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)
|
- Made all id fields BigAutoField for all databases. (owllex)
|
||||||
- `EvForm` refactored. New `literals` mapping, for literal mappings into the
|
- `EvForm` refactored. New `literals` mapping, for literal mappings into the
|
||||||
main template (e.g. for single-character replacements).
|
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
|
## Evennia 0.9.5
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@ import re
|
||||||
from copy import copy
|
from copy import copy
|
||||||
|
|
||||||
from evennia.utils.ansi import ANSIString
|
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.evtable import EvCell, EvTable
|
||||||
from evennia.utils.utils import all_from_module, is_iter, to_str
|
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"))
|
matrix = EvForm._to_ansi(self.literal_form.split("\n"))
|
||||||
|
|
||||||
maxl = max(len(line) for line in matrix)
|
maxl = max(len(line) for line in matrix)
|
||||||
matrix = [line + " " * (maxl - len(line)) for line in matrix]
|
matrix = [line + " " * (maxl - len(line)) for line in matrix]
|
||||||
if matrix and not matrix[0].strip():
|
if matrix and not matrix[0].strip():
|
||||||
|
|
@ -348,9 +350,8 @@ class EvForm:
|
||||||
return obj
|
return obj
|
||||||
elif isinstance(obj, str):
|
elif isinstance(obj, str):
|
||||||
# since ansi will be parsed twice (here and in the normal ansi send), we have to
|
# 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
|
# escape ansi twice.
|
||||||
# which is not ideal for those wanting to replace/extend it ...
|
obj = ansi_raw(obj)
|
||||||
obj = _ANSI_ESCAPE.sub(r"||||", obj)
|
|
||||||
|
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
return dict(
|
return dict(
|
||||||
|
|
@ -370,7 +371,7 @@ class EvForm:
|
||||||
"""
|
"""
|
||||||
formchar = self.data["formchar"]
|
formchar = self.data["formchar"]
|
||||||
tablechar = self.data["tablechar"]
|
tablechar = self.data["tablechar"]
|
||||||
form = self.matrix
|
matrix = self.matrix
|
||||||
|
|
||||||
cell_options = copy(self.cell_options)
|
cell_options = copy(self.cell_options)
|
||||||
cell_options.update(self.options)
|
cell_options.update(self.options)
|
||||||
|
|
@ -378,7 +379,7 @@ class EvForm:
|
||||||
table_options = copy(self.table_options)
|
table_options = copy(self.table_options)
|
||||||
table_options.update(self.options)
|
table_options.update(self.options)
|
||||||
|
|
||||||
nform = len(form)
|
nmatrix = len(matrix)
|
||||||
|
|
||||||
mapping = {}
|
mapping = {}
|
||||||
|
|
||||||
|
|
@ -389,7 +390,7 @@ class EvForm:
|
||||||
regex = re.compile(rf"{char}+([^{INVALID_FORMCHARS}{char}]+){char}+")
|
regex = re.compile(rf"{char}+([^{INVALID_FORMCHARS}{char}]+){char}+")
|
||||||
|
|
||||||
# find the start/width of rectangles for each line
|
# 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
|
ix0 = 0
|
||||||
while True:
|
while True:
|
||||||
match = regex.search(line, ix0)
|
match = regex.search(line, ix0)
|
||||||
|
|
@ -405,15 +406,15 @@ class EvForm:
|
||||||
dy_up = 0
|
dy_up = 0
|
||||||
if iy > 0:
|
if iy > 0:
|
||||||
for i in range(1, iy):
|
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
|
dy_up += 1
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
# find bottom edge of rectangle
|
# find bottom edge of rectangle
|
||||||
dy_down = 0
|
dy_down = 0
|
||||||
if iy < nform - 1:
|
if iy < nmatrix - 1:
|
||||||
for i in range(1, nform - iy - 1):
|
for i in range(1, nmatrix - iy - 1):
|
||||||
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_down += 1
|
dy_down += 1
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
@ -434,8 +435,22 @@ class EvForm:
|
||||||
|
|
||||||
# get data to populate cell
|
# get data to populate cell
|
||||||
data = self.cells_mapping.get(key, "")
|
data = self.cells_mapping.get(key, "")
|
||||||
# generate Cell on the fly
|
if isinstance(data, EvCell):
|
||||||
cell = EvCell(data, width=width, height=height, **cell_options)
|
# 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)
|
mapping[key] = (y, x, width, height, cell)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ from textwrap import TextWrapper
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from evennia.utils.ansi import ANSIString
|
from evennia.utils.ansi import ANSIString
|
||||||
from evennia.utils.utils import display_len as d_len
|
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
|
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
||||||
|
|
||||||
|
|
@ -601,29 +601,8 @@ class EvCell:
|
||||||
align = self.align
|
align = self.align
|
||||||
hfill_char = self.hfill_char
|
hfill_char = self.hfill_char
|
||||||
width = self.width
|
width = self.width
|
||||||
if align == "l":
|
|
||||||
lines = [
|
return [justify(line, width, align=align, fillchar=hfill_char) for line in data]
|
||||||
(
|
|
||||||
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]
|
|
||||||
|
|
||||||
def _valign(self, data):
|
def _valign(self, data):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
Unit tests for the EvForm text form generator
|
Unit tests for the EvForm text form generator
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from unittest import skip
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from evennia.utils import ansi, evform, evtable
|
from evennia.utils import ansi, evform, evtable
|
||||||
|
|
||||||
|
|
@ -261,3 +263,137 @@ class TestEvFormParallelTables(TestCase):
|
||||||
tables={"2": self.table2, "3": self.table3},
|
tables={"2": self.table2, "3": self.table3},
|
||||||
)
|
)
|
||||||
self.assertEqual(ansi.strip_ansi(str(form).strip()), _EXPECTED.strip())
|
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
|
Fully justify a text so that it fits inside `width`. When using
|
||||||
full justification (default) this will be done by padding between
|
full justification (default) this will be done by padding between
|
||||||
|
|
@ -224,16 +224,17 @@ def justify(text, width=None, align="f", indent=0):
|
||||||
Args:
|
Args:
|
||||||
text (str): Text to justify.
|
text (str): Text to justify.
|
||||||
width (int, optional): The length of each line, in characters.
|
width (int, optional): The length of each line, in characters.
|
||||||
align (str, optional): The alignment, 'l', 'c', 'r' or 'f'
|
align (str, optional): The alignment, 'l', 'c', 'r', 'f' or 'a'
|
||||||
for left, center, right or full justification respectively.
|
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
|
indent (int, optional): Number of characters indentation of
|
||||||
entire justified text block.
|
entire justified text block.
|
||||||
|
fillchar (str): The character to use to fill. Defaults to empty space.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
justified (str): The justified and indented block of text.
|
justified (str): The justified and indented block of text.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
width = width if width else settings.CLIENT_DEFAULT_WIDTH
|
|
||||||
|
|
||||||
def _process_line(line):
|
def _process_line(line):
|
||||||
"""
|
"""
|
||||||
|
|
@ -246,29 +247,46 @@ def justify(text, width=None, align="f", indent=0):
|
||||||
if line_rest > 0:
|
if line_rest > 0:
|
||||||
if align == "l":
|
if align == "l":
|
||||||
if line[-1] == "\n\n":
|
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:
|
else:
|
||||||
line[-1] += " " * line_rest
|
line[-1] += sp * line_rest
|
||||||
elif align == "r":
|
elif align == "r":
|
||||||
line[0] = " " * line_rest + line[0]
|
line[0] = sp * line_rest + line[0]
|
||||||
elif align == "c":
|
elif align == "c":
|
||||||
pad = " " * (line_rest // 2)
|
pad = sp * (line_rest // 2)
|
||||||
line[0] = pad + line[0]
|
line[0] = pad + line[0]
|
||||||
if line[-1] == "\n\n":
|
if line[-1] == "\n\n":
|
||||||
line[-1] += (
|
line[-1] += (
|
||||||
pad + " " * (line_rest % 2 - 1) + "\n" + " " * width + "\n" + " " * width
|
pad + sp * (line_rest % 2 - 1) + "\n" + sp * width + "\n" + sp * width
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
line[-1] = line[-1] + pad + " " * (line_rest % 2)
|
line[-1] = line[-1] + pad + sp * (line_rest % 2)
|
||||||
else: # align 'f'
|
else: # align 'f'
|
||||||
gap += " " * (line_rest // max(1, ngaps))
|
gap += sp * (line_rest // max(1, ngaps))
|
||||||
rest_gap = line_rest % max(1, ngaps)
|
rest_gap = line_rest % max(1, ngaps)
|
||||||
for i in range(rest_gap):
|
for i in range(rest_gap):
|
||||||
line[i] += " "
|
line[i] += sp
|
||||||
elif not any(line):
|
elif not any(line):
|
||||||
return [" " * width]
|
return [sp * width]
|
||||||
return gap.join(line)
|
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
|
# split into paragraphs and words
|
||||||
paragraphs = re.split("\n\s*?\n", text, re.MULTILINE)
|
paragraphs = re.split("\n\s*?\n", text, re.MULTILINE)
|
||||||
words = []
|
words = []
|
||||||
|
|
@ -303,7 +321,7 @@ def justify(text, width=None, align="f", indent=0):
|
||||||
|
|
||||||
if line: # catch any line left behind
|
if line: # catch any line left behind
|
||||||
lines.append(_process_line(line))
|
lines.append(_process_line(line))
|
||||||
indentstring = " " * indent
|
indentstring = sp * indent
|
||||||
return "\n".join([indentstring + line for line in lines])
|
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.
|
# result is a typeclassed entity where `.aliases` is an AliasHandler.
|
||||||
aliases = result.aliases.all(return_objs=True)
|
aliases = result.aliases.all(return_objs=True)
|
||||||
# remove pluralization aliases
|
# 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:
|
else:
|
||||||
# result is likely a Command, where `.aliases` is a list of strings.
|
# result is likely a Command, where `.aliases` is a list of strings.
|
||||||
aliases = result.aliases
|
aliases = result.aliases
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue