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, escape(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 if context and context.get('inherit_branding'):
559 # add branding attributes
561 ' %s="%s"' % (name, escape(value))
562 for name, value in self.attributes(
563 cr, uid, field_name, record, options,
564 source_element, g_att, t_att, qweb_context)
567 return self.render_element(cr, uid, source_element, t_att, g_att,
568 qweb_context, content)
570 def qweb_object(self):
571 return self.pool['ir.qweb']
573 def render_element(self, cr, uid, source_element, t_att, g_att,
574 qweb_context, content):
575 """ Final rendering hook, by default just calls ir.qweb's ``render_element``
577 return self.qweb_object().render_element(
578 source_element, t_att, g_att, qweb_context, content or '')
580 def user_lang(self, cr, uid, context):
582 Fetches the res.lang object corresponding to the language code stored
583 in the user's context. Fallbacks to en_US if no lang is present in the
584 context *or the language code is not valid*.
586 :returns: res.lang browse_record
588 if context is None: context = {}
590 lang_code = context.get('lang') or 'en_US'
591 Lang = self.pool['res.lang']
593 lang_ids = Lang.search(cr, uid, [('code', '=', lang_code)], context=context) \
594 or Lang.search(cr, uid, [('code', '=', 'en_US')], context=context)
596 return Lang.browse(cr, uid, lang_ids[0], context=context)
598 class FloatConverter(osv.AbstractModel):
599 _name = 'ir.qweb.field.float'
600 _inherit = 'ir.qweb.field'
602 def precision(self, cr, uid, column, options=None, context=None):
603 _, precision = column.digits or (None, None)
606 def value_to_html(self, cr, uid, value, column, options=None, context=None):
609 precision = self.precision(cr, uid, column, options=options, context=context)
610 fmt = '%f' if precision is None else '%.{precision}f'
612 lang_code = context.get('lang') or 'en_US'
613 lang = self.pool['res.lang']
614 formatted = lang.format(cr, uid, [lang_code], fmt.format(precision=precision), value, grouping=True)
616 # %f does not strip trailing zeroes. %g does but its precision causes
617 # it to switch to scientific notation starting at a million *and* to
618 # strip decimals. So use %f and if no precision was specified manually
620 if precision is None:
621 formatted = re.sub(r'(?:(0|\d+?)0+)$', r'\1', formatted)
624 class DateConverter(osv.AbstractModel):
625 _name = 'ir.qweb.field.date'
626 _inherit = 'ir.qweb.field'
628 def value_to_html(self, cr, uid, value, column, options=None, context=None):
629 if not value: return ''
630 lang = self.user_lang(cr, uid, context=context)
631 locale = babel.Locale.parse(lang.code)
633 if isinstance(value, basestring):
634 value = datetime.datetime.strptime(
635 value, openerp.tools.DEFAULT_SERVER_DATE_FORMAT)
637 if options and 'format' in options:
638 pattern = options['format']
640 strftime_pattern = lang.date_format
641 pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
643 return babel.dates.format_date(
644 value, format=pattern,
647 class DateTimeConverter(osv.AbstractModel):
648 _name = 'ir.qweb.field.datetime'
649 _inherit = 'ir.qweb.field'
651 def value_to_html(self, cr, uid, value, column, options=None, context=None):
652 if not value: return ''
653 lang = self.user_lang(cr, uid, context=context)
654 locale = babel.Locale.parse(lang.code)
656 if isinstance(value, basestring):
657 value = datetime.datetime.strptime(
658 value, openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT)
659 value = fields.datetime.context_timestamp(
660 cr, uid, timestamp=value, context=context)
662 if options and 'format' in options:
663 pattern = options['format']
665 strftime_pattern = (u"%s %s" % (lang.date_format, lang.time_format))
666 pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
668 if options and options.get('hide_seconds'):
669 pattern = pattern.replace(":ss", "").replace(":s", "")
671 return babel.dates.format_datetime(value, format=pattern, locale=locale)
673 class TextConverter(osv.AbstractModel):
674 _name = 'ir.qweb.field.text'
675 _inherit = 'ir.qweb.field'
677 def value_to_html(self, cr, uid, value, column, options=None, context=None):
679 Escapes the value and converts newlines to br. This is bullshit.
681 if not value: return ''
683 return nl2br(value, options=options)
685 class SelectionConverter(osv.AbstractModel):
686 _name = 'ir.qweb.field.selection'
687 _inherit = 'ir.qweb.field'
689 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
690 value = record[field_name]
691 if not value: return ''
692 selection = dict(fields.selection.reify(
693 cr, uid, record._model, column, context=context))
694 return self.value_to_html(
695 cr, uid, selection[value], column, options=options)
697 class ManyToOneConverter(osv.AbstractModel):
698 _name = 'ir.qweb.field.many2one'
699 _inherit = 'ir.qweb.field'
701 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
702 [read] = record.read([field_name])
703 if not read[field_name]: return ''
704 _, value = read[field_name]
705 return nl2br(value, options=options)
707 class HTMLConverter(osv.AbstractModel):
708 _name = 'ir.qweb.field.html'
709 _inherit = 'ir.qweb.field'
711 def value_to_html(self, cr, uid, value, column, options=None, context=None):
712 return HTMLSafe(value or '')
714 class ImageConverter(osv.AbstractModel):
715 """ ``image`` widget rendering, inserts a data:uri-using image tag in the
716 document. May be overridden by e.g. the website module to generate links
719 .. todo:: what happens if different output need different converters? e.g.
720 reports may need embedded images or FS links whereas website
723 _name = 'ir.qweb.field.image'
724 _inherit = 'ir.qweb.field'
726 def value_to_html(self, cr, uid, value, column, options=None, context=None):
728 image = Image.open(cStringIO.StringIO(value.decode('base64')))
731 raise ValueError("Non-image binary fields can not be converted to HTML")
732 except: # image.verify() throws "suitable exceptions", I have no idea what they are
733 raise ValueError("Invalid image content")
735 return HTMLSafe('<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value))
737 class MonetaryConverter(osv.AbstractModel):
738 """ ``monetary`` converter, has a mandatory option
739 ``display_currency``.
741 The currency is used for formatting *and rounding* of the float value. It
742 is assumed that the linked res_currency has a non-empty rounding value and
743 res.currency's ``round`` method is used to perform rounding.
745 .. note:: the monetary converter internally adds the qweb context to its
746 options mapping, so that the context is available to callees.
747 It's set under the ``_qweb_context`` key.
749 _name = 'ir.qweb.field.monetary'
750 _inherit = 'ir.qweb.field'
752 def to_html(self, cr, uid, field_name, record, options,
753 source_element, t_att, g_att, qweb_context, context=None):
754 options['_qweb_context'] = qweb_context
755 return super(MonetaryConverter, self).to_html(
756 cr, uid, field_name, record, options,
757 source_element, t_att, g_att, qweb_context, context=context)
759 def record_to_html(self, cr, uid, field_name, record, column, options, context=None):
762 Currency = self.pool['res.currency']
763 display_currency = self.display_currency(cr, uid, options['display_currency'], options)
765 # lang.format mandates a sprintf-style format. These formats are non-
766 # minimal (they have a default fixed precision instead), and
767 # lang.format will not set one by default. currency.round will not
768 # provide one either. So we need to generate a precision value
769 # (integer > 0) from the currency's rounding (a float generally < 1.0).
771 # The log10 of the rounding should be the number of digits involved if
772 # negative, if positive clamp to 0 digits and call it a day.
773 # nb: int() ~ floor(), we want nearest rounding instead
774 precision = int(round(math.log10(display_currency.rounding)))
775 fmt = "%.{0}f".format(-precision if precision < 0 else 0)
777 from_amount = record[field_name]
779 if options.get('from_currency'):
780 from_currency = self.display_currency(cr, uid, options['from_currency'], options)
781 from_amount = Currency.compute(cr, uid, from_currency.id, display_currency.id, from_amount)
783 lang_code = context.get('lang') or 'en_US'
784 lang = self.pool['res.lang']
785 formatted_amount = lang.format(cr, uid, [lang_code],
786 fmt, Currency.round(cr, uid, display_currency, from_amount),
787 grouping=True, monetary=True)
790 if display_currency.position == 'before':
795 return HTMLSafe(u'{pre}<span class="oe_currency_value">{0}</span>{post}'.format(
799 symbol=display_currency.symbol,
802 def display_currency(self, cr, uid, currency, options):
803 return self.qweb_object().eval_object(
804 currency, options['_qweb_context'])
807 ('year', 3600 * 24 * 365),
808 ('month', 3600 * 24 * 30),
809 ('week', 3600 * 24 * 7),
815 class DurationConverter(osv.AbstractModel):
816 """ ``duration`` converter, to display integral or fractional values as
817 human-readable time spans (e.g. 1.5 as "1 hour 30 minutes").
819 Can be used on any numerical field.
821 Has a mandatory option ``unit`` which can be one of ``second``, ``minute``,
822 ``hour``, ``day``, ``week`` or ``year``, used to interpret the numerical
823 field value before converting it.
825 Sub-second values will be ignored.
827 _name = 'ir.qweb.field.duration'
828 _inherit = 'ir.qweb.field'
830 def value_to_html(self, cr, uid, value, column, options=None, context=None):
831 units = dict(TIMEDELTA_UNITS)
833 raise ValueError(_("Durations can't be negative"))
834 if not options or options.get('unit') not in units:
835 raise ValueError(_("A unit must be provided to duration widgets"))
837 locale = babel.Locale.parse(
838 self.user_lang(cr, uid, context=context).code)
839 factor = units[options['unit']]
843 for unit, secs_per_unit in TIMEDELTA_UNITS:
844 v, r = divmod(r, secs_per_unit)
846 section = babel.dates.format_timedelta(
847 v*secs_per_unit, threshold=1, locale=locale)
849 sections.append(section)
850 return ' '.join(sections)
853 class RelativeDatetimeConverter(osv.AbstractModel):
854 _name = 'ir.qweb.field.relative'
855 _inherit = 'ir.qweb.field'
857 def value_to_html(self, cr, uid, value, column, options=None, context=None):
858 parse_format = openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT
859 locale = babel.Locale.parse(
860 self.user_lang(cr, uid, context=context).code)
862 if isinstance(value, basestring):
863 value = datetime.datetime.strptime(value, parse_format)
865 # value should be a naive datetime in UTC. So is fields.datetime.now()
866 reference = datetime.datetime.strptime(column.now(), parse_format)
868 return babel.dates.format_timedelta(
869 value - reference, add_direction=True, locale=locale)
871 class Contact(orm.AbstractModel):
872 _name = 'ir.qweb.field.contact'
873 _inherit = 'ir.qweb.field.many2one'
875 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
881 opf = options.get('fields') or ["name", "address", "phone", "mobile", "fax", "email"]
882 if not getattr(record, field_name):
885 id = getattr(record, field_name).id
886 context.update(show_address=True)
887 field_browse = self.pool[column._obj].browse(cr, openerp.SUPERUSER_ID, id, context=context)
888 value = field_browse.name_get()[0][1]
891 'name': value.split("\n")[0],
892 'address': escape("\n".join(value.split("\n")[1:])),
893 'phone': field_browse.phone,
894 'mobile': field_browse.mobile,
895 'fax': field_browse.fax,
896 'city': field_browse.city,
897 'country_id': field_browse.country_id.display_name,
898 'website': field_browse.website,
899 'email': field_browse.email,
901 'object': field_browse,
905 html = self.pool["ir.ui.view"].render(cr, uid, "base.contact", val, engine='ir.qweb', context=context).decode('utf8')
907 return HTMLSafe(html)
909 class QwebView(orm.AbstractModel):
910 _name = 'ir.qweb.field.qweb'
911 _inherit = 'ir.qweb.field.many2one'
913 def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
914 if not getattr(record, field_name):
917 view = getattr(record, field_name)
919 if view._model._name != "ir.ui.view":
920 _logger.warning("%s.%s must be a 'ir.ui.view' model." % (record, field_name))
923 ctx = (context or {}).copy()
924 ctx['object'] = record
925 html = view.render(ctx, engine='ir.qweb', context=ctx).decode('utf8')
927 return HTMLSafe(html)
929 class QwebWidget(osv.AbstractModel):
930 _name = 'ir.qweb.widget'
932 def _format(self, inner, options, qwebcontext):
933 return self.pool['ir.qweb'].eval_str(inner, qwebcontext)
935 def format(self, inner, options, qwebcontext):
936 return escape(self._format(inner, options, qwebcontext))
938 class QwebWidgetMonetary(osv.AbstractModel):
939 _name = 'ir.qweb.widget.monetary'
940 _inherit = 'ir.qweb.widget'
942 def _format(self, inner, options, qwebcontext):
943 inner = self.pool['ir.qweb'].eval(inner, qwebcontext)
944 display = self.pool['ir.qweb'].eval_object(options['display_currency'], qwebcontext)
945 precision = int(round(math.log10(display.rounding)))
946 fmt = "%.{0}f".format(-precision if precision < 0 else 0)
947 lang_code = qwebcontext.context.get('lang') or 'en_US'
948 formatted_amount = self.pool['res.lang'].format(
949 qwebcontext.cr, qwebcontext.uid, [lang_code], fmt, inner, grouping=True, monetary=True
952 if display.position == 'before':
957 return u'{pre}{0}{post}'.format(
958 formatted_amount, pre=pre, post=post
959 ).format(symbol=display.symbol,)
961 class HTMLSafe(object):
962 """ HTMLSafe string wrapper, Werkzeug's escape() has special handling for
963 objects with a ``__html__`` methods but AFAIK does not provide any such
966 Wrapping a string in HTML will prevent its escaping
968 __slots__ = ['string']
969 def __init__(self, string):
975 if isinstance(s, unicode):
976 return s.encode('utf-8')
978 def __unicode__(self):
980 if isinstance(s, str):
981 return s.decode('utf-8')
984 def nl2br(string, options=None):
985 """ Converts newlines to HTML linebreaks in ``string``. Automatically
986 escapes content unless options['html-escape'] is set to False, and returns
987 the result wrapped in an HTMLSafe object.
993 if options is None: options = {}
995 if options.get('html-escape', True):
996 string = escape(string)
997 return HTMLSafe(string.replace('\n', '<br>\n'))
999 def get_field_type(column, options):
1000 """ Gets a t-field's effective type from the field's column and its options
1002 return options.get('widget', column._type)
1004 class AssetError(Exception):
1006 class AssetNotFound(AssetError):
1009 class AssetsBundle(object):
1010 # Sass installation:
1012 # sudo gem install sass compass bootstrap-sass
1014 # If the following error is encountered:
1015 # 'ERROR: Cannot load compass.'
1017 # sudo gem install compass --pre
1018 cmd_sass = ['sass', '--stdin', '-t', 'compressed', '--unix-newlines', '--compass', '-r', 'bootstrap-sass']
1019 rx_css_import = re.compile("(@import[^;{]+;?)", re.M)
1020 rx_sass_import = re.compile("""(@import\s?['"]([^'"]+)['"])""")
1021 rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/")
1023 def __init__(self, xmlid, debug=False, cr=None, uid=None, context=None, registry=None):
1025 self.cr = request.cr if cr is None else cr
1026 self.uid = request.uid if uid is None else uid
1027 self.context = request.context if context is None else context
1028 self.registry = request.registry if registry is None else registry
1029 self.javascripts = []
1030 self.stylesheets = []
1031 self.css_errors = []
1033 self._checksum = None
1035 context = self.context.copy()
1036 context['inherit_branding'] = False
1037 context['rendering_bundle'] = True
1038 self.html = self.registry['ir.ui.view'].render(self.cr, self.uid, xmlid, context=context)
1042 fragments = html.fragments_fromstring(self.html)
1043 for el in fragments:
1044 if isinstance(el, basestring):
1045 self.remains.append(el)
1046 elif isinstance(el, html.HtmlElement):
1047 src = el.get('src', '')
1048 href = el.get('href', '')
1049 atype = el.get('type')
1050 media = el.get('media')
1051 if el.tag == 'style':
1052 if atype == 'text/sass' or src.endswith('.sass'):
1053 self.stylesheets.append(SassAsset(self, inline=el.text, media=media))
1055 self.stylesheets.append(StylesheetAsset(self, inline=el.text, media=media))
1056 elif el.tag == 'link' and el.get('rel') == 'stylesheet' and self.can_aggregate(href):
1057 if href.endswith('.sass') or atype == 'text/sass':
1058 self.stylesheets.append(SassAsset(self, url=href, media=media))
1060 self.stylesheets.append(StylesheetAsset(self, url=href, media=media))
1061 elif el.tag == 'script' and not src:
1062 self.javascripts.append(JavascriptAsset(self, inline=el.text))
1063 elif el.tag == 'script' and self.can_aggregate(src):
1064 self.javascripts.append(JavascriptAsset(self, url=src))
1066 self.remains.append(html.tostring(el))
1069 self.remains.append(html.tostring(el))
1071 # notYETimplementederror
1072 raise NotImplementedError
1074 def can_aggregate(self, url):
1075 return not urlparse(url).netloc and not url.startswith(('/web/css', '/web/js'))
1077 def to_html(self, sep=None, css=True, js=True, debug=False):
1082 if css and self.stylesheets:
1084 for style in self.stylesheets:
1085 response.append(style.to_html())
1087 for jscript in self.javascripts:
1088 response.append(jscript.to_html())
1090 url_for = self.context.get('url_for', lambda url: url)
1091 if css and self.stylesheets:
1092 href = '/web/css/%s/%s' % (self.xmlid, self.version)
1093 response.append('<link href="%s" rel="stylesheet"/>' % url_for(href))
1095 src = '/web/js/%s/%s' % (self.xmlid, self.version)
1096 response.append('<script type="text/javascript" src="%s"></script>' % url_for(src))
1097 response.extend(self.remains)
1098 return sep + sep.join(response)
1101 def last_modified(self):
1102 """Returns last modified date of linked files"""
1103 return max(itertools.chain(
1104 (asset.last_modified for asset in self.javascripts),
1105 (asset.last_modified for asset in self.stylesheets),
1110 return self.checksum[0:7]
1115 Not really a full checksum.
1116 We compute a SHA1 on the rendered bundle + max linked files last_modified date
1118 check = self.html + str(self.last_modified)
1119 return hashlib.sha1(check).hexdigest()
1122 content = self.get_cache('js')
1124 content = ';\n'.join(asset.minify() for asset in self.javascripts)
1125 self.set_cache('js', content)
1129 content = self.get_cache('css')
1132 content = '\n'.join(asset.minify() for asset in self.stylesheets)
1135 msg = '\n'.join(self.css_errors)
1136 content += self.css_message(msg.replace('\n', '\\A '))
1138 # move up all @import rules to the top
1141 matches.append(matchobj.group(0))
1144 content = re.sub(self.rx_css_import, push, content)
1146 matches.append(content)
1147 content = u'\n'.join(matches)
1150 self.set_cache('css', content)
1154 def get_cache(self, type):
1156 domain = [('url', '=', '/web/%s/%s/%s' % (type, self.xmlid, self.version))]
1157 bundle = self.registry['ir.attachment'].search_read(self.cr, self.uid, domain, ['datas'], context=self.context)
1158 if bundle and bundle[0]['datas']:
1159 content = bundle[0]['datas'].decode('base64')
1162 def set_cache(self, type, content):
1163 ira = self.registry['ir.attachment']
1164 url_prefix = '/web/%s/%s/' % (type, self.xmlid)
1165 # Invalidate previous caches
1166 oids = ira.search(self.cr, self.uid, [('url', '=like', url_prefix + '%')], context=self.context)
1168 ira.unlink(self.cr, openerp.SUPERUSER_ID, oids, context=self.context)
1169 url = url_prefix + self.version
1170 ira.create(self.cr, openerp.SUPERUSER_ID, dict(
1171 datas=content.encode('utf8').encode('base64'),
1175 ), context=self.context)
1177 def css_message(self, message):
1183 font-family: monospace;
1187 """ % message.replace('"', '\\"')
1189 def compile_sass(self):
1191 Checks if the bundle contains any sass content, then compiles it to css.
1192 Css compilation is done at the bundle level and not in the assets
1193 because they are potentially interdependant.
1195 sass = [asset for asset in self.stylesheets if isinstance(asset, SassAsset)]
1198 source = '\n'.join([asset.get_source() for asset in sass])
1200 # move up all @import rules to the top and exclude file imports
1203 ref = matchobj.group(2)
1204 line = '@import "%s"' % ref
1205 if '.' not in ref and line not in imports and not ref.startswith(('.', '/', '~')):
1206 imports.append(line)
1208 source = re.sub(self.rx_sass_import, push, source)
1209 imports.append(source)
1210 source = u'\n'.join(imports)
1213 compiler = Popen(self.cmd_sass, stdin=PIPE, stdout=PIPE, stderr=PIPE)
1215 msg = "Could not find 'sass' program needed to compile sass/scss files"
1217 self.css_errors.append(msg)
1219 result = compiler.communicate(input=source.encode('utf-8'))
1220 if compiler.returncode:
1221 error = self.get_sass_error(''.join(result), source=source)
1222 _logger.warning(error)
1223 self.css_errors.append(error)
1225 compiled = result[0].strip().decode('utf8')
1226 fragments = self.rx_css_split.split(compiled)[1:]
1228 asset_id = fragments.pop(0)
1229 asset = next(asset for asset in sass if asset.id == asset_id)
1230 asset._content = fragments.pop(0)
1232 def get_sass_error(self, stderr, source=None):
1233 # TODO: try to find out which asset the error belongs to
1234 error = stderr.split('Load paths')[0].replace(' Use --trace for backtrace.', '')
1235 error += "This error occured while compiling the bundle '%s' containing:" % self.xmlid
1236 for asset in self.stylesheets:
1237 if isinstance(asset, SassAsset):
1238 error += '\n - %s' % (asset.url if asset.url else '<inline sass>')
1241 class WebAsset(object):
1244 def __init__(self, bundle, inline=None, url=None):
1245 self.id = str(uuid.uuid4())
1246 self.bundle = bundle
1247 self.inline = inline
1250 self.uid = bundle.uid
1251 self.registry = bundle.registry
1252 self.context = bundle.context
1253 self._content = None
1254 self._filename = None
1255 self._ir_attach = None
1256 name = '<inline asset>' if inline else url
1257 self.name = "%s defined in bundle '%s'" % (name, bundle.xmlid)
1258 if not inline and not url:
1259 raise Exception("An asset should either be inlined or url linked")
1262 if not (self.inline or self._filename or self._ir_attach):
1263 addon = filter(None, self.url.split('/'))[0]
1265 # Test url against modules static assets
1266 mpath = openerp.http.addons_manifest[addon]['addons_path']
1267 self._filename = mpath + self.url.replace('/', os.path.sep)
1270 # Test url against ir.attachments
1271 fields = ['__last_update', 'datas', 'mimetype']
1272 domain = [('type', '=', 'binary'), ('url', '=', self.url)]
1273 ira = self.registry['ir.attachment']
1274 attach = ira.search_read(self.cr, self.uid, domain, fields, context=self.context)
1275 self._ir_attach = attach[0]
1277 raise AssetNotFound("Could not find %s" % self.name)
1280 raise NotImplementedError()
1283 def last_modified(self):
1287 return datetime.datetime.fromtimestamp(os.path.getmtime(self._filename))
1288 elif self._ir_attach:
1289 server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
1290 last_update = self._ir_attach['__last_update']
1292 return datetime.datetime.strptime(last_update, server_format + '.%f')
1294 return datetime.datetime.strptime(last_update, server_format)
1297 return datetime.datetime(1970, 1, 1)
1301 if not self._content:
1302 self._content = self.inline or self._fetch_content()
1303 return self._content
1305 def _fetch_content(self):
1306 """ Fetch content from file or database"""
1310 with open(self._filename, 'rb') as fp:
1311 return fp.read().decode('utf-8')
1313 return self._ir_attach['datas'].decode('base64')
1314 except UnicodeDecodeError:
1315 raise AssetError('%s is not utf-8 encoded.' % self.name)
1317 raise AssetNotFound('File %s does not exist.' % self.name)
1319 raise AssetError('Could not get content for %s.' % self.name)
1324 def with_header(self, content=None):
1326 content = self.content
1327 return '\n/* %s */\n%s' % (self.name, content)
1329 class JavascriptAsset(WebAsset):
1331 return self.with_header(rjsmin(self.content))
1333 def _fetch_content(self):
1335 return super(JavascriptAsset, self)._fetch_content()
1336 except AssetError, e:
1337 return "console.error(%s);" % json.dumps(e.message)
1341 return '<script type="text/javascript" src="%s"></script>' % (self.html_url % self.url)
1343 return '<script type="text/javascript" charset="utf-8">%s</script>' % self.with_header()
1345 class StylesheetAsset(WebAsset):
1346 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
1347 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
1348 rx_sourceMap = re.compile(r'(/\*# sourceMappingURL=.*)', re.U)
1349 rx_charset = re.compile(r'(@charset "[^"]+";)', re.U)
1351 def __init__(self, *args, **kw):
1352 self.media = kw.pop('media', None)
1353 super(StylesheetAsset, self).__init__(*args, **kw)
1357 content = super(StylesheetAsset, self).content
1359 content = '@media %s { %s }' % (self.media, content)
1362 def _fetch_content(self):
1364 content = super(StylesheetAsset, self)._fetch_content()
1365 web_dir = os.path.dirname(self.url)
1367 content = self.rx_import.sub(
1368 r"""@import \1%s/""" % (web_dir,),
1372 content = self.rx_url.sub(
1373 r"url(\1%s/" % (web_dir,),
1377 # remove charset declarations, we only support utf-8
1378 content = self.rx_charset.sub('', content)
1379 except AssetError, e:
1380 self.bundle.css_errors.append(e.message)
1385 # remove existing sourcemaps, make no sense after re-mini
1386 content = self.rx_sourceMap.sub('', self.content)
1388 content = re.sub(r'/\*.*?\*/', '', content, flags=re.S)
1390 content = re.sub(r'\s+', ' ', content)
1391 content = re.sub(r' *([{}]) *', r'\1', content)
1392 return self.with_header(content)
1395 media = (' media="%s"' % werkzeug.utils.escape(self.media)) if self.media else ''
1397 href = self.html_url % self.url
1398 return '<link rel="stylesheet" href="%s" type="text/css"%s/>' % (href, media)
1400 return '<style type="text/css"%s>%s</style>' % (media, self.with_header())
1402 class SassAsset(StylesheetAsset):
1404 rx_indent = re.compile(r'^( +|\t+)', re.M)
1409 return self.with_header()
1413 ira = self.registry['ir.attachment']
1414 url = self.html_url % self.url
1415 domain = [('type', '=', 'binary'), ('url', '=', self.url)]
1416 ira_id = ira.search(self.cr, self.uid, domain, context=self.context)
1418 # TODO: update only if needed
1419 ira.write(self.cr, openerp.SUPERUSER_ID, [ira_id], {'datas': self.content}, context=self.context)
1421 ira.create(self.cr, openerp.SUPERUSER_ID, dict(
1422 datas=self.content.encode('utf8').encode('base64'),
1423 mimetype='text/css',
1427 ), context=self.context)
1428 return super(SassAsset, self).to_html()
1430 def get_source(self):
1431 content = textwrap.dedent(self.inline or self._fetch_content())
1435 if self.indent is None:
1437 if self.indent == self.reindent:
1438 # Don't reindent the file if identation is the final one (reindent)
1439 raise StopIteration()
1440 return ind.replace(self.indent, self.reindent)
1443 content = self.rx_indent.sub(fix_indent, content)
1444 except StopIteration:
1446 return "/*! %s */\n%s" % (self.id, content)
1449 """ Minify js with a clever regex.
1450 Taken from http://opensource.perlig.de/rjsmin
1451 Apache License, Version 2.0 """
1453 """ Substitution callback """
1454 groups = match.groups()
1460 (groups[4] and '\n') or
1461 (groups[5] and ' ') or
1462 (groups[6] and ' ') or
1463 (groups[7] and ' ') or
1468 r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
1469 r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
1470 r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
1471 r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
1472 r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
1473 r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
1474 r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
1475 r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
1476 r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
1477 r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
1478 r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
1479 r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
1480 r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
1481 r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
1482 r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
1483 r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
1484 r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
1485 r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
1486 r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
1487 r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
1488 r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script