Mercurial > piecrust2
comparison piecrust/routing.py @ 711:ab5c6a8ae90a
bake: Replace hard-coded taxonomy support with "generator" system.
* Taxonomies are now implemented one or more `TaxonomyGenerator`s.
* A `BlogArchivesGenerator` stub is there but non-functional.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Thu, 26 May 2016 19:52:47 -0700 |
parents | 2f780b191541 |
children | 606f6d57b5df |
comparison
equal
deleted
inserted
replaced
710:e85f29b28b84 | 711:ab5c6a8ae90a |
---|---|
1 import re | 1 import re |
2 import os.path | 2 import os.path |
3 import copy | 3 import copy |
4 import logging | 4 import logging |
5 import urllib.parse | 5 import urllib.parse |
6 import unidecode | 6 from werkzeug.utils import cached_property |
7 | 7 |
8 | 8 |
9 logger = logging.getLogger(__name__) | 9 logger = logging.getLogger(__name__) |
10 | 10 |
11 | 11 |
12 route_re = re.compile(r'%((?P<qual>path):)?(?P<name>\w+)%') | 12 route_re = re.compile(r'%((?P<qual>path):)?(?P<name>\w+)%') |
13 route_esc_re = re.compile(r'\\%((?P<qual>path)\\:)?(?P<name>\w+)\\%') | 13 route_esc_re = re.compile(r'\\%((?P<qual>path)\\:)?(?P<name>\w+)\\%') |
14 template_func_re = re.compile(r'^(?P<name>\w+)\((?P<first_arg>\w+)' | 14 template_func_re = re.compile(r'^(?P<name>\w+)\((?P<args>.*)\)\s*$') |
15 r'(?P<other_args>.*)\)\s*$') | 15 template_func_arg_re = re.compile(r'(?P<arg>\+?\w+)') |
16 template_func_arg_re = re.compile(r',\s*(?P<arg>\w+)') | |
17 ugly_url_cleaner = re.compile(r'\.html$') | 16 ugly_url_cleaner = re.compile(r'\.html$') |
18 | 17 |
19 | 18 |
20 class RouteNotFoundError(Exception): | 19 class RouteNotFoundError(Exception): |
20 pass | |
21 | |
22 | |
23 class InvalidRouteError(Exception): | |
21 pass | 24 pass |
22 | 25 |
23 | 26 |
24 def create_route_metadata(page): | 27 def create_route_metadata(page): |
25 route_metadata = copy.deepcopy(page.source_metadata) | 28 route_metadata = copy.deepcopy(page.source_metadata) |
36 class IRouteMetadataProvider(object): | 39 class IRouteMetadataProvider(object): |
37 def getRouteMetadata(self): | 40 def getRouteMetadata(self): |
38 raise NotImplementedError() | 41 raise NotImplementedError() |
39 | 42 |
40 | 43 |
41 SLUGIFY_ENCODE = 1 | 44 ROUTE_TYPE_SOURCE = 0 |
42 SLUGIFY_TRANSLITERATE = 2 | 45 ROUTE_TYPE_GENERATOR = 1 |
43 SLUGIFY_LOWERCASE = 4 | |
44 SLUGIFY_DOT_TO_DASH = 8 | |
45 SLUGIFY_SPACE_TO_DASH = 16 | |
46 | |
47 | |
48 re_first_dot_to_dash = re.compile(r'^\.+') | |
49 re_dot_to_dash = re.compile(r'\.+') | |
50 re_space_to_dash = re.compile(r'\s+') | |
51 | |
52 | |
53 def _parse_slugify_mode(value): | |
54 mapping = { | |
55 'encode': SLUGIFY_ENCODE, | |
56 'transliterate': SLUGIFY_TRANSLITERATE, | |
57 'lowercase': SLUGIFY_LOWERCASE, | |
58 'dot_to_dash': SLUGIFY_DOT_TO_DASH, | |
59 'space_to_dash': SLUGIFY_SPACE_TO_DASH} | |
60 mode = 0 | |
61 for v in value.split(','): | |
62 f = mapping.get(v.strip()) | |
63 if f is None: | |
64 if v == 'iconv': | |
65 raise Exception("'iconv' is not supported as a slugify mode " | |
66 "in PieCrust2. Use 'transliterate'.") | |
67 raise Exception("Unknown slugify flag: %s" % v) | |
68 mode |= f | |
69 return mode | |
70 | 46 |
71 | 47 |
72 class Route(object): | 48 class Route(object): |
73 """ Information about a route for a PieCrust application. | 49 """ Information about a route for a PieCrust application. |
74 Each route defines the "shape" of an URL and how it maps to | 50 Each route defines the "shape" of an URL and how it maps to |
75 sources and taxonomies. | 51 sources and generators. |
76 """ | 52 """ |
77 def __init__(self, app, cfg): | 53 def __init__(self, app, cfg): |
78 self.app = app | 54 self.app = app |
79 | 55 |
80 self.source_name = cfg['source'] | 56 self.source_name = cfg.get('source') |
81 self.taxonomy_name = cfg.get('taxonomy') | 57 self.generator_name = cfg.get('generator') |
82 self.taxonomy_term_sep = cfg.get('term_separator', '/') | 58 if not self.source_name and not self.generator_name: |
83 | 59 raise InvalidRouteError( |
84 sm = cfg.get('slugify_mode') | 60 "Both `source` and `generator` are specified.") |
85 if not sm: | |
86 sm = app.config.get('site/slugify_mode', 'encode') | |
87 self.slugify_mode = _parse_slugify_mode(sm) | |
88 | 61 |
89 self.pretty_urls = app.config.get('site/pretty_urls') | 62 self.pretty_urls = app.config.get('site/pretty_urls') |
90 self.trailing_slash = app.config.get('site/trailing_slash') | 63 self.trailing_slash = app.config.get('site/trailing_slash') |
91 self.show_debug_info = app.config.get('site/show_debug_info') | 64 self.show_debug_info = app.config.get('site/show_debug_info') |
92 self.pagination_suffix_format = app.config.get( | 65 self.pagination_suffix_format = app.config.get( |
125 self.required_route_metadata.add(m.group('name')) | 98 self.required_route_metadata.add(m.group('name')) |
126 | 99 |
127 self.template_func = None | 100 self.template_func = None |
128 self.template_func_name = None | 101 self.template_func_name = None |
129 self.template_func_args = [] | 102 self.template_func_args = [] |
103 self.template_func_vararg = None | |
130 self._createTemplateFunc(cfg.get('func')) | 104 self._createTemplateFunc(cfg.get('func')) |
131 | 105 |
132 @property | 106 @property |
133 def is_taxonomy_route(self): | 107 def route_type(self): |
134 return self.taxonomy_name is not None | 108 if self.source_name: |
109 return ROUTE_TYPE_SOURCE | |
110 elif self.generator_name: | |
111 return ROUTE_TYPE_GENERATOR | |
112 else: | |
113 raise InvalidRouteError() | |
135 | 114 |
136 @property | 115 @property |
116 def is_source_route(self): | |
117 return self.route_type == ROUTE_TYPE_SOURCE | |
118 | |
119 @property | |
120 def is_generator_route(self): | |
121 return self.route_type == ROUTE_TYPE_GENERATOR | |
122 | |
123 @cached_property | |
137 def source(self): | 124 def source(self): |
125 if not self.is_source_route: | |
126 return InvalidRouteError("This is not a source route.") | |
138 for src in self.app.sources: | 127 for src in self.app.sources: |
139 if src.name == self.source_name: | 128 if src.name == self.source_name: |
140 return src | 129 return src |
141 raise Exception("Can't find source '%s' for route '%'." % ( | 130 raise Exception("Can't find source '%s' for route '%s'." % ( |
142 self.source_name, self.uri)) | 131 self.source_name, self.uri)) |
143 | 132 |
144 @property | 133 @cached_property |
145 def source_realm(self): | 134 def generator(self): |
146 return self.source.realm | 135 if not self.is_generator_route: |
136 return InvalidRouteError("This is not a generator route.") | |
137 for gen in self.app.generators: | |
138 if gen.name == self.generator_name: | |
139 return gen | |
140 raise Exception("Can't find generator '%s' for route '%s'." % ( | |
141 self.generator_name, self.uri)) | |
147 | 142 |
148 def matchesMetadata(self, route_metadata): | 143 def matchesMetadata(self, route_metadata): |
149 return self.required_route_metadata.issubset(route_metadata.keys()) | 144 return self.required_route_metadata.issubset(route_metadata.keys()) |
150 | 145 |
151 def matchUri(self, uri, strict=False): | 146 def matchUri(self, uri, strict=False): |
232 if self.show_debug_info: | 227 if self.show_debug_info: |
233 uri += '?!debug' | 228 uri += '?!debug' |
234 | 229 |
235 return uri | 230 return uri |
236 | 231 |
237 def getTaxonomyTerms(self, route_metadata): | |
238 if not self.is_taxonomy_route: | |
239 raise Exception("This route isn't a taxonomy route.") | |
240 | |
241 tax = self.app.getTaxonomy(self.taxonomy_name) | |
242 all_values = route_metadata.get(tax.term_name) | |
243 if all_values is None: | |
244 raise Exception("'%s' values couldn't be found in route metadata" % | |
245 tax.term_name) | |
246 | |
247 if self.taxonomy_term_sep in all_values: | |
248 return tuple(all_values.split(self.taxonomy_term_sep)) | |
249 return all_values | |
250 | |
251 def slugifyTaxonomyTerm(self, term): | |
252 if isinstance(term, tuple): | |
253 return self.taxonomy_term_sep.join( | |
254 map(self._slugifyOne, term)) | |
255 return self._slugifyOne(term) | |
256 | |
257 def _slugifyOne(self, term): | |
258 if self.slugify_mode & SLUGIFY_TRANSLITERATE: | |
259 term = unidecode.unidecode(term) | |
260 if self.slugify_mode & SLUGIFY_LOWERCASE: | |
261 term = term.lower() | |
262 if self.slugify_mode & SLUGIFY_DOT_TO_DASH: | |
263 term = re_first_dot_to_dash.sub('', term) | |
264 term = re_dot_to_dash.sub('-', term) | |
265 if self.slugify_mode & SLUGIFY_SPACE_TO_DASH: | |
266 term = re_space_to_dash.sub('-', term) | |
267 return term | |
268 | |
269 def _uriFormatRepl(self, m): | 232 def _uriFormatRepl(self, m): |
270 name = m.group('name') | 233 name = m.group('name') |
271 #TODO: fix this hard-coded shit | 234 #TODO: fix this hard-coded shit |
272 if name == 'year': | 235 if name == 'year': |
273 return '%(year)04d' | 236 return '%(year)04d' |
278 return '%(' + name + ')s' | 241 return '%(' + name + ')s' |
279 | 242 |
280 def _uriPatternRepl(self, m): | 243 def _uriPatternRepl(self, m): |
281 name = m.group('name') | 244 name = m.group('name') |
282 qualifier = m.group('qual') | 245 qualifier = m.group('qual') |
283 if qualifier == 'path' or self.taxonomy_name: | 246 if qualifier == 'path': |
284 return r'(?P<%s>[^\?]*)' % name | 247 return r'(?P<%s>[^\?]*)' % name |
285 return r'(?P<%s>[^/\?]+)' % name | 248 return r'(?P<%s>[^/\?]+)' % name |
286 | 249 |
287 def _uriNoPathRepl(self, m): | 250 def _uriNoPathRepl(self, m): |
288 name = m.group('name') | 251 name = m.group('name') |
300 raise Exception("Template function definition for route '%s' " | 263 raise Exception("Template function definition for route '%s' " |
301 "has invalid syntax: %s" % | 264 "has invalid syntax: %s" % |
302 (self.uri_pattern, func_def)) | 265 (self.uri_pattern, func_def)) |
303 | 266 |
304 self.template_func_name = m.group('name') | 267 self.template_func_name = m.group('name') |
305 self.template_func_args.append(m.group('first_arg')) | 268 self.template_func_args = [] |
306 arg_list = m.group('other_args') | 269 arg_list = m.group('args') |
307 if arg_list: | 270 if arg_list: |
308 self.template_func_args += template_func_arg_re.findall(arg_list) | 271 self.template_func_args = template_func_arg_re.findall(arg_list) |
309 | 272 for i in range(len(self.template_func_args) - 1): |
310 if self.taxonomy_name: | 273 if self.template_func_args[i][0] == '+': |
311 # This will be a taxonomy route function... this means we can | 274 raise Exception("Only the last route parameter can be a " |
312 # have a variable number of parameters, but only one parameter | 275 "variable argument (prefixed with `+`)") |
313 # definition, which is the value. | 276 |
314 if len(self.template_func_args) != 1: | 277 if (self.template_func_args and |
315 raise Exception("Route '%s' is a taxonomy route and must have " | 278 self.template_func_args[-1][0] == '+'): |
316 "only one argument, which is the term value." % | 279 self.template_func_vararg = self.template_func_args[-1][1:] |
317 self.uri_pattern) | 280 |
318 | 281 def template_func(*args): |
319 def template_func(*args): | 282 is_variable = (self.template_func_vararg is not None) |
320 if len(args) == 0: | 283 if not is_variable and len(args) != len(self.template_func_args): |
321 raise Exception( | 284 raise Exception( |
322 "Route function '%s' expected at least one " | 285 "Route function '%s' expected %d arguments, " |
323 "argument." % func_def) | 286 "got %d." % |
324 | 287 (func_def, len(self.template_func_args), |
325 # Term combinations can be passed as an array, or as multiple | 288 len(args))) |
326 # arguments. | 289 elif is_variable and len(args) < len(self.template_func_args): |
327 values = args | 290 raise Exception( |
328 if len(args) == 1 and isinstance(args[0], list): | 291 "Route function '%s' expected at least %d arguments, " |
329 values = args[0] | 292 "got %d." % |
330 | 293 (func_def, len(self.template_func_args), |
331 # We need to register this use of a taxonomy term. | 294 len(args))) |
332 if len(values) == 1: | 295 |
333 registered_values = str(values[0]) | 296 metadata = {} |
334 else: | 297 non_var_args = list(self.template_func_args) |
335 registered_values = tuple([str(v) for v in values]) | 298 if is_variable: |
336 eis = self.app.env.exec_info_stack | 299 del non_var_args[-1] |
337 cpi = eis.current_page_info.render_ctx.current_pass_info | 300 |
338 if cpi: | 301 for arg_name, arg_val in zip(non_var_args, args): |
339 cpi.used_taxonomy_terms.add( | 302 #TODO: fix this hard-coded shit. |
340 (self.source_name, self.taxonomy_name, | 303 if arg_name in ['year', 'month', 'day']: |
341 registered_values)) | 304 arg_val = int(arg_val) |
342 | 305 metadata[arg_name] = arg_val |
343 str_values = self.slugifyTaxonomyTerm(registered_values) | 306 |
344 term_name = self.template_func_args[0] | 307 if is_variable: |
345 metadata = {term_name: str_values} | 308 metadata[self.template_func_vararg] = [] |
346 | 309 for i in range(len(non_var_args), len(args)): |
347 return self.getUri(metadata) | 310 metadata[self.template_func_vararg].append(args[i]) |
348 | 311 |
349 else: | 312 if self.is_generator_route: |
350 # Normal route function. | 313 self.generator.onRouteFunctionUsed(self, metadata) |
351 def template_func(*args): | 314 |
352 if len(args) != len(self.template_func_args): | 315 return self.getUri(metadata) |
353 raise Exception( | |
354 "Route function '%s' expected %d arguments, " | |
355 "got %d." % | |
356 (func_def, len(self.template_func_args), | |
357 len(args))) | |
358 metadata = {} | |
359 for arg_name, arg_val in zip(self.template_func_args, args): | |
360 #TODO: fix this hard-coded shit. | |
361 if arg_name in ['year', 'month', 'day']: | |
362 arg_val = int(arg_val) | |
363 metadata[arg_name] = arg_val | |
364 return self.getUri(metadata) | |
365 | 316 |
366 self.template_func = template_func | 317 self.template_func = template_func |
367 | 318 |
368 | 319 |
369 class CompositeRouteFunction(object): | 320 class CompositeRouteFunction(object): |