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