Add Rails Asset Pipeline helpers to Less #1

Closed
stevenharman opened this Issue Sep 26, 2011 · 15 comments

Projects

None yet

4 participants

@stevenharman

We need the ability to reference other assets (like images, video, etc.) from within a Less sheet. The Sass-rails folks accomplished this by adding helpers (like image-url(...), asset-url(..., ...), etc to the Sass langage. These functions delegate to the Rails path helpers to generate correct urls for various assets.

It may be a bit trickier for Less as Less is written in Javascript and less.rb is just wrapping it.

I think one possible solution would be to have the added Less helpers call back out to the wrapping Ruby code, which would need access to the Rails environment so it could then delegate on to the appropriate Rails-provided helpers.

Sorry if this is rambling - I'm between cups 1 and 2 of coffee, and haven't fully thought this through.

@metaskills
Owner

I am working on the helpers now. Will have some commits soon.

@stouset
stouset commented Oct 5, 2011

Any progress? Can we help?

@metaskills
Owner

Yes, you can. I need some advice. If you check out master, you can see I have a testing framework in place now. I also have a spec setup for the helpers.

https://github.com/metaskills/less-rails/blob/master/test/cases/helpers_spec.rb

This should eventually pass like the sass-rails gem does.

https://github.com/rails/sass-rails/blob/master/test/sass_rails_test.rb#L75

But at this time, I am unsure how to progress. The Sass gem was a bit easier to tackle, from the looks of it they just added a few custom functions to the core of Sass by extending Sass https://github.com/rails/sass-rails/blob/master/lib/sass/rails/helpers.rb This was easy for them since Sass was is written in ruby.

However, less.js is written in JavaScript. From a JS perspective, it would be easy to extend less.js to include some custom functions that ruby would integrate with using less.rb. So here is where we would add functions https://github.com/cloudhead/less.js/blob/master/lib/less/functions.js, but the question is who should be responsible for doing this? From a less-rails perspective that is using less.rb, can we extend the less library before calling the template rendering and giving us less functions like asset-path()

Perhaps @cowboyd would be of help and guidance here too?

@stouset
stouset commented Oct 6, 2011

I don't think handling asset paths client-side is going to be reasonable. If asset digests are enabled, there's no way outside of downloading and parsing the manifest.yml that the Javascript running on the client can possibly know the digest for any assets.

Handling it on the Rails side should be enough.

@stouset
stouset commented Oct 6, 2011

Actually, I may have completely misunderstood. Less is compiled through Javascript regardless of if it's client-side or server-side. So we need to inject those functions into the Javascript, from Ruby, before rendering the template. Yikes.

@stouset
stouset commented Oct 6, 2011

The easiest solution might just be to use .css.less.erb and <%= asset_path(...) %> if you need to use them. :)

@cowboyd
Collaborator
cowboyd commented Oct 6, 2011

@stouset This is, in fact, what I do as a workaround (use an ERB template). You just have to mix in your asset helpers into your Sprockets environment.

That said, therubyracer makes embedding Ruby into JS super simple. I've run a quick spike last night, and embedding helpers is definitely doable.

require 'rubygems'
require 'less'

module Less
  def self.register_helper(name, &block)
    tree = @loader.require('less/tree')
    tree.functions[name] = lambda do |node|
      tree[:Anonymous].new block.call(tree, node)
    end
  end
end

Less.register_helper('asset-url') do |tree, cxt|
  "http://app.com/assets/#{cxt.toCSS()}.png"
end

parser = Less::Parser.new
puts parser.parse('@background: pic; body {width: 10px; background-image: asset-url(@background);}').to_css

outputs

body {
  width: 10px;
  background-image: http://app.com/assets/pic.png;
}

does that seem like a reasonable interface?

@stouset
stouset commented Oct 6, 2011

Wow.

Still, the point should be made: one of the purported benefits of Less.js (which I personally haven't understood the use for) is client-side compilation. Does it makes sense to add features to Less.js that make your stylesheets dependent upon the client? At least with the ERB approach, if you render a .less.erb as .less and send it to the browser, the client can still interpret it in Javascript.

@metaskills
Owner

@stoset I think it is good point. But I do not think that less-rails should be concerned with the client side. My scope for this project is all about ruby and more specifically rails with the asset pipeline. So client side synchronization of features just seems off target.

@stouset
stouset commented Oct 6, 2011

Fair enough. Just thought it should be considered. I would at the very least mention in the README that asset helpers are only available server-side, and something like ERB templates should be used if client-side rendering is desired.

@stevenharman

Unfortunately it is not as easy as just appending .erb onto your Less files - specifically if you're @import-ing "partial" Less files into a higher-order file. For example, how twitter/boostrap.less works.

The problem is in the mechanics of how Sprockets works and those seemingly magical = require ... expressions in your application.css.

When you require in a file, Sprockets reads the contents of a file into a string and then starts pushing that string through the various template engines (Erb, Less, etc.) via Tilt. The problem here is if you're @import-ing a Less sheet in another Less sheet, the @import-ed sheet won't be read into that string by Sprockets because Sprockets doesn't know anything about the workings of Less.

The way to get around this is to = require ... ALL of your Less files that need passed through Erb, which of course will break the way Less naturally works where you would @include child sheets in higher-order parent sheets.

To quote myself: https://twitter.com/#!/stevenharman/status/114724698243350528

@stouset
stouset commented Oct 6, 2011

Sass partially handles that by modifying @import to inform Sprockets that files are being included.

If that support is added (see the issue I opened earlier), @import in the Sprockets-enabled Rails asset pipeline should load the .css.less.erb file and evaluate the ERB before passing it to Less. If you're rendering Less client-side, .less.erb files would be compiled down to .less during a rake assets:precompile and then served to the client who would then @import as usual.

@stevenharman

@stouset Ahh, fair enough.

And to be clear, I'm less interested in making it work client-side, as this is Rails-specific, after all, and I believe direction Rails is going with the pipeline, adding the hash to the file name, far future expires headers, etc., is the correct way to do it. But that's just my opinion. :)

@stouset
stouset commented Oct 6, 2011

I don't disagree. My needs are exclusively Rails-based as well. But if we can manage to make this work for client-side users too, there's no reason not to do so.

@metaskills
Owner

I am almost done with taking @cowboyd example and getting the helpers working. I took me a long time to figure out why my DummyApplication stub in the test setup was not getting the #asset_paths populated. Seems I had to require action_view/base so it would call the run_load_hooks and finalize the sprockets init. Now that is done and I am in a solid test setup, I'll knock this out this weekend.

@metaskills metaskills added a commit that closed this issue Oct 9, 2011
@metaskills Extend LESS with Rails asset pipeline helpers. Fixes #1.
* Changed gemspec for actionpack 3.1.1 since this fixes asset helpers.
* Improved Dummy::Application's initializer for testing.
4bbfac5
@metaskills metaskills closed this in 4bbfac5 Oct 9, 2011
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment