Back to index

moin  1.9.0~rc2
jsrouting.py
Go to the documentation of this file.
00001 # -*- coding: utf-8 -*-
00002 """
00003     werkzeug.contrib.jsrouting
00004     ~~~~~~~~~~~~~~~~~~~~~~~~~~
00005 
00006     Addon module that allows to create a JavaScript function from a map
00007     that generates rules.
00008 
00009     :copyright: (c) 2009 by the Werkzeug Team, see AUTHORS for more details.
00010     :license: BSD, see LICENSE for more details.
00011 """
00012 try:
00013     from simplejson import dumps
00014 except ImportError:
00015     def dumps(*args):
00016         raise RuntimeError('simplejson required for jsrouting')
00017 
00018 from inspect import getmro
00019 from werkzeug.templates import Template
00020 from werkzeug.routing import NumberConverter
00021 
00022 
00023 _javascript_routing_template = Template(u'''\
00024 <% if name_parts %>\
00025 <% for idx in xrange(0, len(name_parts) - 1) %>\
00026 if (typeof ${'.'.join(name_parts[:idx + 1])} === 'undefined') \
00027 ${'.'.join(name_parts[:idx + 1])} = {};
00028 <% endfor %>\
00029 ${'.'.join(name_parts)} = <% endif %>\
00030 (function (server_name, script_name, subdomain, url_scheme) {
00031     var converters = ${', '.join(converters)};
00032     var rules = $rules;
00033     function in_array(array, value) {
00034         if (array.indexOf != undefined) {
00035             return array.indexOf(value) != -1;
00036         }
00037         for (var i = 0; i < array.length; i++) {
00038             if (array[i] == value) {
00039                 return true;
00040             }
00041         }
00042         return false;
00043     }
00044     function array_diff(array1, array2) {
00045         array1 = array1.slice();
00046         for (var i = array1.length-1; i >= 0; i--) {
00047             if (in_array(array2, array1[i])) {
00048                 array1.splice(i, 1);
00049             }
00050         }
00051         return array1;
00052     }
00053     function split_obj(obj) {
00054         var names = [];
00055         var values = [];
00056         for (var name in obj) {
00057             if (typeof(obj[name]) != 'function') {
00058                 names.push(name);
00059                 values.push(obj[name]);
00060             }
00061         }
00062         return {names: names, values: values, original: obj};
00063     }
00064     function suitable(rule, args) {
00065         var default_args = split_obj(rule.defaults || {});
00066         var diff_arg_names = array_diff(rule.arguments, default_args.names);
00067 
00068         for (var i = 0; i < diff_arg_names.length; i++) {
00069             if (!in_array(args.names, diff_arg_names[i])) {
00070                 return false;
00071             }
00072         }
00073 
00074         if (array_diff(rule.arguments, args.names).length == 0) {
00075             if (rule.defaults == null) {
00076                 return true;
00077             }
00078             for (var i = 0; i < default_args.names.length; i++) {
00079                 var key = default_args.names[i];
00080                 var value = default_args.values[i];
00081                 if (value != args.original[key]) {
00082                     return false;
00083                 }
00084             }
00085         }
00086 
00087         return true;
00088     }
00089     function build(rule, args) {
00090         var tmp = [];
00091         var processed = rule.arguments.slice();
00092         for (var i = 0; i < rule.trace.length; i++) {
00093             var part = rule.trace[i];
00094             if (part.is_dynamic) {
00095                 var converter = converters[rule.converters[part.data]];
00096                 var data = converter(args.original[part.data]);
00097                 if (data == null) {
00098                     return null;
00099                 }
00100                 tmp.push(data);
00101                 processed.push(part.name);
00102             } else {
00103                 tmp.push(part.data);
00104             }
00105         }
00106         tmp = tmp.join('');
00107         var pipe = tmp.indexOf('|');
00108         var subdomain = tmp.substring(0, pipe);
00109         var url = tmp.substring(pipe+1);
00110 
00111         var unprocessed = array_diff(args.names, processed);
00112         var first_query_var = true;
00113         for (var i = 0; i < unprocessed.length; i++) {
00114             if (first_query_var) {
00115                 url += '?';
00116             } else {
00117                 url += '&';
00118             }
00119             first_query_var = false;
00120             url += encodeURIComponent(unprocessed[i]);
00121             url += '=';
00122             url += encodeURIComponent(args.original[unprocessed[i]]);
00123         }
00124         return {subdomain: subdomain, path: url};
00125     }
00126     function lstrip(s, c) {
00127         while (s && s.substring(0, 1) == c) {
00128             s = s.substring(1);
00129         }
00130         return s;
00131     }
00132     function rstrip(s, c) {
00133         while (s && s.substring(s.length-1, s.length) == c) {
00134             s = s.substring(0, s.length-1);
00135         }
00136         return s;
00137     }
00138     return function(endpoint, args, force_external) {
00139         args = split_obj(args);
00140         var rv = null;
00141         for (var i = 0; i < rules.length; i++) {
00142             var rule = rules[i];
00143             if (rule.endpoint != endpoint) continue;
00144             if (suitable(rule, args)) {
00145                 rv = build(rule, args);
00146                 if (rv != null) {
00147                     break;
00148                 }
00149             }
00150         }
00151         if (rv == null) {
00152             return null;
00153         }
00154         if (!force_external && rv.subdomain == subdomain) {
00155             return rstrip(script_name, '/') + '/' + lstrip(rv.path, '/');
00156         } else {
00157             return url_scheme + '://'
00158                    + (rv.subdomain ? rv.subdomain + '.' : '')
00159                    + server_name + rstrip(script_name, '/')
00160                    + '/' + lstrip(rv.path, '/');
00161         }
00162     };
00163 })''')
00164 
00165 
00166 def generate_map(map, name='url_map'):
00167     """
00168     Generates a JavaScript function containing the rules defined in
00169     this map, to be used with a MapAdapter's generate_javascript
00170     method.  If you don't pass a name the returned JavaScript code is
00171     an expression that returns a function.  Otherwise it's a standalone
00172     script that assigns the function with that name.  Dotted names are
00173     resolved (so you an use a name like 'obj.url_for')
00174 
00175     In order to use JavaScript generation, simplejson must be installed.
00176 
00177     Note that using this feature will expose the rules
00178     defined in your map to users. If your rules contain sensitive
00179     information, don't use JavaScript generation!
00180     """
00181     map.update()
00182     rules = []
00183     converters = []
00184     for rule in map.iter_rules():
00185         trace = [{
00186             'is_dynamic':   is_dynamic,
00187             'data':         data
00188         } for is_dynamic, data in rule._trace]
00189         rule_converters = {}
00190         for key, converter in rule._converters.iteritems():
00191             js_func = js_to_url_function(converter)
00192             try:
00193                 index = converters.index(js_func)
00194             except ValueError:
00195                 converters.append(js_func)
00196                 index = len(converters) - 1
00197             rule_converters[key] = index
00198         rules.append({
00199             u'endpoint':    rule.endpoint,
00200             u'arguments':   list(rule.arguments),
00201             u'converters':  rule_converters,
00202             u'trace':       trace,
00203             u'defaults':    rule.defaults
00204         })
00205 
00206     return _javascript_routing_template.render({
00207         'name_parts':   name and name.split('.') or [],
00208         'rules':        dumps(rules),
00209         'converters':   converters
00210     })
00211 
00212 
00213 def generate_adapter(adapter, name='url_for', map_name='url_map'):
00214     """Generates the url building function for a map."""
00215     values = {
00216         u'server_name':     dumps(adapter.server_name),
00217         u'script_name':     dumps(adapter.script_name),
00218         u'subdomain':       dumps(adapter.subdomain),
00219         u'url_scheme':      dumps(adapter.url_scheme),
00220         u'name':            name,
00221         u'map_name':        map_name
00222     }
00223     return u'''\
00224 var %(name)s = %(map_name)s(
00225     %(server_name)s,
00226     %(script_name)s,
00227     %(subdomain)s,
00228     %(url_scheme)s
00229 );''' % values
00230 
00231 
00232 def js_to_url_function(converter):
00233     """Get the JavaScript converter function from a rule."""
00234     if hasattr(converter, 'js_to_url_function'):
00235         data = converter.js_to_url_function()
00236     else:
00237         for cls in getmro(type(converter)):
00238             if cls in js_to_url_functions:
00239                 data = js_to_url_functions[cls](converter)
00240                 break
00241         else:
00242             return 'encodeURIComponent'
00243     return '(function(value) { %s })' % data
00244 
00245 
00246 def NumberConverter_js_to_url(conv):
00247     if conv.fixed_digits:
00248         return u'''\
00249 var result = value.toString();
00250 while (result.length < %s)
00251     result = '0' + result;
00252 return result;''' % conv.fixed_digits
00253     return u'return value.toString();'
00254 
00255 
00256 js_to_url_functions = {
00257     NumberConverter:    NumberConverter_js_to_url
00258 }