1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306 |
- # -*- coding: utf-8 -*-
- from contextlib import closing
- from collections import OrderedDict
- from datetime import datetime
- from lxml import etree
- from subprocess import Popen, PIPE
- import base64
- import copy
- import hashlib
- import io
- import itertools
- import json
- import logging
- import os
- import re
- import textwrap
- import uuid
- import psycopg2
- try:
- import sass as libsass
- except ImportError:
- # If the `sass` python library isn't found, we fallback on the
- # `sassc` executable in the path.
- libsass = None
- from odoo import release, SUPERUSER_ID, _
- from odoo.http import request
- from odoo.modules.module import get_resource_path
- from odoo.tools import (func, misc, transpile_javascript,
- is_odoo_module, SourceMapGenerator, profiler,
- apply_inheritance_specs)
- from odoo.tools.misc import file_open, html_escape as escape
- from odoo.tools.pycompat import to_text
- _logger = logging.getLogger(__name__)
- EXTENSIONS = (".js", ".css", ".scss", ".sass", ".less", ".xml")
- class CompileError(RuntimeError): pass
- def rjsmin(script):
- """ Minify js with a clever regex.
- Taken from http://opensource.perlig.de/rjsmin (version 1.1.0)
- Apache License, Version 2.0 """
- def subber(match):
- """ Substitution callback """
- groups = match.groups()
- return (
- groups[0] or
- groups[1] or
- (groups[3] and (groups[2] + '\n')) or
- groups[2] or
- (groups[5] and "%s%s%s" % (
- groups[4] and '\n' or '',
- groups[5],
- groups[6] and '\n' or '',
- )) or
- (groups[7] and '\n') or
- (groups[8] and ' ') or
- (groups[9] and ' ') or
- (groups[10] and ' ') or
- ''
- )
- result = re.sub(
- r'([^\047"\140/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^'
- r'\r\n]|\r?\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^'
- r'\r\n]|\r?\n|\r)[^"\\\r\n]*)*")|(?:\140[^\140\\]*(?:\\(?:[^\r\n'
- r']|\r?\n|\r)[^\140\\]*)*\140))[^\047"\140/\000-\040]*)|(?<=[(,='
- r':\[!&|?{};\r\n+*-])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*'
- r'\*+(?:[^/*][^*]*\*+)*/))*(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-'
- r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)*('
- r'(?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
- r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/))((?:[\000-\011'
- r'\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*(?:(?:('
- r'?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
- r']*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040&)+,.:;=?\]|}-]))?|'
- r'(?<=[\000-#%-,./:-@\[-^\140{-~-]return)(?:[\000-\011\013\014\0'
- r'16-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*(?:((?:(?://[^\r'
- r'\n]*)?[\r\n]))(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?'
- r':[^/*][^*]*\*+)*/))*)*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^'
- r'\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r'
- r'\n]*)*/))((?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
- r'*][^*]*\*+)*/))*(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013'
- r'\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000'
- r'-\040&)+,.:;=?\]|}-]))?|(?<=[^\000-!#%&(*,./:-@\[\\^{|~])(?:['
- r'\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'
- r')*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\014\016-\040'
- r']|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#%-\047'
- r')*,./:-@\\-^\140|-~])|(?<=[^\000-#%-,./:-@\[-^\140{-~-])((?:['
- r'\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'
- r'))+(?=[^\000-#%-,./:-@\[-^\140{-~-])|(?<=\+)((?:[\000-\011\013'
- r'\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<'
- r'=-)((?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]'
- r'*\*+)*/)))+(?=-)|(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*'
- r'+(?:[^/*][^*]*\*+)*/))+|(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-'
- r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
- ).strip()
- return result
- class AssetError(Exception):
- pass
- class AssetNotFound(AssetError):
- pass
- class AssetsBundle(object):
- rx_css_import = re.compile("(@import[^;{]+;?)", re.M)
- rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""")
- rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/")
- TRACKED_BUNDLES = ['web.assets_common', 'web.assets_backend']
- def __init__(self, name, files, env=None, css=True, js=True):
- """
- :param name: bundle name
- :param files: files to be added to the bundle
- :param css: if css is True, the stylesheets files are added to the bundle
- :param js: if js is True, the javascript files are added to the bundle
- """
- self.name = name
- self.env = request.env if env is None else env
- self.javascripts = []
- self.templates = []
- self.stylesheets = []
- self.css_errors = []
- self.files = files
- self.user_direction = self.env['res.lang']._lang_get(
- self.env.context.get('lang') or self.env.user.lang
- ).direction
- # asset-wide html "media" attribute
- for f in files:
- if css:
- if f['atype'] == 'text/sass':
- self.stylesheets.append(SassStylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'], direction=self.user_direction))
- elif f['atype'] == 'text/scss':
- self.stylesheets.append(ScssStylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'], direction=self.user_direction))
- elif f['atype'] == 'text/less':
- self.stylesheets.append(LessStylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'], direction=self.user_direction))
- elif f['atype'] == 'text/css':
- self.stylesheets.append(StylesheetAsset(self, url=f['url'], filename=f['filename'], inline=f['content'], media=f['media'], direction=self.user_direction))
- if js:
- if f['atype'] == 'text/javascript':
- self.javascripts.append(JavascriptAsset(self, url=f['url'], filename=f['filename'], inline=f['content']))
- elif f['atype'] == 'text/xml':
- self.templates.append(XMLAsset(self, url=f['url'], filename=f['filename'], inline=f['content']))
- def to_node(self, css=True, js=True, debug=False, async_load=False, defer_load=False, lazy_load=False):
- """
- :returns [(tagName, attributes, content)] if the tag is auto close
- """
- response = []
- is_debug_assets = debug and 'assets' in debug
- if css and self.stylesheets:
- css_attachments = self.css(is_minified=not is_debug_assets) or []
- for attachment in css_attachments:
- if is_debug_assets:
- href = self.get_debug_asset_url(extra='rtl/' if self.user_direction == 'rtl' else '',
- name=css_attachments.name,
- extension='')
- else:
- href = attachment.url
- attr = dict([
- ["type", "text/css"],
- ["rel", "stylesheet"],
- ["href", href],
- ['data-asset-bundle', self.name],
- ['data-asset-version', self.version],
- ])
- response.append(("link", attr, None))
- if self.css_errors:
- msg = '\n'.join(self.css_errors)
- response.append(JavascriptAsset(self, inline=self.dialog_message(msg)).to_node())
- response.append(StylesheetAsset(self, url="/web/static/lib/bootstrap/dist/css/bootstrap.css").to_node())
- if js and self.javascripts:
- js_attachment = self.js(is_minified=not is_debug_assets)
- src = self.get_debug_asset_url(name=js_attachment.name, extension='') if is_debug_assets else js_attachment[0].url
- attr = dict([
- ["async", "async" if async_load else None],
- ["defer", "defer" if defer_load or lazy_load else None],
- ["type", "text/javascript"],
- ["data-src" if lazy_load else "src", src],
- ['data-asset-bundle', self.name],
- ['data-asset-version', self.version],
- ])
- response.append(("script", attr, None))
- return response
- @func.lazy_property
- def last_modified_combined(self):
- """Returns last modified date of linked files"""
- # WebAsset are recreate here when a better solution would be to use self.stylesheets and self.javascripts
- # We currently have no garanty that they are present since it will depends on js and css parameters
- # last_modified is actually only usefull for the checksum and checksum should be extension specific since
- # they are differents bundles. This will be a future work.
- # changing the logic from max date to combined date to fix bundle invalidation issues.
- assets = [WebAsset(self, url=f['url'], filename=f['filename'], inline=f['content'])
- for f in self.files
- if f['atype'] in ['text/sass', "text/scss", "text/less", "text/css", "text/javascript", "text/xml"]]
- return ','.join(str(asset.last_modified) for asset in assets)
- @func.lazy_property
- def version(self):
- return self.checksum[0:7]
- @func.lazy_property
- def checksum(self):
- """
- Not really a full checksum.
- We compute a SHA512/256 on the rendered bundle + combined linked files last_modified date
- """
- check = u"%s%s" % (json.dumps(self.files, sort_keys=True), self.last_modified_combined)
- return hashlib.sha512(check.encode('utf-8')).hexdigest()[:64]
- def _get_asset_template_url(self):
- return "/web/assets/{id}-{unique}/{extra}{name}{sep}{extension}"
- def _get_asset_url_values(self, id, unique, extra, name, sep, extension): # extra can contain direction or/and website
- return {
- 'id': id,
- 'unique': unique,
- 'extra': extra,
- 'name': name,
- 'sep': sep,
- 'extension': extension,
- }
- def get_asset_url(self, id='%', unique='%', extra='', name='%', sep="%", extension='%'):
- return self._get_asset_template_url().format(
- **self._get_asset_url_values(id=id, unique=unique, extra=extra, name=name, sep=sep, extension=extension)
- )
- def get_debug_asset_url(self, extra='', name='%', extension='%'):
- return f"/web/assets/debug/{extra}{name}{extension}"
- def _unlink_attachments(self, attachments):
- """ Unlinks attachments without actually calling unlink, so that the ORM cache is not cleared.
- Specifically, if an attachment is generated while a view is rendered, clearing the ORM cache
- could unload fields loaded with a sudo(), and expected to be readable by the view.
- Such a view would be website.layout when main_object is an ir.ui.view.
- """
- to_delete = set(attach.store_fname for attach in attachments if attach.store_fname)
- self.env.cr.execute(f"""DELETE FROM {attachments._table} WHERE id IN (
- SELECT id FROM {attachments._table} WHERE id in %s FOR NO KEY UPDATE SKIP LOCKED
- )""", [tuple(attachments.ids)])
- for file_path in to_delete:
- attachments._file_delete(file_path)
- def clean_attachments(self, extension):
- """ Takes care of deleting any outdated ir.attachment records associated to a bundle before
- saving a fresh one.
- When `extension` is js we need to check that we are deleting a different version (and not *any*
- version) because, as one of the creates in `save_attachment` can trigger a rollback, the
- call to `clean_attachments ` is made at the end of the method in order to avoid the rollback
- of an ir.attachment unlink (because we cannot rollback a removal on the filestore), thus we
- must exclude the current bundle.
- """
- ira = self.env['ir.attachment']
- url = self.get_asset_url(
- extra='%s' % ('rtl/' if extension in ['css', 'min.css'] and self.user_direction == 'rtl' else ''),
- name=self.name,
- sep='',
- extension='.%s' % extension
- )
- domain = [
- ('url', '=like', url),
- '!', ('url', '=like', self.get_asset_url(unique=self.version))
- ]
- attachments = ira.sudo().search(domain)
- # avoid to invalidate cache if it's already empty (mainly useful for test)
- if attachments:
- self._unlink_attachments(attachments)
- # force bundle invalidation on other workers
- self.env['ir.qweb'].clear_caches()
- return True
- def get_attachments(self, extension, ignore_version=False):
- """ Return the ir.attachment records for a given bundle. This method takes care of mitigating
- an issue happening when parallel transactions generate the same bundle: while the file is not
- duplicated on the filestore (as it is stored according to its hash), there are multiple
- ir.attachment records referencing the same version of a bundle. As we don't want to source
- multiple time the same bundle in our `to_html` function, we group our ir.attachment records
- by file name and only return the one with the max id for each group.
- :param extension: file extension (js, min.js, css)
- :param ignore_version: if ignore_version, the url contains a version => web/assets/%-%/name.extension
- (the second '%' corresponds to the version),
- else: the url contains a version equal to that of the self.version
- => web/assets/%-self.version/name.extension.
- """
- unique = "%" if ignore_version else self.version
- url_pattern = self.get_asset_url(
- unique=unique,
- extra='%s' % ('rtl/' if extension in ['css', 'min.css'] and self.user_direction == 'rtl' else ''),
- name=self.name,
- sep='',
- extension='.%s' % extension
- )
- self.env.cr.execute("""
- SELECT max(id)
- FROM ir_attachment
- WHERE create_uid = %s
- AND url like %s
- GROUP BY name
- ORDER BY name
- """, [SUPERUSER_ID, url_pattern])
- attachment_ids = [r[0] for r in self.env.cr.fetchall()]
- return self.env['ir.attachment'].sudo().browse(attachment_ids)
- def add_post_rollback(self):
- """
- In some rare cases it is possible that an attachment is created
- during a transaction, added to the ormcache but the transaction
- is rolled back, leading to 404 when getting the attachments.
- This postrollback hook will help fix this issue by clearing the
- cache if it is not committed.
- """
- self.env.cr.postrollback.add(self.env.registry._Registry__cache.clear)
- def save_attachment(self, extension, content):
- """Record the given bundle in an ir.attachment and delete
- all other ir.attachments referring to this bundle (with the same name and extension).
- :param extension: extension of the bundle to be recorded
- :param content: bundle content to be recorded
- :return the ir.attachment records for a given bundle.
- """
- assert extension in ('js', 'min.js', 'js.map', 'css', 'min.css', 'css.map', 'xml', 'min.xml')
- ira = self.env['ir.attachment']
- # Set user direction in name to store two bundles
- # 1 for ltr and 1 for rtl, this will help during cleaning of assets bundle
- # and allow to only clear the current direction bundle
- # (this applies to css bundles only)
- fname = '%s.%s' % (self.name, extension)
- mimetype = (
- 'text/css' if extension in ['css', 'min.css'] else
- 'text/xml' if extension in ['xml', 'min.xml'] else
- 'application/json' if extension in ['js.map', 'css.map'] else
- 'application/javascript'
- )
- values = {
- 'name': fname,
- 'mimetype': mimetype,
- 'res_model': 'ir.ui.view',
- 'res_id': False,
- 'type': 'binary',
- 'public': True,
- 'raw': content.encode('utf8'),
- }
- self.add_post_rollback()
- attachment = ira.with_user(SUPERUSER_ID).create(values)
- url = self.get_asset_url(
- id=attachment.id,
- unique=self.version,
- extra='%s' % ('rtl/' if extension in ['css', 'min.css'] and self.user_direction == 'rtl' else ''),
- name=fname,
- sep='', # included in fname
- extension=''
- )
- values = {
- 'url': url,
- }
- attachment.write(values)
- if self.env.context.get('commit_assetsbundle') is True:
- self.env.cr.commit()
- self.clean_attachments(extension)
- # For end-user assets (common and backend), send a message on the bus
- # to invite the user to refresh their browser
- if self.env and 'bus.bus' in self.env and self.name in self.TRACKED_BUNDLES:
- self.env['bus.bus']._sendone('broadcast', 'bundle_changed', {
- 'server_version': release.version # Needs to be dynamically imported
- })
- _logger.debug('Asset Changed: bundle: %s -- version: %s', self.name, self.version)
- return attachment
- def js(self, is_minified=True):
- extension = 'min.js' if is_minified else 'js'
- js_attachment = self.get_attachments(extension)
- if not js_attachment:
- template_bundle = ''
- if self.templates:
- content = ['<?xml version="1.0" encoding="UTF-8"?>']
- content.append('<templates xml:space="preserve">')
- content.append(self.xml(show_inherit_info=not is_minified))
- content.append('</templates>')
- templates = '\n'.join(content).replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${")
- template_bundle = textwrap.dedent(f"""
- /*******************************************
- * Templates *
- *******************************************/
- odoo.define('{self.name}.bundle.xml', function(require){{
- 'use strict';
- const {{ loadXML }} = require('@web/core/assets');
- const templates = `{templates}`;
- return loadXML(templates);
- }});""")
- if is_minified:
- content_bundle = ';\n'.join(asset.minify() for asset in self.javascripts)
- content_bundle += template_bundle
- js_attachment = self.save_attachment(extension, content_bundle)
- else:
- js_attachment = self.js_with_sourcemap(template_bundle=template_bundle)
- return js_attachment[0]
- def js_with_sourcemap(self, template_bundle=None):
- """Create the ir.attachment representing the not-minified content of the bundleJS
- and create/modify the ir.attachment representing the linked sourcemap.
- :return ir.attachment representing the un-minified content of the bundleJS
- """
- sourcemap_attachment = self.get_attachments('js.map') \
- or self.save_attachment('js.map', '')
- generator = SourceMapGenerator(
- source_root="/".join(
- [".." for i in range(0, len(self.get_debug_asset_url(name=self.name).split("/")) - 2)]
- ) + "/",
- )
- content_bundle_list = []
- content_line_count = 0
- line_header = 5 # number of lines added by with_header()
- for asset in self.javascripts:
- if asset.is_transpiled:
- # '+ 3' corresponds to the 3 lines added at the beginning of the file during transpilation.
- generator.add_source(
- asset.url, asset._content, content_line_count, start_offset=line_header + 3)
- else:
- generator.add_source(
- asset.url, asset.content, content_line_count, start_offset=line_header)
- content_bundle_list.append(asset.with_header(asset.content, minimal=False))
- content_line_count += len(asset.content.split("\n")) + line_header
- content_bundle = ';\n'.join(content_bundle_list)
- if template_bundle:
- content_bundle += template_bundle
- content_bundle += "\n\n//# sourceMappingURL=" + sourcemap_attachment.url
- js_attachment = self.save_attachment('js', content_bundle)
- generator._file = js_attachment.url
- sourcemap_attachment.write({
- "raw": generator.get_content()
- })
- return js_attachment
- def xml(self, show_inherit_info=False):
- """
- Create the ir.attachment representing the content of the bundle XML.
- The xml contents are loaded and parsed with etree. Inheritances are
- applied in the order of files and templates.
- Used parsed attributes:
- * `t-name`: template name
- * `t-inherit`: inherited template name. The template use the
- `apply_inheritance_specs` method from `ir.ui.view` to apply
- inheritance (with xpath and position).
- * 't-inherit-mode': 'primary' to create a new template with the
- update, or 'extension' to apply the update on the inherited
- template.
- * `t-extend` deprecated attribute, used by the JavaScript Qweb.
- :param show_inherit_info: if true add the file url and inherit
- information in the template.
- :return ir.attachment representing the content of the bundle XML
- """
- template_dict = OrderedDict()
- parser = etree.XMLParser(ns_clean=True, recover=True, remove_comments=True)
- for asset in self.templates:
- # Load content.
- try:
- content = asset.content.strip()
- template = content if content.startswith('<odoo>') else f'<templates>{asset.content}</templates>'
- io_content = io.BytesIO(template.encode('utf-8'))
- content_templates_tree = etree.parse(io_content, parser=parser).getroot()
- except etree.ParseError as e:
- _logger.error("Could not parse file %s: %s", asset.url, e.msg)
- raise
- addon = asset.url.split('/')[1]
- template_dict.setdefault(addon, OrderedDict())
- # Process every templates.
- for template_tree in list(content_templates_tree):
- template_name = None
- if 't-name' in template_tree.attrib:
- template_name = template_tree.attrib['t-name']
- dotted_names = template_name.split('.', 1)
- if len(dotted_names) > 1 and dotted_names[0] == addon:
- template_name = dotted_names[1]
- if 't-inherit' in template_tree.attrib:
- inherit_mode = template_tree.attrib.get('t-inherit-mode', 'primary')
- if inherit_mode not in ['primary', 'extension']:
- raise ValueError(_("Invalid inherit mode. Module %r and template name %r", addon, template_name))
- # Get inherited template, the identifier can be "addon.name", just "name" or (silly) "just.name.with.dots"
- parent_dotted_name = template_tree.attrib['t-inherit']
- split_name_attempt = parent_dotted_name.split('.', 1)
- parent_addon, parent_name = split_name_attempt if len(split_name_attempt) == 2 else (addon, parent_dotted_name)
- if parent_addon not in template_dict:
- if parent_dotted_name in template_dict[addon]:
- parent_addon = addon
- parent_name = parent_dotted_name
- else:
- raise ValueError(_("Module %r not loaded or inexistent (try to inherit %r), or templates of addon being loaded %r are misordered (template %r)", parent_addon, parent_name, addon, template_name))
- if parent_name not in template_dict[parent_addon]:
- raise ValueError(_("Cannot create %r because the template to inherit %r is not found.") % (f'{addon}.{template_name}', f'{parent_addon}.{parent_name}'))
- # After several performance tests, we found out that deepcopy is the most efficient
- # solution in this case (compared with copy, xpath with '.' and stringifying).
- parent_tree, parent_urls = template_dict[parent_addon][parent_name]
- parent_tree = copy.deepcopy(parent_tree)
- if show_inherit_info:
- # Add inheritance information as xml comment for debugging.
- xpaths = []
- for item in template_tree:
- position = item.get('position')
- attrib = dict(**item.attrib)
- attrib.pop('position', None)
- comment = etree.Comment(f""" Filepath: {asset.url} ; position="{position}" ; {attrib} """)
- if position == "attributes":
- if item.get('expr'):
- comment_node = etree.Element('xpath', {'expr': item.get('expr'), 'position': 'before'})
- else:
- comment_node = etree.Element(item.tag, item.attrib)
- comment_node.attrib['position'] = 'before'
- comment_node.append(comment)
- xpaths.append(comment_node)
- else:
- if len(item) > 0:
- item[0].addprevious(comment)
- else:
- item.append(comment)
- xpaths.append(item)
- else:
- xpaths = list(template_tree)
- # Apply inheritance.
- if inherit_mode == 'primary':
- parent_tree.tag = template_tree.tag
- inherited_template = apply_inheritance_specs(parent_tree, xpaths)
- if inherit_mode == 'primary': # New template_tree: A' = B(A)
- for attr_name, attr_val in template_tree.attrib.items():
- if attr_name not in ('t-inherit', 't-inherit-mode'):
- inherited_template.set(attr_name, attr_val)
- if not template_name:
- raise ValueError(_("Template name is missing in file %r.", asset.url))
- template_dict[addon][template_name] = (inherited_template, parent_urls + [asset.url])
- else: # Modifies original: A = B(A)
- template_dict[parent_addon][parent_name] = (inherited_template, parent_urls + [asset.url])
- elif template_name:
- if template_name in template_dict[addon]:
- raise ValueError(_("Template %r already exists in module %r", template_name, addon))
- template_dict[addon][template_name] = (template_tree, [asset.url])
- elif template_tree.attrib.get('t-extend'):
- template_name = '%s__extend_%s' % (template_tree.attrib.get('t-extend'), len(template_dict[addon]))
- template_dict[addon][template_name] = (template_tree, [asset.url])
- else:
- raise ValueError(_("Template name is missing in file %r.", asset.url))
- # Concat and render inherited templates
- root = etree.Element('root')
- for addon in template_dict.values():
- for template, urls in addon.values():
- if show_inherit_info:
- tail = "\n"
- if len(root) > 0:
- tail = root[-1].tail
- root[-1].tail = "\n\n"
- comment = etree.Comment(f""" Filepath: {' => '.join(urls)} """)
- comment.tail = tail
- root.append(comment)
- root.append(template)
- # Returns the string by removing the <root> tag.
- return etree.tostring(root, encoding='unicode')[6:-7]
- def css(self, is_minified=True):
- extension = 'min.css' if is_minified else 'css'
- attachments = self.get_attachments(extension)
- if not attachments:
- # get css content
- css = self.preprocess_css()
- if self.css_errors:
- return self.get_attachments(extension, ignore_version=True)
- matches = []
- css = re.sub(self.rx_css_import, lambda matchobj: matches.append(matchobj.group(0)) and '', css)
- if is_minified:
- # move up all @import rules to the top
- matches.append(css)
- css = u'\n'.join(matches)
- self.save_attachment(extension, css)
- attachments = self.get_attachments(extension)
- else:
- return self.css_with_sourcemap(u'\n'.join(matches))
- return attachments
- def css_with_sourcemap(self, content_import_rules):
- """Create the ir.attachment representing the not-minified content of the bundleCSS
- and create/modify the ir.attachment representing the linked sourcemap.
- :param content_import_rules: string containing all the @import rules to put at the beginning of the bundle
- :return ir.attachment representing the un-minified content of the bundleCSS
- """
- sourcemap_attachment = self.get_attachments('css.map') \
- or self.save_attachment('css.map', '')
- debug_asset_url = self.get_debug_asset_url(name=self.name,
- extra='rtl/' if self.user_direction == 'rtl' else '')
- generator = SourceMapGenerator(
- source_root="/".join(
- [".." for i in range(0, len(debug_asset_url.split("/")) - 2)]
- ) + "/",
- )
- # adds the @import rules at the beginning of the bundle
- content_bundle_list = [content_import_rules]
- content_line_count = len(content_import_rules.split("\n"))
- for asset in self.stylesheets:
- if asset.content:
- content = asset.with_header(asset.content)
- if asset.url:
- generator.add_source(asset.url, content, content_line_count)
- # comments all @import rules that have been added at the beginning of the bundle
- content = re.sub(self.rx_css_import, lambda matchobj: f"/* {matchobj.group(0)} */", content)
- content_bundle_list.append(content)
- content_line_count += len(content.split("\n"))
- content_bundle = '\n'.join(content_bundle_list) + f"\n//*# sourceMappingURL={sourcemap_attachment.url} */"
- css_attachment = self.save_attachment('css', content_bundle)
- generator._file = css_attachment.url
- sourcemap_attachment.write({
- "raw": generator.get_content(),
- })
- return css_attachment
- def dialog_message(self, message):
- """
- Returns a JS script which shows a warning to the user on page load.
- TODO: should be refactored to be a base js file whose code is extended
- by related apps (web/website).
- """
- return """
- (function (message) {
- 'use strict';
- if (window.__assetsBundleErrorSeen) {
- return;
- }
- window.__assetsBundleErrorSeen = true;
- if (document.readyState !== 'loading') {
- onDOMContentLoaded();
- } else {
- window.addEventListener('DOMContentLoaded', () => onDOMContentLoaded());
- }
- async function onDOMContentLoaded() {
- var odoo = window.top.odoo;
- if (!odoo || !odoo.define) {
- useAlert();
- return;
- }
- // Wait for potential JS loading
- await new Promise(resolve => {
- const noLazyTimeout = setTimeout(() => resolve(), 10); // 10 since need to wait for promise resolutions of odoo.define
- odoo.define('AssetsBundle.PotentialLazyLoading', function (require) {
- 'use strict';
- const lazyloader = require('web.public.lazyloader');
- clearTimeout(noLazyTimeout);
- lazyloader.allScriptsLoaded.then(() => resolve());
- });
- });
- var alertTimeout = setTimeout(useAlert, 10); // 10 since need to wait for promise resolutions of odoo.define
- odoo.define('AssetsBundle.ErrorMessage', function (require) {
- 'use strict';
- require('web.dom_ready');
- var core = require('web.core');
- var Dialog = require('web.Dialog');
- var _t = core._t;
- clearTimeout(alertTimeout);
- new Dialog(null, {
- title: _t("Style error"),
- $content: $('<div/>')
- .append($('<p/>', {text: _t("The style compilation failed, see the error below. Your recent actions may be the cause, please try reverting the changes you made.")}))
- .append($('<pre/>', {html: message})),
- }).open();
- });
- }
- function useAlert() {
- window.alert(message);
- }
- })("%s");
- """ % message.replace('"', '\\"').replace('\n', '
')
- def _get_assets_domain_for_already_processed_css(self, assets):
- """ Method to compute the attachments' domain to search the already process assets (css).
- This method was created to be overridden.
- """
- return [('url', 'in', list(assets.keys()))]
- def is_css_preprocessed(self):
- preprocessed = True
- old_attachments = self.env['ir.attachment'].sudo()
- asset_types = [SassStylesheetAsset, ScssStylesheetAsset, LessStylesheetAsset]
- if self.user_direction == 'rtl':
- asset_types.append(StylesheetAsset)
- for atype in asset_types:
- outdated = False
- assets = dict((asset.html_url, asset) for asset in self.stylesheets if isinstance(asset, atype))
- if assets:
- assets_domain = self._get_assets_domain_for_already_processed_css(assets)
- attachments = self.env['ir.attachment'].sudo().search(assets_domain)
- old_attachments += attachments
- for attachment in attachments:
- asset = assets[attachment.url]
- if asset.last_modified > attachment['__last_update']:
- outdated = True
- break
- if asset._content is None:
- asset._content = (attachment.raw or b'').decode('utf8')
- if not asset._content and attachment.file_size > 0:
- asset._content = None # file missing, force recompile
- if any(asset._content is None for asset in assets.values()):
- outdated = True
- if outdated:
- preprocessed = False
- return preprocessed, old_attachments
- def preprocess_css(self, debug=False, old_attachments=None):
- """
- Checks if the bundle contains any sass/less content, then compiles it to css.
- If user language direction is Right to Left then consider css files to call run_rtlcss,
- css files are also stored in ir.attachment after processing done by rtlcss.
- Returns the bundle's flat css.
- """
- if self.stylesheets:
- compiled = ""
- for atype in (SassStylesheetAsset, ScssStylesheetAsset, LessStylesheetAsset):
- assets = [asset for asset in self.stylesheets if isinstance(asset, atype)]
- if assets:
- source = '\n'.join([asset.get_source() for asset in assets])
- compiled += self.compile_css(assets[0].compile, source)
- # We want to run rtlcss on normal css, so merge it in compiled
- if self.user_direction == 'rtl':
- stylesheet_assets = [asset for asset in self.stylesheets if not isinstance(asset, (SassStylesheetAsset, ScssStylesheetAsset, LessStylesheetAsset))]
- compiled += '\n'.join([asset.get_source() for asset in stylesheet_assets])
- compiled = self.run_rtlcss(compiled)
- if not self.css_errors and old_attachments:
- self._unlink_attachments(old_attachments)
- old_attachments = None
- fragments = self.rx_css_split.split(compiled)
- at_rules = fragments.pop(0)
- if at_rules:
- # Sass and less moves @at-rules to the top in order to stay css 2.1 compatible
- self.stylesheets.insert(0, StylesheetAsset(self, inline=at_rules))
- while fragments:
- asset_id = fragments.pop(0)
- asset = next(asset for asset in self.stylesheets if asset.id == asset_id)
- asset._content = fragments.pop(0)
- return '\n'.join(asset.minify() for asset in self.stylesheets)
- def compile_css(self, compiler, source):
- """Sanitizes @import rules, remove duplicates @import rules, then compile"""
- imports = []
- def handle_compile_error(e, source):
- error = self.get_preprocessor_error(e, source=source)
- _logger.warning(error)
- self.css_errors.append(error)
- return ''
- def sanitize(matchobj):
- ref = matchobj.group(2)
- line = '@import "%s"%s' % (ref, matchobj.group(3))
- if '.' not in ref and line not in imports and not ref.startswith(('.', '/', '~')):
- imports.append(line)
- return line
- msg = "Local import '%s' is forbidden for security reasons. Please remove all @import {your_file} imports in your custom files. In Odoo you have to import all files in the assets, and not through the @import statement." % ref
- _logger.warning(msg)
- self.css_errors.append(msg)
- return ''
- source = re.sub(self.rx_preprocess_imports, sanitize, source)
- compiled = ''
- try:
- compiled = compiler(source)
- except CompileError as e:
- return handle_compile_error(e, source=source)
- compiled = compiled.strip()
- # Post process the produced css to add required vendor prefixes here
- compiled = re.sub(r'(appearance: (\w+);)', r'-webkit-appearance: \2; -moz-appearance: \2; \1', compiled)
- # Most of those are only useful for wkhtmltopdf (some for old PhantomJS)
- compiled = re.sub(r'(display: ((?:inline-)?)flex((?: ?!important)?);)', r'display: -webkit-\2box\3; display: -webkit-\2flex\3; \1', compiled)
- compiled = re.sub(r'(justify-content: flex-(\w+)((?: ?!important)?);)', r'-webkit-box-pack: \2\3; \1', compiled)
- compiled = re.sub(r'(flex-flow: (\w+ \w+);)', r'-webkit-flex-flow: \2; \1', compiled)
- compiled = re.sub(r'(flex-direction: (column);)', r'-webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: \2; \1', compiled)
- compiled = re.sub(r'(flex-wrap: (\w+);)', r'-webkit-flex-wrap: \2; \1', compiled)
- compiled = re.sub(r'(flex: ((\d)+ \d+ (?:\d+|auto));)', r'-webkit-box-flex: \3; -webkit-flex: \2; \1', compiled)
- return compiled
- def run_rtlcss(self, source):
- rtlcss = 'rtlcss'
- if os.name == 'nt':
- try:
- rtlcss = misc.find_in_path('rtlcss.cmd')
- except IOError:
- rtlcss = 'rtlcss'
- cmd = [rtlcss, '-c', get_resource_path("base", "data/rtlcss.json"), '-']
- try:
- rtlcss = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
- except Exception:
- # Check the presence of rtlcss, if rtlcss not available then we should return normal less file
- try:
- process = Popen(
- ['rtlcss', '--version'], stdout=PIPE, stderr=PIPE
- )
- except (OSError, IOError):
- _logger.warning('You need https://rtlcss.com/ to convert css file to right to left compatiblity. Use: npm install -g rtlcss')
- return source
- msg = "Could not execute command %r" % cmd[0]
- _logger.error(msg)
- self.css_errors.append(msg)
- return ''
- result = rtlcss.communicate(input=source.encode('utf-8'))
- if rtlcss.returncode:
- cmd_output = ''.join(misc.ustr(result))
- if not cmd_output:
- cmd_output = "Process exited with return code %d\n" % rtlcss.returncode
- error = self.get_rtlcss_error(cmd_output, source=source)
- _logger.warning(error)
- self.css_errors.append(error)
- return ''
- rtlcss_result = result[0].strip().decode('utf8')
- return rtlcss_result
- def get_preprocessor_error(self, stderr, source=None):
- """Improve and remove sensitive information from sass/less compilator error messages"""
- error = misc.ustr(stderr).split('Load paths')[0].replace(' Use --trace for backtrace.', '')
- if 'Cannot load compass' in error:
- error += "Maybe you should install the compass gem using this extra argument:\n\n" \
- " $ sudo gem install compass --pre\n"
- error += "This error occurred while compiling the bundle '%s' containing:" % self.name
- for asset in self.stylesheets:
- if isinstance(asset, PreprocessedCSS):
- error += '\n - %s' % (asset.url if asset.url else '<inline sass>')
- return error
- def get_rtlcss_error(self, stderr, source=None):
- """Improve and remove sensitive information from sass/less compilator error messages"""
- error = misc.ustr(stderr).split('Load paths')[0].replace(' Use --trace for backtrace.', '')
- error += "This error occurred while compiling the bundle '%s' containing:" % self.name
- return error
- class WebAsset(object):
- html_url_format = '%s'
- _content = None
- _filename = None
- _ir_attach = None
- _id = None
- def __init__(self, bundle, inline=None, url=None, filename=None):
- self.bundle = bundle
- self.inline = inline
- self._filename = filename
- self.url = url
- self.html_url_args = url
- if not inline and not url:
- raise Exception("An asset should either be inlined or url linked, defined in bundle '%s'" % bundle.name)
- @func.lazy_property
- def id(self):
- if self._id is None: self._id = str(uuid.uuid4())
- return self._id
- @func.lazy_property
- def name(self):
- return '<inline asset>' if self.inline else self.url
- @property
- def html_url(self):
- return self.html_url_format % self.html_url_args
- def stat(self):
- if not (self.inline or self._filename or self._ir_attach):
- path = (segment for segment in self.url.split('/') if segment)
- self._filename = get_resource_path(*path)
- if self._filename:
- return
- try:
- # Test url against ir.attachments
- self._ir_attach = self.bundle.env['ir.attachment'].sudo()._get_serve_attachment(self.url)
- self._ir_attach.ensure_one()
- except ValueError:
- raise AssetNotFound("Could not find %s" % self.name)
- def to_node(self):
- raise NotImplementedError()
- @func.lazy_property
- def last_modified(self):
- try:
- self.stat()
- if self._filename:
- return datetime.fromtimestamp(os.path.getmtime(self._filename))
- elif self._ir_attach:
- return self._ir_attach['__last_update']
- except Exception:
- pass
- return datetime(1970, 1, 1)
- @property
- def content(self):
- if self._content is None:
- self._content = self.inline or self._fetch_content()
- return self._content
- def _fetch_content(self):
- """ Fetch content from file or database"""
- try:
- self.stat()
- if self._filename:
- with closing(file_open(self._filename, 'rb', filter_ext=EXTENSIONS)) as fp:
- return fp.read().decode('utf-8')
- else:
- return self._ir_attach.raw.decode()
- except UnicodeDecodeError:
- raise AssetError('%s is not utf-8 encoded.' % self.name)
- except IOError:
- raise AssetNotFound('File %s does not exist.' % self.name)
- except:
- raise AssetError('Could not get content for %s.' % self.name)
- def minify(self):
- return self.content
- def with_header(self, content=None):
- if content is None:
- content = self.content
- return f'\n/* {self.name} */\n{content}'
- class JavascriptAsset(WebAsset):
- def __init__(self, bundle, inline=None, url=None, filename=None):
- super().__init__(bundle, inline, url, filename)
- self._is_transpiled = None
- self._converted_content = None
- @property
- def is_transpiled(self):
- if self._is_transpiled is None:
- self._is_transpiled = bool(is_odoo_module(super().content))
- return self._is_transpiled
- @property
- def content(self):
- content = super().content
- if self.is_transpiled:
- if not self._converted_content:
- self._converted_content = transpile_javascript(self.url, content)
- return self._converted_content
- return content
- def minify(self):
- return self.with_header(rjsmin(self.content))
- def _fetch_content(self):
- try:
- return super()._fetch_content()
- except AssetError as e:
- return u"console.error(%s);" % json.dumps(to_text(e))
- def to_node(self):
- if self.url:
- return ("script", dict([
- ["type", "text/javascript"],
- ["src", self.html_url],
- ['data-asset-bundle', self.bundle.name],
- ['data-asset-version', self.bundle.version],
- ]), None)
- else:
- return ("script", dict([
- ["type", "text/javascript"],
- ["charset", "utf-8"],
- ['data-asset-bundle', self.bundle.name],
- ['data-asset-version', self.bundle.version],
- ]), self.with_header())
- def with_header(self, content=None, minimal=True):
- if minimal:
- return super().with_header(content)
- # format the header like
- # /**************************
- # * Filepath: <asset_url> *
- # * Lines: 42 *
- # **************************/
- line_count = content.count('\n')
- lines = [
- f"Filepath: {self.url}",
- f"Lines: {line_count}",
- ]
- length = max(map(len, lines))
- return "\n".join([
- "",
- "/" + "*" * (length + 5),
- *(f"* {line:<{length}} *" for line in lines),
- "*" * (length + 5) + "/",
- content,
- ])
- class XMLAsset(WebAsset):
- def _fetch_content(self):
- try:
- content = super()._fetch_content()
- except AssetError as e:
- return f'<error data-asset-bundle={self.bundle.name!r} data-asset-version={self.bundle.version!r}>{json.dumps(to_text(e))}</error>'
- parser = etree.XMLParser(ns_clean=True, recover=True, remove_comments=True)
- root = etree.parse(io.BytesIO(content.encode('utf-8')), parser=parser).getroot()
- if root.tag in ('templates', 'template'):
- return ''.join(etree.tostring(el, encoding='unicode') for el in root)
- return etree.tostring(root, encoding='unicode')
- def to_node(self):
- attributes = {
- 'async': 'async',
- 'defer': 'defer',
- 'type': 'text/xml',
- 'data-src': self.html_url,
- 'data-asset-bundle': self.bundle.name,
- 'data-asset-version': self.bundle.version,
- }
- return ("script", attributes, None)
- def with_header(self, content=None):
- if content is None:
- content = self.content
- # format the header like
- # <!--=========================-->
- # <!-- Filepath: <asset_url> -->
- # <!-- Bundle: <name> -->
- # <!-- Lines: 42 -->
- # <!--=========================-->
- line_count = content.count('\n')
- lines = [
- f"Filepath: {self.url}",
- f"Lines: {line_count}",
- ]
- length = max(map(len, lines))
- return "\n".join([
- "",
- "<!-- " + "=" * length + " -->",
- *(f"<!-- {line:<{length}} -->" for line in lines),
- "<!-- " + "=" * length + " -->",
- content,
- ])
- class StylesheetAsset(WebAsset):
- rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
- rx_url = re.compile(r"""(?<!")url\s*\(\s*('|"|)(?!'|"|/|https?://|data:|#{str)""", re.U)
- rx_sourceMap = re.compile(r'(/\*# sourceMappingURL=.*)', re.U)
- rx_charset = re.compile(r'(@charset "[^"]+";)', re.U)
- def __init__(self, *args, **kw):
- self.media = kw.pop('media', None)
- self.direction = kw.pop('direction', None)
- super().__init__(*args, **kw)
- if self.direction == 'rtl' and self.url:
- self.html_url_args = self.url.rsplit('.', 1)
- self.html_url_format = '%%s/%s/%s.%%s' % ('rtl', self.bundle.name)
- self.html_url_args = tuple(self.html_url_args)
- @property
- def content(self):
- content = super().content
- if self.media:
- content = '@media %s { %s }' % (self.media, content)
- return content
- def _fetch_content(self):
- try:
- content = super()._fetch_content()
- web_dir = os.path.dirname(self.url)
- if self.rx_import:
- content = self.rx_import.sub(
- r"""@import \1%s/""" % (web_dir,),
- content,
- )
- if self.rx_url:
- content = self.rx_url.sub(
- r"url(\1%s/" % (web_dir,),
- content,
- )
- if self.rx_charset:
- # remove charset declarations, we only support utf-8
- content = self.rx_charset.sub('', content)
- return content
- except AssetError as e:
- self.bundle.css_errors.append(str(e))
- return ''
- def get_source(self):
- content = self.inline or self._fetch_content()
- return "/*! %s */\n%s" % (self.id, content)
- def minify(self):
- # remove existing sourcemaps, make no sense after re-mini
- content = self.rx_sourceMap.sub('', self.content)
- # comments
- content = re.sub(r'/\*.*?\*/', '', content, flags=re.S)
- # space
- content = re.sub(r'\s+', ' ', content)
- content = re.sub(r' *([{}]) *', r'\1', content)
- return self.with_header(content)
- def to_node(self):
- if self.url:
- attr = dict([
- ["type", "text/css"],
- ["rel", "stylesheet"],
- ["href", self.html_url],
- ["media", escape(to_text(self.media)) if self.media else None],
- ['data-asset-bundle', self.bundle.name],
- ['data-asset-version', self.bundle.version],
- ])
- return ("link", attr, None)
- else:
- attr = dict([
- ["type", "text/css"],
- ["media", escape(to_text(self.media)) if self.media else None],
- ['data-asset-bundle', self.bundle.name],
- ['data-asset-version', self.bundle.version],
- ])
- return ("style", attr, self.with_header())
- class PreprocessedCSS(StylesheetAsset):
- rx_import = None
- def __init__(self, *args, **kw):
- super().__init__(*args, **kw)
- self.html_url_args = tuple(self.url.rsplit('/', 1))
- self.html_url_format = '%%s/%s%s/%%s.css' % ('rtl/' if self.direction == 'rtl' else '', self.bundle.name)
- def get_command(self):
- raise NotImplementedError
- def compile(self, source):
- command = self.get_command()
- try:
- compiler = Popen(command, stdin=PIPE, stdout=PIPE,
- stderr=PIPE)
- except Exception:
- raise CompileError("Could not execute command %r" % command[0])
- (out, err) = compiler.communicate(input=source.encode('utf-8'))
- if compiler.returncode:
- cmd_output = misc.ustr(out) + misc.ustr(err)
- if not cmd_output:
- cmd_output = u"Process exited with return code %d\n" % compiler.returncode
- raise CompileError(cmd_output)
- return out.decode('utf8')
- class SassStylesheetAsset(PreprocessedCSS):
- rx_indent = re.compile(r'^( +|\t+)', re.M)
- indent = None
- reindent = ' '
- def minify(self):
- return self.with_header()
- def get_source(self):
- content = textwrap.dedent(self.inline or self._fetch_content())
- def fix_indent(m):
- # Indentation normalization
- ind = m.group()
- if self.indent is None:
- self.indent = ind
- if self.indent == self.reindent:
- # Don't reindent the file if identation is the final one (reindent)
- raise StopIteration()
- return ind.replace(self.indent, self.reindent)
- try:
- content = self.rx_indent.sub(fix_indent, content)
- except StopIteration:
- pass
- return "/*! %s */\n%s" % (self.id, content)
- def get_command(self):
- try:
- sass = misc.find_in_path('sass')
- except IOError:
- sass = 'sass'
- return [sass, '--stdin', '-t', 'compressed', '--unix-newlines', '--compass',
- '-r', 'bootstrap-sass']
- class ScssStylesheetAsset(PreprocessedCSS):
- @property
- def bootstrap_path(self):
- return get_resource_path('web', 'static', 'lib', 'bootstrap', 'scss')
- precision = 8
- output_style = 'expanded'
- def compile(self, source):
- if libsass is None:
- return super().compile(source)
- try:
- profiler.force_hook()
- return libsass.compile(
- string=source,
- include_paths=[
- self.bootstrap_path,
- ],
- output_style=self.output_style,
- precision=self.precision,
- )
- except libsass.CompileError as e:
- raise CompileError(e.args[0])
- def get_command(self):
- try:
- sassc = misc.find_in_path('sassc')
- except IOError:
- sassc = 'sassc'
- return [sassc, '--stdin', '--precision', str(self.precision), '--load-path', self.bootstrap_path, '-t', self.output_style]
- class LessStylesheetAsset(PreprocessedCSS):
- def get_command(self):
- try:
- if os.name == 'nt':
- lessc = misc.find_in_path('lessc.cmd')
- else:
- lessc = misc.find_in_path('lessc')
- except IOError:
- lessc = 'lessc'
- return [lessc, '-', '--no-js', '--no-color']
|