1#!/usr/bin/env python
2
3"""
4C.10.2 The array and tabular Environments
5
6"""
7
8import sys
9from plasTeX import Macro, Environment, Command, DimenCommand
10from plasTeX import sourceChildren, sourceArguments
11
12class ColumnType(Macro):
13
14    columnAttributes = {}
15    columnTypes = {}
16
17    def __init__(self, *args, **kwargs):
18        Macro.__init__(self, *args, **kwargs)
19        self.style.update(self.columnAttributes)
20
21    @classmethod
22    def new(cls, name, attributes, args='',
23            before=None, after=None, between=None):
24        """
25        Generate a new column type definition
26
27        Required Arguments:
28        name -- name of the column type
29        attributes -- dictionary of style attributes for this column
30
31        Keyword Arguments:
32        args -- argument description string
33        before -- tokens to insert before this column
34        after -- tokens to insert after this column
35
36        """
37        newclass = type(name, (cls,),
38            {'columnAttributes':attributes, 'args':args,
39             'before': before or [],
40             'after': after or [],
41             'between': between or []})
42        cls.columnTypes[name] = newclass
43
44    def __repr__(self):
45        return '%s: %s' % (type(self).__name__, self.style)
46
47ColumnType.new('r', {'text-align':'right'})
48ColumnType.new('R', {'text-align':'right'})
49ColumnType.new('c', {'text-align':'center'})
50ColumnType.new('C', {'text-align':'center'})
51ColumnType.new('l', {'text-align':'left'})
52ColumnType.new('L', {'text-align':'left'})
53ColumnType.new('J', {'text-align':'left'})
54ColumnType.new('X', {'text-align':'left'})
55ColumnType.new('p', {'text-align':'left'}, args='width:str')
56ColumnType.new('d', {'text-align':'right'}, args='delim:str')
57
58
59class Array(Environment):
60    """
61    Base class for all array-like structures
62
63    """
64
65    colspec = None
66    blockType = True
67    captionable = True
68
69    class caption(Command):
70        """ Table caption """
71        args = '* [ toc ] self'
72        labelable = True
73        counter = 'table'
74        blockType = True
75        def invoke(self, tex):
76            res = Command.invoke(self, tex)
77            self.title = self.captionName
78            return res
79
80    class CellDelimiter(Command):
81        """ Cell delimiter """
82        macroName = 'active::&'
83        def invoke(self, tex):
84            # Pop and push a new context for each cell, this keeps
85            # any formatting changes from the previous cell from
86            # leaking over into the next cell
87            self.ownerDocument.context.pop()
88            self.ownerDocument.context.push()
89            # Add a phantom cell to absorb the appropriate tokens
90            return [self, self.ownerDocument.createElement('ArrayCell')]
91
92    class EndRow(Command):
93        """ End of a row """
94        macroName = '\\'
95        args = '* [ space ]'
96
97        def invoke(self, tex):
98            # Pop and push a new context for each row, this keeps
99            # any formatting changes from the previous row from
100            # leaking over into the next row
101            self.ownerDocument.context.pop()
102            self.parse(tex)
103            self.ownerDocument.context.push()
104            # Add a phantom row and cell to absorb the appropriate tokens
105            return [self, self.ownerDocument.createElement('ArrayRow'),
106                          self.ownerDocument.createElement('ArrayCell')]
107
108    class cr(EndRow):
109        macroName = None
110        args = ''
111
112    class tabularnewline(EndRow):
113        macroName = None
114        args = ''
115
116    class BorderCommand(Command):
117        """
118        Base class for border commands
119
120        """
121        BORDER_BEFORE = 0
122        BORDER_AFTER  = 1
123
124        position = BORDER_BEFORE
125
126        def applyBorders(self, cells, location=None):
127            """
128            Apply borders to the given cells
129
130            Required Arguments:
131            location -- place where the border should be applied.
132                This should be 'top', 'bottom', 'left', or 'right'
133            cells -- iterable containing cell instances to apply
134                the borders
135
136            """
137            # Find out if the border should start and stop, or just
138            # span the whole table.
139            a = self.attributes
140            if a and 'span' in list(a.keys()):
141                try: start, end = a['span']
142                except TypeError: start = end = a['span']
143            else:
144                start = -sys.maxsize
145                end = sys.maxsize
146            # Determine the position of the border
147            if location is None:
148                location = self.locations[self.position]
149            colnum = 1
150            for cell in cells:
151                if colnum < start or colnum > end:
152                    colnum += 1
153                    continue
154                cell.style['border-%s-style' % location] = 'solid'
155                cell.style['border-%s-color' % location] = 'black'
156                cell.style['border-%s-width' % location] = '1px'
157                if cell.attributes:
158                    colnum += cell.attributes.get('colspan', 1)
159                else:
160                    colnum += 1
161
162    class hline(BorderCommand):
163        """ Full horizontal line """
164        locations = ('top','bottom')
165
166    class vline(BorderCommand):
167        """ Vertical line """
168        locations = ('left','right')
169
170    #
171    # booktabs commands
172    #
173
174    class cline(hline):
175        """ Partial horizontal line """
176        args = 'span:list(-):int'
177
178    class _rule(hline):
179        """ Full horizontal line """
180        args = '[ width:str ]'
181
182    class toprule(_rule):
183        pass
184
185    class midrule(_rule):
186        pass
187
188    class bottomrule(_rule):
189        pass
190
191    class cmidrule(cline):
192        args = '[ width:str ] ( trim:str ) span:list(-):int'
193
194    class morecmidrules(Command):
195        pass
196
197    class addlinespace(Command):
198        args = '[ width:str ]'
199
200    class specialrule(Command):
201        args = 'width:str above:str below:str'
202
203    # end booktabs
204
205    class ArrayRow(Macro):
206        """ Table row class """
207        endToken = None
208
209        def digest(self, tokens):
210            # Absorb tokens until the end of the row
211            self.endToken = self.digestUntil(tokens, Array.EndRow)
212            if self.endToken is not None:
213                next(tokens)
214                self.endToken.digest(tokens)
215
216        @property
217        def source(self):
218            """
219            This source property is a little different than most.
220            Instead of printing just the source of the row, it prints
221            out the entire environment with just this row as its content.
222            This allows renderers to render images for arrays a row
223            at a time.
224
225            """
226            name = self.parentNode.nodeName or 'array'
227            escape = '\\'
228            s = []
229            argSource = sourceArguments(self.parentNode)
230            if not argSource:
231                argSource = ' '
232            s.append('%sbegin{%s}%s' % (escape, name, argSource))
233            for cell in self:
234                s.append(sourceChildren(cell, par=not(self.parentNode.mathMode)))
235                if cell.endToken is not None:
236                    s.append(cell.endToken.source)
237            if self.endToken is not None:
238                s.append(self.endToken.source)
239            s.append('%send{%s}' % (escape, name))
240            return ''.join(s)
241
242        def applyBorders(self, tocells=None, location=None):
243            """
244            Apply borders to every cell in the row
245
246            Keyword Arguments:
247            row -- the row of cells to apply borders to.  If none
248               is given, then use the current row
249
250            """
251            if tocells is None:
252                tocells = self
253            for cell in self:
254                horiz, vert = cell.borders
255                # Horizontal borders go across all columns
256                for border in horiz:
257                    border.applyBorders(tocells, location=location)
258                # Vertical borders only get applied to the same column
259                for applyto in tocells:
260                    for border in vert:
261                        border.applyBorders([applyto], location=location)
262
263        @property
264        def isBorderOnly(self):
265            """ Does this row exist only for applying borders? """
266            for cell in self:
267                if not cell.isBorderOnly:
268                    return False
269            return True
270
271    class ArrayCell(Macro):
272        """ Table cell class """
273        endToken = None
274        isHeader = False
275
276        def digest(self, tokens):
277            self.endToken = self.digestUntil(tokens, (Array.CellDelimiter,
278                                                      Array.EndRow))
279            if isinstance(self.endToken, Array.CellDelimiter):
280                next(tokens)
281                self.endToken.digest(tokens)
282            else:
283                self.endToken = None
284
285            # Check for multicols
286            hasmulticol = False
287            for item in self:
288                if item.attributes and 'colspan' in list(item.attributes.keys()):
289                    self.attributes['colspan'] = item.attributes['colspan']
290                if hasattr(item, 'colspec') and not isinstance(item, Array):
291                    self.colspec = item.colspec
292                if hasattr(item, 'isHeader'):
293                    self.isHeader = item.isHeader
294
295            # Cache the border information.  This must be done before
296            # grouping paragraphs since a paragraph might swallow
297            # an hline/vline/cline command.
298            h,v = self.borders
299
300            # Throw out the border commands, we're done with them
301#           for i in range(len(self)-1, -1, -1):
302#               if isinstance(self[i], Array.BorderCommand):
303#                   self.pop(i)
304
305            self.paragraphs()
306
307        @property
308        def borders(self):
309            """
310            Return all of the border control macros
311
312            Returns:
313            list of border command instances
314
315            """
316            # Use cached version if it exists
317            if hasattr(self, '@borders'):
318                return getattr(self, '@borders')
319
320            horiz, vert = [], []
321
322            # Locate the border control macros at the end of the cell
323            for i in range(len(self)-1, -1, -1):
324                item = self[i]
325                if item.isElementContentWhitespace:
326                    continue
327                if isinstance(item, Array.hline):
328                    item.position = Array.hline.BORDER_AFTER
329                    horiz.append(item)
330                    continue
331                elif isinstance(item, Array.vline):
332                    item.position = Array.vline.BORDER_AFTER
333                    vert.append(item)
334                    continue
335                break
336
337            # Locate border control macros at the beginning of the cell
338            for item in self:
339                if item.isElementContentWhitespace:
340                    continue
341                if isinstance(item, Array.hline):
342                    item.position = Array.hline.BORDER_BEFORE
343                    horiz.append(item)
344                    continue
345                elif isinstance(item, Array.vline):
346                    item.position = Array.vline.BORDER_BEFORE
347                    vert.append(item)
348                    continue
349                break
350
351            setattr(self, '@borders', (horiz, vert))
352
353            return horiz, vert
354
355
356        @property
357        def isBorderOnly(self):
358            """ Does this cell exist only for applying borders? """
359            for par in self:
360                for item in par:
361                    if item.isElementContentWhitespace:
362                        continue
363                    elif isinstance(item, Array.BorderCommand):
364                        continue
365                    return False
366            return True
367
368
369        @property
370        def source(self):
371            # Don't put paragraphs into math mode arrays
372            if self.parentNode is None:
373               # no parentNode, assume mathMode==False
374               return sourceChildren(self, True)
375            return sourceChildren(self,
376                       par=not(self.parentNode.parentNode.mathMode))
377
378
379
380    class multicolumn(Command):
381        """ Column spanning cell """
382        args = 'colspan:int colspec:nox self'
383        isHeader = False
384
385        def invoke(self, tex):
386            Command.invoke(self, tex)
387            self.colspec = Array.compileColspec(tex, self.attributes['colspec']).pop(0)
388
389        def digest(self, tokens):
390            Command.digest(self, tokens)
391            #self.paragraphs()
392
393
394    def invoke(self, tex):
395        if self.macroMode == Macro.MODE_END:
396            self.ownerDocument.context.pop(self) # End of table, row, and cell
397            return
398
399        Environment.invoke(self, tex)
400
401#!!!
402#
403# Need to handle colspec processing here so that tokens that must
404# be inserted before and after columns are known
405#
406#!!!
407        if 'colspec' in list(self.attributes.keys()):
408            self.colspec = Array.compileColspec(tex, self.attributes['colspec'])
409
410        self.ownerDocument.context.push() # Beginning of cell
411        # Add a phantom row and cell to absorb the appropriate tokens
412        return [self, self.ownerDocument.createElement('ArrayRow'),
413                      self.ownerDocument.createElement('ArrayCell')]
414
415    def digest(self, tokens):
416        Environment.digest(self, tokens)
417
418        # Give subclasses a hook before going on
419        self.processRows()
420
421        self.applyBorders()
422
423        self.linkCells()
424
425    def processRows(self):
426        """
427        Subcloss hook to process rows after digest
428
429        Tables are fairly complex structures, so subclassing them
430        in a useful way can be difficult.  This method was added
431        simply to allow subclasses to have access to the content of a
432        table immediately after the digest method.
433
434        """
435
436    def linkCells(self):
437        """
438        Add attributes to spanning cells to indicate their start and end points
439
440        This information is added mainly for DocBook's table model.
441        It does spans by indicating the starting and ending points within
442        the table rather than just saying how many columns are spanned.
443
444        """
445        # Link cells to colspec
446        if self.colspec:
447            for r, row in enumerate(self):
448                for c, cell in enumerate(row):
449                    colspan = cell.attributes.get('colspan', 0)
450                    if colspan > 1:
451                        try:
452                            cell.colspecStart = self.colspec[c]
453                            cell.colspecEnd = self.colspec[c+colspan-1]
454                        except IndexError:
455                            if hasattr(cell, 'colspecStart'):
456                                del cell.colspecStart
457                            if hasattr(cell, 'colspecEnd'):
458                                del cell.colspecEnd
459
460        # Determine the number of rows by counting cells
461        if self:
462            cols = []
463            for row in self:
464                numcols = 0
465                for cell in row:
466                    numcols += cell.attributes.get('colspan', 1)
467                cols.append(numcols)
468            self.numCols = max(cols)
469
470    def applyBorders(self):
471        """
472        Apply borders from \\(h|c|v)line and colspecs
473
474        """
475        lastrow = len(self) - 1
476        emptyrows = []
477        prev = None
478        for i, row in enumerate(self):
479            if not isinstance(row, Array.ArrayRow):
480                continue
481            # If the row is only here to apply borders, apply the
482            # borders to the adjacent row.  Empty rows are deleted later.
483            if row.isBorderOnly:
484                if i == 0 and lastrow:
485                    row.applyBorders(self[1], 'top')
486                elif prev is not None:
487                    row.applyBorders(prev, 'bottom')
488                emptyrows.insert(0, i)
489            else:
490                row.applyBorders()
491                if self.colspec:
492                    # Expand multicolumns so that they don't mess up
493                    # the colspec attributes
494                    cells = []
495                    for cell in row:
496                        span = 1
497                        if cell.attributes:
498                            span = cell.attributes.get('colspan', 1)
499                        cells += [cell] * span
500                    for spec, cell in zip(self.colspec, cells):
501                        spec = getattr(cell, 'colspec', spec)
502                        cell.style.update(spec.style)
503                prev = row
504
505        # Pop empty rows
506        for i in emptyrows:
507            self.pop(i)
508
509    @classmethod
510    def compileColspec(cls, tex, colspec):
511        """
512        Compile colspec into an object
513
514        Required Arguments:
515        colspec -- an unexpanded token list that contains a LaTeX colspec
516
517        Returns:
518        list of `ColumnType` instances
519
520        """
521        output = []
522        colspec = iter(colspec)
523        before = None
524        leftborder = None
525
526        tex.pushToken(Array)
527        tex.pushTokens(colspec)
528
529        for tok in tex.itertokens():
530            if tok is Array:
531                break
532
533            if tok.isElementContentWhitespace:
534                continue
535
536            if tok == '|':
537                if not output:
538                    leftborder = True
539                else:
540                    output[-1].style['border-right'] = '1px solid black'
541                continue
542
543            if tok == '>':
544                before = tex.readArgument()
545                continue
546
547            if tok == '<':
548                output[-1].after = tex.readArgument()
549                continue
550
551            if tok == '@':
552                if output:
553                    output[-1].between = tex.readArgument()
554                continue
555
556            if tok == '*':
557                num = tex.readArgument(type=int, expanded=True)
558                spec = tex.readArgument()
559                for i in range(num):
560                    tex.pushTokens(spec)
561                continue
562
563            output.append(ColumnType.columnTypes.get(tok, ColumnType)())
564
565            if tok.lower() in ['p','d']:
566                tex.readArgument()
567
568            if before:
569                output[-1].before = before
570                before = None
571
572        if leftborder:
573            output[0].style['border-left'] = '1px solid black'
574
575        return output
576
577    @property
578    def source(self):
579        """
580        This source property is a little different than most.
581        Instead of calling the source property of the child nodes,
582        it walks through the rows and cells manually.  It does
583        this because rows and cells have special source properties
584        as well that don't return the correct markup for inserting
585        into this source property.
586
587        """
588        name = self.nodeName
589        escape = '\\'
590        # \begin environment
591        # If self.childNodes is not empty, print out the entire environment
592        if self.macroMode == Macro.MODE_BEGIN:
593            s = []
594            argSource = sourceArguments(self)
595            if not argSource:
596                argSource = ' '
597            s.append('%sbegin{%s}%s' % (escape, name, argSource))
598            if self.hasChildNodes():
599                for row in self:
600                    for cell in row:
601                        s.append(sourceChildren(cell, par=not(self.mathMode)))
602                        if cell.endToken is not None:
603                            s.append(cell.endToken.source)
604                    if row.endToken is not None:
605                        s.append(row.endToken.source)
606                s.append('%send{%s}' % (escape, name))
607            return ''.join(s)
608
609        # \end environment
610        if self.macroMode == Macro.MODE_END:
611            return '%send{%s}' % (escape, name)
612
613class array(Array):
614    args = '[ pos:str ] colspec:nox'
615    mathMode = True
616    class nonumber(Command):
617        pass
618
619class tabular(Array):
620    args = '[ pos:str ] colspec:nox'
621
622class TabularStar(tabular):
623    macroName = 'tabular*'
624    args = 'width:dimen [ pos:str ] colspec:nox'
625
626class tabularx(Array):
627    args = 'width:nox colspec:nox'
628
629class tabulary(Array):
630    args = 'width:nox colspec:nox'
631
632# Style Parameters
633
634class arraycolsep(DimenCommand):
635    value = DimenCommand.new(0)
636
637class tabcolsep(DimenCommand):
638    value = DimenCommand.new(0)
639
640class arrayrulewidth(DimenCommand):
641    value = DimenCommand.new(0)
642
643class doublerulesep(DimenCommand):
644    value = DimenCommand.new(0)
645
646class arraystretch(Command):
647    str = '1'
648