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.io.IOException; 019import java.net.URI; 020import java.net.URISyntaxException; 021import java.net.URL; 022import java.nio.charset.StandardCharsets; 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.LinkedList; 026import java.util.List; 027import java.util.Optional; 028 029import org.apache.commons.io.FilenameUtils; 030import org.apache.commons.io.IOUtils; 031import org.apache.commons.lang3.StringUtils; 032import org.apache.excalibur.source.Source; 033import org.apache.excalibur.source.SourceNotFoundException; 034import org.apache.excalibur.source.SourceResolver; 035import org.apache.excalibur.source.TraversableSource; 036 037import org.ametys.core.cocoon.source.ResourceSource; 038import org.ametys.plugins.core.ui.resources.css.CSSFileHelper; 039import org.ametys.plugins.core.ui.resources.css.JSASSResourceURIExtensionPoint; 040 041import io.bit3.jsass.importer.Import; 042import io.bit3.jsass.importer.Importer; 043 044/** 045 * Sass Importer which can resolve Ametys resources 046 */ 047public class AmetysScssImporter implements Importer 048{ 049 private SourceResolver _resolver; 050 private JSASSResourceURIExtensionPoint _jsassResourceURIExtensionPoint; 051 private String _internalContextPath; 052 private String _externalContextPath; 053 054 /** 055 * Default constructor for the Ametys SassImporter. 056 * @param internalContextPath The internal context path 057 * @param externalContextPath The external context path 058 * @param resolver The source resolver 059 * @param jsassResourceURIExtensionPoint The JSASS resourceUri extension point 060 */ 061 public AmetysScssImporter(String internalContextPath, String externalContextPath, SourceResolver resolver, JSASSResourceURIExtensionPoint jsassResourceURIExtensionPoint) 062 { 063 _internalContextPath = internalContextPath; 064 _externalContextPath = externalContextPath; 065 _resolver = resolver; 066 _jsassResourceURIExtensionPoint = jsassResourceURIExtensionPoint; 067 } 068 069 public Collection<Import> apply(String url, Import previous) 070 { 071 String previousUri = Optional.of(previous) 072 .map(Import::getAbsoluteUri) 073 .map(URI::toString) 074 .filter(uri -> !uri.endsWith(".css")) 075 .orElse(null); 076 077 if (previousUri == null || url.endsWith(".css") || url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//")) 078 { 079 // returning null keeps the original @import mention 080 return null; 081 } 082 083 List<Import> list = new LinkedList<>(); 084 085 try 086 { 087 boolean isUrlAbsolute = new URI(url).isAbsolute(); 088 String currentUri = isUrlAbsolute || url.startsWith("/") ? url : StringUtils.removeStart("/" + new URI(FilenameUtils.getFullPath(previousUri) + url).normalize(), _externalContextPath); 089 090 Source importSource = _getImportSource(currentUri, _resolver); 091 String importSourcePath = StringUtils.substringBefore(importSource.getURI(), "?"); 092 093 if (importSourcePath.endsWith(".scss") 094 || importSourcePath.endsWith(".sass") 095 || importSourcePath.endsWith(".css") && !currentUri.endsWith(".css")) // An url ending at the origin by .css should stay an import at the end (while a .css added automatically should be resolved) 096 { 097 String importText = IOUtils.toString(importSource.getInputStream(), StandardCharsets.UTF_8); 098 099 if (importSourcePath.endsWith(".css")) 100 { 101 String contextualizedUri = isUrlAbsolute ? _jsassResourceURIExtensionPoint.resolve(url) : url; 102 importText = CSSFileHelper.replaceRelativeUri(importText, contextualizedUri, _jsassResourceURIExtensionPoint, _internalContextPath, _externalContextPath); 103 currentUri = StringUtils.appendIfMissing(currentUri, ".css"); 104 } 105 106 String uri = StringUtils.removeStart(StringUtils.prependIfMissing(_jsassResourceURIExtensionPoint.resolve(currentUri), _externalContextPath), "/"); 107 list.add(new Import(uri, StringUtils.appendIfMissing(uri, "." + FilenameUtils.getExtension(importSourcePath)), importText)); 108 } 109 else 110 { 111 // returning null keeps the original @import mention 112 return null; 113 } 114 } 115 catch (URISyntaxException | IOException e) 116 { 117 throw new RuntimeException(e); 118 } 119 120 return list; 121 } 122 123 private Source _getImportSource(String currentPath, SourceResolver resolver) throws URISyntaxException, IOException 124 { 125 List<String> pathMatching = new ArrayList<>(); 126 pathMatching.add(currentPath); 127 128 // extension is optional 129 pathMatching.add(currentPath + ".scss"); 130 pathMatching.add(currentPath + ".sass"); 131 pathMatching.add(currentPath + ".css"); 132 133 // add an underscore prefix to the file name for sass partial imports 134 String name = FilenameUtils.getName(currentPath); 135 String partialPath = currentPath.substring(0, currentPath.length() - name.length()) + "_" + name; 136 pathMatching.add(partialPath); 137 pathMatching.add(partialPath + ".scss"); 138 pathMatching.add(partialPath + ".sass"); 139 pathMatching.add(partialPath + ".css"); 140 141 for (String uri : pathMatching) 142 { 143 try 144 { 145 Source importSource = resolver.resolveURI(_jsassResourceURIExtensionPoint.resolvePath(uri)); 146 if (_isSourceValid(importSource)) 147 { 148 return importSource; 149 } 150 } 151 catch (SourceNotFoundException e) 152 { 153 // source does not exists. Do nothing 154 } 155 } 156 157 throw new URISyntaxException(currentPath, "Unable to resolve SASS import, no matching source found"); 158 } 159 160 private boolean _isSourceValid(Source source) 161 { 162 if (!source.exists()) 163 { 164 return false; 165 } 166 167 // ignore folders 168 if (source instanceof TraversableSource && ((TraversableSource) source).isCollection()) 169 { 170 return false; 171 } 172 173 // Folders inside a zip entry (such as a jar file) are not traversable 174 if (source instanceof ResourceSource) 175 { 176 URL location = ((ResourceSource) source).getLocation(); 177 if (location != null && StringUtils.endsWith(location.toString(), "/")) 178 { 179 return false; 180 } 181 } 182 183 return true; 184 } 185} 186