"""
MoinMoin - IndentTable.py processor

Processor for turning indented lists of data into tables.
	$Id: IndentTable.py,v 1.8 2004/12/19 19:36:22 nigel Exp $
        
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
* [v0.0.2] NigelMetheringham - Sun Dec 19 19:05:21 +0000 2004
  - minimal rework for MoinMoin 1.3.x
  - change of option parse means some option forms have changed
  - this needs further work

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

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

Options:
  debug=on      insert debug info in the output
  debug=full    insert more debug info in the output
  extended=on   parse each cell content (useful for lists and headings)
  row=on        force row mode
  column=on     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 row=on
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 row=on
1
2                       ==>     1
 3                              2 3 4
  4
}}}

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

{{{ #!IndentTable extended=on
<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 extended=on
  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 import wikiutil
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

class Parser:
    """ Parser for turning indented lists of data into tables. """

    def __init__(self, raw, request, **kw):
        # save call arguments for later use in format
        self.lines = raw.split("\n");
        self.request = request
        self.form = request.form
        self._ = request.getText
        self.out = kw.get('out', request)
        # default options values
        self.opt_dbg = False
        self.opt_dbgf = False # verbose debug
        self.opt_ext = False
        self.opt_row = False
        self.opt_col = False # not sure this is really useful...
        # deal with arguments
        attrs, msg = wikiutil.parseAttributes(request,
                                              kw.get('format_args',''))
        if not msg:
            if attrs.get('debug','"off"')[1:-1].lower() in ('on', 'true', 'yes'):
                self.opt_dbg = True
            if attrs.get('debug','"off"')[1:-1].lower() in ('full'):
                self.opt_dbgf = True
            if attrs.get('extended','"off"')[1:-1].lower() in ('on', 'true', 'yes'):
                self.opt_ext = True
            if attrs.get('row','"off"')[1:-1].lower() in ('on', 'true', 'yes'):
                self.opt_row = True
            if attrs.get('column','"off"')[1:-1].lower() in ('on', 'true', 'yes'):
                self.opt_col = True


    def format (self, formatter):
        substitute_content = True

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

            # handle unterminated lines
            if line.strip ().endswith ("\\"):
                line_buf = line_buf + line.rstrip ('\\ ')
                if self.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 self.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 self.opt_col: is_by_col = True
        if self.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 (self.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 self.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 self.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 self.opt_dbg or self.opt_dbgf:
            if self.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}}\n" \
                         "\nTable:\n%s\n" \
                         "%s}}}" % \
                         ("\n".join (self.lines), table_fmt_buf, data)

            else: output = "{{{\n%s\n}}}" % "\n".join (self.lines)

            parser = wiki.Parser (output, self.request)
            parser.format (formatter)

        # generate html for table structure, generate each cell, then
        # merge them
        if substitute_content:
            # gen table struct
            html_buf = self.reformat (table_fmt_buf, self.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 = self.reformat (line, self.request, formatter)
                html_buf = html_buf.replace (token, content, 1)
            # proudly emit the result
            self.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, self.request)
            parser.format (formatter)

        # done!
        return


    def reformat (self, 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