# HG changeset patch # User Paul Boddie # Date 1329608115 -3600 # Node ID 103ec898398ad63df80437cceb8e6a7274358a64 # Parent 197226a815ca8c9d5a49190b0d02bf1cf0e2f4ad Introduced table data sorting according to a specification provided by the "sortcolumns" attribute on the table region. Changed the parseAttributes function to strip quoting from region attributes if they are not going to be escaped for formatter usage. diff -r 197226a815ca -r 103ec898398a ImprovedTableParser.py --- a/ImprovedTableParser.py Thu Jan 19 22:48:03 2012 +0100 +++ b/ImprovedTableParser.py Sun Feb 19 00:35:15 2012 +0100 @@ -33,6 +33,10 @@ for name, (value, flags) in syntax.items(): patterns[name] = re.compile(value, re.UNICODE | flags) +# Other regular expressions. + +leading_number_regexp = re.compile(r"\d*") + # Functions. def parse(s): @@ -177,7 +181,8 @@ """ Parse the table attributes string 's', returning a mapping of names to values. If 'escape' is set to a true value, the attributes will be suitable - for use with the formatter API. + for use with the formatter API. If 'escape' is set to a false value, the + attributes will have any quoting removed. """ attrs = {} @@ -190,7 +195,7 @@ # Capture the name if needed. if name is None: - name = escape and wikiutil.escape(token) or token + name = escape and wikiutil.escape(token) or strip_token(token) # Detect either an equals sign or another name. @@ -204,13 +209,16 @@ # Otherwise, capture a value. else: - # Quoting of attributes done similarly to parseAttributes. + # Quoting of attributes done similarly to wikiutil.parseAttributes. - if escape and token: - if token[0] in ("'", '"'): - token = wikiutil.escape(token) + if token: + if escape: + if token[0] in ("'", '"'): + token = wikiutil.escape(token) + else: + token = '"%s"' % wikiutil.escape(token, 1) else: - token = '"%s"' % wikiutil.escape(token, 1) + token = strip_token(token) attrs[name.lower()] = token name = None @@ -218,6 +226,15 @@ return attrs +def strip_token(token): + + "Return the given 'token' stripped of quoting." + + if token[0] in ("'", '"') and token[-1] == token[0]: + return token[1:-1] + else: + return token + # Formatting of embedded content. # NOTE: Borrowed from EventAggregator. @@ -241,15 +258,117 @@ parser = parser_cls(text, request, line_anchors=False) return request.redirectedOutput(parser.format, fmt, inhibit_p=True) +# Sorting utilities. + +def get_sort_columns(s, start=0): + + """ + Split the comma-separated string 's', extracting the column specifications + of the form ["n"] where the prefix "n" indicates an optional + numeric conversion for that column. Column indexes start from the specified + 'start' value (defaulting to 0). + """ + + sort_columns = [] + for column_spec in s.split(","): + column_spec = column_spec.strip() + + ascending = True + if column_spec.endswith("d"): + column_spec = column_spec[:-1] + ascending = False + + # Extract the conversion indicator and column index. + + if column_spec.endswith("n"): + column = column_spec[:-1] + fn = to_number + else: + column = column_spec + fn = str + + # Ignore badly-specified columns. + + try: + sort_columns.append((max(0, int(column) - start), fn, ascending)) + except ValueError: + pass + + return sort_columns + +def to_number(s): + + "Convert 's' to a number, discarding any non-numeric trailing data." + + match = leading_number_regexp.match(s) + if match: + return int(match.group()) + else: + raise ValueError, s + +class Sorter: + + "A sorting helper class." + + def __init__(self, sort_columns): + self.sort_columns = sort_columns + + def __call__(self, row1, row2): + row_attrs1, columns1 = row1 + row_attrs2, columns2 = row2 + + # Apply the conversions to each column, comparing the results. + + for column, fn, ascending in self.sort_columns: + column_attrs1, text1 = columns1[column] + column_attrs2, text2 = columns2[column] + + # Ignore a column when a conversion is not possible. + + try: + text1 = fn(text1) + text2 = fn(text2) + result = cmp(text1, text2) + + # Where the columns differ, return a result observing the sense + # (ascending or descending) of the comparison for the column. + + if result != 0: + return ascending and result or -result + + except ValueError: + pass + + return 0 + # Common formatting functions. -def formatTable(text, request, fmt): +def formatTable(text, request, fmt, attrs=None): - "Format the given 'text' using the specified 'request' and formatter 'fmt'." + """ + Format the given 'text' using the specified 'request' and formatter 'fmt'. + The optional 'attrs' can be used to control the presentation of the table. + """ + + table_attrs, table = parse(text) + + # Sort the rows according to the values in each of the specified columns. - attrs, table = parse(text) + if attrs.has_key("sortcolumns"): + data_start = int(attrs.get("headers", "1")) + headers = table[:data_start] + data = table[data_start:] + + # Get the sort columns using Unix sort-like notation. - request.write(fmt.table(1, attrs)) + sorter = Sorter(get_sort_columns(attrs["sortcolumns"])) + data.sort(cmp=sorter) + + table = headers + data + + # Write the table. + + request.write(fmt.table(1, table_attrs)) for row_attrs, columns in table: request.write(fmt.table_row(1, row_attrs)) diff -r 197226a815ca -r 103ec898398a parsers/table.py --- a/parsers/table.py Thu Jan 19 22:48:03 2012 +0100 +++ b/parsers/table.py Sun Feb 19 00:35:15 2012 +0100 @@ -38,6 +38,6 @@ page = request.page _ = request.getText - formatTable(self.raw, request, fmt) + formatTable(self.raw, request, fmt, self.attrs) # vim: tabstop=4 expandtab shiftwidth=4