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 = {}
254 debugger = element.get('t-debug')
255 if debugger is not None:
256 if openerp.tools.config['dev_mode']:
257 __import__(debugger).set_trace() # pdb, ipdb, pudb, ...
259 _logger.warning("@t-debug in template '%s' is only available in --dev mode" % qwebcontext['__template__'])
261 for (attribute_name, attribute_value) in element.attrib.iteritems():
262 attribute_name = str(attribute_name)
263 if attribute_name == "groups":
264 cr = qwebcontext.get('request') and qwebcontext['request'].cr or None
265 uid = qwebcontext.get('request') and qwebcontext['request'].uid or None
266 can_see = self.user_has_groups(cr, uid, groups=attribute_value) if cr and uid else False
270 attribute_value = attribute_value.encode("utf8")
272 if attribute_name.startswith("t-"):
273 for attribute in self._render_att:
274 if attribute_name[2:].startswith(attribute):
275 att, val = self._render_att[attribute](self, element, attribute_name, attribute_value, qwebcontext)
277 generated_attributes += self.render_attribute(element, att, val, qwebcontext)
280 if attribute_name[2:] in self._render_tag:
281 t_render = attribute_name[2:]
282 template_attributes[attribute_name[2:]] = attribute_value
284 generated_attributes += self.render_attribute(element, attribute_name, attribute_value, qwebcontext)
287 result = self._render_tag[t_render](self, element, template_attributes, generated_attributes, qwebcontext)
289 result = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
292 result += self.render_tail(element.tail, element, qwebcontext)
294 if isinstance(result, unicode):
295 return result.encode('utf-8')
298 def render_element(self, element, template_attributes, generated_attributes, qwebcontext, inner=None):
300 # template_attributes: t-* attributes
301 # generated_attributes: generated attributes
302 # qwebcontext: values
303 # inner: optional innerXml
305 g_inner = inner.encode('utf-8') if isinstance(inner, unicode) else inner
307 g_inner = [] if element.text is None else [self.render_text(element.text, element, qwebcontext)]
308 for current_node in element.iterchildren(tag=etree.Element):
310 g_inner.append(self.render_node(current_node, qwebcontext))
311 except QWebException:
314 template = qwebcontext.get('__template__')
315 raise_qweb_exception(message="Could not render element %r" % element.tag, node=element, template=template)
316 name = str(element.tag)
317 inner = "".join(g_inner)
318 trim = template_attributes.get("trim", 0)
322 inner = inner.lstrip()
323 elif trim == 'right':
324 inner = inner.rstrip()
326 inner = inner.strip()
329 elif len(inner) or name not in self._void_elements:
330 return "<%s%s>%s</%s>" % tuple(
331 qwebcontext if isinstance(qwebcontext, str) else qwebcontext.encode('utf-8')
332 for qwebcontext in (name, generated_attributes, inner, name)
335 return "<%s%s/>" % (name, generated_attributes)
337 def render_attribute(self, element, name, value, qwebcontext):
338 return ' %s="%s"' % (name, escape(value))
340 def render_text(self, text, element, qwebcontext):
341 return text.encode('utf-8')
343 def render_tail(self, tail, element, qwebcontext):
344 return tail.encode('utf-8')
347 def render_att_att(self, element, attribute_name, attribute_value, qwebcontext):
348 if attribute_name.startswith("t-attf-"):
349 att, val = attribute_name[7:], self.eval_format(attribute_value, qwebcontext)
350 elif attribute_name.startswith("t-att-"):
351 att, val = attribute_name[6:], self.eval(attribute_value, qwebcontext)
353 att, val = self.eval_object(attribute_value, qwebcontext)
354 if val and not isinstance(val, str):
355 val = unicode(val).encode("utf8")
359 def render_tag_raw(self, element, template_attributes, generated_attributes, qwebcontext):
360 inner = self.eval_str(template_attributes["raw"], qwebcontext)
361 return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
363 def render_tag_esc(self, element, template_attributes, generated_attributes, qwebcontext):
364 options = json.loads(template_attributes.get('esc-options') or '{}')
365 widget = self.get_widget_for(options.get('widget'))
366 inner = widget.format(template_attributes['esc'], options, qwebcontext)
367 return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
369 def render_tag_foreach(self, element, template_attributes, generated_attributes, qwebcontext):
370 expr = template_attributes["foreach"]
371 enum = self.eval_object(expr, qwebcontext)
373 template = qwebcontext.get('__template__')
374 raise QWebException("foreach enumerator %r is not defined while rendering template %r" % (expr, template), template=template)
376 varname = template_attributes['as'].replace('.', '_')
377 copy_qwebcontext = qwebcontext.copy()
379 if isinstance(enum, collections.Sized):
381 copy_qwebcontext["%s_size" % varname] = size
382 copy_qwebcontext["%s_all" % varname] = enum
384 for index, item in enumerate(enum):
385 copy_qwebcontext.update({
387 '%s_value' % varname: item,
388 '%s_index' % varname: index,
389 '%s_first' % varname: index == 0,
390 '%s_last' % varname: index + 1 == size,
393 copy_qwebcontext.update({
394 '%s_parity' % varname: 'odd',
395 '%s_even' % varname: False,
396 '%s_odd' % varname: True,
399 copy_qwebcontext.update({
400 '%s_parity' % varname: 'even',
401 '%s_even' % varname: True,
402 '%s_odd' % varname: False,
404 ru.append(self.render_element(element, template_attributes, generated_attributes, copy_qwebcontext))
407 def render_tag_if(self, element, template_attributes, generated_attributes, qwebcontext):
408 if self.eval_bool(template_attributes["if"], qwebcontext):
409 return self.render_element(element, template_attributes, generated_attributes, qwebcontext)
412 def render_tag_call(self, element, template_attributes, generated_attributes, qwebcontext):
413 d = qwebcontext.copy()
414 d[0] = self.render_element(element, template_attributes, generated_attributes, d)
415 cr = d.get('request') and d['request'].cr or None
416 uid = d.get('request') and d['request'].uid or None
418 template = self.eval_format(template_attributes["call"], d)
420 template = int(template)
423 return self.render(cr, uid, template, d)
425 def render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext):
426 """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
428 # An asset bundle is rendered in two differents contexts (when genereting html and
429 # when generating the bundle itself) so they must be qwebcontext free
430 # even '0' variable is forbidden
431 template = qwebcontext.get('__template__')
432 raise QWebException("t-call-assets cannot contain children nodes", template=template)
433 xmlid = template_attributes['call-assets']
434 cr, uid, context = [getattr(qwebcontext, attr) for attr in ('cr', 'uid', 'context')]
435 bundle = AssetsBundle(xmlid, cr=cr, uid=uid, context=context, registry=self.pool)
436 css = self.get_attr_bool(template_attributes.get('css'), default=True)
437 js = self.get_attr_bool(template_attributes.get('js'), default=True)
438 return bundle.to_html(css=css, js=js, debug=bool(qwebcontext.get('debug')))
440 def render_tag_set(self, element, template_attributes, generated_attributes, qwebcontext):
441 if "value" in template_attributes:
442 qwebcontext[template_attributes["set"]] = self.eval_object(template_attributes["value"], qwebcontext)
443 elif "valuef" in template_attributes:
444 qwebcontext[template_attributes["set"]] = self.eval_format(template_attributes["valuef"], qwebcontext)
446 qwebcontext[template_attributes["set"]] = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
449 def render_tag_field(self, element, template_attributes, generated_attributes, qwebcontext):
450 """ eg: <span t-record="browse_record(res.partner, 1)" t-field="phone">+1 555 555 8069</span>"""
451 node_name = element.tag
452 assert node_name not in ("table", "tbody", "thead", "tfoot", "tr", "td",
453 "li", "ul", "ol", "dl", "dt", "dd"),\
454 "RTE widgets do not work correctly on %r elements" % node_name
455 assert node_name != 't',\
456 "t-field can not be used on a t element, provide an actual HTML node"
458 record, field_name = template_attributes["field"].rsplit('.', 1)
459 record = self.eval_object(record, qwebcontext)
461 column = record._all_columns[field_name].column
462 options = json.loads(template_attributes.get('field-options') or '{}')
463 field_type = get_field_type(column, options)
465 converter = self.get_converter_for(field_type)
467 return converter.to_html(qwebcontext.cr, qwebcontext.uid, field_name, record, options,
468 element, template_attributes, generated_attributes, qwebcontext, context=qwebcontext.context)
470 def get_converter_for(self, field_type):
471 return self.pool.get('ir.qweb.field.' + field_type, self.pool['ir.qweb.field'])
473 def get_widget_for(self, widget):
474 widget_model = ('ir.qweb.widget.' + widget) if widget else 'ir.qweb.widget'
475 return self.pool.get(widget_model) or self.pool['ir.qweb.widget']
477 def get_attr_bool(self, attr, default=False):
480 if attr in ('false', '0'):
482 elif attr in ('true', '1'):
486 #--------------------------------------------------------------------
487 # QWeb Fields converters
488 #--------------------------------------------------------------------
490 class FieldConverter(osv.AbstractModel):
491 """ Used to convert a t-field specification into an output HTML field.
493 :meth:`~.to_html` is the entry point of this conversion from QWeb, it:
495 * converts the record value to html using :meth:`~.record_to_html`
496 * generates the metadata attributes (``data-oe-``) to set on the root
498 * generates the root result node itself through :meth:`~.render_element`
500 _name = 'ir.qweb.field'
502 def attributes(self, cr, uid, field_name, record, options,
503 source_element, g_att, t_att, qweb_context,
506 Generates the metadata attributes (prefixed by ``data-oe-`` for the
507 root node of the field conversion. Attribute values are escaped by the
510 The default attributes are:
512 * ``model``, the name of the record's model
513 * ``id`` the id of the record to which the field belongs
514 * ``field`` the name of the converted field
515 * ``type`` the logical field type (widget, may not match the column's
516 ``type``, may not be any _column subclass name)
517 * ``translate``, a boolean flag (``0`` or ``1``) denoting whether the
518 column is translatable
519 * ``expression``, the original expression
521 :returns: iterable of (attribute name, attribute value) pairs.
523 column = record._all_columns[field_name].column
524 field_type = get_field_type(column, options)
526 ('data-oe-model', record._name),
527 ('data-oe-id', record.id),
528 ('data-oe-field', field_name),
529 ('data-oe-type', field_type),
530 ('data-oe-expression', t_att['field']),
533 def value_to_html(self, cr, uid, value, column, options=None, context=None):
534 """ Converts a single value to its HTML version/output
536 if not value: return ''
539 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
540 """ Converts the specified field of the browse_record ``record`` to
543 return self.value_to_html(
544 cr, uid, record[field_name], column, options=options, context=context)
546 def to_html(self, cr, uid, field_name, record, options,
547 source_element, t_att, g_att, qweb_context, context=None):
548 """ Converts a ``t-field`` to its HTML output. A ``t-field`` may be
549 extended by a ``t-field-options``, which is a JSON-serialized mapping
550 of configuration values.
552 A default configuration key is ``widget`` which can override the
553 field's own ``_type``.
556 content = self.record_to_html(
557 cr, uid, field_name, record,
558 record._all_columns[field_name].column,
559 options, context=context)
560 if options.get('html-escape', True):
561 content = escape(content)
562 elif hasattr(content, '__html__'):
563 content = content.__html__()
565 _logger.warning("Could not get field %s for model %s",
566 field_name, record._name, exc_info=True)
569 inherit_branding = context and context.get('inherit_branding')
570 if not inherit_branding and context and context.get('inherit_branding_auto'):
571 inherit_branding = self.pool['ir.model.access'].check(cr, uid, record._name, 'write', False, context=context)
574 # add branding attributes
576 ' %s="%s"' % (name, escape(value))
577 for name, value in self.attributes(
578 cr, uid, field_name, record, options,
579 source_element, g_att, t_att, qweb_context)
582 return self.render_element(cr, uid, source_element, t_att, g_att,
583 qweb_context, content)
585 def qweb_object(self):
586 return self.pool['ir.qweb']
588 def render_element(self, cr, uid, source_element, t_att, g_att,
589 qweb_context, content):
590 """ Final rendering hook, by default just calls ir.qweb's ``render_element``
592 return self.qweb_object().render_element(
593 source_element, t_att, g_att, qweb_context, content or '')
595 def user_lang(self, cr, uid, context):
597 Fetches the res.lang object corresponding to the language code stored
598 in the user's context. Fallbacks to en_US if no lang is present in the
599 context *or the language code is not valid*.
601 :returns: res.lang browse_record
603 if context is None: context = {}
605 lang_code = context.get('lang') or 'en_US'
606 Lang = self.pool['res.lang']
608 lang_ids = Lang.search(cr, uid, [('code', '=', lang_code)], context=context) \
609 or Lang.search(cr, uid, [('code', '=', 'en_US')], context=context)
611 return Lang.browse(cr, uid, lang_ids[0], context=context)
613 class FloatConverter(osv.AbstractModel):
614 _name = 'ir.qweb.field.float'
615 _inherit = 'ir.qweb.field'
617 def precision(self, cr, uid, column, options=None, context=None):
618 _, precision = column.digits or (None, None)
621 def value_to_html(self, cr, uid, value, column, options=None, context=None):
624 precision = self.precision(cr, uid, column, options=options, context=context)
625 fmt = '%f' if precision is None else '%.{precision}f'
627 lang_code = context.get('lang') or 'en_US'
628 lang = self.pool['res.lang']
629 formatted = lang.format(cr, uid, [lang_code], fmt.format(precision=precision), value, grouping=True)
631 # %f does not strip trailing zeroes. %g does but its precision causes
632 # it to switch to scientific notation starting at a million *and* to
633 # strip decimals. So use %f and if no precision was specified manually
635 if precision is None:
636 formatted = re.sub(r'(?:(0|\d+?)0+)$', r'\1', formatted)
639 class DateConverter(osv.AbstractModel):
640 _name = 'ir.qweb.field.date'
641 _inherit = 'ir.qweb.field'
643 def value_to_html(self, cr, uid, value, column, options=None, context=None):
644 if not value or len(value)<10: return ''
645 lang = self.user_lang(cr, uid, context=context)
646 locale = babel.Locale.parse(lang.code)
648 if isinstance(value, basestring):
649 value = datetime.datetime.strptime(
650 value[:10], openerp.tools.DEFAULT_SERVER_DATE_FORMAT)
652 if options and 'format' in options:
653 pattern = options['format']
655 strftime_pattern = lang.date_format
656 pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
658 return babel.dates.format_date(
659 value, format=pattern,
662 class DateTimeConverter(osv.AbstractModel):
663 _name = 'ir.qweb.field.datetime'
664 _inherit = 'ir.qweb.field'
666 def value_to_html(self, cr, uid, value, column, options=None, context=None):
667 if not value: return ''
668 lang = self.user_lang(cr, uid, context=context)
669 locale = babel.Locale.parse(lang.code)
671 if isinstance(value, basestring):
672 value = datetime.datetime.strptime(
673 value, openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT)
674 value = fields.datetime.context_timestamp(
675 cr, uid, timestamp=value, context=context)
677 if options and 'format' in options:
678 pattern = options['format']
680 strftime_pattern = (u"%s %s" % (lang.date_format, lang.time_format))
681 pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
683 if options and options.get('hide_seconds'):
684 pattern = pattern.replace(":ss", "").replace(":s", "")
686 return babel.dates.format_datetime(value, format=pattern, locale=locale)
688 class TextConverter(osv.AbstractModel):
689 _name = 'ir.qweb.field.text'
690 _inherit = 'ir.qweb.field'
692 def value_to_html(self, cr, uid, value, column, options=None, context=None):
694 Escapes the value and converts newlines to br. This is bullshit.
696 if not value: return ''
698 return nl2br(value, options=options)
700 class SelectionConverter(osv.AbstractModel):
701 _name = 'ir.qweb.field.selection'
702 _inherit = 'ir.qweb.field'
704 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
705 value = record[field_name]
706 if not value: return ''
707 selection = dict(fields.selection.reify(
708 cr, uid, record._model, column, context=context))
709 return self.value_to_html(
710 cr, uid, selection[value], column, options=options)
712 class ManyToOneConverter(osv.AbstractModel):
713 _name = 'ir.qweb.field.many2one'
714 _inherit = 'ir.qweb.field'
716 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
717 [read] = record.read([field_name])
718 if not read[field_name]: return ''
719 _, value = read[field_name]
720 return nl2br(value, options=options)
722 class HTMLConverter(osv.AbstractModel):
723 _name = 'ir.qweb.field.html'
724 _inherit = 'ir.qweb.field'
726 def value_to_html(self, cr, uid, value, column, options=None, context=None):
727 return HTMLSafe(value or '')
729 class ImageConverter(osv.AbstractModel):
730 """ ``image`` widget rendering, inserts a data:uri-using image tag in the
731 document. May be overridden by e.g. the website module to generate links
734 .. todo:: what happens if different output need different converters? e.g.
735 reports may need embedded images or FS links whereas website
738 _name = 'ir.qweb.field.image'
739 _inherit = 'ir.qweb.field'
741 def value_to_html(self, cr, uid, value, column, options=None, context=None):
743 image = Image.open(cStringIO.StringIO(value.decode('base64')))
746 raise ValueError("Non-image binary fields can not be converted to HTML")
747 except: # image.verify() throws "suitable exceptions", I have no idea what they are
748 raise ValueError("Invalid image content")
750 return HTMLSafe('<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value))
752 class MonetaryConverter(osv.AbstractModel):
753 """ ``monetary`` converter, has a mandatory option
754 ``display_currency``.
756 The currency is used for formatting *and rounding* of the float value. It
757 is assumed that the linked res_currency has a non-empty rounding value and
758 res.currency's ``round`` method is used to perform rounding.
760 .. note:: the monetary converter internally adds the qweb context to its
761 options mapping, so that the context is available to callees.
762 It's set under the ``_qweb_context`` key.
764 _name = 'ir.qweb.field.monetary'
765 _inherit = 'ir.qweb.field'
767 def to_html(self, cr, uid, field_name, record, options,
768 source_element, t_att, g_att, qweb_context, context=None):
769 options['_qweb_context'] = qweb_context
770 return super(MonetaryConverter, self).to_html(
771 cr, uid, field_name, record, options,
772 source_element, t_att, g_att, qweb_context, context=context)
774 def record_to_html(self, cr, uid, field_name, record, column, options, context=None):
777 Currency = self.pool['res.currency']
778 display_currency = self.display_currency(cr, uid, options['display_currency'], options)
780 # lang.format mandates a sprintf-style format. These formats are non-
781 # minimal (they have a default fixed precision instead), and
782 # lang.format will not set one by default. currency.round will not
783 # provide one either. So we need to generate a precision value
784 # (integer > 0) from the currency's rounding (a float generally < 1.0).
786 # The log10 of the rounding should be the number of digits involved if
787 # negative, if positive clamp to 0 digits and call it a day.
788 # nb: int() ~ floor(), we want nearest rounding instead
789 precision = int(round(math.log10(display_currency.rounding)))
790 fmt = "%.{0}f".format(-precision if precision < 0 else 0)
792 from_amount = record[field_name]
794 if options.get('from_currency'):
795 from_currency = self.display_currency(cr, uid, options['from_currency'], options)
796 from_amount = Currency.compute(cr, uid, from_currency.id, display_currency.id, from_amount)
798 lang_code = context.get('lang') or 'en_US'
799 lang = self.pool['res.lang']
800 formatted_amount = lang.format(cr, uid, [lang_code],
801 fmt, Currency.round(cr, uid, display_currency, from_amount),
802 grouping=True, monetary=True)
805 if display_currency.position == 'before':
810 return HTMLSafe(u'{pre}<span class="oe_currency_value">{0}</span>{post}'.format(
814 symbol=display_currency.symbol,
817 def display_currency(self, cr, uid, currency, options):
818 return self.qweb_object().eval_object(
819 currency, options['_qweb_context'])
822 ('year', 3600 * 24 * 365),
823 ('month', 3600 * 24 * 30),
824 ('week', 3600 * 24 * 7),
830 class DurationConverter(osv.AbstractModel):
831 """ ``duration`` converter, to display integral or fractional values as
832 human-readable time spans (e.g. 1.5 as "1 hour 30 minutes").
834 Can be used on any numerical field.
836 Has a mandatory option ``unit`` which can be one of ``second``, ``minute``,
837 ``hour``, ``day``, ``week`` or ``year``, used to interpret the numerical
838 field value before converting it.
840 Sub-second values will be ignored.
842 _name = 'ir.qweb.field.duration'
843 _inherit = 'ir.qweb.field'
845 def value_to_html(self, cr, uid, value, column, options=None, context=None):
846 units = dict(TIMEDELTA_UNITS)
848 raise ValueError(_("Durations can't be negative"))
849 if not options or options.get('unit') not in units:
850 raise ValueError(_("A unit must be provided to duration widgets"))
852 locale = babel.Locale.parse(
853 self.user_lang(cr, uid, context=context).code)
854 factor = units[options['unit']]
858 for unit, secs_per_unit in TIMEDELTA_UNITS:
859 v, r = divmod(r, secs_per_unit)
861 section = babel.dates.format_timedelta(
862 v*secs_per_unit, threshold=1, locale=locale)
864 sections.append(section)
865 return ' '.join(sections)
868 class RelativeDatetimeConverter(osv.AbstractModel):
869 _name = 'ir.qweb.field.relative'
870 _inherit = 'ir.qweb.field'
872 def value_to_html(self, cr, uid, value, column, options=None, context=None):
873 parse_format = openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT
874 locale = babel.Locale.parse(
875 self.user_lang(cr, uid, context=context).code)
877 if isinstance(value, basestring):
878 value = datetime.datetime.strptime(value, parse_format)
880 # value should be a naive datetime in UTC. So is fields.datetime.now()
881 reference = datetime.datetime.strptime(column.now(), parse_format)
883 return babel.dates.format_timedelta(
884 value - reference, add_direction=True, locale=locale)
886 class Contact(orm.AbstractModel):
887 _name = 'ir.qweb.field.contact'
888 _inherit = 'ir.qweb.field.many2one'
890 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
896 opf = options.get('fields') or ["name", "address", "phone", "mobile", "fax", "email"]
897 if not getattr(record, field_name):
900 id = getattr(record, field_name).id
901 context.update(show_address=True)
902 field_browse = self.pool[column._obj].browse(cr, openerp.SUPERUSER_ID, id, context=context)
903 value = field_browse.name_get()[0][1]
906 'name': value.split("\n")[0],
907 'address': escape("\n".join(value.split("\n")[1:])).strip(),
908 'phone': field_browse.phone,
909 'mobile': field_browse.mobile,
910 'fax': field_browse.fax,
911 'city': field_browse.city,
912 'country_id': field_browse.country_id.display_name,
913 'website': field_browse.website,
914 'email': field_browse.email,
916 'object': field_browse,
920 html = self.pool["ir.ui.view"].render(cr, uid, "base.contact", val, engine='ir.qweb', context=context).decode('utf8')
922 return HTMLSafe(html)
924 class QwebView(orm.AbstractModel):
925 _name = 'ir.qweb.field.qweb'
926 _inherit = 'ir.qweb.field.many2one'
928 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
929 if not getattr(record, field_name):
932 view = getattr(record, field_name)
934 if view._model._name != "ir.ui.view":
935 _logger.warning("%s.%s must be a 'ir.ui.view' model." % (record, field_name))
938 ctx = (context or {}).copy()
939 ctx['object'] = record
940 html = view.render(ctx, engine='ir.qweb', context=ctx).decode('utf8')
942 return HTMLSafe(html)
944 class QwebWidget(osv.AbstractModel):
945 _name = 'ir.qweb.widget'
947 def _format(self, inner, options, qwebcontext):
948 return self.pool['ir.qweb'].eval_str(inner, qwebcontext)
950 def format(self, inner, options, qwebcontext):
951 return escape(self._format(inner, options, qwebcontext))
953 class QwebWidgetMonetary(osv.AbstractModel):
954 _name = 'ir.qweb.widget.monetary'
955 _inherit = 'ir.qweb.widget'
957 def _format(self, inner, options, qwebcontext):
958 inner = self.pool['ir.qweb'].eval(inner, qwebcontext)
959 display = self.pool['ir.qweb'].eval_object(options['display_currency'], qwebcontext)
960 precision = int(round(math.log10(display.rounding)))
961 fmt = "%.{0}f".format(-precision if precision < 0 else 0)
962 lang_code = qwebcontext.context.get('lang') or 'en_US'
963 formatted_amount = self.pool['res.lang'].format(
964 qwebcontext.cr, qwebcontext.uid, [lang_code], fmt, inner, grouping=True, monetary=True
967 if display.position == 'before':
972 return u'{pre}{0}{post}'.format(
973 formatted_amount, pre=pre, post=post
974 ).format(symbol=display.symbol,)
976 class HTMLSafe(object):
977 """ HTMLSafe string wrapper, Werkzeug's escape() has special handling for
978 objects with a ``__html__`` methods but AFAIK does not provide any such
981 Wrapping a string in HTML will prevent its escaping
983 __slots__ = ['string']
984 def __init__(self, string):
990 if isinstance(s, unicode):
991 return s.encode('utf-8')
993 def __unicode__(self):
995 if isinstance(s, str):
996 return s.decode('utf-8')
999 def nl2br(string, options=None):
1000 """ Converts newlines to HTML linebreaks in ``string``. Automatically
1001 escapes content unless options['html-escape'] is set to False, and returns
1002 the result wrapped in an HTMLSafe object.
1005 :param dict options:
1008 if options is None: options = {}
1010 if options.get('html-escape', True):
1011 string = escape(string)
1012 return HTMLSafe(string.replace('\n', '<br>\n'))
1014 def get_field_type(column, options):
1015 """ Gets a t-field's effective type from the field's column and its options
1017 return options.get('widget', column._type)
1019 class AssetError(Exception):
1021 class AssetNotFound(AssetError):
1024 class AssetsBundle(object):
1025 rx_css_import = re.compile("(@import[^;{]+;?)", re.M)
1026 rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""")
1027 rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/")
1029 def __init__(self, xmlid, debug=False, cr=None, uid=None, context=None, registry=None):
1031 self.cr = request.cr if cr is None else cr
1032 self.uid = request.uid if uid is None else uid
1033 self.context = request.context if context is None else context
1034 self.registry = request.registry if registry is None else registry
1035 self.javascripts = []
1036 self.stylesheets = []
1037 self.css_errors = []
1039 self._checksum = None
1041 context = self.context.copy()
1042 context['inherit_branding'] = False
1043 context['rendering_bundle'] = True
1044 self.html = self.registry['ir.ui.view'].render(self.cr, self.uid, xmlid, context=context)
1048 fragments = html.fragments_fromstring(self.html)
1049 for el in fragments:
1050 if isinstance(el, basestring):
1051 self.remains.append(el)
1052 elif isinstance(el, html.HtmlElement):
1053 src = el.get('src', '')
1054 href = el.get('href', '')
1055 atype = el.get('type')
1056 media = el.get('media')
1057 if el.tag == 'style':
1058 if atype == 'text/sass' or src.endswith('.sass'):
1059 self.stylesheets.append(SassStylesheetAsset(self, inline=el.text, media=media))
1060 elif atype == 'text/less' or src.endswith('.less'):
1061 self.stylesheets.append(LessStylesheetAsset(self, inline=el.text, media=media))
1063 self.stylesheets.append(StylesheetAsset(self, inline=el.text, media=media))
1064 elif el.tag == 'link' and el.get('rel') == 'stylesheet' and self.can_aggregate(href):
1065 if href.endswith('.sass') or atype == 'text/sass':
1066 self.stylesheets.append(SassStylesheetAsset(self, url=href, media=media))
1067 elif href.endswith('.less') or atype == 'text/less':
1068 self.stylesheets.append(LessStylesheetAsset(self, url=href, media=media))
1070 self.stylesheets.append(StylesheetAsset(self, url=href, media=media))
1071 elif el.tag == 'script' and not src:
1072 self.javascripts.append(JavascriptAsset(self, inline=el.text))
1073 elif el.tag == 'script' and self.can_aggregate(src):
1074 self.javascripts.append(JavascriptAsset(self, url=src))
1076 self.remains.append(html.tostring(el))
1079 self.remains.append(html.tostring(el))
1081 # notYETimplementederror
1082 raise NotImplementedError
1084 def can_aggregate(self, url):
1085 return not urlparse(url).netloc and not url.startswith(('/web/css', '/web/js'))
1087 def to_html(self, sep=None, css=True, js=True, debug=False):
1092 if css and self.stylesheets:
1093 self.preprocess_css()
1095 msg = '\n'.join(self.css_errors)
1096 self.stylesheets.append(StylesheetAsset(self, inline=self.css_message(msg)))
1097 for style in self.stylesheets:
1098 response.append(style.to_html())
1100 for jscript in self.javascripts:
1101 response.append(jscript.to_html())
1103 url_for = self.context.get('url_for', lambda url: url)
1104 if css and self.stylesheets:
1105 href = '/web/css/%s/%s' % (self.xmlid, self.version)
1106 response.append('<link href="%s" rel="stylesheet"/>' % url_for(href))
1108 src = '/web/js/%s/%s' % (self.xmlid, self.version)
1109 response.append('<script type="text/javascript" src="%s"></script>' % url_for(src))
1110 response.extend(self.remains)
1111 return sep + sep.join(response)
1114 def last_modified(self):
1115 """Returns last modified date of linked files"""
1116 return max(itertools.chain(
1117 (asset.last_modified for asset in self.javascripts),
1118 (asset.last_modified for asset in self.stylesheets),
1123 return self.checksum[0:7]
1128 Not really a full checksum.
1129 We compute a SHA1 on the rendered bundle + max linked files last_modified date
1131 check = self.html + str(self.last_modified)
1132 return hashlib.sha1(check).hexdigest()
1135 content = self.get_cache('js')
1137 content = ';\n'.join(asset.minify() for asset in self.javascripts)
1138 self.set_cache('js', content)
1142 """Generate css content from given bundle"""
1143 content = self.get_cache('css')
1145 content = self.preprocess_css()
1148 msg = '\n'.join(self.css_errors)
1149 content += self.css_message(msg)
1151 # move up all @import rules to the top
1154 matches.append(matchobj.group(0))
1157 content = re.sub(self.rx_css_import, push, content)
1159 matches.append(content)
1160 content = u'\n'.join(matches)
1163 self.set_cache('css', content)
1167 def get_cache(self, type):
1169 domain = [('url', '=', '/web/%s/%s/%s' % (type, self.xmlid, self.version))]
1170 bundle = self.registry['ir.attachment'].search_read(self.cr, self.uid, domain, ['datas'], context=self.context)
1171 if bundle and bundle[0]['datas']:
1172 content = bundle[0]['datas'].decode('base64')
1175 def set_cache(self, type, content):
1176 ira = self.registry['ir.attachment']
1177 ira.invalidate_bundle(self.cr, openerp.SUPERUSER_ID, type=type, xmlid=self.xmlid)
1178 url = '/web/%s/%s/%s' % (type, self.xmlid, self.version)
1179 ira.create(self.cr, openerp.SUPERUSER_ID, dict(
1180 datas=content.encode('utf8').encode('base64'),
1184 ), context=self.context)
1186 def css_message(self, message):
1187 # '\A' == css content carriage return
1188 message = message.replace('\n', '\\A ').replace('"', '\\"')
1194 font-family: monospace;
1200 def preprocess_css(self):
1202 Checks if the bundle contains any sass/less content, then compiles it to css.
1203 Returns the bundle's flat css.
1205 for atype in (SassStylesheetAsset, LessStylesheetAsset):
1206 assets = [asset for asset in self.stylesheets if isinstance(asset, atype)]
1208 cmd = assets[0].get_command()
1209 source = '\n'.join([asset.get_source() for asset in assets])
1210 compiled = self.compile_css(cmd, source)
1212 fragments = self.rx_css_split.split(compiled)
1213 at_rules = fragments.pop(0)
1215 # Sass and less moves @at-rules to the top in order to stay css 2.1 compatible
1216 self.stylesheets.insert(0, StylesheetAsset(self, inline=at_rules))
1218 asset_id = fragments.pop(0)
1219 asset = next(asset for asset in self.stylesheets if asset.id == asset_id)
1220 asset._content = fragments.pop(0)
1222 return '\n'.join(asset.minify() for asset in self.stylesheets)
1224 def compile_css(self, cmd, source):
1225 """Sanitizes @import rules, remove duplicates @import rules, then compile"""
1227 def sanitize(matchobj):
1228 ref = matchobj.group(2)
1229 line = '@import "%s"%s' % (ref, matchobj.group(3))
1230 if '.' not in ref and line not in imports and not ref.startswith(('.', '/', '~')):
1231 imports.append(line)
1233 msg = "Local import '%s' is forbidden for security reasons." % ref
1234 _logger.warning(msg)
1235 self.css_errors.append(msg)
1237 source = re.sub(self.rx_preprocess_imports, sanitize, source)
1240 compiler = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
1242 msg = "Could not execute command %r" % cmd[0]
1244 self.css_errors.append(msg)
1246 result = compiler.communicate(input=source.encode('utf-8'))
1247 if compiler.returncode:
1248 error = self.get_preprocessor_error(''.join(result), source=source)
1249 _logger.warning(error)
1250 self.css_errors.append(error)
1252 compiled = result[0].strip().decode('utf8')
1255 def get_preprocessor_error(self, stderr, source=None):
1256 """Improve and remove sensitive information from sass/less compilator error messages"""
1257 error = stderr.split('Load paths')[0].replace(' Use --trace for backtrace.', '')
1258 if 'Cannot load compass' in error:
1259 error += "Maybe you should install the compass gem using this extra argument:\n\n" \
1260 " $ sudo gem install compass --pre\n"
1261 error += "This error occured while compiling the bundle '%s' containing:" % self.xmlid
1262 for asset in self.stylesheets:
1263 if isinstance(asset, PreprocessedCSS):
1264 error += '\n - %s' % (asset.url if asset.url else '<inline sass>')
1267 class WebAsset(object):
1270 def __init__(self, bundle, inline=None, url=None):
1271 self.id = str(uuid.uuid4())
1272 self.bundle = bundle
1273 self.inline = inline
1276 self.uid = bundle.uid
1277 self.registry = bundle.registry
1278 self.context = bundle.context
1279 self._content = None
1280 self._filename = None
1281 self._ir_attach = None
1282 name = '<inline asset>' if inline else url
1283 self.name = "%s defined in bundle '%s'" % (name, bundle.xmlid)
1284 if not inline and not url:
1285 raise Exception("An asset should either be inlined or url linked")
1288 if not (self.inline or self._filename or self._ir_attach):
1289 addon = filter(None, self.url.split('/'))[0]
1291 # Test url against modules static assets
1292 mpath = openerp.http.addons_manifest[addon]['addons_path']
1293 self._filename = mpath + self.url.replace('/', os.path.sep)
1296 # Test url against ir.attachments
1297 fields = ['__last_update', 'datas', 'mimetype']
1298 domain = [('type', '=', 'binary'), ('url', '=', self.url)]
1299 ira = self.registry['ir.attachment']
1300 attach = ira.search_read(self.cr, self.uid, domain, fields, context=self.context)
1301 self._ir_attach = attach[0]
1303 raise AssetNotFound("Could not find %s" % self.name)
1306 raise NotImplementedError()
1309 def last_modified(self):
1313 return datetime.datetime.fromtimestamp(os.path.getmtime(self._filename))
1314 elif self._ir_attach:
1315 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
1316 last_update = self._ir_attach['__last_update']
1318 return datetime.datetime.strptime(last_update, server_format + '.%f')
1320 return datetime.datetime.strptime(last_update, server_format)
1323 return datetime.datetime(1970, 1, 1)
1327 if self._content is None:
1328 self._content = self.inline or self._fetch_content()
1329 return self._content
1331 def _fetch_content(self):
1332 """ Fetch content from file or database"""
1336 with open(self._filename, 'rb') as fp:
1337 return fp.read().decode('utf-8')
1339 return self._ir_attach['datas'].decode('base64')
1340 except UnicodeDecodeError:
1341 raise AssetError('%s is not utf-8 encoded.' % self.name)
1343 raise AssetNotFound('File %s does not exist.' % self.name)
1345 raise AssetError('Could not get content for %s.' % self.name)
1350 def with_header(self, content=None):
1352 content = self.content
1353 return '\n/* %s */\n%s' % (self.name, content)
1355 class JavascriptAsset(WebAsset):
1357 return self.with_header(rjsmin(self.content))
1359 def _fetch_content(self):
1361 return super(JavascriptAsset, self)._fetch_content()
1362 except AssetError, e:
1363 return "console.error(%s);" % json.dumps(e.message)
1367 return '<script type="text/javascript" src="%s"></script>' % (self.html_url % self.url)
1369 return '<script type="text/javascript" charset="utf-8">%s</script>' % self.with_header()
1371 class StylesheetAsset(WebAsset):
1372 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
1373 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
1374 rx_sourceMap = re.compile(r'(/\*# sourceMappingURL=.*)', re.U)
1375 rx_charset = re.compile(r'(@charset "[^"]+";)', re.U)
1377 def __init__(self, *args, **kw):
1378 self.media = kw.pop('media', None)
1379 super(StylesheetAsset, self).__init__(*args, **kw)
1383 content = super(StylesheetAsset, self).content
1385 content = '@media %s { %s }' % (self.media, content)
1388 def _fetch_content(self):
1390 content = super(StylesheetAsset, self)._fetch_content()
1391 web_dir = os.path.dirname(self.url)
1394 content = self.rx_import.sub(
1395 r"""@import \1%s/""" % (web_dir,),
1400 content = self.rx_url.sub(
1401 r"url(\1%s/" % (web_dir,),
1406 # remove charset declarations, we only support utf-8
1407 content = self.rx_charset.sub('', content)
1410 except AssetError, e:
1411 self.bundle.css_errors.append(e.message)
1415 # remove existing sourcemaps, make no sense after re-mini
1416 content = self.rx_sourceMap.sub('', self.content)
1418 content = re.sub(r'/\*.*?\*/', '', content, flags=re.S)
1420 content = re.sub(r'\s+', ' ', content)
1421 content = re.sub(r' *([{}]) *', r'\1', content)
1422 return self.with_header(content)
1425 media = (' media="%s"' % werkzeug.utils.escape(self.media)) if self.media else ''
1427 href = self.html_url % self.url
1428 return '<link rel="stylesheet" href="%s" type="text/css"%s/>' % (href, media)
1430 return '<style type="text/css"%s>%s</style>' % (media, self.with_header())
1432 class PreprocessedCSS(StylesheetAsset):
1437 return self.with_header()
1441 ira = self.registry['ir.attachment']
1442 url = self.html_url % self.url
1443 domain = [('type', '=', 'binary'), ('url', '=', url)]
1444 ira_id = ira.search(self.cr, self.uid, domain, context=self.context)
1445 datas = self.content.encode('utf8').encode('base64')
1447 # TODO: update only if needed
1448 ira.write(self.cr, openerp.SUPERUSER_ID, ira_id, {'datas': datas}, context=self.context)
1450 ira.create(self.cr, openerp.SUPERUSER_ID, dict(
1452 mimetype='text/css',
1456 ), context=self.context)
1457 return super(PreprocessedCSS, self).to_html()
1459 def get_source(self):
1460 content = self.inline or self._fetch_content()
1461 return "/*! %s */\n%s" % (self.id, content)
1463 def get_command(self):
1464 raise NotImplementedError
1466 class SassStylesheetAsset(PreprocessedCSS):
1467 rx_indent = re.compile(r'^( +|\t+)', re.M)
1471 def get_source(self):
1472 content = textwrap.dedent(self.inline or self._fetch_content())
1475 # Indentation normalization
1477 if self.indent is None:
1479 if self.indent == self.reindent:
1480 # Don't reindent the file if identation is the final one (reindent)
1481 raise StopIteration()
1482 return ind.replace(self.indent, self.reindent)
1485 content = self.rx_indent.sub(fix_indent, content)
1486 except StopIteration:
1488 return "/*! %s */\n%s" % (self.id, content)
1490 def get_command(self):
1491 return ['sass', '--stdin', '-t', 'compressed', '--unix-newlines', '--compass',
1492 '-r', 'bootstrap-sass']
1494 class LessStylesheetAsset(PreprocessedCSS):
1495 def get_command(self):
1496 webpath = openerp.http.addons_manifest['web']['addons_path']
1497 lesspath = os.path.join(webpath, 'web', 'static', 'lib', 'bootstrap', 'less')
1498 return ['lessc', '-', '--clean-css', '--no-js', '--no-color', '--include-path=%s' % lesspath]
1501 """ Minify js with a clever regex.
1502 Taken from http://opensource.perlig.de/rjsmin
1503 Apache License, Version 2.0 """
1505 """ Substitution callback """
1506 groups = match.groups()
1512 (groups[4] and '\n') or
1513 (groups[5] and ' ') or
1514 (groups[6] and ' ') or
1515 (groups[7] and ' ') or
1520 r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
1521 r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
1522 r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
1523 r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
1524 r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
1525 r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
1526 r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
1527 r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
1528 r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
1529 r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
1530 r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
1531 r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
1532 r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
1533 r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
1534 r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
1535 r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
1536 r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
1537 r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
1538 r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
1539 r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
1540 r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script