"""
MoinMoin - IndentTable.py processor

Processor for turning indented lists of data into tables.

With classical MoinMoin tables syntax:
* the source can become pretty unreadable when cells contents grow
* it is difficult to handle multiple lines
* it is impossible to have lists (other than by including a page
  in a cell)

This processor is meant to correct the above problems, by the
means of an indented list:
* each cell consists of one line
* its indent determines its position in the table
* one line can be continued on the next line by means of '\' at the
  end; this helps keeping long contents readable
* macros accepted in regular tables are accepted too, e.g [[BR]]
* all regular table formatting (formats in <>'s, cell spanning)
  are supported
* optionally, the cell contents can be parsed, allowing to have
  any kind of lists, headings, etc.

The price to pay for the increased power and readability of the
source, versus the regular table syntax, is the loss of the 2D
matrix layout.

@copyright: Pascal Bauermeister <pascal DOT bauermeister AT hispeed DOT ch>
@license: GPL

Updates:
* [v0.0.1] Pascal - Thu Jul 29 15:41:25 CEST 2004
  - initial release

-------------------------------------------------------------------------------

Usage:
{{{#!IndentTable OPTIONS
indented data
}}}

Options:
  debug         insert debug info in the output
  debug=full    insert more debug info in the output
  +, extended   parse each cell content (useful for lists and headings)
  -             force row mode
  |             force column mode

-------------------------------------------------------------------------------

Samples:

{{{#!IndentTable
1                               1
2                       ==>     2
3                               3
4                               4
}}}

{{{#!IndentTable
1
 2                      ==>     1 2 3 4
  3
   4
}}}

{{{#!IndentTable
1
 2                      ==>     1 2 3
  3                                 4
  4
}}}

{{{#!IndentTable -
1
 2                      ==>     1 2 3
  3                             4
  4
}}}

{{{#!IndentTable
1
 2                      ==>     1 2 3
  3                             4
 4
}}}

{{{#!IndentTable
1
 2                      ==>     1 2
3                               3 4
 4
}}}

{{{#!IndentTable
1
2                       ==>     1 3 4
 3                              2
  4
}}}

{{{#!IndentTable -
1
2                       ==>     1
 3                              2 3 4
  4
}}}

{{{#!IndentTable
1
2                       ==>     1 3 5
 3                              2 4 6
 4
  5
  6
}}}

{{{ #!IndentTable +
<width='30%'>1
 <width='30%'>2
  <width='30%'>3
4                       ==>     1            2            3
 <|2(>== Cool ! ==\
 [[SystemInfo]]\                4  == Cool! ==            6
 Terrific, isn't it ?              <system>     
  6                             7  <information>          9
7                                  Terrific, isn't it ?
  9                             10           11           12
10
 11
  12

}}

{{{#!IndentTable +
  A1                    ==> +----+-----------------+------------+--+
    B1                      |A1  |B1               |    C1+D1   |  |
      ||C1+D1               +----+-----------------+------------+--+
  A2                        |    |                 |C2: Bullets:|  |
    B2                      |    |                 |  * bullet 1|D2|
      C2: Bullets: \        |A2  |B2               |  * bullet 2|  |
        * bullet 1 \        |    |                 |end of cell |  |
        * bullet 2 \        +----+-----------------+------------+--+
      end of cell           |A3  | a. (B3) numberes|C3          |  |
        D2                  |    | b. numbered item|            |  |
  A3                        +----+-----------------+------------+--+
    a. (B3) numbers \       |(A4)|B4               |            |  |
    a. numbered item        +----+-----------------+------------+--+
      C3
  '''''' (A4)
    B4
## You find this list unreadable ?  try to do the same with just ||'s !
}}}
"""

from MoinMoin.parser import wiki
import cStringIO, re, string, random

LETTERS = string.ascii_lowercase
LETTERS_LEN = len (LETTERS)

LIST_HEADERS = ["*", "1.", "a.", "A.", "i.", "I."]

COL_FMT_RE1 = re.compile("\\|*<[^>]*>")
COL_FMT_RE2 = re.compile("\\|*") # FIXME: unify these two regexes

def process (request, formatter, lines):

    # default options values
    opt_dbg = False
    opt_dbgf = False # verbose debug
    opt_ext = False
    opt_row = False
    opt_col = False # not sure this is really useful...
    substitute_content = True

    # parse bangpath for arguments
    bang = lines [0]
    for arg in bang.split () [1:]:
        if   arg=="debug":      opt_dbg = True
        if   arg=="debug=full": opt_dbgf = True
        elif arg=="extended":   opt_ext = True
        elif arg=="+":          opt_ext = True
        elif arg=="-":          opt_row = True
        elif arg=="|":          opt_col = True

    # remove bang path
    del lines [0]

    #
    # collect src lines
    #
    lines_info = []
    line_buf = ""
    last_indent = -1
    nb_indent_eq, nb_indent_dec = 0, 0
    for line in lines:
        # skip comments
        if line.lstrip ().startswith ("##"): continue

        # handle unterminated lines
        if line.strip ().endswith ("\\"):
            line_buf = line_buf + line.rstrip ('\\ ')
            if opt_ext: line_buf = line_buf + "\n"
            continue # continue, to finish line
        
        # append current line to any previously unterminated line
        else: line_buf = line_buf + line

        # skip empty lines
        if len (line_buf.strip ()) == 0: continue

        # calculate indent
        lline = line_buf.lstrip ()
        cur_indent = len (line_buf) - len (lline)
        if cur_indent == last_indent: nb_indent_eq = nb_indent_eq + 1
        if cur_indent < last_indent:  nb_indent_dec = nb_indent_dec + 1

        # detect table formatting
        m = COL_FMT_RE1.match (lline) or COL_FMT_RE2.match (lline)
        if m and m.start() == 0:
            fmt = lline [:m.end ()]
            data = lline [m.end():].strip ()
        else:
            fmt = ""
            data = line_buf.strip ()

        # in extended mode, adjust leading spaces of data lines so
        # that the first data line has none, and all other data lines
        # are aligned relatively to the first one; for lists, preserve
        # one leading space
        if opt_ext:
            start = cur_indent # number of unwanted leading spaces
            for s in ["*", "1.", "a.", "A.", "i.", "I."]:
                if data.startswith (s): start = start -1 # preserve 1 space
            data = " "*cur_indent+data # 'unstrip' the 1st line (w/o tbl fmt)
            data_lines = data.split ("\n")
            for i in range (len (data_lines)):
                data_lines [i] = data_lines [i] [start:] # del unwanted spaces
            data = ("\n").join (data_lines)
            

        # store cell
        lines_info.append ( (cur_indent, fmt, data) )

        # ready for next line
        line_buf = ""
        last_indent = cur_indent

    #
    # generate table structure
    #
    table_fmt_buf = ""

    # decide whether row or column-oriented
    is_by_col = nb_indent_dec==0 and nb_indent_eq > 0
    if opt_col: is_by_col = True
    if opt_row: is_by_col = False

    # generate a token base that does not occur in the source, and
    # that is MoinMoin neutral, and not an HTML sequence
    token_base = "token"
    src = "\n".join (lines)
    while src.find (token_base) >=0:
        # append some random letter
        token_base = token_base + LETTERS [random.randint (0, LETTERS_LEN-1)]
    
    # function to generate tokens
    mk_token = lambda i: "%s%i" % (token_base, i)

    # function to generate a cell, either with a token, or with raw
    # content, depending on whether we must interpret the content
    if opt_ext: mk_cell = lambda fmt, i, data: "||%s %s " % (fmt, mk_token (i))
    else:       mk_cell = lambda fmt, i, data: "||%s %s " % (fmt, data)

    # row-oriented structure:
    #  the table flow is the same as regular MoinMoin tables, all we
    #  have to do is detect the end of rows and generate end of lines
    if not is_by_col:
        indent = 0
        line_index = 0
        if not opt_ext: substitute_content = False
        for cur_indent, fmt, line in lines_info:
            # same or lower indent ?  ==> new row: close previous and start new
            if cur_indent <= indent and len (table_fmt_buf):
                table_fmt_buf = table_fmt_buf +"||\n"

            # add cell
            table_fmt_buf = table_fmt_buf + mk_cell (fmt, line_index, line)

            indent = cur_indent
            line_index = line_index + 1

        # close table
        if len (table_fmt_buf): table_fmt_buf = table_fmt_buf + "||"

    # column-oriented structure:
    #  a bit more complicated; the cells must be reordered; we first
    #  determine the coordinates of data and store them in a (pseudo)
    #  table; then we generate the table structure, picking the right
    #  data
    else:
        # determine coordinates and store data
        indent = -1
        col, row = 0, 0
        max_col, max_row = 0, 0 # will be needed to generate the table
        table = {}
        for index in range (len (lines_info)):
            cur_indent = lines_info [index] [0]
            if cur_indent == indent:
                # new row
                row = row + 1
                if row > max_row: max_row = row
            else:
                # new column
                row = 1
                col = col + 1
                if col > max_col: max_col = col
                indent = cur_indent
            # store coordinates and data index
            table [col-1,row-1] = index

        # generate table
        for row in range (max_row):
            for col in range (max_col):
                if table.has_key ((col,row)):
                    index = table [col,row]
                    fmt, line = lines_info [index] [1:]
                    table_fmt_buf = table_fmt_buf + mk_cell (fmt, index, line)
                else:
                    table_fmt_buf = table_fmt_buf + "|| " # empty cell
            table_fmt_buf = table_fmt_buf +"||\n"                

    #
    # final table generation
    #

    # emit debug
    if opt_dbg or opt_dbgf:
        if opt_dbgf:
            if substitute_content:
                data = "\nData:\n"
                for i in range (len (lines_info)):
                    line = lines_info [i] [2]
                    data = data + "%d: [%s]\n" % (i, line)
            else: data = ""
            output = "{{{\nSource:\n{{ %s\n%s\n}}\n" \
                     "\nTable:\n%s\n" \
                     "%s}}}" % \
                     (bang, "\n".join (lines), table_fmt_buf, data)

        else: output = "{{{\n%s\n}}}" % "\n".join (lines)
            
        parser = wiki.Parser (output, request)
        parser.format (formatter)
        
    # generate html for table structure, generate each cell, then
    # merge them
    if substitute_content:
        # gen table struct
        html_buf = format (table_fmt_buf, request, formatter)
        # gen cells contents and merge in table
        for i in range (len (lines_info)):
            line = lines_info [i] [2]
            token = mk_token (i)
            content = format (line, request, formatter)
            html_buf = html_buf.replace (token, content, 1)
        # proudly emit the result
        request.write(html_buf) # emit html-formatted content

    # we have the table in MoinMoin source format, just HTML-convert it
    else:
        output = "%s\n" % table_fmt_buf
        parser = wiki.Parser (output, request)
        parser.format (formatter)

    # done!
    return


def format (src_text, request, formatter):
    # parse the text (in wiki source format) and make HTML,
    # after diverting sys.stdout to a string
    str_out = cStringIO.StringIO ()     # create str to collect output
    request.redirect (str_out)          # divert output to that string
    # parse this line
    wiki.Parser (src_text, request).format (formatter)
    request.redirect ()                 # restore output
    return str_out.getvalue ().strip () # return what was generated

