1"""Table-related formatting functions.
2
3This module contains functions called from measurers.py to format tables."""
4import sys, mathnode
5
6def getByIndexOrLast(lst, idx):
7    if idx < len(lst): return lst[idx]
8    else: return lst[-1]
9
10class CellDescriptor:
11    """Descriptor of a single cell in a table"""
12    def __init__(self, content, halign, valign, colspan, rowspan):
13        self.content = content
14        self.halign = halign
15        self.valign = valign
16        self.colspan = colspan
17        self.rowspan = rowspan
18
19class ColumnDescriptor:
20    """Descriptor of a single column in a table"""
21    def __init__(self):
22        self.auto = True
23        self.fit = False
24        self.width = 0
25        self.spaceAfter = 0
26        self.lineAfter = None
27
28class RowDescriptor:
29    """Descriptor of a single row in a table; contains cells"""
30    def __init__(self, node, cells, rowalign, columnaligns, busycells):
31        self.alignToAxis = (rowalign == u"axis")
32        self.height = 0
33        self.depth  = 0
34        self.spaceAfter = 0
35        self.lineAfter = None
36        self.cells = []
37        for c in cells:
38            # Find the first free cell
39            while len(busycells) > len(self.cells) and busycells[len(self.cells)] > 0:
40                self.cells.append(None)
41
42            halign = getByIndexOrLast(columnaligns, len(self.cells))
43            valign = rowalign
44            colspan = 1
45            rowspan = 1
46
47            if c.elementName == u"mtd":
48                halign = c.attributes.get(u"columnalign", halign)
49                valign = c.attributes.get(u"rowalign", valign)
50                colspan = node.parseInt(c.attributes.get(u"colspan", u"1"))
51                rowspan = node.parseInt(c.attributes.get(u"rowspan", u"1"))
52
53            while len(self.cells) >= len(node.columns):
54                node.columns.append(ColumnDescriptor())
55            self.cells.append(CellDescriptor(c, halign, valign, colspan, rowspan))
56
57            for i in range (1, colspan): self.cells.append(None)
58            while len(self.cells) > len(node.columns):
59                node.columns.append(ColumnDescriptor())
60
61def arrangeCells(node):
62    node.rows = []
63    node.columns = []
64    busycells = []
65
66    # Read table-level alignment properties
67    table_rowaligns = node.getListProperty(u"rowalign")
68    table_columnaligns = node.getListProperty(u"columnalign")
69
70    for ch in node.children:
71        rowalign = getByIndexOrLast(table_rowaligns, len(node.rows))
72        row_columnaligns = table_columnaligns
73        if ch.elementName == u"mtr" or ch.elementName == "mlabeledtr":
74            cells = ch.children
75            rowalign = ch.attributes.get(u"rowalign", rowalign)
76            if u"columnalign" in ch.attributes.keys():
77                columnaligns = node.getListProperty(u"columnalign", ch.attributes.get(u"columnalign"))
78        else:
79            cells = [ch]
80
81        row = RowDescriptor(node, cells, rowalign, row_columnaligns, busycells)
82        node.rows.append(row)
83        # busycells passes information about cells spanning multiple rows
84        busycells = [max (0, n - 1) for n in busycells]
85        while len(busycells) < len(row.cells): busycells.append(0)
86        for i in range (len(row.cells)):
87            cell = row.cells[i]
88            if cell is None: continue
89            if cell.rowspan > 1:
90                for j in range(i, i + cell.colspan):
91                    busycells[j] = cell.rowspan - 1
92
93    # Pad the table with empty rows until no spanning cell protrudes
94    while max(busycells) > 0:
95        rowalign = getByIndexOrLast(table_rowaligns, len(node.rows))
96        node.rows.append(RowDescriptor(node, [], rowalign, table_columnaligns, busycells))
97        busycells = [max (0, n - 1) for n in busycells]
98
99def arrangeLines(node):
100    # Get spacings and line styles; expand to cover the table fully
101    spacings = map(node.parseLength, node.getListProperty(u"rowspacing"))
102    lines = node.getListProperty(u"rowlines")
103
104    for i in range(len(node.rows) - 1):
105        node.rows[i].spaceAfter = getByIndexOrLast(spacings, i)
106        line = getByIndexOrLast(lines, i)
107        if line != u"none":
108           node.rows[i].lineAfter = line
109           node.rows[i].spaceAfter += node.lineWidth
110
111    spacings = map(node.parseSpace, node.getListProperty(u"columnspacing"))
112    lines = node.getListProperty(u"columnlines")
113
114    for i in range(len(node.columns) - 1):
115        node.columns[i].spaceAfter = getByIndexOrLast(spacings, i)
116        line = getByIndexOrLast(lines, i)
117        if line != u"none":
118           node.columns[i].lineAfter = line
119           node.columns[i].spaceAfter += node.lineWidth
120
121    node.framespacings = [0, 0]
122    node.framelines = [None, None]
123    spacings = map(node.parseSpace, node.getListProperty(u"framespacing"))
124    lines = node.getListProperty(u"frame")
125    for i in range(2):
126        line = getByIndexOrLast(lines, i)
127        if line != u"none":
128            node.framespacings[i] = getByIndexOrLast(spacings, i)
129            node.framelines[i] = line
130
131def calculateColumnWidths(node):
132    # Get total width
133    fullwidthattr = node.attributes.get(u"width", u"auto")
134    if fullwidthattr == u"auto":
135        fullwidth = None
136    else:
137        fullwidth = node.parseLength(fullwidthattr)
138        if fullwidth <= 0: fullwidth = None
139
140    # Fill fixed column widths
141    columnwidths = node.getListProperty(u"columnwidth")
142    for i in range(len(node.columns)):
143        column = node.columns[i]
144        attr = getByIndexOrLast(columnwidths, i)
145        if attr in [u"auto", u"fit"]:
146            column.fit = (attr == u"fit")
147        elif attr.endswith(u'%'):
148            if fullwidth is None:
149                node.error("Percents in column widths supported only in tables with explicit width; width of column %d treated as 'auto'" % (i+1))
150            else:
151                value = node.parseFloat(attr[:-1])
152                if value and value > 0:
153                    column.width = fullwidth * value / 100
154                    column.auto = False
155        else:
156            column.width = node.parseSpace(attr)
157            column.auto = False
158
159    # Set  initial auto widths for cells with colspan == 1
160    for r in node.rows:
161        for i in range(len(r.cells)):
162            c = r.cells[i]
163            if c is None or c.content is None or c.colspan > 1: continue
164            column = node.columns[i]
165            if column.auto: column.width = max(column.width, c.content.width)
166
167    # Calculate auto widths for cells with colspan > 1
168    while True:
169        adjustedColumns = []
170        adjustedWidth = 0
171
172        for r in node.rows:
173            for i in range(len(r.cells)):
174                c = r.cells[i]
175                if c is None or c.content is None or c.colspan == 1: continue
176
177                columns = node.columns[i : i + c.colspan]
178                autoColumns = [x for x in columns if x.auto]
179                if len(autoColumns) == 0: continue   # nothing to adjust
180                fixedColumns = [x for x in columns if not x.auto]
181
182                fixedWidth = sum([x.spaceAfter for x in columns[:-1]])
183                if len(fixedColumns) > 0:
184                    fixedWidth += sum ([x.width for x in fixedColumns])
185                autoWidth = sum ([x.width for x in autoColumns])
186                if c.content.width <= fixedWidth + autoWidth: continue # already fits
187
188                requiredWidth = c.content.width - fixedWidth
189                unitWidth = requiredWidth / len(autoColumns)
190
191                while True:
192                    oversizedColumns = [x for x in autoColumns if x.width >= unitWidth]
193                    if len(oversizedColumns) == 0: break
194
195                    autoColumns = [x for x in autoColumns if x.width < unitWidth]
196                    if len(autoColumns) == 0: break  # weird rounding effects
197                    requiredWidth -=  sum ([x.width for x in oversizedColumns])
198                    unitWidth = requiredWidth / len(autoColumns)
199                if len(autoColumns) == 0: continue; # protection against arithmetic overflow
200
201                # Store the maximum unit width
202                if unitWidth > adjustedWidth:
203                    adjustedWidth = unitWidth
204                    adjustedColumns = autoColumns
205
206        if len(adjustedColumns) == 0: break;
207        for col in adjustedColumns: col.width = adjustedWidth
208
209    if node.getProperty(u"equalcolumns") == u"true":
210        globalWidth = max([col.width for col in node.columns if col.auto])
211        for col in node.columns:
212            if col.auto: col.width = globalWidth
213
214    if fullwidth is not None:
215        delta = fullwidth
216        delta -= sum ([x.width for x in node.columns])
217        delta -= sum ([x.spaceAfter for x in node.columns[:-1]])
218        delta -= 2 * node.framespacings[0]
219        if delta != 0:
220            sizableColumns = [x for x in node.columns if x.fit]
221            if len(sizableColumns) == 0:
222                sizableColumns = [x for x in node.columns if x.auto]
223            if len(sizableColumns) == 0:
224                node.error("Overconstrained table layout: explicit table width specified, but no column has automatic width; table width attribute ignored")
225            else:
226                delta /= len(sizableColumns)
227                for col in sizableColumns: col.width += delta
228
229def calculateRowHeights(node):
230    # Set  initial row heights for cells with rowspan == 1
231    commonAxis = node.axis()
232    for r in node.rows:
233        r.height = 0
234        r.depth  = 0
235        for c in r.cells:
236            if c is None or c.content is None or c.rowspan != 1: continue
237            cellAxis = c.content.axis()
238            c.vshift = 0
239
240            if c.valign == u"baseline":
241                if r.alignToAxis: cell.vshift -= commonAxis
242                if c.content.alignToAxis: c.vshift += cellAxis
243
244            elif c.valign == u"axis":
245                if not r.alignToAxis: c.vshift += commonAxis
246                if not c.content.alignToAxis: c.vshift -= cellAxis
247
248            else:
249               c.vshift = (r.height - r.depth - c.content.height + c.content.depth) / 2
250
251            r.height = max(r.height, c.content.height + c.vshift)
252            r.depth = max(r.depth, c.content.depth - c.vshift)
253
254    # Calculate heights for cells with rowspan > 1
255    while True:
256        adjustedRows = []
257        adjustedSize = 0
258        for i in range(len(node.rows)):
259            r = node.rows[i]
260            for c in r.cells:
261                if c is None or c.content is None or c.rowspan == 1: continue
262                rows = node.rows[i : i + c.rowspan]
263
264                requiredSize = c.content.height + c.content.depth
265                requiredSize -= sum([x.spaceAfter for x in rows[:-1]])
266                fullSize = sum ([x.height + x.depth for x in rows])
267                if fullSize >= requiredSize: continue
268
269                unitSize = requiredSize / len(rows)
270                while True:
271                    oversizedRows = [x for x in rows if x.height + x.depth >= unitSize]
272                    if len(oversizedRows) == 0: break
273
274                    rows = [x for x in rows if x.height + x.depth < unitSize]
275                    if len(rows) == 0: break  # weird rounding effects
276                    requiredSize -= sum ([x.height + x.depth for x in oversizedRows])
277                    unitSize = requiredSize / len(rows)
278                if len(rows) == 0: continue; # protection against arithmetic overflow
279
280                if unitSize > adjustedSize:
281                    adjustedSize = unitSize
282                    adjustedRows = rows
283
284        if len(adjustedRows) == 0: break;
285        for r in adjustedRows:
286            delta = (adjustedSize - r.height - r.depth) / 2
287            r.height += delta; r.depth += delta
288
289    if node.getProperty(u"equalrows") == u"true":
290        maxvsize = max([r.height + r.depth for r in node.rows])
291        for r in node.rows:
292            delta = (maxvsize - r.height - r.depth) / 2
293            r.height += delta; r.depth += delta
294
295
296def getAlign(node):
297    alignattr = node.getProperty(u"align").strip()
298    if len(alignattr) == 0: alignattr = mathnode.globalDefaults[u"align"]
299
300    splitalign = alignattr.split()
301    alignType = splitalign[0]
302
303    if len(splitalign) == 1:
304        alignRow = None
305    else:
306        alignRow = node.parseInt(splitalign[1])
307        if alignrownumber == 0:
308            node.error("Alignment row number cannot be zero")
309            alignrownumber = None
310        elif alignrownumber > len(node.rows):
311            node.error("Alignment row number cannot exceed row count")
312            alignrownumber = len(node.rows)
313        elif alignrownumber < - len(node.rows):
314            node.error("Negative alignment row number cannot exceed row count")
315            alignrownumber = 1
316        elif alignrownumber < 0:
317            alignrownumber = len(node.rows) - alignrownumber + 1
318
319    return (alignType, alignRow)