1 # -*- coding: utf-8 -*-
15 from subprocess import Popen, PIPE
16 from urlparse import urlparse
21 from lxml import etree, html
26 from openerp.tools.func import lazy_property
27 import openerp.tools.lru
28 from openerp.http import request
29 from openerp.tools.safe_eval import safe_eval as eval
30 from openerp.osv import osv, orm, fields
31 from openerp.tools import html_escape as escape
32 from openerp.tools.translate import _
34 _logger = logging.getLogger(__name__)
36 #--------------------------------------------------------------------
37 # QWeb template engine
38 #--------------------------------------------------------------------
39 class QWebException(Exception):
40 def __init__(self, message, **kw):
41 Exception.__init__(self, message)
44 if 'node' not in self.qweb:
46 return etree.tostring(self.qweb['node'], pretty_print=True)
48 class QWebTemplateNotFound(QWebException):
51 def raise_qweb_exception(etype=None, **kw):
54 orig_type, original, tb = sys.exc_info()
56 raise etype, original, tb
58 for k, v in kw.items():
60 # Will use `raise foo from bar` in python 3 and rename cause to __cause__
61 e.qweb['cause'] = original
64 class QWebContext(dict):
65 def __init__(self, cr, uid, data, loader=None, templates=None, context=None):
69 self.templates = templates or {}
70 self.context = context
72 super(QWebContext, self).__init__(dic)
73 self['defined'] = lambda key: key in self
75 def safe_eval(self, expr):
76 locals_dict = collections.defaultdict(lambda: None)
77 locals_dict.update(self)
78 locals_dict.pop('cr', None)
79 locals_dict.pop('loader', None)
80 return eval(expr, None, locals_dict, nocopy=True, locals_builtins=True)
83 return QWebContext(self.cr, self.uid, dict.copy(self),
85 templates=self.templates,
91 class QWeb(orm.AbstractModel):
92 """QWeb Xml templating engine
94 The templating engine use a very simple syntax based "magic" xml
95 attributes, to produce textual output (even non-xml).
97 The core magic attributes are:
100 t-if t-foreach t-call
103 t-att t-raw t-esc t-trim
105 assignation attribute:
108 QWeb can be extended like any OpenERP model and new attributes can be
111 If you need to customize t-fields rendering, subclass the ir.qweb.field
112 model (and its sub-models) then override :meth:`~.get_converter_for` to
113 fetch the right field converters for your qweb model.
115 Beware that if you need extensions or alterations which could be
116 incompatible with other subsystems, you should create a local object
117 inheriting from ``ir.qweb`` and customize that.
122 _void_elements = frozenset([
123 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
124 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'])
125 _format_regex = re.compile(
130 # jinja-style pattern
134 def __init__(self, pool, cr):
135 super(QWeb, self).__init__(pool, cr)
137 self._render_tag = self.prefixed_methods('render_tag_')
138 self._render_att = self.prefixed_methods('render_att_')
140 def prefixed_methods(self, prefix):
141 """ Extracts all methods prefixed by ``prefix``, and returns a mapping
142 of (t-name, method) where the t-name is the method name with prefix
143 removed and underscore converted to dashes
148 n_prefix = len(prefix)
150 (name[n_prefix:].replace('_', '-'), getattr(type(self), name))
151 for name in dir(self)
152 if name.startswith(prefix)
155 def register_tag(self, tag, func):
156 self._render_tag[tag] = func
158 def add_template(self, qwebcontext, name, node):
159 """Add a parsed template in the context. Used to preprocess templates."""
160 qwebcontext.templates[name] = node
162 def load_document(self, document, res_id, qwebcontext):
164 Loads an XML document and installs any contained template in the engine
166 if hasattr(document, 'documentElement'):
168 elif document.startswith("<?xml"):
169 dom = etree.fromstring(document)
171 dom = etree.parse(document)
174 if node.get('t-name'):
175 name = str(node.get("t-name"))
176 self.add_template(qwebcontext, name, node)
177 if res_id and node.tag == "t":
178 self.add_template(qwebcontext, res_id, node)
181 def get_template(self, name, qwebcontext):
182 origin_template = qwebcontext.get('__caller__') or qwebcontext['__stack__'][0]
183 if qwebcontext.loader and name not in qwebcontext.templates:
185 xml_doc = qwebcontext.loader(name)
187 raise_qweb_exception(QWebTemplateNotFound, message="Loader could not find template %r" % name, template=origin_template)
188 self.load_document(xml_doc, isinstance(name, (int, long)) and name or None, qwebcontext=qwebcontext)
190 if name in qwebcontext.templates:
191 return qwebcontext.templates[name]
193 raise QWebTemplateNotFound("Template %r not found" % name, template=origin_template)
195 def eval(self, expr, qwebcontext):
197 return qwebcontext.safe_eval(expr)
199 template = qwebcontext.get('__template__')
200 raise_qweb_exception(message="Could not evaluate expression %r" % expr, expression=expr, template=template)
202 def eval_object(self, expr, qwebcontext):
203 return self.eval(expr, qwebcontext)
205 def eval_str(self, expr, qwebcontext):
207 return qwebcontext.get(0, '')
208 val = self.eval(expr, qwebcontext)
209 if isinstance(val, unicode):
210 return val.encode("utf8")
211 if val is False or val is None:
215 def eval_format(self, expr, qwebcontext):
216 expr, replacements = self._format_regex.subn(
217 lambda m: self.eval_str(m.group(1) or m.group(2), qwebcontext),
225 return str(expr % qwebcontext)
227 template = qwebcontext.get('__template__')
228 raise_qweb_exception(message="Format error for expression %r" % expr, expression=expr, template=template)
230 def eval_bool(self, expr, qwebcontext):
231 return int(bool(self.eval(expr, qwebcontext)))
233 def render(self, cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None):
234 if qwebcontext is None:
237 if not isinstance(qwebcontext, QWebContext):
238 qwebcontext = QWebContext(cr, uid, qwebcontext, loader=loader, context=context)
240 qwebcontext['__template__'] = id_or_xml_id
241 stack = qwebcontext.get('__stack__', [])
243 qwebcontext['__caller__'] = stack[-1]
244 stack.append(id_or_xml_id)
245 qwebcontext['__stack__'] = stack
246 qwebcontext['xmlid'] = str(stack[0]) # Temporary fix
247 return self.render_node(self.get_template(id_or_xml_id, qwebcontext), qwebcontext)
249 def render_node(self, element, qwebcontext):
250 generated_attributes = ""
252 template_attributes = {}
253 for (attribute_name, attribute_value) in element.attrib.iteritems():
254 attribute_name = str(attribute_name)
255 if attribute_name == "groups":
256 cr = qwebcontext.get('request') and qwebcontext['request'].cr or None
257 uid = qwebcontext.get('request') and qwebcontext['request'].uid or None
258 can_see = self.user_has_groups(cr, uid, groups=attribute_value) if cr and uid else False
262 attribute_value = attribute_value.encode("utf8")
264 if attribute_name.startswith("t-"):
265 for attribute in self._render_att:
266 if attribute_name[2:].startswith(attribute):
267 att, val = self._render_att[attribute](self, element, attribute_name, attribute_value, qwebcontext)
269 generated_attributes += self.render_attribute(element, att, val, qwebcontext)
272 if attribute_name[2:] in self._render_tag:
273 t_render = attribute_name[2:]
274 template_attributes[attribute_name[2:]] = attribute_value
276 generated_attributes += self.render_attribute(element, attribute_name, attribute_value, qwebcontext)
278 if 'debug' in template_attributes:
279 debugger = template_attributes.get('debug', 'pdb')
280 __import__(debugger).set_trace() # pdb, ipdb, pudb, ...
282 result = self._render_tag[t_render](self, element, template_attributes, generated_attributes, qwebcontext)
284 result = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
287 result += element.tail.encode('utf-8')
289 if isinstance(result, unicode):
290 return result.encode('utf-8')
293 def render_element(self, element, template_attributes, generated_attributes, qwebcontext, inner=None):
295 # template_attributes: t-* attributes
296 # generated_attributes: generated attributes
297 # qwebcontext: values
298 # inner: optional innerXml
300 g_inner = inner.encode('utf-8') if isinstance(inner, unicode) else inner
302 g_inner = [] if element.text is None else [element.text.encode('utf-8')]
303 for current_node in element.iterchildren(tag=etree.Element):
305 g_inner.append(self.render_node(current_node, qwebcontext))
306 except QWebException:
309 template = qwebcontext.get('__template__')
310 raise_qweb_exception(message="Could not render element %r" % element.tag, node=element, template=template)
311 name = str(element.tag)
312 inner = "".join(g_inner)
313 trim = template_attributes.get("trim", 0)
317 inner = inner.lstrip()
318 elif trim == 'right':
319 inner = inner.rstrip()
321 inner = inner.strip()
324 elif len(inner) or name not in self._void_elements:
325 return "<%s%s>%s</%s>" % tuple(
326 qwebcontext if isinstance(qwebcontext, str) else qwebcontext.encode('utf-8')
327 for qwebcontext in (name, generated_attributes, inner, name)
330 return "<%s%s/>" % (name, generated_attributes)
332 def render_attribute(self, element, name, value, qwebcontext):
333 return ' %s="%s"' % (name, escape(value))
336 def render_att_att(self, element, attribute_name, attribute_value, qwebcontext):
337 if attribute_name.startswith("t-attf-"):
338 att, val = attribute_name[7:], self.eval_format(attribute_value, qwebcontext)
339 elif attribute_name.startswith("t-att-"):
340 att, val = attribute_name[6:], self.eval(attribute_value, qwebcontext)
342 att, val = self.eval_object(attribute_value, qwebcontext)
343 if val and not isinstance(val, str):
344 val = unicode(val).encode("utf8")
348 def render_tag_raw(self, element, template_attributes, generated_attributes, qwebcontext):
349 inner = self.eval_str(template_attributes["raw"], qwebcontext)
350 return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
352 def render_tag_esc(self, element, template_attributes, generated_attributes, qwebcontext):
353 options = json.loads(template_attributes.get('esc-options') or '{}')
354 widget = self.get_widget_for(options.get('widget'))
355 inner = widget.format(template_attributes['esc'], options, qwebcontext)
356 return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
358 def render_tag_foreach(self, element, template_attributes, generated_attributes, qwebcontext):
359 expr = template_attributes["foreach"]
360 enum = self.eval_object(expr, qwebcontext)
362 template = qwebcontext.get('__template__')
363 raise QWebException("foreach enumerator %r is not defined while rendering template %r" % (expr, template), template=template)
365 varname = template_attributes['as'].replace('.', '_')
366 copy_qwebcontext = qwebcontext.copy()
368 if isinstance(enum, collections.Sized):
370 copy_qwebcontext["%s_size" % varname] = size
371 copy_qwebcontext["%s_all" % varname] = enum
373 for index, item in enumerate(enum):
374 copy_qwebcontext.update({
376 '%s_value' % varname: item,
377 '%s_index' % varname: index,
378 '%s_first' % varname: index == 0,
379 '%s_last' % varname: index + 1 == size,
382 copy_qwebcontext.update({
383 '%s_parity' % varname: 'odd',
384 '%s_even' % varname: False,
385 '%s_odd' % varname: True,
388 copy_qwebcontext.update({
389 '%s_parity' % varname: 'even',
390 '%s_even' % varname: True,
391 '%s_odd' % varname: False,
393 ru.append(self.render_element(element, template_attributes, generated_attributes, copy_qwebcontext))
396 def render_tag_if(self, element, template_attributes, generated_attributes, qwebcontext):
397 if self.eval_bool(template_attributes["if"], qwebcontext):
398 return self.render_element(element, template_attributes, generated_attributes, qwebcontext)
401 def render_tag_call(self, element, template_attributes, generated_attributes, qwebcontext):
402 d = qwebcontext.copy()
403 d[0] = self.render_element(element, template_attributes, generated_attributes, d)
404 cr = d.get('request') and d['request'].cr or None
405 uid = d.get('request') and d['request'].uid or None
407 template = self.eval_format(template_attributes["call"], d)
409 template = int(template)
412 return self.render(cr, uid, template, d)
414 def render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext):
415 """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
417 # An asset bundle is rendered in two differents contexts (when genereting html and
418 # when generating the bundle itself) so they must be qwebcontext free
419 # even '0' variable is forbidden
420 template = qwebcontext.get('__template__')
421 raise QWebException("t-call-assets cannot contain children nodes", template=template)
422 xmlid = template_attributes['call-assets']
423 cr, uid, context = [getattr(qwebcontext, attr) for attr in ('cr', 'uid', 'context')]
424 bundle = AssetsBundle(xmlid, cr=cr, uid=uid, context=context, registry=self.pool)
425 css = self.get_attr_bool(template_attributes.get('css'), default=True)
426 js = self.get_attr_bool(template_attributes.get('js'), default=True)
427 return bundle.to_html(css=css, js=js, debug=bool(qwebcontext.get('debug')))
429 def render_tag_set(self, element, template_attributes, generated_attributes, qwebcontext):
430 if "value" in template_attributes:
431 qwebcontext[template_attributes["set"]] = self.eval_object(template_attributes["value"], qwebcontext)
432 elif "valuef" in template_attributes:
433 qwebcontext[template_attributes["set"]] = self.eval_format(template_attributes["valuef"], qwebcontext)
435 qwebcontext[template_attributes["set"]] = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
438 def render_tag_field(self, element, template_attributes, generated_attributes, qwebcontext):
439 """ eg: <span t-record="browse_record(res.partner, 1)" t-field="phone">+1 555 555 8069</span>"""
440 node_name = element.tag
441 assert node_name not in ("table", "tbody", "thead", "tfoot", "tr", "td",
442 "li", "ul", "ol", "dl", "dt", "dd"),\
443 "RTE widgets do not work correctly on %r elements" % node_name
444 assert node_name != 't',\
445 "t-field can not be used on a t element, provide an actual HTML node"
447 record, field_name = template_attributes["field"].rsplit('.', 1)
448 record = self.eval_object(record, qwebcontext)
450 column = record._all_columns[field_name].column
451 options = json.loads(template_attributes.get('field-options') or '{}')
452 field_type = get_field_type(column, options)
454 converter = self.get_converter_for(field_type)
456 return converter.to_html(qwebcontext.cr, qwebcontext.uid, field_name, record, options,
457 element, template_attributes, generated_attributes, qwebcontext, context=qwebcontext.context)
459 def get_converter_for(self, field_type):
460 return self.pool.get('ir.qweb.field.' + field_type, self.pool['ir.qweb.field'])
462 def get_widget_for(self, widget):
463 widget_model = ('ir.qweb.widget.' + widget) if widget else 'ir.qweb.widget'
464 return self.pool.get(widget_model) or self.pool['ir.qweb.widget']
466 def get_attr_bool(self, attr, default=False):
469 if attr in ('false', '0'):
471 elif attr in ('true', '1'):
475 #--------------------------------------------------------------------
476 # QWeb Fields converters
477 #--------------------------------------------------------------------
479 class FieldConverter(osv.AbstractModel):
480 """ Used to convert a t-field specification into an output HTML field.
482 :meth:`~.to_html` is the entry point of this conversion from QWeb, it:
484 * converts the record value to html using :meth:`~.record_to_html`
485 * generates the metadata attributes (``data-oe-``) to set on the root
487 * generates the root result node itself through :meth:`~.render_element`
489 _name = 'ir.qweb.field'
491 def attributes(self, cr, uid, field_name, record, options,
492 source_element, g_att, t_att, qweb_context,
495 Generates the metadata attributes (prefixed by ``data-oe-`` for the
496 root node of the field conversion. Attribute values are escaped by the
499 The default attributes are:
501 * ``model``, the name of the record's model
502 * ``id`` the id of the record to which the field belongs
503 * ``field`` the name of the converted field
504 * ``type`` the logical field type (widget, may not match the column's
505 ``type``, may not be any _column subclass name)
506 * ``translate``, a boolean flag (``0`` or ``1``) denoting whether the
507 column is translatable
508 * ``expression``, the original expression
510 :returns: iterable of (attribute name, attribute value) pairs.
512 column = record._all_columns[field_name].column
513 field_type = get_field_type(column, options)
515 ('data-oe-model', record._name),
516 ('data-oe-id', record.id),
517 ('data-oe-field', field_name),
518 ('data-oe-type', field_type),
519 ('data-oe-expression', t_att['field']),
522 def value_to_html(self, cr, uid, value, column, options=None, context=None):
523 """ Converts a single value to its HTML version/output
525 if not value: return ''
528 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
529 """ Converts the specified field of the browse_record ``record`` to
532 return self.value_to_html(
533 cr, uid, record[field_name], column, options=options, context=context)
535 def to_html(self, cr, uid, field_name, record, options,
536 source_element, t_att, g_att, qweb_context, context=None):
537 """ Converts a ``t-field`` to its HTML output. A ``t-field`` may be
538 extended by a ``t-field-options``, which is a JSON-serialized mapping
539 of configuration values.
541 A default configuration key is ``widget`` which can override the
542 field's own ``_type``.
545 content = self.record_to_html(
546 cr, uid, field_name, record,
547 record._all_columns[field_name].column,
548 options, context=context)
549 if options.get('html-escape', True):
550 content = escape(content)
551 elif hasattr(content, '__html__'):
552 content = content.__html__()
554 _logger.warning("Could not get field %s for model %s",
555 field_name, record._name, exc_info=True)
558 inherit_branding = context and context.get('inherit_branding')
559 if not inherit_branding and context and context.get('inherit_branding_auto'):
560 inherit_branding = self.pool['ir.model.access'].check(cr, uid, record._name, 'write', False, context=context)
563 # add branding attributes
565 ' %s="%s"' % (name, escape(value))
566 for name, value in self.attributes(
567 cr, uid, field_name, record, options,
568 source_element, g_att, t_att, qweb_context)
571 return self.render_element(cr, uid, source_element, t_att, g_att,
572 qweb_context, content)
574 def qweb_object(self):
575 return self.pool['ir.qweb']
577 def render_element(self, cr, uid, source_element, t_att, g_att,
578 qweb_context, content):
579 """ Final rendering hook, by default just calls ir.qweb's ``render_element``
581 return self.qweb_object().render_element(
582 source_element, t_att, g_att, qweb_context, content or '')
584 def user_lang(self, cr, uid, context):
586 Fetches the res.lang object corresponding to the language code stored
587 in the user's context. Fallbacks to en_US if no lang is present in the
588 context *or the language code is not valid*.
590 :returns: res.lang browse_record
592 if context is None: context = {}
594 lang_code = context.get('lang') or 'en_US'
595 Lang = self.pool['res.lang']
597 lang_ids = Lang.search(cr, uid, [('code', '=', lang_code)], context=context) \
598 or Lang.search(cr, uid, [('code', '=', 'en_US')], context=context)
600 return Lang.browse(cr, uid, lang_ids[0], context=context)
602 class FloatConverter(osv.AbstractModel):
603 _name = 'ir.qweb.field.float'
604 _inherit = 'ir.qweb.field'
606 def precision(self, cr, uid, column, options=None, context=None):
607 _, precision = column.digits or (None, None)
610 def value_to_html(self, cr, uid, value, column, options=None, context=None):
613 precision = self.precision(cr, uid, column, options=options, context=context)
614 fmt = '%f' if precision is None else '%.{precision}f'
616 lang_code = context.get('lang') or 'en_US'
617 lang = self.pool['res.lang']
618 formatted = lang.format(cr, uid, [lang_code], fmt.format(precision=precision), value, grouping=True)
620 # %f does not strip trailing zeroes. %g does but its precision causes
621 # it to switch to scientific notation starting at a million *and* to
622 # strip decimals. So use %f and if no precision was specified manually
624 if precision is None:
625 formatted = re.sub(r'(?:(0|\d+?)0+)$', r'\1', formatted)
628 class DateConverter(osv.AbstractModel):
629 _name = 'ir.qweb.field.date'
630 _inherit = 'ir.qweb.field'
632 def value_to_html(self, cr, uid, value, column, options=None, context=None):
633 if not value: return ''
634 lang = self.user_lang(cr, uid, context=context)
635 locale = babel.Locale.parse(lang.code)
637 if isinstance(value, basestring):
638 value = datetime.datetime.strptime(
639 value, openerp.tools.DEFAULT_SERVER_DATE_FORMAT)
641 if options and 'format' in options:
642 pattern = options['format']
644 strftime_pattern = lang.date_format
645 pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
647 return babel.dates.format_date(
648 value, format=pattern,
651 class DateTimeConverter(osv.AbstractModel):
652 _name = 'ir.qweb.field.datetime'
653 _inherit = 'ir.qweb.field'
655 def value_to_html(self, cr, uid, value, column, options=None, context=None):
656 if not value: return ''
657 lang = self.user_lang(cr, uid, context=context)
658 locale = babel.Locale.parse(lang.code)
660 if isinstance(value, basestring):
661 value = datetime.datetime.strptime(
662 value, openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT)
663 value = fields.datetime.context_timestamp(
664 cr, uid, timestamp=value, context=context)
666 if options and 'format' in options:
667 pattern = options['format']
669 if options and options.get('only_date') and options.get('only_date') in ["True", 'true']:
670 strftime_pattern = (u"%s" % (lang.date_format))
672 strftime_pattern = (u"%s %s" % (lang.date_format, lang.time_format))
673 pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
675 if options and options.get('hide_seconds'):
676 pattern = pattern.replace(":ss", "").replace(":s", "")
678 return babel.dates.format_datetime(value, format=pattern, locale=locale)
680 class TextConverter(osv.AbstractModel):
681 _name = 'ir.qweb.field.text'
682 _inherit = 'ir.qweb.field'
684 def value_to_html(self, cr, uid, value, column, options=None, context=None):
686 Escapes the value and converts newlines to br. This is bullshit.
688 if not value: return ''
690 return nl2br(value, options=options)
692 class SelectionConverter(osv.AbstractModel):
693 _name = 'ir.qweb.field.selection'
694 _inherit = 'ir.qweb.field'
696 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
697 value = record[field_name]
698 if not value: return ''
699 selection = dict(fields.selection.reify(
700 cr, uid, record._model, column, context=context))
701 return self.value_to_html(
702 cr, uid, selection[value], column, options=options)
704 class ManyToOneConverter(osv.AbstractModel):
705 _name = 'ir.qweb.field.many2one'
706 _inherit = 'ir.qweb.field'
708 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
709 [read] = record.read([field_name])
710 if not read[field_name]: return ''
711 _, value = read[field_name]
712 return nl2br(value, options=options)
714 class HTMLConverter(osv.AbstractModel):
715 _name = 'ir.qweb.field.html'
716 _inherit = 'ir.qweb.field'
718 def value_to_html(self, cr, uid, value, column, options=None, context=None):
719 return HTMLSafe(value or '')
721 class ImageConverter(osv.AbstractModel):
722 """ ``image`` widget rendering, inserts a data:uri-using image tag in the
723 document. May be overridden by e.g. the website module to generate links
726 .. todo:: what happens if different output need different converters? e.g.
727 reports may need embedded images or FS links whereas website
730 _name = 'ir.qweb.field.image'
731 _inherit = 'ir.qweb.field'
733 def value_to_html(self, cr, uid, value, column, options=None, context=None):
735 image = Image.open(cStringIO.StringIO(value.decode('base64')))
738 raise ValueError("Non-image binary fields can not be converted to HTML")
739 except: # image.verify() throws "suitable exceptions", I have no idea what they are
740 raise ValueError("Invalid image content")
742 return HTMLSafe('<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value))
744 class MonetaryConverter(osv.AbstractModel):
745 """ ``monetary`` converter, has a mandatory option
746 ``display_currency``.
748 The currency is used for formatting *and rounding* of the float value. It
749 is assumed that the linked res_currency has a non-empty rounding value and
750 res.currency's ``round`` method is used to perform rounding.
752 .. note:: the monetary converter internally adds the qweb context to its
753 options mapping, so that the context is available to callees.
754 It's set under the ``_qweb_context`` key.
756 _name = 'ir.qweb.field.monetary'
757 _inherit = 'ir.qweb.field'
759 def to_html(self, cr, uid, field_name, record, options,
760 source_element, t_att, g_att, qweb_context, context=None):
761 options['_qweb_context'] = qweb_context
762 return super(MonetaryConverter, self).to_html(
763 cr, uid, field_name, record, options,
764 source_element, t_att, g_att, qweb_context, context=context)
766 def record_to_html(self, cr, uid, field_name, record, column, options, context=None):
769 Currency = self.pool['res.currency']
770 display_currency = self.display_currency(cr, uid, options['display_currency'], options)
772 # lang.format mandates a sprintf-style format. These formats are non-
773 # minimal (they have a default fixed precision instead), and
774 # lang.format will not set one by default. currency.round will not
775 # provide one either. So we need to generate a precision value
776 # (integer > 0) from the currency's rounding (a float generally < 1.0).
778 # The log10 of the rounding should be the number of digits involved if
779 # negative, if positive clamp to 0 digits and call it a day.
780 # nb: int() ~ floor(), we want nearest rounding instead
781 precision = int(round(math.log10(display_currency.rounding)))
782 fmt = "%.{0}f".format(-precision if precision < 0 else 0)
784 from_amount = record[field_name]
786 if options.get('from_currency'):
787 from_currency = self.display_currency(cr, uid, options['from_currency'], options)
788 from_amount = Currency.compute(cr, uid, from_currency.id, display_currency.id, from_amount)
790 lang_code = context.get('lang') or 'en_US'
791 lang = self.pool['res.lang']
792 formatted_amount = lang.format(cr, uid, [lang_code],
793 fmt, Currency.round(cr, uid, display_currency, from_amount),
794 grouping=True, monetary=True)
797 if display_currency.position == 'before':
802 return HTMLSafe(u'{pre}<span class="oe_currency_value">{0}</span>{post}'.format(
806 symbol=display_currency.symbol,
809 def display_currency(self, cr, uid, currency, options):
810 return self.qweb_object().eval_object(
811 currency, options['_qweb_context'])
814 ('year', 3600 * 24 * 365),
815 ('month', 3600 * 24 * 30),
816 ('week', 3600 * 24 * 7),
822 class DurationConverter(osv.AbstractModel):
823 """ ``duration`` converter, to display integral or fractional values as
824 human-readable time spans (e.g. 1.5 as "1 hour 30 minutes").
826 Can be used on any numerical field.
828 Has a mandatory option ``unit`` which can be one of ``second``, ``minute``,
829 ``hour``, ``day``, ``week`` or ``year``, used to interpret the numerical
830 field value before converting it.
832 Sub-second values will be ignored.
834 _name = 'ir.qweb.field.duration'
835 _inherit = 'ir.qweb.field'
837 def value_to_html(self, cr, uid, value, column, options=None, context=None):
838 units = dict(TIMEDELTA_UNITS)
840 raise ValueError(_("Durations can't be negative"))
841 if not options or options.get('unit') not in units:
842 raise ValueError(_("A unit must be provided to duration widgets"))
844 locale = babel.Locale.parse(
845 self.user_lang(cr, uid, context=context).code)
846 factor = units[options['unit']]
850 for unit, secs_per_unit in TIMEDELTA_UNITS:
851 v, r = divmod(r, secs_per_unit)
853 section = babel.dates.format_timedelta(
854 v*secs_per_unit, threshold=1, locale=locale)
856 sections.append(section)
857 return ' '.join(sections)
860 class RelativeDatetimeConverter(osv.AbstractModel):
861 _name = 'ir.qweb.field.relative'
862 _inherit = 'ir.qweb.field'
864 def value_to_html(self, cr, uid, value, column, options=None, context=None):
865 parse_format = openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT
866 locale = babel.Locale.parse(
867 self.user_lang(cr, uid, context=context).code)
869 if isinstance(value, basestring):
870 value = datetime.datetime.strptime(value, parse_format)
872 # value should be a naive datetime in UTC. So is fields.datetime.now()
873 reference = datetime.datetime.strptime(column.now(), parse_format)
875 return babel.dates.format_timedelta(
876 value - reference, add_direction=True, locale=locale)
878 class Contact(orm.AbstractModel):
879 _name = 'ir.qweb.field.contact'
880 _inherit = 'ir.qweb.field.many2one'
882 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
888 opf = options.get('fields') or ["name", "address", "phone", "mobile", "fax", "email"]
889 if not getattr(record, field_name):
892 id = getattr(record, field_name).id
893 context.update(show_address=True)
894 field_browse = self.pool[column._obj].browse(cr, openerp.SUPERUSER_ID, id, context=context)
895 value = field_browse.name_get()[0][1]
898 'name': value.split("\n")[0],
899 'address': escape("\n".join(value.split("\n")[1:])),
900 'phone': field_browse.phone,
901 'mobile': field_browse.mobile,
902 'fax': field_browse.fax,
903 'city': field_browse.city,
904 'country_id': field_browse.country_id.display_name,
905 'website': field_browse.website,
906 'email': field_browse.email,
908 'object': field_browse,
912 html = self.pool["ir.ui.view"].render(cr, uid, "base.contact", val, engine='ir.qweb', context=context).decode('utf8')
914 return HTMLSafe(html)
916 class QwebView(orm.AbstractModel):
917 _name = 'ir.qweb.field.qweb'
918 _inherit = 'ir.qweb.field.many2one'
920 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
921 if not getattr(record, field_name):
924 view = getattr(record, field_name)
926 if view._model._name != "ir.ui.view":
927 _logger.warning("%s.%s must be a 'ir.ui.view' model." % (record, field_name))
930 ctx = (context or {}).copy()
931 ctx['object'] = record
932 html = view.render(ctx, engine='ir.qweb', context=ctx).decode('utf8')
934 return HTMLSafe(html)
936 class QwebWidget(osv.AbstractModel):
937 _name = 'ir.qweb.widget'
939 def _format(self, inner, options, qwebcontext):
940 return self.pool['ir.qweb'].eval_str(inner, qwebcontext)
942 def format(self, inner, options, qwebcontext):
943 return escape(self._format(inner, options, qwebcontext))
945 class QwebWidgetMonetary(osv.AbstractModel):
946 _name = 'ir.qweb.widget.monetary'
947 _inherit = 'ir.qweb.widget'
949 def _format(self, inner, options, qwebcontext):
950 inner = self.pool['ir.qweb'].eval(inner, qwebcontext)
951 display = self.pool['ir.qweb'].eval_object(options['display_currency'], qwebcontext)
952 precision = int(round(math.log10(display.rounding)))
953 fmt = "%.{0}f".format(-precision if precision < 0 else 0)
954 lang_code = qwebcontext.context.get('lang') or 'en_US'
955 formatted_amount = self.pool['res.lang'].format(
956 qwebcontext.cr, qwebcontext.uid, [lang_code], fmt, inner, grouping=True, monetary=True
959 if display.position == 'before':
964 return u'{pre}{0}{post}'.format(
965 formatted_amount, pre=pre, post=post
966 ).format(symbol=display.symbol,)
968 class HTMLSafe(object):
969 """ HTMLSafe string wrapper, Werkzeug's escape() has special handling for
970 objects with a ``__html__`` methods but AFAIK does not provide any such
973 Wrapping a string in HTML will prevent its escaping
975 __slots__ = ['string']
976 def __init__(self, string):
982 if isinstance(s, unicode):
983 return s.encode('utf-8')
985 def __unicode__(self):
987 if isinstance(s, str):
988 return s.decode('utf-8')
991 def nl2br(string, options=None):
992 """ Converts newlines to HTML linebreaks in ``string``. Automatically
993 escapes content unless options['html-escape'] is set to False, and returns
994 the result wrapped in an HTMLSafe object.
1000 if options is None: options = {}
1002 if options.get('html-escape', True):
1003 string = escape(string)
1004 return HTMLSafe(string.replace('\n', '<br>\n'))
1006 def get_field_type(column, options):
1007 """ Gets a t-field's effective type from the field's column and its options
1009 return options.get('widget', column._type)
1011 class AssetError(Exception):
1013 class AssetNotFound(AssetError):
1016 class AssetsBundle(object):
1017 # Sass installation:
1019 # sudo gem install sass compass bootstrap-sass
1021 # If the following error is encountered:
1022 # 'ERROR: Cannot load compass.'
1024 # sudo gem install compass --pre
1025 cmd_sass = ['sass', '--stdin', '-t', 'compressed', '--unix-newlines', '--compass', '-r', 'bootstrap-sass']
1026 rx_css_import = re.compile("(@import[^;{]+;?)", re.M)
1027 rx_sass_import = re.compile("""(@import\s?['"]([^'"]+)['"])""")
1028 rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/")
1030 def __init__(self, xmlid, debug=False, cr=None, uid=None, context=None, registry=None):
1032 self.cr = request.cr if cr is None else cr
1033 self.uid = request.uid if uid is None else uid
1034 self.context = request.context if context is None else context
1035 self.registry = request.registry if registry is None else registry
1036 self.javascripts = []
1037 self.stylesheets = []
1038 self.css_errors = []
1040 self._checksum = None
1042 context = self.context.copy()
1043 context['inherit_branding'] = False
1044 context['rendering_bundle'] = True
1045 self.html = self.registry['ir.ui.view'].render(self.cr, self.uid, xmlid, context=context)
1049 fragments = html.fragments_fromstring(self.html)
1050 for el in fragments:
1051 if isinstance(el, basestring):
1052 self.remains.append(el)
1053 elif isinstance(el, html.HtmlElement):
1054 src = el.get('src', '')
1055 href = el.get('href', '')
1056 atype = el.get('type')
1057 media = el.get('media')
1058 if el.tag == 'style':
1059 if atype == 'text/sass' or src.endswith('.sass'):
1060 self.stylesheets.append(SassAsset(self, inline=el.text, media=media))
1062 self.stylesheets.append(StylesheetAsset(self, inline=el.text, media=media))
1063 elif el.tag == 'link' and el.get('rel') == 'stylesheet' and self.can_aggregate(href):
1064 if href.endswith('.sass') or atype == 'text/sass':
1065 self.stylesheets.append(SassAsset(self, url=href, media=media))
1067 self.stylesheets.append(StylesheetAsset(self, url=href, media=media))
1068 elif el.tag == 'script' and not src:
1069 self.javascripts.append(JavascriptAsset(self, inline=el.text))
1070 elif el.tag == 'script' and self.can_aggregate(src):
1071 self.javascripts.append(JavascriptAsset(self, url=src))
1073 self.remains.append(html.tostring(el))
1076 self.remains.append(html.tostring(el))
1078 # notYETimplementederror
1079 raise NotImplementedError
1081 def can_aggregate(self, url):
1082 return not urlparse(url).netloc and not url.startswith(('/web/css', '/web/js'))
1084 def to_html(self, sep=None, css=True, js=True, debug=False):
1089 if css and self.stylesheets:
1091 for style in self.stylesheets:
1092 response.append(style.to_html())
1094 for jscript in self.javascripts:
1095 response.append(jscript.to_html())
1097 url_for = self.context.get('url_for', lambda url: url)
1098 if css and self.stylesheets:
1099 href = '/web/css/%s/%s' % (self.xmlid, self.version)
1100 response.append('<link href="%s" rel="stylesheet"/>' % url_for(href))
1102 src = '/web/js/%s/%s' % (self.xmlid, self.version)
1103 response.append('<script type="text/javascript" src="%s"></script>' % url_for(src))
1104 response.extend(self.remains)
1105 return sep + sep.join(response)
1108 def last_modified(self):
1109 """Returns last modified date of linked files"""
1110 return max(itertools.chain(
1111 (asset.last_modified for asset in self.javascripts),
1112 (asset.last_modified for asset in self.stylesheets),
1117 return self.checksum[0:7]
1122 Not really a full checksum.
1123 We compute a SHA1 on the rendered bundle + max linked files last_modified date
1125 check = self.html + str(self.last_modified)
1126 return hashlib.sha1(check).hexdigest()
1129 content = self.get_cache('js')
1131 content = ';\n'.join(asset.minify() for asset in self.javascripts)
1132 self.set_cache('js', content)
1136 content = self.get_cache('css')
1139 content = '\n'.join(asset.minify() for asset in self.stylesheets)
1142 msg = '\n'.join(self.css_errors)
1143 content += self.css_message(msg.replace('\n', '\\A '))
1145 # move up all @import rules to the top
1148 matches.append(matchobj.group(0))
1151 content = re.sub(self.rx_css_import, push, content)
1153 matches.append(content)
1154 content = u'\n'.join(matches)
1157 self.set_cache('css', content)
1161 def get_cache(self, type):
1163 domain = [('url', '=', '/web/%s/%s/%s' % (type, self.xmlid, self.version))]
1164 bundle = self.registry['ir.attachment'].search_read(self.cr, self.uid, domain, ['datas'], context=self.context)
1165 if bundle and bundle[0]['datas']:
1166 content = bundle[0]['datas'].decode('base64')
1169 def set_cache(self, type, content):
1170 ira = self.registry['ir.attachment']
1171 url_prefix = '/web/%s/%s/' % (type, self.xmlid)
1172 # Invalidate previous caches
1173 oids = ira.search(self.cr, self.uid, [('url', '=like', url_prefix + '%')], context=self.context)
1175 ira.unlink(self.cr, openerp.SUPERUSER_ID, oids, context=self.context)
1176 url = url_prefix + self.version
1177 ira.create(self.cr, openerp.SUPERUSER_ID, dict(
1178 datas=content.encode('utf8').encode('base64'),
1182 ), context=self.context)
1184 def css_message(self, message):
1190 font-family: monospace;
1194 """ % message.replace('"', '\\"')
1196 def compile_sass(self):
1198 Checks if the bundle contains any sass content, then compiles it to css.
1199 Css compilation is done at the bundle level and not in the assets
1200 because they are potentially interdependant.
1202 sass = [asset for asset in self.stylesheets if isinstance(asset, SassAsset)]
1205 source = '\n'.join([asset.get_source() for asset in sass])
1207 # move up all @import rules to the top and exclude file imports
1210 ref = matchobj.group(2)
1211 line = '@import "%s"' % ref
1212 if '.' not in ref and line not in imports and not ref.startswith(('.', '/', '~')):
1213 imports.append(line)
1215 source = re.sub(self.rx_sass_import, push, source)
1216 imports.append(source)
1217 source = u'\n'.join(imports)
1220 compiler = Popen(self.cmd_sass, stdin=PIPE, stdout=PIPE, stderr=PIPE)
1222 msg = "Could not find 'sass' program needed to compile sass/scss files"
1224 self.css_errors.append(msg)
1226 result = compiler.communicate(input=source.encode('utf-8'))
1227 if compiler.returncode:
1228 error = self.get_sass_error(''.join(result), source=source)
1229 _logger.warning(error)
1230 self.css_errors.append(error)
1232 compiled = result[0].strip().decode('utf8')
1233 fragments = self.rx_css_split.split(compiled)[1:]
1235 asset_id = fragments.pop(0)
1236 asset = next(asset for asset in sass if asset.id == asset_id)
1237 asset._content = fragments.pop(0)
1239 def get_sass_error(self, stderr, source=None):
1240 # TODO: try to find out which asset the error belongs to
1241 error = stderr.split('Load paths')[0].replace(' Use --trace for backtrace.', '')
1242 error += "This error occured while compiling the bundle '%s' containing:" % self.xmlid
1243 for asset in self.stylesheets:
1244 if isinstance(asset, SassAsset):
1245 error += '\n - %s' % (asset.url if asset.url else '<inline sass>')
1248 class WebAsset(object):
1251 def __init__(self, bundle, inline=None, url=None):
1252 self.id = str(uuid.uuid4())
1253 self.bundle = bundle
1254 self.inline = inline
1257 self.uid = bundle.uid
1258 self.registry = bundle.registry
1259 self.context = bundle.context
1260 self._content = None
1261 self._filename = None
1262 self._ir_attach = None
1263 name = '<inline asset>' if inline else url
1264 self.name = "%s defined in bundle '%s'" % (name, bundle.xmlid)
1265 if not inline and not url:
1266 raise Exception("An asset should either be inlined or url linked")
1269 if not (self.inline or self._filename or self._ir_attach):
1270 addon = filter(None, self.url.split('/'))[0]
1272 # Test url against modules static assets
1273 mpath = openerp.http.addons_manifest[addon]['addons_path']
1274 self._filename = mpath + self.url.replace('/', os.path.sep)
1277 # Test url against ir.attachments
1278 fields = ['__last_update', 'datas', 'mimetype']
1279 domain = [('type', '=', 'binary'), ('url', '=', self.url)]
1280 ira = self.registry['ir.attachment']
1281 attach = ira.search_read(self.cr, self.uid, domain, fields, context=self.context)
1282 self._ir_attach = attach[0]
1284 raise AssetNotFound("Could not find %s" % self.name)
1287 raise NotImplementedError()
1290 def last_modified(self):
1294 return datetime.datetime.fromtimestamp(os.path.getmtime(self._filename))
1295 elif self._ir_attach:
1296 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
1297 last_update = self._ir_attach['__last_update']
1299 return datetime.datetime.strptime(last_update, server_format + '.%f')
1301 return datetime.datetime.strptime(last_update, server_format)
1304 return datetime.datetime(1970, 1, 1)
1308 if not self._content:
1309 self._content = self.inline or self._fetch_content()
1310 return self._content
1312 def _fetch_content(self):
1313 """ Fetch content from file or database"""
1317 with open(self._filename, 'rb') as fp:
1318 return fp.read().decode('utf-8')
1320 return self._ir_attach['datas'].decode('base64')
1321 except UnicodeDecodeError:
1322 raise AssetError('%s is not utf-8 encoded.' % self.name)
1324 raise AssetNotFound('File %s does not exist.' % self.name)
1326 raise AssetError('Could not get content for %s.' % self.name)
1331 def with_header(self, content=None):
1333 content = self.content
1334 return '\n/* %s */\n%s' % (self.name, content)
1336 class JavascriptAsset(WebAsset):
1338 return self.with_header(rjsmin(self.content))
1340 def _fetch_content(self):
1342 return super(JavascriptAsset, self)._fetch_content()
1343 except AssetError, e:
1344 return "console.error(%s);" % json.dumps(e.message)
1348 return '<script type="text/javascript" src="%s"></script>' % (self.html_url % self.url)
1350 return '<script type="text/javascript" charset="utf-8">%s</script>' % self.with_header()
1352 class StylesheetAsset(WebAsset):
1353 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
1354 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
1355 rx_sourceMap = re.compile(r'(/\*# sourceMappingURL=.*)', re.U)
1356 rx_charset = re.compile(r'(@charset "[^"]+";)', re.U)
1358 def __init__(self, *args, **kw):
1359 self.media = kw.pop('media', None)
1360 super(StylesheetAsset, self).__init__(*args, **kw)
1364 content = super(StylesheetAsset, self).content
1366 content = '@media %s { %s }' % (self.media, content)
1369 def _fetch_content(self):
1371 content = super(StylesheetAsset, self)._fetch_content()
1372 web_dir = os.path.dirname(self.url)
1374 content = self.rx_import.sub(
1375 r"""@import \1%s/""" % (web_dir,),
1379 content = self.rx_url.sub(
1380 r"url(\1%s/" % (web_dir,),
1384 # remove charset declarations, we only support utf-8
1385 content = self.rx_charset.sub('', content)
1386 except AssetError, e:
1387 self.bundle.css_errors.append(e.message)
1392 # remove existing sourcemaps, make no sense after re-mini
1393 content = self.rx_sourceMap.sub('', self.content)
1395 content = re.sub(r'/\*.*?\*/', '', content, flags=re.S)
1397 content = re.sub(r'\s+', ' ', content)
1398 content = re.sub(r' *([{}]) *', r'\1', content)
1399 return self.with_header(content)
1402 media = (' media="%s"' % werkzeug.utils.escape(self.media)) if self.media else ''
1404 href = self.html_url % self.url
1405 return '<link rel="stylesheet" href="%s" type="text/css"%s/>' % (href, media)
1407 return '<style type="text/css"%s>%s</style>' % (media, self.with_header())
1409 class SassAsset(StylesheetAsset):
1411 rx_indent = re.compile(r'^( +|\t+)', re.M)
1416 return self.with_header()
1420 ira = self.registry['ir.attachment']
1421 url = self.html_url % self.url
1422 domain = [('type', '=', 'binary'), ('url', '=', self.url)]
1423 ira_id = ira.search(self.cr, self.uid, domain, context=self.context)
1425 # TODO: update only if needed
1426 ira.write(self.cr, openerp.SUPERUSER_ID, [ira_id], {'datas': self.content}, context=self.context)
1428 ira.create(self.cr, openerp.SUPERUSER_ID, dict(
1429 datas=self.content.encode('utf8').encode('base64'),
1430 mimetype='text/css',
1434 ), context=self.context)
1435 return super(SassAsset, self).to_html()
1437 def get_source(self):
1438 content = textwrap.dedent(self.inline or self._fetch_content())
1442 if self.indent is None:
1444 if self.indent == self.reindent:
1445 # Don't reindent the file if identation is the final one (reindent)
1446 raise StopIteration()
1447 return ind.replace(self.indent, self.reindent)
1450 content = self.rx_indent.sub(fix_indent, content)
1451 except StopIteration:
1453 return "/*! %s */\n%s" % (self.id, content)
1456 """ Minify js with a clever regex.
1457 Taken from http://opensource.perlig.de/rjsmin
1458 Apache License, Version 2.0 """
1460 """ Substitution callback """
1461 groups = match.groups()
1467 (groups[4] and '\n') or
1468 (groups[5] and ' ') or
1469 (groups[6] and ' ') or
1470 (groups[7] and ' ') or
1475 r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
1476 r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
1477 r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
1478 r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
1479 r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
1480 r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
1481 r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
1482 r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
1483 r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
1484 r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
1485 r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
1486 r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
1487 r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
1488 r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
1489 r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
1490 r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
1491 r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
1492 r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
1493 r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
1494 r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
1495 r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script