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.3" 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 # Sorting utilities. 273 274 def get_sort_columns(s, start=0): 275 276 """ 277 Split the comma-separated string 's', extracting the column specifications 278 of the form <column>["n"] where the suffix "n" indicates an optional 279 numeric conversion for that column. Column indexes start from the specified 280 'start' value (defaulting to 0). 281 """ 282 283 sort_columns = [] 284 for column_spec in s.split(","): 285 column_spec = column_spec.strip() 286 287 ascending = True 288 if column_spec.endswith("d"): 289 column_spec = column_spec[:-1] 290 ascending = False 291 292 # Extract the conversion indicator and column index. 293 # Ignore badly-specified columns. 294 295 try: 296 column = get_number(column_spec) 297 suffix = column_spec[len(column):] 298 fn = converters[suffix] 299 sort_columns.append((max(0, int(column) - start), fn, ascending)) 300 except ValueError: 301 pass 302 303 return sort_columns 304 305 def get_column_types(sort_columns): 306 307 """ 308 Return a dictionary mapping column indexes to conversion functions. 309 """ 310 311 d = {} 312 for column, fn, ascending in sort_columns: 313 d[column] = fn, ascending 314 return d 315 316 def get_number(s): 317 318 "From 's', get any leading number." 319 320 match = leading_number_regexp.match(s) 321 if match: 322 return match.group() 323 else: 324 return "" 325 326 def to_number(s, request): 327 328 """ 329 Convert 's' to a number, discarding any non-numeric trailing data. 330 Return an empty string if 's' is empty. 331 """ 332 333 if s: 334 return int(get_number(to_plain_text(s, request))) 335 else: 336 return s 337 338 def to_plain_text(s, request): 339 340 "Convert 's' to plain text." 341 342 fmt = getFormatterClass(request, "plain")(request) 343 fmt.setPage(request.page) 344 return formatText(s, request, fmt) 345 346 converters = { 347 "n" : to_number, 348 "" : to_plain_text, 349 } 350 351 suffixes = {} 352 for key, value in converters.items(): 353 suffixes[value] = key 354 355 class Sorter: 356 357 "A sorting helper class." 358 359 def __init__(self, sort_columns, request): 360 self.sort_columns = sort_columns 361 self.request = request 362 363 def __call__(self, row1, row2): 364 row_attrs1, columns1 = row1 365 row_attrs2, columns2 = row2 366 367 # Apply the conversions to each column, comparing the results. 368 369 for column, fn, ascending in self.sort_columns: 370 column_attrs1, text1 = columns1[column] 371 column_attrs2, text2 = columns2[column] 372 373 # Ignore a column when a conversion is not possible. 374 375 try: 376 value1 = fn(text1, self.request) 377 value2 = fn(text2, self.request) 378 379 # Avoid empty strings appearing earlier than other values. 380 381 if value1 == "" and value2 != "": 382 result = 1 383 elif value1 != "" and value2 == "": 384 result = -1 385 else: 386 result = cmp(value1, value2) 387 388 # Where the columns differ, return a result observing the sense 389 # (ascending or descending) of the comparison for the column. 390 391 if result != 0: 392 return ascending and result or -result 393 394 except ValueError: 395 pass 396 397 return 0 398 399 def write_sort_control(request, columnnumber, columns, sort_columns, column_types, table_name, start=0): 400 401 """ 402 Using the 'request', write a sort control for the given 'columnnumber' in 403 the collection of 'columns', using the existing 'sort_columns' and 404 'column_types' to construct labels and links that modify the sort criteria, 405 and using the given 'table_name' to parameterise the links. 406 """ 407 408 fmt = request.formatter 409 write = request.write 410 _ = request.getText 411 412 write(fmt.div(1, css_class="sortcolumns")) 413 414 write(fmt.paragraph(1)) 415 write(fmt.text(_("Sort by columns..."))) 416 write(fmt.paragraph(0)) 417 418 # Start with the existing criteria without this column being involved. 419 420 revised_sort_columns = [(column, fn, ascending) 421 for (column, fn, ascending) in sort_columns if column != columnnumber] 422 423 # Get the specification of this column. 424 425 columnfn, columnascending = column_types.get(columnnumber, (to_plain_text, True)) 426 newsortcolumn = columnnumber, columnfn, columnascending 427 newsortcolumn_reverse = columnnumber, columnfn, not columnascending 428 newlabel = columns[columnnumber][1].strip() 429 430 # Show this column in all possible places in the sorting criteria. 431 432 write(fmt.number_list(1)) 433 434 just_had_this_column = False 435 436 for i, (column, fn, ascending) in enumerate(sort_columns): 437 new_sort_columns = revised_sort_columns[:] 438 new_sort_columns.insert(i, newsortcolumn) 439 label = columns[column][1].strip() 440 441 arrow = columnascending and down_arrow or up_arrow 442 arrow_reverse = not columnascending and down_arrow or up_arrow 443 444 sortcolumns = get_sort_column_output(new_sort_columns) 445 new_sort_columns[i] = newsortcolumn_reverse 446 sortcolumns_reverse = get_sort_column_output(new_sort_columns) 447 448 # Columns permitting the insertion of the selected column. 449 450 if column != columnnumber and not just_had_this_column: 451 write(fmt.listitem(1, css_class="sortcolumn")) 452 453 # Pop-up element showing the column inserted before the sort column. 454 455 write(fmt.span(1, css_class="sortcolumn-container")) 456 write(fmt.span(1, css_class="newsortcolumn")) 457 write(formatText(newlabel, request, fmt)) 458 459 write_sort_link(write, request, fmt, table_name, sortcolumns, arrow, "sortdirection") 460 write_sort_link(write, request, fmt, table_name, sortcolumns_reverse, arrow_reverse, "sortdirection") 461 462 write(fmt.span(0)) 463 write(fmt.span(0)) 464 465 # Link for selection of the modified sort criteria using the current 466 # column and showing its particular direction. 467 468 arrow = ascending and down_arrow or up_arrow 469 arrow_reverse = not ascending and down_arrow or up_arrow 470 write_sort_link(write, request, fmt, table_name, sortcolumns, u"%s %s" % (label, arrow), "") 471 472 # Columns permitting removal or modification. 473 474 else: 475 write(fmt.listitem(1)) 476 477 # Either show the column without a link, since the column to be 478 # inserted is already before the current column. 479 480 if just_had_this_column: 481 just_had_this_column = False 482 arrow = ascending and down_arrow or up_arrow 483 arrow_reverse = not ascending and down_arrow or up_arrow 484 485 # Write the current column with its particular direction. 486 487 write(fmt.span(1, css_class="unlinkedcolumn")) 488 write(formatText(u"%s %s" % (label, arrow), request, fmt)) 489 write(fmt.span(0)) 490 491 # Or show the column with a link for its removal. 492 493 else: 494 just_had_this_column = True 495 sortcolumns_revised = get_sort_column_output(revised_sort_columns) 496 write_sort_link(write, request, fmt, table_name, sortcolumns_revised, u"%s %s" % (label, arrow), "removecolumn") 497 498 # Alternative sort direction. 499 500 write_sort_link(write, request, fmt, table_name, sortcolumns_reverse, arrow_reverse, "altdirection") 501 502 write(fmt.listitem(0)) 503 504 if not just_had_this_column: 505 506 # Write the sorting criteria with this column at the end. 507 508 new_sort_columns = revised_sort_columns[:] 509 new_sort_columns.append(newsortcolumn) 510 511 sortcolumns = get_sort_column_output(new_sort_columns) 512 new_sort_columns[-1] = newsortcolumn_reverse 513 sortcolumns_reverse = get_sort_column_output(new_sort_columns) 514 515 arrow = columnascending and down_arrow or up_arrow 516 arrow_reverse = not columnascending and down_arrow or up_arrow 517 518 write(fmt.listitem(1, css_class="appendcolumn")) 519 520 # Pop-up element showing the column inserted before the sort column. 521 522 write(fmt.span(1, css_class="newsortcolumn")) 523 write_sort_link(write, request, fmt, table_name, sortcolumns, newlabel, "") 524 write_sort_link(write, request, fmt, table_name, sortcolumns, arrow, "sortdirection") 525 write_sort_link(write, request, fmt, table_name, sortcolumns_reverse, arrow_reverse, "sortdirection") 526 write(fmt.span(0)) 527 528 write(fmt.listitem(0)) 529 530 write(fmt.number_list(0)) 531 532 write(fmt.div(0)) 533 534 def write_sort_link(write, request, fmt, table_name, sortcolumns, label, css_class): 535 536 "Write a link expressing sort criteria." 537 538 write(fmt.url(1, "?%s#%s" % ( 539 wikiutil.makeQueryString("%s-sortcolumns=%s" % (table_name, sortcolumns)), 540 fmt.qualify_id(fmt.sanitize_to_id(table_name)) 541 ), css_class=css_class)) 542 write(formatText(label, request, fmt)) 543 write(fmt.url(0)) 544 545 def get_sort_column_output(columns, start=0): 546 547 "Return the output criteria for the given 'columns' indexed from 'start'." 548 549 return ",".join([("%d%s%s" % (column + start, suffixes[fn], not ascending and "d" or "")) 550 for (column, fn, ascending) in columns]) 551 552 # Common formatting functions. 553 554 def formatTable(text, request, fmt, attrs=None): 555 556 """ 557 Format the given 'text' using the specified 'request' and formatter 'fmt'. 558 The optional 'attrs' can be used to control the presentation of the table. 559 """ 560 561 # Parse the table region. 562 563 table_attrs, table = parse(text) 564 565 # Define the table name and an anchor attribute. 566 567 table_name = attrs.get("name") 568 if table_name: 569 table_attrs["tableid"] = table_name 570 else: 571 table_name = table_attrs.get("tableid") 572 573 # Only attempt to offer sorting capabilities if a table name is specified. 574 575 if table_name: 576 577 # Get the underlying column types. 578 579 column_types = get_column_types(get_sort_columns(attrs.get("columntypes", ""))) 580 581 # Get sorting criteria from the region. 582 583 region_sortcolumns = attrs.get("sortcolumns", "") 584 585 # Update the column types from the sort criteria. 586 587 column_types.update(get_column_types(get_sort_columns(region_sortcolumns))) 588 589 # Determine the applicable sort criteria using the request. 590 591 sortcolumns = getQualifiedParameter(request, table_name, "sortcolumns") 592 if sortcolumns is None: 593 sortcolumns = region_sortcolumns 594 595 # Define the final sort criteria. 596 597 sort_columns = get_sort_columns(sortcolumns) 598 data_start = int(getQualifiedParameter(request, table_name, "headers") or attrs.get("headers", "1")) 599 600 # Update the column types from the final sort criteria. 601 602 column_types.update(get_column_types(sort_columns)) 603 604 # Sort the rows according to the values in each of the specified columns. 605 606 if sort_columns: 607 headers = table[:data_start] 608 data = table[data_start:] 609 610 # Perform the sort and reconstruct the table. 611 612 sorter = Sorter(sort_columns, request) 613 data.sort(cmp=sorter) 614 table = headers + data 615 616 # Otherwise, indicate that no sorting is being performed. 617 618 else: 619 sort_columns = None 620 621 # Write the table. 622 623 write = request.write 624 write(fmt.table(1, table_attrs)) 625 626 for rownumber, (row_attrs, columns) in enumerate(table): 627 write(fmt.table_row(1, row_attrs)) 628 sortable_heading = sort_columns is not None and rownumber == data_start - 1 629 630 for columnnumber, (column_attrs, column_text) in enumerate(columns): 631 632 # Always skip column continuation cells. 633 634 if column_attrs.get("colcontinuation"): 635 continue 636 637 # Where sorting has not occurred, preserve rowspans and do not write 638 # cells that continue a rowspan. 639 640 if not sort_columns: 641 if column_attrs.get("rowcontinuation"): 642 continue 643 644 # Where sorting has occurred, replicate cell contents and remove any 645 # rowspans. 646 647 else: 648 if column_attrs.has_key("rowspan"): 649 del column_attrs["rowspan"] 650 651 # Remove any continuation attributes that still apply. 652 653 if column_attrs.has_key("rowcontinuation"): 654 del column_attrs["rowcontinuation"] 655 656 write(fmt.table_cell(1, column_attrs)) 657 658 if sortable_heading: 659 write(fmt.div(1, css_class="sortablecolumn")) 660 661 write(formatText(column_text or "", request, fmt)) 662 663 # Add sorting controls, if appropriate. 664 665 if sortable_heading: 666 write_sort_control(request, columnnumber, columns, sort_columns, column_types, table_name) 667 write(fmt.div(0)) 668 669 write(fmt.table_cell(0)) 670 671 write(fmt.table_row(0)) 672 673 write(fmt.table(0)) 674 675 # vim: tabstop=4 expandtab shiftwidth=4