#!/usr/bin/python
"""
HTML.py - v0.04 2009-07-28 Philippe Lagadec

This module provides a few classes to easily generate HTML code such as tables
and lists.

Project website: http://www.decalage.info/python/html

License: CeCILL (open-source GPL compatible), see source code for details.
         http://www.cecill.info
"""

__version__ = "0.04"
__date__ = "2009-07-28"
__author__ = "Philippe Lagadec"
# -- LICENSE ------------------------------------------------------------------

# Copyright Philippe Lagadec - see http://www.decalage.info/contact for contact info
#
# This module provides a few classes to easily generate HTML tables and lists.
#
# This software is governed by the CeCILL license under French law and
# abiding by the rules of distribution of free software.  You can  use,
# modify and/or redistribute the software under the terms of the CeCILL
# license as circulated by CEA, CNRS and INRIA at the following URL
# "http://www.cecill.info".
#
# A copy of the CeCILL license is also provided in these attached files:
# Licence_CeCILL_V2-en.html and Licence_CeCILL_V2-fr.html
#
# As a counterpart to the access to the source code and  rights to copy,
# modify and redistribute granted by the license, users are provided only
# with a limited warranty  and the software's author, the holder of the
# economic rights, and the successive licensors have only limited
# liability.
#
# In this respect, the user's attention is drawn to the risks associated
# with loading,  using,  modifying and/or developing or reproducing the
# software by the user in light of its specific status of free software,
# that may mean  that it is complicated to manipulate,  and  that  also
# therefore means  that it is reserved for developers  and  experienced
# professionals having in-depth computer knowledge. Users are therefore
# encouraged to load and test the software's suitability as regards their
# requirements in conditions enabling the security of their systems and/or
# data to be ensured and,  more generally, to use and operate it in the
# same conditions as regards security.
#
# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL license and that you accept its terms.


# -- CHANGES ------------------------------------------------------------------

# 2008-10-06 v0.01 PL: - First version
# 2008-10-13 v0.02 PL: - added cellspacing and cellpadding to table
#                      - added functions to ease one-step creation of tables
#                        and lists
# 2009-07-21 v0.03 PL: - added column attributes and styles (first attempt)
#                        (thanks to an idea submitted by Michal Cernoevic)
# 2009-07-28 v0.04 PL: - improved column styles, workaround for Mozilla

# Table style to get thin black lines in Mozilla/Firefox instead of 3D borders
TABLE_STYLE_THINBORDER = "border: 1px solid #000000; border-collapse: collapse;"
# TABLE_STYLE_THINBORDER = "border: 1px solid #000000;"


# == CLASSES ===================================================================


class TableCell:
    """a TableCell object is used to create a cell in a HTML table. (TD or TH)

    :param text: text in the cell (may contain HTML tags). May be any object which
      can be converted to a string using str().
    :param header: bool, false for a normal data cell (TD), true for a header cell (TH)
    :param bgcolor: str, background color
    :param width: str, width
    :param align: str, horizontal alignement (left, center, right, justify or char)
    :param char: str, alignment character, decimal point if not specified
    :param charoff: str, see HTML specs
    :param valign: str, vertical alignment (top|middle|bottom|baseline)
    :param style: str, CSS style
    :param attribs: dict, additional attributes for the TD/TH tag

    Reference: http://www.w3.org/TR/html4/struct/tables.html#h-11.2.6
    """

    def __init__(
        self,
        text="",
        bgcolor=None,
        header=False,
        width=None,
        align=None,
        char=None,
        charoff=None,
        valign=None,
        style=None,
        attribs=None,
    ):
        """TableCell constructor."""
        self.text = text
        self.bgcolor = bgcolor
        self.header = header
        self.width = width
        self.align = align
        self.char = char
        self.charoff = charoff
        self.valign = valign
        self.style = style
        self.attribs = attribs
        if attribs is None:
            self.attribs = {}

    def __str__(self):
        """return the HTML code for the table cell as a string."""
        attribs_str = ""
        if self.bgcolor:
            self.attribs["bgcolor"] = self.bgcolor
        if self.width:
            self.attribs["width"] = self.width
        if self.align:
            self.attribs["align"] = self.align
        if self.char:
            self.attribs["char"] = self.char
        if self.charoff:
            self.attribs["charoff"] = self.charoff
        if self.valign:
            self.attribs["valign"] = self.valign
        if self.style:
            self.attribs["style"] = self.style
        for attr in self.attribs:
            attribs_str += f' {attr}="{self.attribs[attr]}"'
        if self.text:
            text = str(self.text)
        else:
            # An empty cell should at least contain a non-breaking space
            text = "&nbsp;"
        if self.header:
            return f"  <TH{attribs_str}>{text}</TH>\n"
        else:
            return f"  <TD{attribs_str}>{text}</TD>\n"


# ------------------------------------------------------------------------------


class TableRow:
    """a TableRow object is used to create a row in a HTML table. (TR tag)

    :param cells: list, tuple or any iterable, containing one string or TableCell
      object for each cell
    :param header: bool, true for a header row (TH), false for a normal data row (TD)
    :param bgcolor: str, background color
    :param col_align, col_valign, col_char, col_charoff, col_styles: see Table class
    :param attribs: dict, additional attributes for the TR tag

    Reference: http://www.w3.org/TR/html4/struct/tables.html#h-11.2.5
    """

    def __init__(
        self,
        cells=None,
        bgcolor=None,
        header=False,
        attribs=None,
        col_align=None,
        col_valign=None,
        col_char=None,
        col_charoff=None,
        col_styles=None,
    ):
        """TableCell constructor."""
        self.bgcolor = bgcolor
        self.cells = cells
        self.header = header
        self.col_align = col_align
        self.col_valign = col_valign
        self.col_char = col_char
        self.col_charoff = col_charoff
        self.col_styles = col_styles
        self.attribs = attribs
        if attribs is None:
            self.attribs = {}

    def __str__(self):
        """return the HTML code for the table row as a string."""
        attribs_str = ""
        if self.bgcolor:
            self.attribs["bgcolor"] = self.bgcolor
        for attr in self.attribs:
            attribs_str += f' {attr}="{self.attribs[attr]}"'
        result = f" <TR{attribs_str}>\n"
        for cell in self.cells:
            col = self.cells.index(cell)  # cell column index
            if not isinstance(cell, TableCell):
                cell = TableCell(cell, header=self.header)
            # apply column alignment if specified:
            if self.col_align and cell.align is None:
                cell.align = self.col_align[col]
            if self.col_char and cell.char is None:
                cell.char = self.col_char[col]
            if self.col_charoff and cell.charoff is None:
                cell.charoff = self.col_charoff[col]
            if self.col_valign and cell.valign is None:
                cell.valign = self.col_valign[col]
            # apply column style if specified:
            if self.col_styles and cell.style is None:
                cell.style = self.col_styles[col]
            result += str(cell)
        result += " </TR>\n"
        return result


# ------------------------------------------------------------------------------


class Table:
    """a Table object is used to create a HTML table. (TABLE tag)

    :param rows: list, tuple or any iterable, containing one iterable or TableRow
      object for each row
    :param header_row: list, tuple or any iterable, containing the header row (optional)
    :param border: str or int, border width
    :param style: str, table style in CSS syntax (thin black borders by default)
    :param width: str, width of the table on the page
    :param attribs: dict, additional attributes for the TABLE tag
    :param col_width: list or tuple defining width for each column
    :param col_align: list or tuple defining horizontal alignment for each column
    :param col_char: list or tuple defining alignment character for each column
    :param col_charoff: list or tuple defining charoff attribute for each column
    :param col_valign: list or tuple defining vertical alignment for each column
    :param col_styles: list or tuple of HTML styles for each column

    Reference: http://www.w3.org/TR/html4/struct/tables.html#h-11.2.1
    """

    def __init__(
        self,
        rows=None,
        border="1",
        style=None,
        width=None,
        cellspacing=None,
        cellpadding=4,
        attribs=None,
        header_row=None,
        col_width=None,
        col_align=None,
        col_valign=None,
        col_char=None,
        col_charoff=None,
        col_styles=None,
    ):
        """TableCell constructor."""
        self.border = border
        self.style = style
        # style for thin borders by default
        if style is None:
            self.style = TABLE_STYLE_THINBORDER
        self.width = width
        self.cellspacing = cellspacing
        self.cellpadding = cellpadding
        self.header_row = header_row
        self.rows = rows
        if not rows:
            self.rows = []
        self.attribs = attribs
        if not attribs:
            self.attribs = {}
        self.col_width = col_width
        self.col_align = col_align
        self.col_char = col_char
        self.col_charoff = col_charoff
        self.col_valign = col_valign
        self.col_styles = col_styles

    def __str__(self):
        """return the HTML code for the table as a string."""
        attribs_str = ""
        if self.border:
            self.attribs["border"] = self.border
        if self.style:
            self.attribs["style"] = self.style
        if self.width:
            self.attribs["width"] = self.width
        if self.cellspacing:
            self.attribs["cellspacing"] = self.cellspacing
        if self.cellpadding:
            self.attribs["cellpadding"] = self.cellpadding
        for attr in self.attribs:
            attribs_str += f' {attr}="{self.attribs[attr]}"'
        result = f"<TABLE{attribs_str}>\n"
        # insert column tags and attributes if specified:
        if self.col_width:
            for width in self.col_width:
                result += f'  <COL width="{width}">\n'
            # The following code would also generate column attributes for style
            # and alignement according to HTML4 specs,
            # BUT it is not supported completely (only width) on Mozilla Firefox:
            # see https://bugzilla.mozilla.org/show_bug.cgi?id=915
        # n_cols = max(len(self.col_styles), len(self.col_width),
        # len(self.col_align), len(self.col_valign))
        # for i in range(n_cols):
        #             col = ''
        # try:
        # if self.col_styles[i]:
        #                     col += ' style="%s"' % self.col_styles[i]
        # except: pass
        # try:
        # if self.col_width[i]:
        #                     col += ' width="%s"' % self.col_width[i]
        # except: pass
        # try:
        # if self.col_align[i]:
        #                     col += ' align="%s"' % self.col_align[i]
        # except: pass
        # try:
        # if self.col_valign[i]:
        #                     col += ' valign="%s"' % self.col_valign[i]
        # except: pass
        #             result += '<COL%s>\n' % col
        # First insert a header row if specified:
        if self.header_row:
            if not isinstance(self.header_row, TableRow):
                result += str(TableRow(self.header_row, header=True))
            else:
                result += str(self.header_row)
        # Then all data rows:
        for row in self.rows:
            if not isinstance(row, TableRow):
                row = TableRow(row)
            # apply column alignments  and styles to each row if specified:
            # (Mozilla bug workaround)
            if self.col_align and not row.col_align:
                row.col_align = self.col_align
            if self.col_char and not row.col_char:
                row.col_char = self.col_char
            if self.col_charoff and not row.col_charoff:
                row.col_charoff = self.col_charoff
            if self.col_valign and not row.col_valign:
                row.col_valign = self.col_valign
            if self.col_styles and not row.col_styles:
                row.col_styles = self.col_styles
            result += str(row)
        result += "</TABLE>"
        return result


# ------------------------------------------------------------------------------


class List:
    """a List object is used to create an ordered or unordered list in HTML.
    (UL/OL tag)

    Attributes:
    - lines: list, tuple or any iterable, containing one string for each line
    - ordered: bool, choice between an ordered (OL) or unordered list (UL)
    - attribs: dict, additional attributes for the OL/UL tag

    Reference: http://www.w3.org/TR/html4/struct/lists.html
    """

    def __init__(self, lines=None, ordered=False, start=None, attribs=None):
        """List constructor."""
        if lines:
            self.lines = lines
        else:
            self.lines = []
        self.ordered = ordered
        self.start = start
        if attribs:
            self.attribs = attribs
        else:
            self.attribs = {}

    def __str__(self):
        """return the HTML code for the list as a string."""
        attribs_str = ""
        if self.start:
            self.attribs["start"] = self.start
        for attr in self.attribs:
            attribs_str += f' {attr}="{self.attribs[attr]}"'
        if self.ordered:
            tag = "OL"
        else:
            tag = "UL"
        result = f"<{tag}{attribs_str}>\n"
        for line in self.lines:
            result += f" <LI>{str(line)}\n"
        result += f"</{tag}>\n"
        return result


# class Link (object):
# """
# a Link object is used to create link in HTML. (<a> tag)
#
# Attributes:
# - text: str, text of the link
# - url: str, URL of the link
# - attribs: dict, additional attributes for the A tag
#
# Reference: http://www.w3.org/TR/html4
# """
#
# def __init__(self, text, url=None, attribs=None):
#         """Link constructor"""
#         self.text = text
#         self.url = url
# if attribs:
#             self.attribs = attribs
# else:
#             self.attribs = {}
#
# def __str__(self):
#         """return the HTML code for the link as a string"""
#         attribs_str = ""
#         if self.url:  self.attribs['href'] = self.url
# for attr in self.attribs:
#             attribs_str += ' %s="%s"' % (attr, self.attribs[attr])
# return '<a%s>%s</a>' % (attribs_str, text)


# == FUNCTIONS ================================================================


# much simpler definition of a link as a function:
def Link(text, url):
    """create an reference link."""
    return f'<a href="{url}">{text}</a>'


def link(text, url):
    """create an reference link."""
    return f'<a href="{url}">{text}</a>'


def table(*args, **kwargs):
    """return HTML code for a table as a string.

    See Table class for parameters.
    """
    return str(Table(*args, **kwargs))


def list(*args, **kwargs):
    """return HTML code for a list as a string.

    See List class for parameters.
    """
    return str(List(*args, **kwargs))


# == MAIN =====================================================================

# Show sample usage when this file is launched as a script.

if __name__ == "__main__":
    # open an HTML file to show output in a browser
    f = open("test.html", "w")

    t = Table()
    t.rows.append(TableRow(["A", "B", "C"], header=True))
    t.rows.append(TableRow(["D", "E", "F"]))
    t.rows.append(("i", "j", "k"))
    f.write(str(t) + "<p>\n")
    print(str(t))
    print("-" * 79)

    t2 = Table([("1", "2"), ["3", "4"]], width="100%", header_row=("col1", "col2"), col_width=("", "75%"))
    f.write(str(t2) + "<p>\n")
    print(t2)
    print("-" * 79)

    t2.rows.append(["5", "6"])
    t2.rows[1][1] = TableCell("new", bgcolor="red")
    t2.rows.append(TableRow(["7", "8"], attribs={"align": "center"}))
    f.write(str(t2) + "<p>\n")
    print(t2)
    print("-" * 79)

    # sample table with column attributes and styles:
    table_data = [["Smith", "John", 30, 4.5], ["Carpenter", "Jack", 47, 7], ["Johnson", "Paul", 62, 10.55]]
    htmlcode = table(
        table_data,
        header_row=["Last name", "First name", "Age", "Score"],
        col_width=["", "20%", "10%", "10%"],
        col_align=["left", "center", "right", "char"],
        col_styles=["font-size: large", "", "font-size: small", "background-color:yellow"],
    )
    f.write(htmlcode + "<p>\n")
    print(htmlcode)
    print("-" * 79)

    def gen_table_squares(n):
        """Generator to create table rows for integers from 1 to n."""
        # First, header row:
        # yield TableRow(('x', 'square(x)'), header=True, bgcolor='blue')
        # Then all rows:
        for x in range(1, n + 1):
            yield (x, x * x)

    t = Table(rows=gen_table_squares(10), header_row=("x", "square(x)"))
    f.write(str(t) + "<p>\n")

    print("-" * 79)
    li = List(["aaa", "bbb", "ccc"])
    f.write(str(li) + "<p>\n")
    li.ordered = True
    f.write(str(li) + "<p>\n")
    li.start = 10
    f.write(str(li) + "<p>\n")

    f.close()
