""" MoinMoin - SearchInPagesAndSort Macro A line-oriented search macro over multiple pages, with sorting @copyright: Pascal Bauermeister @license: GPL Updates: * [v0.3.1] Pascal Sat Nov 6 16:03:01 CET 2004 * Added NoText, RawText, NbSubs and MoreSubsText arguments * [v0.3.1] Pascal Mon Aug 30 21:27:36 CEST 2004 * Corrected bug: did not work well with multiple pages hit. Bug reported by Craig Johnson. It worked in 0.2.x because one bug corrected another one... * If args are not a kw list (e.g. old macro form) inserts usage in html page (brutal, but we really don't want to support the old form any more) * [v0.3.0] Pascal Wed Aug 18 15:39:54 CEST 2004 * macro arguments are now passed as a list of KEYWORD=VALUE * ACL is handled * new options: Reverse and NoHeader * [v0.2.4] Pascal Mon Jul 19 23:40:54 CEST 2004 * Comparisons to None use the 'is' and 'is not' operator (nicer) * Use get() for dict lookup w/ default value * Do not quote args and retry to compile if they are not valid regexes * Corrected usage samples in the comment below * [v0.2.3] Pascal Sun Jul 18 13:45:46 CEST 2004 Avoid endless recursion when matching page contains this macro * [v0.2.2] Fri Jul 16 14:43:23 CEST 2004 * Use Request.redirect(). Thanks to Craig Johnson and Thomas Waldmann . * No more unused imports. * Catch only expected exceptions. * [v0.2.1] Mon Jun 7 11:54:52 CEST 2004 * options: links, heading * works now with MoinMoin Release 1.2 too * [v0.1.1] Wed Oct 29 14:48:02 CET 2003 works with MoinMoin Release 1.1 [Revision 1.173] and Python 2.3.2 * [v0.1.0] 2003/04/24 10:32:04 Original version ---- Usage: [[ SearchInPagesAndSort ]] [[ SearchInPagesAndSort (KEYWORD=VALUE [, ...] ) ]] Search for 'searchtext' regex in pages marching 'pages' regex, and sort the found lines (=hits) in this order: 1) substring of the hit matching 'sortkey'; group same matches of 'sortkey' by a header 2) substring of the hit matching 'searchtext' 3) the hit itself If no arguments are given, the usage is inserted in the HTML result. Possible keywords: Help = 0, 1, 2 displays 1:short or 2:full help in the page; default: 0 (i.e. no help) Pages = 'PAGES REGEX' pages in which the text is sought; if empty (default) search in the current page and defaults 'NoLinks' to 1; default: empty (i.e. current page) SearchText = 'TEXT REGEX' to search for lines in matching pages; mandatory! SortKey = 'TEXT REGEX' criterion to sort matching lines (=hits); default: empty (i.e. no sorting) Heading = 'TEXT REGEX' follow each hit by the text maching Regex, that preceeds the hit in its source page; default: empty (i.e. no headings) UnassignedText = 'WIKI TEXT' header for hits not matching the sort key; default: '[unassigned]' Reverse = 0 or 1 reverse-sort the hits; default: 0 (i.e. forward sort) RawText = 0 or 1 do not format found text; default: 0 (i.e. formatted) NbSubs = 0, N, or 'all' follow each hit by max N sub lines (i.e. next lines with greater indent) of source text following the hit; if N is 'all', take *all* sub lines; if N is positive, take N *first* sub lines; if N is negative, take the |N| *last* sub lines; default: 0 (i.e. do *not* include subs) MoreSubsText = 'WIKI TEXT' if there are more sub lines than 'NbSubs', follow/preceed the last/first sub lines by this text; default: '...' NoHeader = 0 or 1 disable showing the headers as subtitles; default: 0 (i.e. show headers) NoLinks = 0 or 1 disable following each hit by a link to its page; default: 0 (i.e. show links) or 1 if 'Pages' is omitted NoPageText = 'HTML TEXT' text displayed if no page match 'Pages'; default: an error message w/ Page regex NoText = 0 or 1 disables showing the found text; default: 0 (i.e. show found text) Keywords can be also given in upper or lower cases, or abbreviated. Example: SearchText, searchtext, SEARCHTEXT, st, ST, Pages, p, etc. ---- Sample 1: Given a page named 'ProjectA': 1. Action Items 1. [Alan] {2} to launch this task 1. [Alan] {1} to do this urgent thing 1. [Ben][Clara] {3} do this as background task 1. Deadlines 1. 2003-03-12 [Alan][Clara]: deliver 1st version of the Release X ...and a page named 'ProjectB': * [Denise] {2} Development of task Xyz * [Eric] {1} Tests of feature F * [Eric] (./) Tests of feature E ...using the macro in a page named 'ActionItems' like this: = ActionItems = [[SearchInPagesAndSort(pages="Project.*", searchtext="{[123]}", sortkey="\[[A-Za-z_]*\]")]] = Deadlines = [[SearchInPagesAndSort(pages="Project.*", searchtext="")]] = Completed tasks = [[SearchInPagesAndSort(pages="Project.*", searchtext"(\./)", sortkey="\[[A-Za-z_]*\]")]] ...will give this output (note: _text_ are links): ActionItems * [Alan] * [Alan] {1} to do this urgent thing _ProjectA_ * [Alan] {2} to launch this task _ProjectA_ * [Denise] * [Denise] {2} Development of task Xyz _ProjectB_ * [Ben] * [Ben][Clara] {3} do this as background task _ProjectA_ * [Eric] * [Eric] {1} Tests of feature F _ProjectB_ * [Clara] * [Ben][Clara] {3} do this as background task _ProjectA_ Deadlines * 2003-03-12 [Alan][Clara]: deliver 1st version of the Release X _ProjectA_ Completed tasks * [Eric] * [Eric] (./) Tests of feature E _ProjectB_ Sample 2: Given a page containing: == Tasks for (ABC) == * {1} (due:2003-12-16) [Mike] Do this == Tasks for (XYZ) == * {2} (due:2003-12-17) [John_Doe][Mike] Do that ...the following macro call in the same page: [[SearchInPagesAndSort(searchtext="{[123]}", sortkey="\[[A-Za-z_ -]*\]", links=0, heading="\([ab]*[0-9][0-9][0-9]\)")]] ...will produce: * [John_Doe] * {2} (due:2003-12-17) [John_Doe][Mike] Do that (XYZ) * [Mike] * {1} (due:2003-12-16) [Mike] Do this (ABC) * {2} (due:2003-12-17) [John_Doe][Mike] Do that (XYZ) """ # Imports import re, sys, cStringIO from string import ascii_lowercase, maketrans from MoinMoin import config, wikiutil from MoinMoin.Page import Page from MoinMoin.parser import wiki Dependencies = ["time"] # macro cannot be cached _recursions = 0 FAKETRANS = maketrans ("","") class _Error (Exception): pass def execute (macro, text, args_re=None): global _recursions if _recursions: return '' _recursions += 1 try: res = _execute (macro, text) except _Error, msg: _recursions = 0 return """

Error: macro SearchInPagesAndSort: %s

""" % msg _recursions -=1 return res def _delparam (keyword, params): value = params [keyword] del params [keyword] return value def _param_get (params, spec, default): """Returns the value for a parameter, if specified with one of several acceptable keyword names, or returns its default value if it is missing from the macro call. If the parameter is specified, it is removed from the list, so that remaining params can be signalled as unknown""" # param name is litteral ? if params.has_key (spec): return _delparam (spec, params) # param name is all lower or all upper ? lspec = spec.lower () if params.has_key (lspec): return _delparam (lspec, params) uspec = spec.upper () if params.has_key (uspec): return _delparam (uspec, params) # param name is abbreviated ? cspec = spec [0].upper () + spec [1:] # capitalize 1st letter cspec = cspec.translate (FAKETRANS, ascii_lowercase) if params.has_key (cspec): return _delparam (cspec, params) cspec = cspec.lower () if params.has_key (cspec): return _delparam (cspec, params) # nope: return default value return default def _usage (full = False): """Returns the interesting part of the module's doc""" if full: return __doc__ lines = __doc__.splitlines () start = 0 end = len (lines) for i in range (end): if lines [i].strip ().lower () == "usage:": start = i break for i in range (start, end): if lines [i].startswith ('--'): end = i break return '\n'.join (lines [start:end]) def _re_compile (text, name): try: return re.compile (text, re.IGNORECASE) except Exception, msg: raise _Error ("%s for regex argument %s: '%s'" % (msg, name, text)) def _indent_of (line, pos=0): n = 0 for c in line [pos:]: if c != ' ': break n = n + 1 return n def _subtext_get (body, pos, nbsubs, indent, moresubs): subpos = pos+1 end = len (body) lead = ' '*indent while True: if subpos>=end: break if _indent_of (body, subpos) <= indent: break p = body.find ("\n", subpos) if p == -1: break else: subpos = p + 1 subs = body [pos:subpos].strip ('\n').split ('\n') ls = len (subs) if (nbsubs=='all'): pass elif nbsubs>0 and ls>nbsubs: subs = subs [0:nbsubs] subs.append (lead + moresubs) elif nbsubs<0 and ls>-nbsubs: subs = subs [nbsubs:] subs.insert (0, lead + moresubs) return lead + '\n'.join (subs) # The "raison d'etre" of this module def _execute (macro, text): result = "" # new args syntax try: params = eval ("(lambda **opts: opts)(%s)" % text) except: raise _Error ("""malformed arguments list: %s
usage:
%s
""" % (text, _usage () ) ) arg_text = _param_get (params, 'SearchText', None) arg_pages = _param_get (params, 'Pages', '') arg_key = _param_get (params, 'SortKey', None) opt_heading = _param_get (params, 'Heading', None) opt_unassigned_text = _param_get (params, 'UnassignedText', "[unassigned]") opt_reverse = _param_get (params, 'Reverse', False) opt_rawtext = _param_get (params, 'RawText', False) def_nolinks = (1,0) [len (arg_pages)>0] opt_nolinks = _param_get (params, 'NoLinks', def_nolinks) opt_noheader = _param_get (params, 'NoHeader', False) opt_notext = _param_get (params, 'NoText', False) opt_nopage = _param_get (params, 'NoPageText', None) opt_help = _param_get (params, 'Help', 0) opt_nbsubs = _param_get (params, 'NbSubs', 0) def_moresubs = ('...', None) [opt_nbsubs=='all'] opt_moresubs = _param_get (params, 'MoreSubsText', def_moresubs) # help ? if opt_help: return """

Macro SearchInPagesAndSort usage:

%s

""" % _usage (opt_help==2) # check the args a little bit if len (params): raise _Error ("""unknown argument(s): %s
usage:
%s
""" % (`params.keys ()`, _usage () ) ) if arg_text is None: raise _Error ("missing 'searchtext' argument") # empty page means this page; subpage are also handled if len (arg_pages) == 0 or arg_pages.startswith ('/'): arg_pages = macro.formatter.page.page_name + arg_pages # get a list of pages matching the PageRegex pages_re = _re_compile (arg_pages, 'Pages') all_pages = wikiutil.getPageList (config.text_dir) hits = filter (pages_re.search, all_pages) # check ACL now (since we may end up with no pages) if config.acl_enabled: me = macro.request.user.name def _check_page (page_name): page = Page (page_name) # too bad we must instanciate... return page.getACL ().may (macro.request, me, "read") hits = filter (_check_page, hits) # sort pages, check if we have pages if len (hits) == 0: if opt_nopage: return "%s" % opt_nopage else: raise _Error ("no page matching '%s'!" % arg_pages) else: hits.sort () # compile all regex text_re = _re_compile (arg_text, 'SearchText') if arg_key is not None: key_re = _re_compile (arg_key, 'SortKey') if opt_heading is not None: heading_re = _re_compile (opt_heading, 'Heading') # we will collect matching lines in each matching page all_matches = [] # treat each found page for page_name in hits: body = Page (page_name).get_raw_body () pos = 0 last_start = -1 last_end = -1 heading_text = "" while 1: keep_line = 1 # search text match = text_re.search (body, pos) if not match: break # text is found; now search for heading if opt_heading is not None: heading_pos = pos heading_match = True # keep the nearest heading to the found text while heading_match: heading_match = heading_re.search (body, heading_pos) if heading_match and \ heading_match.start () < match.start (): heading_text = heading_match.group (0) heading_pos = heading_match.end () else: heading_match = False # point to found text pos = match.end ()+1 # cut before start of line start_pos = match.start () rev = 0 while body [start_pos] != '\n' and start_pos: start_pos = start_pos - 1 rev = 1 if rev: start_pos = start_pos + 1 # cut at end of line end_pos = body.find ("\n", match.end ()) # extract line raw_line = body [start_pos:end_pos] indent = _indent_of (raw_line) line = raw_line.strip () # store this record if it differs from previous one if start_pos == last_start or end_pos == last_end: keep_line = 0 # store this record if it it is not a comment elif line.startswith ("##"): keep_line = 0 # remove possible list item leaders if keep_line: for heading in ["*", "1.", "a.", "A.", "i.", "I."]: if line.startswith (heading): line = line.replace (heading, "", 1) line = line.strip () if len (line)==0: keep_line = 0 # handle this record if keep_line: # get sub sections if opt_nbsubs: subtext = '\n' + _subtext_get (body, end_pos, opt_nbsubs, indent, opt_moresubs) else: subtext = '' # find the sort key nbmatches = 0 keypos = 0 found = 0 while 1: if arg_key is None: keyval = "" else: keymatch = key_re.search (line, keypos) if keymatch: keyval = line [keymatch.start ():keymatch.end ()] keypos = keymatch.end () nbmatches = nbmatches + 1 found = 1 else: if nbmatches>0: break keyval = opt_unassigned_text # store info item = [] item.append (keyval) # key text item.append (body [match.start ():match.end ()]) # srch txt item.append (line) # line text item.append (page_name) # page name item.append (heading_text) # heading item.append (subtext) # subsections all_matches.append (item) if found == 0: break last_start = start_pos last_end = end_pos # all occurences of sort key found # this line handled # all lines handled # all pages handled # prepare some formatting text bullet_list_open = macro.formatter.bullet_list (1) bullet_list_close = macro.formatter.bullet_list (0) listitem_open = macro.formatter.listitem (1) listitem_close = macro.formatter.listitem (0) # now sort and format records if not opt_notext: all_matches.sort () if opt_reverse: all_matches.reverse () result = result+"\n" + bullet_list_open keyval = "" head_count = 0 last_pagename = "" # treat records for output for item in all_matches: keytext, srchtext, text, pagename, heading_text, subtext = item if opt_notext: text_fmtted = "" if last_pagename == pagename: continue else: last_pagename = pagename elif opt_rawtext: text_fmtted = wikiutil.escape (text) else: # parse the text (in wiki source format) and make HTML, # after diverting sys.stdout to a string text_fmtted = _format (text, macro.request, macro.formatter) text_fmtted = text_fmtted.strip (' ') # preserve newlines # empty text => drop this item if len (text_fmtted)==0: continue # insert heading (only if not yet done) if not opt_noheader \ and arg_key is not None \ and keytext != keyval: # this is a new heading keyval = keytext if head_count: result = result+"\n " + bullet_list_close result = result+"\n " + listitem_close head_count = head_count +1 result = result+"\n " + listitem_open result = result+ _format (keyval, macro.request, macro.formatter) result = result+"\n " + bullet_list_open # correct the text format (berk) if text_fmtted.startswith ("\n

"): text_fmtted = text_fmtted [4:] if text_fmtted.endswith ("

\n"): text_fmtted = text_fmtted [:-5] text_trailer = "\n

\n" else: text_trailer = "" # insert text result = result+"\n " + listitem_open result = result + text_fmtted if not opt_nolinks: result = result + "   " try: # try MoinMoin 1.1 API link_text = wikiutil.link_tag (pagename) except TypeError: # try MoinMoin 1.2 API link_text = wikiutil.link_tag (macro.request, pagename) result = result + link_text result = result + "" if opt_heading is not None: result = result + "   " result = result + heading_text result = result + "" if opt_nbsubs: result = result + _format (subtext, macro.request, macro.formatter) result = result + text_trailer + "\n " + listitem_close # all items done, close (hopefully) gracefully if head_count: result = result+"\n " + listitem_close result = result+"\n " + bullet_list_close if not opt_noheader and arg_key is not None: result = result+"\n " + listitem_close result = result+"\n" + bullet_list_close # done return result def _format (src_text, request, formatter): # parse the text (in wiki source format) and make HTML, # after diverting sys.stdout to a string str_out = cStringIO.StringIO () # create str to collect output request.redirect (str_out) # divert output to that string # parse this line wiki.Parser (src_text, request).format (formatter) request.redirect () # restore output return str_out.getvalue () # return what was generated