1.1 --- a/ImprovedTableParser.py Thu Jan 19 22:48:03 2012 +0100
1.2 +++ b/ImprovedTableParser.py Sun Feb 19 00:35:15 2012 +0100
1.3 @@ -33,6 +33,10 @@
1.4 for name, (value, flags) in syntax.items():
1.5 patterns[name] = re.compile(value, re.UNICODE | flags)
1.6
1.7 +# Other regular expressions.
1.8 +
1.9 +leading_number_regexp = re.compile(r"\d*")
1.10 +
1.11 # Functions.
1.12
1.13 def parse(s):
1.14 @@ -177,7 +181,8 @@
1.15 """
1.16 Parse the table attributes string 's', returning a mapping of names to
1.17 values. If 'escape' is set to a true value, the attributes will be suitable
1.18 - for use with the formatter API.
1.19 + for use with the formatter API. If 'escape' is set to a false value, the
1.20 + attributes will have any quoting removed.
1.21 """
1.22
1.23 attrs = {}
1.24 @@ -190,7 +195,7 @@
1.25 # Capture the name if needed.
1.26
1.27 if name is None:
1.28 - name = escape and wikiutil.escape(token) or token
1.29 + name = escape and wikiutil.escape(token) or strip_token(token)
1.30
1.31 # Detect either an equals sign or another name.
1.32
1.33 @@ -204,13 +209,16 @@
1.34 # Otherwise, capture a value.
1.35
1.36 else:
1.37 - # Quoting of attributes done similarly to parseAttributes.
1.38 + # Quoting of attributes done similarly to wikiutil.parseAttributes.
1.39
1.40 - if escape and token:
1.41 - if token[0] in ("'", '"'):
1.42 - token = wikiutil.escape(token)
1.43 + if token:
1.44 + if escape:
1.45 + if token[0] in ("'", '"'):
1.46 + token = wikiutil.escape(token)
1.47 + else:
1.48 + token = '"%s"' % wikiutil.escape(token, 1)
1.49 else:
1.50 - token = '"%s"' % wikiutil.escape(token, 1)
1.51 + token = strip_token(token)
1.52
1.53 attrs[name.lower()] = token
1.54 name = None
1.55 @@ -218,6 +226,15 @@
1.56
1.57 return attrs
1.58
1.59 +def strip_token(token):
1.60 +
1.61 + "Return the given 'token' stripped of quoting."
1.62 +
1.63 + if token[0] in ("'", '"') and token[-1] == token[0]:
1.64 + return token[1:-1]
1.65 + else:
1.66 + return token
1.67 +
1.68 # Formatting of embedded content.
1.69 # NOTE: Borrowed from EventAggregator.
1.70
1.71 @@ -241,15 +258,117 @@
1.72 parser = parser_cls(text, request, line_anchors=False)
1.73 return request.redirectedOutput(parser.format, fmt, inhibit_p=True)
1.74
1.75 +# Sorting utilities.
1.76 +
1.77 +def get_sort_columns(s, start=0):
1.78 +
1.79 + """
1.80 + Split the comma-separated string 's', extracting the column specifications
1.81 + of the form <column>["n"] where the prefix "n" indicates an optional
1.82 + numeric conversion for that column. Column indexes start from the specified
1.83 + 'start' value (defaulting to 0).
1.84 + """
1.85 +
1.86 + sort_columns = []
1.87 + for column_spec in s.split(","):
1.88 + column_spec = column_spec.strip()
1.89 +
1.90 + ascending = True
1.91 + if column_spec.endswith("d"):
1.92 + column_spec = column_spec[:-1]
1.93 + ascending = False
1.94 +
1.95 + # Extract the conversion indicator and column index.
1.96 +
1.97 + if column_spec.endswith("n"):
1.98 + column = column_spec[:-1]
1.99 + fn = to_number
1.100 + else:
1.101 + column = column_spec
1.102 + fn = str
1.103 +
1.104 + # Ignore badly-specified columns.
1.105 +
1.106 + try:
1.107 + sort_columns.append((max(0, int(column) - start), fn, ascending))
1.108 + except ValueError:
1.109 + pass
1.110 +
1.111 + return sort_columns
1.112 +
1.113 +def to_number(s):
1.114 +
1.115 + "Convert 's' to a number, discarding any non-numeric trailing data."
1.116 +
1.117 + match = leading_number_regexp.match(s)
1.118 + if match:
1.119 + return int(match.group())
1.120 + else:
1.121 + raise ValueError, s
1.122 +
1.123 +class Sorter:
1.124 +
1.125 + "A sorting helper class."
1.126 +
1.127 + def __init__(self, sort_columns):
1.128 + self.sort_columns = sort_columns
1.129 +
1.130 + def __call__(self, row1, row2):
1.131 + row_attrs1, columns1 = row1
1.132 + row_attrs2, columns2 = row2
1.133 +
1.134 + # Apply the conversions to each column, comparing the results.
1.135 +
1.136 + for column, fn, ascending in self.sort_columns:
1.137 + column_attrs1, text1 = columns1[column]
1.138 + column_attrs2, text2 = columns2[column]
1.139 +
1.140 + # Ignore a column when a conversion is not possible.
1.141 +
1.142 + try:
1.143 + text1 = fn(text1)
1.144 + text2 = fn(text2)
1.145 + result = cmp(text1, text2)
1.146 +
1.147 + # Where the columns differ, return a result observing the sense
1.148 + # (ascending or descending) of the comparison for the column.
1.149 +
1.150 + if result != 0:
1.151 + return ascending and result or -result
1.152 +
1.153 + except ValueError:
1.154 + pass
1.155 +
1.156 + return 0
1.157 +
1.158 # Common formatting functions.
1.159
1.160 -def formatTable(text, request, fmt):
1.161 +def formatTable(text, request, fmt, attrs=None):
1.162
1.163 - "Format the given 'text' using the specified 'request' and formatter 'fmt'."
1.164 + """
1.165 + Format the given 'text' using the specified 'request' and formatter 'fmt'.
1.166 + The optional 'attrs' can be used to control the presentation of the table.
1.167 + """
1.168 +
1.169 + table_attrs, table = parse(text)
1.170 +
1.171 + # Sort the rows according to the values in each of the specified columns.
1.172
1.173 - attrs, table = parse(text)
1.174 + if attrs.has_key("sortcolumns"):
1.175 + data_start = int(attrs.get("headers", "1"))
1.176 + headers = table[:data_start]
1.177 + data = table[data_start:]
1.178 +
1.179 + # Get the sort columns using Unix sort-like notation.
1.180
1.181 - request.write(fmt.table(1, attrs))
1.182 + sorter = Sorter(get_sort_columns(attrs["sortcolumns"]))
1.183 + data.sort(cmp=sorter)
1.184 +
1.185 + table = headers + data
1.186 +
1.187 + # Write the table.
1.188 +
1.189 + request.write(fmt.table(1, table_attrs))
1.190
1.191 for row_attrs, columns in table:
1.192 request.write(fmt.table_row(1, row_attrs))
2.1 --- a/parsers/table.py Thu Jan 19 22:48:03 2012 +0100
2.2 +++ b/parsers/table.py Sun Feb 19 00:35:15 2012 +0100
2.3 @@ -38,6 +38,6 @@
2.4 page = request.page
2.5 _ = request.getText
2.6
2.7 - formatTable(self.raw, request, fmt)
2.8 + formatTable(self.raw, request, fmt, self.attrs)
2.9
2.10 # vim: tabstop=4 expandtab shiftwidth=4