1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - ImprovedTableParser library 4 5 @copyright: 2012 by Paul Boddie <paul@boddie.org.uk> 6 @copyright: 2008 MoinMoin:FlorianKrupicka (redirectedOutput code) 7 @license: GNU GPL (v2 or later), see COPYING.txt for details. 8 """ 9 10 from MoinMoin import wikiutil 11 from StringIO import StringIO 12 from MoinSupport import * 13 import re 14 15 __version__ = "0.2" 16 17 # Regular expressions. 18 19 syntax = { 20 # At start of line: 21 "rows" : (r"^==(?!.*?==$)", re.MULTILINE), # == not-heading 22 23 # Within text: 24 "markers" : (r"([{]{3,}|[}]{3,})", re.MULTILINE | re.DOTALL), # {{{... or }}}... 25 "columns" : (r"\|\|[ \t]*", 0), # || ws-excl-nl 26 27 # At start of column text: 28 "column" : (r"^\s*<(.*?)>\s*(.*)", re.DOTALL), # ws < attributes > ws 29 } 30 31 patterns = {} 32 for name, (value, flags) in syntax.items(): 33 patterns[name] = re.compile(value, re.UNICODE | flags) 34 35 # Other regular expressions. 36 37 leading_number_regexp = re.compile(r"\d*") 38 39 # Constants. 40 41 up_arrow = u'\u2191' 42 down_arrow = u'\u2193' 43 44 # Functions. 45 46 def parse(s): 47 48 "Parse 's', returning a table definition." 49 50 table_attrs = {} 51 rows = [] 52 53 # The following will be redefined upon the construction of the first row. 54 55 row_attrs = {} 56 columns = [] 57 columnnumber = 0 58 59 # The following will be redefined upon the construction of the first column. 60 61 column_attrs = {} 62 63 # Process exposed text and sections. 64 65 marker = None 66 is_region = True 67 68 # Initially, start a new row. 69 70 row_continued = False 71 72 for match_text in patterns["markers"].split(s): 73 74 # Only look for table features in exposed text. Where a section is 75 # defined, a marker will have been read and all regions before the 76 # closing marker will not be exposed. 77 78 if is_region and not marker: 79 80 # Extract each row from the definition. 81 82 for row_text in patterns["rows"].split(match_text): 83 84 # Only create a new row when a boundary has been found. 85 86 if not row_continued: 87 88 # Complete any existing row. 89 90 if columns: 91 extractAttributes(columns, row_attrs, table_attrs) 92 span_columns(columns, columnnumber) 93 94 # Replicate the last row to determine column usage. 95 96 column_usage = [] 97 98 for column_attrs, text in columns: 99 rowspan = int(strip_token(column_attrs.get("rowspan", "1"))) 100 if rowspan > 1: 101 attrs = {} 102 attrs.update(column_attrs) 103 attrs["rowspan"] = str(rowspan - 1) 104 attrs["rowcontinuation"] = True 105 column_usage.append((attrs, text)) 106 else: 107 column_usage.append(({}, None)) 108 109 columns = column_usage 110 111 # Define a new collection of row attributes. 112 113 row_attrs = {} 114 115 # Reset the columns and make the list available for the 116 # addition of new columns, starting a new column 117 # immediately. 118 119 rows.append((row_attrs, columns)) 120 column_continued = False 121 columnnumber = 0 122 123 # Extract each column from the row. 124 125 for text in patterns["columns"].split(row_text): 126 127 # Only create a new column when a boundary has been found. 128 129 if not column_continued: 130 131 # Complete any existing column. 132 133 if columns: 134 columnnumber = span_columns(columns, columnnumber) 135 136 # Extract the attribute and text sections. 137 138 match = patterns["column"].search(text) 139 if match: 140 attribute_text, text = match.groups() 141 column_attrs = parseAttributes(attribute_text, True) 142 else: 143 column_attrs = {} 144 145 # Define the new column with a mutable container 146 # permitting the extension of the text. 147 148 details = [column_attrs, text] 149 150 # Find the next gap in the columns. 151 152 while columnnumber != -1 and columnnumber < len(columns): 153 attrs, text = columns[columnnumber] 154 if text is None: 155 columns[columnnumber] = details 156 break 157 columnnumber += 1 158 159 # Or start adding at the end of the row. 160 161 else: 162 columnnumber = -1 163 columns.append(details) 164 165 else: 166 columns[columnnumber][1] += text 167 168 # Permit columns immediately following this one. 169 170 column_continued = False 171 172 # Permit a continuation of the current column. 173 174 column_continued = True 175 176 # Permit rows immediately following this one. 177 178 row_continued = False 179 180 # Permit a continuation if the current row. 181 182 row_continued = True 183 184 else: 185 186 # Handle section markers. 187 188 if not is_region: 189 190 # Interpret the given marker, closing the current section if the 191 # given marker is the corresponding end marker for the current 192 # section. 193 194 if marker: 195 if match_text.startswith("}") and len(marker) == len(match_text): 196 marker = None 197 198 # Without a current marker, start a section if an appropriate marker 199 # is given. 200 201 elif match_text.startswith("{"): 202 marker = match_text 203 204 # Markers and section text are incorporated into the current column. 205 206 columns[columnnumber][1] += match_text 207 208 is_region = not is_region 209 210 # Complete any final row. 211 212 if columns: 213 extractAttributes(columns, row_attrs, table_attrs) 214 215 return table_attrs, rows 216 217 def span_columns(columns, columnnumber): 218 219 """ 220 In the 'columns', make the column with the 'columnnumber' span the specified 221 number of columns, returning the next appropriate column number. 222 """ 223 224 column_attrs, text = columns[columnnumber] 225 226 # Handle any previous column spanning other columns. 227 228 if column_attrs.has_key("colspan"): 229 colspan = int(strip_token(column_attrs["colspan"])) 230 231 # Duplicate the current column as continuation 232 # columns for as long as the colspan is defined. 233 234 colspan -= 1 235 while colspan > 0: 236 attrs = {} 237 attrs.update(column_attrs) 238 attrs["colspan"] = str(colspan) 239 attrs["colcontinuation"] = True 240 241 if columnnumber != -1: 242 columnnumber += 1 243 if columnnumber < len(columns): 244 columns[columnnumber] = attrs, text 245 else: 246 columnnumber = -1 247 248 if columnnumber == -1: 249 columns.append((attrs, text)) 250 251 colspan -= 1 252 253 return columnnumber 254 255 def extractAttributes(columns, row_attrs, table_attrs): 256 257 """ 258 Extract row- and table-level attributes from 'columns', storing them in 259 'row_attrs' and 'table_attrs' respectively. 260 """ 261 262 for column in columns: 263 attrs = column[0] 264 for name, value in attrs.items(): 265 if name.startswith("row") and name not in ("rowspan", "rowcontinuation"): 266 row_attrs[name] = value 267 del attrs[name] 268 elif name.startswith("table"): 269 table_attrs[name] = value 270 del attrs[name] 271 272 # Formatting of embedded content. 273 # NOTE: Borrowed from EventAggregator. 274 275 def getParserClass(request, format): 276 277 """ 278 Return a parser class using the 'request' for the given 'format', returning 279 a plain text parser if no parser can be found for the specified 'format'. 280 """ 281 282 try: 283 return wikiutil.searchAndImportPlugin(request.cfg, "parser", format or "plain") 284 except wikiutil.PluginMissingError: 285 return wikiutil.searchAndImportPlugin(request.cfg, "parser", "plain") 286 287 def getFormatterClass(request, format): 288 289 """ 290 Return a formatter class using the 'request' for the given output 'format', 291 returning a plain text formatter if no formatter can be found for the 292 specified 'format'. 293 """ 294 295 try: 296 return wikiutil.searchAndImportPlugin(request.cfg, "formatter", format or "plain") 297 except wikiutil.PluginMissingError: 298 return wikiutil.searchAndImportPlugin(request.cfg, "formatter", "plain") 299 300 def formatText(text, request, fmt): 301 302 "Format the given 'text' using the specified 'request' and formatter 'fmt'." 303 304 parser_cls = getParserClass(request, request.page.pi["format"]) 305 parser = parser_cls(text, request, line_anchors=False) 306 old_fmt = request.formatter 307 request.formatter = fmt 308 try: 309 return redirectedOutput(request, parser, fmt, inhibit_p=True) 310 finally: 311 request.formatter = old_fmt 312 313 def redirectedOutput(request, parser, fmt, **kw): 314 315 "A fixed version of the request method of the same name." 316 317 buf = StringIO() 318 request.redirect(buf) 319 try: 320 parser.format(fmt, **kw) 321 if hasattr(fmt, "flush"): 322 buf.write(fmt.flush(True)) 323 finally: 324 request.redirect() 325 text = buf.getvalue() 326 buf.close() 327 return text 328 329 # Sorting utilities. 330 331 def get_sort_columns(s, start=0): 332 333 """ 334 Split the comma-separated string 's', extracting the column specifications 335 of the form <column>["n"] where the suffix "n" indicates an optional 336 numeric conversion for that column. Column indexes start from the specified 337 'start' value (defaulting to 0). 338 """ 339 340 sort_columns = [] 341 for column_spec in s.split(","): 342 column_spec = column_spec.strip() 343 344 ascending = True 345 if column_spec.endswith("d"): 346 column_spec = column_spec[:-1] 347 ascending = False 348 349 # Extract the conversion indicator and column index. 350 # Ignore badly-specified columns. 351 352 try: 353 column = get_number(column_spec) 354 suffix = column_spec[len(column):] 355 fn = converters[suffix] 356 sort_columns.append((max(0, int(column) - start), fn, ascending)) 357 except ValueError: 358 pass 359 360 return sort_columns 361 362 def get_column_types(sort_columns): 363 364 """ 365 Return a dictionary mapping column indexes to conversion functions. 366 """ 367 368 d = {} 369 for column, fn, ascending in sort_columns: 370 d[column] = fn, ascending 371 return d 372 373 def get_number(s): 374 375 "From 's', get any leading number." 376 377 match = leading_number_regexp.match(s) 378 if match: 379 return match.group() 380 else: 381 return "" 382 383 def to_number(s, request): 384 385 """ 386 Convert 's' to a number, discarding any non-numeric trailing data. 387 Return an empty string if 's' is empty. 388 """ 389 390 if s: 391 return int(get_number(to_plain_text(s, request))) 392 else: 393 return s 394 395 def to_plain_text(s, request): 396 397 "Convert 's' to plain text." 398 399 fmt = getFormatterClass(request, "plain")(request) 400 fmt.setPage(request.page) 401 return formatText(s, request, fmt) 402 403 converters = { 404 "n" : to_number, 405 "" : to_plain_text, 406 } 407 408 suffixes = {} 409 for key, value in converters.items(): 410 suffixes[value] = key 411 412 class Sorter: 413 414 "A sorting helper class." 415 416 def __init__(self, sort_columns, request): 417 self.sort_columns = sort_columns 418 self.request = request 419 420 def __call__(self, row1, row2): 421 row_attrs1, columns1 = row1 422 row_attrs2, columns2 = row2 423 424 # Apply the conversions to each column, comparing the results. 425 426 for column, fn, ascending in self.sort_columns: 427 column_attrs1, text1 = columns1[column] 428 column_attrs2, text2 = columns2[column] 429 430 # Ignore a column when a conversion is not possible. 431 432 try: 433 value1 = fn(text1, self.request) 434 value2 = fn(text2, self.request) 435 436 # Avoid empty strings appearing earlier than other values. 437 438 if value1 == "" and value2 != "": 439 result = 1 440 elif value1 != "" and value2 == "": 441 result = -1 442 else: 443 result = cmp(value1, value2) 444 445 # Where the columns differ, return a result observing the sense 446 # (ascending or descending) of the comparison for the column. 447 448 if result != 0: 449 return ascending and result or -result 450 451 except ValueError: 452 pass 453 454 return 0 455 456 def write_sort_control(request, columnnumber, columns, sort_columns, column_types, table_name, start=0): 457 458 """ 459 Using the 'request', write a sort control for the given 'columnnumber' in 460 the collection of 'columns', using the existing 'sort_columns' and 461 'column_types' to construct labels and links that modify the sort criteria, 462 and using the given 'table_name' to parameterise the links. 463 """ 464 465 fmt = request.formatter 466 write = request.write 467 _ = request.getText 468 469 write(fmt.div(1, css_class="sortcolumns")) 470 471 write(fmt.paragraph(1)) 472 write(fmt.text(_("Sort by columns..."))) 473 write(fmt.paragraph(0)) 474 475 # Start with the existing criteria without this column being involved. 476 477 revised_sort_columns = [(column, fn, ascending) 478 for (column, fn, ascending) in sort_columns if column != columnnumber] 479 480 # Get the specification of this column. 481 482 columnfn, columnascending = column_types.get(columnnumber, (to_plain_text, True)) 483 newsortcolumn = columnnumber, columnfn, columnascending 484 newsortcolumn_reverse = columnnumber, columnfn, not columnascending 485 newlabel = columns[columnnumber][1].strip() 486 487 # Show this column in all possible places in the sorting criteria. 488 489 write(fmt.number_list(1)) 490 491 just_had_this_column = False 492 493 for i, (column, fn, ascending) in enumerate(sort_columns): 494 new_sort_columns = revised_sort_columns[:] 495 new_sort_columns.insert(i, newsortcolumn) 496 label = columns[column][1].strip() 497 498 arrow = columnascending and down_arrow or up_arrow 499 arrow_reverse = not columnascending and down_arrow or up_arrow 500 501 sortcolumns = get_sort_column_output(new_sort_columns) 502 new_sort_columns[i] = newsortcolumn_reverse 503 sortcolumns_reverse = get_sort_column_output(new_sort_columns) 504 505 # Columns permitting the insertion of the selected column. 506 507 if column != columnnumber and not just_had_this_column: 508 write(fmt.listitem(1, css_class="sortcolumn")) 509 510 # Pop-up element showing the column inserted before the sort column. 511 512 write(fmt.span(1, css_class="sortcolumn-container")) 513 write(fmt.span(1, css_class="newsortcolumn")) 514 write(formatText(newlabel, request, fmt)) 515 516 write_sort_link(write, request, fmt, table_name, sortcolumns, arrow, "sortdirection") 517 write_sort_link(write, request, fmt, table_name, sortcolumns_reverse, arrow_reverse, "sortdirection") 518 519 write(fmt.span(0)) 520 write(fmt.span(0)) 521 522 # Link for selection of the modified sort criteria using the current 523 # column and showing its particular direction. 524 525 arrow = ascending and down_arrow or up_arrow 526 arrow_reverse = not ascending and down_arrow or up_arrow 527 write_sort_link(write, request, fmt, table_name, sortcolumns, u"%s %s" % (label, arrow), "") 528 529 # Columns permitting removal or modification. 530 531 else: 532 write(fmt.listitem(1)) 533 534 # Either show the column without a link, since the column to be 535 # inserted is already before the current column. 536 537 if just_had_this_column: 538 just_had_this_column = False 539 arrow = ascending and down_arrow or up_arrow 540 arrow_reverse = not ascending and down_arrow or up_arrow 541 542 # Write the current column with its particular direction. 543 544 write(fmt.span(1, css_class="unlinkedcolumn")) 545 write(formatText(u"%s %s" % (label, arrow), request, fmt)) 546 write(fmt.span(0)) 547 548 # Or show the column with a link for its removal. 549 550 else: 551 just_had_this_column = True 552 sortcolumns_revised = get_sort_column_output(revised_sort_columns) 553 write_sort_link(write, request, fmt, table_name, sortcolumns_revised, u"%s %s" % (label, arrow), "removecolumn") 554 555 # Alternative sort direction. 556 557 write_sort_link(write, request, fmt, table_name, sortcolumns_reverse, arrow_reverse, "altdirection") 558 559 write(fmt.listitem(0)) 560 561 if not just_had_this_column: 562 563 # Write the sorting criteria with this column at the end. 564 565 new_sort_columns = revised_sort_columns[:] 566 new_sort_columns.append(newsortcolumn) 567 568 sortcolumns = get_sort_column_output(new_sort_columns) 569 new_sort_columns[-1] = newsortcolumn_reverse 570 sortcolumns_reverse = get_sort_column_output(new_sort_columns) 571 572 arrow = columnascending and down_arrow or up_arrow 573 arrow_reverse = not columnascending and down_arrow or up_arrow 574 575 write(fmt.listitem(1, css_class="appendcolumn")) 576 577 # Pop-up element showing the column inserted before the sort column. 578 579 write(fmt.span(1, css_class="newsortcolumn")) 580 write_sort_link(write, request, fmt, table_name, sortcolumns, newlabel, "") 581 write_sort_link(write, request, fmt, table_name, sortcolumns, arrow, "sortdirection") 582 write_sort_link(write, request, fmt, table_name, sortcolumns_reverse, arrow_reverse, "sortdirection") 583 write(fmt.span(0)) 584 585 write(fmt.listitem(0)) 586 587 write(fmt.number_list(0)) 588 589 write(fmt.div(0)) 590 591 def write_sort_link(write, request, fmt, table_name, sortcolumns, label, css_class): 592 593 "Write a link expressing sort criteria." 594 595 write(fmt.url(1, "?%s#%s" % ( 596 wikiutil.makeQueryString("%s-sortcolumns=%s" % (table_name, sortcolumns)), 597 fmt.qualify_id(fmt.sanitize_to_id(table_name)) 598 ), css_class=css_class)) 599 write(formatText(label, request, fmt)) 600 write(fmt.url(0)) 601 602 def get_sort_column_output(columns, start=0): 603 604 "Return the output criteria for the given 'columns' indexed from 'start'." 605 606 return ",".join([("%d%s%s" % (column + start, suffixes[fn], not ascending and "d" or "")) 607 for (column, fn, ascending) in columns]) 608 609 # Common formatting functions. 610 611 def formatTable(text, request, fmt, attrs=None): 612 613 """ 614 Format the given 'text' using the specified 'request' and formatter 'fmt'. 615 The optional 'attrs' can be used to control the presentation of the table. 616 """ 617 618 # Parse the table region. 619 620 table_attrs, table = parse(text) 621 622 # Define the table name and an anchor attribute. 623 624 table_name = attrs.get("name") 625 if table_name: 626 table_attrs["tableid"] = table_name 627 else: 628 table_name = table_attrs.get("tableid") 629 630 # Only attempt to offer sorting capabilities if a table name is specified. 631 632 if table_name: 633 634 # Get the underlying column types. 635 636 column_types = get_column_types(get_sort_columns(attrs.get("columntypes", ""))) 637 638 # Get sorting criteria from the region. 639 640 region_sortcolumns = attrs.get("sortcolumns", "") 641 642 # Update the column types from the sort criteria. 643 644 column_types.update(get_column_types(get_sort_columns(region_sortcolumns))) 645 646 # Determine the applicable sort criteria using the request. 647 648 sortcolumns = getQualifiedParameter(request, table_name, "sortcolumns") 649 if sortcolumns is None: 650 sortcolumns = region_sortcolumns 651 652 # Define the final sort criteria. 653 654 sort_columns = get_sort_columns(sortcolumns) 655 data_start = int(getQualifiedParameter(request, table_name, "headers") or attrs.get("headers", "1")) 656 657 # Update the column types from the final sort criteria. 658 659 column_types.update(get_column_types(sort_columns)) 660 661 # Sort the rows according to the values in each of the specified columns. 662 663 if sort_columns: 664 headers = table[:data_start] 665 data = table[data_start:] 666 667 # Perform the sort and reconstruct the table. 668 669 sorter = Sorter(sort_columns, request) 670 data.sort(cmp=sorter) 671 table = headers + data 672 673 # Otherwise, indicate that no sorting is being performed. 674 675 else: 676 sort_columns = None 677 678 # Write the table. 679 680 write = request.write 681 write(fmt.table(1, table_attrs)) 682 683 for rownumber, (row_attrs, columns) in enumerate(table): 684 write(fmt.table_row(1, row_attrs)) 685 sortable_heading = sort_columns is not None and rownumber == data_start - 1 686 687 for columnnumber, (column_attrs, column_text) in enumerate(columns): 688 689 # Always skip column continuation cells. 690 691 if column_attrs.get("colcontinuation"): 692 continue 693 694 # Where sorting has not occurred, preserve rowspans and do not write 695 # cells that continue a rowspan. 696 697 if not sort_columns: 698 if column_attrs.get("rowcontinuation"): 699 continue 700 701 # Where sorting has occurred, replicate cell contents and remove any 702 # rowspans. 703 704 else: 705 if column_attrs.has_key("rowspan"): 706 del column_attrs["rowspan"] 707 708 # Remove any continuation attributes that still apply. 709 710 if column_attrs.has_key("rowcontinuation"): 711 del column_attrs["rowcontinuation"] 712 713 write(fmt.table_cell(1, column_attrs)) 714 715 if sortable_heading: 716 write(fmt.div(1, css_class="sortablecolumn")) 717 718 write(formatText(column_text or "", request, fmt)) 719 720 # Add sorting controls, if appropriate. 721 722 if sortable_heading: 723 write_sort_control(request, columnnumber, columns, sort_columns, column_types, table_name) 724 write(fmt.div(0)) 725 726 write(fmt.table_cell(0)) 727 728 write(fmt.table_row(0)) 729 730 write(fmt.table(0)) 731 732 # vim: tabstop=4 expandtab shiftwidth=4