From 2d6b54c54c5452bcd34097a7f5b6bfa81fee78ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Gir=C3=A1ldez?= Date: Fri, 30 Sep 2011 14:43:27 -0300 Subject: [PATCH 1/2] Don't hardcode URL in functional test. Added .gitignore. --- .gitignore | 5 +++++ test/functional/LesscssBundleSpec.groovy | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8f6412 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.swp +stacktrace.log +target/* +web-app/WEB-INF/tld/* + diff --git a/test/functional/LesscssBundleSpec.groovy b/test/functional/LesscssBundleSpec.groovy index 11244b4..251d608 100644 --- a/test/functional/LesscssBundleSpec.groovy +++ b/test/functional/LesscssBundleSpec.groovy @@ -10,11 +10,11 @@ class LesscssBundleSpec extends GebSpec { } def "check lesscss rules rendered"(){ when: - go('http://localhost:8080/lesscss-resources') + go('') then: $('h1').text() == 'Less Test' $('h1').jquery.css('color') == 'rgb(34, 34, 251)' $('h2').jquery.css('color') == 'rgb(132, 34, 16)' $('h3').jquery.css('color') == 'rgb(34, 251, 34)' } -} \ No newline at end of file +} From 6171a6911c506b97484694d24e8e82badf586ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Gir=C3=A1ldez?= Date: Fri, 30 Sep 2011 16:08:58 -0300 Subject: [PATCH 2/2] Add native support for .less files via monkey-patchin ResourceService and ResourceTagLib. Compile the less files from URLs, using resourceService as path resolver. --- LesscssResourcesGrailsPlugin.groovy | 15 +++++++ .../LesscssResourceMapper.groovy | 39 +++++++++++++------ test/unit/LesscssResourceMapperTests.groovy | 23 ++++++----- 3 files changed, 55 insertions(+), 22 deletions(-) diff --git a/LesscssResourcesGrailsPlugin.groovy b/LesscssResourcesGrailsPlugin.groovy index e84de14..c4c375e 100644 --- a/LesscssResourcesGrailsPlugin.groovy +++ b/LesscssResourcesGrailsPlugin.groovy @@ -1,3 +1,6 @@ +import org.grails.plugin.resource.ResourceService +import org.grails.plugin.resource.ResourceTagLib + class LesscssResourcesGrailsPlugin { // the plugin version def version = "0.6.1" @@ -21,4 +24,16 @@ This plugin supports server-side compilation of .less CSS files to their .css co // URL to the plugin's documentation def documentation = "https://github.com/paulfairless/grails-lesscss-resources" + + def doWithSpring = { -> + // monkey-patch ResourceService and ResourceTagLib to support .less files + ResourceService.DEFAULT_MODULE_SETTINGS['less'] = [ + disposition: 'head' + ] + ResourceTagLib.SUPPORTED_TYPES['less'] = [ + type: "text/css", + rel: 'stylesheet/less', + media: 'screen, projection' + ] + } } diff --git a/grails-app/resourceMappers/LesscssResourceMapper.groovy b/grails-app/resourceMappers/LesscssResourceMapper.groovy index b320f1c..749cc3d 100644 --- a/grails-app/resourceMappers/LesscssResourceMapper.groovy +++ b/grails-app/resourceMappers/LesscssResourceMapper.groovy @@ -7,16 +7,17 @@ import org.grails.plugin.resource.mapper.MapperPhase * * Mapping file to compile .less files into .css files */ -import org.codehaus.groovy.grails.plugins.support.aware.GrailsApplicationAware -import org.codehaus.groovy.grails.commons.GrailsApplication -class LesscssResourceMapper implements GrailsApplicationAware { +class LesscssResourceMapper { - GrailsApplication grailsApplication + def grailsApplication + def resourceService def phase = MapperPhase.GENERATION // need to run early so that we don't miss out on all the good stuff + def operation = "compile" static defaultExcludes = ['**/*.js','**/*.png','**/*.gif','**/*.jpg','**/*.jpeg','**/*.gz','**/*.zip'] + static defaultIncludes = ['**/*.less'] static String LESS_FILE_EXTENSION = '.less' def map(resource, config){ @@ -25,34 +26,48 @@ class LesscssResourceMapper implements GrailsApplicationAware { if (resource.sourceUrl && originalFile.name.toLowerCase().endsWith(LESS_FILE_EXTENSION)) { LessEngine engine = new LessEngine() - File input = getOriginalFileSystemFile(resource.sourceUrl); + URL input = getOriginalResourceURLForURI(resource.sourceUrl) target = new File(generateCompiledFileFromOriginal(originalFile.absolutePath)) + // save current context classloader and set the classloader from the + // Grails application, otherwise we have trouble accessing the original + // resources when reloading + def thread = Thread.currentThread() + def saveCL = thread.contextClassLoader + thread.contextClassLoader = grailsApplication.classLoader + if (log.debugEnabled) { log.debug "Compiling LESS file [${originalFile}] into [${target}]" } try { - engine.compile input, target + target.text = engine.compile(input) + // Update mapping entry // We need to reference the new css file from now on resource.processedFile = target - // Not sure if i really need these + resource.updateActualUrlFromProcessedFile() + + // Change the source extension so the compiled CSS gets into the + // .css bundle, not a separate .less bundle resource.sourceUrlExtension = 'css' - resource.actualUrl = generateCompiledFileFromOriginal(resource.originalUrl) - resource.contentType = 'text/css' + // fixup the rel attribute resource.tagAttributes.rel = 'stylesheet' + } catch (LessException e) { log.error("error compiling less file: ${originalFile}") e.printStackTrace() + } finally { + // restore saved classloader + thread.contextClassLoader = saveCL } } } private String generateCompiledFileFromOriginal(String original) { - original.replaceAll(/(?i)\.less/, '_less.css') + original.replaceAll(/(?i)\.less/, '_less.css') } - private File getOriginalFileSystemFile(String sourcePath) { - grailsApplication.parentContext.getResource(sourcePath).file + private URL getOriginalResourceURLForURI(String sourceUri) { + resourceService.getOriginalResourceURLForURI(sourceUri); } } diff --git a/test/unit/LesscssResourceMapperTests.groovy b/test/unit/LesscssResourceMapperTests.groovy index ad4b074..3966867 100644 --- a/test/unit/LesscssResourceMapperTests.groovy +++ b/test/unit/LesscssResourceMapperTests.groovy @@ -16,33 +16,35 @@ class LesscssResourceMapperTests extends GroovyTestCase{ void setUp() { mapper = new LesscssResourceMapper() mapper.metaClass.log = [debug:{}, error:{}] + mapper.grailsApplication = [ classLoader: Thread.currentThread().contextClassLoader ] } @Test void testMapperGeneratesCssFromLessResource() { String fileName = "file.less" def targetFile = mock(File, constructor('/var/file/file_less.css')) + targetFile.setText(match { true }).once() - def originalFile = mock(File) + def originalFile = mock(URL) def processedFile = mock(File) processedFile.getName().returns(fileName).stub() processedFile.getAbsolutePath().returns('/var/file/'+fileName).stub() def lessEngine = mock(LessEngine, constructor()) - lessEngine.compile(originalFile, targetFile).once() + lessEngine.compile(originalFile).once() - def resource = [processedFile:processedFile, sourceUrl:fileName, actualUrl:'', sourceUrlExtension:'less', contentType:'', originalUrl:'file.less', tagAttributes:[rel:'stylesheet/less']] + def resource = [processedFile:processedFile, sourceUrl:fileName, actualUrl:'', sourceUrlExtension:'less', contentType:'', originalUrl:'file.less', tagAttributes:[rel:'stylesheet/less'], updateActualUrlFromProcessedFile: { actualUrl = 'file_less.css' }] + resource.updateActualUrlFromProcessedFile.delegate = resource def config = [:] def mockedMapper = mock(mapper) - mockedMapper.getOriginalFileSystemFile(fileName).returns(originalFile) + mockedMapper.getOriginalResourceURLForURI(fileName).returns(originalFile) play { mockedMapper.map (resource, config) assertEquals 'file_less.css', resource.actualUrl assertEquals 'css', resource.sourceUrlExtension assertEquals 'stylesheet', resource.tagAttributes.rel - assertEquals 'text/css', resource.contentType } } @@ -50,26 +52,27 @@ class LesscssResourceMapperTests extends GroovyTestCase{ void testMapperHandlesUpperCaseFileExtension() { String fileName = "file.LESS" def targetFile = mock(File, constructor('/var/file/file_less.css')) + targetFile.setText(match { true }).once() - def originalFile = mock(File) + def originalFile = mock(URL) def processedFile = mock(File) processedFile.getName().returns(fileName).stub() processedFile.getAbsolutePath().returns('/var/file/'+fileName).stub() def lessEngine = mock(LessEngine, constructor()) - lessEngine.compile(originalFile, targetFile).once() + lessEngine.compile(originalFile).once() def mockedMapper = mock(mapper) - mockedMapper.getOriginalFileSystemFile(fileName).returns(originalFile) + mockedMapper.getOriginalResourceURLForURI(fileName).returns(originalFile) - def resource = [processedFile:processedFile, sourceUrl:fileName, actualUrl:'', sourceUrlExtension:'LESS', contentType:'', originalUrl:'file.LESS', tagAttributes:[rel:'stylesheet/less']] + def resource = [processedFile:processedFile, sourceUrl:fileName, actualUrl:'', sourceUrlExtension:'LESS', contentType:'', originalUrl:'file.LESS', tagAttributes:[rel:'stylesheet/less'], updateActualUrlFromProcessedFile: { actualUrl = 'file_less.css' }] + resource.updateActualUrlFromProcessedFile.delegate = resource def config = [:] play { mockedMapper.map (resource, config) assertEquals 'file_less.css', resource.actualUrl assertEquals 'css', resource.sourceUrlExtension assertEquals 'stylesheet', resource.tagAttributes.rel - assertEquals 'text/css', resource.contentType } }