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