diff --git a/examples/ansi.mojo b/examples/ansi.mojo index b02cbd5..a09404e 100644 --- a/examples/ansi.mojo +++ b/examples/ansi.mojo @@ -4,12 +4,11 @@ import mog fn main(): var s = mog.Style().foreground(mog.Color(240)) - var table = mog.new_table() + var table = mog.Table.new() table.width = 50 table = ( table.row("Bubble Tea", s.render("Milky")) .row("Milk Tea", s.render("Also milky")) .row("Actual milk", s.render("Milky as well")) ) - print(repr(table.render())) - print(table.render()) + print(table) diff --git a/examples/pokemon.mojo b/examples/pokemon.mojo index 200dd6d..75b6955 100644 --- a/examples/pokemon.mojo +++ b/examples/pokemon.mojo @@ -114,4 +114,4 @@ fn main(): style_function=style_func, ).rows(data) - print(table.render()) + print(table) diff --git a/src/mog/__init__.mojo b/src/mog/__init__.mojo index 9f87849..f46095f 100644 --- a/src/mog/__init__.mojo +++ b/src/mog/__init__.mojo @@ -14,7 +14,7 @@ from .border import ( HIDDEN_BORDER, NO_BORDER, ) -from .table import Table, default_styles, StringData, new_table, Data, Filter +from .table import Table, default_styles, StringData, Data, Filter from .size import get_height, get_width, get_dimensions from .color import ( NoColor, diff --git a/src/mog/join.mojo b/src/mog/join.mojo index 9af9cad..6a0e216 100644 --- a/src/mog/join.mojo +++ b/src/mog/join.mojo @@ -105,7 +105,7 @@ fn join_horizontal(pos: Position, strs: List[String]) -> String: If you just want to align to the left, right or center you may as well just use the helper constants Top, Center, and Bottom. - Examples: + #### Examples: ```mojo import mog @@ -152,7 +152,7 @@ fn join_horizontal(pos: Position, strs: List[String]) -> String: if len(blocks[i]) >= max_height: continue - var extra_lines = List[String]() + var extra_lines = List[String](capacity=max_height - len(blocks[i])) extra_lines.resize(max_height - len(blocks[i]), "") if pos == top: @@ -176,11 +176,10 @@ fn join_horizontal(pos: Position, strs: List[String]) -> String: # remember, all blocks have the same number of members now for i in range(len(blocks[0])): for j in range(len(blocks)): - var block = blocks[j] - result.write(block[i]) + result.write(blocks[j][i]) # Also make lines the same length by padding with whitespace - var spaces = WHITESPACE * (max_widths[j] - printable_rune_width(block[i])) + var spaces = WHITESPACE * (max_widths[j] - printable_rune_width(blocks[j][i])) result.write(spaces) if i < len(blocks[0]) - 1: diff --git a/src/mog/style.mojo b/src/mog/style.mojo index 9b119fa..99ab0aa 100644 --- a/src/mog/style.mojo +++ b/src/mog/style.mojo @@ -1147,7 +1147,7 @@ struct Style: A new Style with the border rule unset. """ var new = self - new._unset_attribute(PropKey.TAB_WIDTH) + new._unset_attribute(PropKey.BORDER_RIGHT) return new fn border_foreground(self, *colors: AnyTerminalColor) -> Style: @@ -1447,6 +1447,8 @@ struct Style: A new Style with the padding rule set. #### Notes: + Padding is applied inside the text area, inside of the border if there is one. + Margin is applied outside the text area, outside of the border if there is one. * With one argument, the value is applied to all sides. * With two arguments, the value is applied to the vertical and horizontal sides, in that order. @@ -1499,6 +1501,9 @@ struct Style: Returns: A new Style with the padding top rule set. + + #### Notes: + Padding is applied inside the text area, inside of the border if there is one. """ var new = self new._set_attribute(PropKey.PADDING_TOP, width) @@ -1522,6 +1527,9 @@ struct Style: Returns: A new Style with the padding right rule set. + + #### Notes: + Padding is applied inside the text area, inside of the border if there is one. """ var new = self new._set_attribute(PropKey.PADDING_RIGHT, width) @@ -1545,6 +1553,9 @@ struct Style: Returns: A new Style with the padding bottom rule set. + + #### Notes: + Padding is applied inside the text area, inside of the border if there is one. """ var new = self new._set_attribute(PropKey.PADDING_BOTTOM, width) @@ -1568,6 +1579,9 @@ struct Style: Returns: A new Style with the padding left rule set. + + #### Notes: + Padding is applied inside the text area, inside of the border if there is one. """ var new = self new._set_attribute(PropKey.PADDING_LEFT, width) @@ -1584,26 +1598,25 @@ struct Style: return new fn margin(self, *widths: Int) -> Style: - """Shorthand method for setting padding on all sides at once. - - With one argument, the value is applied to all sides. - - With two arguments, the value is applied to the vertical and horizontal - sides, in that order. - - With three arguments, the value is applied to the top side, the horizontal - sides, and the bottom side, in that order. - - With four arguments, the value is applied clockwise starting from the top - side, followed by the right side, then the bottom, and finally the left. - - With more than four arguments no padding will be added. + """Shorthand method for setting margin on all sides at once. Args: widths: The padding widths to apply. Returns: A new Style with the margin rule set. + + #### Notes: + Padding is applied inside the text area, inside of the border if there is one. + Margin is applied outside the text area, outside of the border if there is one. + * With one argument, the value is applied to all sides. + * With two arguments, the value is applied to the vertical and horizontal + sides, in that order. + * With three arguments, the value is applied to the top side, the horizontal + sides, and the bottom side, in that order. + * With four arguments, the value is applied clockwise starting from the top + side, followed by the right side, then the bottom, and finally the left. + * With more than four arguments no margin will be added. """ var top = 0 var bottom = 0 @@ -1648,6 +1661,9 @@ struct Style: Returns: A new Style with the margin top rule set. + + #### Notes: + Margin is applied uotside the text area, outside of the border if there is one. """ var new = self new._set_attribute(PropKey.MARGIN_TOP, width) @@ -1671,6 +1687,9 @@ struct Style: Returns: A new Style with the margin right rule set. + + #### Notes: + Margin is applied uotside the text area, outside of the border if there is one. """ var new = self new._set_attribute(PropKey.MARGIN_RIGHT, width) @@ -1694,6 +1713,9 @@ struct Style: Returns: A new Style with the margin bottom rule set. + + #### Notes: + Margin is applied uotside the text area, outside of the border if there is one. """ var new = self new._set_attribute(PropKey.MARGIN_BOTTOM, width) @@ -1717,6 +1739,9 @@ struct Style: Returns: A new Style with the margin left rule set. + + #### Notes: + Margin is applied uotside the text area, outside of the border if there is one. """ var new = self new._set_attribute(PropKey.MARGIN_LEFT, width) diff --git a/src/mog/table/__init__.mojo b/src/mog/table/__init__.mojo index dd38e1e..3c3fdef 100644 --- a/src/mog/table/__init__.mojo +++ b/src/mog/table/__init__.mojo @@ -1,2 +1,2 @@ -from .table import Table, default_styles, StyleFunction, new_table +from .table import Table, default_styles, StyleFunction from .rows import StringData, Data, Filter diff --git a/src/mog/table/rows.mojo b/src/mog/table/rows.mojo index c4f8e4e..a867eb2 100644 --- a/src/mog/table/rows.mojo +++ b/src/mog/table/rows.mojo @@ -58,15 +58,28 @@ struct StringData(Data): var _columns: Int """The number of columns in the table.""" - fn __init__(out self, rows: List[List[String]] = List[List[String]](), columns: Int = 0): + fn __init__(out self, rows: List[List[String]] = List[List[String]]()): """Initializes a new StringData instance. Args: rows: The rows of the table. - columns: The number of columns in the table. """ self._rows = rows - self._columns = columns + self._columns = len(rows) + + fn __init__(out self, *rows: List[String]): + """Initializes a new StringData instance. + + Args: + rows: The rows of the table. + """ + var widest = 0 + var r = List[List[String]](capacity=len(rows)) + for row in rows: + widest = max(widest, len(row[])) + r.append(row[]) + self._rows = r + self._columns = widest # TODO: Can't return ref String because it depends on the origin of a struct attribute # and Traits do not support variables yet. @@ -106,19 +119,38 @@ struct StringData(Data): """ self._columns = max(self._columns, len(row)) self._rows.append(row) - - fn item(mut self, rows: List[String]) -> Self: + + fn append(mut self, *elements: String): """Appends the given row to the table. Args: - rows: The row to append. + elements: The row to append. + """ + self._columns = max(self._columns, len(elements)) + var row = List[String](capacity=len(elements)) + for element in elements: + row.append(element[]) + self._rows.append(row) + + fn __add__(self, other: Self) -> Self: + """Concatenates two StringData instances. + + Args: + other: The other StringData instance to concatenate. Returns: - The updated table. + The concatenated StringData instance. """ - self._columns = max(self._columns, len(rows)) - self._rows.append(rows) - return self + return StringData(self._rows + other._rows, max(self.columns(), other.columns())) + + fn __iadd__(mut self, other: Self): + """Concatenates two StringData instances in place. + + Args: + other: The other StringData instance to concatenate. + """ + self._rows.extend(other._rows) + self._columns = max(self.columns(), other.columns()) alias FilterFunction = fn (row: Int) -> Bool @@ -126,7 +158,7 @@ alias FilterFunction = fn (row: Int) -> Bool @value -struct Filter[DataType: Data](Data): +struct Filter[DataType: Data, //](Data): """Applies a filter function on some data. Parameters: @@ -135,20 +167,9 @@ struct Filter[DataType: Data](Data): var data: DataType """The data of the table.""" - var filter_function: FilterFunction + var filter: FilterFunction """The filter function to apply.""" - fn filter(self, data: Int) -> Bool: - """Applies the given filter function to the data. - - Args: - data: The data to filter. - - Returns: - The filtered data. - """ - return self.filter_function(data) - fn __getitem__(self, row: Int, column: Int) -> String: """Returns the contents of the cell at the given index. diff --git a/src/mog/table/table.mojo b/src/mog/table/table.mojo index e3815c5..f226038 100644 --- a/src/mog/table/table.mojo +++ b/src/mog/table/table.mojo @@ -9,14 +9,12 @@ from .util import median, largest, sum alias StyleFunction = fn (row: Int, col: Int) -> Style -""" -Styling function that determines the style of a Cell. +"""Styling function that determines the style of a Cell. It takes the row and column of the cell as an input and determines the lipgloss Style to use for that cell position. -Examples: - +#### Examples: ```mojo import mog @@ -33,12 +31,12 @@ fn main(): else: return odd_row_style - var t = mog.new_table(). + var t = mog.Table.new(). set_headers("Name", "Age"). row("Kini", "4"). row("Eli", "1"). row("Iris", "102"). - style_function(styler) + _styler(styler) print(t) ``` @@ -61,11 +59,11 @@ fn default_styles(row: Int, col: Int) -> Style: # TODO: Parametrize on data field, so other structs that implement `Data` can be used. For now it only support `StringData`. @value -struct Table: +struct Table(Writable, Stringable, CollectionElement): """Used to model and render tabular data as a table. - Examples: - ``` + #### Examples: + ```mojo import mog fn main(): @@ -81,55 +79,56 @@ struct Table: else: return odd_row_style - var t = mog.new_table(). + var t = mog.Table.new(). set_headers("Name", "Age"). row("Kini", "4"). row("Eli", "1"). row("Iris", "102"). - style_function(styler) + set_style(styler) print(t) ``` . """ - var style_function: StyleFunction + var _styler: StyleFunction """The style function that determines the style of a cell. It returns a `mog.Style` for a given row and column position.""" - var border: Border + var _border: Border """The border style to use for the table.""" - var border_top: Bool + var _border_top: Bool """Whether to render the top border of the table.""" - var border_bottom: Bool + var _border_bottom: Bool """Whether to render the bottom border of the table.""" - var border_left: Bool + var _border_left: Bool """Whether to render the left border of the table.""" - var border_right: Bool + var _border_right: Bool """Whether to render the right border of the table.""" - var border_header: Bool + var _border_header: Bool """Whether to render the header border of the table.""" - var border_column: Bool + var _border_column: Bool """Whether to render the column border of the table.""" - var border_row: Bool + var _border_row: Bool """Whether to render the row divider borders for each row of the table.""" - var border_style: Style + var _border_style: Style """The style to use for the border.""" - var headers: List[String] + var _headers: List[String] """The headers of the table.""" - var data: StringData + var _data: StringData """The data of the table.""" var width: Int """The width of the table.""" var height: Int """The height of the table.""" - var offset: Int + var _offset: Int """The offset of the table.""" - var widths: List[Int] - """Tracks the width of each column.""" - var heights: List[Int] - """Tracks the height of each row.""" + # var widths: List[Int] + # """Tracks the width of each column.""" + # var heights: List[Int] + # """Tracks the height of each row.""" fn __init__( out self, + *, style_function: StyleFunction, border_style: Style, border: Border = ROUNDED_BORDER, @@ -163,23 +162,34 @@ struct Table: width: The width of the table. height: The height of the table. """ - self.style_function = style_function - self.border = border - self.border_style = border_style - self.border_top = border_top - self.border_bottom = border_bottom - self.border_left = border_left - self.border_right = border_right - self.border_header = border_header - self.border_column = border_column - self.border_row = border_row - self.headers = headers - self.data = data + self._styler = style_function + self._border = border + self._border_style = border_style + self._border_top = border_top + self._border_bottom = border_bottom + self._border_left = border_left + self._border_right = border_right + self._border_header = border_header + self._border_column = border_column + self._border_row = border_row + self._headers = headers + self._data = data self.width = width self.height = height - self.offset = 0 - self.widths = List[Int]() - self.heights = List[Int]() + self._offset = 0 + # self.widths = List[Int]() + # self.heights = List[Int]() + + @staticmethod + fn new() -> Self: + """Returns a new Table, this is to bypass the compiler limitation on these args having default values. + It seems like argument default values are handled at compile time, and mog Styles are not compile time constants, + UNLESS a profile is specified ahead of time. + + Returns: + A new Table. + """ + return Table(style_function=default_styles, border_style=mog.Style()) fn clear_rows(self) -> Table: """Clears the table rows. @@ -188,7 +198,7 @@ struct Table: The updated table. """ var new = self - new.data = StringData() + new._data = StringData() return new fn style(self, row: Int, col: Int) -> Style: @@ -201,7 +211,7 @@ struct Table: Returns: The style for the cell. """ - return self.style_function(row, col) + return self._styler(row, col) fn rows(self, *rows: List[String]) -> Table: """Returns the style for a cell based on it's position (row, column). @@ -214,7 +224,7 @@ struct Table: """ var new = self for i in range(len(rows)): - new.data.append(rows[i]) + new._data.append(rows[i]) return new fn rows(self, rows: List[List[String]]) -> Table: @@ -228,7 +238,7 @@ struct Table: """ var new = self for row in rows: - new.data.append(row[]) + new._data.append(row[]) return new fn row(self, *row: String) -> Table: @@ -244,7 +254,7 @@ struct Table: var temp = List[String](capacity=len(row)) for element in row: temp.append(element[]) - new.data.append(temp) + new._data.append(temp) return new fn row(self, row: List[String]) -> Table: @@ -257,7 +267,7 @@ struct Table: The updated table. """ var new = self - new.data.append(row) + new._data.append(row) return new fn set_headers(self, *headers: String) -> Table: @@ -270,10 +280,10 @@ struct Table: The updated table. """ var new = self - var temp = List[String]() + var temp = List[String](capacity=len(headers)) for element in headers: temp.append(element[]) - new.headers = temp + new._headers = temp return new fn set_headers(self, headers: List[String]) -> Table: @@ -286,61 +296,77 @@ struct Table: The updated table. """ var new = self - new.headers = headers + new._headers = headers return new + + fn set_style(self, styler: StyleFunction) -> Table: + """Sets the table headers. - fn __str__(mut self) -> String: - """Returns the table as a String. - + Args: + styler: The style function to use. + Returns: - The table as a string. + The updated table. """ - var has_headers = len(self.headers) > 0 - var has_rows = self.data.rows() > 0 + var new = self + new._styler = styler + return new + + fn write_to[W: Writer, //](self, mut writer: W): + """Writes the table to the writer. + + Parameters: + W: The type of writer to write to. + Args: + writer: The writer to write to. + """ + var has_headers = len(self._headers) > 0 + var has_rows = self._data.rows() > 0 if not has_headers and not has_rows: - return "" + return var result = String() - # Add empty cells to the headers, until it's the same length as the longest # row (only if there are at headers in the first place). + var headers = self._headers if has_headers: - var i = len(self.headers) - while i < self.data.columns(): - self.headers.append("") + var i = len(headers) + while i < self._data.columns(): + headers.append("") i += 1 # Initialize the widths. - var widths_len = max(len(self.headers), self.data.columns()) - self.widths = List[Int](capacity=widths_len) + var widths_len = max(len(self._headers), self._data.columns()) + var widths = List[Int](capacity=widths_len) for _ in range(widths_len): - self.widths.append(0) + widths.append(0) - var heights_len = int(has_headers) + self.data.rows() - self.heights = List[Int](capacity=heights_len) + # Initialize the heights. + var heights_len = int(has_headers) + self._data.rows() + var heights = List[Int](capacity=heights_len) for _ in range(heights_len): - self.heights.append(0) + heights.append(0) # The style function may affect width of the table. It's possible to set # the StyleFunction after the headers and rows. Update the widths for a final # time. - for i in range(len(self.headers)): - self.widths[i] = get_width(self.style(0, i).render(self.headers[i])) - self.heights[0] = get_height(self.style(0, i).render(self.headers[i])) + for i in range(len(headers)): + widths[i] = get_width(self.style(0, i).render(headers[i])) + heights[0] = get_height(self.style(0, i).render(headers[i])) var row_number = 0 - while row_number < self.data.rows(): + while row_number < self._data.rows(): var column_number = 0 - while column_number < self.data.columns(): - var rendered = self.style(row_number + 1, column_number).render(self.data[row_number, column_number]) + while column_number < self._data.columns(): + var rendered = self.style(row_number + 1, column_number).render(self._data[row_number, column_number]) var row_number_with_header_offset = row_number + int(has_headers) - self.heights[row_number_with_header_offset] = max( - self.heights[row_number_with_header_offset], + heights[row_number_with_header_offset] = max( + heights[row_number_with_header_offset], get_height(rendered), ) - self.widths[column_number] = max(self.widths[column_number], get_width(rendered)) + widths[column_number] = max(widths[column_number], get_width(rendered)) column_number += 1 row_number += 1 @@ -381,25 +407,25 @@ struct Table: # # The biggest difference is 15 - 2, so we can shrink the 2nd column by 13. - var width = self.compute_width() + var width = self._compute_width(widths) if width < self.width and self.width > 0: # Table is too narrow, expand the columns evenly until it reaches the # desired width. var i = 0 while width < self.width: - self.widths[i] += 1 + widths[i] += 1 width += 1 - i = (i + 1) % len(self.widths) + i = (i + 1) % len(widths) elif width > self.width and self.width > 0: # Table is too wide, calculate the median non-whitespace length of each # column, and shrink the columns based on the largest difference. - var column_medians = List[Int](capacity=len(self.widths)) - for i in range(len(self.widths)): - var trimmed_width = List[Int](capacity=self.data.rows()) + var column_medians = List[Int](capacity=len(widths)) + for i in range(len(widths)): + var trimmed_width = List[Int](capacity=self._data.rows()) - for r in range(self.data.rows()): - var rendered_cell = self.style(r + int(has_headers), i).render(self.data[r, i]) + for r in range(self._data.rows()): + var rendered_cell = self.style(r + int(has_headers), i).render(self._data[r, i]) var non_whitespace_chars = get_width(rendered_cell.removesuffix(" ")) trimmed_width[r] = non_whitespace_chars + 1 @@ -407,9 +433,9 @@ struct Table: # Find the biggest differences between the median and the column width. # Shrink the columns based on the largest difference. - var differences = List[Int](capacity=len(self.widths)) - for i in range(len(self.widths)): - differences[i] = self.widths[i] - column_medians[i] + var differences = List[Int](capacity=len(widths)) + for i in range(len(widths)): + differences[i] = widths[i] - column_medians[i] while width > self.width: index, _ = largest(differences) @@ -417,223 +443,239 @@ struct Table: break var shrink = min(differences[index], width - self.width) - self.widths[index] -= shrink + widths[index] -= shrink width -= shrink differences[index] = 0 # Table is still too wide, begin shrinking the columns based on the # largest column. while width > self.width: - index, _ = largest(self.widths) - if self.widths[index] < 1: + index, _ = largest(widths) + if widths[index] < 1: break - self.widths[index] -= 1 + widths[index] -= 1 width -= 1 - if self.border_top: - result.write(self.construct_top_border(), NEWLINE) + if self._border_top: + result.write(self._construct_top_border(widths), NEWLINE) if has_headers: - result.write(self.construct_headers(), NEWLINE) + result.write(self._construct_headers(widths, headers), NEWLINE) - var r = self.offset - while r < self.data.rows(): - result.write(self.construct_row(r)) + var r = self._offset + while r < self._data.rows(): + result.write(self._construct_row(r, widths, heights, headers)) r += 1 - if self.border_bottom: - result.write(self.construct_bottom_border()) + if self._border_bottom: + result.write(self._construct_bottom_border(widths)) + + # TODO: mog.Style() without a specific profile type makes this not compile time friendly. + writer.write(mog.Style().max_height(self._compute_height(heights)).max_width(self.width).render(result)) + + fn __str__(self) -> String: + """Returns the table as a String. + + Returns: + The table as a string. + """ + return String.write(self) + + fn render(self) -> String: + """Returns the table as a String. - return mog.Style().max_height(self.compute_height()).max_width(self.width).render(result) + Returns: + The table as a string. + """ + return self.__str__() - fn compute_width(self) -> Int: + fn _compute_width(self, widths: List[Int]) -> Int: """Computes the width of the table in it's current configuration. + Args: + widths: The widths of the columns. + Returns: The width of the table. """ - var width = sum(self.widths) + int(self.border_left) + int(self.border_right) - if self.border_column: - width += len(self.widths) - 1 + var width = sum(widths) + int(self._border_left) + int(self._border_right) + if self._border_column: + width += len(widths) - 1 return width - fn compute_height(self) -> Int: + fn _compute_height(self, heights: List[Int]) -> Int: """Computes the height of the table in it's current configuration. + Args: + heights: The heights of the rows. + Returns: The height of the table. """ return ( - sum(self.heights) + sum(heights) - 1 - + int(len(self.headers) > 0) - + int(self.border_top) - + int(self.border_bottom) - + int(self.border_header) - + self.data.rows() * int(self.border_row) + + int(len(self._headers) > 0) + + int(self._border_top) + + int(self._border_bottom) + + int(self._border_header) + + self._data.rows() * int(self._border_row) ) - # render - fn render(mut self) -> String: - """Returns the table as a String. - - Returns: - The table as a string. - """ - return self.__str__() - - fn construct_top_border(self) -> String: + fn _construct_top_border(self, widths: List[Int]) -> String: """Constructs the top border for the table given it's current border configuration and data. + Args: + widths: The widths of the columns. + Returns: The constructed top border as a string. """ var result = String() - if self.border_left: - result.write(self.border_style.render(self.border.top_left)) + if self._border_left: + result.write(self._border_style.render(self._border.top_left)) var i = 0 - while i < len(self.widths): - result.write(self.border_style.render(self.border.top * self.widths[i])) - if i < len(self.widths) - 1 and self.border_column: - result.write(self.border_style.render(self.border.middle_top)) + while i < len(widths): + result.write(self._border_style.render(self._border.top * widths[i])) + if i < len(widths) - 1 and self._border_column: + result.write(self._border_style.render(self._border.middle_top)) i += 1 - if self.border_right: - result.write(self.border_style.render(self.border.top_right)) + if self._border_right: + result.write(self._border_style.render(self._border.top_right)) return result - fn construct_bottom_border(self) -> String: + fn _construct_bottom_border(self, widths: List[Int]) -> String: """Constructs the bottom border for the table given it's current border configuration and data. + Args: + widths: The widths of the columns. + Returns: The constructed bottom border as a string. """ var result = String() - if self.border_left: - result.write(self.border_style.render(self.border.bottom_left)) + if self._border_left: + result.write(self._border_style.render(self._border.bottom_left)) var i = 0 - while i < len(self.widths): - result.write(self.border_style.render(self.border.bottom * self.widths[i])) - if i < len(self.widths) - 1 and self.border_column: - result.write(self.border_style.render(self.border.middle_bottom)) + while i < len(widths): + result.write(self._border_style.render(self._border.bottom * widths[i])) + if i < len(widths) - 1 and self._border_column: + result.write(self._border_style.render(self._border.middle_bottom)) i += 1 - if self.border_right: - result.write(self.border_style.render(self.border.bottom_right)) + if self._border_right: + result.write(self._border_style.render(self._border.bottom_right)) return result - fn construct_headers(self) -> String: + fn _construct_headers(self, widths: List[Int], headers: List[String]) -> String: """Constructs the headers for the table given it's current header configuration and data. + Args: + widths: The widths of the columns. + headers: The headers of the table. + Returns: The constructed headers as a string. """ var result = String() - if self.border_left: - result.write(self.border_style.render(self.border.left)) + if self._border_left: + result.write(self._border_style.render(self._border.left)) - for i in range(len(self.headers)): - var style = self.style(0, i).max_height(1).width(self.widths[i]).max_width(self.widths[i]) + for i in range(len(headers)): + var style = self.style(0, i).max_height(1).width(widths[i]).max_width(widths[i]) - result.write(style.render(truncate(self.headers[i], self.widths[i], "…"))) - if (i < len(self.headers) - 1) and (self.border_column): - result.write(self.border_style.render(self.border.left)) + result.write(style.render(truncate(headers[i], widths[i], "…"))) + if (i < len(headers) - 1) and (self._border_column): + result.write(self._border_style.render(self._border.left)) - if self.border_header: - if self.border_right: - result.write(self.border_style.render(self.border.right)) + if self._border_header: + if self._border_right: + result.write(self._border_style.render(self._border.right)) result.write("\n") - if self.border_left: - result.write(self.border_style.render(self.border.middle_left)) + if self._border_left: + result.write(self._border_style.render(self._border.middle_left)) var i = 0 - while i < len(self.headers): - result.write(self.border_style.render(self.border.bottom * self.widths[i])) - if i < len(self.headers) - 1 and self.border_column: - result.write(self.border_style.render(self.border.middle)) + while i < len(headers): + result.write(self._border_style.render(self._border.bottom * widths[i])) + if i < len(headers) - 1 and self._border_column: + result.write(self._border_style.render(self._border.middle)) i += 1 - if self.border_right: - result.write(self.border_style.render(self.border.middle_right)) + if self._border_right: + result.write(self._border_style.render(self._border.middle_right)) - if self.border_right and not self.border_header: - result.write(self.border_style.render(self.border.right)) + if self._border_right and not self._border_header: + result.write(self._border_style.render(self._border.right)) return result - fn construct_row(self, index: Int) -> String: + fn _construct_row(self, index: Int, widths: List[Int], heights: List[Int], headers: List[String]) -> String: """Constructs the row for the table given an index and row data based on the current configuration. Args: index: The index of the row to construct. + widths: The widths of the columns. + heights: The heights of the rows. + headers: The headers of the table. Returns: The constructed row as a string. """ var result = String() - var has_headers = len(self.headers) > 0 - var height = self.heights[index + int(has_headers)] + var has_headers = len(headers) > 0 + var height = heights[index + int(has_headers)] var cells = List[String]() - var left = (self.border_style.render(self.border.left) + "\n") * height - if self.border_left: + var left = (self._border_style.render(self._border.left) + "\n") * height + if self._border_left: cells.append(left) var c = 0 - while c < self.data.columns(): - var style = self.style(index + 1, c).height(height).max_height(height).width(self.widths[c]).max_width( - self.widths[c] + while c < self._data.columns(): + var style = self.style(index + 1, c).height(height).max_height(height).width(widths[c]).max_width( + widths[c] ) - - cells.append(style.render(truncate(self.data[index, c], self.widths[c] * height, "…"))) - - if c < self.data.columns() - 1 and self.border_column: + cells.append(style.render(truncate(self._data[index, c], widths[c] * height, "…"))) + if c < self._data.columns() - 1 and self._border_column: cells.append(left) c += 1 - if self.border_right: - var right = (self.border_style.render(self.border.right) + "\n") * height - cells.append(right) - - for i in range(len(cells)): - cells[i] = cells[i].removesuffix("\n") - + if self._border_right: + cells.append((self._border_style.render(self._border.right) + "\n") * height) + + # TODO: removesuffix doesn't seem to work with all utf8 chars, maybe it'll be fixed upstream soon. + # It wasn't recognizing the last character as a newline. + for cell in cells: + if cell[][-1] == "\n": + cell[] = cell[][:-1] + result.write(join_horizontal(position.top, cells), "\n") - if self.border_row and index < self.data.rows() - 1: - result.write(self.border_style.render(self.border.middle_left)) + if self._border_row and index < self._data.rows() - 1: + result.write(self._border_style.render(self._border.middle_left)) var i = 0 - while i < len(self.widths): - result.write(self.border_style.render(self.border.middle * self.widths[i])) - if i < len(self.widths) - 1 and self.border_column: - result.write(self.border_style.render(self.border.middle)) + while i < len(widths): + result.write(self._border_style.render(self._border.middle * widths[i])) + if i < len(widths) - 1 and self._border_column: + result.write(self._border_style.render(self._border.middle)) i += 1 - result.write(self.border_style.render(self.border.middle_right) + "\n") + result.write(self._border_style.render(self._border.middle_right) + "\n") return result - - -fn new_table() -> Table: - """Returns a new Table, this is to bypass the compiler limitation on these args having default values. - It seems like argument default values are handled at compile time, and mog Styles are not compile time constants, - UNLESS a profile is specified ahead of time. - - Returns: - A new Table. - """ - return Table(style_function=default_styles, border_style=mog.Style()) diff --git a/test/test_style.mojo b/test/test_style.mojo index a726e29..985d989 100644 --- a/test/test_style.mojo +++ b/test/test_style.mojo @@ -340,7 +340,7 @@ def test_border_right(): # Turn on border right (flag has a value set), but then set it to False (flag has value set, value is False). # testing.assert_equal(style.border_right(False).render("hello"), "hello") - +# TODO: All border unsets not working correctly! At least it seems like it. All sides set to false, then activating one and deactivating it makes all sides render!? # def test_unset_border_right(): # alias style = ansi_style.border(mog.PLUS_BORDER, False, False, False, False).border_right().unset_border_right() # testing.assert_equal(style.render("hello"), "hello") @@ -376,7 +376,8 @@ def test_border_foreground(): def test_border_top_foreground(): - pass + print(repr(ansi_style.border(mog.PLUS_BORDER).border_top_foreground(mog.Color(12)).render("hello"))) + testing.assert_equal(ansi_style.border(mog.PLUS_BORDER).border_top_foreground(mog.Color(12)).render("hello"), "\x1b[;94m+++++++\x1b[0m\n+hello+\n+++++++") def test_unset_border_top_foreground(): @@ -384,7 +385,8 @@ def test_unset_border_top_foreground(): def test_border_left_foreground(): - pass + print(repr(ansi_style.border(mog.PLUS_BORDER).border_left_foreground(mog.Color(12)).render("hello"))) + testing.assert_equal(ansi_style.border(mog.PLUS_BORDER).border_left_foreground(mog.Color(12)).render("hello"), "+++++++\n\x1b[;94m+\x1b[0mhello+\n+++++++") def test_unset_border_left_foreground(): @@ -392,7 +394,8 @@ def test_unset_border_left_foreground(): def test_border_right_foreground(): - pass + print(repr(ansi_style.border(mog.PLUS_BORDER).border_right_foreground(mog.Color(12)).render("hello"))) + testing.assert_equal(ansi_style.border(mog.PLUS_BORDER).border_right_foreground(mog.Color(12)).render("hello"), "+++++++\n+hello\x1b[;94m+\x1b[0m\n+++++++") def test_unset_border_right_foreground(): @@ -400,7 +403,8 @@ def test_unset_border_right_foreground(): def test_border_bottom_foreground(): - pass + print(repr(ansi_style.border(mog.PLUS_BORDER).border_bottom_foreground(mog.Color(12)).render("hello"))) + testing.assert_equal(ansi_style.border(mog.PLUS_BORDER).border_bottom_foreground(mog.Color(12)).render("hello"), "+++++++\n+hello+\n\x1b[;94m+++++++\x1b[0m") def test_unset_border_bottom_foreground(): @@ -408,11 +412,24 @@ def test_unset_border_bottom_foreground(): def test_border_background(): - pass + alias style = ansi_style.border(mog.PLUS_BORDER) + + # One for all sides + testing.assert_equal(style.border_background(mog.Color(12)).render("hello"), "\x1b[;104m+++++++\x1b[0m\n\x1b[;104m+\x1b[0mhello\x1b[;104m+\x1b[0m\n\x1b[;104m+++++++\x1b[0m") + + # Two colors for top/bottom and left/right + testing.assert_equal(style.border_background(mog.Color(12), mog.Color(13)).render("hello"), "\x1b[;104m+++++++\x1b[0m\n\x1b[;105m+\x1b[0mhello\x1b[;105m+\x1b[0m\n\x1b[;104m+++++++\x1b[0m") + + # Three colors for top, left/right, and bottom + testing.assert_equal(style.border_background(mog.Color(12), mog.Color(13), mog.Color(14)).render("hello"), "\x1b[;104m+++++++\x1b[0m\n\x1b[;105m+\x1b[0mhello\x1b[;105m+\x1b[0m\n\x1b[;106m+++++++\x1b[0m") + + # Four colors for top, right, bottom, left + testing.assert_equal(style.border_background(mog.Color(12), mog.Color(13), mog.Color(14), mog.Color(15)).render("hello"), "\x1b[;104m+++++++\x1b[0m\n\x1b[;107m+\x1b[0mhello\x1b[;105m+\x1b[0m\n\x1b[;106m+++++++\x1b[0m") def test_border_top_background(): - pass + alias style = ansi_style.border(mog.PLUS_BORDER).border_top_background(mog.Color(12)) + testing.assert_equal(style.render("hello"), "\x1b[;104m+++++++\x1b[0m\n+hello+\n+++++++") def test_unset_border_top_background(): @@ -420,7 +437,8 @@ def test_unset_border_top_background(): def test_border_left_background(): - pass + alias style = ansi_style.border(mog.PLUS_BORDER).border_left_background(mog.Color(12)) + testing.assert_equal(style.render("hello"), "+++++++\n\x1b[;104m+\x1b[0mhello+\n+++++++") def test_unset_border_left_background(): @@ -428,7 +446,8 @@ def test_unset_border_left_background(): def test_border_right_background(): - pass + alias style = ansi_style.border(mog.PLUS_BORDER).border_right_background(mog.Color(12)) + testing.assert_equal(style.render("hello"), "+++++++\n+hello\x1b[;104m+\x1b[0m\n+++++++") def test_unset_border_right_background(): @@ -436,7 +455,8 @@ def test_unset_border_right_background(): def test_border_bottom_background(): - pass + alias style = ansi_style.border(mog.PLUS_BORDER).border_bottom_background(mog.Color(12)) + testing.assert_equal(style.render("hello"), "+++++++\n+hello+\n\x1b[;104m+++++++\x1b[0m") def test_unset_border_bottom_background(): @@ -444,21 +464,32 @@ def test_unset_border_bottom_background(): def test_padding(): + """Test padding on all sides, top/bottom and left/right, top, left/right, bottom, and all sides. + Note: padding is applied inside of the text area. As opposed to margin which is applied outside the text area. + """ + alias border_style = ansi_style.border(mog.PLUS_BORDER) + # Padding on all sides testing.assert_equal(ansi_style.padding(1).render("hello"), " \n hello \n ") + testing.assert_equal(border_style.padding(1).render("hello"), "+++++++++\n+ +\n+ hello +\n+ +\n+++++++++") # Top/bottom and left/right testing.assert_equal(ansi_style.padding(1, 2).render("hello"), " \n hello \n ") + testing.assert_equal(border_style.padding(1, 2).render("hello"), "+++++++++++\n+ +\n+ hello +\n+ +\n+++++++++++") # Top, left/right, bottom testing.assert_equal(ansi_style.padding(1, 2, 3).render("hello"), " \n hello \n \n \n ") + testing.assert_equal(border_style.padding(1, 2, 3).render("hello"), "+++++++++++\n+ +\n+ hello +\n+ +\n+ +\n+ +\n+++++++++++") # All sides testing.assert_equal(ansi_style.padding(1, 2, 3, 4).render("hello"), " \n hello \n \n \n ") + testing.assert_equal(border_style.padding(1, 2, 3, 4).render("hello"), "+++++++++++++\n+ +\n+ hello +\n+ +\n+ +\n+ +\n+++++++++++++") def test_padding_top(): - pass + alias border_style = ansi_style.border(mog.PLUS_BORDER) + testing.assert_equal(ansi_style.padding_top(1).render("hello"), " \nhello") + testing.assert_equal(border_style.padding_top(1).render("hello"), "+++++++\n+ +\n+hello+\n+++++++") def test_unset_padding_top(): @@ -466,7 +497,9 @@ def test_unset_padding_top(): def test_padding_left(): - pass + alias border_style = ansi_style.border(mog.PLUS_BORDER) + testing.assert_equal(ansi_style.padding_left(1).render("hello"), " hello") + testing.assert_equal(border_style.padding_left(1).render("hello"), "++++++++\n+ hello+\n++++++++") def test_unset_padding_left(): @@ -474,7 +507,9 @@ def test_unset_padding_left(): def test_padding_right(): - pass + alias border_style = ansi_style.border(mog.PLUS_BORDER) + testing.assert_equal(ansi_style.padding_right(1).render("hello"), "hello ") + testing.assert_equal(border_style.padding_right(1).render("hello"), "++++++++\n+hello +\n++++++++") def test_unset_padding_right(): @@ -482,7 +517,9 @@ def test_unset_padding_right(): def test_padding_bottom(): - pass + alias border_style = ansi_style.border(mog.PLUS_BORDER) + testing.assert_equal(ansi_style.padding_bottom(1).render("hello"), "hello\n ") + testing.assert_equal(border_style.padding_bottom(1).render("hello"), "+++++++\n+hello+\n+ +\n+++++++") def test_unset_padding_bottom(): @@ -490,11 +527,32 @@ def test_unset_padding_bottom(): def test_margin(): - pass + """Test margin on all sides, top/bottom and left/right, top, left/right, bottom, and all sides. + Note: margins are applied outside of the text area. As opposed to padding which is applied inside the text area. + """ + alias border_style = ansi_style.border(mog.PLUS_BORDER) + + # Margin on all sides + testing.assert_equal(ansi_style.margin(1).render("hello"), " \n hello \n ") + testing.assert_equal(border_style.margin(1).render("hello"), " \n +++++++ \n +hello+ \n +++++++ \n ") + + # Top/bottom and left/right + testing.assert_equal(ansi_style.margin(1, 2).render("hello"), " \n hello \n ") + testing.assert_equal(border_style.margin(1, 2).render("hello"), " \n +++++++ \n +hello+ \n +++++++ \n ") + + # Top, left/right, bottom + testing.assert_equal(ansi_style.margin(1, 2, 3).render("hello"), " \n hello \n \n \n ") + testing.assert_equal(border_style.margin(1, 2, 3).render("hello"), " \n +++++++ \n +hello+ \n +++++++ \n \n \n ") + + # All sides + testing.assert_equal(ansi_style.margin(1, 2, 3, 4).render("hello"), " \n hello \n \n \n ") + testing.assert_equal(border_style.margin(1, 2, 3, 4).render("hello"), " \n +++++++ \n +hello+ \n +++++++ \n \n \n ") def test_margin_top(): - pass + alias border_style = ansi_style.border(mog.PLUS_BORDER) + testing.assert_equal(ansi_style.margin_top(1).render("hello"), " \nhello") + testing.assert_equal(border_style.margin_top(1).render("hello"), " \n+++++++\n+hello+\n+++++++") def test_unset_margin_top(): @@ -502,7 +560,9 @@ def test_unset_margin_top(): def test_margin_left(): - pass + alias border_style = ansi_style.border(mog.PLUS_BORDER) + testing.assert_equal(ansi_style.margin_left(1).render("hello"), " hello") + testing.assert_equal(border_style.margin_left(1).render("hello"), " +++++++\n +hello+\n +++++++") def test_unset_margin_left(): @@ -510,7 +570,9 @@ def test_unset_margin_left(): def test_margin_right(): - pass + alias border_style = ansi_style.border(mog.PLUS_BORDER) + testing.assert_equal(ansi_style.margin_right(1).render("hello"), "hello ") + testing.assert_equal(border_style.margin_right(1).render("hello"), "+++++++ \n+hello+ \n+++++++ ") def test_unset_margin_right(): @@ -518,7 +580,9 @@ def test_unset_margin_right(): def test_margin_bottom(): - pass + alias border_style = ansi_style.border(mog.PLUS_BORDER) + testing.assert_equal(ansi_style.margin_bottom(1).render("hello"), "hello\n ") + testing.assert_equal(border_style.margin_bottom(1).render("hello"), "+++++++\n+hello+\n+++++++\n ") def test_unset_margin_bottom(): @@ -526,7 +590,14 @@ def test_unset_margin_bottom(): def test_maybe_convert_tabs(): - pass + # Default tab width of 4 + testing.assert_equal(ansi_style._maybe_convert_tabs("\tHello world!"), " Hello world!") + + # Set tab width to 1 + testing.assert_equal(ansi_style.tab_width(1)._maybe_convert_tabs("\tHello world!"), " Hello world!") + + # Set tab width to -1, which disables `\t` conversion to spaces. + testing.assert_equal(ansi_style.tab_width(-1)._maybe_convert_tabs("\tHello world!"), "\tHello world!") def test_style_border(): @@ -534,7 +605,22 @@ def test_style_border(): def test_apply_border(): - pass + # Uses no border by default + testing.assert_equal(ansi_style._apply_border("hello"), "hello") + + # Standard pathway for applying a border with no other styling. + alias border_style = ansi_style.border(mog.PLUS_BORDER) + testing.assert_equal(border_style._apply_border("hello"), "+++++++\n+hello+\n+++++++") + + # Render with individual border sides disabled. + testing.assert_equal(border_style.unset_border_top()._apply_border("hello"), "+hello+\n+++++++") + testing.assert_equal(border_style.unset_border_left()._apply_border("hello"), "++++++\nhello+\n++++++") + testing.assert_equal(border_style.unset_border_right()._apply_border("hello"), "++++++\n+hello\n++++++") + testing.assert_equal(border_style.unset_border_bottom()._apply_border("hello"), "+++++++\n+hello+") + + # If the border sides are set, but the character used is an empty string "", then it should be replaced with a whitespace " ". + # testing.assert_equal(ansi_style.border(mog.NO_BORDER).unset_border_top().unset_border_bottom()._apply_border("hello"), " hello ") + def test_apply_margin(): diff --git a/test/test_table_rows.mojo b/test/test_table_rows.mojo new file mode 100644 index 0000000..1f42ec3 --- /dev/null +++ b/test/test_table_rows.mojo @@ -0,0 +1,70 @@ +import testing +from mog.table import StringData, Filter + + +def test_string_data_append(): + var data = StringData( + List[String]("Name", "Age"), + List[String]("My Name", "30"), + List[String]("Your Name", "25"), + List[String]("Their Name", "35") + ) + testing.assert_equal(data.rows(), 4) + testing.assert_equal(data.columns(), 2) + + data.append(List[String]("Her Name", "40", "105")) + testing.assert_equal(data.rows(), 5) + testing.assert_equal(data.columns(), 3) + + data.append("No Name", "0") + testing.assert_equal(data.rows(), 6) + testing.assert_equal(data.columns(), 3) + + +def test_string_data_add(): + var data = StringData( + List[String]("Name", "Age"), + List[String]("My Name", "30"), + List[String]("Your Name", "25"), + List[String]("Their Name", "35") + ) + var data2 = StringData( + List[String]("No Name", "0", "999"), + ) + var new = data + data2 + testing.assert_equal(new.rows(), 5) + testing.assert_equal(new.columns(), 3) + testing.assert_equal(new[4, 0], "No Name") + + +def test_string_data_iadd(): + var data = StringData( + List[String]("Name", "Age"), + List[String]("My Name", "30"), + List[String]("Your Name", "25"), + List[String]("Their Name", "35") + ) + var data2 = StringData( + List[String]("No Name", "0", "999"), + ) + data += data2 + testing.assert_equal(data.rows(), 5) + testing.assert_equal(data.columns(), 3) + testing.assert_equal(data[4, 0], "No Name") + + +def test_filter(): + var data = StringData( + List[String]("Name", "Age"), + List[String]("My Name", "30"), + List[String]("Your Name", "25"), + List[String]("Their Name", "35") + ) + + fn filter_headers(row: Int) -> Bool: + return row != 0 + + var filter = Filter(data, filter_headers) + testing.assert_equal(filter.rows(), 3) + testing.assert_equal(filter.columns(), 2) + testing.assert_equal(filter[0, 0], "My Name") diff --git a/test/test_table_table.mojo b/test/test_table_table.mojo new file mode 100644 index 0000000..500842b --- /dev/null +++ b/test/test_table_table.mojo @@ -0,0 +1,14 @@ +# import testing +# import mog +# from mog.table import Table + + +# def test_table_render(): +# var s = mog.Style(mog.ANSI).foreground(mog.Color(240)) +# var table = Table.new().row("Bubble Tea", s.render("Milky")) +# table.width = 50 +# print(table) +# testing.assert_equal( +# "str(table)", +# "Bubble Tea Milky\nMilk Tea Also milky\nActual milk Milky as well\n", +# ) \ No newline at end of file diff --git a/test/test_table_util.mojo b/test/test_table_util.mojo new file mode 100644 index 0000000..e500e7e --- /dev/null +++ b/test/test_table_util.mojo @@ -0,0 +1,17 @@ +import testing +import mog +from mog.table.util import sum, median, largest + + +def test_sum(): + testing.assert_equal(sum(List[Int](1, 2, 3, 4, 5)), 15) + + +def test_median(): + testing.assert_equal(median(List[Int](1, 2, 3, 4, 5)), 3) + + +def test_largest(): + var result = largest(List[Int](1, 2, 3, 4, 5)) + testing.assert_equal(result[0], 4) + testing.assert_equal(result[1], 5)