IndentTable
This is a rework of Pascal Bauermeister's IndentTable.py processor as a Moin 1.3.x parser.
This allows tables to be laid out in a way that is easier to edit, at least for complex tables.
- IndentTable.py
- Original version
- IndentTable-0.0.3.py
Patched version which allows non-ASCII chars. Only reformat() at the end was modified. -- Aaron Digulla
Code
1 """
2 MoinMoin - IndentTable.py processor
3
4 Processor for turning indented lists of data into tables.
5 $Id: IndentTable.py,v 1.8 2004/12/19 19:36:22 nigel Exp $
6
7 With classical MoinMoin tables syntax:
8 * the source can become pretty unreadable when cells contents grow
9 * it is difficult to handle multiple lines
10 * it is impossible to have lists (other than by including a page
11 in a cell)
12
13 This processor is meant to correct the above problems, by the
14 means of an indented list:
15 * each cell consists of one line
16 * its indent determines its position in the table
17 * one line can be continued on the next line by means of '\' at the
18 end; this helps keeping long contents readable
19 * macros accepted in regular tables are accepted too, e.g [[BR]]
20 * all regular table formatting (formats in <>'s, cell spanning)
21 are supported
22 * optionally, the cell contents can be parsed, allowing to have
23 any kind of lists, headings, etc.
24
25 The price to pay for the increased power and readability of the
26 source, versus the regular table syntax, is the loss of the 2D
27 matrix layout.
28
29 @copyright: Pascal Bauermeister <pascal DOT bauermeister AT hispeed DOT ch>
30 @license: GPL
31
32 Updates:
33 * [v0.0.1] Pascal - Thu Jul 29 15:41:25 CEST 2004
34 - initial release
35 * [v0.0.2] NigelMetheringham - Sun Dec 19 19:05:21 +0000 2004
36 - minimal rework for MoinMoin 1.3.x
37 - change of option parse means some option forms have changed
38 - this needs further work
39 * [v0.0.3] Aaron Digulla - Thursday Jun 02 20:00:00 +0100 2006
40 - Small patch to allow non-ASCII characters (see reformat())
41
42 -------------------------------------------------------------------------------
43
44 Usage:
45 {{{#!IndentTable OPTIONS
46 indented data
47 }}}
48
49 Options:
50 debug=on insert debug info in the output
51 debug=full insert more debug info in the output
52 extended=on parse each cell content (useful for lists and headings)
53 row=on force row mode
54 column=on force column mode
55
56 -------------------------------------------------------------------------------
57
58 Samples:
59
60 {{{#!IndentTable
61 1 1
62 2 ==> 2
63 3 3
64 4 4
65 }}}
66
67 {{{#!IndentTable
68 1
69 2 ==> 1 2 3 4
70 3
71 4
72 }}}
73
74 {{{#!IndentTable
75 1
76 2 ==> 1 2 3
77 3 4
78 4
79 }}}
80
81 {{{#!IndentTable row=on
82 1
83 2 ==> 1 2 3
84 3 4
85 4
86 }}}
87
88 {{{#!IndentTable
89 1
90 2 ==> 1 2 3
91 3 4
92 4
93 }}}
94
95 {{{#!IndentTable
96 1
97 2 ==> 1 2
98 3 3 4
99 4
100 }}}
101
102 {{{#!IndentTable
103 1
104 2 ==> 1 3 4
105 3 2
106 4
107 }}}
108
109 {{{#!IndentTable row=on
110 1
111 2 ==> 1
112 3 2 3 4
113 4
114 }}}
115
116 {{{#!IndentTable
117 1
118 2 ==> 1 3 5
119 3 2 4 6
120 4
121 5
122 6
123 }}}
124
125 {{{ #!IndentTable extended=on
126 <width='30%'>1
127 <width='30%'>2
128 <width='30%'>3
129 4 ==> 1 2 3
130 <|2(>== Cool ! ==\
131 [[SystemInfo]]\ 4 == Cool! == 6
132 Terrific, isn't it ? <system>
133 6 7 <information> 9
134 7 Terrific, isn't it ?
135 9 10 11 12
136 10
137 11
138 12
139
140 }}
141
142 {{{#!IndentTable extended=on
143 A1 ==> +----+-----------------+------------+--+
144 B1 |A1 |B1 | C1+D1 | |
145 ||C1+D1 +----+-----------------+------------+--+
146 A2 | | |C2: Bullets:| |
147 B2 | | | * bullet 1|D2|
148 C2: Bullets: \ |A2 |B2 | * bullet 2| |
149 * bullet 1 \ | | |end of cell | |
150 * bullet 2 \ +----+-----------------+------------+--+
151 end of cell |A3 | a. (B3) numberes|C3 | |
152 D2 | | b. numbered item| | |
153 A3 +----+-----------------+------------+--+
154 a. (B3) numbers \ |(A4)|B4 | | |
155 a. numbered item +----+-----------------+------------+--+
156 C3
157 '''''' (A4)
158 B4
159 ## You find this list unreadable ? try to do the same with just ||'s !
160 }}}
161 """
162
163 from MoinMoin import wikiutil
164 from MoinMoin.parser import wiki
165 import cStringIO, re, string, random
166
167 LETTERS = string.ascii_lowercase
168 LETTERS_LEN = len (LETTERS)
169
170 LIST_HEADERS = ["*", "1.", "a.", "A.", "i.", "I."]
171
172 COL_FMT_RE1 = re.compile("\\|*<[^>]*>")
173 COL_FMT_RE2 = re.compile("\\|*") # FIXME: unify these two regexes
174
175 class Parser:
176 """ Parser for turning indented lists of data into tables. """
177
178 def __init__(self, raw, request, **kw):
179 # save call arguments for later use in format
180 self.lines = raw.split("\n");
181 self.request = request
182 self.form = request.form
183 self._ = request.getText
184 self.out = kw.get('out', request)
185 # default options values
186 self.opt_dbg = False
187 self.opt_dbgf = False # verbose debug
188 self.opt_ext = False
189 self.opt_row = False
190 self.opt_col = False # not sure this is really useful...
191 # deal with arguments
192 attrs, msg = wikiutil.parseAttributes(request,
193 kw.get('format_args',''))
194 if not msg:
195 if attrs.get('debug','"off"')[1:-1].lower() in ('on', 'true', 'yes'):
196 self.opt_dbg = True
197 if attrs.get('debug','"off"')[1:-1].lower() in ('full'):
198 self.opt_dbgf = True
199 if attrs.get('extended','"off"')[1:-1].lower() in ('on', 'true', 'yes'):
200 self.opt_ext = True
201 if attrs.get('row','"off"')[1:-1].lower() in ('on', 'true', 'yes'):
202 self.opt_row = True
203 if attrs.get('column','"off"')[1:-1].lower() in ('on', 'true', 'yes'):
204 self.opt_col = True
205
206
207 def format (self, formatter):
208 substitute_content = True
209
210 #
211 # collect src lines
212 #
213 lines_info = []
214 line_buf = ""
215 last_indent = -1
216 nb_indent_eq, nb_indent_dec = 0, 0
217 for line in self.lines:
218 # skip comments
219 if line.lstrip ().startswith ("##"): continue
220
221 # handle unterminated lines
222 if line.strip ().endswith ("\\"):
223 line_buf = line_buf + line.rstrip ('\\ ')
224 if self.opt_ext: line_buf = line_buf + "\n"
225 continue # continue, to finish line
226
227 # append current line to any previously unterminated line
228 else: line_buf = line_buf + line
229
230 # skip empty lines
231 if len (line_buf.strip ()) == 0: continue
232
233 # calculate indent
234 lline = line_buf.lstrip ()
235 cur_indent = len (line_buf) - len (lline)
236 if cur_indent == last_indent: nb_indent_eq = nb_indent_eq + 1
237 if cur_indent < last_indent: nb_indent_dec = nb_indent_dec + 1
238
239 # detect table formatting
240 m = COL_FMT_RE1.match (lline) or COL_FMT_RE2.match (lline)
241 if m and m.start() == 0:
242 fmt = lline [:m.end ()]
243 data = lline [m.end():].strip ()
244 else:
245 fmt = ""
246 data = line_buf.strip ()
247
248 # in extended mode, adjust leading spaces of data lines so
249 # that the first data line has none, and all other data lines
250 # are aligned relatively to the first one; for lists, preserve
251 # one leading space
252 if self.opt_ext:
253 start = cur_indent # number of unwanted leading spaces
254 for s in ["*", "1.", "a.", "A.", "i.", "I."]:
255 if data.startswith (s): start = start -1 # preserve 1 space
256 data = " "*cur_indent+data # 'unstrip' the 1st line (w/o tbl fmt)
257 data_lines = data.split ("\n")
258 for i in range (len (data_lines)):
259 data_lines [i] = data_lines [i] [start:] # del unwanted spaces
260 data = ("\n").join (data_lines)
261
262
263 # store cell
264 lines_info.append ( (cur_indent, fmt, data) )
265
266 # ready for next line
267 line_buf = ""
268 last_indent = cur_indent
269
270 #
271 # generate table structure
272 #
273 table_fmt_buf = ""
274
275 # decide whether row or column-oriented
276 is_by_col = nb_indent_dec==0 and nb_indent_eq > 0
277 if self.opt_col: is_by_col = True
278 if self.opt_row: is_by_col = False
279
280 # generate a token base that does not occur in the source, and
281 # that is MoinMoin neutral, and not an HTML sequence
282 token_base = "token"
283 src = "\n".join (self.lines)
284 while src.find (token_base) >=0:
285 # append some random letter
286 token_base = token_base + LETTERS [random.randint (0, LETTERS_LEN-1)]
287
288 # function to generate tokens
289 mk_token = lambda i: "%s%i" % (token_base, i)
290
291 # function to generate a cell, either with a token, or with raw
292 # content, depending on whether we must interpret the content
293 if self.opt_ext: mk_cell = lambda fmt, i, data: "||%s %s " % (fmt, mk_token (i))
294 else: mk_cell = lambda fmt, i, data: "||%s %s " % (fmt, data)
295
296 # row-oriented structure:
297 # the table flow is the same as regular MoinMoin tables, all we
298 # have to do is detect the end of rows and generate end of lines
299 if not is_by_col:
300 indent = 0
301 line_index = 0
302 if not self.opt_ext: substitute_content = False
303 for cur_indent, fmt, line in lines_info:
304 # same or lower indent ? ==> new row: close previous and start new
305 if cur_indent <= indent and len (table_fmt_buf):
306 table_fmt_buf = table_fmt_buf +"||\n"
307
308 # add cell
309 table_fmt_buf = table_fmt_buf + mk_cell (fmt, line_index, line)
310
311 indent = cur_indent
312 line_index = line_index + 1
313
314 # close table
315 if len (table_fmt_buf): table_fmt_buf = table_fmt_buf + "||"
316
317 # column-oriented structure:
318 # a bit more complicated; the cells must be reordered; we first
319 # determine the coordinates of data and store them in a (pseudo)
320 # table; then we generate the table structure, picking the right
321 # data
322 else:
323 # determine coordinates and store data
324 indent = -1
325 col, row = 0, 0
326 max_col, max_row = 0, 0 # will be needed to generate the table
327 table = {}
328 for index in range (len (lines_info)):
329 cur_indent = lines_info [index] [0]
330 if cur_indent == indent:
331 # new row
332 row = row + 1
333 if row > max_row: max_row = row
334 else:
335 # new column
336 row = 1
337 col = col + 1
338 if col > max_col: max_col = col
339 indent = cur_indent
340 # store coordinates and data index
341 table [col-1,row-1] = index
342
343 # generate table
344 for row in range (max_row):
345 for col in range (max_col):
346 if table.has_key ((col,row)):
347 index = table [col,row]
348 fmt, line = lines_info [index] [1:]
349 table_fmt_buf = table_fmt_buf + mk_cell (fmt, index, line)
350 else:
351 table_fmt_buf = table_fmt_buf + "|| " # empty cell
352 table_fmt_buf = table_fmt_buf +"||\n"
353
354 #
355 # final table generation
356 #
357
358 # emit debug
359 if self.opt_dbg or self.opt_dbgf:
360 if self.opt_dbgf:
361 if substitute_content:
362 data = "\nData:\n"
363 for i in range (len (lines_info)):
364 line = lines_info [i] [2]
365 data = data + "%d: [%s]\n" % (i, line)
366 else: data = ""
367 output = "{{{\nSource:\n{{ %s\n}}\n" \
368 "\nTable:\n%s\n" \
369 "%s}}}" % \
370 ("\n".join (self.lines), table_fmt_buf, data)
371
372 else: output = "{{{\n%s\n}}}" % "\n".join (self.lines)
373
374 parser = wiki.Parser (output, self.request)
375 parser.format (formatter)
376
377 # generate html for table structure, generate each cell, then
378 # merge them
379 if substitute_content:
380 # gen table struct
381 html_buf = self.reformat (table_fmt_buf, self.request, formatter)
382 # gen cells contents and merge in table
383 for i in range (len (lines_info)):
384 line = lines_info [i] [2]
385 token = mk_token (i)
386 content = self.reformat (line, self.request, formatter)
387 html_buf = html_buf.replace (token, content, 1)
388 # proudly emit the result
389 self.request.write(html_buf) # emit html-formatted content
390
391 # we have the table in MoinMoin source format, just HTML-convert it
392 else:
393 output = "%s\n" % table_fmt_buf
394 parser = wiki.Parser (output, self.request)
395 parser.format (formatter)
396
397 # done!
398 return
399
400 def reformat (self, src_text, request, formatter):
401 # parse the text (in wiki source format) and make HTML,
402 # after diverting sys.stdout to a string
403 str_out = cStringIO.StringIO () # create str to collect output
404
405 # cStringIO doesn't like Unicode, so wrap with a utf8 encoder/decoder.
406 encoder, decoder, reader, writer = codecs.lookup('utf8')
407 utf8io = codecs.StreamReaderWriter(str_out, reader, writer, 'replace')
408
409 request.redirect (utf8io) # divert output to that string
410 # parse this line
411 wiki.Parser (src_text, request).format (formatter)
412 request.redirect () # restore output
413
414 utf8io.seek(0)
415 result = utf8io.read()
416 str_out.close()
417 utf8io.close()
418 return result.strip () # return what was generated