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