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