001/* 002 * Copyright 2018 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.core.minimize.css.sass; 017 018import java.net.URI; 019import java.net.URISyntaxException; 020import java.util.List; 021import java.util.stream.Collectors; 022 023import org.apache.avalon.framework.activity.Initializable; 024import org.apache.avalon.framework.component.Component; 025import org.apache.avalon.framework.context.Context; 026import org.apache.avalon.framework.context.ContextException; 027import org.apache.avalon.framework.context.Contextualizable; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.avalon.framework.service.Serviceable; 031import org.apache.cocoon.components.ContextHelper; 032import org.apache.commons.io.FilenameUtils; 033import org.apache.commons.lang3.StringUtils; 034import org.apache.excalibur.source.SourceResolver; 035 036import org.ametys.core.minimize.SourceMapCache; 037import org.ametys.core.minimize.css.JSASSFixHelper; 038import org.ametys.core.resources.ProxiedContextPathProvider; 039import org.ametys.plugins.core.ui.resources.css.CSSFileHelper; 040import org.ametys.plugins.core.ui.resources.css.JSASSResourceURIExtensionPoint; 041import org.ametys.plugins.core.ui.resources.css.sass.SassFunctionsProvider; 042import org.ametys.plugins.core.ui.resources.css.sass.SassFunctionsProviderExtensionPoint; 043import org.ametys.runtime.plugin.component.AbstractLogEnabled; 044 045import io.bit3.jsass.CompilationException; 046import io.bit3.jsass.Compiler; 047import io.bit3.jsass.Options; 048import io.bit3.jsass.Output; 049import io.bit3.jsass.OutputStyle; 050 051/** 052 * Minimize manager for CSS files 053 */ 054public class MinimizeSassManager extends AbstractLogEnabled implements Component, Serviceable, Initializable, Contextualizable 055{ 056 /** The avalon ROLE */ 057 public static final String ROLE = MinimizeSassManager.class.getName(); 058 private Compiler _jsassCompiler; 059 060 private JSASSResourceURIExtensionPoint _jsassResourceURIExtensionPoint; 061 062 private SourceResolver _resolver; 063 064 private List<SassFunctionsProvider> _sassFunctionsProviders; 065 066 private SourceMapCache _sourceMapCache; 067 068 private ProxiedContextPathProvider _proxiedContextPathProvider; 069 070 private Context _context; 071 072 public void initialize() throws Exception 073 { 074 _jsassCompiler = new Compiler(); 075 } 076 077 public void contextualize(Context context) throws ContextException 078 { 079 _context = context; 080 } 081 082 @Override 083 public void service(ServiceManager smanager) throws ServiceException 084 { 085 _jsassResourceURIExtensionPoint = (JSASSResourceURIExtensionPoint) smanager.lookup(JSASSResourceURIExtensionPoint.ROLE); 086 _resolver = (SourceResolver) smanager.lookup(SourceResolver.ROLE); 087 SassFunctionsProviderExtensionPoint sassFunctionsProviderEP = (SassFunctionsProviderExtensionPoint) smanager.lookup(SassFunctionsProviderExtensionPoint.ROLE); 088 _sassFunctionsProviders = sassFunctionsProviderEP.getExtensionsIds().stream().map(id -> sassFunctionsProviderEP.getExtension(id)).collect(Collectors.toList()); 089 _sourceMapCache = (SourceMapCache) smanager.lookup(SourceMapCache.ROLE); 090 _proxiedContextPathProvider = (ProxiedContextPathProvider) smanager.lookup(ProxiedContextPathProvider.ROLE); 091 } 092 093 094 /** 095 * Compile a sass file. 096 * The source map of the file is generated and stored in the source map cache 097 * @param content The content of the sass file 098 * @param sassLocation The location of the sass file, e.g. plugin:core-ui:/resources/example/test.scss 099 * @param location The location of the compiled css, e.g. plugin:core-ui:/resources/example/test.css or plugin:core-ui:/resources/example/test.min.css 100 * @param minimize Minimize the output 101 * @param lastModified The date of last modification 102 * @return The content of the compiled, and optionally minimized, sass file 103 * @throws URISyntaxException If an exception occurred 104 * @throws CompilationException If an exception occurred 105 */ 106 public String compileCss(String content, String sassLocation, String location, boolean minimize, long lastModified) throws URISyntaxException, CompilationException 107 { 108 String externalContextPath = _proxiedContextPathProvider.getContextPath(); 109 110 String contextualizedUri = null; 111 try 112 { 113 // example: /plugins/core-ui/resources/example/test.scss 114 contextualizedUri = _jsassResourceURIExtensionPoint.resolve(sassLocation); 115 } 116 catch (URISyntaxException e) 117 { 118 getLogger().error("Unable to resolve URI location because URI syntax '" + location + "' is not supported", e); 119 } 120 121 if (contextualizedUri != null && contextualizedUri.contains("://")) 122 { 123 throw new URISyntaxException(contextualizedUri, "Unable to compile Sass file, unsupported URI starts with unknown protocol"); 124 } 125 126 // example: /EXTERNAL_CONTEXT/plugins/core-ui/resources/example/test.scss 127 URI uri = contextualizedUri != null ? new URI(externalContextPath + contextualizedUri) : null; 128 129 // example: test.css or test.min.css 130 String locationFilename = FilenameUtils.getName(location); 131 Output compiledString = _compileCss(content, locationFilename, uri, minimize, FilenameUtils.getExtension(sassLocation)); 132 String compileResult = compiledString.getCss(); 133 134 if (uri != null) 135 { 136 String result = CSSFileHelper.replaceRelativeResourcesUri(compileResult, contextualizedUri, _jsassResourceURIExtensionPoint, externalContextPath); 137 if (!result.equals(compileResult)) 138 { 139 getLogger().warn("Relative path found in '{}'. Relative paths inside SASS files need to be avoided, as it will offset the source mapping. Please use functions ('pluginUrl()' or similar helpers) instead.", uri.toString()); 140 } 141 142 String sourceMap = compiledString.getSourceMap(); 143 // example: /EXTERNAL_CONTEXT/plugins/core-ui/resources/example/test.scss.map or /EXTERNAL_CONTEXT/plugins/core-ui/resources/example/test.css.map 144 String sourceMapName = !minimize ? uri.toString() + ".map" : StringUtils.removeEnd(uri.toString(), ".scss") + ".css.map"; 145 146 sourceMap = JSASSFixHelper.fixJsassSourceMap(sourceMap, externalContextPath); 147 148 // example: /plugins/core-ui/resources/example/test.scss.map or /plugins/core-ui/resources/example/test.css.map 149 String cacheKey = StringUtils.removeStart(sourceMapName, externalContextPath); 150 _sourceMapCache.put(cacheKey, sourceMap, lastModified); 151 152 result += "/*# sourceMappingURL=" + FilenameUtils.getName(sourceMapName) + " */\n"; 153 return result; 154 } 155 156 return compileResult; 157 } 158 159 160 /** 161 * Generate the source map of compiled sass file. 162 * The source map of the file is generated and stored in the source map cache 163 * @param content The content of the file 164 * @param location The location of the source map file, e.g. plugin:core-ui:/resources/example/test.scss.map or plugin:core-ui:/resources/example/test.css.map 165 * @param extension The extension of the sass file, e.g. scss or sass 166 * @param internalContextPath The internal context path 167 * @param externalContextPath The external context path 168 * @param minimize Minimize the output 169 * @param lastModified The date of last modification 170 * @return The content of the source map, or null if no source map was generated 171 * @throws URISyntaxException If an exception occurred 172 * @throws CompilationException If an exception occurred 173 */ 174 public String generateCssSourceMap(String content, String location, String extension, String internalContextPath, String externalContextPath, boolean minimize, long lastModified) throws URISyntaxException, CompilationException 175 { 176 String contextualizedUri = null; 177 try 178 { 179 // example: /plugins/core-ui/resources/example/test.scss or /plugins/core-ui/resources/example/test.css 180 contextualizedUri = _jsassResourceURIExtensionPoint.resolve(StringUtils.removeEnd(location, ".map")); 181 } 182 catch (URISyntaxException e) 183 { 184 getLogger().error("Unable to resolve URI location because URI syntax '" + location + "' is not supported", e); 185 return null; // No source map generated 186 } 187 188 if (contextualizedUri.contains("://")) 189 { 190 throw new URISyntaxException(contextualizedUri, "Unable to compile Sass file, unsupported URI '" + location + "' contains unknown protocol"); 191 } 192 193 // example: /EXTERNAL_CONTEXT/plugins/core-ui/resources/example/test.scss 194 URI sassUri = new URI(externalContextPath + (!minimize ? contextualizedUri : StringUtils.removeEnd(contextualizedUri, ".css") + "." + extension)); 195 196 // example: test.css 197 String locationFilename = FilenameUtils.getName(StringUtils.removeEnd(sassUri.toString(), "." + extension) + (!minimize ? ".css" : ".min.css")); 198 Output compiledString = _compileCss(content, locationFilename, sassUri, minimize, extension); 199 200 String sourceMap = compiledString.getSourceMap(); 201 // example: /EXTERNAL_CONTEXT/plugins/core-ui/resources/example/test.scss or /EXTERNAL_CONTEXT/plugins/core-ui/resources/example/test.css 202 URI uri = new URI(externalContextPath + contextualizedUri); 203 // example: /EXTERNAL_CONTEXT/plugins/core-ui/resources/example/test.scss.map or /EXTERNAL_CONTEXT/plugins/core-ui/resources/example/test.css.map 204 String sourceMapName = !minimize ? uri.toString() + ".map" : StringUtils.removeEnd(uri.toString(), "." + extension) + ".css.map"; 205 206 sourceMap = JSASSFixHelper.fixJsassSourceMap(sourceMap, externalContextPath); 207 208 // example: /plugins/core-ui/resources/example/test.scss.map or /plugins/core-ui/resources/example/test.css.map 209 String cacheKey = StringUtils.removeStart(sourceMapName, externalContextPath); 210 _sourceMapCache.put(cacheKey, sourceMap, lastModified); 211 212 return sourceMap; 213 } 214 215 /** 216 * Compile a SCSS and output the JSASS result containing the content and source map 217 * @param content The content of the file 218 * @param fileName The name of the compiled sass file, e.g. test.css or test.min.css 219 * @param uri The contextualized uri of the sass file, e.g. /EXTERNAL_CONTEXT/plugins/core-ui/resources/example/test.scss 220 * @param minimize Minimize the output 221 * @param extension The sass extension such as scss or sass 222 * @return The output of the compilation 223 * @throws URISyntaxException if an error occurred with the uri syntax 224 * @throws CompilationException if an error occured when compiling the sass 225 */ 226 private Output _compileCss(String content, String fileName, URI uri, boolean minimize, String extension) throws URISyntaxException, CompilationException 227 { 228 String externalContextPath = _proxiedContextPathProvider.getContextPath(); 229 230 Options options = new Options(); 231 AmetysScssImporter importer = new AmetysScssImporter(ContextHelper.getRequest(_context).getContextPath(), externalContextPath, _resolver, _jsassResourceURIExtensionPoint); 232 options.getImporters().add(importer); 233 options.getFunctionProviders().addAll(_sassFunctionsProviders); 234 235 if (minimize) 236 { 237 options.setOutputStyle(OutputStyle.COMPRESSED); 238 } 239 240 URI inputPath; 241 URI outputPath; 242 if (fileName != null && uri != null) 243 { 244 options.setSourceMapFile(new URI("unused")); // Parameter required to generate the source map, but value is unused 245 options.setOmitSourceMapUrl(true); 246 247 // Transform the absolute path in relative path "/contextPath/plugins" becomes "./contextPath/plugins" to prevent jsass messing with it 248 // The relative path is transformed back into absolute in post-processing of the source map 249 inputPath = new URI("." + uri.toString()); 250 outputPath = new URI(fileName); 251 } 252 else 253 { 254 // Compile the sass file as best as possible, due to an unknown location. No source map will be generated 255 options.setSourceMapFile(null); 256 inputPath = new URI("unknown-location"); 257 outputPath = new URI("unknown-location"); 258 } 259 260 return _jsassCompiler.compileString(content, inputPath, outputPath, options); 261 } 262 263}