Skip to content
This repository has been archived by the owner on Dec 7, 2021. It is now read-only.

Commit

Permalink
More support for RTL chars. Vertical align/pad.
Browse files Browse the repository at this point in the history
Renamed string_width() to visible_width(). Better describes the purpose
of the function.

Added justification() which detects RTL characters.

Refactored align_and_pad_cell(). Combined padding and increasing
width/height. Using ljust/rjust to do half the padding. Started adding
code to support height padding and alignment. No longer repeatedly
joining and line splitting.

For:
#22
#23
  • Loading branch information
Robpol86 committed May 15, 2016
1 parent 967b0e7 commit 02d57ba
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 85 deletions.
5 changes: 5 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ This project adheres to `Semantic Versioning <http://semver.org/>`_.
Unreleased
----------

Added
* Support for https://pypi.python.org/pypi/colorama
* Support for https://pypi.python.org/pypi/termcolor
* Support for RTL characters (Arabic and Hebrew).

Changed
* Refactored `terminaltables.terminal_io`. Fixed set_terminal_title() Unicode handling on Windows.

Expand Down
12 changes: 7 additions & 5 deletions terminaltables/base_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import re

from terminaltables.terminal_io import terminal_size
from terminaltables.width_and_alignment import align_and_pad_cell, column_widths, string_width
from terminaltables.width_and_alignment import align_and_pad_cell, column_widths, visible_width


def join_row(row, left, middle, right):
Expand Down Expand Up @@ -114,8 +114,10 @@ def padded_table_data(self):
for row in new_table_data:
height = max([c.count('\n') for c in row] or [0]) + 1
for i in range(len(row)):
align = self.justify_columns.get(i, 'left')
cell = align_and_pad_cell(row[i], align, (widths[i], height, self.padding_left, self.padding_right))
align = (self.justify_columns.get(i, 'left'),)
dimensions = (widths[i], height)
padding = (self.padding_left, self.padding_right, 0, 0)
cell = align_and_pad_cell(row[i], align, dimensions, padding)
row[i] = cell

return new_table_data
Expand All @@ -129,7 +131,7 @@ def table(self):

# Append top border.
max_title = sum(widths) + ((len(widths) - 1) if self.inner_column_border else 0)
if self.outer_border and self.title and string_width(self.title) <= max_title:
if self.outer_border and self.title and visible_width(self.title) <= max_title:
pseudo_row = join_row(
['h' * w for w in widths],
'l', 't' if self.inner_column_border else '',
Expand All @@ -139,7 +141,7 @@ def table(self):
r=self.CHAR_CORNER_UPPER_RIGHT)
pseudo_row_re = re.compile('({0})'.format('|'.join(pseudo_row_key.keys())))
substitute = lambda s: pseudo_row_re.sub(lambda x: pseudo_row_key[x.string[x.start():x.end()]], s)
row = substitute(pseudo_row[:1]) + self.title + substitute(pseudo_row[1 + string_width(self.title):])
row = substitute(pseudo_row[:1]) + self.title + substitute(pseudo_row[1 + visible_width(self.title):])
final_table_data.append(row)
elif self.outer_border:
row = join_row(
Expand Down
81 changes: 57 additions & 24 deletions terminaltables/width_and_alignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
RE_COLOR_ANSI = re.compile(r'(\033\[([\d;]+)m)')


def string_width(string):
def visible_width(string):
"""Get the visible width of a unicode string.
Some CJK unicode characters are more than one byte unlike ASCII and latin unicode characters.
Expand Down Expand Up @@ -37,41 +37,74 @@ def string_width(string):
return width


def align_and_pad_cell(string, align, size_dims):
"""Align a string with center/rjust/ljust and adds additional padding.
def justification(string, align):
"""Either returns left/center/right if either are in `align`, else determines the default justification.
Default depends if any RTL (right to left) characters are present in the string (e.g. arabic/hebrew).
:param str string: String to analyze.
:param tuple align: Requested alignment from align_and_pad_cell() args.
:return: One of: left, center, right
:rtype: str
"""
if 'right' in align:
return 'right'
if 'center' in align:
return 'center'
if 'left' in align:
return 'left'

# Convert to unicode.
try:
decoded = string.decode('u8')
except (AttributeError, UnicodeEncodeError):
decoded = string

# Look for RTL char.
for char in decoded:
if unicodedata.bidirectional(char) in ('R', 'AL', 'RLE', 'RLO'):
return 'right'
return 'left'


def align_and_pad_cell(string, align, dimensions, padding, space=' '):
"""Align a string horizontally and vertically. Also add additional padding in both dimensions.
:param str string: Input string to operate on.
:param str align: 'left', 'right', or 'center'.
:param iter size_dims: Size and dimensions. A 4-item tuple of integers representing width, height, lpad, and rpad.
:param tuple align: Tuple that contains one of left/center/right and/or top/middle/bottom.
:param tuple dimensions: Width and height ints to expand string to without padding.
:param tuple padding: 4-int tuple. Number of space chars for left, right, top, and bottom.
:param str space: Character to use as white space for resizing/padding (use single visible chars only).
:return: Modified string.
:rtype: str
"""
width, height, lpad, rpad = size_dims

# Handle trailing newlines or empty strings, str.splitlines() does not satisfy.
lines = string.splitlines() or ['']
if string.endswith('\n'):
lines.append('')

# Align.
if align == 'center':
method = 'center'
elif align == 'right':
method = 'rjust'
# Vertically align and pad.
if 'bottom' in align:
lines = ([''] * (dimensions[1] - len(lines) + padding[2])) + lines + ([''] * padding[3])
elif 'middle' in align:
raise NotImplementedError('TODO') # lines = ([''] * 1) + lines + ([''] * 1)
else:
method = 'ljust'
aligned = '\n'.join(getattr(l, method)(width + len(l) - string_width(l)) for l in lines)

# Pad.
padded = '\n'.join((' ' * lpad) + l + (' ' * rpad) for l in aligned.splitlines() or [''])

# Increase height.
additional_padding = height - 1 - padded.count('\n')
if additional_padding > 0:
padded += ('\n{0}'.format(' ' * (width + lpad + rpad))) * additional_padding
lines = ([''] * padding[2]) + lines + ([''] * (dimensions[1] - len(lines) + padding[3]))

# Horizontally align and pad.
for i, line in enumerate(lines):
new_width = dimensions[0] + len(line) - visible_width(line)
justify = justification(line, align)
if justify == 'right':
lines[i] = line.rjust(padding[0] + new_width, space) + (space * padding[1])
elif justify == 'center':
lines[i] = (space * padding[0]) + line.center(new_width, space) + (space * padding[1])
else:
lines[i] = (space * padding[0]) + line.ljust(new_width + padding[1], space)

return padded
return '\n'.join(lines)


def column_widths(table_data):
Expand All @@ -92,6 +125,6 @@ def column_widths(table_data):
for i in range(len(row)):
if not row[i]:
continue
widths[i] = max(widths[i], string_width(max(row[i].splitlines(), key=len)))
widths[i] = max(widths[i], visible_width(max(row[i].splitlines(), key=len)))

return widths
108 changes: 54 additions & 54 deletions tests/test_width_and_alignment_align_and_pad_cell.py
Original file line number Diff line number Diff line change
@@ -1,82 +1,82 @@
# coding: utf-8
"""Tests for character length calculating."""
"""Test function in module."""

from terminaltables.width_and_alignment import align_and_pad_cell


def test_align():
"""Test alignment/justifications."""
assert align_and_pad_cell('test', '', (1, 1, 0, 0)) == 'test'
assert align_and_pad_cell('test', 'left', (1, 1, 0, 0)) == 'test'
assert align_and_pad_cell('', 'left', (4, 1, 0, 0)) == ' '
assert align_and_pad_cell('', 'left', (0, 1, 0, 0)) == ''
assert align_and_pad_cell('', 'left', (0, 1, 1, 1)) == ' '
assert align_and_pad_cell('', 'left', (1, 1, 1, 1)) == ' '
assert align_and_pad_cell('', 'left', (4, 1, 1, 1)) == ' '

assert align_and_pad_cell('test', 'left', (4, 1, 0, 0)) == 'test'
assert align_and_pad_cell('test', 'left', (5, 1, 0, 0)) == 'test '
assert align_and_pad_cell('test', 'left', (6, 1, 0, 0)) == 'test '
assert align_and_pad_cell('test', 'left', (7, 1, 0, 0)) == 'test '
assert align_and_pad_cell(' test', 'left', (7, 1, 0, 0)) == ' test '

assert align_and_pad_cell('test', 'right', (1, 1, 0, 0)) == 'test'
assert align_and_pad_cell('test', 'right', (4, 1, 0, 0)) == 'test'
assert align_and_pad_cell('test', 'right', (5, 1, 0, 0)) == ' test'
assert align_and_pad_cell('test', 'right', (6, 1, 0, 0)) == ' test'
assert align_and_pad_cell('test', 'right', (7, 1, 0, 0)) == ' test'

assert align_and_pad_cell('test', 'center', (1, 1, 0, 0)) == 'test'
assert align_and_pad_cell('test', 'center', (4, 1, 0, 0)) == 'test'
assert align_and_pad_cell('test', 'center', (5, 1, 0, 0)) == ' test'
assert align_and_pad_cell('test', 'center', (6, 1, 0, 0)) == ' test '
assert align_and_pad_cell('test', 'center', (7, 1, 0, 0)) == ' test '
assert align_and_pad_cell('test', ('',), (1, 1), (0, 0, 0, 0)) == 'test'
assert align_and_pad_cell('test', ('left',), (1, 1), (0, 0, 0, 0)) == 'test'
assert align_and_pad_cell('', ('left',), (4, 1), (0, 0, 0, 0)) == ' '
assert align_and_pad_cell('', ('left',), (0, 1), (0, 0, 0, 0)) == ''
assert align_and_pad_cell('', ('left',), (0, 1), (1, 1, 0, 0)) == ' '
assert align_and_pad_cell('', ('left',), (1, 1), (1, 1, 0, 0)) == ' '
assert align_and_pad_cell('', ('left',), (4, 1), (1, 1, 0, 0)) == ' '

assert align_and_pad_cell('test', ('left',), (4, 1), (0, 0, 0, 0)) == 'test'
assert align_and_pad_cell('test', ('left',), (5, 1), (0, 0, 0, 0)) == 'test '
assert align_and_pad_cell('test', ('left',), (6, 1), (0, 0, 0, 0)) == 'test '
assert align_and_pad_cell('test', ('left',), (7, 1), (0, 0, 0, 0)) == 'test '
assert align_and_pad_cell(' test', ('left',), (7, 1), (0, 0, 0, 0)) == ' test '

assert align_and_pad_cell('test', ('right',), (1, 1), (0, 0, 0, 0)) == 'test'
assert align_and_pad_cell('test', ('right',), (4, 1), (0, 0, 0, 0)) == 'test'
assert align_and_pad_cell('test', ('right',), (5, 1), (0, 0, 0, 0)) == ' test'
assert align_and_pad_cell('test', ('right',), (6, 1), (0, 0, 0, 0)) == ' test'
assert align_and_pad_cell('test', ('right',), (7, 1), (0, 0, 0, 0)) == ' test'

assert align_and_pad_cell('test', ('center',), (1, 1), (0, 0, 0, 0)) == 'test'
assert align_and_pad_cell('test', ('center',), (4, 1), (0, 0, 0, 0)) == 'test'
assert align_and_pad_cell('test', ('center',), (5, 1), (0, 0, 0, 0)) == ' test'
assert align_and_pad_cell('test', ('center',), (6, 1), (0, 0, 0, 0)) == ' test '
assert align_and_pad_cell('test', ('center',), (7, 1), (0, 0, 0, 0)) == ' test '


def test_padding():
"""Test padding."""
assert align_and_pad_cell('test', 'left', (4, 1, 1, 0)) == ' test'
assert align_and_pad_cell('test', 'left', (4, 1, 0, 1)) == 'test '
assert align_and_pad_cell('test', 'left', (4, 1, 1, 1)) == ' test '
assert align_and_pad_cell('test', ('left',), (4, 1), (1, 0, 0, 0)) == ' test'
assert align_and_pad_cell('test', ('left',), (4, 1), (0, 1, 0, 0)) == 'test '
assert align_and_pad_cell('test', ('left',), (4, 1), (1, 1, 0, 0)) == ' test '


def test_multi_line():
"""Test multi-line support."""
assert align_and_pad_cell('test\n', 'left', (8, 2, 1, 1)) == ' test \n '
assert align_and_pad_cell('\ntest', 'left', (8, 2, 1, 1)) == ' \n test '
assert align_and_pad_cell('\ntest\n', 'left', (8, 3, 1, 1)) == ' \n test \n '
assert align_and_pad_cell('test\n', ('left',), (8, 2), (1, 1, 0, 0)) == ' test \n '
assert align_and_pad_cell('\ntest', ('left',), (8, 2), (1, 1, 0, 0)) == ' \n test '
assert align_and_pad_cell('\ntest\n', ('left',), (8, 3), (1, 1, 0, 0)) == ' \n test \n '


def test_multi_line_align_padding():
"""Test alignment and padding on multi-line cells."""
assert align_and_pad_cell('test\ntest', 'left', (4, 2, 0, 0)) == 'test\ntest'
assert align_and_pad_cell('test\ntest', 'left', (5, 2, 0, 0)) == 'test \ntest '
assert align_and_pad_cell('test\ntest', 'left', (6, 2, 0, 0)) == 'test \ntest '
assert align_and_pad_cell('test\ntest', 'left', (7, 2, 0, 0)) == 'test \ntest '
assert align_and_pad_cell(' test\ntest', 'left', (7, 2, 0, 0)) == ' test \ntest '
assert align_and_pad_cell(' test\n test', 'left', (7, 2, 0, 0)) == ' test \n test '
assert align_and_pad_cell('test\ntest', ('left',), (4, 2), (0, 0, 0, 0)) == 'test\ntest'
assert align_and_pad_cell('test\ntest', ('left',), (5, 2), (0, 0, 0, 0)) == 'test \ntest '
assert align_and_pad_cell('test\ntest', ('left',), (6, 2), (0, 0, 0, 0)) == 'test \ntest '
assert align_and_pad_cell('test\ntest', ('left',), (7, 2), (0, 0, 0, 0)) == 'test \ntest '
assert align_and_pad_cell(' test\ntest', ('left',), (7, 2), (0, 0, 0, 0)) == ' test \ntest '
assert align_and_pad_cell(' test\n test', ('left',), (7, 2), (0, 0, 0, 0)) == ' test \n test '

assert align_and_pad_cell('test\ntest', 'right', (4, 2, 0, 0)) == 'test\ntest'
assert align_and_pad_cell('test\ntest', 'right', (5, 2, 0, 0)) == ' test\n test'
assert align_and_pad_cell('test\ntest', 'right', (6, 2, 0, 0)) == ' test\n test'
assert align_and_pad_cell('test\ntest', 'right', (7, 2, 0, 0)) == ' test\n test'
assert align_and_pad_cell('test\ntest', ('right',), (4, 2), (0, 0, 0, 0)) == 'test\ntest'
assert align_and_pad_cell('test\ntest', ('right',), (5, 2), (0, 0, 0, 0)) == ' test\n test'
assert align_and_pad_cell('test\ntest', ('right',), (6, 2), (0, 0, 0, 0)) == ' test\n test'
assert align_and_pad_cell('test\ntest', ('right',), (7, 2), (0, 0, 0, 0)) == ' test\n test'

assert align_and_pad_cell('test\ntest', 'center', (4, 2, 0, 0)) == 'test\ntest'
assert align_and_pad_cell('test\ntest', 'center', (5, 2, 0, 0)) == ' test\n test'
assert align_and_pad_cell('test\ntest', 'center', (6, 2, 0, 0)) == ' test \n test '
assert align_and_pad_cell('test\ntest', 'center', (7, 2, 0, 0)) == ' test \n test '
assert align_and_pad_cell('test\ntest', ('center',), (4, 2), (0, 0, 0, 0)) == 'test\ntest'
assert align_and_pad_cell('test\ntest', ('center',), (5, 2), (0, 0, 0, 0)) == ' test\n test'
assert align_and_pad_cell('test\ntest', ('center',), (6, 2), (0, 0, 0, 0)) == ' test \n test '
assert align_and_pad_cell('test\ntest', ('center',), (7, 2), (0, 0, 0, 0)) == ' test \n test '

assert align_and_pad_cell('test\ntest', 'left', (4, 2, 1, 0)) == ' test\n test'
assert align_and_pad_cell('test\ntest', 'left', (4, 2, 0, 1)) == 'test \ntest '
assert align_and_pad_cell('test\ntest', 'left', (4, 2, 1, 1)) == ' test \n test '
assert align_and_pad_cell('test\ntest', ('left',), (4, 2), (1, 0, 0, 0)) == ' test\n test'
assert align_and_pad_cell('test\ntest', ('left',), (4, 2), (0, 1, 0, 0)) == 'test \ntest '
assert align_and_pad_cell('test\ntest', ('left',), (4, 2), (1, 1, 0, 0)) == ' test \n test '


def test_height():
"""Test height of multi-line cells."""
assert align_and_pad_cell('test', 'left', (4, 2, 0, 0)) == 'test\n '
assert align_and_pad_cell('test', ('left',), (4, 2), (0, 0, 0, 0)) == 'test\n '

assert align_and_pad_cell('test\n', 'left', (4, 1, 0, 0)) == 'test\n '
assert align_and_pad_cell('test\n', 'left', (4, 2, 0, 0)) == 'test\n '
assert align_and_pad_cell('test\n', 'left', (4, 3, 0, 0)) == 'test\n \n '
assert align_and_pad_cell('test\n', ('left',), (4, 1), (0, 0, 0, 0)) == 'test\n '
assert align_and_pad_cell('test\n', ('left',), (4, 2), (0, 0, 0, 0)) == 'test\n '
assert align_and_pad_cell('test\n', ('left',), (4, 3), (0, 0, 0, 0)) == 'test\n \n '

assert align_and_pad_cell('test\n', 'left', (4, 3, 1, 1)) == ' test \n \n '
assert align_and_pad_cell('test\n', ('left',), (4, 3), (1, 1, 0, 0)) == ' test \n \n '
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from colorclass import Color
from termcolor import colored

from terminaltables.width_and_alignment import string_width
from terminaltables.width_and_alignment import visible_width


@pytest.mark.parametrize('string,expected_length', [
Expand Down Expand Up @@ -56,4 +56,4 @@ def test(string, expected_length):
:param str string: Input string to measure.
:param int expected_length: Expected visible width of string (some characters are len() == 1 but take up 2 spaces).
"""
assert string_width(string) == expected_length
assert visible_width(string) == expected_length

0 comments on commit 02d57ba

Please sign in to comment.