1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - ImprovedMoinSearch library 4 5 @copyright: 2010 Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 """ 8 9 from MoinMoin.search import searchPages 10 from MoinMoin.Page import Page 11 from MoinMoin import wikiutil 12 import re 13 14 heading_regexp = re.compile(r"^(?P<level>=+)(?P<heading>.*?)(?P=level)$", re.UNICODE | re.MULTILINE) 15 paragraph_regexp = re.compile(r"(?P<paragraph>(?:^[^#=\s].*$\n)+)", re.UNICODE | re.MULTILINE) 16 17 def range_groups(min_name, max_name): 18 return r"(?:\((?P<%s>-?\d+)?(?:\s*-\s*(?P<%s>-?\d+))?\))" % (min_name, max_name) 19 20 format_options_regexp = re.compile( 21 r"(" 22 r"(?P<link>(link|l):)" 23 r"|(?P<strong>(strong|str|bold|b):)" 24 r"|(?P<em>(emphasis|em|italic|i):)" 25 r")*" 26 r"(" 27 r"(?P<heading>(heading|title|h)\s*" + range_groups("min_heading", "max_heading") + ")" 28 r"|(?P<paragraph>(paragraph|para|p)\s*" + r"(?:\((?P<paragraph_number>\d+)?\))?" + ")" 29 r"|(?P<name>(name|page)\s*" + range_groups("first", "last") + ")" 30 r"|(?P<break>(break|br))" 31 r")", 32 re.UNICODE) 33 34 def convert_index(i, length): 35 36 """ 37 Convert from a 1-based indexing scheme to a 0-based scheme for the given 38 index 'i' in a sequence having the given 'length'. 39 """ 40 41 if i is None: 42 return i 43 elif i > 0: 44 return i - 1 45 elif i < 0: 46 return length + i 47 else: 48 return i 49 50 def getSearchResultPages(request, query, **kw): 51 52 """ 53 Return matching pages using the given 'request' and search 'query'. Optional 54 keyword arguments are passed to the underlying search infrastructure. 55 """ 56 57 results = searchPages(request, query, **kw) 58 return results.hits 59 60 # Action functions. 61 62 def getFirstPageHeading(request, page, start=0, min_level=None, max_level=None): 63 64 """ 65 Using the given 'request', return the first heading in the given 'page' 66 from the given 'start' point (optional, defaulting to the start of the page) 67 having a heading level of at least 'min_level' (which is undefined if not 68 specified) and at most 'max_level' (which is undefined if not specified). 69 70 A tuple containing the heading and the span (the start offset and the end 71 offset as a tuple) is returned for a successful retrieval. Otherwise, None 72 is returned. 73 """ 74 75 full_page = Page(request, page.page_name) 76 body = full_page.get_raw_body() 77 if start != 0: 78 body = body[start:] 79 80 for match in heading_regexp.finditer(body): 81 level = len(match.group("level")) 82 83 if (min_level is None or level >= min_level) and \ 84 (max_level is None or level <= max_level): 85 86 return match.group("heading"), match.span() 87 88 return None 89 90 def getParagraph(request, page, start=0, number=None): 91 92 """ 93 Using the given 'request', return from the given 'page', starting from the 94 optional 'start' offset (or the beginning, if no such offset is specified), 95 the first paragraph or, if the optional 'number' is given, the paragraph 96 whose position corresponds to that number, with a number of 1 being the 97 first paragraph found, 2 being the second, and so on. 98 """ 99 100 full_page = Page(request, page.page_name) 101 body = full_page.get_raw_body() 102 if start != 0: 103 body = body[start:] 104 105 for i, match in enumerate(paragraph_regexp.finditer(body)): 106 if number is None or i == max(0, number - 1): 107 return match.group("paragraph"), match.span() 108 109 return None 110 111 def getPageName(request, page, start=0, first=None, last=None): 112 113 """ 114 Using the given 'request', return the name of the given 'page'. The optional 115 'start' offset refers to the body of the page and is returned as the start 116 and end of the result span if specified. 117 118 If the optional 'first' or 'last' parameters are specified, only the 119 specified span of parts extracted from the page name will be returned, where 120 the parts of the name are obtained by splitting the full name where the 121 slash ("/") character is found. The first part has an index of 1, and the 122 last part can be referred to using an index of -1. 123 """ 124 125 parts = page.page_name.split("/") 126 127 first = convert_index(first, len(parts)) 128 last = convert_index(last, len(parts)) 129 130 if first is None: 131 if last is None: 132 pass 133 else: 134 parts = parts[:last+1] 135 else: 136 if last is None: 137 parts = parts[first:] 138 else: 139 parts = parts[first:last+1] 140 141 return "/".join(parts), (start, start) 142 143 # Formatting styles. 144 145 def asLink(styles, formatter, text, page): 146 output = [] 147 output.append(formatter.pagelink(on=1, pagename=page.page_name)) 148 if not styles: 149 output.append(asText(None, formatter, text, page)) 150 else: 151 output.append(next_style(styles, formatter, text, page)) 152 output.append(formatter.pagelink(on=0)) 153 return u''.join(output) 154 155 def _asStyledText(styles, formatter, text, page, fn): 156 output = [] 157 output.append(fn(on=1)) 158 if not styles: 159 output.append(asText(None, formatter, text, page)) 160 else: 161 output.append(next_style(styles, formatter, text, page)) 162 output.append(fn(on=0)) 163 return u''.join(output) 164 165 def asStrong(styles, formatter, text, page): 166 return _asStyledText(styles, formatter, text, page, formatter.strong) 167 168 def asEmphasis(styles, formatter, text, page): 169 return _asStyledText(styles, formatter, text, page, formatter.emphasis) 170 171 def asText(styles, formatter, text, page): 172 if not styles: 173 return formatter.text(text) 174 else: 175 return next_style(styles, formatter, text, page) 176 177 def asBreak(styles, formatter, text, page): 178 return formatter.linebreak(0) 179 180 def next_style(styles, formatter, text, page): 181 return styles[0](styles[1:], formatter, text, page) 182 183 style_functions = { 184 "link" : asLink, 185 "strong" : asStrong, 186 "em" : asEmphasis, 187 } 188 189 # Formatting functions. 190 191 def formatResultPages(request, formatter, pages, paging, format, page_from=0): 192 193 """ 194 Using the given 'request' and 'formatter', return a formatted string showing 195 the result 'pages', providing paging controls when 'paging' is set to a true 196 value, and providing page details according to the given 'format'. 197 198 If the optional 'pages_from' parameter is set, the result pages from the 199 given result (specified within a range from 0 to the length of the 'pages' 200 collection) will be shown. 201 """ 202 203 actions = [] 204 205 if format: 206 for match in format_options_regexp.finditer(format): 207 208 # Apply styles by gathering style functions. 209 210 styles = [] 211 for style in ("strong", "em", "link"): 212 if match.group(style): 213 styles.append(style_functions[style]) 214 styles.append(asText) 215 216 # Add actions, arguments and styles. 217 218 if match.group("heading"): 219 actions.append((getFirstPageHeading, map(int_or_none, (match.group("min_heading"), match.group("max_heading"))), styles)) 220 elif match.group("paragraph"): 221 actions.append((getParagraph, map(int_or_none, (match.group("paragraph_number"),)), styles)) 222 elif match.group("name"): 223 actions.append((getPageName, map(int_or_none, (match.group("first"), match.group("last"))), styles)) 224 elif match.group("break"): 225 actions.append((None, None, [asBreak])) 226 else: 227 actions.append((getPageName, (), [asLink])) 228 229 # Use paging only when there are enough results. 230 231 results_per_page = request.cfg.search_results_per_page 232 paging = paging and len(pages) > results_per_page 233 234 if paging: 235 pages_to_show = pages[page_from:page_from + results_per_page] 236 else: 237 pages_to_show = pages 238 239 # Prepare the output. 240 241 output = [] 242 output.append(formatter.number_list(on=1, start=page_from + 1)) 243 244 for page in pages_to_show: 245 output.append(formatter.listitem(on=1)) 246 247 start = 0 248 first = 1 249 for action, args, styles in actions: 250 251 # Process requested actions. 252 253 if action is not None: 254 result = action(request, page, start, *args) 255 if result is not None: 256 text, span = result 257 258 # Or handle null actions. 259 260 else: 261 text, span = None, None 262 263 # Where actions are performed, there must be a result. 264 265 if action is None or result is not None: 266 267 if not first: 268 output.append(" ") 269 270 output.append(next_style(styles, formatter, text, page)) 271 272 # Position the search for the next action. 273 274 if span is not None: 275 _start, _end = span 276 start = _end + 1 277 278 first = 0 279 280 output.append(formatter.listitem(on=0)) 281 282 output.append(formatter.number_list(on=0)) 283 284 # Show paging navigation. 285 286 if paging: 287 output.append(formatPagingNavigation(request, formatter, pages, page_from)) 288 289 return "".join(output) 290 291 def formatPagingNavigation(request, formatter, pages, page_from=0): 292 293 """ 294 Using the given 'request' and 'formatter', return a formatted string showing 295 the paging navigation for the result 'pages', according to the 'page_from' 296 indicator which provides the current position in the result set. 297 """ 298 299 page = formatter.page 300 pagename = page.page_name 301 _ = request.getText 302 303 output = [] 304 305 results_per_page = request.cfg.search_results_per_page 306 number_of_results = len(pages) 307 308 pages_total = number_of_results / results_per_page 309 pages_before = page_from / results_per_page 310 pages_after = ((number_of_results - page_from) / results_per_page) - 1 311 312 querydict = wikiutil.parseQueryString(request.query_string) 313 314 output.append(formatter.paragraph(on=1)) 315 output.append(formatter.text(_("Result pages:"))) 316 output.append(formatter.text(" ")) 317 318 n = 0 319 while n < pages_before: 320 output.append(formatter.pagelink(on=1, pagename=pagename, querystr=getPagingQueryString(querydict, n * results_per_page))) 321 output.append(formatter.text(str(n + 1))) 322 output.append(formatter.pagelink(on=0)) 323 output.append(formatter.text(" ")) 324 n += 1 325 326 output.append(formatter.text(str(n + 1))) 327 output.append(formatter.text(" ")) 328 n += 1 329 330 while n <= pages_total: 331 output.append(formatter.pagelink(on=1, pagename=pagename, querystr=getPagingQueryString(querydict, n * results_per_page))) 332 output.append(formatter.text(str(n + 1))) 333 output.append(formatter.pagelink(on=0)) 334 output.append(formatter.text(" ")) 335 n += 1 336 337 output.append(formatter.paragraph(on=0)) 338 339 return "".join(output) 340 341 def getPagingQueryString(querydict, page_from): 342 querydict["from"] = page_from 343 return wikiutil.makeQueryString(querydict) 344 345 def int_or_none(x): 346 if x is None: 347 return x 348 else: 349 return int(x) 350 351 # vim: tabstop=4 expandtab shiftwidth=4