pax_global_header00006660000000000000000000000064134512520760014517gustar00rootroot0000000000000052 comment=7a50eae567260a23d3bbf4d5aaf1a76db43dec32 ruby-sass-3.7.4/000077500000000000000000000000001345125207600134625ustar00rootroot00000000000000ruby-sass-3.7.4/.gitignore000066400000000000000000000001361345125207600154520ustar00rootroot00000000000000/.yardoc /coverage /doc /pkg /test/rails /.sass-cache /.haml /.sass /site *.rbc /.sass *.lock ruby-sass-3.7.4/.mailmap000066400000000000000000000004671345125207600151120ustar00rootroot00000000000000Natalie Weizenbaum nex3 Natalie Weizenbaum Nathan Weizenbaum Natalie Weizenbaum Nathan Weizenbaum Natalie Weizenbaum Nathan Weizenbaum ruby-sass-3.7.4/.rubocop.yml000066400000000000000000000146271345125207600157460ustar00rootroot00000000000000Lint/BlockAlignment: Enabled: false Lint/EndAlignment: # Our alignment style differs significantly # and this doesn't seem to be a big deal. Enabled: false Lint/HandleExceptions: # We legitimately ignore exceptions in some cases and this is easy to catch in code review. Enabled: false Lint/Loop: # This isn't a big deal. Enabled: false Metrics/BlockNesting: # We'll catch this in code review. There are some legitimate uses. Enabled: false Metrics/ClassLength: # It's not worth bending over backwards to avoid long classes. Enabled: false Metrics/CyclomaticComplexity: # It's difficult to reason about what will reduce cyclomatic complexity. Enabled: false Metrics/LineLength: Enabled: true Max: 110 Metrics/MethodLength: # TODO: This max should actually be 25 but that will require significant refactoring. Max: 50 CountComments: false Style/AccessorMethodName: # We have good reasons for choosing our method names. Enabled: false Style/AlignParameters: # Natalie doesn't like to align parameters to the method call. Enabled: false Style/AsciiComments: # Why even have this restriction? Enabled: false Style/CharacterLiteral: # Character literals are pretty useful when working with text like we do. Enabled: false Style/ClassVars: # Class variables are useful for managing deprecation, and class instance # variables would be much more cumbersome. Enabled: false Style/CollectionMethods: PreferredMethods: collect: 'map' reduce: 'inject' detect: 'find' find_all: 'select' Style/DeprecatedHashMethods: # has_xxx? reads better. Enabled: false Style/Documentation: # TODO: We need to add a bunch of docs before we can enable this. Enabled: false Style/DotPosition: # This check doesn't want chained method invocation to end with a dot. # But we like to do that. Enabled: false Style/EmptyLineBetweenDefs: AllowAdjacentOneLineDefs: true Style/Encoding: # We use utf-8 comments when utf-8 characters are in use. Enabled: false Style/FormatString: # We like the string % operator. Enabled: false Style/HashSyntax: # We are a 1.8 compatable project still. Enabled: false Style/IfUnlessModifier: # We don't feel strongly about this. Enabled: false Style/IndentationConsistency: # We use indentation to convey meaning more often than we screw it up. Enabled: false Style/IndentationWidth: Enabled: false Style/Lambda: # We are a 1.8 compatible project still. Enabled: false Style/ModuleFunction: # The module_function declaration makes methods private so it means you can't use the module as a module. Enabled: false Style/ParenthesesAroundCondition: AllowSafeAssignment: true Style/PerlBackrefs: # We like perl backrefs. Enabled: false Style/PredicateName: # We have good reasons for choosing our method names. Enabled: false Style/RaiseArgs: # We prefer "raise Exception.new(msg)". EnforcedStyle: compact Style/RedundantReturn: # We allow explicit returns for multiple return values. AllowMultipleReturnValues: true Style/Semicolon: # Makes a good line separator Enabled: false Style/SignalException: # We like saying raise. Enabled: false Style/SingleLineBlockParams: # We have good reasons for choosing our argument names. Enabled: false Style/SingleLineMethods: # We like single line methods for simple methods. Enabled: false Style/SpaceBeforeBlockBraces: # We prefer "foo {|arg| body}". EnforcedStyle: space Style/SpaceInsideBlockBraces: EnforcedStyle: no_space SpaceBeforeBlockParameters: false Style/SpaceInsideHashLiteralBraces: EnforcedStyle: no_space Style/StringLiterals: # They say to not use double quoted strings unless there is interpolation. # While this gives a marginal parse time speedup, it makes working with # strings annoying. Enabled: false Style/TrailingComma: Enabled: false Style/TrivialAccessors: AllowPredicates: true ExactNameMatch: true Style/WhenThen: # We like semi-colons. Enabled: false Style/WhileUntilModifier: # We don't feel strongly about this. Enabled: false Lint/AssignmentInCondition: Enabled: true Lint/FormatParameterMismatch: Enabled: true Lint/NestedMethodDefinition: Enabled: true Lint/NonLocalExitFromIterator: # We do this a number of times and it cleans up the code. Enabled: false Lint/StringConversionInInterpolation: Enabled: true Lint/UselessAccessModifier: Enabled: true Lint/UnneededDisable: Enabled: true Lint/UnusedBlockArgument: Enabled: true Lint/UnusedMethodArgument: # Lots of polymorphic methods take arguments # they don't use. Enabled: false Metrics/AbcSize: # TODO: It's probably good to address this complexity measurement # but it's a lot of work so this remains disabled. Enabled: false Metrics/ModuleLength: Enabled: true Metrics/PerceivedComplexity: # TODO: It's probably good to address this complexity measurement # but it's a lot of work so this remains disabled. Enabled: false Performance/FlatMap: # This requires a newer version of ruby than we require. Enabled: false Performance/ReverseEach: Enabled: true Performance/StringReplacement: Enabled: true Style/BarePercentLiterals: Enabled: true Style/ClassAndModuleChildren: # We hates it. Enabled: false Style/ClassCheck: Enabled: true Style/ClosingParenthesisIndentation: Enabled: true Style/DoubleNegation: # We don't dislike double negation. Enabled: false Style/EachWithObject: # This requires a newer version of ruby than we require. Enabled: false Style/EmptyLinesAroundBlockBody: Enabled: true Style/ExtraSpacing: Enabled: true Style/FirstParameterIndentation: Enabled: true Style/GuardClause: Enabled: true Style/IndentHash: Enabled: true Style/LineEndConcatenation: Enabled: false Style/Next: Enabled: true Style/MultilineOperationIndentation: Enabled: false Style/MultilineTernaryOperator: Enabled: true Style/ParallelAssignment: Enabled: false Style/PercentLiteralDelimiters: Enabled: true Style/RegexpLiteral: Enabled: true Style/SelfAssignment: Enabled: true Style/SingleSpaceBeforeFirstArg: Enabled: true Style/SpaceAroundOperators: Enabled: true Style/SpaceInsideRangeLiteral: Enabled: true Style/SpaceInsideStringInterpolation: Enabled: true Style/StringLiteralsInInterpolation: Enabled: true Style/StructInheritance: Enabled: false Style/SymbolProc: # We need to enable this for Sass 3.5+ # Since we will have dropped support for older rubies. Enabled: false Style/TrailingUnderscoreVariable: Enabled: false ruby-sass-3.7.4/.travis.yml000066400000000000000000000013651345125207600156000ustar00rootroot00000000000000sudo: false language: ruby cache: bundler install: # If we're running for a pull request, check out the revision of sass-spec # referenced by that pull request. - | if [ ! -z "$TRAVIS_PULL_REQUEST" -a "$TRAVIS_PULL_REQUEST" != false ]; then ref=$(extra/sass-spec-ref.sh) mkdir sass-spec git -C sass-spec init git -C sass-spec pull --depth=1 git://github.com/sass/sass-spec "$ref" bundle config local.sass-spec "$(pwd)/sass-spec" fi - bundle install --jobs=3 --retry=3 --path=${BUNDLE_PATH:-vendor/bundle} - bundle update sass-spec rvm: - 2.3 - 2.4 - 2.5 gemfile: Gemfile branches: only: - master - stable - stable_3_2 - next notifications: irc: {channels: "irc.freenode.org#sass"} ruby-sass-3.7.4/.yardopts000066400000000000000000000005761345125207600153400ustar00rootroot00000000000000--readme README.md --markup markdown --markup-provider redcarpet --default-return "" --title "Sass Documentation" --query 'object.type != :classvariable' --query 'object.type != :constant || @api && @api.text == "public"' --hide-void-return --protected --no-private --no-highlight --tag comment --hide-tag comment ruby-sass-3.7.4/CODE_OF_CONDUCT.md000066400000000000000000000010311345125207600162540ustar00rootroot00000000000000Sass is more than a technology; Sass is driven by the community of individuals that power its development and use every day. As a community, we want to embrace the very differences that have made our collaboration so powerful, and work together to provide the best environment for learning, growing, and sharing of ideas. It is imperative that we keep Sass a fun, welcoming, challenging, and fair place to play. [The full community guidelines can be found on the Sass website.][link] [link]: https://sass-lang.com/community-guidelines ruby-sass-3.7.4/CONTRIBUTING.md000066400000000000000000000162211345125207600157150ustar00rootroot00000000000000Contributions are welcomed. Please see the following site for guidelines: [https://sass-lang.com/community#Contribute](https://sass-lang.com/community#Contribute) * [Branches](#main-development-branches) * [Feature Branches](#feature-branches) * [Experimental Branches](#experimental-branches) * [Old Stable Branches](#old-stable-branches) * [Versioning](#versioning) * [Making Breaking Changes](#making-breaking-changes) * [Exceptional Breakages](#exceptional-breakages) ## Branches The Sass repository has three primary development branches, each of which tracks a different line of releases (see [versioning](#versioning) below). Each branch is regularly merged into the one below: `stable` into `next`, `next` into `master`. * The `stable` branch is the default—it's what GitHub shows if you go to [sass/ruby-sass](https://github.com/sass/ruby-sass), and it's the default place for pull requests to go. This branch is where we work on the next patch release. Bug fixes and documentation improvements belong here, but not new features. * The `next` branch is where we work on the next minor release. It's where most new features go, as long as they're not breaking changes. Very occasionally breaking changes will go here as well—see [exceptional breakages](#exceptional-breakages) below for details. * The `master` branch is where we work on the next major release. It's where breaking changes go. We also occasionally decide that a non-breaking feature is big enough to warrant saving until the next major release, in which case it will also be developed here. Ideally, pull requests would be made against the appropriate branch, but don't worry about it too much; if you make a request against the wrong branch, the maintainer will take responsibility for rebasing it before merging. ### Testing Tests for changes to the Sass language go in [sass-spec](https://github.com/sass/sass-spec) so that other implementations (E.g. libSass) can be tested against the same test suite. The sass-spec repo follows a "trunk development" model in that the tests there test against different version of the Sass language (as opposed to having branches that track different Sass versions). When contributing changes to Sass, update the Gemfile to use sass-spec from a branch or fork that has the new tests. When the feature lands in Sass, the committer will also merge the corresponding sass-spec changes. The [documentation of sass-spec](https://github.com/sass/sass-spec/blob/master/README.md) explains how to run sass-spec and contribute changes. In development, Change the Gemfile(s) to use the `:path` option against the sass-spec gem to link your local checkout of sass and sass-spec together in one or both directions. Changes to Sass internals or Ruby Sass specific features (E.g. the `sass-convert` tool) should always have tests in the Sass `test` directory following the conventions you see there. ### Feature Branches Sometimes it won't be possible to merge a new feature into `next` or `master` immediately. It may require longer-term work before it's complete, or we may not want to release it as part of any alpha releases of the branch in question. Branches like this are labeled `feature.#{name}` and stay on GitHub until they're ready to be merged. ### Experimental Branches Not all features pan out, and not all code is a good fit for merging into the main codebase. Usually when this happens the code is just discarded, but every so often it's interesting or promising enough that it's worth keeping around. This is what experimental branches (labeled `experimental.#{name}`) are for. While they're not currently in use, they contain code that might be useful in the future. ### Old Stable Branches Usually Sass doesn't have the development time to do long-term maintenance of old release. But occasionally, very rarely, it becomes necessary. In cases like that, a branch named `stable_#{version}` will be created, starting from the last tag in that version series. ## Versioning Starting with version 3.5.0, Sass uses [semantic versioning](http://semver.org/) to indicate the evolution of its language semantics as much as possible. This means that patch releases (such as 3.5.3) contain only bug fixes, minor releases (such as 3.6.0) contain backwards-compatible features, and only major releases (such as 4.0.0) are allowed to have backwards-incompatible behavior. There are [exceptions](#exceptional-breakages), but we try to follow this rule as closely as possible. Note, however, that the semantic versioning applies only to the language's semantics, not to the Ruby APIs. Although we try hard to keep widely-used APIs like [`Sass::Engine`][Sass::Engine] stable, we don't have a strong distinction between public and private APIs and we need to be able to freely refactor our code. [Sass::Engine]: https://sass-lang.com/documentation/Sass/Engine.html ### Making Breaking Changes Sometimes the old way of doing something just isn't going to work anymore, and the new way just can't be made backwards-compatible. In that case, a breaking change is necessary. These changes are rarely pleasant, but they contribute to making the language better in the long term. Our breaking change process tries to make such changes as clear to users and as easy to adapt to as possible. We want to ensure that there's a clear path forward for users using functionality that will no longer exist, and that they are able to understand what's changing and what they need to do. We've developed the following process for this: 1. Deprecate the old behavior [in `stable`](#branches). At minimum, deprecating some behavior involves printing a warning when that behavior is used explaining that it's going to go away in the future. Ideally, this message will also include code that will do the same thing in a non-deprecated way. If there's a thorough prose explanation of the change available online, the message should link to that as well. 2. If possible, make `sass-convert` (also in `stable`) convert the deprecated behavior into a non-deprecated form. This allows users to run `sass-convert -R -i` to automatically update their stylesheets. 3. Implement the new behavior in `master`. The sooner this happens, the better: it may be unclear exactly what needs to be deprecated until the new implementation exists. 4. Release an alpha version of `master` that includes the new behavior. This allows users who are dissatisfied with the workaround to use the new behavior early. Normally a maintainer will take care of this. ### Exceptional Breakages Because Sass's syntax and semantics are closely tied to those of CSS, there are occasionally times when CSS syntax is introduced that overlaps with previously-valid Sass. In this case in particular, we may introduce a breaking change in a minor version to get back to CSS compatibility as soon as possible. Exceptional breakages still require the full deprecation process; the only change is that the new behavior is implemented in `next` rather than `master`. Because there are no minor releases between the deprecation and the removal of the old behavior, the deprecation warning should be introduced soon as it becomes clear that an exceptional breakage is necessary. ruby-sass-3.7.4/Gemfile000066400000000000000000000007331345125207600147600ustar00rootroot00000000000000source "https://rubygems.org" gemspec if RUBY_VERSION =~ /^1\.8/ || RUBY_VERSION =~ /^1\.9\.[012]$/ gem 'rake', '~> 10.5.0' else gem 'rake', '~> 11.0' end gem 'minitest', '>= 5.0.0', '< 6.0.0', :group => :test # gem "sass-spec", :path => "../sass-spec" gem "sass-spec", :git => 'https://github.com/sass/sass-spec.git', :branch => 'master' # Removed from standard library in Ruby 2.5.0. if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.5.0') gem 'mathn' end ruby-sass-3.7.4/MIT-LICENSE000066400000000000000000000021171345125207600151170ustar00rootroot00000000000000Copyright (c) 2006-2016 Hampton Catlin, Natalie Weizenbaum, and Chris Eppstein Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ruby-sass-3.7.4/README.md000066400000000000000000000170111345125207600147410ustar00rootroot00000000000000## Ruby Sass Has Reached End-of-Life Ruby Sass should no longer be used, and will no longer be receiving any updates. See [the Sass blog][], and consider switching to the [`sassc` gem]. [the Sass blog]: https://sass-lang.com/blog/posts/7828841 [`sassc` gem]: https://rubygems.org/gems/sassc # Sass [![Travis Build Status](https://travis-ci.org/sass/ruby-sass.svg?branch=next)](https://travis-ci.org/sass/ruby-sass) [![Gem Version](https://badge.fury.io/rb/sass.svg)](http://badge.fury.io/rb/sass) [![Inline docs](http://inch-ci.org/github/sass/sass.svg)](http://inch-ci.org/github/sass/sass) **Sass makes CSS fun again**. Sass is an extension of CSS, adding nested rules, variables, mixins, selector inheritance, and more. It's translated to well-formatted, standard CSS using the command line tool or a web-framework plugin. Sass has two syntaxes. The new main syntax (as of Sass 3) is known as "SCSS" (for "Sassy CSS"), and is a superset of CSS's syntax. This means that every valid CSS stylesheet is valid SCSS as well. SCSS files use the extension `.scss`. The second, older syntax is known as the indented syntax (or just "Sass"). Inspired by Haml's terseness, it's intended for people who prefer conciseness over similarity to CSS. Instead of brackets and semicolons, it uses the indentation of lines to specify blocks. Although no longer the primary syntax, the indented syntax will continue to be supported. Files in the indented syntax use the extension `.sass`. ## Using Sass can be used from the command line or as part of a web framework. The first step is to install the gem: gem install sass After you convert some CSS to Sass, you can run sass style.scss to compile it back to CSS. For more information on these commands, check out sass --help To install Sass in Rails 2, just add `config.gem "sass"` to `config/environment.rb`. In Rails 3, add `gem "sass"` to your Gemfile instead. `.sass` or `.scss` files should be placed in `public/stylesheets/sass`, where they'll be automatically compiled to corresponding CSS files in `public/stylesheets` when needed (the Sass template directory is customizable... see [the Sass reference](https://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#template_location-option) for details). Sass can also be used with any Rack-enabled web framework. To do so, just add ```ruby require 'sass/plugin/rack' use Sass::Plugin::Rack ``` to `config.ru`. Then any Sass files in `public/stylesheets/sass` will be compiled into CSS files in `public/stylesheets` on every request. To use Sass programmatically, check out the [YARD documentation](https://sass-lang.com/documentation/file.SASS_REFERENCE.html#using_sass). ## Formatting Sass is an extension of CSS that adds power and elegance to the basic language. It allows you to use [variables][vars], [nested rules][nested], [mixins][mixins], [inline imports][imports], and more, all with a fully CSS-compatible syntax. Sass helps keep large stylesheets well-organized, and get small stylesheets up and running quickly, particularly with the help of [the Compass style library](http://compass-style.org). [vars]: https://sass-lang.com/documentation/file.SASS_REFERENCE.html#variables_ [nested]: https://sass-lang.com/documentation/file.SASS_REFERENCE.html#nested_rules [mixins]: https://sass-lang.com/documentation/file.SASS_REFERENCE.html#mixins [imports]: https://sass-lang.com/documentation/file.SASS_REFERENCE.html#import Sass has two syntaxes. The one presented here, known as "SCSS" (for "Sassy CSS"), is fully CSS-compatible. The other (older) syntax, known as the indented syntax or just "Sass", is whitespace-sensitive and indentation-based. For more information, see the [reference documentation][syntax]. [syntax]: https://sass-lang.com/documentation/file.SASS_REFERENCE.html#syntax To run the following examples and see the CSS they produce, put them in a file called `test.scss` and run `sass test.scss`. ### Nesting Sass avoids repetition by nesting selectors within one another. The same thing works for properties. ```scss table.hl { margin: 2em 0; td.ln { text-align: right; } } li { font: { family: serif; weight: bold; size: 1.2em; } } ``` ### Variables Use the same color all over the place? Need to do some math with height and width and text size? Sass supports variables, math operations, and many useful functions. ```scss $blue: #3bbfce; $margin: 16px; .content_navigation { border-color: $blue; color: darken($blue, 10%); } .border { padding: $margin / 2; margin: $margin / 2; border-color: $blue; } ``` ### Mixins Even more powerful than variables, mixins allow you to re-use whole chunks of CSS, properties or selectors. You can even give them arguments. ```scss @mixin table-scaffolding { th { text-align: center; font-weight: bold; } td, th { padding: 2px; } } @mixin left($dist) { float: left; margin-left: $dist; } #data { @include left(10px); @include table-scaffolding; } ``` A comprehensive list of features is available in the [Sass reference](https://sass-lang.com/documentation/file.SASS_REFERENCE.html). ## Executables The Sass gem includes several executables that are useful for dealing with Sass from the command line. ### `sass` The `sass` executable transforms a source Sass file into CSS. See `sass --help` for further information and options. ### `sass-convert` The `sass-convert` executable converts between CSS, Sass, and SCSS. When converting from CSS to Sass or SCSS, nesting is applied where appropriate. See `sass-convert --help` for further information and options. ### Running locally To run the Sass executables from a source checkout instead of from rubygems: ``` $ cd sass $ bundle $ bundle exec sass ... $ bundle exec scss ... $ bundle exec sass-convert ... ``` ## Authors Sass was envisioned by [Hampton Catlin](http://www.hamptoncatlin.com) (@hcatlin). However, Hampton doesn't even know his way around the code anymore and now occasionally consults on the language issues. Hampton lives in San Francisco, California and works as VP of Technology at [Moovweb](http://www.moovweb.com/). [Natalie Weizenbaum](https://twitter.com/nex3) is the primary developer and architect of Sass. Her hard work has kept the project alive by endlessly answering forum posts, fixing bugs, refactoring, finding speed improvements, writing documentation, implementing new features, and designing the language. Natalie lives in Seattle, Washington and works on [Dart](http://dartlang.org) application libraries at Google. [Chris Eppstein](http://twitter.com/chriseppstein) is a core contributor to Sass and the creator of [Compass](http://compass-style.org/), the first Sass-based framework, and [Eyeglass](http://github.com/sass-eyeglass/eyeglass), a node-sass plugin ecosystem for NPM. Chris focuses on making Sass more powerful, easy to use, and on ways to speed its adoption through the web development community. Chris lives in San Jose, California with his wife and two children. He is an Engineer for [LinkedIn.com](http://linkedin.com), where his primary responsibility is to maintain Sass and many other Sass-related open source projects. If you use this software, we'd be truly honored if you'd make a tax-deductible donation to a non-profit organization and then [let us know on twitter](http://twitter.com/SassCSS), so that we can thank you. Here's a few that we endorse: * [Trans Justice Funding Project](http://www.transjusticefundingproject.org/) * [United Mitochondrial Disease Foundation](http://umdf.org/compass) * [Girl Develop It](https://www.girldevelopit.com/donate) Sass is licensed under the MIT License. ruby-sass-3.7.4/Rakefile000066400000000000000000000235551345125207600151410ustar00rootroot00000000000000require 'rubygems/package' # ----- Utility Functions ----- def scope(path) File.join(File.dirname(__FILE__), path) end # ----- Default: Testing ------ task :default => :test require 'rake/testtask' LINE_SIZE = 80 DECORATION_CHAR = '#' def print_header(string) length = string.length puts DECORATION_CHAR * LINE_SIZE puts string.center(length + 2, ' ').center(LINE_SIZE, DECORATION_CHAR) puts DECORATION_CHAR * LINE_SIZE end desc "Run all tests" task :test do test_cases = [ { 'env' => {'MATHN' => 'true'}, 'tasks' => ['test:ruby', 'test:spec'] }, { 'env' => {'MATHN' => 'false'}, 'tasks' => ['test:ruby'] } ] test_cases.each do |test_case| env = test_case['env'] tasks = test_case['tasks'] env.each do |key, value| ENV[key] = value end tasks.each do |task| print_header("Running task: #{task}, env: #{env}") Rake::Task[task].execute end end end namespace :test do desc "Run the ruby tests (without sass-spec)" Rake::TestTask.new("ruby") do |t| t.libs << 'test' test_files = FileList[scope('test/**/*_test.rb')] test_files.exclude(scope('test/rails/*')) test_files.exclude(scope('test/plugins/*')) t.test_files = test_files t.warning = true t.verbose = true end desc "Run sass-spec tests against the local code." task :spec do old_load_path = $:.dup begin $:.unshift(File.join(File.dirname(__FILE__), "lib")) begin require 'sass_spec' rescue LoadError puts "You probably forgot to run: bundle exec rake" raise end SassSpec::Runner.new( language_version: get_version[/^\d+\.\d+/], spec_directory: SassSpec::SPEC_DIR, engine_adapter: SassEngineAdapter.new, generate: false, tap: false, skip: false, verbose: false, filter: "", limit: -1, unexpected_pass: false, nuke: false, ).run || exit(1) ensure $:.replace(old_load_path) end end end # ----- Code Style Enforcement ----- def ruby_version_at_least?(version_string) ruby_version = Gem::Version.new(RUBY_VERSION.dup) version = Gem::Version.new(version_string) ruby_version >= version end # ----- Packaging ----- # Don't use Rake::GemPackageTast because we want prerequisites to run # before we load the gemspec. desc "Build all the packages." task :package => [:revision_file, :date_file, :permissions] do version = get_version File.open(scope('VERSION'), 'w') {|f| f.puts(version)} load scope('sass.gemspec') Gem::Package.build(SASS_GEMSPEC) sh %{git checkout VERSION} pkg = "#{SASS_GEMSPEC.name}-#{SASS_GEMSPEC.version}" mkdir_p "pkg" verbose(true) {mv "#{pkg}.gem", "pkg/#{pkg}.gem"} sh %{rm -f pkg/#{pkg}.tar.gz} verbose(false) {SASS_GEMSPEC.files.each {|f| sh %{tar rf pkg/#{pkg}.tar #{f}}}} sh %{gzip pkg/#{pkg}.tar} end task :permissions do sh %{chmod -R a+rx bin} sh %{chmod -R a+r .} require 'shellwords' Dir.glob('test/**/*_test.rb') do |file| next if file =~ %r{^test/haml/spec/} sh %{chmod a+rx #{file}} end end task :revision_file do require scope('lib/sass') release = Rake.application.top_level_tasks.include?('release') || File.exist?(scope('EDGE_GEM_VERSION')) if Sass.version[:rev] && !release File.open(scope('REVISION'), 'w') { |f| f.puts Sass.version[:rev] } elsif release File.open(scope('REVISION'), 'w') { |f| f.puts "(release)" } else File.open(scope('REVISION'), 'w') { |f| f.puts "(unknown)" } end end task :date_file do File.open(scope('VERSION_DATE'), 'w') do |f| f.puts Time.now.utc.strftime('%d %B %Y %T %Z') end end # We also need to get rid of this file after packaging. at_exit do File.delete(scope('REVISION')) rescue nil File.delete(scope('VERSION_DATE')) rescue nil end desc "Install Sass as a gem. Use SUDO=1 to install with sudo." task :install => [:package] do gem = RUBY_PLATFORM =~ /java/ ? 'jgem' : 'gem' sh %{#{'sudo ' if ENV["SUDO"]}#{gem} install --no-ri pkg/sass-#{get_version}} end desc "Release a new Sass package to RubyGems.org." task :release => [:check_release, :package] do version = File.read(scope("VERSION")).strip sh %{gem push pkg/sass-#{version}.gem} end # Ensures that the VERSION file has been updated for a new release. task :check_release do version = File.read(scope("VERSION")).strip raise "There have been changes since current version (#{version})" if changed_since?(version) raise "VERSION_NAME must not be 'Bleeding Edge'" if File.read(scope("VERSION_NAME")) == "Bleeding Edge" end # Reads a password from the command line. # # @param name [String] The prompt to use to read the password def read_password(prompt) require 'readline' system "stty -echo" Readline.readline("#{prompt}: ").strip ensure system "stty echo" puts end # Returns whether or not the repository, or specific files, # has/have changed since a given revision. # # @param rev [String] The revision to check against # @param files [Array] The files to check. # If this is empty, checks the entire repository def changed_since?(rev, *files) IO.popen("git diff --exit-code #{rev} #{files.join(' ')}") {} return !$?.success? end # Get the version string. If this is being installed from Git, # this includes the proper prerelease version. def get_version File.read(scope('VERSION').strip) end task :watch_for_update do sh %{ruby extra/update_watch.rb} end # ----- Documentation ----- task :rdoc do puts '=' * 100, < [:yard, 'doc:add_ids'] task :redoc => [:yard, 'doc:add_ids'] rescue LoadError desc "Generate Documentation" task :doc => :rdoc task :yard => :rdoc end # ----- Coverage ----- begin require 'rcov/rcovtask' Rcov::RcovTask.new do |t| t.test_files = FileList[scope('test/**/*_test.rb')] t.rcov_opts << '-x' << '"^\/"' if ENV['NON_NATIVE'] t.rcov_opts << "--no-rcovrt" end t.verbose = true end rescue LoadError; end # ----- Profiling ----- begin require 'ruby-prof' desc <`][declaration-value] production in their argument list. This will provide better forwards-compatibility for future CSS syntax. * Pseudo selectors that take selectors as arguments will no longer always be eliminated if they contain placeholder selectors that aren't extended. Instead, they'll be reduced to valid CSS selectors if possible. * Generated transparent colors will now be emitted as `rgba(0, 0, 0, 0)` rather than `transparent`. This works around a bug wherein IE incorrectly handles the latter format. * The indented syntax now allows different indentation to be used for different lines, as long as they define a consistent tree structure. [hex alpha spec]: https://drafts.csswg.org/css-color/#hex-notation [directional focus spec]: https://www.w3.org/TR/css-ui-3/#nav-dir ### Backwards Incompatibilities -- Must Read! * The way [CSS variables][] are handled has changed to better correspond to the CSS spec. They no longer allow arbitrary SassScript in their values; instead, almost all text in the property values will be passed through unchanged to CSS. The only exception is `#{}`, which will inject a SassScript value as before. ## 3.4.25 (7 July 2017) * Fix a bug where `*` wouldn't always be eliminated during selector unification. ### Deprecations -- Must Read! * Extending compound selectors such as `.a.b` is deprecated. This never followed the stated semantics of extend: elements that match the extending selector are styled as though they matches the extended selector. When you write `h1 {@extend .a.b}`, this *should* mean that all `h1` elements are styled as though they match `.a.b`—that is, as though they have `class="a b"`, which means they'd match both `.a` and `.b` separately. But instead we extend only selectors that contain *both* `.a` and `.b`, which is incorrect. * Color arithmetic is deprecated. Channel-by-channel arithmetic doesn't correspond closely to intuitive understandings of color. Sass's suite of [color functions][] are a much cleaner and more comprehensible way of manipulating colors dynamically. * The reference combinator, `/foo/`, is deprecated since it hasn't been in the CSS specification for some time. * The old-style `:name value` property syntax is deprecated. This syntax is not widely used, and is unnecessarily different from CSS. [color functions]: https://sass-lang.com/documentation/Sass/Script/Functions.html#other_color_functions ## 3.4.24 (18 May 2017) * Elements without a namespace (such as `div`) are no longer unified with elements with the empty namespace (such as `|div`). This unification didn't match the results returned by `is-superselector()`, and was not guaranteed to be valid. ## 3.4.23 (19 December 2016) * The Sass logger is now instantiated on a per-thread/per-fiber basis and can now be configured to output to any IO object. This can help services and processes that wrap Sass compilation reliably extract warnings in a concurrent environment. * Setting the numeric precision by assigning to `Sass::Script::Value::Number.precision` is now thread safe. To set for all threads, be sure to set the precision on the main thread. * Sass cache files will now be world and group writable if your umask allows it. [Issue #1623](https://github.com/sass/sass/issues/1623) * The `supports(...)` clause in `@import` statements now allows bare declarations as per the CSS specification. [Issue #1967](https://github.com/sass/sass/issues/1967) * Fix a bug where, under some circumstances, `str-slice()` would go to the end of the string even if `$end-at` was set. * Fix conversions between numbers with `dpi`, `dpcm`, and `dppx` units. Previously these conversions were inverted. * Support `url()`s containing quoted strings within unknown directives. ## 3.4.22 (28 March 2016) * Sass now runs without warnings when running ruby with code style warnings enabled. * Sass no longer watches the current working directory unless it is on the load path or the files being compiled are in the current working directory. This was causing performance issues for users with large numbers of files in their project directory. [Issue #1562](https://github.com/sass/sass/issues/1562), [Issue #1966](https://github.com/sass/sass/issues/1966), [Issue #2006](https://github.com/sass/sass/issues/2006). * `sass-convert` now accepts a `-q` and `--quiet` option to disable ouput while it is running. * Fixed a bug in sass-convert when recursively processing CSS files into Sass files which caused the process to crash without processing any files. [Issue #1827](https://github.com/sass/sass/issues/1827), ### Deprecation -- Must Read! * Support for Ruby 1.8.7 and 1.9.3 is deprecated. See [this blog post][Ruby deprecation] for details. * The current handling of [CSS variables][] is deprecated. In order to support the CSS spec as fully as possible, no Sass-specific constructs other than `#{}` will be supported in CSS variable values. For forwards-compatibility, any SassScript being used in CSS variables must be moved into `#{}`. [Ruby deprecation]: http://blog.sass-lang.com/posts/560719 [CSS variables]: https://www.w3.org/TR/css-variables/ ## 3.4.21 (11 January 2016) * Consistent output formatting for numbers close to an integer. [Issue #1931](https://github.com/sass/sass/issues/1931) * Correctly round negative numbers that were almost but not quite a whole number (slightly greater than the negative number). [Issue #1938](https://github.com/sass/sass/issues/1938) * Don't strip escaped semicolons from compressed output. [Issue #1932](https://github.com/sass/sass/issues/1932) * Only compress around dashes within nth selectors. [Issue #1933](https://github.com/sass/sass/issues/1933) * Selector compression of whitespace around commas was affecting attribute values. [Issue #1947](https://github.com/sass/sass/issues/1947) * Make subtraction work when a unit is followed directly by a hyphen and then a period. For example, `1em-.75em` now returns `0.25em` rather than `1em -0.75em`. This is consistent with the behavior when the subtrahend begins with a `0`. [Issue #1954](https://github.com/sass/sass/issues/1954) ## 3.4.20 (09 December 2015) * Fix a bug with the rounding changes from 3.4.14 and 3.4.15 where some negative numbers would incorrectly be rounded up instead of down. * Better compression for `:nth` pseudoselectors with subtraction. [Issue #1650](https://github.com/sass/sass/issues/1874) * Add support for the new `supports()` clause for CSS `@import` directives. * Rounding numbers now respects Sass's precision setting for numbers very close to half an integer. * Add support for the `q` unit, representing one quarter of a millimeter. * Mitigate a race condition when multiple threads are using the same `Sass::Plugin` object at once. * In compressed mode, numbers between -1 and 1 now have the leading 0 omitted. * Source maps now include source ranges for comments. ### Deprecation -- Must Read! Certain ways of using `#{}` without quotes in property and variable values have been deprecated in order to simplify the feature. Currently, `#{}` behaves unpredictably. If it's used near operators, it will cause those operators to become part of an unquoted string instead of having their normal meaning. This isn't an especially useful feature, and it makes it hard to reason about some code that includes `#{}`, so we're getting rid of it. In the new world, `#{}` just returns an unquoted string that acts like any other unquoted string. For example, `foo + #{$var}` will now do the same thing as `foo + $var`, instead of doing the same thing as `unquote("foo + #{$var}")`. In order to ease the transition, Sass will now emit deprecation warnings for all uses of `#{}` that will change behavior in 4.0. We don't anticipate many warnings to appear in practice, and you can fix most of them automatically by running `sass-convert --in-place` on the affected files. For more details, see [the blog post on the deprecation][interp-blog] and [the GitHub issue in which it was planned][interp-issue]. [interp-blog]: http://sass.logdown.com/posts/308328-cleaning-up-interpolation [interp-issue]: https://github.com/sass/sass/issues/1778 ## 3.4.19 (09 October 2015) * Sass numeric equality now better handles float-point errors. Any numbers that are within `1 / (10 ^ (precision + 1))` of each other are now considered equal. * Allow importing relative paths on standard input even when `--stdin` isn't explicitly passed. * Fix some busted edge cases with `sass-convert` and string interpolation. * Since we require ruby 1.8.7 or greater, support for ruby 1.8.6 was removed from the code. * Small performance enhancements. ## 3.4.18 (25 August 2015) * A fix in 3.4.17 to address unnecessary semi-colons in compressed mode was too aggressive and removed some that *were* necessary. This is now fixed. ## 3.4.17 (21 August 2015) * Allow passing calc values to rgb/hsl color constructors. * The source map end character for lists now correctly uses the end of the list instead of the end of the first element in the list. * Fix up some edge cases where extra semicolons could be added to unknown directives in compressed mode. * If you try to do a stupid color operation with a stupid value, the error message will now be less stupid than it was. * Make `is-superselector("a > c d", "a > b c d")` return `false`. This also fixes some related `@extend` behavior. * A `/` in a parenthesized list is consistently treated as a plain `/` unless it meets the criteria for being treated as division. * In `sass-convert`, ensure that literal `/`es are preserved. ## 3.4.16 (10 July 2015) * When converting from Sass to SCSS or vice versa, double-space around nested rules the same as around top-level rules. * Compatibility with listen 3. * Parse a minus operator with no surrounding whitespace (e.g. 20px-10px) the same regardless of whether it's being used in a static property. ## 3.4.15 (22 June 2015) * Further improve rounding for various numeric operations. * Be more explicit in the function documentation about functions being immutable. * Improve rounding in operator-based color operations. ### Deprecations -- Must Read! * When using `--stdin` with the `sass` or `scss` executables, importing relative to the working directory is deprecated. Having the working directory on the load path was deprecated in 3.3 and removed in 3.4, but due to an oversight the deprecation process never happened for files read over standard input in particular. This is the first step of that process. ## 3.4.14 (22 May 2015) * Further avoid race conditions when caching. * Only emit one warning for each line that uses the deprecated form of `unquote()`. * Stop parsing and emitting invalid `@supports` directives. * Add a deprecation warning for using `!=` to compare a number with units to a number without. Such a warning already existed for `==`. * Improve rounding of the results of color operations. ## 3.4.13 (26 February 2015) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.4.13). * Be clearer in the reference about hyphen/underscore equivalence. * `@keyframes` rules are now converted from CSS properly. * Extending a selector that contains a non-final pseudo-class no longer crashes. * When `@extending`, only a single `:root` element will be retained. ## 3.4.12 (13 February 2015) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.4.12). * Non-string interpolation within string interpolation is now parsed correctly. * `random()` now returns the correct result if it has an integer value but a float representation. ## 3.4.11 (30 January 2015) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.4.11). * Parent selectors used both outside and inside a pseudo selector (e.g. `&:not(&--foo)`) now compile correctly. * Interpolation in a multiline indented-syntax selector is no longer omitted. * Add a stack trace to the `unquote()` deprecation warning. * When converting a space-separated list with `sass-convert`, add parentheses when they make it easier to read even if they aren't strictly required. ### Deprecations -- Must Read! * Compiling directories on the command line without using either `--watch` or `--update` is deprecated. This only worked inconsistently before. ## 3.4.10 (16 January 2015) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.4.10). * `inspect()` no longer adds extra parentheses to nested maps. * Update the documentation of the `random()` function to accurate reflect its behavior. ### Deprecations -- Must Read! * Passing a non-string value to the `unquote()` function didn't do anything useful and is now deprecated. In future, this function will follow its documentation and emit an error if a non-string value is passed. * Defining a function named `calc`, `element`, `expression`, or `url` (or the former two with a vendor-style prefix) is now deprecated. These functions were already shadowed by CSS functions with special parsing rules, and so were impossible to use anyway. In the future, attempting to define these functions will throw an error. ## 3.4.9 (24 November 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.4.9). * Fix an incompatibility with listen 2.7.12 and later. * Properly handle interpolation within `calc()` and similar functions with `sass-convert`. * Properly handle conversion of `@extend` with `!optional` to SCSS. * Properly handle conversion of `@at-root` with a selector to SCSS. * Don't crash on selectors containing escape codes. ## 3.4.8 (14 November 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.4.8). * When `@extending` selectors inside `:not()`, don't add a complex selector where none existed before to avoid breaking selectors on browsers that don't support that. * Add a deprecation warning for passing numbers with units to the alpha arguments to `rgba()` and `hsla()`. In a future release, a percentage will be interpreted according to [the spec][alpha value] and other units will produce errors. [alpha value]: http://dev.w3.org/csswg/css-color/#typedef-alpha-value ## 3.4.7 (31 October 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.4.7). * Stop crashing when extending selector pseudoclasses such as `:not()`. * `@extend` resolution and `is-superselector()` no longer consider `.foo > .baz` to be a superselector of `.foo > .bar > .baz`. * Update documentation for `set-nth` ### Deprecations -- Must Read! * Sass will now print a warning when `==` is used for numbers when one number doesn't have units, the other does, and their numeric values are equal. In the future, unitless numbers will never be equal to numbers with units. ## 3.4.6 (16 October 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.4.6). * Parent selectors now work in selector pseudoclasses (for example, `:not(&)`). * `@for` loops no longer crash when one bound is an integer-like float. * Fix exception on `Sass::Importers::Filesystem#eql?`. ## 3.4.5 (19 September 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.4.5). * Fix `sass-convert --recursive`. * When using `sass --watch`, imported stylesheets within the working directory will be watched for changes. This matches the behavior of Sass 3.3. * Set exit code 65, indicating a [data error][], when the compiler fails due to a Sass error. [data error]: http://www.freebsd.org/cgi/man.cgi?query=sysexits ## 3.4.4 (12 September 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.4.4). * Produce more useful error messages when paths have incompatible encodings on Windows. * Allow `@keyframes` selectors to use arbitrary identifiers to support libraries like Skrollr. * `sass-convert` now preserves double-star mutliline comments (e.g. `/** foo */`). ## 3.4.3 (4 September 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.4.3). * Don't crash when a non-existent directory is on the load path. * Fix `--watch` on Windows. * Passing too many arguments to a function via `...` is now a warning rather than silently discarding additional arguments. In future versions of Sass, this will become an error. ## 3.4.2 (28 August 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.4.2). * Properly detect the output format from the output filename for `sass-convert`. * Properly parse interpolation immediately following a string. * Fix `--watch` for symlinked files and directories. ## 3.4.1 (22 August 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.4.1). * Fix `--watch`. * Don't generate `:not()` selectors that contain more than one complex selector. * Fix a parsing bug with attribute selectors containing single quotes. * Don't put rulesets inside `@keyframes` directives when bubbling them up to the top level. * Properly handle `@keyframes` rules that contain no properties. * Properly handle `--sourcemap=none` with `--update`. * Silence "template deleted" notifications for templates that weren't being watched. * Top-level control structures can assign to global variables without needing `!global`. Variables first defined in these structures will still be local without `!global`. ## 3.4.0 (18 August 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.4.0). ### Using `&` in SassScript For a long time, Sass has supported a special {file:SASS_REFERENCE.md#parent-selector "parent selector", `&`}, which is used when nesting selectors to describe how a nested selector relates to the selectors above it. Until now, this has only been usable in selectors, but now it can be used in SassScript as well. In a SassScript expression, `&` refers to the current parent selector. It's a comma-separated list of space-separated lists. For example: .foo.bar .baz.bang, .bip.qux { $selector: &; } The value of `$selector` is now `((".foo.bar" ".baz.bang"), ".bip.qux")`. The compound selectors are quoted here to indicate that they're strings, but in reality they would be unquoted. If there is no parent selector, the value of `&` will be null. This means you can use it in a mixin to detect whether a parent selector exists: @mixin does-parent-exist { @if & { &:hover { color: red; } } @else { a { color: red; } } } ### Selector Functions Complementing the ability to use `&` in SassScript, there's a new suite of functions that use Sass's powerful `@extend` infrastructure to allow users to manipulate selectors. These functions take selectors in the fully-parsed format returned by `&`, plain strings, or anything in between. Those that return selectors return them in the same format as `&`. * The {Sass::Script::Functions#selector_nest `selector-nest($selectors...)` function} nests each selector within one another just like they would be nested in the stylesheet if you wrote them separated by spaces. For example, `selector-nest(".foo, .bar", ".baz")` returns `.foo .baz, .bar .baz` (well, technically `(("foo" ".baz") (".bar" ".baz"))`). * The {Sass::Script::Functions#selector_append `selector-append($selectors...)` function} concatenates each selector without a space. It handles selectors with commas gracefully, so it's safer than just concatenating the selectors using `#{}`. For example, `selector-append(".foo, .bar", "-suffix")` returns `.foo-suffix, .bar-suffix`. * The {Sass::Script::Functions#selector_extend `selector-extend($selector, $extendee, $extender)` function} works just like `@extend`. It adds new selectors to `$selector` as though you'd written `$extender { @extend $extendee; }`. * The {Sass::Script::Functions#selector_replace `selector-replace($selector, $original, $replacement)` function} replaces all instances of `$original` in `$selector` with `$replacement`. It uses the same logic as `@extend`, so you can replace complex selectors with confidence. * The {Sass::Script::Functions#selector_unify `selector-unify($selector1, $selector2)` function} returns a selector that matches only elements matched by both `$selector1` and `$selector2`. * The {Sass::Script::Functions#is_superselector `is-superselector($super, $sub)` function} returns whether or not `$super` matches a superset of the elements `$sub` matches. * The {Sass::Script::Functions#simple_selectors `simple-selectors($selector)` function} takes only a selector with no commas or spaces (that is, a [compound selector](http://dev.w3.org/csswg/selectors4/#structure)). It returns the list of simple selectors that make up that compound selector. * The {Sass::Script::Functions#selector_parse `selector-parse($selector)` function} takes a selector in any format accepted by selector functions and returns it in the same format returned by `&`. It's useful for getting a selector into a consistent format before manually manipulating its contents. One of the most straightforward applications of selector functions and `&` is adding multiple suffixes to the same parent selector. For example: .list { @at-root #{selector-append(&, "--big", &, "--active")} { color: red; } } ### Smaller Improvements * Sourcemaps are now generated by default when using the `sass` or `scss` executables and when using Sass as a plugin. This can be disabled by passing `--sourcemap=none` on the command line or setting the `:sourcemap` option to `:none` in Ruby. * If a relative URI from a sourcemap to a Sass file can't be generated, it will now fall back to using an absolute "file:" URI. In addition, `--sourcemap=file` can be passed on the command line to force all URIs to Sass files to be absolute. * To guarantee that a sourcemap will be portable, `--sourcemap=inline` can be passed to include the full source text in the sourcemap. Note that this can cause very large sourcemaps to be generated for large projects. * `@extend` can now extend selectors within selector pseudoclasses such as `:not` and `:matches`. This also means that placeholder selectors can be used within selector pseudoclasses. This behavior can be detected using `feature-exists(extend-selector-pseudoclass)`. * Sass now supports an `@error` directive that prints a message as a fatal error. This is useful for user-defined mixins and functions that want to validate arguments and throw useful errors for unexpected conditions. Support for this directive can be detected using `feature-exists(at-error)`. * When using colors in SassScript, the original representation of the color will be preserved wherever possible. If you write `#f00`, it will be rendered as `#f00`, not as `red` or `#ff0000`. In compressed mode, Sass will continue to choose the most compact possible representation for colors. * Add support for unit arithmetic with many more units, including angles, times, frequencies, and resolutions. This behavior can be detected using `feature-exists(units-level-3)`. * Sass now follows the [CSS Syntax Level 3][encodings level 3] specification for determining a stylesheet's encoding. In addition, it now only emits UTF-8 CSS rather than trying to match the source encoding. [encodings level 3]: http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#determine-the-fallback-encoding * Sass now allows numbers to be written using scientific notation. It will not emit numbers in scientific notation until it's more widely supported in browsers. * Sass now allows almost any identifier to be used as a custom numeric unit. Identifiers that are ambiguous with subtraction, such as `px-1px`, are disallowed. * Sass now supports using ids as values in SassScript as defined in the CSS Basic User Interface Module. They're treated as unquoted strings. * SassScript strings and `@import` directives now support the full CSS string grammar. This includes handling escape codes and ignoring backslashes followed by newlines. * When the `sass` and `scss` executables encounter an error, they will now produce a CSS file describing that error. Previously this was enabled only for `--watch` and `--update` mode; now it's enabled whenever a CSS file is being written to disk. * The command-line `--help` documentation for the `sass`, `scss`, and `sass-convert` executables is revised and re-organized. * The `map-remove` function now allows multiple map keys to be removed at once. * The new `rebeccapurple` color is now supported. * The `rgb()`, `rgba()`, `hsl()`, and `hsla()` functions now follow the CSS spec by clamping their arguments to the valid ranges rather than emitting an error. Sass-specific color functions still throw errors for out-of-range values. * Sass will now emit a warning when a named color is used directly in interpolation (`#{}`) in a context where the hex representation would likely be invalid. This should help users avoid having their stylesheets break in compressed output mode. * Fix a bug where some `@media` queries would be followed by newlines in compressed mode. ### Backwards Incompatibilities -- Must Read! * The current working directory will no longer be placed onto the Sass load path by default. If you need the current working directory to be available, set `SASS_PATH=.` in your shell's environment. * Sass will now throw an error when a list of pairs is passed to a map function. * `mix()`'s deprecated argument names, `$color-1` and `$color-2`, will now throw errors. Use `$color1` and `$color2` instead. * `comparable()`'s deprecated argument names, `$number-1` and `$number-2`, will now throw errors. Use `$number1` and `$number2` instead. * `percentage()`'s, `round()`'s, `ceil()`'s, `floor()`'s, and `abs()`'s deprecated argument name, `$value`, will now throw an error. Use `$number` instead. * `index()` now returns `null` rather than `false` if the value isn't found in the list. * All variable assignments not at the top level of the document are now local by default. If there's a global variable with the same name, it won't be overwritten unless the `!global` flag is used. For example, `$var: value !global` will assign to `$var` globally. This behavior can be detected using `feature-exists(global-variable-shadowing)`. * Unescaped newlines in SassScript strings in SCSS are deprecated, since they're invalid according to the CSS string grammar. To include a newline in a string, use "\a" or "\a " as in CSS. * The subject selector operator, `!`, is deprecated and will produce a warning if used. The `:has()` selector should be used instead. * Quoted strings in lists will now be unquoted when those lists are interpolated in anything else. This makes it much easier to add user-defined strings to selector lists returned by `&`. This is unlikely to be behavior anyone's relying on and would be difficult to deprecate gracefully, so there will be no deprecation period for this change. ## 3.3.14 (1 August 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.3.14). * Improved edge-case parsing of special-syntax functions like `calc()` and `expression()`. * Fixed a bug when using `--update` with absolute paths on Windows. ## 3.3.13 (31 July 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.3.13). * Fixed a bug on ruby 2.0 where watching several folders was broken. Work around for a [bug in listen](https://github.com/guard/listen/issues/243). ## 3.3.12 (29 July 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.3.12). * The Sass::Compiler class has a number of new minor features to support Compass's compilation needs: * The template_deleted event of the Sass Compiler class now runs before the side-effect events. * The Sass Compiler class can now be used to clean output files. * The Sass Compiler class now exposes an updating_stylesheets callback that runs before stylesheets are mass-updated. * The Sass Compiler class now exposes a the compilation_starting callback that runs before an individual stylesheet is compiled. * The Sass Compiler class now runs the updated_stylesheets callback after stylesheets are mass-updated. * The Sass Compiler can now be made to skip the initial update when watching. ## 3.3.11 (25 July 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.3.11). * `str-slice()` now correctly returns an empty string when `$end-at` is 0. ## 3.3.10 (11 July 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.3.10). * Properly encode URLs in sourcemaps. ## 3.3.9 (27 June 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.3.9). * Defining a function named "and", "or", or "not" is now an error at definition-time. This isn't considered a backwards-incompatible change because although these functions could be defined previously, they could never be successfully invoked. * Fix deprecation warnings for using `File.exists?` on recent Ruby versions. * Fix a bug where `@extend` within `@at-root` could crash the compiler. * Gracefully handle the inability to change cache files' permissions. ## 3.3.8 (20 May 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.3.8). * When a use of `@at-root` doesn't add any new rules, it will no longer split its containing block in two ([issue 1239][]). * Fix a `sass-convert` bug where rules would occasionally be folded together incorrectly when converting from CSS to Sass. * Fix error messages for dynamically-generated `@media` queries with empty queries. [issue 1239]: https://github.com/nex3/sass/issues/1239 ## 3.3.7 (2 May 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.3.7). * Properly set the default `Sass::Plugin` options in Rails 3.0. * Fix a few cases where source ranges were being computed incorrectly for SassScript expressions. ## 3.3.6 (25 April 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.3.6). * The `inspect()` function will only interpret `/` between numbers as division under the same circumstances that it would be interpreted as division when used in a property. * Fix several cases where parsing pathological comments would cause Sass to take exponential time and consume all available CPU. ## 3.3.5 (14 April 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.3.5). * Fix `LoadError`s when using `--watch` with the bundled version of Listen. * Properly parse negative numbers preceded by a comment. * Avoid unnecessary interpolation when running `sass-convert` on media queries. * Avoid freezing Ruby's `true` or `false` values. ## 3.3.4 (21 March 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.3.4). * Improve the warning message for `index(...) == false`. * Fix the use of directives like `@font-face` within `@at-root`. * Fix a `sass --watch` issue on Windows where too many files would be updated on every change. * Avoid freezing Ruby's `nil` value. ## 3.3.3 (14 March 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.3.3). * Fix a bug in Sass that was causing caching errors when unserializable objects were in the Ruby options hash. Note that these errors may persist when using Sass with Sprockets until the Sprockets importer is made serializable. ## 3.3.2 (11 March 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.3.2). * Fix a bug with loading the bundled version of Listen. ## 3.3.1 (10 March 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.3.1). This release includes a number of fixes for issues that popped up in the immediate aftermath of the 3.3.0 release. ### Re-bundle [listen](http://github.com/guard/listen) With 3.3.0, we un-bundled the listen library from Sass. We did so hoping that it would make it easier for users to keep up to date with the latest features and bug fixes, but unfortunately listen 2.0 and on have dropped support for Ruby 1.8.7, which Sass continues to support. Further complicating things, RubyGems lacks the ability to install only the version of listen supported by the current Ruby installation, so we were unable to use a standard Gem dependency on listen. To work around this, we tried to piggyback on RubyGems' native extension support to install the correct version of listen when Sass was installed. This is what we released in 3.3.0. However, this caused numerous problems in practice, especially for users on Windows. It quickly became clear that this wasn't a viable long-term solution. As such, we're going back to the bundling strategy. While not perfect, this worked well enough for the duration of the Sass 3.2 release, and we expect it to cause much less havoc than un-bundling. We'll bundle listen 1.3.1, the most recent version that retains Ruby 1.8.7 compatibility. If a user of Sass has a more recent version of listen installed, that will be preferred to the bundled version. Listen versions through 2.7.0 have been tested, and we expect the code to work without modification on versions up to 3.0.0, assuming no major API changes. ### Smaller Changes * Fixed a small interface incompatibility with listen 2.7.0. * Fix some corner cases of path handling on Windows. * Avoid errors when trying to watch read-only directories using listen 1.x. ## 3.3.0 (7 March 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.3.0). ### SassScript Maps SassScript has a new data type: maps. These are associations from SassScript values (often strings, but potentially any value) to other SassScript values. They look like this: $map: (key1: value1, key2: value2, key3: value3); Unlike lists, maps must always be surrounded by parentheses. `()` is now an empty map in addition to an empty list. Maps will allow users to collect values into named groups and access those groups dynamically. For example, you could use them to manage themes for your stylesheet: $themes: ( mist: ( header: #DCFAC0, text: #00968B, border: #85C79C ), spring: ( header: #F4FAC7, text: #C2454E, border: #FFB158 ), // ... ); @mixin themed-header($theme-name) { h1 { color: map-get(map-get($themes, $theme-name), header); } } There are a variety of functions for working with maps: * The {Sass::Script::Functions#map_get `map-get($map, $key)` function} returns the value in the map associated with the given key. If no value is found, it returns `null`. * The {Sass::Script::Functions#map_merge `map-merge($map1, $map2)` function} merges two maps together into a new map. If there are any conflicts, the second map takes precedence, making this a good way to modify values in a map as well. * The {Sass::Script::Functions#map_remove `map-remove($map, $key)` function} returns a new map with a key removed. * The {Sass::Script::Functions#map_keys `map-keys($map)` function} returns all the keys in a map as a comma-separated list. * The {Sass::Script::Functions#map_values `map-values($map)` function} returns all the values in a map as a comma-separated list. * The {Sass::Script::Functions#map_has_key `map-has-key($map, $key)` function} returns whether or not a map contains a pair with the given key. All the existing list functions also work on maps, treating them as lists of pairs. For example, `nth((foo: 1, bar: 2), 1)` returns `foo 1`. Maps can also be used with `@each`, using the new multiple assignment feature (see below): @each $header, $size in (h1: 2em, h2: 1.5em, h3: 1.2em) { #{$header} { font-size: $size; } } Produces: h1 { font-size: 2em; } h2 { font-size: 1.5em; } h3 { font-size: 1.2em; } #### Variable Keyword Arguments Maps can be passed as variable arguments, just like lists. For example, if `$map` is `(alpha: -10%, "blue": 30%)`, you can write `scale-color($color, $map...)` and it will do the same thing as `scale-color($color, $alpha: -10%, $blue: 30%)`. To pass a variable argument list and map at the same time, just do the list first, then the map, as in `fn($list..., $map...)`. You can also access the keywords passed to a function that accepts a variable argument list using the new {Sass::Script::Functions#keywords `keywords($args)` function}. For example: @function create-map($args...) { @return keywords($args); } create-map($foo: 10, $bar: 11); // returns (foo: 10, bar: 11) #### Lists of Pairs as Maps The new map functions work on lists of pairs as well, for the time being. This feature exists to help libraries that previously used lists of pairs to simulate maps. These libraries can now use map functions internally without introducing backwards-incompatibility. For example: $themes: ( mist ( header #DCFAC0, text #00968B, border #85C79C ), spring ( header #F4FAC7, text #C2454E, border #FFB158 ), // ... ); @mixin themed-header($theme-name) { h1 { color: map-get(map-get($themes, $theme-name), header); } } Since it's just a migration feature, using lists of pairs in place of maps is already deprecated. Library authors should encourage their users to use actual maps instead. ### Source Maps Sass now has the ability to generate standard JSON [source maps][] of a format that will soon be supported in most major browsers. These source maps tell the browser how to find the Sass styles that caused each CSS style to be generated. They're much more fine-grained than the old Sass-specific debug info that was generated; rather than providing the source location of entire CSS rules at a time, source maps provide the source location of each individual selector and property. Source maps can be generated by passing the `--sourcemap` flag to the `sass` executable, by passing the {file:SASS_REFERENCE.md#sourcemap-option `:sourcemap` option} to \{Sass::Plugin}, or by using the \{Sass::Engine#render\_with\_sourcemap} method. By default, Sass assumes that the source stylesheets will be made available on whatever server you're using, and that their relative location will be the same as it is on the local filesystem. If this isn't the case, you'll need to make a custom class that extends \{Sass::Importers::Base} or \{Sass::Importers::Filesystem} and overrides \{Sass::Importers::Base#public\_url `#public_url`}. Thanks to Alexander Pavlov for implementing this. [source maps]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?hl=en_US&pli=1&pli=1 #### `@at-root` Sass 3.3 adds the `@at-root` directive, which is a way to tell Sass to put a collection of rules at the top-level root of the document. The easiest way to use it is with a selector: .badge { @at-root .info { ... } @at-root .header { ... } } In addition to using `@at-root` on a single selector, you can also use it on a whole block of them. For example: .badge { @at-root { .info { ... } .header { ... } } } Also produces: .info { ... } .header { ... } #### `@at-root (without: ...)` and `@at-root (with: ...)` By default, `@at-root` just excludes selectors. However, it's also possible to use `@at-root` to move outside of nested directives such as `@media` as well. For example: @media print { .page { width: 8in; @at-root (without: media) { color: red; } } } produces: @media print { .page { width: 8in; } } .page { color: red; } You can use `@at-root (without: ...)` to move outside of any directive. You can also do it with multiple directives separated by a space: `@at-root (without: media supports)` moves outside of both `@media` and `@supports` queries. There are two special values you can pass to `@at-root`. "rule" refers to normal CSS rules; `@at-root (without: rule)` is the same as `@at-root` with no query. `@at-root (without: all)` means that the styles should be moved outside of *all* directives and CSS rules. If you want to specify which directives or rules to include, rather than listing which ones should be excluded, you can use `with` instead of `without`. For example, `@at-root (with: rule)` will move outside of all directives, but will preserve any CSS rules. ### Smaller Improvements * The parent selector, `&`, can be used with an identifier suffix. For example, `&-suffix` and `&_suffix` are now legal. The suffix will be added to the end of the parent selector, and will throw an error if this isn't possible. `&` must still appear at the beginning of a compound selector -- that is, `.foo-&` is still illegal. * [listen](http://github.com/guard/listen) is no longer bundled with Sass, nor is it a standard RubyGems dependency. Instead, it's automatically installed along with Sass in order to ensure that the user ends up with a version of Listen that works with their local Ruby version. * Sass now has numerous functions for working with strings: \{Sass::Script::Functions#str_length `str-length`} will return the length of a string; \{Sass::Script::Functions#str_insert `str-insert`} will insert one string into another; \{Sass::Script::Functions#str_index `str-index`} will return the index of a substring within another string; \{Sass::Script::Functions#str_slice `str-slice`} will slice a substring from a string; \{Sass::Script::Functions#to_upper_case `to-upper-case`} will transform a string to upper case characters; and \{Sass::Script::Functions#to_lower_case `to-lower-case`} will transform a string to lower case characters. * A \{Sass::Script::Functions#list_separator `list-separator`} function has been added to determine what separator a list uses. Thanks to [Sam Richard](https://github.com/Snugug). * Custom Ruby functions can now access the global environment, which allows them the same power as Sass-based functions with respect to reading and setting variables defined elsewhere in the stylesheet. * The `set-nth($list, $n, $value)` function lets you construct a new list based on `$list`, with the nth element changed to the value specified. * In order to make it easier for mixins to process maps, they may now recursively call themselves and one another. It is no longer an error to have a mixin `@include` loop. * Add "grey" and "transparent" as recognized SassScript colors. Thanks to [Rob Wierzbowski](https://github.com/robwierzbowski). * Add a function \{Sass::Script::Functions#unique\_id `unique-id()`} that will return a CSS identifier that is unique within the scope of a single CSS file. * Allow negative indices into lists when using `nth()`. * You can now detect the presence of a Sass feature using the new function `feature-exists($feature-name)`. There are no detectable features in this release, this is provided so that subsequent releases can begin to use it. Additionally, plugins can now expose their functionality through `feature-exists` by calling `Sass.add_feature(feature_name)`. Features exposed by plugins must begin with a dash to distinguish them from official features. * It is now possible to determine the existence of different Sass constructs using these new functions: * `variable-exists($name)` checks if a variable resolves in the current scope. * `global-variable-exists($name)` checks if a global variable of the given name exists. * `function-exists($name)` checks if a function exists. * `mixin-exists($name)` checks if a mixin exists. * You can call a function by name by passing the function name to the call function. For example, `call(nth, a b c, 2)` returns `b`. * Comments following selectors in the indented syntax will be correctly converted using `sass-convert`. * `@each` now supports "multiple assignment", which makes it easier to iterate over lists of lists. If you write `@each $var1, $var2, $var3 in a b c, d e f, g h i`, the elements of the sub-lists will be assigned individually to the variables. `$var1`, `$var2`, and `$var3` will be `a`, `b` and `c`; then `d`, `e`, and `f`; and then `g`, `h`, and `i`. For more information, see {file:SASS_REFERENCE.md#each-multi-assign the `@each` reference}. * `@for` loops can now go downward as well as upward. For example, `@for $var from 5 through 1` will set `$var` to `5`, `4`, `3`, `2`, and `1`. Thanks to [Robin Roestenburg](http://twitter.com/robinroest). * There is a new {Sass::Script::Value::Helpers convenience API} for creating Sass values from within ruby extensions. * The `if()` function now only evaluates the argument corresponding to the value of the first argument. * Comma-separated lists may now have trailing commas (e.g. `1, 2, 3,`). This also allows you to use a trailing comma to distinguish a list with a single element from that element itself -- for example, `(1,)` is explicitly a list containing the value `1`. * All directives that are nested in CSS rules or properties and that contain more CSS rules or properties are now bubbled up through their parent rules. * A new `random()` function returns a random number. * A new function inspect($value) is provided for debugging the current sass representation of a value. * The `@debug` directive now automatically inspects sass objects that are not strings. * Numbers will no longer be emitted in scientific notation. * `sass-convert` will now correctly handle silent (`//`-style) comments contained within loud (`/* */`-style) comments. * Allow modulo arithmetic for numbers with compatible units. Thanks to [Isaac Devine](http://www.devinesystems.co.nz). * Keyword arguments to mixins and functions that contain hyphens will have the hyphens preserved when using `sass-convert`. ### Backwards Incompatibilities -- Must Read! * Sass will now throw an error when `@extend` is used to extend a selector outside the `@media` context of the extending selector. This means the following will be an error: @media screen { .foo { @extend .bar; } } .bar { color: blue; } * Sass will now throw an error when an `@extend` that has no effect is used. The `!optional` flag may be used to avoid this behavior for a single `@extend`. * Sass will now throw an error when it encounters a single `@import` statement that tries to import more than one file. For example, if you have `@import "screen"` and both `screen.scss` and `_screen.scss` exist, a warning will be printed. * `grey` and `transparent` are no longer interpreted as strings; they're now interpreted as colors, as per the CSS spec. * The automatic placement of the current working directory onto the Sass load path is now deprecated as this causes unpredictable build processes. If you need the current working directory to be available, set `SASS_PATH=.` in your shell's environment. * `Sass::Compiler.on_updating_stylesheet` has been removed. * `Sass::Plugin.options=` has been removed. * `Sass::Script::Number::PRECISION` has been removed. * The methods in the `Sass::Util` module can no longer be used by including it. They must be invoked on the module itself for performance reasons. * Sass values have always been immutable. The ruby object that backs each sass value is now "frozen" to prevent accidental modification and for performance. * Many classes in the \{Sass::Script} have been rearranged. All the value classes have been moved into \{Sass::Script::Value} (e.g. \{Sass::Script::Value::Color}, \{Sass::Script::Value::String}, etc). Their base class is now \{Sass::Script::Value::Base} instead of `Sass::Script::Literal`. All the parse tree classes have been moved into \{Sass::Script::Tree} (e.g. \{Sass::Script::Tree::Node}, \{Sass::Script::Tree::Operation}, etc). The old names will continue to work for the next couple releases, but they will be removed eventually. Any code using them should upgrade to the new names. * As part of a migration to cleaner variable semantics, assigning to global variables in a local context by default is deprecated. If there's a global variable named `$color` and you write `$color: blue` within a CSS rule, Sass will now print a warning; in the future, it will create a new local variable named `$color`. You may now explicitly assign to global variables using the `!global` flag; for example, `$color: blue !global` will always assign to the global `$color` variable. * Two numbers separated by a hyphen with no whitespace will now be parsed as a subtraction operation rather than two numbers in a list. That is, `2px-1px` will parse the same as `2px - 1px` rather than `2px -1px`. * `index()`'s `false` return value when a value isn't found is deprecated. In future Sass releases it will be `null` instead, so it should be used in ways that are compatible with both `false` and `null`. * `mix()`'s arguments are now `$color1` and `$color2` rather than `$color-1` and `$color-2`, in keeping with other functions. * `comparable()`'s arguments are now `$number1` and `$number2` rather than `$number-1` and `$number-2`, in keeping with other functions. * `percentage()`, `round()`, `ceil()`, `floor()`, and `abs()` now take arguments named '$number' instead of '$value'. ## 3.2.16 (17 March 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.2.16). * Handle a race condition in the filesystem cache store when a cache entry becomes invalidated. ## 3.2.15 (7 March 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.2.15). * Support `&.foo` when the parent selector has a newline followed by a comma. ## 3.2.14 (24 January 2014) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.2.14). * Don't crash when parsing a directive with no name in the indented syntax. * Clean up file paths when importing to avoid errors for overlong path names. * Parse calls to functions named `true`, `false`, and `null` as function calls. * Don't move CSS `@import`s to the top of the file unless it's necessary. ## 3.2.13 (19 December 2013) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.2.13). * Numbers returned by user-defined functions now trigger division, just like numbers stored in variables. * Support importing files in paths with open brackets. * Fix `sass-convert`'s handling of rules with empty bodies when converting from CSS. * Fix CSS imports using `url()` with a quoted string and media queries. ## 3.2.12 (4 October 2013) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.2.12). * Add a couple missing `require`s, fixing some load errors, especially when using the command-line interface. * Tune up some heuristics for eliminating redundant generated selectors. This will prevent some selector elimination in cases where multi-layered `@extend` is being used and where it seems intuitively like selectors shouldn't be eliminated. ## 3.2.11 (27 September 2013) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.2.11). * Fix `@extend`'s semantics with respect to pseudo-elements. They are no longer treated identically to pseudo-classes. * A more understandable error is now provided when the `-E` option is passed to the Sass command line in ruby 1.8 * Fixed a bug in the output of lists containing unary plus or minus operations during sass <=> scss conversion. * Avoid the [IE7 `content: counter` bug][cc bug] with `content: counters` as well. * Fix some thread-safety issues. ## 3.2.10 (26 July 2013) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.2.10). * Use the Sass logger infrastructure for `@debug` directives. * When printing a Sass error into a CSS comment, escape `*/` so the comment doesn't end prematurely. * Preserve the `!` in `/*! ... */`-style comments. * Fix a bug where selectors were being incorrectly trimmed when using `@extend`. * Fix a bug where `sass --unix-newlines` and `sass-convert --in-place` are not working on Windows (thanks [SATO Kentaro](http://www.ranvis.com)). ## 3.2.9 (10 May 2013) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.2.9). * Fix a bug where `@extend`s would occasionally cause a selector to be generated with the incorrect specificity. * Avoid loading [listen](http://github.com/guard/listen) v1.0, even if it's installed as a Gem (see [issue 719](https://github.com/nex3/sass/issues/719)). * Update the bundled version of [listen](http://github.com/guard/listen) to 0.7.3. * Automatically avoid the [IE7 `content: counter` bug][cc bug]. [cc bug]: http://jes.st/2013/ie7s-css-breaking-content-counter-bug/ ## 3.2.8 (22 April 2013) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.2.8). * Fix some edge cases where redundant selectors were emitted when using `@extend`. * Fix a bug where comma-separated lists with interpolation could lose elements. * Fix a bug in `sass-convert` where lists being passed as arguments to functions or mixins would lose their surrounding parentheses. * Fix a bug in `sass-convert` where `null` wasn't being converted correctly. * Fix a bug where multiple spaces in a string literal would sometimes be folded together. * `sass` and `sass-convert` won't create an empty file before writing to it. This fixes a flash of unstyled content when using LiveReload and similar tools. * Fix a case where a corrupted cache could produce fatal errors on some versions of Ruby. * Fix a case where a mixin loop error would be incorrectly reported when using `@content`. ## 3.2.7 (8 March 2013) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.2.7). * The \{Sass::Script::Functions#index `index`} and \{Sass::Script::Functions#zip `zip`} functions now work like all other list functions and treat individual values as single-element lists. * Avoid stack overflow errors caused by very long function or mixin argument lists. * Emit relative paths when using the `--line-comments` flag of the `sass` executable. * Fix a case where very long numbers would cause the SCSS parser to take exponential time. ## 3.2.6 (22 February 2013) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.2.6). * Support for Rubinius 2.0.0.rc1. All tests pass in 1.8 mode. 1.9 mode has some tests blocked on [Rubinius issue 2139](https://github.com/rubinius/rubinius/issues/2139). * Support for JRuby 1.7.2. * Support for symlinked executables. Thanks to [Yin-So Chen](http://yinsochen.com/). * Support for bubbling `@supports` queries in the indented syntax. * Fix an incorrect warning when using `@extend` from within nested `@media` queries. * Update the bundled version of [listen](http://github.com/guard/listen) to 0.7.2. ## 3.2.5 (4 January 2013) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.2.5). * Fix a bug where bogus `@extend` warnings were being generated. * Fix an `@import` bug on Windows. Thanks to [Darryl Miles](https://github.com/dlmiles). * Ruby 2.0.0-preview compatibility. Thanks to [Eric Saxby](http://www.livinginthepast.org/). * Fix incorrect line numbering when using DOS line endings with the indented syntax. ## 3.2.4 (21 December 2012) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.2.4). * Fix imports from `.jar` files in JRuby. Thanks to [Alex Hvostov](https://github.com/argv-minus-one). * Allow comments within `@import` statements in SCSS. * Fix a parsing performance bug where long decimals would occasionally take many minutes to parse. ## 3.2.3 (9 November 2012) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.2.3). * `sass --watch` no longer crashs when a file in a watched directory is deleted. * Allow `@extend` within bubbling nodes such as `@media`. * Fix various JRuby incompatibilities and test failures. * Work around a performance bug that arises from using `@extend` with deeply-nested selectors. ## 3.2.2 (2 November 2012) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.2.2). * Add a `--poll` option to force `sass --watch` to use the polling backend to [Listen](https://github.com/guard/listen). * Fix some error reporting bugs related to `@import`. * Treat [protocol-relative URLs][pru] in `@import`s as static URLs, just like `http` and `https` URLs. * Improve the error message for misplaced simple selectors. * Fix an option-handling bug that was causing errors with the Compass URL helpers. * Fix a performance issue with `@import` that only appears when ActiveSupport is loaded. * Fix flushing of actions to stdout. Thanks to [Russell Davis](http://github.com/russelldavis). * Fix the documentation for the `max()` function. * Fix a `@media` parsing bug. [pru]: http://paulirish.com/2010/the-protocol-relative-url/ ### Deprecations -- Must Read! * Sass will now print a warning when it encounters a single `@import` statement that tries to import more than one file. For example, if you have `@import "screen"` and both `screen.scss` and `_screen.scss` exist, a warning will be printed. This will become an error in future versions of Sass. ## 3.2.1 (15 August 2012) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.2.1). * Fix a buggy interaction with Pow and Capybara that caused `EOFError`s. ## 3.2.0 (10 August 2012) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.2.0). ### `@content` A mixin include can now accept a block of content ({file:SASS_REFERENCE.md#mixin-content Reference Documentation}). The style block will be passed to the mixin and can be placed at the point @content is used. E.g.: @mixin iphone { @media only screen and (max-width: 480px) { @content; } } @include iphone { body { color: red } } Or in `.sass` syntax: =iphone @media only screen and (max-width: 480px) @content +iphone body color: red Produces: @media only screen and (max-width: 480px) { body { color: red } } Note that the contents passed to the mixin are evaluated in the scope they are used, not the scope of the mixin. {file:SASS_REFERENCE.md#variable_scope_and_content_blocks More on variable scoping.} ### Placeholder Selectors: `%foo` Sass supports a new, special type of selector called a "placeholder selector". These look like class and id selectors, except the `#` or `.` is replaced by `%`. They're meant to be used with the {file:SASS_REFERENCE.md#extend `@extend` directive}, when you want to write styles to be extended but you don't want the base styles to appear in the CSS. On its own, a placeholder selector just causes a ruleset not to be rendered. For example: // This ruleset won't be rendered on its own. #context a%extreme { color: blue; font-weight: bold; font-size: 2em; } However, placeholder selectors can be extended, just like classes and ids. The extended selectors will be generated, but the base placeholder selector will not. For example: .notice { @extend %extreme; } Is compiled to: #context a.notice { color: blue; font-weight: bold; font-size: 2em; } ### Variable Arguments Mixins and functions now both support variable arguments. When defining a mixin or function, you can add `...` after the final argument to have it accept an unbounded number of arguments and package them into a list. When calling a mixin or function, you can add `...` to expand the final argument (if it's a list) so that each value is passed as a separate argument. For example: @mixin box-shadow($shadows...) { // $shadows is a list of all arguments passed to box-shadow -moz-box-shadow: $shadows; -webkit-box-shadow: $shadows; box-shadow: $shadows; } // This is the same as "@include spacing(1, 2, 3);" $values: 1, 2, 3; @include spacing($values...); Finally, if a variable argument list is passed directly on to another mixin or function, it will also pass along any keyword arguments. This means that you can wrap a pre-existing mixin or function and add new functionality without changing the call signature. ### Directive Interpolation `#{}` interpolation is now allowed in all plain CSS directives (such as `@font-face`, `@keyframes`, and of course `@media`). In addition, `@media` gets some special treatment. In addition to allowing `#{}` interpolation, expressions may be used directly in media feature queries. This means that you can write e.g.: $media: screen; $feature: -webkit-min-device-pixel-ratio; $value: 1.5; @media #{$media} and ($feature: $value) { ... } This is intended to allow authors to easily write mixins that make use of `@media` and other directives dynamically. ### Smaller Improvements * Mixins and functions may now be defined in a nested context, for example within `@media` rules. This also allows files containing them to be imported in such contexts. * Previously, only the `:-moz-any` selector was supported; this has been expanded to support any vendor prefix, as well as the plain `:any` selector. * All proposed [CSS4 selectors](http://dev.w3.org/csswg/selectors4/) are now supported, including reference selectors (e.g. `.foo /attr/ .bar`) and subject selectors (e.g. `.foo!`). * Sass now supports a global list of load paths, accessible via {Sass.load_paths}. This allows plugins and libraries to easily register their Sass files such that they're accessible to all {Sass::Engine} instances. * `Sass.load_paths` is initialized to the value of the `SASS_PATH` environment variable. This variable should contain a colon-separated list of load paths (semicolon-separated on Windows). * In certain cases, redundant selectors used to be created as a result of a single rule having multiple `@extend`s. That redundancy has been eliminated. * Redundant selectors were also sometimes created by nested selectors using `@extend`. That redundancy has been eliminated as well. * There is now much more comprehensive support for using `@extend` alongside CSS3 selector combinators (`+`, `~`, and `>`). These combinators will now be merged as much as possible. * The full set of [extended color keywords](http://www.w3.org/TR/css3-color/#svg-color) are now supported by Sass. They may be used to refer to color objects, and colors will render using those color names when appropriate. * Sass 3.2 adds the \{Sass::Script::Functions#ie_hex_str `ie-hex-str`} function which returns a hex string for a color suitable for use with IE filters. * Sass 3.2 adds the \{Sass::Script::Functions#min `min`} and \{Sass::Script::Functions#max `max`} functions, which return the minimum and maximum of several values. * Sass functions are now more strict about how keyword arguments can be passed. * Decimal numbers now default to five digits of precision after the decimal point. * The \{Sass::Script::Functions::EvaluationContext#options options hash} available to Sass functions now contains the filename of the file that the function was executed in, rather than the top-level file. ### Backwards Incompatibilities -- Must Read! #### `@extend` Warnings Any `@extend` that doesn't match any selectors in the document will now print a warning. These warnings will become errors in future versions of Sass. This will help protect against typos and make it clearer why broken styles aren't working. For example: h1.notice {color: red} a.important {@extend .notice} This will print a warning, since the only use of `.notice` can't be merged with `a`. You can declare that you don't want warnings for a specific `@extend` by using the `!optional` flag. For example: h1.notice {color: red} a.important {@extend .notice !optional} This will not print a warning. #### Smaller Incompatibilities * Parent selectors followed immediately by identifiers (e.g. `&foo`) are fully disallowed. They were deprecated in 3.1.8. * `#{}` interpolation is now allowed in all comments. * The `!` flag may not be used with `//` comments (e.g. `//!`). * `#{}` interpolation is now disallowed in all `@import` statements except for those using `url()`. * `sass-convert` no longer supports converting files from LessCSS. ## 3.1.21 (10 August 2012) [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.21). * Preserve single-line comments that are embedded within multi-line comments. * Preserve newlines in nested selectors when those selectors are used multiple times in the same document. * Allow tests to be run without the `LANG` environment variable set. * Update the bundled version of [Listen](https://github.com/guard/listen) to 0.4.7. * Sass will now convert `px` to other absolute units using the conversion ratio of `96px == 1in` as dictated by the [CSS Spec](http://www.w3.org/TR/CSS21/syndata.html#length-units) ## 3.1.20 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.20). * Don't crash if a UTF encoding isn't found. Thanks to [Andrew Garbutt](http://github.com/techsplicer). * Properly watch files recursively with `sass --watch`. Thanks to [Sébastien Tisserant](https://github.com/sebweaver). * Fix the documentation for the \{Sass::Script::Functions#append append()} function. * Support the `saturate()`, `opacity()`, and `invert()` functions when used as in the [Filter Effects][filter] spec. * Support MacRuby. Thanks to [Will Glynn](http://github.com/delta407). [filter]: https://dvcs.w3.org/hg/FXTF/raw-file/tip/filters/index.html ## 3.1.19 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.19). * Fix an `uninitialized constant Sass::Exec::Sass::Util` error when using the command-line tool. * Allow `@extend` within directives such as `@media` as long as it only extends selectors that are within the same directive. ## 3.1.18 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.18). * Ruby 2.0 compatibility. Thanks to [Jeremy Kemper](https://github.com/jeremy). ### Deprecations -- Must Read! * Deprecate the use of `@extend` within directives such as `@media`. This has never worked correctly, and now it's officially deprecated. It will be an error in 3.2. ## 3.1.17 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.17). * Don't crash when calling `#inspect` on an internal Sass tree object in Ruby 1.9. * Fix some bugs in `sass --watch` introduced in 3.1.16. Thanks to [Maher Sallam](https://github.com/Maher4Ever). * Support bare interpolation in the value portion of attribute selectors (e.g. `[name=#{$value}]`). * Support keyword arguments for the `invert()` function. * Handle backslash-separated paths better on Windows. * Fix `rake install` on Ruby 1.9. * Properly convert nested `@if` statements with `sass-convert`. ## 3.1.16 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.16). * Fix some bugs in `sass-convert` selector parsing when converting from CSS. * Substantially improve compilation performance on Ruby 1.8. * Support the `@-moz-document` directive's non-standard `url-prefix` and `domain` function syntax. * Support the [`@supports` directive](http://www.w3.org/TR/css3-conditional/#at-supports). * Fix a performance issue when using `/*! */` comments with the Rails asset pipeline. * Support `-moz-element`. * Properly handle empty lists in `sass-convert`. * Move from [FSSM](https://github.com/ttilley/fssm) to [Listen](https://github.com/guard/listen) for file-system monitoring. ## 3.1.15 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.15). * Support extending multiple comma-separated selectors (e.g. `@extend .foo, .bar`). This is just a terser way to write multiple `@extend`s (e.g. `@extend .foo; @extend .bar`). This wasn't previously intended to work, but it did in the indented syntax only. * Avoid more stack overflows when there are import loops in files. * Update the bundled [FSSM](https://github.com/ttilley/fssm) to version 0.2.8.1. * Make the `grayscale` function work with `-webkit-filter`. * Provide a better error message for selectors beginning with `/` in the indented syntax. * Flush standard output after printing notifications in `sass --watch`. * Fix variable definitions in the REPL. ## 3.1.14 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.14). * Fix a typo that was causing crashes on Ruby 1.9. ## 3.1.13 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.13). * Fix a smattering of subtle bugs that would crop up when using multibyte character sets. * Fix a bug when using `@extend` with selectors containing newlines. * Make boolean operators short-circuit. * Remove unnecessary whitespace in selectors in `:compressed` mode. * Don't output debug info within non-`@media` directives. * Make sure `:after` and `:before` selectors end up on the end of selectors resulting from `@extend`. * Fix a bug when using imports containing invalid path characters on Windows. * Bubble CSS `@import` statements to the top of stylesheets. ## 3.1.12 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.12). * Compatibility with the `mathn` library (thanks to [Thomas Walpole](https://github.com/twalpole)). * Fix some infinite loops with mixins that were previously uncaught. * Catch infinite `@import` loops. * Fix a deprecation warning in `sass --update` and `--watch` (thanks to [Marcel Köppen](https://github.com/Marzelpan)). * Don't make `$important` a special pre-initialized variable. * Fix exponential parsing time of certain complex property values and selectors. * Properly merge `@media` directives with comma-separated queries. E.g. `@media foo, bar { @media baz { ... } }` now becomes `@media foo and baz, bar and baz { ... }`. ## 3.1.11 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.11). * Allow control directives (such as `@if`) to be nested beneath properties. * Allow property names to begin with a hyphen followed by interpolation (e.g. `-#{...}`). * Fix a parsing error with interpolation in comma-separated lists. * Make `--cache-store` with with `--update`. * Properly report `ArgumentError`s that occur within user-defined functions. * Don't crash on JRuby if the underlying Java doesn't support every Unicode encoding. * Add new `updated_stylesheet` callback, which is run after each stylesheet has been successfully compiled. Thanks to [Christian Peters](https://github.com/ChristianPeters). * Allow absolute paths to be used in an importer with a different root. * Don't destructively modify the options when running `Sass::Plugin.force_update`. * Prevent Regexp buffer overflows when parsing long strings (thanks to [Agworld](https://github.com/Agworld). ### Deprecations -- Must Read! * The `updating_stylesheet` is deprecated and will be removed in a future release. Use the new `updated_stylesheet` callback instead. ## 3.1.10 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.10). * Fix another aspect of the 3.1.8 regression relating to `+`. ## 3.1.9 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.9). * Fix a regression in 3.1.8 that broke the `+` combinator in selectors. * Deprecate the loud-comment flag when used with silent comments (e.g. `//!`). Using it with multi-line comments (e.g. `/*!`) still works. ## 3.1.8 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.8). * Deprecate parent selectors followed immediately by identifiers (e.g. `&foo`). This should never have worked, since it violates the rule of `&` only being usable where an element selector would. * Add a `--force` option to the `sass` executable which makes `--update` always compile all stylesheets, even if the CSS is newer. * Disallow semicolons at the end of `@import` directives in the indented syntax. * Don't error out when being used as a library without requiring `fileutil`. * Don't crash when Compass-style sprite imports are used with `StalenessChecker` (thanks to [Matthias Bauer](https://github.com/moeffju)). * The numeric precision of numbers in Sass can now be set using the `--precision` option to the command line. Additionally, the default number of digits of precision in Sass output can now be changed by setting `Sass::Script::Number.precision` to an integer (defaults to 3). Since this value can now be changed, the `PRECISION` constant in `Sass::Script::Number` has been deprecated. In the unlikely event that you were using it in your code, you should now use `Sass::Script::Number.precision_factor` instead. * Don't crash when running `sass-convert` with selectors with two commas in a row. * Explicitly require Ruby >= 1.8.7 (thanks [Eric Mason](https://github.com/ericmason)). * Properly validate the nesting of elements in imported stylesheets. * Properly compile files in parent directories with `--watch` and `--update`. * Properly null out options in mixin definitions before caching them. This fixes a caching bug that has been plaguing some Rails 3.1 users. ## 3.1.7 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.7). * Don't crash when doing certain operations with `@function`s. ## 3.1.6 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.6). * The option `:trace_selectors` can now be used to emit a full trace before each selector. This can be helpful for in-browser debugging of stylesheet imports and mixin includes. This option supersedes the `:line_comments` option and is superseded by the `:debug_info` option. * Fix a bug where long `@if`/`@else` chains would cause exponential slowdown under some circumstances. ## 3.1.5 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.5). * Updated the vendored FSSM version, which will avoid segfaults on OS X Lion when using `--watch`. ## 3.1.4 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.4). * Sass no longer unnecessarily caches the sass options hash. This allows objects that cannot be marshaled to be placed into the options hash. ## 3.1.3 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.3). * Sass now logs message thru a logger object which can be changed to provide integration with other frameworks' logging infrastructure. ## 3.1.2 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.2). * Fix some issues that were breaking Sass when running within Rubinius. * Fix some issues that were affecting Rails 3.1 integration. * New function `zip` allows several lists to be combined into one list of lists. For example: `zip(1px 1px 3px, solid dashed solid, red green blue)` becomes `1px solid red, 1px dashed green, 3px solid blue` * New function `index` returns the list index of a value within a list. For example: `index(1px solid red, solid)` returns `2`. When the value is not found `false` is returned. ## 3.1.1 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.1). * Make sure `Sass::Plugin` is loaded at the correct time in Rails 3. ## 3.1.0 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.1.0). * Add an {Sass::Script::Functions#invert `invert` function} that takes the inverse of colors. * A new sass function called `if` can be used to emit one of two values based on the truth value of the first argument. For example, `if(true, 1px, 2px)` returns `1px` and `if(false, 1px, 2px)` returns `2px`. * Compass users can now use the `--compass` flag to make the Compass libraries available for import. This will also load the Compass project configuration if run from the project root. * Many performance optimizations have been made by [thedarkone](http://github.com/thedarkone). * Allow selectors to contain extra commas to make them easier to modify. Extra commas will be removed when the selectors are converted to CSS. * `@import` may now be used within CSS or `@media` rules. The imported file will be treated as though it were nested within the rule. Files with mixins may not be imported in nested contexts. * If a comment starts with `!`, that comment will now be interpolated (`#{...}` will be replaced with the resulting value of the expression inside) and the comment will always be printed out in the generated CSS file -- even with compressed output. This is useful for adding copyright notices to your stylesheets. * A new executable named `scss` is now available. It is exactly like the `sass` executable except it defaults to assuming input is in the SCSS syntax. Both programs will use the source file's extension to determine the syntax where possible. ### Sass-based Functions While it has always been possible to add functions to Sass with Ruby, this release adds the ability to define new functions within Sass files directly. For example: $grid-width: 40px; $gutter-width: 10px; @function grid-width($n) { @return $n * $grid-width + ($n - 1) * $gutter-width; } #sidebar { width: grid-width(5); } Becomes: #sidebar { width: 240px; } ### Keyword Arguments Both mixins and Sass functions now support the ability to pass in keyword arguments. For example, with mixins: @mixin border-radius($value, $moz: true, $webkit: true, $css3: true) { @if $moz { -moz-border-radius: $value } @if $webkit { -webkit-border-radius: $value } @if $css3 { border-radius: $value } } @include border-radius(10px, $webkit: false); And with functions: p { color: hsl($hue: 180, $saturation: 78%, $lightness: 57%); } Keyword arguments are of the form `$name: value` and come after normal arguments. They can be used for either optional or required arguments. For mixins, the names are the same as the argument names for the mixins. For functions, the names are defined along with the functions. The argument names for the built-in functions are listed {Sass::Script::Functions in the function documentation}. Sass functions defined in Ruby can use the {Sass::Script::Functions.declare} method to declare the names of the arguments they take. #### New Keyword Functions The new keyword argument functionality enables new Sass color functions that use keywords to encompass a large amount of functionality in one function. * The {Sass::Script::Functions#adjust_color adjust-color} function works like the old `lighten`, `saturate`, and `adjust-hue` methods. It increases and/or decreases the values of a color's properties by fixed amounts. For example, `adjust-color($color, $lightness: 10%)` is the same as `lighten($color, 10%)`: it returns `$color` with its lightness increased by 10%. * The {Sass::Script::Functions#scale_color scale_color} function is similar to {Sass::Script::Functions#adjust_color adjust_color}, but instead of increasing and/or decreasing a color's properties by fixed amounts, it scales them fluidly by percentages. The closer the percentage is to 100% (or -100%), the closer the new property value will be to its maximum (or minimum). For example, `scale-color(hsl(120, 70, 80), $lightness: 50%)` will change the lightness from 80% to 90%, because 90% is halfway between 80% and 100%. Similarly, `scale-color(hsl(120, 70, 50), $lightness: 50%)` will change the lightness from 50% to 75%. * The {Sass::Script::Functions#change_color change-color} function simply changes a color's properties regardless of their old values. For example `change-color($color, $lightness: 10%)` returns `$color` with 10% lightness, and `change-color($color, $alpha: 0.7)` returns color with opacity 0.7. Each keyword function accepts `$hue`, `$saturation`, `$value`, `$red`, `$green`, `$blue`, and `$alpha` keywords, with the exception of `scale-color()` which doesn't accept `$hue`. These keywords modify the respective properties of the given color. Each keyword function can modify multiple properties at once. For example, `adjust-color($color, $lightness: 15%, $saturation: -10%)` both lightens and desaturates `$color`. HSL properties cannot be modified at the same time as RGB properties, though. ### Lists Lists are now a first-class data type in Sass, alongside strings, numbers, colors, and booleans. They can be assigned to variables, passed to mixins, and used in CSS declarations. Just like the other data types (except booleans), Sass lists look just like their CSS counterparts. They can be separated either by spaces (e.g. `1px 2px 0 10px`) or by commas (e.g. `Helvetica, Arial, sans-serif`). In addition, individual values count as single-item lists. Lists won't behave any differently in Sass 3.1 than they did in 3.0. However, you can now do more with them using the new [list functions](Sass/Script/Functions.html#list-functions): * The {Sass::Script::Functions#nth `nth($list, $n)` function} returns the nth item in a list. For example, `nth(1px 2px 10px, 2)` returns the second item, `2px`. Note that lists in Sass start at 1, not at 0 like they do in some other languages. * The {Sass::Script::Functions#join `join($list1, $list2)` function} joins together two lists into one. For example, `join(1px 2px, 10px 5px)` returns `1px 2px 10px 5px`. * The {Sass::Script::Functions#append `append($list, $val)` function} appends values to the end of a list. For example, `append(1px 2px, 10px)` returns `1px 2px 10px`. * The {Sass::Script::Functions#join `length($list)` function} returns the length of a list. For example, `length(1px 2px 10px 5px)` returns `4`. For more details about lists see {file:SASS_REFERENCE.md#lists the reference}. #### `@each` There's also a new directive that makes use of lists. The {file:SASS_REFERENCE.md#each-directive `@each` directive} assigns a variable to each item in a list in turn, like `@for` does for numbers. This is useful for writing a bunch of similar styles without having to go to the trouble of creating a mixin. For example: @each $animal in puma, sea-slug, egret, salamander { .#{$animal}-icon { background-image: url('/images/#{$animal}.png'); } } is compiled to: .puma-icon { background-image: url('/images/puma.png'); } .sea-slug-icon { background-image: url('/images/sea-slug.png'); } .egret-icon { background-image: url('/images/egret.png'); } .salamander-icon { background-image: url('/images/salamander.png'); } ### `@media` Bubbling Modern stylesheets often use `@media` rules to target styles at certain sorts of devices, screen resolutions, or even orientations. They're also useful for print and aural styling. Unfortunately, it's annoying and repetitive to break the flow of a stylesheet and add a `@media` rule containing selectors you've already written just to tweak the style a little. Thus, Sass 3.1 now allows you to nest `@media` rules within selectors. It will automatically bubble them up to the top level, putting all the selectors on the way inside the rule. For example: .sidebar { width: 300px; @media screen and (orientation: landscape) { width: 500px; } } is compiled to: .sidebar { width: 300px; } @media screen and (orientation: landscape) { .sidebar { width: 500px; } } You can also nest `@media` directives within one another. The queries will then be combined using the `and` operator. For example: @media screen { .sidebar { @media (orientation: landscape) { width: 500px; } } } is compiled to: @media screen and (orientation: landscape) { .sidebar { width: 500px; } } ### Nested `@import` The `@import` statement can now be nested within other structures such as CSS rules and `@media` rules. For example: @media print { @import "print"; } This imports `print.scss` and places all rules so imported within the `@media print` block. This makes it easier to create stylesheets for specific media or sections of the document and distributing those stylesheets across multiple files. ### Backwards Incompatibilities -- Must Read! * When `@import` is given a path without `.sass`, `.scss`, or `.css` extension, and no file exists at that path, it will now throw an error. The old behavior of becoming a plain-CSS `@import` was deprecated and has now been removed. * Get rid of the `--rails` flag for the `sass` executable. This flag hasn't been necessary since Rails 2.0. Existing Rails 2.0 installations will continue to work. * Removed deprecated support for ! prefixed variables. Use $ to prefix variables now. * Removed the deprecated css2sass executable. Use sass-convert now. * Removed support for the equals operator in variable assignment. Use : now. * Removed the sass2 mode from sass-convert. Users who have to migrate from sass2 should install Sass 3.0 and quiet all deprecation warnings before installing Sass 3.1. ### Sass Internals * It is now possible to define a custom importer that can be used to find imports using different import semantics than the default filesystem importer that Sass provides. For instance, you can use this to generate imports on the fly, look them up from a database, or implement different file naming conventions. See the {Sass::Importers::Base Importer Base class} for more information. * It is now possible to define a custom cache store to allow for efficient caching of Sass files using alternative cache stores like memcached in environments where a writable filesystem is not available or where the cache need to be shared across many servers for dynamically generated stylesheet environments. See the {Sass::CacheStores::Base CacheStore Base class} for more information. ## 3.0.26 (Unreleased) * Fix a performance bug in large SCSS stylesheets with many nested selectors. This should dramatically decrease compilation time of such stylesheets. * Upgrade the bundled FSSM to version 0.2.3. This means `sass --watch` will work out of the box with Rubinius. ## 3.0.25 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.25). * When displaying a Sass error in an imported stylesheet, use the imported stylesheet's contents rather than the top-level stylesheet. * Fix a bug that caused some lines with non-ASCII characters to be ignored in Ruby 1.8. * Fix a bug where boolean operators (`and`, `or`, and `not`) wouldn't work at the end of a line in a multiline SassScript expression. * When using `sass --update`, only update individual files when they've changed. ## 3.0.24 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.24). * Raise an error when `@else` appears without an `@if` in SCSS. * Fix some cases where `@if` rules were causing the line numbers in error reports to become incorrect. ## 3.0.23 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.23). * Fix the error message for unloadable modules when running the executables under Ruby 1.9.2. ### `@charset` Change The behavior of `@charset` has changed in version 3.0.23 in order to work around a bug in Safari, where `@charset` declarations placed anywhere other than the beginning of the document cause some CSS rules to be ignored. This change also makes `@charset`s in imported files behave in a more useful way. #### Ruby 1.9 When using Ruby 1.9, which keeps track of the character encoding of the Sass document internally, `@charset` directive in the Sass stylesheet and any stylesheets it imports are no longer directly output to the generated CSS. They're still used for determining the encoding of the input and output stylesheets, but they aren't rendered in the same way other directives are. Instead, Sass adds a single `@charset` directive at the beginning of the output stylesheet if necessary, whether or not the input stylesheet had a `@charset` directive. It will add this directive if and only if the output stylesheet contains non-ASCII characters. By default, the declared charset will be UTF-8, but if the Sass stylesheet declares a different charset then that will be used instead if possible. One important consequence of this scheme is that it's possible for a Sass file to import partials with different encodings (e.g. one encoded as UTF-8 and one as IBM866). The output will then be UTF-8, unless the importing stylesheet declares a different charset. #### Ruby 1.8 Ruby 1.8 doesn't have good support for encodings, so it uses a simpler but less accurate scheme for figuring out what `@charset` declaration to use for the output stylesheet. It just takes the first `@charset` declaration to appear in the stylesheet or any of its imports and moves it to the beginning of the document. This means that under Ruby 1.8 it's *not* safe to import files with different encodings. ## 3.0.22 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.22). * Remove `vendor/sass`, which snuck into the gem by mistake and was causing trouble for Heroku users (thanks to [Jacques Crocker](http://railsjedi.com/)). * `sass-convert` now understands better when it's acceptable to remove parentheses from expressions. ## 3.0.21 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.21). * Fix the permissions errors for good. * Fix more `#options` attribute errors. ## 3.0.20 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.20). * Fix some permissions errors. * Fix `#options` attribute errors when CSS functions were used with commas. ## 3.0.19 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.19). * Make the alpha value for `rgba` colors respect {Sass::Script::Value::Number.precision}. * Remove all newlines in selectors in `:compressed` mode. * Make color names case-insensitive. * Properly detect SCSS files when using `sass -c`. * Remove spaces after commas in `:compressed` mode. * Allow the `--unix-newlines` flag to work on Unix, where it's a no-op. ## 3.0.18 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.18). * Don't require `rake` in the gemspec, for bundler compatibility under JRuby. Thanks to [Gordon McCreight](http://www.gmccreight.com/blog). * Add a command-line option `--stop-on-error` that causes Sass to exit when a file fails to compile using `--watch` or `--update`. * Fix a bug in `haml_tag` that would allow duplicate attributes to be added and make `data-` attributes not work. * Get rid of the annoying RDoc errors on install. * Disambiguate references to the `Rails` module when `haml-rails` is installed. * Allow `@import` in SCSS to import multiple files in the same `@import` rule. ## 3.0.17 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.17). * Disallow `#{}` interpolation in `@media` queries or unrecognized directives. This was never allowed, but now it explicitly throws an error rather than just producing invalid CSS. * Make `sass --watch` not throw an error when passed a single file or directory. * Understand that mingw counts as Windows. * Make `sass --update` return a non-0 exit code if one or more files being updated contained an error. ## 3.0.16 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.16). * Fix a bug where certain sorts of comments would get improperly rendered in the `:compact` style. * Always allow a trailing `*/` in loud comments in the indented syntax. * Fix a performance issue with SCSS parsing in rare cases. Thanks to [Chris Eppstein](http://chriseppstein.github.com). * Use better heuristics for figuring out when someone might be using the wrong syntax with `sass --watch`. ## 3.0.15 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.15). * Fix a bug where `sass --watch` and `sass --update` were completely broken. * Allow `@import`ed values to contain commas. ## 3.0.14 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.14). * Properly parse paths with drive letters on Windows (e.g. `C:\Foo\Bar.sass`) in the Sass executable. * Compile Sass files in a deterministic order. * Fix a bug where comments after `@if` statements in SCSS weren't getting passed through to the output document. ## 3.0.13 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.13). ## CSS `@import` Directives Sass is now more intelligent about when to compile `@import` directives to plain CSS. Any of the following conditions will cause a literal CSS `@import`: * Importing a path with a `.css` extension (e.g. `@import "foo.css"`). * Importing a path with a media type (e.g. `@import "foo" screen;`). * Importing an HTTP path (e.g. `@import "http://foo.com/style.css"`). * Importing any URL (e.g. `@import url(foo)`). The former two conditions always worked, but the latter two are new. ## `-moz-calc` Support The new [`-moz-calc()` function](http://hacks.mozilla.org/2010/06/css3-calc/) in Firefox 4 will now be properly parsed by Sass. `calc()` was already supported, but because the parsing rules are different than for normal CSS functions, this had to be expanded to include `-moz-calc`. In anticipation of wider browser support, in fact, *any* function named `-*-calc` (such as `-webkit-calc` or `-ms-calc`) will be parsed the same as the `calc` function. ## `:-moz-any` Support The [`:-moz-any` pseudoclass selector](http://hacks.mozilla.org/2010/05/moz-any-selector-grouping/) is now parsed by Sass. ## `--require` Flag The Sass command-line executable can now require Ruby files using the `--require` flag (or `-r` for short). ## Rails Support Make sure the default Rails options take precedence over the default non-Rails options. This makes `./script/server --daemon` work again. ### Rails 3 Support Support for Rails 3 versions prior to beta 4 has been removed. Upgrade to Rails 3.0.0.beta4 if you haven't already. ## 3.0.12 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.12). ## Rails 3 Support Apparently the last version broke in new and exciting ways under Rails 3, due to the inconsistent load order caused by certain combinations of gems. 3.0.12 hacks around that inconsistency, and *should* be fully Rails 3-compatible. ### Deprecated: Rails 3 Beta 3 Haml's support for Rails 3.0.0.beta.3 has been deprecated. Haml 3.0.13 will only support 3.0.0.beta.4. ## 3.0.11 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.11). There were no changes made to Haml between versions 3.0.10 and 3.0.11. ## Rails 3 Support Make sure Sass *actually* regenerates stylesheets under Rails 3. The fix in 3.0.10 didn't work because the Rack stack we were modifying wasn't reloaded at the proper time. ## Bug Fixes * Give a decent error message when `--recursive` is used in `sass-convert` without a directory. ## 3.0.10 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.10). ### Appengine-JRuby Support The way we determine the location of the Haml installation no longer breaks the version of JRuby used by [`appengine-jruby`](http://code.google.com/p/appengine-jruby/). ### Rails 3 Support Sass will regenerate stylesheets under Rails 3 even when no controllers are being accessed. ### Other Improvements * When using `sass-convert --from sass2 --to sass --recursive`, suggest the use of `--in-place` as well. ## 3.0.9 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.9). There were no changes made to Sass between versions 3.0.8 and 3.0.9. A bug in Gemcutter caused the gem to be uploaded improperly. ## 3.0.8 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.8). * Fix a bug with Rails versions prior to Rails 3. ## 3.0.7 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.7). ### Encoding Support Sass 3.0.7 adds support for `@charset` for declaring the encoding of a stylesheet. For details see {file:SASS_REFERENCE.md#encodings the reference}. The `sass` and `sass-convert` executables also now take an `-E` option for specifying the encoding of Sass/SCSS/CSS files. ### Bug Fixes * When compiling a file named `.sass` but with SCSS syntax specified, use the latter (and vice versa). * Fix a bug where interpolation would cause some selectors to render improperly. * If a line in a Sass comment starts with `*foo`, render it as `*foo` rather than `* *foo`. ## 3.0.6 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.6). There were no changes made to Sass between versions 3.0.5 and 3.0.6. ## 3.0.5 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.5). ### `#{}` Interpolation in Properties Previously, using `#{}` in some places in properties would cause a syntax error. Now it can be used just about anywhere. Note that when `#{}` is used near operators like `/`, those operators are treated as plain CSS rather than math operators. For example: p { $font-size: 12px; $line-height: 30px; font: #{$font-size}/#{$line-height}; } is compiled to: p { font: 12px/30px; } This is useful, since normally {file:SASS_REFERENCE.md#division-and-slash a slash with variables is treated as division}. ### Recursive Mixins Mixins that include themselves will now print much more informative error messages. For example: @mixin foo {@include bar} @mixin bar {@include foo} @include foo will print: An @include loop has been found: foo includes bar bar includes foo Although it was previously possible to use recursive mixins without causing infinite looping, this is now disallowed, since there's no good reason to do it. ### Rails 3 Support Fix Sass configuration under Rails 3. Thanks [Dan Cheail](http://github.com/codeape). ### `sass --no-cache` Make the `--no-cache` flag properly forbid Sass from writing `.sass-cache` files. ## 3.0.4 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.4). * Raise an informative error when function arguments have a misplaced comma, as in `foo(bar, )`. * Fix a performance problem when using long function names such as `-moz-linear-gradient`. ## 3.0.3 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.3). ### Rails 3 Support Make sure Sass is loaded properly when using Rails 3 along with non-Rails-3-compatible plugins like some versions of `will_paginate`. Also, In order to make some Rails loading errors like the above easier to debug, Sass will now raise an error if `Rails.root` is `nil` when Sass is loading. Previously, this would just cause the paths to be mis-set. ### Merb Support Merb, including 1.1.0 as well as earlier versions, should *really* work with this release. ### Bug Fixes * Raise an informative error when mixin arguments have a misplaced comma, as in `@include foo(bar, )`. * Make sure SassScript subtraction happens even when nothing else dynamic is going on. * Raise an error when colors are used with the wrong number of digits. ## 3.0.2 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.2). ### Merb 1.1.0 Support Fixed a bug inserting the Sass plugin into the Merb 1.1.0 Rack application. ### Bug Fixes * Allow identifiers to begin with multiple underscores. * Don't raise an error when using `haml --rails` with older Rails versions. ## 3.0.1 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.1). ### Installation in Rails `haml --rails` is no longer necessary for installing Sass in Rails. Now all you need to do is add `gem "haml"` to the Gemfile for Rails 3, or add `config.gem "haml"` to `config/environment.rb` for previous versions. `haml --rails` will still work, but it has been deprecated and will print an error message. It will not work in the next version of Sass. ### Rails 3 Beta Integration * Make sure manually importing the Sass Rack plugin still works with Rails, even though it's not necessary now. * Allow Sass to be configured in Rails even when it's being lazy-loaded. ### `:template_location` Methods The {file:SASS_REFERENCE.md#template_location-option `:template_location` option} can be either a String, a Hash, or an Array. This makes it difficult to modify or use with confidence. Thus, three new methods have been added for handling it: * {Sass::Plugin::Configuration#template_location_array Sass::Plugin#template_location_array} -- Returns the template locations and CSS locations formatted as an array. * {Sass::Plugin::Configuration#add_template_location Sass::Plugin#add_template_location} -- Converts the template location option to an array and adds a new location. * {Sass::Plugin::Configuration#remove_template_location Sass::Plugin#remove_template_location} -- Converts the template location option to an array and removes an existing location. ## 3.0.0 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.0). {#3-0-0} [Tagged on GitHub](https://github.com/sass/sass/releases/tag/3.0.0). ### Deprecations -- Must Read! {#3-0-0-deprecations} * Using `=` for SassScript properties and variables is deprecated, and will be removed in Sass 3.2. Use `:` instead. See also [this changelog entry](#3-0-0-sass-script-context) * Because of the above, property values using `:` will be parsed more thoroughly than they were before. Although all valid CSS3 properties as well as most hacks and proprietary syntax should be supported, it's possible that some properties will break. If this happens, please report it to [the Sass mailing list](http://groups.google.com/group/haml). * In addition, setting the default value of variables with `||=` is now deprecated and will be removed in Sass 3.2. Instead, add `!default` to the end of the value. See also [this changelog entry](#3-0-0-default-flag) * The `!` prefix for variables is deprecated, and will be removed in Sass 3.2. Use `$` as a prefix instead. See also [this changelog entry](#3-0-0-dollar-prefix). * The `css2sass` command-line tool has been deprecated, and will be removed in Sass 3.2. Use the new `sass-convert` tool instead. See also [this changelog entry](#3-0-0-sass-convert). * Selector parent references using `&` can now only be used where element names are valid. This is because Sass 3 fully parses selectors to support the new [`@extend` directive](#3-0-0-extend), and it's possible that the `&` could be replaced by an element name. ### SCSS (Sassy CSS) Sass 3 introduces a new syntax known as SCSS which is fully compatible with the syntax of CSS3, while still supporting the full power of Sass. This means that every valid CSS3 stylesheet is a valid SCSS file with the same meaning. In addition, SCSS understands most CSS hacks and vendor-specific syntax, such as [IE's old `filter` syntax](http://msdn.microsoft.com/en-us/library/ms533754%28VS.85%29.aspx). SCSS files use the `.scss` extension. They can import `.sass` files, and vice-versa. Their syntax is fully described in the {file:SASS_REFERENCE.md Sass reference}; if you're already familiar with Sass, though, you may prefer the {file:SCSS_FOR_SASS_USERS.md intro to SCSS for Sass users}. Since SCSS is a much more approachable syntax for those new to Sass, it will be used as the default syntax for the reference, as well as for most other Sass documentation. The indented syntax will continue to be fully supported, however. Sass files can be converted to SCSS using the new `sass-convert` command-line tool. For example: # Convert a Sass file to SCSS $ sass-convert style.sass style.scss **Note that if you're converting a Sass file written for Sass 2**, you should use the `--from sass2` flag. For example: # Convert a Sass file to SCSS $ sass-convert --from sass2 style.sass style.scss # Convert all Sass files to SCSS $ sass-convert --recursive --in-place --from sass2 --to scss stylesheets/ ### Syntax Changes {#3-0-0-syntax-changes} #### SassScript Context {#3-0-0-sass-script-context} The `=` character is no longer required for properties that use SassScript (that is, variables and operations). All properties now use SassScript automatically; this means that `:` should be used instead. Variables should also be set with `:`. For example, what used to be // Indented syntax .page color = 5px + 9px should now be // Indented syntax .page color: 5px + 9px This means that SassScript is now an extension of the CSS3 property syntax. All valid CSS3 properties are valid SassScript, and will compile without modification (some invalid properties work as well, such as Microsoft's proprietary `filter` syntax). This entails a few changes to SassScript to make it fully CSS3-compatible, which are detailed below. This also means that Sass will now be fully parsing all property values, rather than passing them through unchanged to the CSS. Although care has been taken to support all valid CSS3, as well as hacks and proprietary syntax, it's possible that a property that worked in Sass 2 won't work in Sass 3. If this happens, please report it to [the Sass mailing list](http://groups.google.com/group/haml). Note that if `=` is used, SassScript will be interpreted as backwards-compatibly as posssible. In particular, the changes listed below don't apply in an `=` context. The `sass-convert` command-line tool can be used to upgrade Sass files to the new syntax using the `--in-place` flag. For example: # Upgrade style.sass: $ sass-convert --in-place style.sass # Upgrade all Sass files: $ sass-convert --recursive --in-place --from sass2 --to sass stylesheets/ ##### Quoted Strings Quoted strings (e.g. `"foo"`) in SassScript now render with quotes. In addition, unquoted strings are no longer deprecated, and render without quotes. This means that almost all strings that had quotes in Sass 2 should not have quotes in Sass 3. Although quoted strings render with quotes when used with `:`, they do not render with quotes when used with `#{}`. This allows quoted strings to be used for e.g. selectors that are passed to mixins. Strings can be forced to be quoted and unquoted using the new \{Sass::Script::Functions#unquote unquote} and \{Sass::Script::Functions#quote quote} functions. ##### Division and `/` Two numbers separated by a `/` character are allowed as property syntax in CSS, e.g. for the `font` property. SassScript also uses `/` for division, however, which means it must decide what to do when it encounters numbers separated by `/`. For CSS compatibility, SassScript does not perform division by default. However, division will be done in almost all cases where division is intended. In particular, SassScript will perform division in the following three situations: 1. If the value, or any part of it, is stored in a variable. 2. If the value is surrounded by parentheses. 3. If the value is used as part of another arithmetic expression. For example: p font: 10px/8px $width: 1000px width: $width/2 height: (500px/2) margin-left: 5px + 8px/2px is compiled to: p { font: 10px/8px; width: 500px; height: 250px; margin-left: 9px; } ##### Variable Defaults Since `=` is no longer used for variable assignment, assigning defaults to variables with `||=` no longer makes sense. Instead, the `!default` flag should be added to the end of the variable value. This syntax is meant to be similar to CSS's `!important` flag. For example: $var: 12px !default; #### Variable Prefix Character {#3-0-0-dollar-prefix} The Sass variable character has been changed from `!` to the more aesthetically-appealing `$`. For example, what used to be !width = 13px .icon width = !width should now be $width: 13px .icon width: $width The `sass-convert` command-line tool can be used to upgrade Sass files to the new syntax using the `--in-place` flag. For example: # Upgrade style.sass: $ sass-convert --in-place style.sass # Upgrade all Sass files: $ sass-convert --recursive --in-place --from sass2 --to sass stylesheets/ `!` may still be used, but it's deprecated and will print a warning. It will be removed in the next version of Sass, 3.2. #### Variable and Mixin Names SassScript variable and mixin names may now contain hyphens. In fact, they may be any valid CSS3 identifier. For example: $prettiest-color: #542FA9 =pretty-text color: $prettiest-color In order to allow frameworks like [Compass](http://compass-style.org) to use hyphens in variable names while maintaining backwards-compatibility, variables and mixins using hyphens may be referred to with underscores, and vice versa. For example: $prettiest-color: #542FA9 .pretty // Using an underscore instead of a hyphen works color: $prettiest_color #### Single-Quoted Strings SassScript now supports single-quoted strings. They behave identically to double-quoted strings, except that single quotes need to be backslash-escaped and double quotes do not. #### Mixin Definition and Inclusion Sass now supports the `@mixin` directive as a way of defining mixins (like `=`), as well as the `@include` directive as a way of including them (like `+`). The old syntax is *not* deprecated, and the two are fully compatible. For example: @mixin pretty-text color: $prettiest-color a @include pretty-text is the same as: =pretty-text color: $prettiest-color a +pretty-text #### Sass Properties New-style properties (with the colon after the name) in indented syntax now allow whitespace before the colon. For example: foo color : blue #### Sass `@import` The Sass `@import` statement now allows non-CSS files to be specified with quotes, for similarity with the SCSS syntax. For example, `@import "foo.sass"` will now import the `foo.sass` file, rather than compiling to `@import "foo.sass";`. ### `@extend` {#3-0-0-extend} There are often cases when designing a page when one class should have all the styles of another class, as well as its own specific styles. The most common way of handling this is to use both the more general class and the more specific class in the HTML. For example, suppose we have a design for a normal error and also for a serious error. We might write our markup like so:
Oh no! You've been hacked!
And our styles like so: .error { border: 1px #f00; background-color: #fdd; } .seriousError { border-width: 3px; } Unfortunately, this means that we have to always remember to use `.error` with `.seriousError`. This is a maintenance burden, leads to tricky bugs, and can bring non-semantic style concerns into the markup. The `@extend` directive avoids these problems by telling Sass that one selector should inherit the styles of another selector. For example: .error { border: 1px #f00; background-color: #fdd; } .seriousError { @extend .error; border-width: 3px; } This means that all styles defined for `.error` are also applied to `.seriousError`, in addition to the styles specific to `.seriousError`. In effect, everything with class `.seriousError` also has class `.error`. Other rules that use `.error` will work for `.seriousError` as well. For example, if we have special styles for errors caused by hackers: .error.intrusion { background-image: url("/image/hacked.png"); } Then `
` will have the `hacked.png` background image as well. #### How it Works `@extend` works by inserting the extending selector (e.g. `.seriousError`) anywhere in the stylesheet that the extended selector (.e.g `.error`) appears. Thus the example above: .error { border: 1px #f00; background-color: #fdd; } .error.intrusion { background-image: url("/image/hacked.png"); } .seriousError { @extend .error; border-width: 3px; } is compiled to: .error, .seriousError { border: 1px #f00; background-color: #fdd; } .error.intrusion, .seriousError.intrusion { background-image: url("/image/hacked.png"); } .seriousError { border-width: 3px; } When merging selectors, `@extend` is smart enough to avoid unnecessary duplication, so something like `.seriousError.seriousError` gets translated to `.seriousError`. In addition, it won't produce selectors that can't match anything, like `#main#footer`. See also {file:SASS_REFERENCE.md#extend the `@extend` reference documentation}. ### Colors SassScript color values are much more powerful than they were before. Support was added for alpha channels, and most of Chris Eppstein's [compass-colors](http://chriseppstein.github.com/compass-colors) plugin was merged in, providing color-theoretic functions for modifying colors. One of the most interesting of these functions is {Sass::Script::Functions#mix mix}, which mixes two colors together. This provides a much better way of combining colors and creating themes than standard color arithmetic. #### Alpha Channels Sass now supports colors with alpha channels, constructed via the {Sass::Script::Functions#rgba rgba} and {Sass::Script::Functions#hsla hsla} functions. Alpha channels are unaffected by color arithmetic. However, the {Sass::Script::Functions#opacify opacify} and {Sass::Script::Functions#transparentize transparentize} functions allow colors to be made more and less opaque, respectively. Sass now also supports functions that return the values of the {Sass::Script::Functions#red red}, {Sass::Script::Functions#blue blue}, {Sass::Script::Functions#green green}, and {Sass::Script::Functions#alpha alpha} components of colors. #### HSL Colors Sass has many new functions for using the HSL values of colors. For an overview of HSL colors, check out [the CSS3 Spec](http://www.w3.org/TR/css3-color/#hsl-color). All these functions work just as well on RGB colors as on colors constructed with the {Sass::Script::Functions#hsl hsl} function. * The {Sass::Script::Functions#lighten lighten} and {Sass::Script::Functions#darken darken} functions adjust the lightness of a color. * The {Sass::Script::Functions#saturate saturate} and {Sass::Script::Functions#desaturate desaturate} functions adjust the saturation of a color. * The {Sass::Script::Functions#adjust_hue adjust-hue} function adjusts the hue of a color. * The {Sass::Script::Functions#hue hue}, {Sass::Script::Functions#saturation saturation}, and {Sass::Script::Functions#lightness lightness} functions return the corresponding HSL values of the color. * The {Sass::Script::Functions#grayscale grayscale} function converts a color to grayscale. * The {Sass::Script::Functions#complement complement} function returns the complement of a color. ### Other New Functions Several other new functions were added to make it easier to have more flexible arguments to mixins and to enable deprecation of obsolete APIs. * {Sass::Script::Functions#type_of `type-of`} -- Returns the type of a value. * {Sass::Script::Functions#unit `unit`} -- Returns the units associated with a number. * {Sass::Script::Functions#unitless `unitless`} -- Returns whether a number has units or not. * {Sass::Script::Functions#comparable `comparable`} -- Returns whether two numbers can be added or compared. ### Watching for Updates {#3-0-0-watch} The `sass` command-line utility has a new flag: `--watch`. `sass --watch` monitors files or directories for updated Sass files and compiles those files to CSS automatically. This will allow people not using Ruby or [Compass](http://compass-style.org) to use Sass without having to manually recompile all the time. Here's the syntax for watching a directory full of Sass files: sass --watch app/stylesheets:public/stylesheets This will watch every Sass file in `app/stylesheets`. Whenever one of them changes, the corresponding CSS file in `public/stylesheets` will be regenerated. Any files that import that file will be regenerated, too. The syntax for watching individual files is the same: sass --watch style.sass:out.css You can also omit the output filename if you just want it to compile to name.css. For example: sass --watch style.sass This will update `style.css` whenever `style.sass` changes. You can list more than one file and/or directory, and all of them will be watched: sass --watch foo/style:public/foo bar/style:public/bar sass --watch screen.sass print.sass awful-hacks.sass:ie.css sass --watch app/stylesheets:public/stylesheets public/stylesheets/test.sass File and directory watching is accessible from Ruby, using the {Sass::Plugin::Compiler#watch Sass::Plugin#watch} function. #### Bulk Updating Another new flag for the `sass` command-line utility is `--update`. It checks a group of Sass files to see if their CSS needs to be updated, and updates if so. The syntax for `--update` is just like watch: sass --update app/stylesheets:public/stylesheets sass --update style.sass:out.css sass --watch screen.sass print.sass awful-hacks.sass:ie.css In fact, `--update` work exactly the same as `--watch`, except that it doesn't continue watching the files after the first check. ### `sass-convert` (née `css2sass`) {#3-0-0-sass-convert} The `sass-convert` tool, which used to be known as `css2sass`, has been greatly improved in various ways. It now uses a full-fledged CSS3 parser, so it should be able to handle any valid CSS3, as well as most hacks and proprietary syntax. `sass-convert` can now convert between Sass and SCSS. This is normally inferred from the filename, but it can also be specified using the `--from` and `--to` flags. For example: $ generate-sass | sass-convert --from sass --to scss | consume-scss It's also now possible to convert a file in-place -- that is, overwrite the old file with the new file. This is useful for converting files in the [Sass 2 syntax](#3-0-0-deprecations) to the new Sass 3 syntax, e.g. by doing `sass-convert --in-place --from sass2 style.sass`. #### `--recursive` The `--recursive` option allows `sass-convert` to convert an entire directory of files. `--recursive` requires both the `--from` and `--to` flags to be specified. For example: # Convert all .sass files in stylesheets/ to SCSS. # "sass2" means that these files are assumed to use the Sass 2 syntax. $ sass-convert --recursive --from sass2 --to scss stylesheets/ #### `--dasherize` The `--dasherize` options converts all underscores to hyphens, which are now allowed as part of identifiers in Sass. Note that since underscores may still be used in place of hyphens when referring to mixins and variables, this won't cause any backwards-incompatibilities. #### Convert Less to SCSS `sass-convert` can also convert [Less](http://lesscss.org) files to SCSS (or the indented syntax, although I anticipate less interest in that). For example: # Convert all .less files in the current directory into .scss files sass-convert --from less --to scss --recursive . This is done using the Less parser, so it requires that the `less` RubyGem be installed. ##### Incompatibilities Because of the reasonably substantial differences between Sass and Less, there are some things that can't be directly translated, and one feature that can't be translated at all. In the tests I've run on open-source Less stylesheets, none of these have presented issues, but it's good to be aware of them. First, Less doesn't distinguish fully between mixins and selector inheritance. In Less, all classes and some other selectors may be used as mixins, alongside more Sass-like mixins. If a class is being used as a mixin, it may also be used directly in the HTML, so it's not safe to translate it into a Sass mixin. What `sass-convert` does instead is leave the class in the stylesheet as a class, and use {file:SASS_REFERENCE.md#extend `@extend`} rather than {file:SASS_REFERENCE.md#including_a_mixin `@include`} to take on the styles of that class. Although `@extend` and mixins work quite differently, using `@extend` here doesn't actually seem to make a difference in practice. Another issue with Less mixins is that Less allows nested selectors (such as `.body .button` or `.colors > .teal`) to be used as a means of "namespacing" mixins. Sass's `@extend` doesn't work that way, so it does away with the namespacing and just extends the base class (so `.colors > .teal` becomes simply `@extend .teal`). In practice, this feature doesn't seem to be widely-used, but `sass-convert` will print a warning and leave a comment when it encounters it just in case. Finally, Less has the ability to directly access variables and property values defined in other selectors, which Sass does not support. Whenever such an accessor is used, `sass-convert` will print a warning and comment it out in the SCSS output. Like namespaced mixins, though, this does not seem to be a widely-used feature. ### `@warn` Directive A new directive `@warn` has been added that allows Sass libraries to emit warnings. This can be used to issue deprecation warnings, discourage sloppy use of mixins, etc. `@warn` takes a single argument: a SassScript expression that will be displayed on the console along with a stylesheet trace for locating the warning. For example: @mixin blue-text { @warn "The blue-text mixin is deprecated. Use new-blue-text instead."; color: #00f; } Warnings may be silenced with the new `--quiet` command line option, or the corresponding {file:SASS_REFERENCE.md#quiet-option `:quiey` Sass option}. This option will also affect warnings printed by Sass itself. Warnings are off by default in the Rails, Rack, and Merb production environments. ### Sass::Plugin API {Sass::Plugin} now has a large collection of callbacks that allow users to run code when various actions are performed. For example: Sass::Plugin.on_updating_stylesheet do |template, css| puts "#{template} has been compiled to #{css}!" end For a full list of callbacks and usage notes, see the {Sass::Plugin} documentation. {Sass::Plugin} also has a new method, {Sass::Plugin#force_update_stylesheets force_update_stylesheets}. This works just like {Sass::Plugin#update_stylesheets}, except that it doesn't check modification times and doesn't use the cache; all stylesheets are always compiled anew. ### Output Formatting Properties with a value and *also* nested properties are now rendered with the nested properties indented. For example: margin: auto top: 10px bottom: 20px is now compiled to: margin: auto; margin-top: 10px; margin-bottom: 20px; #### `:compressed` Style When the `:compressed` style is used, colors will be output as the minimal possible representation. This means whichever is smallest of the HTML4 color name and the hex representation (shortened to the three-letter version if possible). ### Stylesheet Updating Speed Several caching layers were added to Sass's stylesheet updater. This means that it should run significantly faster. This benefit will be seen by people using Sass in development mode with Rails, Rack, and Merb, as well as people using `sass --watch` from the command line, and to a lesser (but still significant) extent `sass --update`. Thanks to [thedarkone](http://github.com/thedarkone). ### Error Backtraces Numerous bugs were fixed with the backtraces given for Sass errors, especially when importing files and using mixins. All imports and mixins will now show up in the Ruby backtrace, with the proper filename and line number. In addition, when the `sass` executable encounters an error, it now prints the filename where the error occurs, as well as a backtrace of Sass imports and mixins. ### Ruby 1.9 Support * Sass and `css2sass` now produce more descriptive errors when given a template with invalid byte sequences for that template's encoding, including the line number and the offending character. * Sass and `css2sass` now accept Unicode documents with a [byte-order-mark](http://en.wikipedia.org/wiki/Byte_order_mark). ### Firebug Support A new {file:SASS_REFERENCE.md#debug_info-option `:debug_info` option} has been added that emits line-number and filename information to the CSS file in a browser-readable format. This can be used with the new [FireSass Firebug extension](https://addons.mozilla.org/en-US/firefox/addon/103988) to report the Sass filename and line number for generated CSS files. This is also available via the `--debug-info` command-line flag. ### Minor Improvements * If a CSS or Sass function is used that has the name of a color, it will now be parsed as a function rather than as a color. For example, `fuchsia(12)` now renders as `fuchsia(12)` rather than `fuchsia 12`, and `tealbang(12)` now renders as `tealbang(12)` rather than `teal bang(12)`. * The Sass Rails and Merb plugins now use Rack middleware by default. * Haml is now compatible with the [Rip](http://hellorip.com/) package management system. Thanks to [Josh Peek](http://joshpeek.com/). * Indented-syntax `/*` comments may now include `*` on lines beyond the first. * A {file:SASS_REFERENCE.md#read_cache-option `:read_cache`} option has been added to allow the Sass cache to be read from but not written to. * Stylesheets are no longer checked during each request when running tests in Rails. This should speed up some tests significantly. ## 2.2.24 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.24). * Parent references -- the `&` character -- may only be placed at the beginning of simple selector sequences in Sass 3. Placing them elsewhere is deprecated in 2.2.24 and will print a warning. For example, `foo &.bar` is allowed, but `foo .bar&` is not. ## 2.2.23 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.23). * Don't crash when `rake gems` is run in Rails with Sass installed. Thanks to [Florian Frank](http://github.com/flori). * When raising a file-not-found error, add a list of load paths that were checked. * If an import isn't found for a cached Sass file and the {file:SASS_REFERENCE.md#full_exception `:full_exception option`} is enabled, print the full exception rather than raising it. * Fix a bug with a weird interaction with Haml, DataMapper, and Rails 3 that caused some tag helpers to go into infinite recursion. ## 2.2.22 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.22). * Add a railtie so Haml and Sass will be automatically loaded in Rails 3. Thanks to [Daniel Neighman](http://pancakestacks.wordpress.com/). * Make loading the gemspec not crash on read-only filesystems like Heroku's. ## 2.2.21 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.21). * Fix a few bugs in the git-revision-reporting in {Sass::Version#version}. In particular, it will still work if `git gc` has been called recently, or if various files are missing. * Always use `__FILE__` when reading files within the Haml repo in the `Rakefile`. According to [this bug report](http://github.com/carlhuda/bundler/issues/issue/44), this should make Sass work better with Bundler. ## 2.2.20 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.20). * If the cache file for a given Sass file is corrupt because it doesn't have enough content, produce a warning and read the Sass file rather than letting the exception bubble up. This is consistent with other sorts of sassc corruption handling. * Calls to `defined?` shouldn't interfere with Rails' autoloading in very old versions (1.2.x). ## 2.2.19 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.19). [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.18). There were no changes made to Sass between versions 2.2.18 and 2.2.19. ## 2.2.18 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.18). * Use `Rails.env` rather than `RAILS_ENV` when running under Rails 3.0. Thanks to [Duncan Grazier](http://duncangrazier.com/). * Support `:line_numbers` as an alias for {file:SASS_REFERENCE.md#line_numbers-option `:line_comments`}, since that's what the docs have said forever. Similarly, support `--line-numbers` as a command-line option. * Add a `--unix-newlines` flag to all executables for outputting Unix-style newlines on Windows. * Add a {file:SASS_REFERENCE.md#unix_newlines-option `:unix_newlines` option} for {Sass::Plugin} for outputting Unix-style newlines on Windows. * Fix the `--cache-location` flag, which was previously throwing errors. Thanks to [tav](http://tav.espians.com/). * Allow comments at the beginning of the document to have arbitrary indentation, just like comments elsewhere. Similarly, comment parsing is a little nicer than before. ## 2.2.17 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.17). [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.16). * When the {file:SASS_REFERENCE.md#full_exception-option `:full_exception` option} is false, raise the error in Ruby code rather than swallowing it and printing something uninformative. * Fixed error-reporting when something goes wrong when loading Sass using the `sass` executable. This used to raise a NameError because `Sass::SyntaxError` wasn't defined. Now it'll raise the correct exception instead. * Report the filename in warnings about selectors without properties. * `nil` values for Sass options are now ignored, rather than raising errors. * Fix a bug that appears when Plugin template locations have multiple trailing slashes. Thanks to [Jared Grippe](http://jaredgrippe.com/). ### Must Read! * When `@import` is given a filename without an extension, the behavior of rendering a CSS `@import` if no Sass file is found is deprecated. In future versions, `@import foo` will either import the template or raise an error. ## 2.2.16 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.16). * Fixed a bug where modules containing user-defined Sass functions weren't made available when simply included in {Sass::Script::Functions} ({Sass::Script::Functions Functions} needed to be re-included in {Sass::Script::Functions::EvaluationContext Functions::EvaluationContext}). Now the module simply needs to be included in {Sass::Script::Functions}. ## 2.2.15 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.15). * Added {Sass::Script::Value::Color#with} for a way of setting color channels that's easier than manually constructing a new color and is forwards-compatible with alpha-channel colors (to be introduced in Sass 2.4). * Added a missing require in Sass that caused crashes when it was being run standalone. ## 2.2.14 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.14). * All Sass functions now raise explicit errors if their inputs are of the incorrect type. * Allow the SassScript `rgb()` function to take percentages in addition to numerical values. * Fixed a bug where SassScript strings with `#` followed by `#{}` interpolation didn't evaluate the interpolation. ### SassScript Ruby API These changes only affect people defining their own Sass functions using {Sass::Script::Functions}. * `Sass::Script::Color#value` attribute is deprecated. Use {Sass::Script::Value::Color#rgb} instead. The returned array is now frozen as well. * Add an `assert_type` function that's available to {Sass::Script::Functions}. This is useful for typechecking the inputs to functions. ### Rack Support Sass 2.2.14 includes Rack middleware for running Sass, meaning that all Rack-enabled frameworks can now use Sass. To activate this, just add require 'sass/plugin/rack' use Sass::Plugin::Rack to your `config.ru`. See the {Sass::Plugin::Rack} documentation for more details. ## 2.2.13 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.13). There were no changes made to Sass between versions 2.2.12 and 2.2.13. ## 2.2.12 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.12). * Fix a stupid bug introduced in 2.2.11 that broke the Sass Rails plugin. ## 2.2.11 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.11). * Added a note to errors on properties that could be pseudo-classes (e.g. `:focus`) indicating that they should be backslash-escaped. * Automatically interpret properties that could be pseudo-classes as such if {file:SASS_REFERENCE.md.html#property_syntax-option `:property_syntax`} is set to `:new`. * Fixed `css2sass`'s generation of pseudo-classes so that they're backslash-escaped. * Don't crash if the Haml plugin skeleton is installed and `rake gems:install` is run. * Don't use `RAILS_ROOT` directly. This no longer exists in Rails 3.0. Instead abstract this out as `Haml::Util.rails_root`. This changes makes Haml fully compatible with edge Rails as of this writing. * Make use of a Rails callback rather than a monkeypatch to check for stylesheet updates in Rails 3.0+. ## 2.2.10 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.10). * Add support for attribute selectors with spaces around the `=`. For example: a[href = http://google.com] color: blue ## 2.2.9 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.9). There were no changes made to Sass between versions 2.2.8 and 2.2.9. ## 2.2.8 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.8). There were no changes made to Sass between versions 2.2.7 and 2.2.8. ## 2.2.7 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.7). There were no changes made to Sass between versions 2.2.6 and 2.2.7. ## 2.2.6 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.6). * Don't crash when the `__FILE__` constant of a Ruby file is a relative path, as apparently happens sometimes in TextMate (thanks to [Karl Varga](http://github.com/kjvarga)). * Add "Sass" to the `--version` string for the executables. ## 2.2.5 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.5). There were no changes made to Sass between versions 2.2.4 and 2.2.5. ## 2.2.4 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.4). * Don't add `require 'rubygems'` to the top of init.rb when installed via `sass --rails`. This isn't necessary, and actually gets clobbered as soon as haml/template is loaded. * Document the previously-undocumented {file:SASS_REFERENCE.md#line-option `:line` option}, which allows the number of the first line of a Sass file to be set for error reporting. ## 2.2.3 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.3). Sass 2.2.3 prints line numbers for warnings about selectors with no properties. ## 2.2.2 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.2). Sass 2.2.2 is a minor bug-fix release. Notable changes include better parsing of mixin definitions and inclusions and better support for Ruby 1.9. ## 2.2.1 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.1). Sass 2.2.1 is a minor bug-fix release. ### Must Read! * It used to be acceptable to use `-` immediately following variable names, without any whitespace in between (for example, `!foo-!bar`). This is now deprecated, so that in the future variables with hyphens can be supported. Surround `-` with spaces. ## 2.2.0 [Tagged on GitHub](https://github.com/sass/sass/releases/tag/2.2.0). The 2.2 release marks a significant step in the evolution of the Sass language. The focus has been to increase the power of Sass to keep your stylesheets maintainable by allowing new forms of abstraction to be created within your stylesheets and the stylesheets provided by others that you can download and import into your own. The fundamental units of abstraction in Sass are variables and mixins. Please read below for a list of changes: ### Must Read! * Sass Comments (//) used to only comment out a single line. This was deprecated in 2.0.10 and starting in 2.2, Sass comments will comment out any lines indented under them. Upgrade to 2.0.10 in order to see deprecation warnings where this change affects you. * Implicit Strings within SassScript are now deprecated and will be removed in 2.4. For example: `border= !width solid #00F` should now be written as `border: #{!width} solid #00F` or as `border= !width "solid" #00F`. After upgrading to 2.2, you will see deprecation warnings if you have sass files that use implicit strings. ### Sass Syntax Changes #### Flexible Indentation The indentation of Sass documents is now flexible. The first indent that is detected will determine the indentation style for that document. Tabs and spaces may never be mixed, but within a document, you may choose to use tabs or a flexible number of spaces. #### Multiline Sass Comments Sass Comments (//) will now comment out whatever is indented beneath them. Previously they were single line when used at the top level of a document. Upgrading to the latest stable version will give you deprecation warnings if you have silent comments with indentation underneath them. #### Mixin Arguments Sass Mixins now accept any number of arguments. To define a mixin with arguments, specify the arguments as a comma-delimited list of variables like so: =my-mixin(!arg1, !arg2, !arg3) As before, the definition of the mixin is indented below the mixin declaration. The variables declared in the argument list may be used and will be bound to the values passed to the mixin when it is invoked. Trailing arguments may have default values as part of the declaration: =my-mixin(!arg1, !arg2 = 1px, !arg3 = blue) In the example above, the mixin may be invoked by passing 1, 2 or 3 arguments to it. A similar syntax is used to invoke a mixin that accepts arguments: div.foo +my-mixin(1em, 3px) When a mixin has no required arguments, the parenthesis are optional. The default values for mixin arguments are evaluated in the global context at the time when the mixin is invoked, they may also reference the previous arguments in the declaration. For example: !default_width = 30px =my-fancy-mixin(!width = !default_width, !height = !width) width= !width height= !height .default-box +my-fancy-mixin .square-box +my-fancy-mixin(50px) .rectangle-box +my-fancy-mixin(25px, 75px) !default_width = 10px .small-default-box +my-fancy-mixin compiles to: .default-box { width: 30px; height: 30px; } .square-box { width: 50px; height: 50px; } .rectangle-box { width: 25px; height: 75px; } .small-default-box { width: 10px; height: 10px; } ### Sass, Interactive The sass command line option -i now allows you to quickly and interactively experiment with SassScript expressions. The value of the expression you enter will be printed out after each line. Example: $ sass -i >> 5px 5px >> 5px + 10px 15px >> !five_pixels = 5px 5px >> !five_pixels + 10px 15px ### SassScript The features of SassScript have been greatly enhanced with new control directives, new fundamental data types, and variable scoping. #### New Data Types SassScript now has four fundamental data types: 1. Number 2. String 3. Boolean (New in 2.2) 4. Colors #### More Flexible Numbers Like JavaScript, SassScript numbers can now change between floating point and integers. No explicit casting or decimal syntax is required. When a number is emitted into a CSS file it will be rounded to the nearest thousandth, however the internal representation maintains much higher precision. #### Improved Handling of Units While Sass has long supported numbers with units, it now has a much deeper understanding of them. The following are examples of legal numbers in SassScript: 0, 1000, 6%, -2px, 5pc, 20em, or 2foo. Numbers of the same unit may always be added and subtracted. Numbers that have units that Sass understands and finds comparable, can be combined, taking the unit of the first number. Numbers that have non-comparable units may not be added nor subtracted -- any attempt to do so will cause an error. However, a unitless number takes on the unit of the other number during a mathematical operation. For example: >> 3mm + 4cm 43mm >> 4cm + 3mm 4.3cm >> 3cm + 2in 8.08cm >> 5foo + 6foo 11foo >> 4% + 5px SyntaxError: Incompatible units: 'px' and '%'. >> 5 + 10px 15px Sass allows compound units to be stored in any intermediate form, but will raise an error if you try to emit a compound unit into your css file. >> !em_ratio = 1em / 16px 0.063em/px >> !em_ratio * 32px 2em >> !em_ratio * 40px 2.5em #### Colors A color value can be declared using a color name, hexadecimal, shorthand hexadecimal, the rgb function, or the hsl function. When outputting a color into css, the color name is used, if any, otherwise it is emitted as hexadecimal value. Examples: > #fff white >> white white >> #FFFFFF white >> hsl(180, 100, 100) white >> rgb(255, 255, 255) white >> #AAA #aaaaaa Math on color objects is performed piecewise on the rgb components. However, these operations rarely have meaning in the design domain (mostly they make sense for gray-scale colors). >> #aaa + #123 #bbccdd >> #333 * 2 #666666 #### Booleans Boolean objects can be created by comparison operators or via the `true` and `false` keywords. Booleans can be combined using the `and`, `or`, and `not` keywords. >> true true >> true and false false >> 5 < 10 true >> not (5 < 10) false >> not (5 < 10) or not (10 < 5) true >> 30mm == 3cm true >> 1px == 1em false #### Strings Unicode escapes are now allowed within SassScript strings. ### Control Directives New directives provide branching and looping within a sass stylesheet based on SassScript expressions. See the [Sass Reference](SASS_REFERENCE.md.html#control_directives) for complete details. #### @for The `@for` directive loops over a set of numbers in sequence, defining the current number into the variable specified for each loop. The `through` keyword means that the last iteration will include the number, the `to` keyword means that it will stop just before that number. @for !x from 1px through 5px .border-#{!x} border-width= !x compiles to: .border-1px { border-width: 1px; } .border-2px { border-width: 2px; } .border-3px { border-width: 3px; } .border-4px { border-width: 4px; } .border-5px { border-width: 5px; } #### @if / @else if / @else The branching directives `@if`, `@else if`, and `@else` let you select between several branches of sass to be emitted, based on the result of a SassScript expression. Example: !type = "monster" p @if !type == "ocean" color: blue @else if !type == "matador" color: red @else if !type == "monster" color: green @else color: black is compiled to: p { color: green; } #### @while The `@while` directive lets you iterate until a condition is met. Example: !i = 6 @while !i > 0 .item-#{!i} width = 2em * !i !i = !i - 2 is compiled to: .item-6 { width: 12em; } .item-4 { width: 8em; } .item-2 { width: 4em; } ### Variable Scoping The term "constant" has been renamed to "variable." Variables can be declared at any scope (a.k.a. nesting level) and they will only be visible to the code until the next outdent. However, if a variable is already defined in a higher level scope, setting it will overwrite the value stored previously. In this code, the `!local_var` variable is scoped and hidden from other higher level scopes or sibling scopes: .foo .bar !local_var = 1px width= !local_var .baz // this will raise an undefined variable error. width= !local_var // as will this width= !local_var In this example, since the `!global_var` variable is first declared at a higher scope, it is shared among all lower scopes: !global_var = 1px .foo .bar !global_var = 2px width= !global_var .baz width= !global_var width= !global_var compiles to: .foo { width: 2px; } .foo .bar { width: 2px; } .foo .baz { width: 2px; } ### Interpolation Interpolation has been added. This allows SassScript to be used to create dynamic properties and selectors. It also cleans up some uses of dynamic values when dealing with compound properties. Using interpolation, the result of a SassScript expression can be placed anywhere: !x = 1 !d = 3 !property = "border" div.#{!property} #{!property}: #{!x + !d}px solid #{!property}-color: blue is compiled to: div.border { border: 4px solid; border-color: blue; } ### Sass Functions SassScript defines some useful functions that are called using the normal CSS function syntax: p color = hsl(0, 100%, 50%) is compiled to: #main { color: #ff0000; } The following functions are provided: `hsl`, `percentage`, `round`, `ceil`, `floor`, and `abs`. You can define additional functions in ruby. See {Sass::Script::Functions} for more information. ### New Options #### `:line_comments` To aid in debugging, You may set the `:line_comments` option to `true`. This will cause the sass engine to insert a comment before each selector saying where that selector was defined in your sass code. #### `:template_location` The {Sass::Plugin} `:template_location` option now accepts a hash of sass paths to corresponding css paths. Please be aware that it is possible to import sass files between these separate locations -- they are not isolated from each other. ### Miscellaneous Features #### `@debug` Directive The `@debug` directive accepts a SassScript expression and emits the value of that expression to the terminal (stderr). Example: @debug 1px + 2px During compilation the following will be printed: Line 1 DEBUG: 3px #### Ruby 1.9 Support Sass now fully supports Ruby 1.9.1. #### Sass Cache By default, Sass caches compiled templates and [partials](SASS_REFERENCE.md.html#partials). This dramatically speeds up re-compilation of large collections of Sass files, and works best if the Sass templates are split up into separate files that are all [`@import`](SASS_REFERENCE.md.html#import)ed into one large file. Without a framework, Sass puts the cached templates in the `.sass-cache` directory. In Rails and Merb, they go in `tmp/sass-cache`. The directory can be customized with the [`:cache_location`](#cache_location-option) option. If you don't want Sass to use caching at all, set the [`:cache`](#cache-option) option to `false`. ruby-sass-3.7.4/doc-src/SASS_REFERENCE.md000066400000000000000000002372661345125207600175050ustar00rootroot00000000000000# Sass (Syntactically Awesome StyleSheets) Sass is an extension of CSS that adds power and elegance to the basic language. It allows you to use [variables](#variables_), [nested rules](#nested_rules), [mixins](#mixins), [inline imports](#import), and more, all with a fully CSS-compatible syntax. Sass helps keep large stylesheets well-organized, and get small stylesheets up and running quickly, particularly with the help of [the Compass style library](http://compass-style.org). ## Features * Fully CSS-compatible * Language extensions such as variables, nesting, and mixins * Many {Sass::Script::Functions useful functions} for manipulating colors and other values * Advanced features like [control directives](#control_directives__expressions) for libraries * Well-formatted, customizable output ## Syntax There are two syntaxes available for Sass. The first, known as SCSS (Sassy CSS) and used throughout this reference, is an extension of the syntax of CSS. This means that every valid CSS stylesheet is a valid SCSS file with the same meaning. In addition, SCSS understands most CSS hacks and vendor-specific syntax, such as [IE's old `filter` syntax][filter]. This syntax is enhanced with the Sass features described below. Files using this syntax have the `.scss` extension. [filter]: http://msdn.microsoft.com/en-us/library/ms530752.aspx The second and older syntax, known as the indented syntax (or sometimes just "Sass"), provides a more concise way of writing CSS. It uses indentation rather than brackets to indicate nesting of selectors, and newlines rather than semicolons to separate properties. Some people find this to be easier to read and quicker to write than SCSS. The indented syntax has all the same features, although some of them have slightly different syntax; this is described in {file:INDENTED_SYNTAX.md the indented syntax reference}. Files using this syntax have the `.sass` extension. Either syntax can [import](#import) files written in the other. Files can be automatically converted from one syntax to the other using the `sass-convert` command line tool: # Convert Sass to SCSS $ sass-convert style.sass style.scss # Convert SCSS to Sass $ sass-convert style.scss style.sass Note that this command does *not* generate CSS files. For that, use the `sass` command described elsewhere. ## Using Sass Sass can be used in three ways: as a command-line tool, as a standalone Ruby module, and as a plugin for any Rack-enabled framework, including Ruby on Rails and Merb. The first step for all of these is to install the Sass gem: gem install sass If you're using Windows, you may need to [install Ruby](http://rubyinstaller.org/download.html) first. To run Sass from the command line, just use sass input.scss output.css You can also tell Sass to watch the file and update the CSS every time the Sass file changes: sass --watch input.scss:output.css If you have a directory with many Sass files, you can also tell Sass to watch the entire directory: sass --watch app/sass:public/stylesheets Use `sass --help` for full documentation. Using Sass in Ruby code is very simple. After installing the Sass gem, you can use it by running `require "sass"` and using {Sass::Engine} like so: engine = Sass::Engine.new("#main {background-color: #0000ff}", :syntax => :scss) engine.render #=> "#main { background-color: #0000ff; }\n" ### Rack/Rails/Merb Plugin To enable Sass in Rails versions before Rails 3, add the following line to `environment.rb`: config.gem "sass" For Rails 3, instead add the following line to the Gemfile: gem "sass" To enable Sass in Merb, add the following line to `config/dependencies.rb`: dependency "merb-haml" To enable Sass in a Rack application, add the following lines to `config.ru`. require 'sass/plugin/rack' use Sass::Plugin::Rack Sass stylesheets don't work the same as views. They don't contain dynamic content, so the CSS only needs to be generated when the Sass file has been updated. By default, `.sass` and `.scss` files are placed in public/stylesheets/sass (this can be customized with the [`:template_location`](#template_location-option) option). Then, whenever necessary, they're compiled into corresponding CSS files in public/stylesheets. For instance, public/stylesheets/sass/main.scss would be compiled to public/stylesheets/main.css. ### Caching By default, Sass caches compiled templates and [partials](#partials). This dramatically speeds up re-compilation of large collections of Sass files, and works best if the Sass templates are split up into separate files that are all [`@import`](#import)ed into one large file. Without a framework, Sass puts the cached templates in the `.sass-cache` directory. In Rails and Merb, they go in `tmp/sass-cache`. The directory can be customized with the [`:cache_location`](#cache_location-option) option. If you don't want Sass to use caching at all, set the [`:cache`](#cache-option) option to `false`. ### Options Options can be set by setting the {Sass::Plugin::Configuration#options Sass::Plugin#options} hash in `environment.rb` in Rails or `config.ru` in Rack... Sass::Plugin.options[:style] = :compact ...or by setting the `Merb::Plugin.config[:sass]` hash in `init.rb` in Merb... Merb::Plugin.config[:sass][:style] = :compact ...or by passing an options hash to {Sass::Engine#initialize}. All relevant options are also available via flags to the `sass` and `scss` command-line executables. Available options are: * **`:style`**: Sets the style of the CSS output. See [Output Style](#output_style). * **`:syntax`**: The syntax of the input file, `:sass` for the indented syntax and `:scss` for the CSS-extension syntax. This is only useful when you're constructing {Sass::Engine} instances yourself; it's automatically set properly when using {Sass::Plugin}. Defaults to `:sass`. * **`:property_syntax`**: Forces indented-syntax documents to use one syntax for properties. If the correct syntax isn't used, an error is thrown. `:new` forces the use of a colon after the property name. For example: `color: #0f3` or `width: $main_width`. `:old` forces the use of a colon before the property name. For example: `:color #0f3` or `:width $main_width`. By default, either syntax is valid. This has no effect on SCSS documents. * **`:cache`**: Whether parsed Sass files should be cached, allowing greater speed. Defaults to true. * **`:read_cache`**: If this is set and `:cache` is not, only read the Sass cache if it exists, don't write to it if it doesn't. * **`:cache_store`**: If this is set to an instance of a subclass of {Sass::CacheStores::Base}, that cache store will be used to store and retrieve cached compilation results. Defaults to a {Sass::CacheStores::Filesystem} that is initialized using the [`:cache_location` option](#cache_location-option). * **`:never_update`**: Whether the CSS files should never be updated, even if the template file changes. Setting this to true may give small performance gains. It always defaults to false. Only has meaning within Rack, Ruby on Rails, or Merb. * **`:always_update`**: Whether the CSS files should be updated every time a controller is accessed, as opposed to only when the template has been modified. Defaults to false. Only has meaning within Rack, Ruby on Rails, or Merb. * **`:always_check`**: Whether a Sass template should be checked for updates every time a controller is accessed, as opposed to only when the server starts. If a Sass template has been updated, it will be recompiled and will overwrite the corresponding CSS file. Defaults to false in production mode, true otherwise. Only has meaning within Rack, Ruby on Rails, or Merb. * **`:poll`**: When true, always use the polling backend for {Sass::Plugin::Compiler#watch} rather than the native filesystem backend. * **`:full_exception`**: Whether an error in the Sass code should cause Sass to provide a detailed description within the generated CSS file. If set to true, the error will be displayed along with a line number and source snippet both as a comment in the CSS file and at the top of the page (in supported browsers). Otherwise, an exception will be raised in the Ruby code. Defaults to false in production mode, true otherwise. * **`:template_location`**: A path to the root sass template directory for your application. If a hash, `:css_location` is ignored and this option designates a mapping between input and output directories. May also be given a list of 2-element lists, instead of a hash. Defaults to `css_location + "/sass"`. Only has meaning within Rack, Ruby on Rails, or Merb. Note that if multiple template locations are specified, all of them are placed in the import path, allowing you to import between them. **Note that due to the many possible formats it can take, this option should only be set directly, not accessed or modified. Use the {Sass::Plugin::Configuration#template_location_array Sass::Plugin#template_location_array}, {Sass::Plugin::Configuration#add_template_location Sass::Plugin#add_template_location}, and {Sass::Plugin::Configuration#remove_template_location Sass::Plugin#remove_template_location} methods instead**. * **`:css_location`**: The path where CSS output should be written to. This option is ignored when `:template_location` is a Hash. Defaults to `"./public/stylesheets"`. Only has meaning within Rack, Ruby on Rails, or Merb. * **`:cache_location`**: The path where the cached `sassc` files should be written to. Defaults to `"./tmp/sass-cache"` in Rails and Merb, or `"./.sass-cache"` otherwise. If the [`:cache_store` option](#cache_location-option) is set, this is ignored. * **`:unix_newlines`**: If true, use Unix-style newlines when writing files. Only has meaning on Windows, and only when Sass is writing the files (in Rack, Rails, or Merb, when using {Sass::Plugin} directly, or when using the command-line executable). * **`:filename`**: The filename of the file being rendered. This is used solely for reporting errors, and is automatically set when using Rack, Rails, or Merb. * **`:line`**: The number of the first line of the Sass template. Used for reporting line numbers for errors. This is useful to set if the Sass template is embedded in a Ruby file. * **`:load_paths`**: An array of filesystem paths or importers which should be searched for Sass templates imported with the [`@import`](#import) directive. These may be strings, `Pathname` objects, or subclasses of {Sass::Importers::Base}. This defaults to the working directory and, in Rack, Rails, or Merb, whatever `:template_location` is. The load path is also informed by {Sass.load_paths} and the `SASS_PATH` environment variable. * **`:filesystem_importer`**: A {Sass::Importers::Base} subclass used to handle plain string load paths. This should import files from the filesystem. It should be a Class object inheriting from {Sass::Importers::Base} with a constructor that takes a single string argument (the load path). Defaults to {Sass::Importers::Filesystem}. * **`:sourcemap`**: Controls how sourcemaps are generated. These sourcemaps tell the browser how to find the Sass styles that caused each CSS style to be generated. This has three valid values: **`:auto`** uses relative URIs where possible, assuming that that the source stylesheets will be made available on whatever server you're using, and that their relative location will be the same as it is on the local filesystem. If a relative URI is unavailable, a "file:" URI is used instead. **`:file`** always uses "file:" URIs, which will work locally but can't be deployed to a remote server. **`:inline`** includes the full source text in the sourcemap, which is maximally portable but can create very large sourcemap files. Finally, **`:none`** causes no sourcemaps to be generated at all. * **`:line_numbers`**: When set to true, causes the line number and file where a selector is defined to be emitted into the compiled CSS as a comment. Useful for debugging, especially when using imports and mixins. This option may also be called `:line_comments`. Automatically disabled when using the `:compressed` output style or the `:debug_info`/`:trace_selectors` options. * **`:trace_selectors`**: When set to true, emit a full trace of imports and mixins before each selector. This can be helpful for in-browser debugging of stylesheet imports and mixin includes. This option supersedes the `:line_comments` option and is superseded by the `:debug_info` option. Automatically disabled when using the `:compressed` output style. * **`:debug_info`**: When set to true, causes the line number and file where a selector is defined to be emitted into the compiled CSS in a format that can be understood by the browser. Useful in conjunction with [the FireSass Firebug extension](https://addons.mozilla.org/en-US/firefox/addon/103988) for displaying the Sass filename and line number. Automatically disabled when using the `:compressed` output style. * **`:custom`**: An option that's available for individual applications to set to make data available to {Sass::Script::Functions custom Sass functions}. * **`:quiet`**: When set to true, causes warnings to be disabled. [source maps]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?hl=en_US&pli=1&pli=1 ### Syntax Selection The Sass command-line tool will use the file extension to determine which syntax you are using, but there's not always a filename. The `sass` command-line program defaults to the indented syntax but you can pass the `--scss` option to it if the input should be interpreted as SCSS syntax. Alternatively, you can use the `scss` command-line program which is exactly like the `sass` program but it defaults to assuming the syntax is SCSS. ### Encodings When running on Ruby 1.9 and later, Sass is aware of the character encoding of documents. Sass follows the [CSS spec][syntax level 3] to determine the encoding of a stylesheet, and falls back to the Ruby string encoding. This means that it first checks the Unicode byte order mark, then the `@charset` declaration, then the Ruby string encoding. If none of these are set, it will assume the document is in UTF-8. [syntax level 3]: http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#determine-the-fallback-encoding To explicitly specify the encoding of your stylesheet, use a `@charset` declaration just like in CSS. Add `@charset "encoding-name";` at the beginning of the stylesheet (before any whitespace or comments) and Sass will interpret it as the given encoding. Note that whatever encoding you use, it must be convertible to Unicode. Sass will always encode its output as UTF-8. It will include a `@charset` declaration if and only if the output file contains non-ASCII characters. In compressed mode, a UTF-8 byte order mark is used in place of a `@charset` declaration. ## CSS Extensions ### Nested Rules Sass allows CSS rules to be nested within one another. The inner rule then only applies within the outer rule's selector. For example: #main p { color: #00ff00; width: 97%; .redbox { background-color: #ff0000; color: #000000; } } is compiled to: #main p { color: #00ff00; width: 97%; } #main p .redbox { background-color: #ff0000; color: #000000; } This helps avoid repetition of parent selectors, and makes complex CSS layouts with lots of nested selectors much simpler. For example: #main { width: 97%; p, div { font-size: 2em; a { font-weight: bold; } } pre { font-size: 3em; } } is compiled to: #main { width: 97%; } #main p, #main div { font-size: 2em; } #main p a, #main div a { font-weight: bold; } #main pre { font-size: 3em; } ### Referencing Parent Selectors: `&` Sometimes it's useful to use a nested rule's parent selector in other ways than the default. For instance, you might want to have special styles for when that selector is hovered over or for when the body element has a certain class. In these cases, you can explicitly specify where the parent selector should be inserted using the `&` character. For example: a { font-weight: bold; text-decoration: none; &:hover { text-decoration: underline; } body.firefox & { font-weight: normal; } } is compiled to: a { font-weight: bold; text-decoration: none; } a:hover { text-decoration: underline; } body.firefox a { font-weight: normal; } `&` will be replaced with the parent selector as it appears in the CSS. This means that if you have a deeply nested rule, the parent selector will be fully resolved before the `&` is replaced. For example: #main { color: black; a { font-weight: bold; &:hover { color: red; } } } is compiled to: #main { color: black; } #main a { font-weight: bold; } #main a:hover { color: red; } `&` must appear at the beginning of a compound selector, but it can be followed by a suffix that will be added to the parent selector. For example: #main { color: black; &-sidebar { border: 1px solid; } } is compiled to: #main { color: black; } #main-sidebar { border: 1px solid; } If the parent selector can't have a suffix applied, Sass will throw an error. ### Nested Properties CSS has quite a few properties that are in "namespaces;" for instance, `font-family`, `font-size`, and `font-weight` are all in the `font` namespace. In CSS, if you want to set a bunch of properties in the same namespace, you have to type it out each time. Sass provides a shortcut for this: just write the namespace once, then nest each of the sub-properties within it. For example: .funky { font: { family: fantasy; size: 30em; weight: bold; } } is compiled to: .funky { font-family: fantasy; font-size: 30em; font-weight: bold; } The property namespace itself can also have a value. For example: .funky { font: 20px/24px fantasy { weight: bold; } } is compiled to: .funky { font: 20px/24px fantasy; font-weight: bold; } ### Placeholder Selectors: `%foo` Sass supports a special type of selector called a "placeholder selector". These look like class and id selectors, except the `#` or `.` is replaced by `%`. They're meant to be used with the [`@extend` directive](#extend); for more information see [`@extend`-Only Selectors](#placeholders). On their own, without any use of `@extend`, rulesets that use placeholder selectors will not be rendered to CSS. ## Comments: `/* */` and `//` Sass supports standard multiline CSS comments with `/* */`, as well as single-line comments with `//`. The multiline comments are preserved in the CSS output where possible, while the single-line comments are removed. For example: /* This comment is * several lines long. * since it uses the CSS comment syntax, * it will appear in the CSS output. */ body { color: black; } // These comments are only one line long each. // They won't appear in the CSS output, // since they use the single-line comment syntax. a { color: green; } is compiled to: /* This comment is * several lines long. * since it uses the CSS comment syntax, * it will appear in the CSS output. */ body { color: black; } a { color: green; } When the first letter of a multiline comment is `!`, the comment will always rendered into css output even in compressed output modes. This is useful for adding Copyright notices to your generated CSS. Since multiline comments become part of the resulting CSS, interpolation within them is resolved. For example: $version: "1.2.3"; /* This CSS is generated by My Snazzy Framework version #{$version}. */ is compiled to: /* This CSS is generated by My Snazzy Framework version 1.2.3. */ ## SassScript In addition to the plain CSS property syntax, Sass supports a small set of extensions called SassScript. SassScript allows properties to use variables, arithmetic, and extra functions. SassScript can be used in any property value. SassScript can also be used to generate selectors and property names, which is useful when writing [mixins](#mixins). This is done via [interpolation](#interpolation_). ### Interactive Shell You can easily experiment with SassScript using the interactive shell. To launch the shell run the sass command-line with the `-i` option. At the prompt, enter any legal SassScript expression to have it evaluated and the result printed out for you: $ sass -i >> "Hello, Sassy World!" "Hello, Sassy World!" >> 1px + 1px + 1px 3px >> #777 + #777 #eeeeee >> #777 + #888 white ### Variables: `$` The most straightforward way to use SassScript is to use variables. Variables begin with dollar signs, and are set like CSS properties: $width: 5em; You can then refer to them in properties: #main { width: $width; } Variables are only available within the level of nested selectors where they're defined. If they're defined outside of any nested selectors, they're available everywhere. They can also be defined with the `!global` flag, in which case they're also available everywhere. For example: #main { $width: 5em !global; width: $width; } #sidebar { width: $width; } is compiled to: #main { width: 5em; } #sidebar { width: 5em; } For historical reasons, variable names (and all other Sass identifiers) can use hyphens and underscores interchangeably. For example, if you define a variable called `$main-width`, you can access it as `$main_width`, and vice versa. ### Data Types SassScript supports eight data types: * numbers (e.g. `1.2`, `13`, `10px`) * strings of text, with and without quotes (e.g. `"foo"`, `'bar'`, `baz`) * colors (e.g. `blue`, `#04a3f9`, `rgba(255, 0, 0, 0.5)`) * booleans (e.g. `true`, `false`) * nulls (e.g. `null`) * lists of values, separated by spaces or commas (e.g. `1.5em 1em 0 2em`, `Helvetica, Arial, sans-serif`) * maps from one value to another (e.g. `(key1: value1, key2: value2)`) * function references SassScript also supports all other types of CSS property value, such as Unicode ranges and `!important` declarations. However, it has no special handling for these types. They're treated just like unquoted strings. #### Strings CSS specifies two kinds of strings: those with quotes, such as `"Lucida Grande"` or `'https://sass-lang.com'`, and those without quotes, such as `sans-serif` or `bold`. SassScript recognizes both kinds, and in general if one kind of string is used in the Sass document, that kind of string will be used in the resulting CSS. There is one exception to this, though: when using [`#{}` interpolation](#interpolation_), quoted strings are unquoted. This makes it easier to use e.g. selector names in [mixins](#mixins). For example: @mixin firefox-message($selector) { body.firefox #{$selector}:before { content: "Hi, Firefox users!"; } } @include firefox-message(".header"); is compiled to: body.firefox .header:before { content: "Hi, Firefox users!"; } #### Lists Lists are how Sass represents the values of CSS declarations like `margin: 10px 15px 0 0` or `font-face: Helvetica, Arial, sans-serif`. Lists are just a series of other values, separated by either spaces or commas. In fact, individual values count as lists, too: they're just lists with one item. On their own, lists don't do much, but the [SassScript list functions](Sass/Script/Functions.html#list-functions) make them useful. The {Sass::Script::Functions#nth `nth` function} can access items in a list, the {Sass::Script::Functions#join `join` function} can join multiple lists together, and the {Sass::Script::Functions#append `append` function} can add items to lists. The [`@each` directive](#each-directive) can also add styles for each item in a list. In addition to containing simple values, lists can contain other lists. For example, `1px 2px, 5px 6px` is a two-item list containing the list `1px 2px` and the list `5px 6px`. If the inner lists have the same separator as the outer list, you'll need to use parentheses to make it clear where the inner lists start and stop. For example, `(1px 2px) (5px 6px)` is also a two-item list containing the list `1px 2px` and the list `5px 6px`. The difference is that the outer list is space-separated, where before it was comma-separated. When lists are turned into plain CSS, Sass doesn't add any parentheses, since CSS doesn't understand them. That means that `(1px 2px) (5px 6px)` and `1px 2px 5px 6px` will look the same when they become CSS. However, they aren't the same when they're Sass: the first is a list containing two lists, while the second is a list containing four numbers. Lists can also have no items in them at all. These lists are represented as `()` (which is also an empty [map](#maps)). They can't be output directly to CSS; if you try to do e.g. `font-family: ()`, Sass will raise an error. If a list contains empty lists or null values, as in `1px 2px () 3px` or `1px 2px null 3px`, the empty lists and null values will be removed before the containing list is turned into CSS. Comma-separated lists may have a trailing comma. This is especially useful because it allows you to represent a single-element list. For example, `(1,)` is a list containing `1` and `(1 2 3,)` is a comma-separated list containing a space-separated list containing `1`, `2`, and `3`. ##### Bracketed Lists Lists can also be written with square brackets—we call these bracketed lists. Bracketed lists containing are used as line names in [CSS Grid Layout][], but they can also be used in pure Sass code just like any other list. Bracketed lists can be comma- or space-separated. [CSS Grid Layout]: https://www.w3.org/TR/css-grid-1/ #### Maps Maps represent an association between keys and values, where keys are used to look up values. They make it easy to collect values into named groups and access those groups dynamically. They have no direct parallel in CSS, although they're syntactically similar to media query expressions: $map: (key1: value1, key2: value2, key3: value3); Unlike lists, maps must always be surrounded by parentheses and must always be comma-separated. Both the keys and values in maps can be any SassScript object. A map may only have one value associated with a given key (although that value may be a list). A given value may be associated with many keys, though. Like lists, maps are mostly manipulated using [SassScript functions](Sass/Script/Functions.html#map-functions). The {Sass::Script::Functions#map-get `map-get` function} looks up values in a map and the {Sass::Script::Functions#map-merge `map-merge` function} adds values to a map. The [`@each` directive](#each-multi-assign) can be used to add styles for each key/value pair in a map. The order of pairs in a map is always the same as when the map was created. Maps can also be used anywhere lists can. When used by a list function, a map is treated as a list of pairs. For example, `(key1: value1, key2: value2)` would be treated as the nested list `key1 value1, key2 value2` by list functions. Lists cannot be treated as maps, though, with the exception of the empty list. `()` represents both a map with no key/value pairs and a list with no elements. Note that map keys can be any Sass data type (even another map) and the syntax for declaring a map allows arbitrary SassScript expressions that will be evaluated to determine the key. Maps cannot be converted to plain CSS. Using one as the value of a variable or an argument to a CSS function will cause an error. Use the `inspect($value)` function to produce an output string useful for debugging maps. #### Colors Any CSS color expression returns a SassScript Color value. This includes [a large number of named colors](https://github.com/nex3/sass/blob/stable/lib/sass/script/value/color.rb#L28-L180) which are indistinguishable from unquoted strings. In compressed output mode, Sass will output the smallest CSS representation of a color. For example, `#FF0000` will output as `red` in compressed mode, but `blanchedalmond` will output as `#FFEBCD`. A common issue users encounter with named colors is that since Sass prefers the same output format as was typed in other output modes, a color interpolated into a selector becomes invalid syntax when compressed. To avoid this, always quote named colors if they are meant to be used in the construction of a selector. #### First Class Functions A function reference is returned by `get-function($function-name)`. The function can be passed to `call($function, $args...)` and the function it refers to will be invoked. First class functions cannot be used directly as CSS output and any attempt to do so will result in an error. ### Operations All types support equality operations (`==` and `!=`). In addition, each type has its own operations that it has special support for. #### Number Operations SassScript supports the standard arithmetic operations on numbers (addition `+`, subtraction `-`, multiplication `*`, division `/`, and modulo `%`). Sass math functions preserve units during arithmetic operations. This means that, just like in real life, you cannot work on numbers with incompatible units (such as adding a number with `px` and `em`) and two numbers with the same unit that are multiplied together will produce square units (`10px * 10px == 100px * px`). **Be Aware** that `px * px` is an invalid CSS unit and you will get an error from Sass for attempting to use invalid units in CSS. Relational operators (`<`, `>`, `<=`, `>=`) are also supported for numbers, and equality operators (`==`, `!=`) are supported for all types. ##### Division and `/` CSS allows `/` to appear in property values as a way of separating numbers. Since SassScript is an extension of the CSS property syntax, it must support this, while also allowing `/` to be used for division. This means that by default, if two numbers are separated by `/` in SassScript, then they will appear that way in the resulting CSS. However, there are three situations where the `/` will be interpreted as division. These cover the vast majority of cases where division is actually used. They are: 1. If the value, or any part of it, is stored in a variable or returned by a function. 2. If the value is surrounded by parentheses, unless those parentheses are outside a list and the value is inside. 3. If the value is used as part of another arithmetic expression. For example: p { font: 10px/8px; // Plain CSS, no division $width: 1000px; width: $width/2; // Uses a variable, does division width: round(1.5)/2; // Uses a function, does division height: (500px/2); // Uses parentheses, does division margin-left: 5px + 8px/2px; // Uses +, does division font: (italic bold 10px/8px); // In a list, parentheses don't count } is compiled to: p { font: 10px/8px; width: 500px; height: 250px; margin-left: 9px; } If you want to use variables along with a plain CSS `/`, you can use `#{}` to insert them. For example: p { $font-size: 12px; $line-height: 30px; font: #{$font-size}/#{$line-height}; } is compiled to: p { font: 12px/30px; } ##### Subtraction, Negative Numbers, and `-` There are a number of different things `-` can mean in CSS and in Sass. It can be a subtraction operator (as in `5px - 3px`), the beginning of a negative number (as in `-3px`), a unary negation operator (as in `-$var`), or part of an identifier (as in `font-weight`). Most of the time, it's clear which is which, but there are some tricky cases. As a general rule, you're safest if: * You always include spaces on both sides of `-` when subtracting. * You include a space before `-` but not after for a negative number or a unary negation. * You wrap a unary negation in parentheses if it's in a space-separated list, as in `10px (-$var)`. The different meanings of `-` take precedence in the following order: 1. A `-` as part of an identifier. This means that `a-1` is an unquoted string with value `"a-1"`. The only exception are units; Sass normally allows any valid identifier to be used as an identifier, but identifiers may not contain a hyphen followed by a digit. This means that `5px-3px` is the same as `5px - 3px`. 2. A `-` between two numbers with no whitespace. This indicates subtraction, so `1-2` is the same as `1 - 2`. 3. A `-` at the beginning of a literal number. This indicates a negative number, so `1 -2` is a list containing `1` and `-2`. 4. A `-` between two numbers regardless of whitespace. This indicates subtraction, so `1 -$var` are the same as `1 - $var`. 5. A `-` before a value. This indicates the unary negation operator; that is, the operator that takes a number and returns its negative. #### Color Operations Older versions of Sass supported arithmetic operations for color values, where they worked separately for each color channel. This means that the operation is performed on the red, green, and blue channels in turn. For example: p { color: #010203 + #040506; } computes `01 + 04 = 05`, `02 + 05 = 07`, and `03 + 06 = 09`, and is compiled to: p { color: #050709; } However, these operations rarely corresponded with any human understandings of how colors in practice. They were deprecated and are no longer supported in recent versions of Sass. Stylesheets should use {Sass::Script::Functions color functions} instead to manipulate colors. #### String Operations The `+` operation can be used to concatenate strings: p { cursor: e + -resize; } is compiled to: p { cursor: e-resize; } Note that if a quoted string is added to an unquoted string (that is, the quoted string is to the left of the `+`), the result is a quoted string. Likewise, if an unquoted string is added to a quoted string (the unquoted string is to the left of the `+`), the result is an unquoted string. For example: p:before { content: "Foo " + Bar; font-family: sans- + "serif"; } is compiled to: p:before { content: "Foo Bar"; font-family: sans-serif; } By default, if two values are placed next to one another, they are concatenated with a space: p { margin: 3px + 4px auto; } is compiled to: p { margin: 7px auto; } Within a string of text, #{} style interpolation can be used to place dynamic values within the string: p:before { content: "I ate #{5 + 10} pies!"; } is compiled to: p:before { content: "I ate 15 pies!"; } Null values are treated as empty strings for string interpolation: $value: null; p:before { content: "I ate #{$value} pies!"; } is compiled to: p:before { content: "I ate pies!"; } #### Boolean Operations SassScript supports `and`, `or`, and `not` operators for boolean values. #### List Operations Lists don't support any special operations. Instead, they're manipulated using the [list functions](Sass/Script/Functions.html#list-functions). ### Parentheses Parentheses can be used to affect the order of operations: p { width: 1em + (2em * 3); } is compiled to: p { width: 7em; } ### Functions SassScript defines some useful functions that are called using the normal CSS function syntax: p { color: hsl(0, 100%, 50%); } is compiled to: p { color: #ff0000; } See {Sass::Script::Functions this page} for a full list of available functions. #### Keyword Arguments Sass functions can also be called using explicit keyword arguments. The above example can also be written as: p { color: hsl($hue: 0, $saturation: 100%, $lightness: 50%); } While this is less concise, it can make the stylesheet easier to read. It also allows functions to present more flexible interfaces, providing many arguments without becoming difficult to call. Named arguments can be passed in any order, and arguments with default values can be omitted. Since the named arguments are variable names, underscores and dashes can be used interchangeably. See {Sass::Script::Functions} for a full listing of Sass functions and their argument names, as well as instructions on defining your own in Ruby. ### Interpolation: `#{}` You can also use SassScript variables in selectors and property names using `#{}` interpolation syntax: $name: foo; $attr: border; p.#{$name} { #{$attr}-color: blue; } is compiled to: p.foo { border-color: blue; } It's also possible to use `#{}` to put SassScript into property values. In most cases this isn't any better than using a variable, but using `#{}` does mean that any operations near it will be treated as plain CSS. For example: p { $font-size: 12px; $line-height: 30px; font: #{$font-size}/#{$line-height}; } is compiled to: p { font: 12px/30px; } ### `&` in SassScript Just like when it's used [in selectors](#parent-selector), `&` in SassScript refers to the current parent selector. It's a comma-separated list of space-separated lists. For example: .foo.bar .baz.bang, .bip.qux { $selector: &; } The value of `$selector` is now `((".foo.bar" ".baz.bang"), ".bip.qux")`. The compound selectors are quoted here to indicate that they're strings, but in reality they would be unquoted. Even if the parent selector doesn't contain a comma or a space, `&` will always have two levels of nesting, so it can be accessed consistently. If there is no parent selector, the value of `&` will be null. This means you can use it in a mixin to detect whether a parent selector exists: @mixin does-parent-exist { @if & { &:hover { color: red; } } @else { a { color: red; } } } ### Variable Defaults: `!default` You can assign to variables if they aren't already assigned by adding the `!default` flag to the end of the value. This means that if the variable has already been assigned to, it won't be re-assigned, but if it doesn't have a value yet, it will be given one. For example: $content: "First content"; $content: "Second content?" !default; $new_content: "First time reference" !default; #main { content: $content; new-content: $new_content; } is compiled to: #main { content: "First content"; new-content: "First time reference"; } Variables with `null` values are treated as unassigned by !default: $content: null; $content: "Non-null content" !default; #main { content: $content; } is compiled to: #main { content: "Non-null content"; } ## `@`-Rules and Directives Sass supports all CSS3 `@`-rules, as well as some additional Sass-specific ones known as "directives." These have various effects in Sass, detailed below. See also [control directives](#control_directives) and [mixin directives](#mixins). ### `@import` Sass extends the CSS `@import` rule to allow it to import SCSS and Sass files. All imported SCSS and Sass files will be merged together into a single CSS output file. In addition, any variables or [mixins](#mixins) defined in imported files can be used in the main file. Sass looks for other Sass files in the current directory, and the Sass file directory under Rack, Rails, or Merb. Additional search directories may be specified using the [`:load_paths`](#load_paths-option) option, or the `--load-path` option on the command line. `@import` takes a filename to import. By default, it looks for a Sass file to import directly, but there are a few circumstances under which it will compile to a CSS `@import` rule: * If the file's extension is `.css`. * If the filename begins with `http://`. * If the filename is a `url()`. * If the `@import` has any media queries. If none of the above conditions are met and the extension is `.scss` or `.sass`, then the named Sass or SCSS file will be imported. If there is no extension, Sass will try to find a file with that name and the `.scss` or `.sass` extension and import it. For example, @import "foo.scss"; or @import "foo"; would both import the file `foo.scss`, whereas @import "foo.css"; @import "foo" screen; @import "http://foo.com/bar"; @import url(foo); would all compile to @import "foo.css"; @import "foo" screen; @import "http://foo.com/bar"; @import url(foo); It's also possible to import multiple files in one `@import`. For example: @import "rounded-corners", "text-shadow"; would import both the `rounded-corners` and the `text-shadow` files. Imports may contain `#{}` interpolation, but only with certain restrictions. It's not possible to dynamically import a Sass file based on a variable; interpolation is only for CSS imports. As such, it only works with `url()` imports. For example: $family: unquote("Droid+Sans"); @import url("http://fonts.googleapis.com/css?family=#{$family}"); would compile to @import url("http://fonts.googleapis.com/css?family=Droid+Sans"); #### Partials If you have a SCSS or Sass file that you want to import but don't want to compile to a CSS file, you can add an underscore to the beginning of the filename. This will tell Sass not to compile it to a normal CSS file. You can then import these files without using the underscore. For example, you might have `_colors.scss`. Then no `_colors.css` file would be created, and you can do @import "colors"; and `_colors.scss` would be imported. Note that you may not include a partial and a non-partial with the same name in the same directory. For example, `_colors.scss` may not exist alongside `colors.scss`. #### Index Files If you write a file with the special name `_index.scss` or `_index.sass`, it will be loaded if you import the directory that contains it. For example, if you have `dir/_index.scss`, you can write `@import "dir";` and it will load your file. However, if you have a file named `_dir.scss` *and* a file named `dir/_index.scss`, `_dir.scss` will take precedence. #### Nested `@import` Although most of the time it's most useful to just have `@import`s at the top level of the document, it is possible to include them within CSS rules and `@media` rules. Like a base-level `@import`, this includes the contents of the `@import`ed file. However, the imported rules will be nested in the same place as the original `@import`. For example, if `example.scss` contains .example { color: red; } then #main { @import "example"; } would compile to #main .example { color: red; } Directives that are only allowed at the base level of a document, like `@mixin` or `@charset`, are not allowed in files that are `@import`ed in a nested context. It's not possible to nest `@import` within mixins or control directives. ### `@media` `@media` directives in Sass behave just like they do in plain CSS, with one extra capability: they can be nested in CSS rules. If a `@media` directive appears within a CSS rule, it will be bubbled up to the top level of the stylesheet, putting all the selectors on the way inside the rule. This makes it easy to add media-specific styles without having to repeat selectors or break the flow of the stylesheet. For example: .sidebar { width: 300px; @media screen and (orientation: landscape) { width: 500px; } } is compiled to: .sidebar { width: 300px; } @media screen and (orientation: landscape) { .sidebar { width: 500px; } } `@media` queries can also be nested within one another. The queries will then be combined using the `and` operator. For example: @media screen { .sidebar { @media (orientation: landscape) { width: 500px; } } } is compiled to: @media screen and (orientation: landscape) { .sidebar { width: 500px; } } Finally, `@media` queries can contain SassScript expressions (including variables, functions, and operators) in place of the feature names and feature values. For example: $media: screen; $feature: -webkit-min-device-pixel-ratio; $value: 1.5; @media #{$media} and ($feature: $value) { .sidebar { width: 500px; } } is compiled to: @media screen and (-webkit-min-device-pixel-ratio: 1.5) { .sidebar { width: 500px; } } ### `@extend` There are often cases when designing a page when one class should have all the styles of another class, as well as its own specific styles. The most common way of handling this is to use both the more general class and the more specific class in the HTML. For example, suppose we have a design for a normal error and also for a serious error. We might write our markup like so:
Oh no! You've been hacked!
And our styles like so: .error { border: 1px #f00; background-color: #fdd; } .seriousError { border-width: 3px; } Unfortunately, this means that we have to always remember to use `.error` with `.seriousError`. This is a maintenance burden, leads to tricky bugs, and can bring non-semantic style concerns into the markup. The `@extend` directive avoids these problems by telling Sass that one selector should inherit the styles of another selector—in other words, that all elements that match one selector should be styled as though they also match the other selector. For example: .error { border: 1px #f00; background-color: #fdd; } .seriousError { @extend .error; border-width: 3px; } is compiled to: .error, .seriousError { border: 1px #f00; background-color: #fdd; } .seriousError { border-width: 3px; } This means that all styles defined for `.error` are also applied to `.seriousError`, in addition to the styles specific to `.seriousError`. Think of it as a shorthand that lets you write `class="seriousError"` instead of `class="error seriousError"`. Other rules that use `.error` will work for `.seriousError` as well. For example, if we have special styles for errors caused by hackers: .error.intrusion { background-image: url("/image/hacked.png"); } Then `
` will have the `hacked.png` background image as well. #### How it Works `@extend` works by inserting the extending selector (e.g. `.seriousError`) anywhere in the stylesheet that the extended selector (.e.g `.error`) appears. Thus the example above: .error { border: 1px #f00; background-color: #fdd; } .error.intrusion { background-image: url("/image/hacked.png"); } .seriousError { @extend .error; border-width: 3px; } is compiled to: .error, .seriousError { border: 1px #f00; background-color: #fdd; } .error.intrusion, .seriousError.intrusion { background-image: url("/image/hacked.png"); } .seriousError { border-width: 3px; } When merging selectors, `@extend` is smart enough to avoid unnecessary duplication, so something like `.seriousError.seriousError` gets translated to `.seriousError`. In addition, it won't produce selectors that can't match anything, like `#main#footer`. #### Multiple Extends A single selector can extend more than one selector. This means that it inherits the styles of all the extended selectors. For example: .error { border: 1px #f00; background-color: #fdd; } .attention { font-size: 3em; background-color: #ff0; } .seriousError { @extend .error; @extend .attention; border-width: 3px; } is compiled to: .error, .seriousError { border: 1px #f00; background-color: #fdd; } .attention, .seriousError { font-size: 3em; background-color: #ff0; } .seriousError { border-width: 3px; } In effect, every element with class `.seriousError` also has class `.error` *and* class `.attention`. Thus, the styles defined later in the document take precedence: `.seriousError` has background color `#ff0` rather than `#fdd`, since `.attention` is defined later than `.error`. Multiple extends can also be written using a comma-separated list of selectors. For example, `@extend .error, .attention` is the same as `@extend .error; @extend .attention`. #### Chaining Extends It's possible for one selector to extend another selector that in turn extends a third. For example: .error { border: 1px #f00; background-color: #fdd; } .seriousError { @extend .error; border-width: 3px; } .criticalError { @extend .seriousError; position: fixed; top: 10%; bottom: 10%; left: 10%; right: 10%; } Now everything with class `.seriousError` also has class `.error`, and everything with class `.criticalError` has class `.seriousError` *and* class `.error`. It's compiled to: .error, .seriousError, .criticalError { border: 1px #f00; background-color: #fdd; } .seriousError, .criticalError { border-width: 3px; } .criticalError { position: fixed; top: 10%; bottom: 10%; left: 10%; right: 10%; } #### Selector Sequences Selector sequences, such as `.foo .bar` or `.foo + .bar`, currently can't be extended. However, it is possible for nested selectors themselves to use `@extend`. For example: #fake-links .link { @extend a; } a { color: blue; &:hover { text-decoration: underline; } } is compiled to a, #fake-links .link { color: blue; } a:hover, #fake-links .link:hover { text-decoration: underline; } ##### Merging Selector Sequences Sometimes a selector sequence extends another selector that appears in another sequence. In this case, the two sequences need to be merged. For example: #admin .tabbar a { font-weight: bold; } #demo .overview .fakelink { @extend a; } While it would technically be possible to generate all selectors that could possibly match either sequence, this would make the stylesheet far too large. The simple example above, for instance, would require ten selectors. Instead, Sass generates only selectors that are likely to be useful. When the two sequences being merged have no selectors in common, then two new selectors are generated: one with the first sequence before the second, and one with the second sequence before the first. For example: #admin .tabbar a { font-weight: bold; } #demo .overview .fakelink { @extend a; } is compiled to: #admin .tabbar a, #admin .tabbar #demo .overview .fakelink, #demo .overview #admin .tabbar .fakelink { font-weight: bold; } If the two sequences do share some selectors, then those selectors will be merged together and only the differences (if any still exist) will alternate. In this example, both sequences contain the id `#admin`, so the resulting selectors will merge those two ids: #admin .tabbar a { font-weight: bold; } #admin .overview .fakelink { @extend a; } This is compiled to: #admin .tabbar a, #admin .tabbar .overview .fakelink, #admin .overview .tabbar .fakelink { font-weight: bold; } #### `@extend`-Only Selectors Sometimes you'll write styles for a class that you only ever want to `@extend`, and never want to use directly in your HTML. This is especially true when writing a Sass library, where you may provide styles for users to `@extend` if they need and ignore if they don't. If you use normal classes for this, you end up creating a lot of extra CSS when the stylesheets are generated, and run the risk of colliding with other classes that are being used in the HTML. That's why Sass supports "placeholder selectors" (for example, `%foo`). Placeholder selectors look like class and id selectors, except the `#` or `.` is replaced by `%`. They can be used anywhere a class or id could, and on their own they prevent rulesets from being rendered to CSS. For example: // This ruleset won't be rendered on its own. #context a%extreme { color: blue; font-weight: bold; font-size: 2em; } However, placeholder selectors can be extended, just like classes and ids. The extended selectors will be generated, but the base placeholder selector will not. For example: .notice { @extend %extreme; } Is compiled to: #context a.notice { color: blue; font-weight: bold; font-size: 2em; } #### The `!optional` Flag Normally when you extend a selector, it's an error if that `@extend` doesn't work. For example, if you write `a.important {@extend .notice}`, it's an error if there are no selectors that contain `.notice`. It's also an error if the only selector containing `.notice` is `h1.notice`, since `h1` conflicts with `a` and so no new selector would be generated. Sometimes, though, you want to allow an `@extend` not to produce any new selectors. To do so, just add the `!optional` flag after the selector. For example: a.important { @extend .notice !optional; } #### `@extend` in Directives There are some restrictions on the use of `@extend` within directives such as `@media`. Sass is unable to make CSS rules outside of the `@media` block apply to selectors inside it without creating a huge amount of stylesheet bloat by copying styles all over the place. This means that if you use `@extend` within `@media` (or other CSS directives), you may only extend selectors that appear within the same directive block. For example, the following works fine: @media print { .error { border: 1px #f00; background-color: #fdd; } .seriousError { @extend .error; border-width: 3px; } } But this is an error: .error { border: 1px #f00; background-color: #fdd; } @media print { .seriousError { // INVALID EXTEND: .error is used outside of the "@media print" directive @extend .error; border-width: 3px; } } Someday we hope to have `@extend` supported natively in the browser, which will allow it to be used within `@media` and other directives. #### Extending Compound Selectors Older versions of Sass allowed compound selectors, such as `.special.cool` or `a:hover`, to be extended. Only style rules containing *all* simple selectors would be extended. However, this violated the rule that the elements matching the style rule should be styled as though it matched the extended selector. For example, .neat { @extend .special; } means that all elements with `class="neat"` should be styled as though they had `class="neat special"`, so .neat { @extend .special.cool; } *should mean* that all elements with `class="neat"` should be styled as though they had `class="neat special cool"`. But that's not how it actually worked. They were instead styled in a way that was impossible to achieve with pure HTML, which was inconsistent, violated guarantees that CSS usually provides, and was very expensive to implement leading to slow compile times for stylesheets with many `@extend`s. So the old behavior was deprecated and is no longer supported in the most recent Sass releases. Most old stylesheets that extend complex selectors can be updated to extend both simple selectors individually, as in: .neat { @extend .special, .cool; } This doesn't do *exactly* the same thing, but it usually works. If that's not sufficient, you can use a [placeholder selector](#placeholder_selectors_foo) to refer to both selectors at once: .special.cool { @extend %special-cool; } .neat { @extend %special-cool; } ### `@at-root` The `@at-root` directive causes one or more rules to be emitted at the root of the document, rather than being nested beneath their parent selectors. It can either be used with a single inline selector: .parent { ... @at-root .child { ... } } Which would produce: .parent { ... } .child { ... } Or it can be used with a block containing multiple selectors: .parent { ... @at-root { .child1 { ... } .child2 { ... } } .step-child { ... } } Which would output the following: .parent { ... } .child1 { ... } .child2 { ... } .parent .step-child { ... } #### `@at-root (without: ...)` and `@at-root (with: ...)` By default, `@at-root` just excludes selectors. However, it's also possible to use `@at-root` to move outside of nested directives such as `@media` as well. For example: @media print { .page { width: 8in; @at-root (without: media) { color: red; } } } produces: @media print { .page { width: 8in; } } .page { color: red; } You can use `@at-root (without: ...)` to move outside of any directive. You can also do it with multiple directives separated by a space: `@at-root (without: media supports)` moves outside of both `@media` and `@supports` queries. There are two special values you can pass to `@at-root`. "rule" refers to normal CSS rules; `@at-root (without: rule)` is the same as `@at-root` with no query. `@at-root (without: all)` means that the styles should be moved outside of *all* directives and CSS rules. If you want to specify which directives or rules to include, rather than listing which ones should be excluded, you can use `with` instead of `without`. For example, `@at-root (with: rule)` will move outside of all directives, but will preserve any CSS rules. ### `@debug` The `@debug` directive prints the value of a SassScript expression to the standard error output stream. It's useful for debugging Sass files that have complicated SassScript going on. For example: @debug 10em + 12em; outputs: Line 1 DEBUG: 22em ### `@warn` The `@warn` directive prints the value of a SassScript expression to the standard error output stream. It's useful for libraries that need to warn users of deprecations or recovering from minor mixin usage mistakes. There are two major distinctions between `@warn` and `@debug`: 1. You can turn warnings off with the `--quiet` command-line option or the `:quiet` Sass option. 2. A stylesheet trace will be printed out along with the message so that the user being warned can see where their styles caused the warning. Usage Example: @mixin adjust-location($x, $y) { @if unitless($x) { @warn "Assuming #{$x} to be in pixels"; $x: 1px * $x; } @if unitless($y) { @warn "Assuming #{$y} to be in pixels"; $y: 1px * $y; } position: relative; left: $x; top: $y; } ### `@error` The `@error` directive throws the value of a SassScript expression as a fatal error, including a nice stack trace. It's useful for validating arguments to mixins and functions. For example: @mixin adjust-location($x, $y) { @if unitless($x) { @error "$x may not be unitless, was #{$x}."; } @if unitless($y) { @error "$y may not be unitless, was #{$y}."; } position: relative; left: $x; top: $y; } There is currently no way to catch errors. ## Control Directives & Expressions SassScript supports basic control directives and expressions for including styles only under some conditions or including the same style several times with variations. **Note:** Control directives are an advanced feature, and are uncommon in day-to-day styling. They exist mainly for use in [mixins](#mixins), particularly those that are part of libraries like [Compass](http://compass-style.org), and so require substantial flexibility. ### `if()` The built-in `if()` function allows you to branch on a condition and returns only one of two possible outcomes. It can be used in any script context. The `if` function only evaluates the argument corresponding to the one that it will return -- this allows you to refer to variables that may not be defined or to have calculations that would otherwise cause an error (E.g. divide by zero). if(true, 1px, 2px) => 1px if(false, 1px, 2px) => 2px ### `@if` The `@if` directive takes a SassScript expression and uses the styles nested beneath it if the expression returns anything other than `false` or `null`: p { @if 1 + 1 == 2 { border: 1px solid; } @if 5 < 3 { border: 2px dotted; } @if null { border: 3px double; } } is compiled to: p { border: 1px solid; } You can explicitly test for `$var == false` or `$var == null` if you want to distinguish between these. The `@if` statement can be followed by several `@else if` statements and one `@else` statement. If the `@if` statement fails, the `@else if` statements are tried in order until one succeeds or the `@else` is reached. For example: $type: monster; p { @if $type == ocean { color: blue; } @else if $type == matador { color: red; } @else if $type == monster { color: green; } @else { color: black; } } is compiled to: p { color: green; } ### `@for` The `@for` directive repeatedly outputs a set of styles. For each repetition, a counter variable is used to adjust the output. The directive has two forms: `@for $var from through ` and `@for $var from to `. Note the difference in the keywords `through` and `to`. `$var` can be any variable name, like `$i`; `` and `` are SassScript expressions that should return integers. When `` is greater than `` the counter will decrement instead of increment. The `@for` statement sets `$var` to each successive number in the specified range and each time outputs the nested styles using that value of `$var`. For the form `from ... through`, the range *includes* the values of `` and ``, but the form `from ... to` runs up to *but not including* the value of ``. Using the `through` syntax, @for $i from 1 through 3 { .item-#{$i} { width: 2em * $i; } } is compiled to: .item-1 { width: 2em; } .item-2 { width: 4em; } .item-3 { width: 6em; } ### `@each` The `@each` directive usually has the form `@each $var in `. `$var` can be any variable name, like `$length` or `$name`, and `` is a SassScript expression that returns a list or a map. The `@each` rule sets `$var` to each item in the list or map, then outputs the styles it contains using that value of `$var`. For example: @each $animal in puma, sea-slug, egret, salamander { .#{$animal}-icon { background-image: url('/images/#{$animal}.png'); } } is compiled to: .puma-icon { background-image: url('/images/puma.png'); } .sea-slug-icon { background-image: url('/images/sea-slug.png'); } .egret-icon { background-image: url('/images/egret.png'); } .salamander-icon { background-image: url('/images/salamander.png'); } #### Multiple Assignment The `@each` directive can also use multiple variables, as in `@each $var1, $var2, ... in `. If `` is a list of lists, each element of the sub-lists is assigned to the respective variable. For example: @each $animal, $color, $cursor in (puma, black, default), (sea-slug, blue, pointer), (egret, white, move) { .#{$animal}-icon { background-image: url('/images/#{$animal}.png'); border: 2px solid $color; cursor: $cursor; } } is compiled to: .puma-icon { background-image: url('/images/puma.png'); border: 2px solid black; cursor: default; } .sea-slug-icon { background-image: url('/images/sea-slug.png'); border: 2px solid blue; cursor: pointer; } .egret-icon { background-image: url('/images/egret.png'); border: 2px solid white; cursor: move; } Since [maps](#maps) are treated as lists of pairs, multiple assignment works with them as well. For example: @each $header, $size in (h1: 2em, h2: 1.5em, h3: 1.2em) { #{$header} { font-size: $size; } } is compiled to: h1 { font-size: 2em; } h2 { font-size: 1.5em; } h3 { font-size: 1.2em; } ### `@while` The `@while` directive takes a SassScript expression and repeatedly outputs the nested styles until the statement evaluates to `false`. This can be used to achieve more complex looping than the `@for` statement is capable of, although this is rarely necessary. For example: $i: 6; @while $i > 0 { .item-#{$i} { width: 2em * $i; } $i: $i - 2; } is compiled to: .item-6 { width: 12em; } .item-4 { width: 8em; } .item-2 { width: 4em; } ## Mixin Directives Mixins allow you to define styles that can be re-used throughout the stylesheet without needing to resort to non-semantic classes like `.float-left`. Mixins can also contain full CSS rules, and anything else allowed elsewhere in a Sass document. They can even take [arguments](#mixin-arguments) which allows you to produce a wide variety of styles with very few mixins. ### Defining a Mixin: `@mixin` Mixins are defined with the `@mixin` directive. It's followed by the name of the mixin and optionally the [arguments](#mixin-arguments), and a block containing the contents of the mixin. For example, the `large-text` mixin is defined as follows: @mixin large-text { font: { family: Arial; size: 20px; weight: bold; } color: #ff0000; } Mixins may also contain selectors, possibly mixed with properties. The selectors can even contain [parent references](#referencing_parent_selectors_). For example: @mixin clearfix { display: inline-block; &:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html & { height: 1px } } For historical reasons, mixin names (and all other Sass identifiers) can use hyphens and underscores interchangeably. For example, if you define a mixin called `add-column`, you can include it as `add_column`, and vice versa. ### Including a Mixin: `@include` Mixins are included in the document with the `@include` directive. This takes the name of a mixin and optionally [arguments to pass to it](#mixin-arguments), and includes the styles defined by that mixin into the current rule. For example: .page-title { @include large-text; padding: 4px; margin-top: 10px; } is compiled to: .page-title { font-family: Arial; font-size: 20px; font-weight: bold; color: #ff0000; padding: 4px; margin-top: 10px; } Mixins may also be included outside of any rule (that is, at the root of the document) as long as they don't directly define any properties or use any parent references. For example: @mixin silly-links { a { color: blue; background-color: red; } } @include silly-links; is compiled to: a { color: blue; background-color: red; } Mixin definitions can also include other mixins. For example: @mixin compound { @include highlighted-background; @include header-text; } @mixin highlighted-background { background-color: #fc0; } @mixin header-text { font-size: 20px; } Mixins may include themselves. This is different than the behavior of Sass versions prior to 3.3, where mixin recursion was forbidden. Mixins that only define descendent selectors can be safely mixed into the top most level of a document. ### Arguments Mixins can take SassScript values as arguments, which are given when the mixin is included and made available within the mixin as variables. When defining a mixin, the arguments are written as variable names separated by commas, all in parentheses after the name. Then when including the mixin, values can be passed in in the same manner. For example: @mixin sexy-border($color, $width) { border: { color: $color; width: $width; style: dashed; } } p { @include sexy-border(blue, 1in); } is compiled to: p { border-color: blue; border-width: 1in; border-style: dashed; } Mixins can also specify default values for their arguments using the normal variable-setting syntax. Then when the mixin is included, if it doesn't pass in that argument, the default value will be used instead. For example: @mixin sexy-border($color, $width: 1in) { border: { color: $color; width: $width; style: dashed; } } p { @include sexy-border(blue); } h1 { @include sexy-border(blue, 2in); } is compiled to: p { border-color: blue; border-width: 1in; border-style: dashed; } h1 { border-color: blue; border-width: 2in; border-style: dashed; } #### Keyword Arguments Mixins can also be included using explicit keyword arguments. For instance, the above example could be written as: p { @include sexy-border($color: blue); } h1 { @include sexy-border($color: blue, $width: 2in); } While this is less concise, it can make the stylesheet easier to read. It also allows functions to present more flexible interfaces, providing many arguments without becoming difficult to call. Named arguments can be passed in any order, and arguments with default values can be omitted. Since the named arguments are variable names, underscores and dashes can be used interchangeably. #### Trailing Commas When the last argument to a mixin or function is a positional or keyword-style argument, that argument can be followed by a trailing comma. Some prefer this coding style as it can lead to more concise diffs and fewer syntax errors when refactoring. #### Variable Arguments Sometimes it makes sense for a mixin or function to take an unknown number of arguments. For example, a mixin for creating box shadows might take any number of shadows as arguments. For these situations, Sass supports "variable arguments," which are arguments at the end of a mixin or function declaration that take all leftover arguments and package them up as a [list](#lists). These arguments look just like normal arguments, but are followed by `...`. For example: @mixin box-shadow($shadows...) { -moz-box-shadow: $shadows; -webkit-box-shadow: $shadows; box-shadow: $shadows; } .shadows { @include box-shadow(0px 4px 5px #666, 2px 6px 10px #999); } is compiled to: .shadows { -moz-box-shadow: 0px 4px 5px #666, 2px 6px 10px #999; -webkit-box-shadow: 0px 4px 5px #666, 2px 6px 10px #999; box-shadow: 0px 4px 5px #666, 2px 6px 10px #999; } Variable arguments also contain any keyword arguments passed to the mixin or function. These can be accessed using the {Sass::Script::Functions#keywords `keywords($args)` function}, which returns them as a map from strings (without `$`) to values. Variable arguments can also be used when calling a mixin. Using the same syntax, you can expand a list of values so that each value is passed as a separate argument, or expand a map of values so that each pair is treated as a keyword argument. For example: @mixin colors($text, $background, $border) { color: $text; background-color: $background; border-color: $border; } $values: #ff0000, #00ff00, #0000ff; .primary { @include colors($values...); } $value-map: (text: #00ff00, background: #0000ff, border: #ff0000); .secondary { @include colors($value-map...); } is compiled to: .primary { color: #ff0000; background-color: #00ff00; border-color: #0000ff; } .secondary { color: #00ff00; background-color: #0000ff; border-color: #ff0000; } You can pass both an argument list and a map as long as the list comes before the map, as in `@include colors($values..., $map...)`. You can use variable arguments to wrap a mixin and add additional styles without changing the argument signature of the mixin. If you do, keyword arguments will get directly passed through to the wrapped mixin. For example: @mixin wrapped-stylish-mixin($args...) { font-weight: bold; @include stylish-mixin($args...); } .stylish { // The $width argument will get passed on to "stylish-mixin" as a keyword @include wrapped-stylish-mixin(#00ff00, $width: 100px); } ### Passing Content Blocks to a Mixin It is possible to pass a block of styles to the mixin for placement within the styles included by the mixin. The styles will appear at the location of any `@content` directives found within the mixin. This makes it possible to define abstractions relating to the construction of selectors and directives. For example: @mixin apply-to-ie6-only { * html { @content; } } @include apply-to-ie6-only { #logo { background-image: url(/logo.gif); } } Generates: * html #logo { background-image: url(/logo.gif); } The same mixins can be done in the `.sass` shorthand syntax: =apply-to-ie6-only * html @content +apply-to-ie6-only #logo background-image: url(/logo.gif) **Note:** when the `@content` directive is specified more than once or in a loop, the style block will be duplicated with each invocation. Some mixins may require a passed content block or may have different behavior depending on whether a content block was passed. The [`content-exists()` function](Sass/Script/Functions.html#content_exists-instance_method) will return true when a content block is passed to the current mixin and can be used to implement such behaviors. #### Variable Scope and Content Blocks The block of content passed to a mixin are evaluated in the scope where the block is defined, not in the scope of the mixin. This means that variables local to the mixin **cannot** be used within the passed style block and variables will resolve to the global value: $color: white; @mixin colors($color: blue) { background-color: $color; @content; border-color: $color; } .colors { @include colors { color: $color; } } Compiles to: .colors { background-color: blue; color: white; border-color: blue; } Additionally, this makes it clear that the variables and mixins that are used within the passed block are related to the other styles around where the block is defined. For example: #sidebar { $sidebar-width: 300px; width: $sidebar-width; @include smartphone { width: $sidebar-width / 3; } } ## Function Directives It is possible to define your own functions in sass and use them in any value or script context. For example: $grid-width: 40px; $gutter-width: 10px; @function grid-width($n) { @return $n * $grid-width + ($n - 1) * $gutter-width; } #sidebar { width: grid-width(5); } Becomes: #sidebar { width: 240px; } As you can see functions can access any globally defined variables as well as accept arguments just like a mixin. A function may have several statements contained within it, and you must call `@return` to set the return value of the function. As with mixins, you can call Sass-defined functions using keyword arguments. In the above example we could have called the function like this: #sidebar { width: grid-width($n: 5); } It is recommended that you prefix your functions to avoid naming conflicts and so that readers of your stylesheets know they are not part of Sass or CSS. For example, if you work for ACME Corp, you might have named the function above `-acme-grid-width`. User-defined functions also support [variable arguments](#variable_arguments) in the same way as mixins. For historical reasons, function names (and all other Sass identifiers) can use hyphens and underscores interchangeably. For example, if you define a function called `grid-width`, you can use it as `grid_width`, and vice versa. ## Output Style Although the default CSS style that Sass outputs is very nice and reflects the structure of the document, tastes and needs vary and so Sass supports several other styles. Sass allows you to choose between four different output styles by setting the [`:style` option](#style-option) or using the `--style` command-line flag. ### `:nested` Nested style is the default Sass style, because it reflects the structure of the CSS styles and the HTML document they're styling. Each property has its own line, but the indentation isn't constant. Each rule is indented based on how deeply it's nested. For example: #main { color: #fff; background-color: #000; } #main p { width: 10em; } .huge { font-size: 10em; font-weight: bold; text-decoration: underline; } Nested style is very useful when looking at large CSS files: it allows you to easily grasp the structure of the file without actually reading anything. ### `:expanded` Expanded is a more typical human-made CSS style, with each property and rule taking up one line. Properties are indented within the rules, but the rules aren't indented in any special way. For example: #main { color: #fff; background-color: #000; } #main p { width: 10em; } .huge { font-size: 10em; font-weight: bold; text-decoration: underline; } ### `:compact` Compact style takes up less space than Nested or Expanded. It also draws the focus more to the selectors than to their properties. Each CSS rule takes up only one line, with every property defined on that line. Nested rules are placed next to each other with no newline, while separate groups of rules have newlines between them. For example: #main { color: #fff; background-color: #000; } #main p { width: 10em; } .huge { font-size: 10em; font-weight: bold; text-decoration: underline; } ### `:compressed` Compressed style takes up the minimum amount of space possible, having no whitespace except that necessary to separate selectors and a newline at the end of the file. It also includes some other minor compressions, such as choosing the smallest representation for colors. It's not meant to be human-readable. For example: #main{color:#fff;background-color:#000}#main p{width:10em}.huge{font-size:10em;font-weight:bold;text-decoration:underline} ## Extending Sass Sass provides a number of advanced customizations for users with unique requirements. Using these features requires a strong understanding of Ruby. ### Defining Custom Sass Functions Users can define their own Sass functions using the Ruby API. For more information, see the [source documentation](Sass/Script/Functions.html#adding_custom_functions). ### Cache Stores Sass caches parsed documents so that they can be reused without parsing them again unless they have changed. By default, Sass will write these cache files to a location on the filesystem indicated by [`:cache_location`](#cache_location-option). If you cannot write to the filesystem or need to share cache across ruby processes or machines, then you can define your own cache store and set the[`:cache_store` option](#cache_store-option). For details on creating your own cache store, please see the {Sass::CacheStores::Base source documentation}. ### Custom Importers Sass importers are in charge of taking paths passed to `@import` and finding the appropriate Sass code for those paths. By default, this code is loaded from the {Sass::Importers::Filesystem filesystem}, but importers could be added to load from a database, over HTTP, or use a different file naming scheme than what Sass expects. Each importer is in charge of a single load path (or whatever the corresponding notion is for the backend). Importers can be placed in the {file:SASS_REFERENCE.md#load_paths-option `:load_paths` array} alongside normal filesystem paths. When resolving an `@import`, Sass will go through the load paths looking for an importer that successfully imports the path. Once one is found, the imported file is used. User-created importers must inherit from {Sass::Importers::Base}. ruby-sass-3.7.4/doc-src/SCSS_FOR_SASS_USERS.md000066400000000000000000000103071345125207600204320ustar00rootroot00000000000000# Intro to SCSS for Sass Users Sass 3 introduces a new syntax known as SCSS which is fully compatible with the syntax of CSS, while still supporting the full power of Sass. This means that every valid CSS stylesheet is a valid SCSS file with the same meaning. In addition, SCSS understands most CSS hacks and vendor-specific syntax, such as [IE's old `filter` syntax](http://msdn.microsoft.com/en-us/library/ms532847%28v=vs.85%29.aspx#Defining_Visual_Filt). Since SCSS is a CSS extension, everything that works in CSS works in SCSS. This means that for a Sass user to understand it, they need only understand how the Sass extensions work. Most of these, such as variables, parent references, and directives work the same; the only difference is that SCSS requires semicolons and brackets instead of newlines and indentation. For example, a simple rule in Sass: #sidebar width: 30% background-color: #faa could be converted to SCSS just by adding brackets and semicolons: #sidebar { width: 30%; background-color: #faa; } In addition, SCSS is completely whitespace-insensitive. That means the above could also be written as: #sidebar {width: 30%; background-color: #faa} There are some differences that are slightly more complicated. These are detailed below. Note, though, that SCSS uses all the {file:SASS_CHANGELOG.md#3-0-0-syntax-changes syntax changes in Sass 3}, so make sure you understand those before going forward. ## Nested Selectors To nest selectors, simply define a new ruleset inside an existing ruleset: #sidebar { a { text-decoration: none; } } Of course, white space is insignificant and the last trailing semicolon is optional so you can also do it like this: #sidebar { a { text-decoration: none } } ## Nested Properties To nest properties, simply create a new property set after an existing property's colon: #footer { border: { width: 1px; color: #ccc; style: solid; } } This compiles to: #footer { border-width: 1px; border-color: #cccccc; border-style: solid; } ## Mixins A mixin is declared with the `@mixin` directive: @mixin rounded($amount) { -moz-border-radius: $amount; -webkit-border-radius: $amount; border-radius: $amount; } A mixin is used with the `@include` directive: .box { border: 3px solid #777; @include rounded(0.5em); } This syntax is also available in the indented syntax, although the old `=` and `+` syntax still works. This is rather verbose compared to the `=` and `+` characters used in Sass syntax. This is because the SCSS format is designed for CSS compatibility rather than conciseness, and creating new syntax when the CSS directive syntax already exists adds new syntax needlessly and could create incompatibilities with future versions of CSS. ## Comments Like Sass, SCSS supports both comments that are preserved in the CSS output and comments that aren't. However, SCSS's comments are significantly more flexible. It supports standard multiline CSS comments with `/* */`, which are preserved where possible in the output. These comments can have whatever formatting you like; Sass will do its best to format them nicely. SCSS also uses `//` for comments that are thrown away, like Sass. Unlike Sass, though, `//` comments in SCSS may appear anywhere and last only until the end of the line. For example: /* This comment is * several lines long. * since it uses the CSS comment syntax, * it will appear in the CSS output. */ body { color: black; } // These comments are only one line long each. // They won't appear in the CSS output, // since they use the single-line comment syntax. a { color: green; } is compiled to: /* This comment is * several lines long. * since it uses the CSS comment syntax, * it will appear in the CSS output. */ body { color: black; } a { color: green; } ## `@import` The `@import` directive in SCSS functions just like that in Sass, except that it takes a quoted string to import. For example, this Sass: @import themes/dark @import font.sass would be this SCSS: @import "themes/dark"; @import "font.sass"; ruby-sass-3.7.4/ext/000077500000000000000000000000001345125207600142625ustar00rootroot00000000000000ruby-sass-3.7.4/ext/extconf.rb000066400000000000000000000003701345125207600162550ustar00rootroot00000000000000root = File.expand_path("../..", __FILE__) File.open(File.expand_path("lib/sass/root.rb", root), "w") do |f| f << <<-RUBY module Sass ROOT_DIR = #{root.inspect} end RUBY end File.open('Makefile', 'w') { |f| f.puts("install:\n\t$(exit 0)") } ruby-sass-3.7.4/extra/000077500000000000000000000000001345125207600146055ustar00rootroot00000000000000ruby-sass-3.7.4/extra/sass-spec-ref.sh000077500000000000000000000022711345125207600176210ustar00rootroot00000000000000#!/bin/bash -e # Copyright 2016 Google Inc. Use of this source code is governed by an MIT-style # license that can be found in the LICENSE file or at # https://opensource.org/licenses/MIT. # Echoes the sass-spec Git ref that should be checked out for the current Travis # run. If we're running specs for a pull request which refers to a sass-spec # pull request, we'll run against the latter rather than sass-spec master. default=master if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then >&2 echo "TRAVIS_PULL_REQUEST: $TRAVIS_PULL_REQUEST." >&2 echo "Ref: $default." echo "$default" exit 0 fi >&2 echo "Fetching pull request $TRAVIS_PULL_REQUEST..." url=https://api.github.com/repos/sass/ruby-sass/pulls/$TRAVIS_PULL_REQUEST if [ -z "$GITHUB_AUTH" ]; then >&2 echo "Fetching pull request info without authentication" JSON=$(curl -L -sS $url) else >&2 echo "Fetching pull request info as sassbot" JSON=$(curl -u "sassbot:$GITHUB_AUTH" -L -sS $url) fi >&2 echo "$JSON" RE_SPEC_PR="sass\/sass-spec(#|\/pull\/)([0-9]+)" if [[ $JSON =~ $RE_SPEC_PR ]]; then ref="pull/${BASH_REMATCH[2]}/head" >&2 echo "Ref: $ref." echo "$ref" else >&2 echo "Ref: $default." echo "$default" fi ruby-sass-3.7.4/extra/update_watch.rb000066400000000000000000000005131345125207600176010ustar00rootroot00000000000000require 'rubygems' require 'sinatra' require 'json' set :port, 3124 set :environment, :production enable :lock Dir.chdir(File.dirname(__FILE__) + "/..") post "/" do puts "Received payload!" puts "Rev: #{`git name-rev HEAD`.strip}" system %{rake handle_update --trace REF=#{JSON.parse(params["payload"])["ref"].inspect}} end ruby-sass-3.7.4/init.rb000066400000000000000000000011131345125207600147460ustar00rootroot00000000000000begin require File.join(File.dirname(__FILE__), 'lib', 'sass') # From here rescue LoadError begin require 'sass' # From gem rescue LoadError => e # gems:install may be run to install Haml with the skeleton plugin # but not the gem itself installed. # Don't die if this is the case. raise e unless defined?(Rake) && (Rake.application.top_level_tasks.include?('gems') || Rake.application.top_level_tasks.include?('gems:install')) end end # Load Sass. # Sass may be undefined if we're running gems:install. require 'sass/plugin' if defined?(Sass) ruby-sass-3.7.4/lib/000077500000000000000000000000001345125207600142305ustar00rootroot00000000000000ruby-sass-3.7.4/lib/sass.rb000066400000000000000000000075521345125207600155370ustar00rootroot00000000000000dir = File.dirname(__FILE__) $LOAD_PATH.unshift dir unless $LOAD_PATH.include?(dir) require 'sass/version' # The module that contains everything Sass-related: # # * {Sass::Engine} is the class used to render Sass/SCSS within Ruby code. # * {Sass::Plugin} is interfaces with web frameworks (Rails and Merb in particular). # * {Sass::SyntaxError} is raised when Sass encounters an error. # * {Sass::CSS} handles conversion of CSS to Sass. # # Also see the {file:SASS_REFERENCE.md full Sass reference}. module Sass class << self # @private attr_accessor :tests_running end # The global load paths for Sass files. This is meant for plugins and # libraries to register the paths to their Sass stylesheets to that they may # be `@imported`. This load path is used by every instance of {Sass::Engine}. # They are lower-precedence than any load paths passed in via the # {file:SASS_REFERENCE.md#load_paths-option `:load_paths` option}. # # If the `SASS_PATH` environment variable is set, # the initial value of `load_paths` will be initialized based on that. # The variable should be a colon-separated list of path names # (semicolon-separated on Windows). # # Note that files on the global load path are never compiled to CSS # themselves, even if they aren't partials. They exist only to be imported. # # @example # Sass.load_paths << File.dirname(__FILE__ + '/sass') # @return [Array] def self.load_paths @load_paths ||= if ENV['SASS_PATH'] ENV['SASS_PATH'].split(Sass::Util.windows? ? ';' : ':') else [] end end # Compile a Sass or SCSS string to CSS. # Defaults to SCSS. # # @param contents [String] The contents of the Sass file. # @param options [{Symbol => Object}] An options hash; # see {file:SASS_REFERENCE.md#Options the Sass options documentation} # @raise [Sass::SyntaxError] if there's an error in the document # @raise [Encoding::UndefinedConversionError] if the source encoding # cannot be converted to UTF-8 # @raise [ArgumentError] if the document uses an unknown encoding with `@charset` def self.compile(contents, options = {}) options[:syntax] ||= :scss Engine.new(contents, options).to_css end # Compile a file on disk to CSS. # # @raise [Sass::SyntaxError] if there's an error in the document # @raise [Encoding::UndefinedConversionError] if the source encoding # cannot be converted to UTF-8 # @raise [ArgumentError] if the document uses an unknown encoding with `@charset` # # @overload compile_file(filename, options = {}) # Return the compiled CSS rather than writing it to a file. # # @param filename [String] The path to the Sass, SCSS, or CSS file on disk. # @param options [{Symbol => Object}] An options hash; # see {file:SASS_REFERENCE.md#Options the Sass options documentation} # @return [String] The compiled CSS. # # @overload compile_file(filename, css_filename, options = {}) # Write the compiled CSS to a file. # # @param filename [String] The path to the Sass, SCSS, or CSS file on disk. # @param options [{Symbol => Object}] An options hash; # see {file:SASS_REFERENCE.md#Options the Sass options documentation} # @param css_filename [String] The location to which to write the compiled CSS. def self.compile_file(filename, *args) options = args.last.is_a?(Hash) ? args.pop : {} css_filename = args.shift result = Sass::Engine.for_file(filename, options).render if css_filename options[:css_filename] ||= css_filename open(css_filename, "w") {|css_file| css_file.write(result)} nil else result end end end require 'sass/logger' require 'sass/util' require 'sass/engine' require 'sass/plugin' if defined?(Merb::Plugins) require 'sass/railtie' require 'sass/features' ruby-sass-3.7.4/lib/sass/000077500000000000000000000000001345125207600152015ustar00rootroot00000000000000ruby-sass-3.7.4/lib/sass/cache_stores.rb000066400000000000000000000006051345125207600201710ustar00rootroot00000000000000require 'stringio' module Sass # Sass cache stores are in charge of storing cached information, # especially parse trees for Sass documents. # # User-created importers must inherit from {CacheStores::Base}. module CacheStores end end require 'sass/cache_stores/base' require 'sass/cache_stores/filesystem' require 'sass/cache_stores/memory' require 'sass/cache_stores/chain' ruby-sass-3.7.4/lib/sass/cache_stores/000077500000000000000000000000001345125207600176435ustar00rootroot00000000000000ruby-sass-3.7.4/lib/sass/cache_stores/base.rb000066400000000000000000000072001345125207600211010ustar00rootroot00000000000000module Sass module CacheStores # An abstract base class for backends for the Sass cache. # Any key-value store can act as such a backend; # it just needs to implement the # \{#_store} and \{#_retrieve} methods. # # To use a cache store with Sass, # use the {file:SASS_REFERENCE.md#cache_store-option `:cache_store` option}. # # @abstract class Base # Store cached contents for later retrieval # Must be implemented by all CacheStore subclasses # # Note: cache contents contain binary data. # # @param key [String] The key to store the contents under # @param version [String] The current sass version. # Cached contents must not be retrieved across different versions of sass. # @param sha [String] The sha of the sass source. # Cached contents must not be retrieved if the sha has changed. # @param contents [String] The contents to store. def _store(key, version, sha, contents) raise "#{self.class} must implement #_store." end # Retrieved cached contents. # Must be implemented by all subclasses. # # Note: if the key exists but the sha or version have changed, # then the key may be deleted by the cache store, if it wants to do so. # # @param key [String] The key to retrieve # @param version [String] The current sass version. # Cached contents must not be retrieved across different versions of sass. # @param sha [String] The sha of the sass source. # Cached contents must not be retrieved if the sha has changed. # @return [String] The contents that were previously stored. # @return [NilClass] when the cache key is not found or the version or sha have changed. def _retrieve(key, version, sha) raise "#{self.class} must implement #_retrieve." end # Store a {Sass::Tree::RootNode}. # # @param key [String] The key to store it under. # @param sha [String] The checksum for the contents that are being stored. # @param root [Object] The root node to cache. def store(key, sha, root) _store(key, Sass::VERSION, sha, Marshal.dump(root)) rescue TypeError, LoadError => e Sass::Util.sass_warn "Warning. Error encountered while saving cache #{path_to(key)}: #{e}" nil end # Retrieve a {Sass::Tree::RootNode}. # # @param key [String] The key the root element was stored under. # @param sha [String] The checksum of the root element's content. # @return [Object] The cached object. def retrieve(key, sha) contents = _retrieve(key, Sass::VERSION, sha) Marshal.load(contents) if contents rescue EOFError, TypeError, ArgumentError, LoadError => e Sass::Util.sass_warn "Warning. Error encountered while reading cache #{path_to(key)}: #{e}" nil end # Return the key for the sass file. # # The `(sass_dirname, sass_basename)` pair # should uniquely identify the Sass document, # but otherwise there are no restrictions on their content. # # @param sass_dirname [String] # The fully-expanded location of the Sass file. # This corresponds to the directory name on a filesystem. # @param sass_basename [String] The name of the Sass file that is being referenced. # This corresponds to the basename on a filesystem. def key(sass_dirname, sass_basename) dir = Digest::SHA1.hexdigest(sass_dirname) filename = "#{sass_basename}c" "#{dir}/#{filename}" end end end end ruby-sass-3.7.4/lib/sass/cache_stores/chain.rb000066400000000000000000000016711345125207600212570ustar00rootroot00000000000000module Sass module CacheStores # A meta-cache that chains multiple caches together. # Specifically: # # * All `#store`s are passed to all caches. # * `#retrieve`s are passed to each cache until one has a hit. # * When one cache has a hit, the value is `#store`d in all earlier caches. class Chain < Base # Create a new cache chaining the given caches. # # @param caches [Array] The caches to chain. def initialize(*caches) @caches = caches end # @see Base#store def store(key, sha, obj) @caches.each {|c| c.store(key, sha, obj)} end # @see Base#retrieve def retrieve(key, sha) @caches.each_with_index do |c, i| obj = c.retrieve(key, sha) next unless obj @caches[0...i].each {|prev| prev.store(key, sha, obj)} return obj end nil end end end end ruby-sass-3.7.4/lib/sass/cache_stores/filesystem.rb000066400000000000000000000033071345125207600223570ustar00rootroot00000000000000require 'fileutils' module Sass module CacheStores # A backend for the Sass cache using the filesystem. class Filesystem < Base # The directory where the cached files will be stored. # # @return [String] attr_accessor :cache_location # @param cache_location [String] see \{#cache\_location} def initialize(cache_location) @cache_location = cache_location end # @see Base#\_retrieve def _retrieve(key, version, sha) return unless File.readable?(path_to(key)) begin File.open(path_to(key), "rb") do |f| if f.readline("\n").strip == version && f.readline("\n").strip == sha return f.read end end File.unlink path_to(key) rescue Errno::ENOENT # Already deleted. Race condition? end nil rescue EOFError, TypeError, ArgumentError => e Sass::Util.sass_warn "Warning. Error encountered while reading cache #{path_to(key)}: #{e}" end # @see Base#\_store def _store(key, version, sha, contents) compiled_filename = path_to(key) FileUtils.mkdir_p(File.dirname(compiled_filename)) Sass::Util.atomic_create_and_write_file(compiled_filename) do |f| f.puts(version) f.puts(sha) f.write(contents) end rescue Errno::EACCES # pass end private # Returns the path to a file for the given key. # # @param key [String] # @return [String] The path to the cache file. def path_to(key) key = key.gsub(/[<>:\\|?*%]/) {|c| "%%%03d" % c.ord} File.join(cache_location, key) end end end end ruby-sass-3.7.4/lib/sass/cache_stores/memory.rb000066400000000000000000000022341345125207600215010ustar00rootroot00000000000000module Sass module CacheStores # A backend for the Sass cache using in-process memory. class Memory < Base # Since the {Memory} store is stored in the Sass tree's options hash, # when the options get serialized as part of serializing the tree, # you get crazy exponential growth in the size of the cached objects # unless you don't dump the cache. # # @private def _dump(depth) "" end # If we deserialize this class, just make a new empty one. # # @private def self._load(repr) Memory.new end # Create a new, empty cache store. def initialize @contents = {} end # @see Base#retrieve def retrieve(key, sha) return unless @contents.has_key?(key) return unless @contents[key][:sha] == sha obj = @contents[key][:obj] obj.respond_to?(:deep_copy) ? obj.deep_copy : obj.dup end # @see Base#store def store(key, sha, obj) @contents[key] = {:sha => sha, :obj => obj} end # Destructively clear the cache. def reset! @contents = {} end end end end ruby-sass-3.7.4/lib/sass/cache_stores/null.rb000066400000000000000000000007601345125207600211450ustar00rootroot00000000000000module Sass module CacheStores # Doesn't store anything, but records what things it should have stored. # This doesn't currently have any use except for testing and debugging. # # @private class Null < Base def initialize @keys = {} end def _retrieve(key, version, sha) nil end def _store(key, version, sha, contents) @keys[key] = true end def was_set?(key) @keys[key] end end end end ruby-sass-3.7.4/lib/sass/callbacks.rb000066400000000000000000000036241345125207600174520ustar00rootroot00000000000000module Sass # A lightweight infrastructure for defining and running callbacks. # Callbacks are defined using \{#define\_callback\} at the class level, # and called using `run_#{name}` at the instance level. # # Clients can add callbacks by calling the generated `on_#{name}` method, # and passing in a block that's run when the callback is activated. # # @example Define a callback # class Munger # extend Sass::Callbacks # define_callback :string_munged # # def munge(str) # res = str.gsub(/[a-z]/, '\1\1') # run_string_munged str, res # res # end # end # # @example Use a callback # m = Munger.new # m.on_string_munged {|str, res| puts "#{str} was munged into #{res}!"} # m.munge "bar" #=> bar was munged into bbaarr! module Callbacks # Automatically includes {InstanceMethods} # when something extends this module. # # @param base [Module] def self.extended(base) base.send(:include, InstanceMethods) end protected module InstanceMethods # Removes all callbacks registered against this object. def clear_callbacks! @_sass_callbacks = {} end end # Define a callback with the given name. # This will define an `on_#{name}` method # that registers a block, # and a `run_#{name}` method that runs that block # (optionall with some arguments). # # @param name [Symbol] The name of the callback # @return [void] def define_callback(name) class_eval < "p\n color: blue" # Sass::CSS.new("p { color: blue }").render(:scss) #=> "p {\n color: blue; }" class CSS # @param template [String] The CSS stylesheet. # This stylesheet can be encoded using any encoding # that can be converted to Unicode. # If the stylesheet contains an `@charset` declaration, # that overrides the Ruby encoding # (see {file:SASS_REFERENCE.md#Encodings the encoding documentation}) # @option options :old [Boolean] (false) # Whether or not to output old property syntax # (`:color blue` as opposed to `color: blue`). # This is only meaningful when generating Sass code, # rather than SCSS. # @option options :indent [String] (" ") # The string to use for indenting each line. Defaults to two spaces. def initialize(template, options = {}) if template.is_a? IO template = template.read end @options = options.merge(:_convert => true) # Backwards compatibility @options[:old] = true if @options[:alternate] == false @template = template @checked_encoding = false end # Converts the CSS template into Sass or SCSS code. # # @param fmt [Symbol] `:sass` or `:scss`, designating the format to return. # @return [String] The resulting Sass or SCSS code # @raise [Sass::SyntaxError] if there's an error parsing the CSS template def render(fmt = :sass) check_encoding! build_tree.send("to_#{fmt}", @options).strip + "\n" rescue Sass::SyntaxError => err err.modify_backtrace(:filename => @options[:filename] || '(css)') raise err end # Returns the original encoding of the document. # # @return [Encoding, nil] # @raise [Encoding::UndefinedConversionError] if the source encoding # cannot be converted to UTF-8 # @raise [ArgumentError] if the document uses an unknown encoding with `@charset` def source_encoding check_encoding! @original_encoding end private def check_encoding! return if @checked_encoding @checked_encoding = true @template, @original_encoding = Sass::Util.check_sass_encoding(@template) end # Parses the CSS template and applies various transformations # # @return [Tree::Node] The root node of the parsed tree def build_tree root = Sass::SCSS::CssParser.new(@template, @options[:filename], nil).parse parse_selectors(root) expand_commas(root) nest_seqs(root) parent_ref_rules(root) flatten_rules(root) bubble_subject(root) fold_commas(root) dump_selectors(root) root end # Parse all the selectors in the document and assign them to # {Sass::Tree::RuleNode#parsed_rules}. # # @param root [Tree::Node] The parent node def parse_selectors(root) root.children.each do |child| next parse_selectors(child) if child.is_a?(Tree::DirectiveNode) next unless child.is_a?(Tree::RuleNode) parser = Sass::SCSS::CssParser.new(child.rule.first, child.filename, nil, child.line) child.parsed_rules = parser.parse_selector end end # Transform # # foo, bar, baz # color: blue # # into # # foo # color: blue # bar # color: blue # baz # color: blue # # @param root [Tree::Node] The parent node def expand_commas(root) root.children.map! do |child| # child.parsed_rules.members.size > 1 iff the rule contains a comma unless child.is_a?(Tree::RuleNode) && child.parsed_rules.members.size > 1 expand_commas(child) if child.is_a?(Tree::DirectiveNode) next child end child.parsed_rules.members.map do |seq| node = Tree::RuleNode.new([]) node.parsed_rules = make_cseq(seq) node.children = child.children node end end root.children.flatten! end # Make rules use nesting so that # # foo # color: green # foo bar # color: red # foo baz # color: blue # # becomes # # foo # color: green # bar # color: red # baz # color: blue # # @param root [Tree::Node] The parent node def nest_seqs(root) current_rule = nil root.children.map! do |child| unless child.is_a?(Tree::RuleNode) nest_seqs(child) if child.is_a?(Tree::DirectiveNode) next child end seq = first_seq(child) seq.members.reject! {|sseq| sseq == "\n"} first, rest = seq.members.first, seq.members[1..-1] if current_rule.nil? || first_sseq(current_rule) != first current_rule = Tree::RuleNode.new([]) current_rule.parsed_rules = make_seq(first) end if rest.empty? current_rule.children += child.children else child.parsed_rules = make_seq(*rest) current_rule << child end current_rule end root.children.compact! root.children.uniq! root.children.each {|v| nest_seqs(v)} end # Make rules use parent refs so that # # foo # color: green # foo.bar # color: blue # # becomes # # foo # color: green # &.bar # color: blue # # @param root [Tree::Node] The parent node def parent_ref_rules(root) current_rule = nil root.children.map! do |child| unless child.is_a?(Tree::RuleNode) parent_ref_rules(child) if child.is_a?(Tree::DirectiveNode) next child end sseq = first_sseq(child) next child unless sseq.is_a?(Sass::Selector::SimpleSequence) firsts, rest = [sseq.members.first], sseq.members[1..-1] firsts.push rest.shift if firsts.first.is_a?(Sass::Selector::Parent) last_simple_subject = rest.empty? && sseq.subject? if current_rule.nil? || first_sseq(current_rule).members != firsts || !!first_sseq(current_rule).subject? != !!last_simple_subject current_rule = Tree::RuleNode.new([]) current_rule.parsed_rules = make_sseq(last_simple_subject, *firsts) end if rest.empty? current_rule.children += child.children else rest.unshift Sass::Selector::Parent.new child.parsed_rules = make_sseq(sseq.subject?, *rest) current_rule << child end current_rule end root.children.compact! root.children.uniq! root.children.each {|v| parent_ref_rules(v)} end # Flatten rules so that # # foo # bar # color: red # # becomes # # foo bar # color: red # # and # # foo # &.bar # color: blue # # becomes # # foo.bar # color: blue # # @param root [Tree::Node] The parent node def flatten_rules(root) root.children.each do |child| case child when Tree::RuleNode flatten_rule(child) when Tree::DirectiveNode flatten_rules(child) end end end # Flattens a single rule. # # @param rule [Tree::RuleNode] The candidate for flattening # @see #flatten_rules def flatten_rule(rule) while rule.children.size == 1 && rule.children.first.is_a?(Tree::RuleNode) child = rule.children.first if first_simple_sel(child).is_a?(Sass::Selector::Parent) rule.parsed_rules = child.parsed_rules.resolve_parent_refs(rule.parsed_rules) else rule.parsed_rules = make_seq(*(first_seq(rule).members + first_seq(child).members)) end rule.children = child.children end flatten_rules(rule) end def bubble_subject(root) root.children.each do |child| bubble_subject(child) if child.is_a?(Tree::RuleNode) || child.is_a?(Tree::DirectiveNode) next unless child.is_a?(Tree::RuleNode) && !child.children.empty? next unless child.children.all? do |c| next unless c.is_a?(Tree::RuleNode) first_simple_sel(c).is_a?(Sass::Selector::Parent) && first_sseq(c).subject? end first_sseq(child).subject = true child.children.each {|c| first_sseq(c).subject = false} end end # Transform # # foo # bar # color: blue # baz # color: blue # # into # # foo # bar, baz # color: blue # # @param root [Tree::Node] The parent node def fold_commas(root) prev_rule = nil root.children.map! do |child| unless child.is_a?(Tree::RuleNode) fold_commas(child) if child.is_a?(Tree::DirectiveNode) next child end if prev_rule && prev_rule.children.map {|c| c.to_sass} == child.children.map {|c| c.to_sass} prev_rule.parsed_rules.members << first_seq(child) next nil end fold_commas(child) prev_rule = child child end root.children.compact! end # Dump all the parsed {Sass::Tree::RuleNode} selectors to strings. # # @param root [Tree::Node] The parent node def dump_selectors(root) root.children.each do |child| next dump_selectors(child) if child.is_a?(Tree::DirectiveNode) next unless child.is_a?(Tree::RuleNode) child.rule = [child.parsed_rules.to_s] dump_selectors(child) end end # Create a {Sass::Selector::CommaSequence}. # # @param seqs [Array] # @return [Sass::Selector::CommaSequence] def make_cseq(*seqs) Sass::Selector::CommaSequence.new(seqs) end # Create a {Sass::Selector::CommaSequence} containing only a single # {Sass::Selector::Sequence}. # # @param sseqs [Array] # @return [Sass::Selector::CommaSequence] def make_seq(*sseqs) make_cseq(Sass::Selector::Sequence.new(sseqs)) end # Create a {Sass::Selector::CommaSequence} containing only a single # {Sass::Selector::Sequence} which in turn contains only a single # {Sass::Selector::SimpleSequence}. # # @param subject [Boolean] Whether this is a subject selector # @param sseqs [Array] # @return [Sass::Selector::CommaSequence] def make_sseq(subject, *sseqs) make_seq(Sass::Selector::SimpleSequence.new(sseqs, subject)) end # Return the first {Sass::Selector::Sequence} in a {Sass::Tree::RuleNode}. # # @param rule [Sass::Tree::RuleNode] # @return [Sass::Selector::Sequence] def first_seq(rule) rule.parsed_rules.members.first end # Return the first {Sass::Selector::SimpleSequence} in a # {Sass::Tree::RuleNode}. # # @param rule [Sass::Tree::RuleNode] # @return [Sass::Selector::SimpleSequence, String] def first_sseq(rule) first_seq(rule).members.first end # Return the first {Sass::Selector::Simple} in a {Sass::Tree::RuleNode}, # unless the rule begins with a combinator. # # @param rule [Sass::Tree::RuleNode] # @return [Sass::Selector::Simple?] def first_simple_sel(rule) sseq = first_sseq(rule) return unless sseq.is_a?(Sass::Selector::SimpleSequence) sseq.members.first end end end ruby-sass-3.7.4/lib/sass/deprecation.rb000066400000000000000000000034011345125207600200210ustar00rootroot00000000000000module Sass # A deprecation warning that should only be printed once for a given line in a # given file. # # A global Deprecation instance should be created for each type of deprecation # warning, and `warn` should be called each time a warning is needed. class Deprecation @@allow_double_warnings = false # Runs a block in which double deprecation warnings for the same location # are allowed. def self.allow_double_warnings old_allow_double_warnings = @@allow_double_warnings @@allow_double_warnings = true yield ensure @@allow_double_warnings = old_allow_double_warnings end def initialize # A set of filename, line pairs for which warnings have been emitted. @seen = Set.new end # Prints `message` as a deprecation warning associated with `filename`, # `line`, and optionally `column`. # # This ensures that only one message will be printed for each line of a # given file. # # @overload warn(filename, line, message) # @param filename [String, nil] # @param line [Number] # @param message [String] # @overload warn(filename, line, column, message) # @param filename [String, nil] # @param line [Number] # @param column [Number] # @param message [String] def warn(filename, line, column_or_message, message = nil) return if !@@allow_double_warnings && @seen.add?([filename, line]).nil? if message column = column_or_message else message = column_or_message end location = "line #{line}" location << ", column #{column}" if column location << " of #{filename}" if filename Sass::Util.sass_warn("DEPRECATION WARNING on #{location}:\n#{message}") end end end ruby-sass-3.7.4/lib/sass/engine.rb000066400000000000000000001336001345125207600167760ustar00rootroot00000000000000require 'set' require 'digest/sha1' require 'sass/cache_stores' require 'sass/deprecation' require 'sass/source/position' require 'sass/source/range' require 'sass/source/map' require 'sass/tree/node' require 'sass/tree/root_node' require 'sass/tree/rule_node' require 'sass/tree/comment_node' require 'sass/tree/prop_node' require 'sass/tree/directive_node' require 'sass/tree/media_node' require 'sass/tree/supports_node' require 'sass/tree/css_import_node' require 'sass/tree/variable_node' require 'sass/tree/mixin_def_node' require 'sass/tree/mixin_node' require 'sass/tree/trace_node' require 'sass/tree/content_node' require 'sass/tree/function_node' require 'sass/tree/return_node' require 'sass/tree/extend_node' require 'sass/tree/if_node' require 'sass/tree/while_node' require 'sass/tree/for_node' require 'sass/tree/each_node' require 'sass/tree/debug_node' require 'sass/tree/warn_node' require 'sass/tree/import_node' require 'sass/tree/charset_node' require 'sass/tree/at_root_node' require 'sass/tree/keyframe_rule_node' require 'sass/tree/error_node' require 'sass/tree/visitors/base' require 'sass/tree/visitors/perform' require 'sass/tree/visitors/cssize' require 'sass/tree/visitors/extend' require 'sass/tree/visitors/convert' require 'sass/tree/visitors/to_css' require 'sass/tree/visitors/deep_copy' require 'sass/tree/visitors/set_options' require 'sass/tree/visitors/check_nesting' require 'sass/selector' require 'sass/environment' require 'sass/script' require 'sass/scss' require 'sass/stack' require 'sass/error' require 'sass/importers' require 'sass/shared' require 'sass/media' require 'sass/supports' module Sass # A Sass mixin or function. # # `name`: `String` # : The name of the mixin/function. # # `args`: `Array<(Script::Tree::Node, Script::Tree::Node)>` # : The arguments for the mixin/function. # Each element is a tuple containing the variable node of the argument # and the parse tree for the default value of the argument. # # `splat`: `Script::Tree::Node?` # : The variable node of the splat argument for this callable, or null. # # `environment`: {Sass::Environment} # : The environment in which the mixin/function was defined. # This is captured so that the mixin/function can have access # to local variables defined in its scope. # # `tree`: `Array` # : The parse tree for the mixin/function. # # `has_content`: `Boolean` # : Whether the callable accepts a content block. # # `type`: `String` # : The user-friendly name of the type of the callable. # # `origin`: `Symbol` # : From whence comes the callable: `:stylesheet`, `:builtin`, `:css` # A callable with an origin of `:stylesheet` was defined in the stylesheet itself. # A callable with an origin of `:builtin` was defined in ruby. # A callable (function) with an origin of `:css` returns a function call with arguments to CSS. Callable = Struct.new(:name, :args, :splat, :environment, :tree, :has_content, :type, :origin) # This class handles the parsing and compilation of the Sass template. # Example usage: # # template = File.read('stylesheets/sassy.sass') # sass_engine = Sass::Engine.new(template) # output = sass_engine.render # puts output class Engine @@old_property_deprecation = Deprecation.new # A line of Sass code. # # `text`: `String` # : The text in the line, without any whitespace at the beginning or end. # # `tabs`: `Integer` # : The level of indentation of the line. # # `index`: `Integer` # : The line number in the original document. # # `offset`: `Integer` # : The number of bytes in on the line that the text begins. # This ends up being the number of bytes of leading whitespace. # # `filename`: `String` # : The name of the file in which this line appeared. # # `children`: `Array` # : The lines nested below this one. # # `comment_tab_str`: `String?` # : The prefix indentation for this comment, if it is a comment. class Line < Struct.new(:text, :tabs, :index, :offset, :filename, :children, :comment_tab_str) def comment? text[0] == COMMENT_CHAR && (text[1] == SASS_COMMENT_CHAR || text[1] == CSS_COMMENT_CHAR) end end # The character that begins a CSS property. PROPERTY_CHAR = ?: # The character that designates the beginning of a comment, # either Sass or CSS. COMMENT_CHAR = ?/ # The character that follows the general COMMENT_CHAR and designates a Sass comment, # which is not output as a CSS comment. SASS_COMMENT_CHAR = ?/ # The character that indicates that a comment allows interpolation # and should be preserved even in `:compressed` mode. SASS_LOUD_COMMENT_CHAR = ?! # The character that follows the general COMMENT_CHAR and designates a CSS comment, # which is embedded in the CSS document. CSS_COMMENT_CHAR = ?* # The character used to denote a compiler directive. DIRECTIVE_CHAR = ?@ # Designates a non-parsed rule. ESCAPE_CHAR = ?\\ # Designates block as mixin definition rather than CSS rules to output MIXIN_DEFINITION_CHAR = ?= # Includes named mixin declared using MIXIN_DEFINITION_CHAR MIXIN_INCLUDE_CHAR = ?+ # The regex that matches and extracts data from # properties of the form `:name prop`. PROPERTY_OLD = /^:([^\s=:"]+)\s*(?:\s+|$)(.*)/ # The default options for Sass::Engine. # @api public DEFAULT_OPTIONS = { :style => :nested, :load_paths => [], :cache => true, :cache_location => './.sass-cache', :syntax => :sass, :filesystem_importer => Sass::Importers::Filesystem }.freeze # Converts a Sass options hash into a standard form, filling in # default values and resolving aliases. # # @param options [{Symbol => Object}] The options hash; # see {file:SASS_REFERENCE.md#Options the Sass options documentation} # @return [{Symbol => Object}] The normalized options hash. # @private def self.normalize_options(options) options = DEFAULT_OPTIONS.merge(options.reject {|_k, v| v.nil?}) # If the `:filename` option is passed in without an importer, # assume it's using the default filesystem importer. options[:importer] ||= options[:filesystem_importer].new(".") if options[:filename] # Tracks the original filename of the top-level Sass file options[:original_filename] ||= options[:filename] options[:cache_store] ||= Sass::CacheStores::Chain.new( Sass::CacheStores::Memory.new, Sass::CacheStores::Filesystem.new(options[:cache_location])) # Support both, because the docs said one and the other actually worked # for quite a long time. options[:line_comments] ||= options[:line_numbers] options[:load_paths] = (options[:load_paths] + Sass.load_paths).map do |p| next p unless p.is_a?(String) || (defined?(Pathname) && p.is_a?(Pathname)) options[:filesystem_importer].new(p.to_s) end # Remove any deprecated importers if the location is imported explicitly options[:load_paths].reject! do |importer| importer.is_a?(Sass::Importers::DeprecatedPath) && options[:load_paths].find do |other_importer| other_importer.is_a?(Sass::Importers::Filesystem) && other_importer != importer && other_importer.root == importer.root end end # Backwards compatibility options[:property_syntax] ||= options[:attribute_syntax] case options[:property_syntax] when :alternate; options[:property_syntax] = :new when :normal; options[:property_syntax] = :old end options[:sourcemap] = :auto if options[:sourcemap] == true options[:sourcemap] = :none if options[:sourcemap] == false options end # Returns the {Sass::Engine} for the given file. # This is preferable to Sass::Engine.new when reading from a file # because it properly sets up the Engine's metadata, # enables parse-tree caching, # and infers the syntax from the filename. # # @param filename [String] The path to the Sass or SCSS file # @param options [{Symbol => Object}] The options hash; # See {file:SASS_REFERENCE.md#Options the Sass options documentation}. # @return [Sass::Engine] The Engine for the given Sass or SCSS file. # @raise [Sass::SyntaxError] if there's an error in the document. def self.for_file(filename, options) had_syntax = options[:syntax] if had_syntax # Use what was explicitly specified elsif filename =~ /\.scss$/ options.merge!(:syntax => :scss) elsif filename =~ /\.sass$/ options.merge!(:syntax => :sass) end Sass::Engine.new(File.read(filename), options.merge(:filename => filename)) end # The options for the Sass engine. # See {file:SASS_REFERENCE.md#Options the Sass options documentation}. # # @return [{Symbol => Object}] attr_reader :options # Creates a new Engine. Note that Engine should only be used directly # when compiling in-memory Sass code. # If you're compiling a single Sass file from the filesystem, # use \{Sass::Engine.for\_file}. # If you're compiling multiple files from the filesystem, # use {Sass::Plugin}. # # @param template [String] The Sass template. # This template can be encoded using any encoding # that can be converted to Unicode. # If the template contains an `@charset` declaration, # that overrides the Ruby encoding # (see {file:SASS_REFERENCE.md#Encodings the encoding documentation}) # @param options [{Symbol => Object}] An options hash. # See {file:SASS_REFERENCE.md#Options the Sass options documentation}. # @see {Sass::Engine.for_file} # @see {Sass::Plugin} def initialize(template, options = {}) @options = self.class.normalize_options(options) @template = template @checked_encoding = false @filename = nil @line = nil end # Render the template to CSS. # # @return [String] The CSS # @raise [Sass::SyntaxError] if there's an error in the document # @raise [Encoding::UndefinedConversionError] if the source encoding # cannot be converted to UTF-8 # @raise [ArgumentError] if the document uses an unknown encoding with `@charset` def render return _to_tree.render unless @options[:quiet] Sass::Util.silence_sass_warnings {_to_tree.render} end # Render the template to CSS and return the source map. # # @param sourcemap_uri [String] The sourcemap URI to use in the # `@sourceMappingURL` comment. If this is relative, it should be relative # to the location of the CSS file. # @return [(String, Sass::Source::Map)] The rendered CSS and the associated # source map # @raise [Sass::SyntaxError] if there's an error in the document, or if the # public URL for this document couldn't be determined. # @raise [Encoding::UndefinedConversionError] if the source encoding # cannot be converted to UTF-8 # @raise [ArgumentError] if the document uses an unknown encoding with `@charset` def render_with_sourcemap(sourcemap_uri) return _render_with_sourcemap(sourcemap_uri) unless @options[:quiet] Sass::Util.silence_sass_warnings {_render_with_sourcemap(sourcemap_uri)} end alias_method :to_css, :render # Parses the document into its parse tree. Memoized. # # @return [Sass::Tree::Node] The root of the parse tree. # @raise [Sass::SyntaxError] if there's an error in the document def to_tree @tree ||= if @options[:quiet] Sass::Util.silence_sass_warnings {_to_tree} else _to_tree end end # Returns the original encoding of the document. # # @return [Encoding, nil] # @raise [Encoding::UndefinedConversionError] if the source encoding # cannot be converted to UTF-8 # @raise [ArgumentError] if the document uses an unknown encoding with `@charset` def source_encoding check_encoding! @source_encoding end # Gets a set of all the documents # that are (transitive) dependencies of this document, # not including the document itself. # # @return [[Sass::Engine]] The dependency documents. def dependencies _dependencies(Set.new, engines = Set.new) Sass::Util.array_minus(engines, [self]) end # Helper for \{#dependencies}. # # @private def _dependencies(seen, engines) key = [@options[:filename], @options[:importer]] return if seen.include?(key) seen << key engines << self to_tree.grep(Tree::ImportNode) do |n| next if n.css_import? n.imported_file._dependencies(seen, engines) end end private def _render_with_sourcemap(sourcemap_uri) filename = @options[:filename] importer = @options[:importer] sourcemap_dir = @options[:sourcemap_filename] && File.dirname(File.expand_path(@options[:sourcemap_filename])) if filename.nil? raise Sass::SyntaxError.new(< e e.modify_backtrace(:filename => @options[:filename], :line => @line) e.sass_template = @template raise e end def sassc_key @options[:cache_store].key(*@options[:importer].key(@options[:filename], @options)) end def check_encoding! return if @checked_encoding @checked_encoding = true @template, @source_encoding = Sass::Util.check_sass_encoding(@template) end def tabulate(string) tab_str = nil comment_tab_str = nil first = true lines = [] string.scan(/^[^\n]*?$/).each_with_index do |line, index| index += (@options[:line] || 1) if line.strip.empty? lines.last.text << "\n" if lines.last && lines.last.comment? next end line_tab_str = line[/^\s*/] unless line_tab_str.empty? if tab_str.nil? comment_tab_str ||= line_tab_str next if try_comment(line, lines.last, "", comment_tab_str, index) comment_tab_str = nil end tab_str ||= line_tab_str raise SyntaxError.new("Indenting at the beginning of the document is illegal.", :line => index) if first raise SyntaxError.new("Indentation can't use both tabs and spaces.", :line => index) if tab_str.include?(?\s) && tab_str.include?(?\t) end first &&= !tab_str.nil? if tab_str.nil? lines << Line.new(line.strip, 0, index, 0, @options[:filename], []) next end comment_tab_str ||= line_tab_str if try_comment(line, lines.last, tab_str * lines.last.tabs, comment_tab_str, index) next else comment_tab_str = nil end line_tabs = line_tab_str.scan(tab_str).size if tab_str * line_tabs != line_tab_str message = < index) end lines << Line.new(line.strip, line_tabs, index, line_tab_str.size, @options[:filename], []) end lines end def try_comment(line, last, tab_str, comment_tab_str, index) return unless last && last.comment? # Nested comment stuff must be at least one whitespace char deeper # than the normal indentation return unless line =~ /^#{tab_str}\s/ unless line =~ /^(?:#{comment_tab_str})(.*)$/ raise SyntaxError.new(< index) Inconsistent indentation: previous line was indented by #{Sass::Shared.human_indentation comment_tab_str}, but this line was indented by #{Sass::Shared.human_indentation line[/^\s*/]}. MSG end last.comment_tab_str ||= comment_tab_str last.text << "\n" << line true end def tree(arr, i = 0) return [], i if arr[i].nil? base = arr[i].tabs nodes = [] while (line = arr[i]) && line.tabs >= base if line.tabs > base nodes.last.children, i = tree(arr, i) else nodes << line i += 1 end end return nodes, i end def build_tree(parent, line, root = false) @line = line.index @offset = line.offset node_or_nodes = parse_line(parent, line, root) Array(node_or_nodes).each do |node| # Node is a symbol if it's non-outputting, like a variable assignment next unless node.is_a? Tree::Node node.line = line.index node.filename = line.filename append_children(node, line.children, false) end node_or_nodes end def append_children(parent, children, root) continued_rule = nil continued_comment = nil children.each do |line| child = build_tree(parent, line, root) if child.is_a?(Tree::RuleNode) if child.continued? && child.children.empty? if continued_rule continued_rule.add_rules child else continued_rule = child end next elsif continued_rule continued_rule.add_rules child continued_rule.children = child.children continued_rule, child = nil, continued_rule end elsif continued_rule continued_rule = nil end if child.is_a?(Tree::CommentNode) && child.type == :silent if continued_comment && child.line == continued_comment.line + continued_comment.lines + 1 continued_comment.value.last.sub!(%r{ \*/\Z}, '') child.value.first.gsub!(%r{\A/\*}, ' *') continued_comment.value += ["\n"] + child.value next end continued_comment = child end check_for_no_children(child) validate_and_append_child(parent, child, line, root) end parent end def validate_and_append_child(parent, child, line, root) case child when Array child.each {|c| validate_and_append_child(parent, c, line, root)} when Tree::Node parent << child end end def check_for_no_children(node) return unless node.is_a?(Tree::RuleNode) && node.children.empty? Sass::Util.sass_warn(< @line) if name.nil? || value.nil? @@old_property_deprecation.warn(@options[:filename], @line, < @line + 1) end parser = Sass::SCSS::Parser.new(value, @options[:filename], @options[:importer], @line, to_parser_offset(@offset)) parsed_value = parser.parse_declaration_value end_offset = start_offset + value.length elsif value.strip.empty? parsed_value = [Sass::Script::Tree::Literal.new(Sass::Script::Value::String.new(""))] end_offset = start_offset else expr = parse_script(value, :offset => to_parser_offset(start_offset)) end_offset = expr.source_range.end_pos.offset - 1 parsed_value = [expr] end node = Tree::PropNode.new(parse_interp(name), parsed_value, prop) node.value_source_range = Sass::Source::Range.new( Sass::Source::Position.new(line.index, to_parser_offset(start_offset)), Sass::Source::Position.new(line.index, to_parser_offset(end_offset)), @options[:filename], @options[:importer]) if !node.custom_property? && value.strip.empty? && line.children.empty? raise SyntaxError.new( "Invalid property: \"#{node.declaration}\" (no value)." + node.pseudo_class_selector_message) end node end def parse_variable(line) name, value, flags = line.text.scan(Script::MATCH)[0] raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath variable declarations.", :line => @line + 1) unless line.children.empty? raise SyntaxError.new("Invalid variable: \"#{line.text}\".", :line => @line) unless name && value flags = flags ? flags.split(/\s+/) : [] if (invalid_flag = flags.find {|f| f != '!default' && f != '!global'}) raise SyntaxError.new("Invalid flag \"#{invalid_flag}\".", :line => @line) end # This workaround is needed for the case when the variable value is part of the identifier, # otherwise we end up with the offset equal to the value index inside the name: # $red_color: red; var_lhs_length = 1 + name.length # 1 stands for '$' index = line.text.index(value, line.offset + var_lhs_length) || 0 expr = parse_script(value, :offset => to_parser_offset(line.offset + index)) Tree::VariableNode.new(name, expr, flags.include?('!default'), flags.include?('!global')) end def parse_comment(line) if line.text[1] == CSS_COMMENT_CHAR || line.text[1] == SASS_COMMENT_CHAR silent = line.text[1] == SASS_COMMENT_CHAR loud = !silent && line.text[2] == SASS_LOUD_COMMENT_CHAR if silent value = [line.text] else value = self.class.parse_interp( line.text, line.index, to_parser_offset(line.offset), :filename => @filename) end value = Sass::Util.with_extracted_values(value) do |str| str = str.gsub(/^#{line.comment_tab_str}/m, '')[2..-1] # get rid of // or /* format_comment_text(str, silent) end type = if silent :silent elsif loud :loud else :normal end comment = Tree::CommentNode.new(value, type) comment.line = line.index text = line.text.rstrip if text.include?("\n") end_offset = text.length - text.rindex("\n") else end_offset = to_parser_offset(line.offset + text.length) end comment.source_range = Sass::Source::Range.new( Sass::Source::Position.new(@line, to_parser_offset(line.offset)), Sass::Source::Position.new(@line + text.count("\n"), end_offset), @options[:filename]) comment else Tree::RuleNode.new(parse_interp(line.text), full_line_range(line)) end end DIRECTIVES = Set[:mixin, :include, :function, :return, :debug, :warn, :for, :each, :while, :if, :else, :extend, :import, :media, :charset, :content, :at_root, :error] def parse_directive(parent, line, root) directive, whitespace, value = line.text[1..-1].split(/(\s+)/, 2) raise SyntaxError.new("Invalid directive: '@'.") unless directive offset = directive.size + whitespace.size + 1 if whitespace directive_name = directive.tr('-', '_').to_sym if DIRECTIVES.include?(directive_name) return send("parse_#{directive_name}_directive", parent, line, root, value, offset) end unprefixed_directive = directive.gsub(/^-[a-z0-9]+-/i, '') if unprefixed_directive == 'supports' parser = Sass::SCSS::Parser.new(value, @options[:filename], @line) return Tree::SupportsNode.new(directive, parser.parse_supports_condition) end Tree::DirectiveNode.new( value.nil? ? ["@#{directive}"] : ["@#{directive} "] + parse_interp(value, offset)) end def parse_while_directive(parent, line, root, value, offset) raise SyntaxError.new("Invalid while directive '@while': expected expression.") unless value Tree::WhileNode.new(parse_script(value, :offset => offset)) end def parse_if_directive(parent, line, root, value, offset) raise SyntaxError.new("Invalid if directive '@if': expected expression.") unless value Tree::IfNode.new(parse_script(value, :offset => offset)) end def parse_debug_directive(parent, line, root, value, offset) raise SyntaxError.new("Invalid debug directive '@debug': expected expression.") unless value raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath debug directives.", :line => @line + 1) unless line.children.empty? offset = line.offset + line.text.index(value).to_i Tree::DebugNode.new(parse_script(value, :offset => offset)) end def parse_error_directive(parent, line, root, value, offset) raise SyntaxError.new("Invalid error directive '@error': expected expression.") unless value raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath error directives.", :line => @line + 1) unless line.children.empty? offset = line.offset + line.text.index(value).to_i Tree::ErrorNode.new(parse_script(value, :offset => offset)) end def parse_extend_directive(parent, line, root, value, offset) raise SyntaxError.new("Invalid extend directive '@extend': expected expression.") unless value raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath extend directives.", :line => @line + 1) unless line.children.empty? optional = !!value.gsub!(/\s+#{Sass::SCSS::RX::OPTIONAL}$/, '') offset = line.offset + line.text.index(value).to_i interp_parsed = parse_interp(value, offset) selector_range = Sass::Source::Range.new( Sass::Source::Position.new(@line, to_parser_offset(offset)), Sass::Source::Position.new(@line, to_parser_offset(line.offset) + line.text.length), @options[:filename], @options[:importer] ) Tree::ExtendNode.new(interp_parsed, optional, selector_range) end def parse_warn_directive(parent, line, root, value, offset) raise SyntaxError.new("Invalid warn directive '@warn': expected expression.") unless value raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath warn directives.", :line => @line + 1) unless line.children.empty? offset = line.offset + line.text.index(value).to_i Tree::WarnNode.new(parse_script(value, :offset => offset)) end def parse_return_directive(parent, line, root, value, offset) raise SyntaxError.new("Invalid @return: expected expression.") unless value raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath return directives.", :line => @line + 1) unless line.children.empty? offset = line.offset + line.text.index(value).to_i Tree::ReturnNode.new(parse_script(value, :offset => offset)) end def parse_charset_directive(parent, line, root, value, offset) name = value && value[/\A(["'])(.*)\1\Z/, 2] # " raise SyntaxError.new("Invalid charset directive '@charset': expected string.") unless name raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath charset directives.", :line => @line + 1) unless line.children.empty? Tree::CharsetNode.new(name) end def parse_media_directive(parent, line, root, value, offset) parser = Sass::SCSS::Parser.new(value, @options[:filename], @options[:importer], @line, to_parser_offset(@offset)) offset = line.offset + line.text.index('media').to_i - 1 parsed_media_query_list = parser.parse_media_query_list.to_a node = Tree::MediaNode.new(parsed_media_query_list) node.source_range = Sass::Source::Range.new( Sass::Source::Position.new(@line, to_parser_offset(offset)), Sass::Source::Position.new(@line, to_parser_offset(line.offset) + line.text.length), @options[:filename], @options[:importer]) node end def parse_at_root_directive(parent, line, root, value, offset) return Sass::Tree::AtRootNode.new unless value if value.start_with?('(') parser = Sass::SCSS::Parser.new(value, @options[:filename], @options[:importer], @line, to_parser_offset(@offset)) offset = line.offset + line.text.index('at-root').to_i - 1 return Tree::AtRootNode.new(parser.parse_at_root_query) end at_root_node = Tree::AtRootNode.new parsed = parse_interp(value, offset) rule_node = Tree::RuleNode.new(parsed, full_line_range(line)) # The caller expects to automatically add children to the returned node # and we want it to add children to the rule node instead, so we # manually handle the wiring here and return nil so the caller doesn't # duplicate our efforts. append_children(rule_node, line.children, false) at_root_node << rule_node parent << at_root_node nil end def parse_for_directive(parent, line, root, value, offset) var, from_expr, to_name, to_expr = value.scan(/^([^\s]+)\s+from\s+(.+)\s+(to|through)\s+(.+)$/).first if var.nil? # scan failed, try to figure out why for error message if value !~ /^[^\s]+/ expected = "variable name" elsif value !~ /^[^\s]+\s+from\s+.+/ expected = "'from '" else expected = "'to ' or 'through '" end raise SyntaxError.new("Invalid for directive '@for #{value}': expected #{expected}.") end raise SyntaxError.new("Invalid variable \"#{var}\".") unless var =~ Script::VALIDATE var = var[1..-1] parsed_from = parse_script(from_expr, :offset => line.offset + line.text.index(from_expr)) parsed_to = parse_script(to_expr, :offset => line.offset + line.text.index(to_expr)) Tree::ForNode.new(var, parsed_from, parsed_to, to_name == 'to') end def parse_each_directive(parent, line, root, value, offset) vars, list_expr = value.scan(/^([^\s]+(?:\s*,\s*[^\s]+)*)\s+in\s+(.+)$/).first if vars.nil? # scan failed, try to figure out why for error message if value !~ /^[^\s]+/ expected = "variable name" elsif value !~ /^[^\s]+(?:\s*,\s*[^\s]+)*[^\s]+\s+from\s+.+/ expected = "'in '" end raise SyntaxError.new("Invalid each directive '@each #{value}': expected #{expected}.") end vars = vars.split(',').map do |var| var.strip! raise SyntaxError.new("Invalid variable \"#{var}\".") unless var =~ Script::VALIDATE var[1..-1] end parsed_list = parse_script(list_expr, :offset => line.offset + line.text.index(list_expr)) Tree::EachNode.new(vars, parsed_list) end def parse_else_directive(parent, line, root, value, offset) previous = parent.children.last raise SyntaxError.new("@else must come after @if.") unless previous.is_a?(Tree::IfNode) if value if value !~ /^if\s+(.+)/ raise SyntaxError.new("Invalid else directive '@else #{value}': expected 'if '.") end expr = parse_script($1, :offset => line.offset + line.text.index($1)) end node = Tree::IfNode.new(expr) append_children(node, line.children, false) previous.add_else node nil end def parse_import_directive(parent, line, root, value, offset) raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.", :line => @line + 1) unless line.children.empty? scanner = Sass::Util::MultibyteStringScanner.new(value) values = [] loop do unless (node = parse_import_arg(scanner, offset + scanner.pos)) raise SyntaxError.new( "Invalid @import: expected file to import, was #{scanner.rest.inspect}", :line => @line) end values << node break unless scanner.scan(/,\s*/) end if scanner.scan(/;/) raise SyntaxError.new("Invalid @import: expected end of line, was \";\".", :line => @line) end values end def parse_import_arg(scanner, offset) return if scanner.eos? if scanner.match?(/url\(/i) script_parser = Sass::Script::Parser.new(scanner, @line, to_parser_offset(offset), @options) str = script_parser.parse_string if scanner.eos? end_pos = str.source_range.end_pos node = Tree::CssImportNode.new(str) else supports_parser = Sass::SCSS::Parser.new(scanner, @options[:filename], @options[:importer], @line, str.source_range.end_pos.offset) supports_condition = supports_parser.parse_supports_clause if scanner.eos? node = Tree::CssImportNode.new(str, [], supports_condition) else media_parser = Sass::SCSS::Parser.new(scanner, @options[:filename], @options[:importer], @line, str.source_range.end_pos.offset) media = media_parser.parse_media_query_list end_pos = Sass::Source::Position.new(@line, media_parser.offset + 1) node = Tree::CssImportNode.new(str, media.to_a, supports_condition) end end node.source_range = Sass::Source::Range.new( str.source_range.start_pos, end_pos, @options[:filename], @options[:importer]) return node end unless (quoted_val = scanner.scan(Sass::SCSS::RX::STRING)) scanned = scanner.scan(/[^,;]+/) node = Tree::ImportNode.new(scanned) start_parser_offset = to_parser_offset(offset) node.source_range = Sass::Source::Range.new( Sass::Source::Position.new(@line, start_parser_offset), Sass::Source::Position.new(@line, start_parser_offset + scanned.length), @options[:filename], @options[:importer]) return node end start_offset = offset offset += scanner.matched.length val = Sass::Script::Value::String.value(scanner[1] || scanner[2]) scanned = scanner.scan(/\s*/) if !scanner.match?(/[,;]|$/) offset += scanned.length if scanned media_parser = Sass::SCSS::Parser.new(scanner, @options[:filename], @options[:importer], @line, offset) media = media_parser.parse_media_query_list node = Tree::CssImportNode.new(quoted_val, media.to_a) node.source_range = Sass::Source::Range.new( Sass::Source::Position.new(@line, to_parser_offset(start_offset)), Sass::Source::Position.new(@line, media_parser.offset), @options[:filename], @options[:importer]) elsif val =~ %r{^(https?:)?//} node = Tree::CssImportNode.new(quoted_val) node.source_range = Sass::Source::Range.new( Sass::Source::Position.new(@line, to_parser_offset(start_offset)), Sass::Source::Position.new(@line, to_parser_offset(offset)), @options[:filename], @options[:importer]) else node = Tree::ImportNode.new(val) node.source_range = Sass::Source::Range.new( Sass::Source::Position.new(@line, to_parser_offset(start_offset)), Sass::Source::Position.new(@line, to_parser_offset(offset)), @options[:filename], @options[:importer]) end node end def parse_mixin_directive(parent, line, root, value, offset) parse_mixin_definition(line) end MIXIN_DEF_RE = /^(?:=|@mixin)\s*(#{Sass::SCSS::RX::IDENT})(.*)$/ def parse_mixin_definition(line) name, arg_string = line.text.scan(MIXIN_DEF_RE).first raise SyntaxError.new("Invalid mixin \"#{line.text[1..-1]}\".") if name.nil? offset = line.offset + line.text.size - arg_string.size args, splat = Script::Parser.new(arg_string.strip, @line, to_parser_offset(offset), @options). parse_mixin_definition_arglist Tree::MixinDefNode.new(name, args, splat) end CONTENT_RE = /^@content\s*(.+)?$/ def parse_content_directive(parent, line, root, value, offset) trailing = line.text.scan(CONTENT_RE).first.first unless trailing.nil? raise SyntaxError.new( "Invalid content directive. Trailing characters found: \"#{trailing}\".") end raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath @content directives.", :line => line.index + 1) unless line.children.empty? Tree::ContentNode.new end def parse_include_directive(parent, line, root, value, offset) parse_mixin_include(line, root) end MIXIN_INCLUDE_RE = /^(?:\+|@include)\s*(#{Sass::SCSS::RX::IDENT})(.*)$/ def parse_mixin_include(line, root) name, arg_string = line.text.scan(MIXIN_INCLUDE_RE).first raise SyntaxError.new("Invalid mixin include \"#{line.text}\".") if name.nil? offset = line.offset + line.text.size - arg_string.size args, keywords, splat, kwarg_splat = Script::Parser.new(arg_string.strip, @line, to_parser_offset(offset), @options). parse_mixin_include_arglist Tree::MixinNode.new(name, args, keywords, splat, kwarg_splat) end FUNCTION_RE = /^@function\s*(#{Sass::SCSS::RX::IDENT})(.*)$/ def parse_function_directive(parent, line, root, value, offset) name, arg_string = line.text.scan(FUNCTION_RE).first raise SyntaxError.new("Invalid function definition \"#{line.text}\".") if name.nil? offset = line.offset + line.text.size - arg_string.size args, splat = Script::Parser.new(arg_string.strip, @line, to_parser_offset(offset), @options). parse_function_definition_arglist Tree::FunctionNode.new(name, args, splat) end def parse_script(script, options = {}) line = options[:line] || @line offset = options[:offset] || @offset + 1 Script.parse(script, line, offset, @options) end def format_comment_text(text, silent) content = text.split("\n") if content.first && content.first.strip.empty? removed_first = true content.shift end return "/* */" if content.empty? content.last.gsub!(%r{ ?\*/ *$}, '') first = content.shift unless removed_first content.map! {|l| l.gsub!(/^\*( ?)/, '\1') || (l.empty? ? "" : " ") + l} content.unshift first unless removed_first if silent "/*" + content.join("\n *") + " */" else # The #gsub fixes the case of a trailing */ "/*" + content.join("\n *").gsub(/ \*\Z/, '') + " */" end end def parse_interp(text, offset = 0) self.class.parse_interp(text, @line, offset, :filename => @filename) end # Parser tracks 1-based line and offset, so our offset should be converted. def to_parser_offset(offset) offset + 1 end def full_line_range(line) Sass::Source::Range.new( Sass::Source::Position.new(@line, to_parser_offset(line.offset)), Sass::Source::Position.new(@line, to_parser_offset(line.offset) + line.text.length), @options[:filename], @options[:importer]) end # It's important that this have strings (at least) # at the beginning, the end, and between each Script::Tree::Node. # # @private def self.parse_interp(text, line, offset, options) res = [] rest = Sass::Shared.handle_interpolation text do |scan| escapes = scan[2].size res << scan.matched[0...-2 - escapes] if escapes.odd? res << "\\" * (escapes - 1) << '#{' else res << "\\" * [0, escapes - 1].max if scan[1].include?("\n") line += scan[1].count("\n") offset = scan.matched_size - scan[1].rindex("\n") else offset += scan.matched_size end node = Script::Parser.new(scan, line, offset, options).parse_interpolated offset = node.source_range.end_pos.offset res << node end end res << rest end end end ruby-sass-3.7.4/lib/sass/environment.rb000066400000000000000000000152411345125207600200750ustar00rootroot00000000000000require 'set' module Sass # The abstract base class for lexical environments for SassScript. class BaseEnvironment class << self # Note: when updating this, # update sass/yard/inherited_hash.rb as well. def inherited_hash_accessor(name) inherited_hash_reader(name) inherited_hash_writer(name) end def inherited_hash_reader(name) class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{name}(name) _#{name}(name.tr('_', '-')) end def _#{name}(name) (@#{name}s && @#{name}s[name]) || @parent && @parent._#{name}(name) end protected :_#{name} def is_#{name}_global?(name) return !@parent if @#{name}s && @#{name}s.has_key?(name) @parent && @parent.is_#{name}_global?(name) end RUBY end def inherited_hash_writer(name) class_eval <<-RUBY, __FILE__, __LINE__ + 1 def set_#{name}(name, value) name = name.tr('_', '-') @#{name}s[name] = value unless try_set_#{name}(name, value) end def try_set_#{name}(name, value) @#{name}s ||= {} if @#{name}s.include?(name) @#{name}s[name] = value true elsif @parent && !@parent.global? @parent.try_set_#{name}(name, value) else false end end protected :try_set_#{name} def set_local_#{name}(name, value) @#{name}s ||= {} @#{name}s[name.tr('_', '-')] = value end def set_global_#{name}(name, value) global_env.set_#{name}(name, value) end RUBY end end # The options passed to the Sass Engine. attr_reader :options attr_writer :caller attr_writer :content attr_writer :selector # variable # Script::Value inherited_hash_reader :var # mixin # Sass::Callable inherited_hash_reader :mixin # function # Sass::Callable inherited_hash_reader :function # @param options [{Symbol => Object}] The options hash. See # {file:SASS_REFERENCE.md#Options the Sass options documentation}. # @param parent [Environment] See \{#parent} def initialize(parent = nil, options = nil) @parent = parent @options = options || (parent && parent.options) || {} @stack = @parent.nil? ? Sass::Stack.new : nil @caller = nil @content = nil @filename = nil @functions = nil @mixins = nil @selector = nil @vars = nil end # Returns whether this is the global environment. # # @return [Boolean] def global? @parent.nil? end # The environment of the caller of this environment's mixin or function. # @return {Environment?} def caller @caller || (@parent && @parent.caller) end # The content passed to this environment. This is naturally only set # for mixin body environments with content passed in. # # @return {[Array, Environment]?} The content nodes and # the lexical environment of the content block. def content @content || (@parent && @parent.content) end # The selector for the current CSS rule, or nil if there is no # current CSS rule. # # @return [Selector::CommaSequence?] The current selector, with any # nesting fully resolved. def selector @selector || (@caller && @caller.selector) || (@parent && @parent.selector) end # The top-level Environment object. # # @return [Environment] def global_env @global_env ||= global? ? self : @parent.global_env end # The import/mixin stack. # # @return [Sass::Stack] def stack @stack || global_env.stack end end # The lexical environment for SassScript. # This keeps track of variable, mixin, and function definitions. # # A new environment is created for each level of Sass nesting. # This allows variables to be lexically scoped. # The new environment refers to the environment in the upper scope, # so it has access to variables defined in enclosing scopes, # but new variables are defined locally. # # Environment also keeps track of the {Engine} options # so that they can be made available to {Sass::Script::Functions}. class Environment < BaseEnvironment # The enclosing environment, # or nil if this is the global environment. # # @return [Environment] attr_reader :parent # variable # Script::Value inherited_hash_writer :var # mixin # Sass::Callable inherited_hash_writer :mixin # function # Sass::Callable inherited_hash_writer :function end # A read-only wrapper for a lexical environment for SassScript. class ReadOnlyEnvironment < BaseEnvironment def initialize(parent = nil, options = nil) super @content_cached = nil end # The read-only environment of the caller of this environment's mixin or function. # # @see BaseEnvironment#caller # @return {ReadOnlyEnvironment} def caller return @caller if @caller env = super @caller ||= env.is_a?(ReadOnlyEnvironment) ? env : ReadOnlyEnvironment.new(env, env.options) end # The content passed to this environment. If the content's environment isn't already # read-only, it's made read-only. # # @see BaseEnvironment#content # # @return {[Array, ReadOnlyEnvironment]?} The content nodes and # the lexical environment of the content block. # Returns `nil` when there is no content in this environment. def content # Return the cached content from a previous invocation if any return @content if @content_cached # get the content with a read-write environment from the superclass read_write_content = super if read_write_content tree, env = read_write_content # make the content's environment read-only if env && !env.is_a?(ReadOnlyEnvironment) env = ReadOnlyEnvironment.new(env, env.options) end @content_cached = true @content = [tree, env] else @content_cached = true @content = nil end end end # An environment that can write to in-scope global variables, but doesn't # create new variables in the global scope. Useful for top-level control # directives. class SemiGlobalEnvironment < Environment def try_set_var(name, value) @vars ||= {} if @vars.include?(name) @vars[name] = value true elsif @parent @parent.try_set_var(name, value) else false end end end end ruby-sass-3.7.4/lib/sass/error.rb000066400000000000000000000151411345125207600166610ustar00rootroot00000000000000module Sass # An exception class that keeps track of # the line of the Sass template it was raised on # and the Sass file that was being parsed (if applicable). # # All Sass errors are raised as {Sass::SyntaxError}s. # # When dealing with SyntaxErrors, # it's important to provide filename and line number information. # This will be used in various error reports to users, including backtraces; # see \{#sass\_backtrace} for details. # # Some of this information is usually provided as part of the constructor. # New backtrace entries can be added with \{#add\_backtrace}, # which is called when an exception is raised between files (e.g. with `@import`). # # Often, a chunk of code will all have similar backtrace information - # the same filename or even line. # It may also be useful to have a default line number set. # In those situations, the default values can be used # by omitting the information on the original exception, # and then calling \{#modify\_backtrace} in a wrapper `rescue`. # When doing this, be sure that all exceptions ultimately end up # with the information filled in. class SyntaxError < StandardError # The backtrace of the error within Sass files. # This is an array of hashes containing information for a single entry. # The hashes have the following keys: # # `:filename` # : The name of the file in which the exception was raised, # or `nil` if no filename is available. # # `:mixin` # : The name of the mixin in which the exception was raised, # or `nil` if it wasn't raised in a mixin. # # `:line` # : The line of the file on which the error occurred. Never nil. # # This information is also included in standard backtrace format # in the output of \{#backtrace}. # # @return [Aray<{Symbol => Object>}] attr_accessor :sass_backtrace # The text of the template where this error was raised. # # @return [String] attr_accessor :sass_template # @param msg [String] The error message # @param attrs [{Symbol => Object}] The information in the backtrace entry. # See \{#sass\_backtrace} def initialize(msg, attrs = {}) @message = msg @sass_backtrace = [] add_backtrace(attrs) end # The name of the file in which the exception was raised. # This could be `nil` if no filename is available. # # @return [String, nil] def sass_filename sass_backtrace.first[:filename] end # The name of the mixin in which the error occurred. # This could be `nil` if the error occurred outside a mixin. # # @return [String] def sass_mixin sass_backtrace.first[:mixin] end # The line of the Sass template on which the error occurred. # # @return [Integer] def sass_line sass_backtrace.first[:line] end # Adds an entry to the exception's Sass backtrace. # # @param attrs [{Symbol => Object}] The information in the backtrace entry. # See \{#sass\_backtrace} def add_backtrace(attrs) sass_backtrace << attrs.reject {|_k, v| v.nil?} end # Modify the top Sass backtrace entries # (that is, the most deeply nested ones) # to have the given attributes. # # Specifically, this goes through the backtrace entries # from most deeply nested to least, # setting the given attributes for each entry. # If an entry already has one of the given attributes set, # the pre-existing attribute takes precedence # and is not used for less deeply-nested entries # (even if they don't have that attribute set). # # @param attrs [{Symbol => Object}] The information to add to the backtrace entry. # See \{#sass\_backtrace} def modify_backtrace(attrs) attrs = attrs.reject {|_k, v| v.nil?} # Move backwards through the backtrace (0...sass_backtrace.size).to_a.reverse_each do |i| entry = sass_backtrace[i] sass_backtrace[i] = attrs.merge(entry) attrs.reject! {|k, _v| entry.include?(k)} break if attrs.empty? end end # @return [String] The error message def to_s @message end # Returns the standard exception backtrace, # including the Sass backtrace. # # @return [Array] def backtrace return nil if super.nil? return super if sass_backtrace.all? {|h| h.empty?} sass_backtrace.map do |h| "#{h[:filename] || '(sass)'}:#{h[:line]}" + (h[:mixin] ? ":in `#{h[:mixin]}'" : "") end + super end # Returns a string representation of the Sass backtrace. # # @param default_filename [String] The filename to use for unknown files # @see #sass_backtrace # @return [String] def sass_backtrace_str(default_filename = "an unknown file") lines = message.split("\n") msg = lines[0] + lines[1..-1]. map {|l| "\n" + (" " * "Error: ".size) + l}.join "Error: #{msg}" + sass_backtrace.each_with_index.map do |entry, i| "\n #{i == 0 ? 'on' : 'from'} line #{entry[:line]}" + " of #{entry[:filename] || default_filename}" + (entry[:mixin] ? ", in `#{entry[:mixin]}'" : "") end.join end class << self # Returns an error report for an exception in CSS format. # # @param e [Exception] # @param line_offset [Integer] The number of the first line of the Sass template. # @return [String] The error report # @raise [Exception] `e`, if the # {file:SASS_REFERENCE.md#full_exception-option `:full_exception`} option # is set to false. def exception_to_css(e, line_offset = 1) header = header_string(e, line_offset) <] The command-line arguments def initialize(args) @args = args @options = {} end # Parses the command-line arguments and runs the executable. # Calls `Kernel#exit` at the end, so it never returns. # # @see #parse def parse! begin parse rescue Exception => e # Exit code 65 indicates invalid data per # http://www.freebsd.org/cgi/man.cgi?query=sysexits. Setting it via # at_exit is a bit of a hack, but it allows us to rethrow when --trace # is active and get both the built-in exception formatting and the # correct exit code. at_exit {exit Sass::Util.windows? ? 13 : 65} if e.is_a?(Sass::SyntaxError) raise e if @options[:trace] || e.is_a?(SystemExit) if e.is_a?(Sass::SyntaxError) $stderr.puts e.sass_backtrace_str("standard input") else $stderr.print "#{e.class}: " unless e.class == RuntimeError $stderr.puts e.message.to_s end $stderr.puts " Use --trace for backtrace." exit 1 end exit 0 end # Parses the command-line arguments and runs the executable. # This does not handle exceptions or exit the program. # # @see #parse! def parse @opts = OptionParser.new(&method(:set_opts)) @opts.parse!(@args) process_result @options end # @return [String] A description of the executable def to_s @opts.to_s end protected # Finds the line of the source template # on which an exception was raised. # # @param exception [Exception] The exception # @return [String] The line number def get_line(exception) # SyntaxErrors have weird line reporting # when there's trailing whitespace if exception.is_a?(::SyntaxError) return (exception.message.scan(/:(\d+)/).first || ["??"]).first end (exception.backtrace[0].scan(/:(\d+)/).first || ["??"]).first end # Tells optparse how to parse the arguments # available for all executables. # # This is meant to be overridden by subclasses # so they can add their own options. # # @param opts [OptionParser] def set_opts(opts) Sass::Util.abstract(this) end # Set an option for specifying `Encoding.default_external`. # # @param opts [OptionParser] def encoding_option(opts) encoding_desc = 'Specify the default encoding for input files.' opts.on('-E', '--default-encoding ENCODING', encoding_desc) do |encoding| Encoding.default_external = encoding end end # Processes the options set by the command-line arguments. In particular, # sets `@options[:input]` and `@options[:output]` to appropriate IO streams. # # This is meant to be overridden by subclasses # so they can run their respective programs. def process_result input, output = @options[:input], @options[:output] args = @args.dup input ||= begin filename = args.shift @options[:filename] = filename open_file(filename) || $stdin end @options[:output_filename] = args.shift output ||= @options[:output_filename] || $stdout @options[:input], @options[:output] = input, output end COLORS = {:red => 31, :green => 32, :yellow => 33} # Prints a status message about performing the given action, # colored using the given color (via terminal escapes) if possible. # # @param name [#to_s] A short name for the action being performed. # Shouldn't be longer than 11 characters. # @param color [Symbol] The name of the color to use for this action. # Can be `:red`, `:green`, or `:yellow`. def puts_action(name, color, arg) return if @options[:for_engine][:quiet] printf color(color, "%11s %s\n"), name, arg STDOUT.flush end # Same as `Kernel.puts`, but doesn't print anything if the `--quiet` option is set. # # @param args [Array] Passed on to `Kernel.puts` def puts(*args) return if @options[:for_engine][:quiet] Kernel.puts(*args) end # Wraps the given string in terminal escapes # causing it to have the given color. # If terminal escapes aren't supported on this platform, # just returns the string instead. # # @param color [Symbol] The name of the color to use. # Can be `:red`, `:green`, or `:yellow`. # @param str [String] The string to wrap in the given color. # @return [String] The wrapped string. def color(color, str) raise "[BUG] Unrecognized color #{color}" unless COLORS[color] # Almost any real Unix terminal will support color, # so we just filter for Windows terms (which don't set TERM) # and not-real terminals, which aren't ttys. return str if ENV["TERM"].nil? || ENV["TERM"].empty? || !STDOUT.tty? "\e[#{COLORS[color]}m#{str}\e[0m" end def write_output(text, destination) if destination.is_a?(String) open_file(destination, 'w') {|file| file.write(text)} else destination.write(text) end end private def open_file(filename, flag = 'r') return if filename.nil? flag = 'wb' if @options[:unix_newlines] && flag == 'w' file = File.open(filename, flag) return file unless block_given? yield file file.close end def handle_load_error(err) dep = err.message[/^no such file to load -- (.*)/, 1] raise err if @options[:trace] || dep.nil? || dep.empty? $stderr.puts <] The command-line arguments def initialize(args) super require 'sass' @options[:for_tree] = {} @options[:for_engine] = {:cache => false, :read_cache => true} end # Tells optparse how to parse the arguments. # # @param opts [OptionParser] def set_opts(opts) opts.banner = < e raise e if @options[:trace] file = " of #{e.sass_filename}" if e.sass_filename raise "Error on line #{e.sass_line}#{file}: #{e.message}\n Use --trace for backtrace" rescue LoadError => err handle_load_error(err) end def path_for(file) return file.path if file.is_a?(File) return file if file.is_a?(String) end def read(file) if file.respond_to?(:read) file.read else open(file, 'rb') {|f| f.read} end end end end ruby-sass-3.7.4/lib/sass/exec/sass_scss.rb000066400000000000000000000345351345125207600204700ustar00rootroot00000000000000module Sass::Exec # The `sass` and `scss` executables. class SassScss < Base attr_reader :default_syntax # @param args [Array] The command-line arguments def initialize(args, default_syntax) super(args) @options[:sourcemap] = :auto @options[:for_engine] = { :load_paths => default_sass_path } @default_syntax = default_syntax end protected # Tells optparse how to parse the arguments. # # @param opts [OptionParser] def set_opts(opts) opts.banner = <>> Sass is watching for changes. Press Ctrl-C to stop." Sass::Plugin.on_template_modified do |template| puts ">>> Change detected to: #{template}" STDOUT.flush end Sass::Plugin.on_template_created do |template| puts ">>> New template detected: #{template}" STDOUT.flush end Sass::Plugin.on_template_deleted do |template| puts ">>> Deleted template detected: #{template}" STDOUT.flush end Sass::Plugin.watch(files) end def run input = @options[:input] output = @options[:output] if input == $stdin # See issue 1745 (@options[:for_engine][:load_paths] ||= []) << ::Sass::Importers::DeprecatedPath.new(".") end @options[:for_engine][:syntax] ||= :scss if input.is_a?(File) && input.path =~ /\.scss$/ @options[:for_engine][:syntax] ||= @default_syntax engine = if input.is_a?(File) && !@options[:check_syntax] Sass::Engine.for_file(input.path, @options[:for_engine]) else # We don't need to do any special handling of @options[:check_syntax] here, # because the Sass syntax checking happens alongside evaluation # and evaluation doesn't actually evaluate any code anyway. Sass::Engine.new(input.read, @options[:for_engine]) end input.close if input.is_a?(File) if @options[:sourcemap] != :none && @options[:sourcemap_filename] relative_sourcemap_path = Sass::Util.relative_path_from( @options[:sourcemap_filename], Sass::Util.pathname(@options[:output_filename]).dirname) rendered, mapping = engine.render_with_sourcemap(relative_sourcemap_path.to_s) write_output(rendered, output) write_output( mapping.to_json( :type => @options[:sourcemap], :css_path => @options[:output_filename], :sourcemap_path => @options[:sourcemap_filename]) + "\n", @options[:sourcemap_filename]) else write_output(engine.render, output) end rescue Sass::SyntaxError => e write_output(Sass::SyntaxError.exception_to_css(e), output) if output.is_a?(String) raise e ensure output.close if output.is_a? File end def colon_path?(path) !split_colon_path(path)[1].nil? end def split_colon_path(path) one, two = path.split(':', 2) if one && two && Sass::Util.windows? && one =~ /\A[A-Za-z]\Z/ && two =~ %r{\A[/\\]} # If we're on Windows and we were passed a drive letter path, # don't split on that colon. one2, two = two.split(':', 2) one = one + ':' + one2 end return one, two end # Whether path is likely to be meant as the destination # in a source:dest pair. def probably_dest_dir?(path) return false unless path return false if colon_path?(path) Sass::Util.glob(File.join(path, "*.s[ca]ss")).empty? end def default_sass_path return unless ENV['SASS_PATH'] # The select here prevents errors when the environment's # load paths specified do not exist. ENV['SASS_PATH'].split(File::PATH_SEPARATOR).select {|d| File.directory?(d)} end end end ruby-sass-3.7.4/lib/sass/features.rb000066400000000000000000000030331345125207600173430ustar00rootroot00000000000000require 'set' module Sass # Provides `Sass.has_feature?` which allows for simple feature detection # by providing a feature name. module Features # This is the set of features that can be detected. # # When this is updated, the documentation of `feature-exists()` should be # updated as well. KNOWN_FEATURES = Set[*%w( global-variable-shadowing extend-selector-pseudoclass units-level-3 at-error custom-property )] # Check if a feature exists by name. This is used to implement # the Sass function `feature-exists($feature)` # # @param feature_name [String] The case sensitive name of the feature to # check if it exists in this version of Sass. # @return [Boolean] whether the feature of that name exists. def has_feature?(feature_name) KNOWN_FEATURES.include?(feature_name) end # Add a feature to Sass. Plugins can use this to easily expose their # availability to end users. Plugins must prefix their feature # names with a dash to distinguish them from official features. # # @example # Sass.add_feature("-import-globbing") # Sass.add_feature("-math-cos") # # # @param feature_name [String] The case sensitive name of the feature to # to add to Sass. Must begin with a dash. def add_feature(feature_name) unless feature_name[0] == ?- raise ArgumentError.new("Plugin feature names must begin with a dash") end KNOWN_FEATURES << feature_name end end extend Features end ruby-sass-3.7.4/lib/sass/importers.rb000066400000000000000000000016341345125207600175560ustar00rootroot00000000000000module Sass # Sass importers are in charge of taking paths passed to `@import` # and finding the appropriate Sass code for those paths. # By default, this code is always loaded from the filesystem, # but importers could be added to load from a database or over HTTP. # # Each importer is in charge of a single load path # (or whatever the corresponding notion is for the backend). # Importers can be placed in the {file:SASS_REFERENCE.md#load_paths-option `:load_paths` array} # alongside normal filesystem paths. # # When resolving an `@import`, Sass will go through the load paths # looking for an importer that successfully imports the path. # Once one is found, the imported file is used. # # User-created importers must inherit from {Importers::Base}. module Importers end end require 'sass/importers/base' require 'sass/importers/filesystem' require 'sass/importers/deprecated_path' ruby-sass-3.7.4/lib/sass/importers/000077500000000000000000000000001345125207600172255ustar00rootroot00000000000000ruby-sass-3.7.4/lib/sass/importers/base.rb000066400000000000000000000175011345125207600204700ustar00rootroot00000000000000module Sass module Importers # The abstract base class for Sass importers. # All importers should inherit from this. # # At the most basic level, an importer is given a string # and must return a {Sass::Engine} containing some Sass code. # This string can be interpreted however the importer wants; # however, subclasses are encouraged to use the URI format # for pathnames. # # Importers that have some notion of "relative imports" # should take a single load path in their constructor, # and interpret paths as relative to that. # They should also implement the \{#find\_relative} method. # # Importers should be serializable via `Marshal.dump`. # # @abstract class Base # Find a Sass file relative to another file. # Importers without a notion of "relative paths" # should just return nil here. # # If the importer does have a notion of "relative paths", # it should ignore its load path during this method. # # See \{#find} for important information on how this method should behave. # # The `:filename` option passed to the returned {Sass::Engine} # should be of a format that could be passed to \{#find}. # # @param uri [String] The URI to import. This is not necessarily relative, # but this method should only return true if it is. # @param base [String] The base filename. If `uri` is relative, # it should be interpreted as relative to `base`. # `base` is guaranteed to be in a format importable by this importer. # @param options [{Symbol => Object}] Options for the Sass file # containing the `@import` that's currently being resolved. # @return [Sass::Engine, nil] An Engine containing the imported file, # or nil if it couldn't be found or was in the wrong format. def find_relative(uri, base, options) Sass::Util.abstract(self) end # Find a Sass file, if it exists. # # This is the primary entry point of the Importer. # It corresponds directly to an `@import` statement in Sass. # It should do three basic things: # # * Determine if the URI is in this importer's format. # If not, return nil. # * Determine if the file indicated by the URI actually exists and is readable. # If not, return nil. # * Read the file and place the contents in a {Sass::Engine}. # Return that engine. # # If this importer's format allows for file extensions, # it should treat them the same way as the default {Filesystem} importer. # If the URI explicitly has a `.sass` or `.scss` filename, # the importer should look for that exact file # and import it as the syntax indicated. # If it doesn't exist, the importer should return nil. # # If the URI doesn't have either of these extensions, # the importer should look for files with the extensions. # If no such files exist, it should return nil. # # The {Sass::Engine} to be returned should be passed `options`, # with a few modifications. `:syntax` should be set appropriately, # `:filename` should be set to `uri`, # and `:importer` should be set to this importer. # # @param uri [String] The URI to import. # @param options [{Symbol => Object}] Options for the Sass file # containing the `@import` that's currently being resolved. # This is safe for subclasses to modify destructively. # Callers should only pass in a value they don't mind being destructively modified. # @return [Sass::Engine, nil] An Engine containing the imported file, # or nil if it couldn't be found or was in the wrong format. def find(uri, options) Sass::Util.abstract(self) end # Returns the time the given Sass file was last modified. # # If the given file has been deleted or the time can't be accessed # for some other reason, this should return nil. # # @param uri [String] The URI of the file to check. # Comes from a `:filename` option set on an engine returned by this importer. # @param options [{Symbol => Object}] Options for the Sass file # containing the `@import` currently being checked. # @return [Time, nil] def mtime(uri, options) Sass::Util.abstract(self) end # Get the cache key pair for the given Sass URI. # The URI need not be checked for validity. # # The only strict requirement is that the returned pair of strings # uniquely identify the file at the given URI. # However, the first component generally corresponds roughly to the directory, # and the second to the basename, of the URI. # # Note that keys must be unique *across importers*. # Thus it's probably a good idea to include the importer name # at the beginning of the first component. # # @param uri [String] A URI known to be valid for this importer. # @param options [{Symbol => Object}] Options for the Sass file # containing the `@import` currently being checked. # @return [(String, String)] The key pair which uniquely identifies # the file at the given URI. def key(uri, options) Sass::Util.abstract(self) end # Get the publicly-visible URL for an imported file. This URL is used by # source maps to link to the source stylesheet. This may return `nil` to # indicate that no public URL is available; however, this will cause # sourcemap generation to fail if any CSS is generated from files imported # from this importer. # # If an absolute "file:" URI can be produced for an imported file, that # should be preferred to returning `nil`. However, a URL relative to # `sourcemap_directory` should be preferred over an absolute "file:" URI. # # @param uri [String] A URI known to be valid for this importer. # @param sourcemap_directory [String, NilClass] The absolute path to a # directory on disk where the sourcemap will be saved. If uri refers to # a file on disk that's accessible relative to sourcemap_directory, this # may return a relative URL. This may be `nil` if the sourcemap's # eventual location is unknown. # @return [String?] The publicly-visible URL for this file, or `nil` # indicating that no publicly-visible URL exists. This should be # appropriately URL-escaped. def public_url(uri, sourcemap_directory) return if @public_url_warning_issued @public_url_warning_issued = true Sass::Util.sass_warn <] List of absolute paths of directories to watch def directories_to_watch [] end # If this importer is based on files on the local filesystem This method # should return true if the file, when changed, should trigger a # recompile. # # It is acceptable for non-sass files to be watched and trigger a recompile. # # @param filename [String] The absolute filename for a file that has changed. # @return [Boolean] When the file changed should cause a recompile. def watched_file?(filename) false end end end end ruby-sass-3.7.4/lib/sass/importers/deprecated_path.rb000066400000000000000000000031441345125207600226700ustar00rootroot00000000000000module Sass module Importers # This importer emits a deprecation warning the first time it is used to # import a file. It is used to deprecate the current working # directory from the list of automatic sass load paths. class DeprecatedPath < Filesystem # @param root [String] The absolute, expanded path to the folder that is deprecated. def initialize(root) @specified_root = root @warning_given = false super end # @see Sass::Importers::Base#find def find(*args) found = super if found && !@warning_given @warning_given = true Sass::Util.sass_warn deprecation_warning end found end # @see Base#directories_to_watch def directories_to_watch # The current working directory was not watched in Sass 3.2, # so we continue not to watch it while it's deprecated. [] end # @see Sass::Importers::Base#to_s def to_s "#{@root} (DEPRECATED)" end protected # @return [String] The deprecation warning that will be printed the first # time an import occurs. def deprecation_warning path = @specified_root == "." ? "the current working directory" : @specified_root < Symbol}] def extensions {'sass' => :sass, 'scss' => :scss} end # Given an `@import`ed path, returns an array of possible # on-disk filenames and their corresponding syntaxes for that path. # # @param name [String] The filename. # @return [Array(String, Symbol)] An array of pairs. # The first element of each pair is a filename to look for; # the second element is the syntax that file would be in (`:sass` or `:scss`). def possible_files(name) name = escape_glob_characters(name) dirname, basename, extname = split(name) sorted_exts = extensions.sort syntax = extensions[extname] if syntax ret = [["#{dirname}/{_,}#{basename}.#{extensions.invert[syntax]}", syntax]] else ret = sorted_exts.map {|ext, syn| ["#{dirname}/{_,}#{basename}.#{ext}", syn]} end # JRuby chokes when trying to import files from JARs when the path starts with './'. ret.map {|f, s| [f.sub(%r{^\./}, ''), s]} end def escape_glob_characters(name) name.gsub(/[\*\[\]\{\}\?]/) do |char| "\\#{char}" end end REDUNDANT_DIRECTORY = /#{Regexp.escape(File::SEPARATOR)}\.#{Regexp.escape(File::SEPARATOR)}/ # Given a base directory and an `@import`ed name, # finds an existent file that matches the name. # # @param dir [String] The directory relative to which to search. # @param name [String] The filename to search for. # @return [(String, Symbol)] A filename-syntax pair. def find_real_file(dir, name, options) # On windows 'dir' or 'name' can be in native File::ALT_SEPARATOR form. dir = dir.gsub(File::ALT_SEPARATOR, File::SEPARATOR) unless File::ALT_SEPARATOR.nil? name = name.gsub(File::ALT_SEPARATOR, File::SEPARATOR) unless File::ALT_SEPARATOR.nil? found = possible_files(remove_root(name)).map do |f, s| path = if dir == "." || Sass::Util.pathname(f).absolute? f else "#{escape_glob_characters(dir)}/#{f}" end Dir[path].map do |full_path| full_path.gsub!(REDUNDANT_DIRECTORY, File::SEPARATOR) [Sass::Util.cleanpath(full_path).to_s, s] end end.flatten(1) if found.empty? && split(name)[2].nil? && File.directory?("#{dir}/#{name}") return find_real_file("#{dir}/#{name}", "index", options) end if found.size > 1 && !@same_name_warnings.include?(found.first.first) found.each {|(f, _)| @same_name_warnings << f} relative_to = Sass::Util.pathname(dir) if options[:_from_import_node] # If _line exists, we're here due to an actual import in an # import_node and we want to print a warning for a user writing an # ambiguous import. candidates = found.map do |(f, _)| " " + Sass::Util.pathname(f).relative_path_from(relative_to).to_s end.join("\n") raise Sass::SyntaxError.new(<= log_levels[min_level] end def log_level(name, options = {}) if options[:prepend] level = log_levels.values.min level = level.nil? ? 0 : level - 1 else level = log_levels.values.max level = level.nil? ? 0 : level + 1 end log_levels.update(name => level) define_logger(name) end def define_logger(name, options = {}) class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{name}(message) #{options.fetch(:to, :log)}(#{name.inspect}, message) end RUBY end end end end end ruby-sass-3.7.4/lib/sass/media.rb000066400000000000000000000154151345125207600166130ustar00rootroot00000000000000# A namespace for the `@media` query parse tree. module Sass::Media # A comma-separated list of queries. # # media_query [ ',' S* media_query ]* class QueryList # The queries contained in this list. # # @return [Array] attr_accessor :queries # @param queries [Array] See \{#queries} def initialize(queries) @queries = queries end # Merges this query list with another. The returned query list # queries for the intersection between the two inputs. # # Both query lists should be resolved. # # @param other [QueryList] # @return [QueryList?] The merged list, or nil if there is no intersection. def merge(other) new_queries = queries.map {|q1| other.queries.map {|q2| q1.merge(q2)}}.flatten.compact return if new_queries.empty? QueryList.new(new_queries) end # Returns the CSS for the media query list. # # @return [String] def to_css queries.map {|q| q.to_css}.join(', ') end # Returns the Sass/SCSS code for the media query list. # # @param options [{Symbol => Object}] An options hash (see {Sass::CSS#initialize}). # @return [String] def to_src(options) queries.map {|q| q.to_src(options)}.join(', ') end # Returns a representation of the query as an array of strings and # potentially {Sass::Script::Tree::Node}s (if there's interpolation in it). # When the interpolation is resolved and the strings are joined together, # this will be the string representation of this query. # # @return [Array] def to_a Sass::Util.intersperse(queries.map {|q| q.to_a}, ', ').flatten end # Returns a deep copy of this query list and all its children. # # @return [QueryList] def deep_copy QueryList.new(queries.map {|q| q.deep_copy}) end end # A single media query. # # [ [ONLY | NOT]? S* media_type S* | expression ] [ AND S* expression ]* class Query # The modifier for the query. # # When parsed as Sass code, this contains strings and SassScript nodes. When # parsed as CSS, it contains a single string (accessible via # \{#resolved_modifier}). # # @return [Array] attr_accessor :modifier # The type of the query (e.g. `"screen"` or `"print"`). # # When parsed as Sass code, this contains strings and SassScript nodes. When # parsed as CSS, it contains a single string (accessible via # \{#resolved_type}). # # @return [Array] attr_accessor :type # The trailing expressions in the query. # # When parsed as Sass code, each expression contains strings and SassScript # nodes. When parsed as CSS, each one contains a single string. # # @return [Array>] attr_accessor :expressions # @param modifier [Array] See \{#modifier} # @param type [Array] See \{#type} # @param expressions [Array>] See \{#expressions} def initialize(modifier, type, expressions) @modifier = modifier @type = type @expressions = expressions end # See \{#modifier}. # @return [String] def resolved_modifier # modifier should contain only a single string modifier.first || '' end # See \{#type}. # @return [String] def resolved_type # type should contain only a single string type.first || '' end # Merges this query with another. The returned query queries for # the intersection between the two inputs. # # Both queries should be resolved. # # @param other [Query] # @return [Query?] The merged query, or nil if there is no intersection. def merge(other) m1, t1 = resolved_modifier.downcase, resolved_type.downcase m2, t2 = other.resolved_modifier.downcase, other.resolved_type.downcase t1 = t2 if t1.empty? t2 = t1 if t2.empty? if (m1 == 'not') ^ (m2 == 'not') return if t1 == t2 type = m1 == 'not' ? t2 : t1 mod = m1 == 'not' ? m2 : m1 elsif m1 == 'not' && m2 == 'not' # CSS has no way of representing "neither screen nor print" return unless t1 == t2 type = t1 mod = 'not' elsif t1 != t2 return else # t1 == t2, neither m1 nor m2 are "not" type = t1 mod = m1.empty? ? m2 : m1 end Query.new([mod], [type], other.expressions + expressions) end # Returns the CSS for the media query. # # @return [String] def to_css css = '' css << resolved_modifier css << ' ' unless resolved_modifier.empty? css << resolved_type css << ' and ' unless resolved_type.empty? || expressions.empty? css << expressions.map do |e| # It's possible for there to be script nodes in Expressions even when # we're converting to CSS in the case where we parsed the document as # CSS originally (as in css_test.rb). e.map {|c| c.is_a?(Sass::Script::Tree::Node) ? c.to_sass : c.to_s}.join end.join(' and ') css end # Returns the Sass/SCSS code for the media query. # # @param options [{Symbol => Object}] An options hash (see {Sass::CSS#initialize}). # @return [String] def to_src(options) src = '' src << Sass::Media._interp_to_src(modifier, options) src << ' ' unless modifier.empty? src << Sass::Media._interp_to_src(type, options) src << ' and ' unless type.empty? || expressions.empty? src << expressions.map do |e| Sass::Media._interp_to_src(e, options) end.join(' and ') src end # @see \{MediaQuery#to\_a} def to_a res = [] res += modifier res << ' ' unless modifier.empty? res += type res << ' and ' unless type.empty? || expressions.empty? res += Sass::Util.intersperse(expressions, ' and ').flatten res end # Returns a deep copy of this query and all its children. # # @return [Query] def deep_copy Query.new( modifier.map {|c| c.is_a?(Sass::Script::Tree::Node) ? c.deep_copy : c}, type.map {|c| c.is_a?(Sass::Script::Tree::Node) ? c.deep_copy : c}, expressions.map {|e| e.map {|c| c.is_a?(Sass::Script::Tree::Node) ? c.deep_copy : c}}) end end # Converts an interpolation array to source. # # @param interp [Array] The interpolation array to convert. # @param options [{Symbol => Object}] An options hash (see {Sass::CSS#initialize}). # @return [String] def self._interp_to_src(interp, options) interp.map {|r| r.is_a?(String) ? r : r.to_sass(options)}.join end end ruby-sass-3.7.4/lib/sass/plugin.rb000066400000000000000000000115101345125207600170220ustar00rootroot00000000000000require 'fileutils' require 'sass' require 'sass/plugin/compiler' module Sass # This module provides a single interface to the compilation of Sass/SCSS files # for an application. It provides global options and checks whether CSS files # need to be updated. # # This module is used as the primary interface with Sass # when it's used as a plugin for various frameworks. # All Rack-enabled frameworks are supported out of the box. # The plugin is # {file:SASS_REFERENCE.md#Rack_Rails_Merb_Plugin automatically activated for Rails and Merb}. # Other frameworks must enable it explicitly; see {Sass::Plugin::Rack}. # # This module has a large set of callbacks available # to allow users to run code (such as logging) when certain things happen. # All callback methods are of the form `on_#{name}`, # and they all take a block that's called when the given action occurs. # # Note that this class proxies almost all methods to its {Sass::Plugin::Compiler} instance. # See \{#compiler}. # # @example Using a callback # Sass::Plugin.on_updating_stylesheet do |template, css| # puts "Compiling #{template} to #{css}" # end # Sass::Plugin.update_stylesheets # #=> Compiling app/sass/screen.scss to public/stylesheets/screen.css # #=> Compiling app/sass/print.scss to public/stylesheets/print.css # #=> Compiling app/sass/ie.scss to public/stylesheets/ie.css # @see Sass::Plugin::Compiler module Plugin extend self @checked_for_updates = false # Whether or not Sass has **ever** checked if the stylesheets need to be updated # (in this Ruby instance). # # @return [Boolean] attr_accessor :checked_for_updates # Same as \{#update\_stylesheets}, but respects \{#checked\_for\_updates} # and the {file:SASS_REFERENCE.md#always_update-option `:always_update`} # and {file:SASS_REFERENCE.md#always_check-option `:always_check`} options. # # @see #update_stylesheets def check_for_updates return unless !Sass::Plugin.checked_for_updates || Sass::Plugin.options[:always_update] || Sass::Plugin.options[:always_check] update_stylesheets end # Returns the singleton compiler instance. # This compiler has been pre-configured according # to the plugin configuration. # # @return [Sass::Plugin::Compiler] def compiler @compiler ||= Compiler.new end # Updates out-of-date stylesheets. # # Checks each Sass/SCSS file in # {file:SASS_REFERENCE.md#template_location-option `:template_location`} # to see if it's been modified more recently than the corresponding CSS file # in {file:SASS_REFERENCE.md#css_location-option `:css_location`}. # If it has, it updates the CSS file. # # @param individual_files [Array<(String, String)>] # A list of files to check for updates # **in addition to those specified by the # {file:SASS_REFERENCE.md#template_location-option `:template_location` option}.** # The first string in each pair is the location of the Sass/SCSS file, # the second is the location of the CSS file that it should be compiled to. def update_stylesheets(individual_files = []) return if options[:never_update] compiler.update_stylesheets(individual_files) end # Updates all stylesheets, even those that aren't out-of-date. # Ignores the cache. # # @param individual_files [Array<(String, String)>] # A list of files to check for updates # **in addition to those specified by the # {file:SASS_REFERENCE.md#template_location-option `:template_location` option}.** # The first string in each pair is the location of the Sass/SCSS file, # the second is the location of the CSS file that it should be compiled to. # @see #update_stylesheets def force_update_stylesheets(individual_files = []) Compiler.new( options.dup.merge( :never_update => false, :always_update => true, :cache => false)).update_stylesheets(individual_files) end # All other method invocations are proxied to the \{#compiler}. # # @see #compiler # @see Sass::Plugin::Compiler def method_missing(method, *args, &block) if compiler.respond_to?(method) compiler.send(method, *args, &block) else super end end # For parity with method_missing def respond_to?(method) super || compiler.respond_to?(method) end # There's a small speedup by not using method missing for frequently delegated methods. def options compiler.options end end end if defined?(ActionController) # On Rails 3+ the rails plugin is loaded at the right time in railtie.rb require 'sass/plugin/rails' unless Sass::Util.ap_geq_3? elsif defined?(Merb::Plugins) require 'sass/plugin/merb' else require 'sass/plugin/generic' end ruby-sass-3.7.4/lib/sass/plugin/000077500000000000000000000000001345125207600164775ustar00rootroot00000000000000ruby-sass-3.7.4/lib/sass/plugin/compiler.rb000066400000000000000000000524441345125207600206470ustar00rootroot00000000000000require 'fileutils' require 'sass' # XXX CE: is this still necessary now that we have the compiler class? require 'sass/callbacks' require 'sass/plugin/configuration' require 'sass/plugin/staleness_checker' module Sass::Plugin # The Compiler class handles compilation of multiple files and/or directories, # including checking which CSS files are out-of-date and need to be updated # and calling Sass to perform the compilation on those files. # # {Sass::Plugin} uses this class to update stylesheets for a single application. # Unlike {Sass::Plugin}, though, the Compiler class has no global state, # and so multiple instances may be created and used independently. # # If you need to compile a Sass string into CSS, # please see the {Sass::Engine} class. # # Unlike {Sass::Plugin}, this class doesn't keep track of # whether or how many times a stylesheet should be updated. # Therefore, the following `Sass::Plugin` options are ignored by the Compiler: # # * `:never_update` # * `:always_check` class Compiler include Configuration extend Sass::Callbacks # Creates a new compiler. # # @param opts [{Symbol => Object}] # See {file:SASS_REFERENCE.md#Options the Sass options documentation}. def initialize(opts = {}) @watched_files = Set.new options.merge!(opts) end # Register a callback to be run before stylesheets are mass-updated. # This is run whenever \{#update\_stylesheets} is called, # unless the \{file:SASS_REFERENCE.md#never_update-option `:never_update` option} # is enabled. # # @yield [files] # @yieldparam files [<(String, String, String)>] # Individual files to be updated. Files in directories specified are included in this list. # The first element of each pair is the source file, # the second is the target CSS file, # the third is the target sourcemap file. define_callback :updating_stylesheets # Register a callback to be run after stylesheets are mass-updated. # This is run whenever \{#update\_stylesheets} is called, # unless the \{file:SASS_REFERENCE.md#never_update-option `:never_update` option} # is enabled. # # @yield [updated_files] # @yieldparam updated_files [<(String, String)>] # Individual files that were updated. # The first element of each pair is the source file, the second is the target CSS file. define_callback :updated_stylesheets # Register a callback to be run after a single stylesheet is updated. # The callback is only run if the stylesheet is really updated; # if the CSS file is fresh, this won't be run. # # Even if the \{file:SASS_REFERENCE.md#full_exception-option `:full_exception` option} # is enabled, this callback won't be run # when an exception CSS file is being written. # To run an action for those files, use \{#on\_compilation\_error}. # # @yield [template, css, sourcemap] # @yieldparam template [String] # The location of the Sass/SCSS file being updated. # @yieldparam css [String] # The location of the CSS file being generated. # @yieldparam sourcemap [String] # The location of the sourcemap being generated, if any. define_callback :updated_stylesheet # Register a callback to be run when compilation starts. # # In combination with on_updated_stylesheet, this could be used # to collect compilation statistics like timing or to take a # diff of the changes to the output file. # # @yield [template, css, sourcemap] # @yieldparam template [String] # The location of the Sass/SCSS file being updated. # @yieldparam css [String] # The location of the CSS file being generated. # @yieldparam sourcemap [String] # The location of the sourcemap being generated, if any. define_callback :compilation_starting # Register a callback to be run when Sass decides not to update a stylesheet. # In particular, the callback is run when Sass finds that # the template file and none of its dependencies # have been modified since the last compilation. # # Note that this is **not** run when the # \{file:SASS_REFERENCE.md#never-update_option `:never_update` option} is set, # nor when Sass decides not to compile a partial. # # @yield [template, css] # @yieldparam template [String] # The location of the Sass/SCSS file not being updated. # @yieldparam css [String] # The location of the CSS file not being generated. define_callback :not_updating_stylesheet # Register a callback to be run when there's an error # compiling a Sass file. # This could include not only errors in the Sass document, # but also errors accessing the file at all. # # @yield [error, template, css] # @yieldparam error [Exception] The exception that was raised. # @yieldparam template [String] # The location of the Sass/SCSS file being updated. # @yieldparam css [String] # The location of the CSS file being generated. define_callback :compilation_error # Register a callback to be run when Sass creates a directory # into which to put CSS files. # # Note that even if multiple levels of directories need to be created, # the callback may only be run once. # For example, if "foo/" exists and "foo/bar/baz/" needs to be created, # this may only be run for "foo/bar/baz/". # This is not a guarantee, however; # it may also be run for "foo/bar/". # # @yield [dirname] # @yieldparam dirname [String] # The location of the directory that was created. define_callback :creating_directory # Register a callback to be run when Sass detects # that a template has been modified. # This is only run when using \{#watch}. # # @yield [template] # @yieldparam template [String] # The location of the template that was modified. define_callback :template_modified # Register a callback to be run when Sass detects # that a new template has been created. # This is only run when using \{#watch}. # # @yield [template] # @yieldparam template [String] # The location of the template that was created. define_callback :template_created # Register a callback to be run when Sass detects # that a template has been deleted. # This is only run when using \{#watch}. # # @yield [template] # @yieldparam template [String] # The location of the template that was deleted. define_callback :template_deleted # Register a callback to be run when Sass deletes a CSS file. # This happens when the corresponding Sass/SCSS file has been deleted # and when the compiler cleans the output files. # # @yield [filename] # @yieldparam filename [String] # The location of the CSS file that was deleted. define_callback :deleting_css # Register a callback to be run when Sass deletes a sourcemap file. # This happens when the corresponding Sass/SCSS file has been deleted # and when the compiler cleans the output files. # # @yield [filename] # @yieldparam filename [String] # The location of the sourcemap file that was deleted. define_callback :deleting_sourcemap # Updates out-of-date stylesheets. # # Checks each Sass/SCSS file in # {file:SASS_REFERENCE.md#template_location-option `:template_location`} # to see if it's been modified more recently than the corresponding CSS file # in {file:SASS_REFERENCE.md#css_location-option `:css_location`}. # If it has, it updates the CSS file. # # @param individual_files [Array<(String, String[, String])>] # A list of files to check for updates # **in addition to those specified by the # {file:SASS_REFERENCE.md#template_location-option `:template_location` option}.** # The first string in each pair is the location of the Sass/SCSS file, # the second is the location of the CSS file that it should be compiled to. # The third string, if provided, is the location of the Sourcemap file. def update_stylesheets(individual_files = []) Sass::Plugin.checked_for_updates = true staleness_checker = StalenessChecker.new(engine_options) files = file_list(individual_files) run_updating_stylesheets(files) updated_stylesheets = [] files.each do |file, css, sourcemap| # TODO: Does staleness_checker need to check the sourcemap file as well? if options[:always_update] || staleness_checker.stylesheet_needs_update?(css, file) # XXX For consistency, this should return the sourcemap too, but it would # XXX be an API change. updated_stylesheets << [file, css] update_stylesheet(file, css, sourcemap) else run_not_updating_stylesheet(file, css, sourcemap) end end run_updated_stylesheets(updated_stylesheets) end # Construct a list of files that might need to be compiled # from the provided individual_files and the template_locations. # # Note: this method does not cache the results as they can change # across invocations when sass files are added or removed. # # @param individual_files [Array<(String, String[, String])>] # A list of files to check for updates # **in addition to those specified by the # {file:SASS_REFERENCE.md#template_location-option `:template_location` option}.** # The first string in each pair is the location of the Sass/SCSS file, # the second is the location of the CSS file that it should be compiled to. # The third string, if provided, is the location of the Sourcemap file. # @return [Array<(String, String, String)>] # A list of [sass_file, css_file, sourcemap_file] tuples similar # to what was passed in, but expanded to include the current state # of the directories being updated. def file_list(individual_files = []) files = individual_files.map do |tuple| if engine_options[:sourcemap] == :none tuple[0..1] elsif tuple.size < 3 [tuple[0], tuple[1], Sass::Util.sourcemap_name(tuple[1])] else tuple.dup end end template_location_array.each do |template_location, css_location| Sass::Util.glob(File.join(template_location, "**", "[^_]*.s[ca]ss")).sort.each do |file| # Get the relative path to the file name = Sass::Util.relative_path_from(file, template_location).to_s css = css_filename(name, css_location) sourcemap = Sass::Util.sourcemap_name(css) unless engine_options[:sourcemap] == :none files << [file, css, sourcemap] end end files end # Watches the template directory (or directories) # and updates the CSS files whenever the related Sass/SCSS files change. # `watch` never returns. # # Whenever a change is detected to a Sass/SCSS file in # {file:SASS_REFERENCE.md#template_location-option `:template_location`}, # the corresponding CSS file in {file:SASS_REFERENCE.md#css_location-option `:css_location`} # will be recompiled. # The CSS files of any Sass/SCSS files that import the changed file will also be recompiled. # # Before the watching starts in earnest, `watch` calls \{#update\_stylesheets}. # # Note that `watch` uses the [Listen](http://github.com/guard/listen) library # to monitor the filesystem for changes. # Listen isn't loaded until `watch` is run. # The version of Listen distributed with Sass is loaded by default, # but if another version has already been loaded that will be used instead. # # @param individual_files [Array<(String, String[, String])>] # A list of files to check for updates # **in addition to those specified by the # {file:SASS_REFERENCE.md#template_location-option `:template_location` option}.** # The first string in each pair is the location of the Sass/SCSS file, # the second is the location of the CSS file that it should be compiled to. # The third string, if provided, is the location of the Sourcemap file. # @param options [Hash] The options that control how watching works. # @option options [Boolean] :skip_initial_update # Don't do an initial update when starting the watcher when true def watch(individual_files = [], options = {}) @inferred_directories = [] options, individual_files = individual_files, [] if individual_files.is_a?(Hash) update_stylesheets(individual_files) unless options[:skip_initial_update] directories = watched_paths individual_files.each do |(source, _, _)| source = File.expand_path(source) @watched_files << Sass::Util.realpath(source).to_s @inferred_directories << File.dirname(source) end directories += @inferred_directories directories = remove_redundant_directories(directories) # TODO: Keep better track of what depends on what # so we don't have to run a global update every time anything changes. # XXX The :additional_watch_paths option exists for Compass to use until # a deprecated feature is removed. It may be removed without warning. directories += Array(options[:additional_watch_paths]) options = { :relative_paths => false, # The native windows listener is much slower than the polling option, according to # https://github.com/nex3/sass/commit/a3031856b22bc834a5417dedecb038b7be9b9e3e :force_polling => @options[:poll] || Sass::Util.windows? } listener = create_listener(*directories, options) do |modified, added, removed| on_file_changed(individual_files, modified, added, removed) yield(modified, added, removed) if block_given? end begin listener.start sleep rescue Interrupt # Squelch Interrupt for clean exit from Listen::Listener end end # Non-destructively modifies \{#options} so that default values are properly set, # and returns the result. # # @param additional_options [{Symbol => Object}] An options hash with which to merge \{#options} # @return [{Symbol => Object}] The modified options hash def engine_options(additional_options = {}) opts = options.merge(additional_options) opts[:load_paths] = load_paths(opts) options[:sourcemap] = :auto if options[:sourcemap] == true options[:sourcemap] = :none if options[:sourcemap] == false opts end # Compass expects this to exist def stylesheet_needs_update?(css_file, template_file) StalenessChecker.stylesheet_needs_update?(css_file, template_file) end # Remove all output files that would be created by calling update_stylesheets, if they exist. # # This method runs the deleting_css and deleting_sourcemap callbacks for # the files that are deleted. # # @param individual_files [Array<(String, String[, String])>] # A list of files to check for updates # **in addition to those specified by the # {file:SASS_REFERENCE.md#template_location-option `:template_location` option}.** # The first string in each pair is the location of the Sass/SCSS file, # the second is the location of the CSS file that it should be compiled to. # The third string, if provided, is the location of the Sourcemap file. def clean(individual_files = []) file_list(individual_files).each do |(_, css_file, sourcemap_file)| if File.exist?(css_file) run_deleting_css css_file File.delete(css_file) end if sourcemap_file && File.exist?(sourcemap_file) run_deleting_sourcemap sourcemap_file File.delete(sourcemap_file) end end nil end private # This is mocked out in compiler_test.rb. def create_listener(*args, &block) require 'sass-listen' SassListen.to(*args, &block) end def remove_redundant_directories(directories) dedupped = [] directories.each do |new_directory| # no need to add a directory that is already watched. next if dedupped.any? do |existing_directory| child_of_directory?(existing_directory, new_directory) end # get rid of any sub directories of this new directory dedupped.reject! do |existing_directory| child_of_directory?(new_directory, existing_directory) end dedupped << new_directory end dedupped end def on_file_changed(individual_files, modified, added, removed) recompile_required = false modified.uniq.each do |f| next unless watched_file?(f) recompile_required = true run_template_modified(relative_to_pwd(f)) end added.uniq.each do |f| next unless watched_file?(f) recompile_required = true run_template_created(relative_to_pwd(f)) end removed.uniq.each do |f| next unless watched_file?(f) run_template_deleted(relative_to_pwd(f)) if (files = individual_files.find {|(source, _, _)| File.expand_path(source) == f}) recompile_required = true # This was a file we were watching explicitly and compiling to a particular location. # Delete the corresponding file. try_delete_css files[1] else next unless watched_file?(f) recompile_required = true # Look for the sass directory that contained the sass file # And try to remove the css file that corresponds to it template_location_array.each do |(sass_dir, css_dir)| sass_dir = File.expand_path(sass_dir) next unless child_of_directory?(sass_dir, f) remainder = f[(sass_dir.size + 1)..-1] try_delete_css(css_filename(remainder, css_dir)) break end end end return unless recompile_required # In case a file we're watching is removed and then recreated we # prune out the non-existant files here. watched_files_remaining = individual_files.select {|(source, _, _)| File.exist?(source)} update_stylesheets(watched_files_remaining) end def update_stylesheet(filename, css, sourcemap) dir = File.dirname(css) unless File.exist?(dir) run_creating_directory dir FileUtils.mkdir_p dir end begin File.read(filename) unless File.readable?(filename) # triggers an error for handling engine_opts = engine_options(:css_filename => css, :filename => filename, :sourcemap_filename => sourcemap) mapping = nil run_compilation_starting(filename, css, sourcemap) engine = Sass::Engine.for_file(filename, engine_opts) if sourcemap rendered, mapping = engine.render_with_sourcemap(File.basename(sourcemap)) else rendered = engine.render end rescue StandardError => e compilation_error_occurred = true run_compilation_error e, filename, css, sourcemap raise e unless options[:full_exception] rendered = Sass::SyntaxError.exception_to_css(e, options[:line] || 1) end write_file(css, rendered) if mapping write_file( sourcemap, mapping.to_json( :css_path => css, :sourcemap_path => sourcemap, :type => options[:sourcemap])) end run_updated_stylesheet(filename, css, sourcemap) unless compilation_error_occurred end def write_file(fileName, content) flag = 'w' flag = 'wb' if Sass::Util.windows? && options[:unix_newlines] File.open(fileName, flag) do |file| file.set_encoding(content.encoding) file.print(content) end end def try_delete_css(css) if File.exist?(css) run_deleting_css css File.delete css end map = Sass::Util.sourcemap_name(css) return unless File.exist?(map) run_deleting_sourcemap map File.delete map end def watched_file?(file) @watched_files.include?(file) || normalized_load_paths.any? {|lp| lp.watched_file?(file)} || @inferred_directories.any? {|d| sass_file_in_directory?(d, file)} end def sass_file_in_directory?(directory, filename) filename =~ /\.s[ac]ss$/ && filename.start_with?(directory + File::SEPARATOR) end def watched_paths @watched_paths ||= normalized_load_paths.map {|lp| lp.directories_to_watch}.compact.flatten end def normalized_load_paths @normalized_load_paths ||= Sass::Engine.normalize_options(:load_paths => load_paths)[:load_paths] end def load_paths(opts = options) (opts[:load_paths] || []) + template_locations end def template_locations template_location_array.to_a.map {|l| l.first} end def css_locations template_location_array.to_a.map {|l| l.last} end def css_filename(name, path) "#{path}#{File::SEPARATOR unless path.end_with?(File::SEPARATOR)}#{name}". gsub(/\.s[ac]ss$/, '.css') end def relative_to_pwd(f) Sass::Util.relative_path_from(f, Dir.pwd).to_s rescue ArgumentError # when a relative path cannot be computed f end def child_of_directory?(parent, child) parent_dir = parent.end_with?(File::SEPARATOR) ? parent : (parent + File::SEPARATOR) child.start_with?(parent_dir) || parent == child end end end ruby-sass-3.7.4/lib/sass/plugin/configuration.rb000066400000000000000000000123211345125207600216720ustar00rootroot00000000000000module Sass module Plugin # We keep configuration in its own self-contained file so that we can load # it independently in Rails 3, where the full plugin stuff is lazy-loaded. # # Note that this is not guaranteed to be thread-safe. For guaranteed thread # safety, use a separate {Sass::Plugin} for each thread. module Configuration # Returns the default options for a {Sass::Plugin::Compiler}. # # @return [{Symbol => Object}] def default_options @default_options ||= { :css_location => './public/stylesheets', :always_update => false, :always_check => true, :full_exception => true, :cache_location => ".sass-cache" }.freeze end # Resets the options and # {Sass::Callbacks::InstanceMethods#clear_callbacks! clears all callbacks}. def reset! @options = nil clear_callbacks! end # An options hash. See {file:SASS_REFERENCE.md#Options the Sass options # documentation}. # # @return [{Symbol => Object}] def options @options ||= default_options.dup end # Adds a new template-location/css-location mapping. # This means that Sass/SCSS files in `template_location` # will be compiled to CSS files in `css_location`. # # This is preferred over manually manipulating the # {file:SASS_REFERENCE.md#template_location-option `:template_location` option} # since the option can be in multiple formats. # # Note that this method will change `options[:template_location]` # to be in the Array format. # This means that even if `options[:template_location]` # had previously been a Hash or a String, # it will now be an Array. # # @param template_location [String] The location where Sass/SCSS files will be. # @param css_location [String] The location where compiled CSS files will go. def add_template_location(template_location, css_location = options[:css_location]) normalize_template_location! template_location_array << [template_location, css_location] end # Removes a template-location/css-location mapping. # This means that Sass/SCSS files in `template_location` # will no longer be compiled to CSS files in `css_location`. # # This is preferred over manually manipulating the # {file:SASS_REFERENCE.md#template_location-option `:template_location` option} # since the option can be in multiple formats. # # Note that this method will change `options[:template_location]` # to be in the Array format. # This means that even if `options[:template_location]` # had previously been a Hash or a String, # it will now be an Array. # # @param template_location [String] # The location where Sass/SCSS files were, # which is now going to be ignored. # @param css_location [String] # The location where compiled CSS files went, but will no longer go. # @return [Boolean] # Non-`nil` if the given mapping already existed and was removed, # or `nil` if nothing was changed. def remove_template_location(template_location, css_location = options[:css_location]) normalize_template_location! template_location_array.delete([template_location, css_location]) end # Returns the template locations configured for Sass # as an array of `[template_location, css_location]` pairs. # See the {file:SASS_REFERENCE.md#template_location-option `:template_location` option} # for details. # # Modifications to the returned array may not be persistent. Use {#add_template_location} # and {#remove_template_location} instead. # # @return [Array<(String, String)>] # An array of `[template_location, css_location]` pairs. def template_location_array convert_template_location(options[:template_location], options[:css_location]) end private # Returns the given template location, as an array. If it's already an array, # it is returned unmodified. Otherwise, a new array is created and returned. # # @param template_location [String, Array<(String, String)>] # A single template location, or a pre-normalized array of template # locations and CSS locations. # @param css_location [String?] # The location for compiled CSS files. # @return [Array<(String, String)>] # An array of `[template_location, css_location]` pairs. def convert_template_location(template_location, css_location) return template_location if template_location.is_a?(Array) case template_location when nil if css_location [[File.join(css_location, 'sass'), css_location]] else [] end when String [[template_location, css_location]] else template_location.to_a end end def normalize_template_location! options[:template_location] = convert_template_location( options[:template_location], options[:css_location]) end end end end ruby-sass-3.7.4/lib/sass/plugin/generic.rb000066400000000000000000000011651345125207600204430ustar00rootroot00000000000000# The reason some options are declared here rather than in sass/plugin/configuration.rb # is that otherwise they'd clobber the Rails-specific options. # Since Rails' options are lazy-loaded in Rails 3, # they're reverse-merged with the default options # so that user configuration is preserved. # This means that defaults that differ from Rails' # must be declared here. unless defined?(Sass::GENERIC_LOADED) Sass::GENERIC_LOADED = true Sass::Plugin.options.merge!(:css_location => './public/stylesheets', :always_update => false, :always_check => true) end ruby-sass-3.7.4/lib/sass/plugin/merb.rb000066400000000000000000000026051345125207600177540ustar00rootroot00000000000000unless defined?(Sass::MERB_LOADED) Sass::MERB_LOADED = true module Sass::Plugin::Configuration # Different default options in a m environment. def default_options @default_options ||= begin version = Merb::VERSION.split('.').map {|n| n.to_i} if version[0] <= 0 && version[1] < 5 root = MERB_ROOT env = MERB_ENV else root = Merb.root.to_s env = Merb.environment end { :always_update => false, :template_location => root + '/public/stylesheets/sass', :css_location => root + '/public/stylesheets', :cache_location => root + '/tmp/sass-cache', :always_check => env != "production", :quiet => env != "production", :full_exception => env != "production" }.freeze end end end config = Merb::Plugins.config[:sass] || Merb::Plugins.config["sass"] || {} if defined? config.symbolize_keys! config.symbolize_keys! end Sass::Plugin.options.merge!(config) require 'sass/plugin/rack' class Sass::Plugin::MerbBootLoader < Merb::BootLoader after Merb::BootLoader::RackUpApplication def self.run # Apparently there's no better way than this to add Sass # to Merb's Rack stack. Merb::Config[:app] = Sass::Plugin::Rack.new(Merb::Config[:app]) end end end ruby-sass-3.7.4/lib/sass/plugin/rack.rb000066400000000000000000000034031345125207600177440ustar00rootroot00000000000000module Sass module Plugin # Rack middleware for compiling Sass code. # # ## Activate # # require 'sass/plugin/rack' # use Sass::Plugin::Rack # # ## Customize # # Sass::Plugin.options.merge!( # :cache_location => './tmp/sass-cache', # :never_update => environment != :production, # :full_exception => environment != :production) # # {file:SASS_REFERENCE.md#Options See the Reference for more options}. # # ## Use # # Put your Sass files in `public/stylesheets/sass`. # Your CSS will be generated in `public/stylesheets`, # and regenerated every request if necessary. # The locations and frequency {file:SASS_REFERENCE.md#Options can be customized}. # That's all there is to it! class Rack # The delay, in seconds, between update checks. # Useful when many resources are requested for a single page. # `nil` means no delay at all. # # @return [Float] attr_accessor :dwell # Initialize the middleware. # # @param app [#call] The Rack application # @param dwell [Float] See \{#dwell} def initialize(app, dwell = 1.0) @app = app @dwell = dwell @check_after = Time.now.to_f end # Process a request, checking the Sass stylesheets for changes # and updating them if necessary. # # @param env The Rack request environment # @return [(#to_i, {String => String}, Object)] The Rack response def call(env) if @dwell.nil? || Time.now.to_f > @check_after Sass::Plugin.check_for_updates @check_after = Time.now.to_f + @dwell if @dwell end @app.call(env) end end end end require 'sass/plugin' ruby-sass-3.7.4/lib/sass/plugin/rails.rb000066400000000000000000000030071345125207600201360ustar00rootroot00000000000000unless defined?(Sass::RAILS_LOADED) Sass::RAILS_LOADED = true module Sass::Plugin::Configuration # Different default options in a rails environment. def default_options return @default_options if @default_options opts = { :quiet => Sass::Util.rails_env != "production", :full_exception => Sass::Util.rails_env != "production", :cache_location => Sass::Util.rails_root + '/tmp/sass-cache' } opts.merge!( :always_update => false, :template_location => Sass::Util.rails_root + '/public/stylesheets/sass', :css_location => Sass::Util.rails_root + '/public/stylesheets', :always_check => Sass::Util.rails_env == "development") @default_options = opts.freeze end end Sass::Plugin.options.reverse_merge!(Sass::Plugin.default_options) # Rails 3.1 loads and handles Sass all on its own if defined?(ActionController::Metal) # 3.1 > Rails >= 3.0 require 'sass/plugin/rack' Rails.configuration.middleware.use(Sass::Plugin::Rack) elsif defined?(ActionController::Dispatcher) && defined?(ActionController::Dispatcher.middleware) # Rails >= 2.3 require 'sass/plugin/rack' ActionController::Dispatcher.middleware.use(Sass::Plugin::Rack) else module ActionController class Base alias_method :sass_old_process, :process def process(*args) Sass::Plugin.check_for_updates sass_old_process(*args) end end end end end ruby-sass-3.7.4/lib/sass/plugin/staleness_checker.rb000066400000000000000000000200411345125207600225060ustar00rootroot00000000000000require 'thread' module Sass module Plugin # The class handles `.s[ca]ss` file staleness checks via their mtime timestamps. # # To speed things up two level of caches are employed: # # * A class-level dependency cache which stores @import paths for each file. # This is a long-lived cache that is reused by every StalenessChecker instance. # * Three short-lived instance-level caches, one for file mtimes, # one for whether a file is stale during this particular run. # and one for the parse tree for a file. # These are only used by a single StalenessChecker instance. # # Usage: # # * For a one-off staleness check of a single `.s[ca]ss` file, # the class-level {stylesheet_needs_update?} method # should be used. # * For a series of staleness checks (e.g. checking all files for staleness) # a StalenessChecker instance should be created, # and the instance-level \{#stylesheet\_needs\_update?} method should be used. # the caches should make the whole process significantly faster. # *WARNING*: It is important not to retain the instance for too long, # as its instance-level caches are never explicitly expired. class StalenessChecker @dependencies_cache = {} @dependency_cache_mutex = Mutex.new class << self # TODO: attach this to a compiler instance. # @private attr_accessor :dependencies_cache attr_reader :dependency_cache_mutex end # Creates a new StalenessChecker # for checking the staleness of several stylesheets at once. # # @param options [{Symbol => Object}] # See {file:SASS_REFERENCE.md#Options the Sass options documentation}. def initialize(options) # URIs that are being actively checked for staleness. Protects against # import loops. @actively_checking = Set.new # Entries in the following instance-level caches are never explicitly expired. # Instead they are supposed to automatically go out of scope when a series of staleness # checks (this instance of StalenessChecker was created for) is finished. @mtimes, @dependencies_stale, @parse_trees = {}, {}, {} @options = Sass::Engine.normalize_options(options) end # Returns whether or not a given CSS file is out of date # and needs to be regenerated. # # @param css_file [String] The location of the CSS file to check. # @param template_file [String] The location of the Sass or SCSS template # that is compiled to `css_file`. # @return [Boolean] Whether the stylesheet needs to be updated. def stylesheet_needs_update?(css_file, template_file, importer = nil) template_file = File.expand_path(template_file) begin css_mtime = File.mtime(css_file) rescue Errno::ENOENT return true end stylesheet_modified_since?(template_file, css_mtime, importer) end # Returns whether a Sass or SCSS stylesheet has been modified since a given time. # # @param template_file [String] The location of the Sass or SCSS template. # @param mtime [Time] The modification time to check against. # @param importer [Sass::Importers::Base] The importer used to locate the stylesheet. # Defaults to the filesystem importer. # @return [Boolean] Whether the stylesheet has been modified. def stylesheet_modified_since?(template_file, mtime, importer = nil) importer ||= @options[:filesystem_importer].new(".") dependency_updated?(mtime).call(template_file, importer) end # Returns whether or not a given CSS file is out of date # and needs to be regenerated. # # The distinction between this method and the instance-level \{#stylesheet\_needs\_update?} # is that the instance method preserves mtime and stale-dependency caches, # so it's better to use when checking multiple stylesheets at once. # # @param css_file [String] The location of the CSS file to check. # @param template_file [String] The location of the Sass or SCSS template # that is compiled to `css_file`. # @return [Boolean] Whether the stylesheet needs to be updated. def self.stylesheet_needs_update?(css_file, template_file, importer = nil) new(Plugin.engine_options).stylesheet_needs_update?(css_file, template_file, importer) end # Returns whether a Sass or SCSS stylesheet has been modified since a given time. # # The distinction between this method and the instance-level \{#stylesheet\_modified\_since?} # is that the instance method preserves mtime and stale-dependency caches, # so it's better to use when checking multiple stylesheets at once. # # @param template_file [String] The location of the Sass or SCSS template. # @param mtime [Time] The modification time to check against. # @param importer [Sass::Importers::Base] The importer used to locate the stylesheet. # Defaults to the filesystem importer. # @return [Boolean] Whether the stylesheet has been modified. def self.stylesheet_modified_since?(template_file, mtime, importer = nil) new(Plugin.engine_options).stylesheet_modified_since?(template_file, mtime, importer) end private def dependencies_stale?(uri, importer, css_mtime) timestamps = @dependencies_stale[[uri, importer]] ||= {} timestamps.each_pair do |checked_css_mtime, is_stale| if checked_css_mtime <= css_mtime && !is_stale return false elsif checked_css_mtime > css_mtime && is_stale return true end end timestamps[css_mtime] = dependencies(uri, importer).any?(&dependency_updated?(css_mtime)) rescue Sass::SyntaxError # If there's an error finding dependencies, default to recompiling. true end def mtime(uri, importer) @mtimes[[uri, importer]] ||= begin mtime = importer.mtime(uri, @options) if mtime.nil? with_dependency_cache {|cache| cache.delete([uri, importer])} nil else mtime end end end def dependencies(uri, importer) stored_mtime, dependencies = with_dependency_cache {|cache| Sass::Util.destructure(cache[[uri, importer]])} if !stored_mtime || stored_mtime < mtime(uri, importer) dependencies = compute_dependencies(uri, importer) with_dependency_cache do |cache| cache[[uri, importer]] = [mtime(uri, importer), dependencies] end end dependencies end def dependency_updated?(css_mtime) proc do |uri, importer| next true if @actively_checking.include?(uri) begin @actively_checking << uri sass_mtime = mtime(uri, importer) !sass_mtime || sass_mtime > css_mtime || dependencies_stale?(uri, importer, css_mtime) ensure @actively_checking.delete uri end end end def compute_dependencies(uri, importer) tree(uri, importer).grep(Tree::ImportNode) do |n| next if n.css_import? file = n.imported_file key = [file.options[:filename], file.options[:importer]] @parse_trees[key] = file.to_tree key end.compact end def tree(uri, importer) @parse_trees[[uri, importer]] ||= importer.find(uri, @options).to_tree end # Get access to the global dependency cache in a threadsafe manner. # Inside the block, no other thread can access the dependency cache. # # @yieldparam cache [Hash] The hash that is the global dependency cache # @return The value returned by the block to which this method yields def with_dependency_cache StalenessChecker.dependency_cache_mutex.synchronize do yield StalenessChecker.dependencies_cache end end end end end ruby-sass-3.7.4/lib/sass/railtie.rb000066400000000000000000000005031345125207600171550ustar00rootroot00000000000000# Rails 3.0.0.beta.2+, < 3.1 if defined?(ActiveSupport) && ActiveSupport.public_methods.include?(:on_load) && !Sass::Util.ap_geq?('3.1.0.beta') require 'sass/plugin/configuration' ActiveSupport.on_load(:before_configuration) do require 'sass' require 'sass/plugin' require 'sass/plugin/rails' end end ruby-sass-3.7.4/lib/sass/repl.rb000066400000000000000000000024651345125207600164770ustar00rootroot00000000000000require 'readline' module Sass # Runs a SassScript read-eval-print loop. # It presents a prompt on the terminal, # reads in SassScript expressions, # evaluates them, # and prints the result. class Repl # @param options [{Symbol => Object}] An options hash. def initialize(options = {}) @options = options end # Starts the read-eval-print loop. def run environment = Environment.new @line = 0 loop do @line += 1 unless (text = Readline.readline('>> ')) puts return end Readline::HISTORY << text parse_input(environment, text) end end private def parse_input(environment, text) case text when Script::MATCH name = $1 guarded = !!$3 val = Script::Parser.parse($2, @line, text.size - ($3 || '').size - $2.size) unless guarded && environment.var(name) environment.set_var(name, val.perform(environment)) end p environment.var(name) else p Script::Parser.parse(text, @line, 0).perform(environment) end rescue Sass::SyntaxError => e puts "SyntaxError: #{e.message}" if @options[:trace] e.backtrace.each do |line| puts "\tfrom #{line}" end end end end end ruby-sass-3.7.4/lib/sass/root.rb000066400000000000000000000004021345125207600165050ustar00rootroot00000000000000module Sass # The root directory of the Sass source tree. # This may be overridden by the package manager # if the lib directory is separated from the main source tree. # @api public ROOT_DIR = File.expand_path(File.join(__FILE__, "../../..")) end ruby-sass-3.7.4/lib/sass/script.rb000066400000000000000000000047121345125207600170360ustar00rootroot00000000000000require 'sass/scss/rx' module Sass # SassScript is code that's embedded in Sass documents # to allow for property values to be computed from variables. # # This module contains code that handles the parsing and evaluation of SassScript. module Script # The regular expression used to parse variables. MATCH = /^\$(#{Sass::SCSS::RX::IDENT})\s*:\s*(.+?) (!#{Sass::SCSS::RX::IDENT}(?:\s+!#{Sass::SCSS::RX::IDENT})*)?$/x # The regular expression used to validate variables without matching. VALIDATE = /^\$#{Sass::SCSS::RX::IDENT}$/ # Parses a string of SassScript # # @param value [String] The SassScript # @param line [Integer] The number of the line on which the SassScript appeared. # Used for error reporting # @param offset [Integer] The number of characters in on `line` that the SassScript started. # Used for error reporting # @param options [{Symbol => Object}] An options hash; # see {file:SASS_REFERENCE.md#Options the Sass options documentation} # @return [Script::Tree::Node] The root node of the parse tree def self.parse(value, line, offset, options = {}) Parser.parse(value, line, offset, options) rescue Sass::SyntaxError => e e.message << ": #{value.inspect}." if e.message == "SassScript error" e.modify_backtrace(:line => line, :filename => options[:filename]) raise e end require 'sass/script/functions' require 'sass/script/parser' require 'sass/script/tree' require 'sass/script/value' # @private CONST_RENAMES = { :Literal => Sass::Script::Value::Base, :ArgList => Sass::Script::Value::ArgList, :Bool => Sass::Script::Value::Bool, :Color => Sass::Script::Value::Color, :List => Sass::Script::Value::List, :Null => Sass::Script::Value::Null, :Number => Sass::Script::Value::Number, :String => Sass::Script::Value::String, :Node => Sass::Script::Tree::Node, :Funcall => Sass::Script::Tree::Funcall, :Interpolation => Sass::Script::Tree::Interpolation, :Operation => Sass::Script::Tree::Operation, :StringInterpolation => Sass::Script::Tree::StringInterpolation, :UnaryOperation => Sass::Script::Tree::UnaryOperation, :Variable => Sass::Script::Tree::Variable, } # @private def self.const_missing(name) klass = CONST_RENAMES[name] super unless klass CONST_RENAMES.each {|n, k| const_set(n, k)} klass end end end ruby-sass-3.7.4/lib/sass/script/000077500000000000000000000000001345125207600165055ustar00rootroot00000000000000ruby-sass-3.7.4/lib/sass/script/css_lexer.rb000066400000000000000000000013631345125207600210240ustar00rootroot00000000000000module Sass module Script # This is a subclass of {Lexer} for use in parsing plain CSS properties. # # @see Sass::SCSS::CssParser class CssLexer < Lexer private def token important || super end def string(re, *args) if re == :uri uri = scan(URI) return unless uri return [:string, Script::Value::String.new(uri)] end return unless scan(STRING) string_value = Sass::Script::Value::String.value(@scanner[1] || @scanner[2]) value = Script::Value::String.new(string_value, :string) [:string, value] end def important s = scan(IMPORTANT) return unless s [:raw, s] end end end end ruby-sass-3.7.4/lib/sass/script/css_parser.rb000066400000000000000000000016401345125207600211770ustar00rootroot00000000000000require 'sass/script' require 'sass/script/css_lexer' module Sass module Script # This is a subclass of {Parser} for use in parsing plain CSS properties. # # @see Sass::SCSS::CssParser class CssParser < Parser private # @private def lexer_class; CssLexer; end # We need a production that only does /, # since * and % aren't allowed in plain CSS production :div, :unary_plus, :div def string tok = try_tok(:string) return number unless tok return if @lexer.peek && @lexer.peek.type == :begin_interpolation literal_node(tok.value, tok.source_range) end # Short-circuit all the SassScript-only productions def interpolation(first: nil, inner: :space) first || send(inner) end alias_method :or_expr, :div alias_method :unary_div, :ident alias_method :paren, :string end end end ruby-sass-3.7.4/lib/sass/script/functions.rb000066400000000000000000003424471345125207600210600ustar00rootroot00000000000000require 'sass/script/value/helpers' module Sass::Script # YARD can't handle some multiline tags, and we need really long tags for function declarations. # Methods in this module are accessible from the SassScript context. # For example, you can write # # $color: hsl(120deg, 100%, 50%) # # and it will call {Functions#hsl}. # # The following functions are provided: # # *Note: These functions are described in more detail below.* # # ## RGB Functions # # \{#rgb rgb($red, $green, $blue)} # : Creates a {Sass::Script::Value::Color Color} from red, green, and blue # values. # # \{#rgba rgba($red, $green, $blue, $alpha)} # : Creates a {Sass::Script::Value::Color Color} from red, green, blue, and # alpha values. # # \{#red red($color)} # : Gets the red component of a color. # # \{#green green($color)} # : Gets the green component of a color. # # \{#blue blue($color)} # : Gets the blue component of a color. # # \{#mix mix($color1, $color2, \[$weight\])} # : Mixes two colors together. # # ## HSL Functions # # \{#hsl hsl($hue, $saturation, $lightness)} # : Creates a {Sass::Script::Value::Color Color} from hue, saturation, and # lightness values. # # \{#hsla hsla($hue, $saturation, $lightness, $alpha)} # : Creates a {Sass::Script::Value::Color Color} from hue, saturation, # lightness, and alpha values. # # \{#hue hue($color)} # : Gets the hue component of a color. # # \{#saturation saturation($color)} # : Gets the saturation component of a color. # # \{#lightness lightness($color)} # : Gets the lightness component of a color. # # \{#adjust_hue adjust-hue($color, $degrees)} # : Changes the hue of a color. # # \{#lighten lighten($color, $amount)} # : Makes a color lighter. # # \{#darken darken($color, $amount)} # : Makes a color darker. # # \{#saturate saturate($color, $amount)} # : Makes a color more saturated. # # \{#desaturate desaturate($color, $amount)} # : Makes a color less saturated. # # \{#grayscale grayscale($color)} # : Converts a color to grayscale. # # \{#complement complement($color)} # : Returns the complement of a color. # # \{#invert invert($color, \[$weight\])} # : Returns the inverse of a color. # # ## Opacity Functions # # \{#alpha alpha($color)} / \{#opacity opacity($color)} # : Gets the alpha component (opacity) of a color. # # \{#rgba rgba($color, $alpha)} # : Changes the alpha component for a color. # # \{#opacify opacify($color, $amount)} / \{#fade_in fade-in($color, $amount)} # : Makes a color more opaque. # # \{#transparentize transparentize($color, $amount)} / \{#fade_out fade-out($color, $amount)} # : Makes a color more transparent. # # ## Other Color Functions # # \{#adjust_color adjust-color($color, \[$red\], \[$green\], \[$blue\], \[$hue\], \[$saturation\], \[$lightness\], \[$alpha\])} # : Increases or decreases one or more components of a color. # # \{#scale_color scale-color($color, \[$red\], \[$green\], \[$blue\], \[$saturation\], \[$lightness\], \[$alpha\])} # : Fluidly scales one or more properties of a color. # # \{#change_color change-color($color, \[$red\], \[$green\], \[$blue\], \[$hue\], \[$saturation\], \[$lightness\], \[$alpha\])} # : Changes one or more properties of a color. # # \{#ie_hex_str ie-hex-str($color)} # : Converts a color into the format understood by IE filters. # # ## String Functions # # \{#unquote unquote($string)} # : Removes quotes from a string. # # \{#quote quote($string)} # : Adds quotes to a string. # # \{#str_length str-length($string)} # : Returns the number of characters in a string. # # \{#str_insert str-insert($string, $insert, $index)} # : Inserts `$insert` into `$string` at `$index`. # # \{#str_index str-index($string, $substring)} # : Returns the index of the first occurrence of `$substring` in `$string`. # # \{#str_slice str-slice($string, $start-at, [$end-at])} # : Extracts a substring from `$string`. # # \{#to_upper_case to-upper-case($string)} # : Converts a string to upper case. # # \{#to_lower_case to-lower-case($string)} # : Converts a string to lower case. # # ## Number Functions # # \{#percentage percentage($number)} # : Converts a unitless number to a percentage. # # \{#round round($number)} # : Rounds a number to the nearest whole number. # # \{#ceil ceil($number)} # : Rounds a number up to the next whole number. # # \{#floor floor($number)} # : Rounds a number down to the previous whole number. # # \{#abs abs($number)} # : Returns the absolute value of a number. # # \{#min min($numbers...)\} # : Finds the minimum of several numbers. # # \{#max max($numbers...)\} # : Finds the maximum of several numbers. # # \{#random random([$limit])\} # : Returns a random number. # # ## List Functions {#list-functions} # # Lists in Sass are immutable; all list functions return a new list rather # than updating the existing list in-place. # # All list functions work for maps as well, treating them as lists of pairs. # # \{#length length($list)} # : Returns the length of a list. # # \{#nth nth($list, $n)} # : Returns a specific item in a list. # # \{#set-nth set-nth($list, $n, $value)} # : Replaces the nth item in a list. # # \{#join join($list1, $list2, \[$separator, $bracketed\])} # : Joins together two lists into one. # # \{#append append($list1, $val, \[$separator\])} # : Appends a single value onto the end of a list. # # \{#zip zip($lists...)} # : Combines several lists into a single multidimensional list. # # \{#index index($list, $value)} # : Returns the position of a value within a list. # # \{#list_separator list-separator($list)} # : Returns the separator of a list. # # \{#is_bracketed is-bracketed($list)} # : Returns whether a list has square brackets. # # ## Map Functions {#map-functions} # # Maps in Sass are immutable; all map functions return a new map rather than # updating the existing map in-place. # # \{#map_get map-get($map, $key)} # : Returns the value in a map associated with a given key. # # \{#map_merge map-merge($map1, $map2)} # : Merges two maps together into a new map. # # \{#map_remove map-remove($map, $keys...)} # : Returns a new map with keys removed. # # \{#map_keys map-keys($map)} # : Returns a list of all keys in a map. # # \{#map_values map-values($map)} # : Returns a list of all values in a map. # # \{#map_has_key map-has-key($map, $key)} # : Returns whether a map has a value associated with a given key. # # \{#keywords keywords($args)} # : Returns the keywords passed to a function that takes variable arguments. # # ## Selector Functions # # Selector functions are very liberal in the formats they support # for selector arguments. They can take a plain string, a list of # lists as returned by `&` or anything in between: # # * A plain string, such as `".foo .bar, .baz .bang"`. # * A space-separated list of strings such as `(".foo" ".bar")`. # * A comma-separated list of strings such as `(".foo .bar", ".baz .bang")`. # * A comma-separated list of space-separated lists of strings such # as `((".foo" ".bar"), (".baz" ".bang"))`. # # In general, selector functions allow placeholder selectors # (`%foo`) but disallow parent-reference selectors (`&`). # # \{#selector_nest selector-nest($selectors...)} # : Nests selector beneath one another like they would be nested in the # stylesheet. # # \{#selector_append selector-append($selectors...)} # : Appends selectors to one another without spaces in between. # # \{#selector_extend selector-extend($selector, $extendee, $extender)} # : Extends `$extendee` with `$extender` within `$selector`. # # \{#selector_replace selector-replace($selector, $original, $replacement)} # : Replaces `$original` with `$replacement` within `$selector`. # # \{#selector_unify selector-unify($selector1, $selector2)} # : Unifies two selectors to produce a selector that matches # elements matched by both. # # \{#is_superselector is-superselector($super, $sub)} # : Returns whether `$super` matches all the elements `$sub` does, and # possibly more. # # \{#simple_selectors simple-selectors($selector)} # : Returns the simple selectors that comprise a compound selector. # # \{#selector_parse selector-parse($selector)} # : Parses a selector into the format returned by `&`. # # ## Introspection Functions # # \{#feature_exists feature-exists($feature)} # : Returns whether a feature exists in the current Sass runtime. # # \{#variable_exists variable-exists($name)} # : Returns whether a variable with the given name exists in the current scope. # # \{#global_variable_exists global-variable-exists($name)} # : Returns whether a variable with the given name exists in the global scope. # # \{#function_exists function-exists($name)} # : Returns whether a function with the given name exists. # # \{#mixin_exists mixin-exists($name)} # : Returns whether a mixin with the given name exists. # # \{#content_exists content-exists()} # : Returns whether the current mixin was passed a content block. # # \{#inspect inspect($value)} # : Returns the string representation of a value as it would be represented in Sass. # # \{#type_of type-of($value)} # : Returns the type of a value. # # \{#unit unit($number)} # : Returns the unit(s) associated with a number. # # \{#unitless unitless($number)} # : Returns whether a number has units. # # \{#comparable comparable($number1, $number2)} # : Returns whether two numbers can be added, subtracted, or compared. # # \{#call call($function, $args...)} # : Dynamically calls a Sass function reference returned by `get-function`. # # \{#get_function get-function($name, $css: false)} # : Looks up a function with the given name in the current lexical scope # and returns a reference to it. # # ## Miscellaneous Functions # # \{#if if($condition, $if-true, $if-false)} # : Returns one of two values, depending on whether or not `$condition` is # true. # # \{#unique_id unique-id()} # : Returns a unique CSS identifier. # # ## Adding Custom Functions # # New Sass functions can be added by adding Ruby methods to this module. # For example: # # module Sass::Script::Functions # def reverse(string) # assert_type string, :String # Sass::Script::Value::String.new(string.value.reverse) # end # declare :reverse, [:string] # end # # Calling {declare} tells Sass the argument names for your function. # If omitted, the function will still work, but will not be able to accept keyword arguments. # {declare} can also allow your function to take arbitrary keyword arguments. # # There are a few things to keep in mind when modifying this module. # First of all, the arguments passed are {Value} objects. # Value objects are also expected to be returned. # This means that Ruby values must be unwrapped and wrapped. # # Most Value objects support the {Value::Base#value value} accessor for getting # their Ruby values. Color objects, though, must be accessed using # {Sass::Script::Value::Color#rgb rgb}, {Sass::Script::Value::Color#red red}, # {Sass::Script::Value::Color#blue green}, or {Sass::Script::Value::Color#blue # blue}. # # Second, making Ruby functions accessible from Sass introduces the temptation # to do things like database access within stylesheets. # This is generally a bad idea; # since Sass files are by default only compiled once, # dynamic code is not a great fit. # # If you really, really need to compile Sass on each request, # first make sure you have adequate caching set up. # Then you can use {Sass::Engine} to render the code, # using the {file:SASS_REFERENCE.md#custom-option `options` parameter} # to pass in data that {EvaluationContext#options can be accessed} # from your Sass functions. # # Within one of the functions in this module, # methods of {EvaluationContext} can be used. # # ### Caveats # # When creating new {Value} objects within functions, be aware that it's not # safe to call {Value::Base#to_s #to_s} (or other methods that use the string # representation) on those objects without first setting {Tree::Node#options= # the #options attribute}. # module Functions @signatures = {} # A class representing a Sass function signature. # # @attr args [Array] The names of the arguments to the function. # @attr delayed_args [Array] The names of the arguments whose evaluation should be # delayed. # @attr var_args [Boolean] Whether the function takes a variable number of arguments. # @attr var_kwargs [Boolean] Whether the function takes an arbitrary set of keyword arguments. Signature = Struct.new(:args, :delayed_args, :var_args, :var_kwargs, :deprecated) # Declare a Sass signature for a Ruby-defined function. # This includes the names of the arguments, # whether the function takes a variable number of arguments, # and whether the function takes an arbitrary set of keyword arguments. # # It's not necessary to declare a signature for a function. # However, without a signature it won't support keyword arguments. # # A single function can have multiple signatures declared # as long as each one takes a different number of arguments. # It's also possible to declare multiple signatures # that all take the same number of arguments, # but none of them but the first will be used # unless the user uses keyword arguments. # # @example # declare :rgba, [:hex, :alpha] # declare :rgba, [:red, :green, :blue, :alpha] # declare :accepts_anything, [], :var_args => true, :var_kwargs => true # declare :some_func, [:foo, :bar, :baz], :var_kwargs => true # # @param method_name [Symbol] The name of the method # whose signature is being declared. # @param args [Array] The names of the arguments for the function signature. # @option options :var_args [Boolean] (false) # Whether the function accepts a variable number of (unnamed) arguments # in addition to the named arguments. # @option options :var_kwargs [Boolean] (false) # Whether the function accepts other keyword arguments # in addition to those in `:args`. # If this is true, the Ruby function will be passed a hash from strings # to {Value}s as the last argument. # In addition, if this is true and `:var_args` is not, # Sass will ensure that the last argument passed is a hash. def self.declare(method_name, args, options = {}) delayed_args = [] args = args.map do |a| a = a.to_s if a[0] == ?& a = a[1..-1] delayed_args << a end a end # We don't expose this functionality except to certain builtin methods. if delayed_args.any? && method_name != :if raise ArgumentError.new("Delayed arguments are not allowed for method #{method_name}") end @signatures[method_name] ||= [] @signatures[method_name] << Signature.new( args, delayed_args, options[:var_args], options[:var_kwargs], options[:deprecated] && options[:deprecated].map {|a| a.to_s}) end # Determine the correct signature for the number of arguments # passed in for a given function. # If no signatures match, the first signature is returned for error messaging. # # @param method_name [Symbol] The name of the Ruby function to be called. # @param arg_arity [Integer] The number of unnamed arguments the function was passed. # @param kwarg_arity [Integer] The number of keyword arguments the function was passed. # # @return [{Symbol => Object}, nil] # The signature options for the matching signature, # or nil if no signatures are declared for this function. See {declare}. def self.signature(method_name, arg_arity, kwarg_arity) return unless @signatures[method_name] @signatures[method_name].each do |signature| sig_arity = signature.args.size return signature if sig_arity == arg_arity + kwarg_arity next unless sig_arity < arg_arity + kwarg_arity # We have enough args. # Now we need to figure out which args are varargs # and if the signature allows them. t_arg_arity, t_kwarg_arity = arg_arity, kwarg_arity if sig_arity > t_arg_arity # we transfer some kwargs arity to args arity # if it does not have enough args -- assuming the names will work out. t_kwarg_arity -= (sig_arity - t_arg_arity) t_arg_arity = sig_arity end if (t_arg_arity == sig_arity || t_arg_arity > sig_arity && signature.var_args) && (t_kwarg_arity == 0 || t_kwarg_arity > 0 && signature.var_kwargs) return signature end end @signatures[method_name].first end # Sets the random seed used by Sass's internal random number generator. # # This can be used to ensure consistent random number sequences which # allows for consistent results when testing, etc. # # @param seed [Integer] # @return [Integer] The same seed. def self.random_seed=(seed) @random_number_generator = Random.new(seed) end # Get Sass's internal random number generator. # # @return [Random] def self.random_number_generator @random_number_generator ||= Random.new end # The context in which methods in {Script::Functions} are evaluated. # That means that all instance methods of {EvaluationContext} # are available to use in functions. class EvaluationContext include Functions include Value::Helpers # The human-readable names for [Sass::Script::Value::Base]. The default is # just the downcased name of the type. TYPE_NAMES = {:ArgList => 'variable argument list'} # The environment for this function. This environment's # {Environment#parent} is the global environment, and its # {Environment#caller} is a read-only view of the local environment of the # caller of this function. # # @return [Environment] attr_reader :environment # The options hash for the {Sass::Engine} that is processing the function call # # @return [{Symbol => Object}] attr_reader :options # @param environment [Environment] See \{#environment} def initialize(environment) @environment = environment @options = environment.options end # Asserts that the type of a given SassScript value # is the expected type (designated by a symbol). # # Valid types are `:Bool`, `:Color`, `:Number`, and `:String`. # Note that `:String` will match both double-quoted strings # and unquoted identifiers. # # @example # assert_type value, :String # assert_type value, :Number # @param value [Sass::Script::Value::Base] A SassScript value # @param type [Symbol, Array] The name(s) of the type the value is expected to be # @param name [String, Symbol, nil] The name of the argument. # @raise [ArgumentError] if value is not of the correct type. def assert_type(value, type, name = nil) valid_types = Array(type) found_type = valid_types.find do |t| value.is_a?(Sass::Script::Value.const_get(t)) || t == :Map && value.is_a?(Sass::Script::Value::List) && value.value.empty? end if found_type value.check_deprecated_interp if found_type == :String return end err = if valid_types.size == 1 "#{value.inspect} is not a #{TYPE_NAMES[type] || type.to_s.downcase}" else type_names = valid_types.map {|t| TYPE_NAMES[t] || t.to_s.downcase} "#{value.inspect} is not any of #{type_names.join(', ')}" end err = "$#{name.to_s.tr('_', '-')}: " + err if name raise ArgumentError.new(err) end # Asserts that the unit of the number is as expected. # # @example # assert_unit number, "px" # assert_unit number, nil # @param number [Sass::Script::Value::Number] The number to be validated. # @param unit [::String] # The unit that the number must have. # If nil, the number must be unitless. # @param name [::String] The name of the parameter being validated. # @raise [ArgumentError] if number is not of the correct unit or is not a number. def assert_unit(number, unit, name = nil) assert_type number, :Number, name return if number.is_unit?(unit) expectation = unit ? "have a unit of #{unit}" : "be unitless" if name raise ArgumentError.new("Expected $#{name} to #{expectation} but got #{number}") else raise ArgumentError.new("Expected #{number} to #{expectation}") end end # Asserts that the value is an integer. # # @example # assert_integer 2px # assert_integer 2.5px # => SyntaxError: "Expected 2.5px to be an integer" # assert_integer 2.5px, "width" # => SyntaxError: "Expected width to be an integer but got 2.5px" # @param number [Sass::Script::Value::Base] The value to be validated. # @param name [::String] The name of the parameter being validated. # @raise [ArgumentError] if number is not an integer or is not a number. def assert_integer(number, name = nil) assert_type number, :Number, name return if number.int? if name raise ArgumentError.new("Expected $#{name} to be an integer but got #{number}") else raise ArgumentError.new("Expected #{number} to be an integer") end end # Performs a node that has been delayed for execution. # # @private # @param node [Sass::Script::Tree::Node, # Sass::Script::Value::Base] When this is a tree node, it's # performed in the caller's environment. When it's a value # (which can happen when the value had to be performed already # -- like for a splat), it's returned as-is. # @param env [Sass::Environment] The environment within which to perform the node. # Defaults to the (read-only) environment of the caller. def perform(node, env = environment.caller) if node.is_a?(Sass::Script::Value::Base) node else node.perform(env) end end end class << self # Returns whether user function with a given name exists. # # @param function_name [String] # @return [Boolean] alias_method :callable?, :public_method_defined? private def include(*args) r = super # We have to re-include ourselves into EvaluationContext to work around # an icky Ruby restriction. EvaluationContext.send :include, self r end end # Creates a {Sass::Script::Value::Color Color} object from red, green, and # blue values. # # @see #rgba # @overload rgb($red, $green, $blue) # @param $red [Sass::Script::Value::Number] The amount of red in the color. # Must be between 0 and 255 inclusive, or between `0%` and `100%` # inclusive # @param $green [Sass::Script::Value::Number] The amount of green in the # color. Must be between 0 and 255 inclusive, or between `0%` and `100%` # inclusive # @param $blue [Sass::Script::Value::Number] The amount of blue in the # color. Must be between 0 and 255 inclusive, or between `0%` and `100%` # inclusive # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if any parameter is the wrong type or out of bounds def rgb(red, green = nil, blue = nil) if green.nil? return unquoted_string("rgb(#{red})") if var?(red) raise ArgumentError.new("wrong number of arguments (1 for 3)") elsif blue.nil? return unquoted_string("rgb(#{red}, #{green})") if var?(red) || var?(green) raise ArgumentError.new("wrong number of arguments (2 for 3)") end if special_number?(red) || special_number?(green) || special_number?(blue) return unquoted_string("rgb(#{red}, #{green}, #{blue})") end assert_type red, :Number, :red assert_type green, :Number, :green assert_type blue, :Number, :blue color_attrs = [ percentage_or_unitless(red, 255, "red"), percentage_or_unitless(green, 255, "green"), percentage_or_unitless(blue, 255, "blue") ] # Don't store the string representation for function-created colors, both # because it's not very useful and because some functions aren't supported # on older browsers. Sass::Script::Value::Color.new(color_attrs) end declare :rgb, [:red, :green, :blue] declare :rgb, [:red, :green] declare :rgb, [:red] # Creates a {Sass::Script::Value::Color Color} from red, green, blue, and # alpha values. # @see #rgb # # @overload rgba($red, $green, $blue, $alpha) # @param $red [Sass::Script::Value::Number] The amount of red in the # color. Must be between 0 and 255 inclusive or 0% and 100% inclusive # @param $green [Sass::Script::Value::Number] The amount of green in the # color. Must be between 0 and 255 inclusive or 0% and 100% inclusive # @param $blue [Sass::Script::Value::Number] The amount of blue in the # color. Must be between 0 and 255 inclusive or 0% and 100% inclusive # @param $alpha [Sass::Script::Value::Number] The opacity of the color. # Must be between 0 and 1 inclusive # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if any parameter is the wrong type or out of # bounds # # @overload rgba($color, $alpha) # Sets the opacity of an existing color. # # @example # rgba(#102030, 0.5) => rgba(16, 32, 48, 0.5) # rgba(blue, 0.2) => rgba(0, 0, 255, 0.2) # # @param $color [Sass::Script::Value::Color] The color whose opacity will # be changed. # @param $alpha [Sass::Script::Value::Number] The new opacity of the # color. Must be between 0 and 1 inclusive # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if `$alpha` is out of bounds or either parameter # is the wrong type def rgba(*args) case args.size when 1 return unquoted_string("rgba(#{args.first})") if var?(args.first) raise ArgumentError.new("wrong number of arguments (1 for 4)") when 2 color, alpha = args if var?(color) return unquoted_string("rgba(#{color}, #{alpha})") elsif var?(alpha) if color.is_a?(Sass::Script::Value::Color) return unquoted_string("rgba(#{color.red}, #{color.green}, #{color.blue}, #{alpha})") else return unquoted_string("rgba(#{color}, #{alpha})") end end assert_type color, :Color, :color if special_number?(alpha) unquoted_string("rgba(#{color.red}, #{color.green}, #{color.blue}, #{alpha})") else assert_type alpha, :Number, :alpha color.with(:alpha => percentage_or_unitless(alpha, 1, "alpha")) end when 3 if var?(args[0]) || var?(args[1]) || var?(args[2]) unquoted_string("rgba(#{args.join(', ')})") else raise ArgumentError.new("wrong number of arguments (3 for 4)") end when 4 red, green, blue, alpha = args if special_number?(red) || special_number?(green) || special_number?(blue) || special_number?(alpha) unquoted_string("rgba(#{red}, #{green}, #{blue}, #{alpha})") else rgba(rgb(red, green, blue), alpha) end else raise ArgumentError.new("wrong number of arguments (#{args.size} for 4)") end end declare :rgba, [:red, :green, :blue, :alpha] declare :rgba, [:red, :green, :blue] declare :rgba, [:color, :alpha] declare :rgba, [:red] # Creates a {Sass::Script::Value::Color Color} from hue, saturation, and # lightness values. Uses the algorithm from the [CSS3 spec][]. # # [CSS3 spec]: http://www.w3.org/TR/css3-color/#hsl-color # # @see #hsla # @overload hsl($hue, $saturation, $lightness) # @param $hue [Sass::Script::Value::Number] The hue of the color. Should be # between 0 and 360 degrees, inclusive # @param $saturation [Sass::Script::Value::Number] The saturation of the # color. Must be between `0%` and `100%`, inclusive # @param $lightness [Sass::Script::Value::Number] The lightness of the # color. Must be between `0%` and `100%`, inclusive # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if `$saturation` or `$lightness` are out of bounds # or any parameter is the wrong type def hsl(hue, saturation = nil, lightness = nil) if saturation.nil? return unquoted_string("hsl(#{hue})") if var?(hue) raise ArgumentError.new("wrong number of arguments (1 for 3)") elsif lightness.nil? return unquoted_string("hsl(#{hue}, #{saturation})") if var?(hue) || var?(saturation) raise ArgumentError.new("wrong number of arguments (2 for 3)") end if special_number?(hue) || special_number?(saturation) || special_number?(lightness) unquoted_string("hsl(#{hue}, #{saturation}, #{lightness})") else hsla(hue, saturation, lightness, number(1)) end end declare :hsl, [:hue, :saturation, :lightness] declare :hsl, [:hue, :saturation] declare :hsl, [:hue] # Creates a {Sass::Script::Value::Color Color} from hue, # saturation, lightness, and alpha values. Uses the algorithm from # the [CSS3 spec][]. # # [CSS3 spec]: http://www.w3.org/TR/css3-color/#hsl-color # # @see #hsl # @overload hsla($hue, $saturation, $lightness, $alpha) # @param $hue [Sass::Script::Value::Number] The hue of the color. Should be # between 0 and 360 degrees, inclusive # @param $saturation [Sass::Script::Value::Number] The saturation of the # color. Must be between `0%` and `100%`, inclusive # @param $lightness [Sass::Script::Value::Number] The lightness of the # color. Must be between `0%` and `100%`, inclusive # @param $alpha [Sass::Script::Value::Number] The opacity of the color. Must # be between 0 and 1, inclusive # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if `$saturation`, `$lightness`, or `$alpha` are out # of bounds or any parameter is the wrong type def hsla(hue, saturation = nil, lightness = nil, alpha = nil) if saturation.nil? return unquoted_string("hsla(#{hue})") if var?(hue) raise ArgumentError.new("wrong number of arguments (1 for 4)") elsif lightness.nil? return unquoted_string("hsla(#{hue}, #{saturation})") if var?(hue) || var?(saturation) raise ArgumentError.new("wrong number of arguments (2 for 4)") elsif alpha.nil? if var?(hue) || var?(saturation) || var?(lightness) return unquoted_string("hsla(#{hue}, #{saturation}, #{lightness})") else raise ArgumentError.new("wrong number of arguments (2 for 4)") end end if special_number?(hue) || special_number?(saturation) || special_number?(lightness) || special_number?(alpha) return unquoted_string("hsla(#{hue}, #{saturation}, #{lightness}, #{alpha})") end assert_type hue, :Number, :hue assert_type saturation, :Number, :saturation assert_type lightness, :Number, :lightness assert_type alpha, :Number, :alpha h = hue.value s = saturation.value l = lightness.value # Don't store the string representation for function-created colors, both # because it's not very useful and because some functions aren't supported # on older browsers. Sass::Script::Value::Color.new( :hue => h, :saturation => s, :lightness => l, :alpha => percentage_or_unitless(alpha, 1, "alpha")) end declare :hsla, [:hue, :saturation, :lightness, :alpha] declare :hsla, [:hue, :saturation, :lightness] declare :hsla, [:hue, :saturation] declare :hsla, [:hue] # Gets the red component of a color. Calculated from HSL where necessary via # [this algorithm][hsl-to-rgb]. # # [hsl-to-rgb]: http://www.w3.org/TR/css3-color/#hsl-color # # @overload red($color) # @param $color [Sass::Script::Value::Color] # @return [Sass::Script::Value::Number] The red component, between 0 and 255 # inclusive # @raise [ArgumentError] if `$color` isn't a color def red(color) assert_type color, :Color, :color number(color.red) end declare :red, [:color] # Gets the green component of a color. Calculated from HSL where necessary # via [this algorithm][hsl-to-rgb]. # # [hsl-to-rgb]: http://www.w3.org/TR/css3-color/#hsl-color # # @overload green($color) # @param $color [Sass::Script::Value::Color] # @return [Sass::Script::Value::Number] The green component, between 0 and # 255 inclusive # @raise [ArgumentError] if `$color` isn't a color def green(color) assert_type color, :Color, :color number(color.green) end declare :green, [:color] # Gets the blue component of a color. Calculated from HSL where necessary # via [this algorithm][hsl-to-rgb]. # # [hsl-to-rgb]: http://www.w3.org/TR/css3-color/#hsl-color # # @overload blue($color) # @param $color [Sass::Script::Value::Color] # @return [Sass::Script::Value::Number] The blue component, between 0 and # 255 inclusive # @raise [ArgumentError] if `$color` isn't a color def blue(color) assert_type color, :Color, :color number(color.blue) end declare :blue, [:color] # Returns the hue component of a color. See [the CSS3 HSL # specification][hsl]. Calculated from RGB where necessary via [this # algorithm][rgb-to-hsl]. # # [hsl]: http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV # [rgb-to-hsl]: http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV # # @overload hue($color) # @param $color [Sass::Script::Value::Color] # @return [Sass::Script::Value::Number] The hue component, between 0deg and # 360deg # @raise [ArgumentError] if `$color` isn't a color def hue(color) assert_type color, :Color, :color number(color.hue, "deg") end declare :hue, [:color] # Returns the saturation component of a color. See [the CSS3 HSL # specification][hsl]. Calculated from RGB where necessary via [this # algorithm][rgb-to-hsl]. # # [hsl]: http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV # [rgb-to-hsl]: http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV # # @overload saturation($color) # @param $color [Sass::Script::Value::Color] # @return [Sass::Script::Value::Number] The saturation component, between 0% # and 100% # @raise [ArgumentError] if `$color` isn't a color def saturation(color) assert_type color, :Color, :color number(color.saturation, "%") end declare :saturation, [:color] # Returns the lightness component of a color. See [the CSS3 HSL # specification][hsl]. Calculated from RGB where necessary via [this # algorithm][rgb-to-hsl]. # # [hsl]: http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV # [rgb-to-hsl]: http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV # # @overload lightness($color) # @param $color [Sass::Script::Value::Color] # @return [Sass::Script::Value::Number] The lightness component, between 0% # and 100% # @raise [ArgumentError] if `$color` isn't a color def lightness(color) assert_type color, :Color, :color number(color.lightness, "%") end declare :lightness, [:color] # Returns the alpha component (opacity) of a color. This is 1 unless # otherwise specified. # # This function also supports the proprietary Microsoft `alpha(opacity=20)` # syntax as a special case. # # @overload alpha($color) # @param $color [Sass::Script::Value::Color] # @return [Sass::Script::Value::Number] The alpha component, between 0 and 1 # @raise [ArgumentError] if `$color` isn't a color def alpha(*args) if args.all? do |a| a.is_a?(Sass::Script::Value::String) && a.type == :identifier && a.value =~ /^[a-zA-Z]+\s*=/ end # Support the proprietary MS alpha() function return identifier("alpha(#{args.map {|a| a.to_s}.join(', ')})") end raise ArgumentError.new("wrong number of arguments (#{args.size} for 1)") if args.size != 1 assert_type args.first, :Color, :color number(args.first.alpha) end declare :alpha, [:color] # Returns the alpha component (opacity) of a color. This is 1 unless # otherwise specified. # # @overload opacity($color) # @param $color [Sass::Script::Value::Color] # @return [Sass::Script::Value::Number] The alpha component, between 0 and 1 # @raise [ArgumentError] if `$color` isn't a color def opacity(color) if color.is_a?(Sass::Script::Value::Number) return identifier("opacity(#{color})") end assert_type color, :Color, :color number(color.alpha) end declare :opacity, [:color] # Makes a color more opaque. Takes a color and a number between 0 and 1, and # returns a color with the opacity increased by that amount. # # @see #transparentize # @example # opacify(rgba(0, 0, 0, 0.5), 0.1) => rgba(0, 0, 0, 0.6) # opacify(rgba(0, 0, 17, 0.8), 0.2) => #001 # @overload opacify($color, $amount) # @param $color [Sass::Script::Value::Color] # @param $amount [Sass::Script::Value::Number] The amount to increase the # opacity by, between 0 and 1 # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if `$amount` is out of bounds, or either parameter # is the wrong type def opacify(color, amount) _adjust(color, amount, :alpha, 0..1, :+) end declare :opacify, [:color, :amount] alias_method :fade_in, :opacify declare :fade_in, [:color, :amount] # Makes a color more transparent. Takes a color and a number between 0 and # 1, and returns a color with the opacity decreased by that amount. # # @see #opacify # @example # transparentize(rgba(0, 0, 0, 0.5), 0.1) => rgba(0, 0, 0, 0.4) # transparentize(rgba(0, 0, 0, 0.8), 0.2) => rgba(0, 0, 0, 0.6) # @overload transparentize($color, $amount) # @param $color [Sass::Script::Value::Color] # @param $amount [Sass::Script::Value::Number] The amount to decrease the # opacity by, between 0 and 1 # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if `$amount` is out of bounds, or either parameter # is the wrong type def transparentize(color, amount) _adjust(color, amount, :alpha, 0..1, :-) end declare :transparentize, [:color, :amount] alias_method :fade_out, :transparentize declare :fade_out, [:color, :amount] # Makes a color lighter. Takes a color and a number between `0%` and `100%`, # and returns a color with the lightness increased by that amount. # # @see #darken # @example # lighten(hsl(0, 0%, 0%), 30%) => hsl(0, 0, 30) # lighten(#800, 20%) => #e00 # @overload lighten($color, $amount) # @param $color [Sass::Script::Value::Color] # @param $amount [Sass::Script::Value::Number] The amount to increase the # lightness by, between `0%` and `100%` # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if `$amount` is out of bounds, or either parameter # is the wrong type def lighten(color, amount) _adjust(color, amount, :lightness, 0..100, :+, "%") end declare :lighten, [:color, :amount] # Makes a color darker. Takes a color and a number between 0% and 100%, and # returns a color with the lightness decreased by that amount. # # @see #lighten # @example # darken(hsl(25, 100%, 80%), 30%) => hsl(25, 100%, 50%) # darken(#800, 20%) => #200 # @overload darken($color, $amount) # @param $color [Sass::Script::Value::Color] # @param $amount [Sass::Script::Value::Number] The amount to decrease the # lightness by, between `0%` and `100%` # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if `$amount` is out of bounds, or either parameter # is the wrong type def darken(color, amount) _adjust(color, amount, :lightness, 0..100, :-, "%") end declare :darken, [:color, :amount] # Makes a color more saturated. Takes a color and a number between 0% and # 100%, and returns a color with the saturation increased by that amount. # # @see #desaturate # @example # saturate(hsl(120, 30%, 90%), 20%) => hsl(120, 50%, 90%) # saturate(#855, 20%) => #9e3f3f # @overload saturate($color, $amount) # @param $color [Sass::Script::Value::Color] # @param $amount [Sass::Script::Value::Number] The amount to increase the # saturation by, between `0%` and `100%` # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if `$amount` is out of bounds, or either parameter # is the wrong type def saturate(color, amount = nil) # Support the filter effects definition of saturate. # https://dvcs.w3.org/hg/FXTF/raw-file/tip/filters/index.html return identifier("saturate(#{color})") if amount.nil? _adjust(color, amount, :saturation, 0..100, :+, "%") end declare :saturate, [:color, :amount] declare :saturate, [:amount] # Makes a color less saturated. Takes a color and a number between 0% and # 100%, and returns a color with the saturation decreased by that value. # # @see #saturate # @example # desaturate(hsl(120, 30%, 90%), 20%) => hsl(120, 10%, 90%) # desaturate(#855, 20%) => #726b6b # @overload desaturate($color, $amount) # @param $color [Sass::Script::Value::Color] # @param $amount [Sass::Script::Value::Number] The amount to decrease the # saturation by, between `0%` and `100%` # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if `$amount` is out of bounds, or either parameter # is the wrong type def desaturate(color, amount) _adjust(color, amount, :saturation, 0..100, :-, "%") end declare :desaturate, [:color, :amount] # Changes the hue of a color. Takes a color and a number of degrees (usually # between `-360deg` and `360deg`), and returns a color with the hue rotated # along the color wheel by that amount. # # @example # adjust-hue(hsl(120, 30%, 90%), 60deg) => hsl(180, 30%, 90%) # adjust-hue(hsl(120, 30%, 90%), -60deg) => hsl(60, 30%, 90%) # adjust-hue(#811, 45deg) => #886a11 # @overload adjust_hue($color, $degrees) # @param $color [Sass::Script::Value::Color] # @param $degrees [Sass::Script::Value::Number] The number of degrees to # rotate the hue # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if either parameter is the wrong type def adjust_hue(color, degrees) assert_type color, :Color, :color assert_type degrees, :Number, :degrees color.with(:hue => color.hue + degrees.value) end declare :adjust_hue, [:color, :degrees] # Converts a color into the format understood by IE filters. # # @example # ie-hex-str(#abc) => #FFAABBCC # ie-hex-str(#3322BB) => #FF3322BB # ie-hex-str(rgba(0, 255, 0, 0.5)) => #8000FF00 # @overload ie_hex_str($color) # @param $color [Sass::Script::Value::Color] # @return [Sass::Script::Value::String] The IE-formatted string # representation of the color # @raise [ArgumentError] if `$color` isn't a color def ie_hex_str(color) assert_type color, :Color, :color alpha = Sass::Util.round(color.alpha * 255).to_s(16).rjust(2, '0') identifier("##{alpha}#{color.send(:hex_str)[1..-1]}".upcase) end declare :ie_hex_str, [:color] # Increases or decreases one or more properties of a color. This can change # the red, green, blue, hue, saturation, value, and alpha properties. The # properties are specified as keyword arguments, and are added to or # subtracted from the color's current value for that property. # # All properties are optional. You can't specify both RGB properties # (`$red`, `$green`, `$blue`) and HSL properties (`$hue`, `$saturation`, # `$value`) at the same time. # # @example # adjust-color(#102030, $blue: 5) => #102035 # adjust-color(#102030, $red: -5, $blue: 5) => #0b2035 # adjust-color(hsl(25, 100%, 80%), $lightness: -30%, $alpha: -0.4) => hsla(25, 100%, 50%, 0.6) # @overload adjust_color($color, [$red], [$green], [$blue], [$hue], [$saturation], [$lightness], [$alpha]) # @param $color [Sass::Script::Value::Color] # @param $red [Sass::Script::Value::Number] The adjustment to make on the # red component, between -255 and 255 inclusive # @param $green [Sass::Script::Value::Number] The adjustment to make on the # green component, between -255 and 255 inclusive # @param $blue [Sass::Script::Value::Number] The adjustment to make on the # blue component, between -255 and 255 inclusive # @param $hue [Sass::Script::Value::Number] The adjustment to make on the # hue component, in degrees # @param $saturation [Sass::Script::Value::Number] The adjustment to make on # the saturation component, between `-100%` and `100%` inclusive # @param $lightness [Sass::Script::Value::Number] The adjustment to make on # the lightness component, between `-100%` and `100%` inclusive # @param $alpha [Sass::Script::Value::Number] The adjustment to make on the # alpha component, between -1 and 1 inclusive # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if any parameter is the wrong type or out-of # bounds, or if RGB properties and HSL properties are adjusted at the # same time def adjust_color(color, kwargs) assert_type color, :Color, :color with = Sass::Util.map_hash( "red" => [-255..255, ""], "green" => [-255..255, ""], "blue" => [-255..255, ""], "hue" => nil, "saturation" => [-100..100, "%"], "lightness" => [-100..100, "%"], "alpha" => [-1..1, ""] ) do |name, (range, units)| val = kwargs.delete(name) next unless val assert_type val, :Number, name Sass::Util.check_range("$#{name}: Amount", range, val, units) if range adjusted = color.send(name) + val.value adjusted = [0, Sass::Util.restrict(adjusted, range)].max if range [name.to_sym, adjusted] end unless kwargs.empty? name, val = kwargs.to_a.first raise ArgumentError.new("Unknown argument $#{name} (#{val})") end color.with(with) end declare :adjust_color, [:color], :var_kwargs => true # Fluidly scales one or more properties of a color. Unlike # \{#adjust_color adjust-color}, which changes a color's properties by fixed # amounts, \{#scale_color scale-color} fluidly changes them based on how # high or low they already are. That means that lightening an already-light # color with \{#scale_color scale-color} won't change the lightness much, # but lightening a dark color by the same amount will change it more # dramatically. This has the benefit of making `scale-color($color, ...)` # have a similar effect regardless of what `$color` is. # # For example, the lightness of a color can be anywhere between `0%` and # `100%`. If `scale-color($color, $lightness: 40%)` is called, the resulting # color's lightness will be 40% of the way between its original lightness # and 100. If `scale-color($color, $lightness: -40%)` is called instead, the # lightness will be 40% of the way between the original and 0. # # This can change the red, green, blue, saturation, value, and alpha # properties. The properties are specified as keyword arguments. All # arguments should be percentages between `0%` and `100%`. # # All properties are optional. You can't specify both RGB properties # (`$red`, `$green`, `$blue`) and HSL properties (`$saturation`, `$value`) # at the same time. # # @example # scale-color(hsl(120, 70%, 80%), $lightness: 50%) => hsl(120, 70%, 90%) # scale-color(rgb(200, 150%, 170%), $green: -40%, $blue: 70%) => rgb(200, 90, 229) # scale-color(hsl(200, 70%, 80%), $saturation: -90%, $alpha: -30%) => hsla(200, 7%, 80%, 0.7) # @overload scale_color($color, [$red], [$green], [$blue], [$saturation], [$lightness], [$alpha]) # @param $color [Sass::Script::Value::Color] # @param $red [Sass::Script::Value::Number] # @param $green [Sass::Script::Value::Number] # @param $blue [Sass::Script::Value::Number] # @param $saturation [Sass::Script::Value::Number] # @param $lightness [Sass::Script::Value::Number] # @param $alpha [Sass::Script::Value::Number] # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if any parameter is the wrong type or out-of # bounds, or if RGB properties and HSL properties are adjusted at the # same time def scale_color(color, kwargs) assert_type color, :Color, :color with = Sass::Util.map_hash( "red" => 255, "green" => 255, "blue" => 255, "saturation" => 100, "lightness" => 100, "alpha" => 1 ) do |name, max| val = kwargs.delete(name) next unless val assert_type val, :Number, name assert_unit val, '%', name Sass::Util.check_range("$#{name}: Amount", -100..100, val, '%') current = color.send(name) scale = val.value / 100.0 diff = scale > 0 ? max - current : current [name.to_sym, current + diff * scale] end unless kwargs.empty? name, val = kwargs.to_a.first raise ArgumentError.new("Unknown argument $#{name} (#{val})") end color.with(with) end declare :scale_color, [:color], :var_kwargs => true # Changes one or more properties of a color. This can change the red, green, # blue, hue, saturation, value, and alpha properties. The properties are # specified as keyword arguments, and replace the color's current value for # that property. # # All properties are optional. You can't specify both RGB properties # (`$red`, `$green`, `$blue`) and HSL properties (`$hue`, `$saturation`, # `$value`) at the same time. # # @example # change-color(#102030, $blue: 5) => #102005 # change-color(#102030, $red: 120, $blue: 5) => #782005 # change-color(hsl(25, 100%, 80%), $lightness: 40%, $alpha: 0.8) => hsla(25, 100%, 40%, 0.8) # @overload change_color($color, [$red], [$green], [$blue], [$hue], [$saturation], [$lightness], [$alpha]) # @param $color [Sass::Script::Value::Color] # @param $red [Sass::Script::Value::Number] The new red component for the # color, within 0 and 255 inclusive # @param $green [Sass::Script::Value::Number] The new green component for # the color, within 0 and 255 inclusive # @param $blue [Sass::Script::Value::Number] The new blue component for the # color, within 0 and 255 inclusive # @param $hue [Sass::Script::Value::Number] The new hue component for the # color, in degrees # @param $saturation [Sass::Script::Value::Number] The new saturation # component for the color, between `0%` and `100%` inclusive # @param $lightness [Sass::Script::Value::Number] The new lightness # component for the color, within `0%` and `100%` inclusive # @param $alpha [Sass::Script::Value::Number] The new alpha component for # the color, within 0 and 1 inclusive # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if any parameter is the wrong type or out-of # bounds, or if RGB properties and HSL properties are adjusted at the # same time def change_color(color, kwargs) assert_type color, :Color, :color with = Sass::Util.map_hash( 'red' => ['Red value', 0..255], 'green' => ['Green value', 0..255], 'blue' => ['Blue value', 0..255], 'hue' => [], 'saturation' => ['Saturation', 0..100, '%'], 'lightness' => ['Lightness', 0..100, '%'], 'alpha' => ['Alpha channel', 0..1] ) do |name, (desc, range, unit)| val = kwargs.delete(name) next unless val assert_type val, :Number, name if range val = Sass::Util.check_range(desc, range, val, unit) else val = val.value end [name.to_sym, val] end unless kwargs.empty? name, val = kwargs.to_a.first raise ArgumentError.new("Unknown argument $#{name} (#{val})") end color.with(with) end declare :change_color, [:color], :var_kwargs => true # Mixes two colors together. Specifically, takes the average of each of the # RGB components, optionally weighted by the given percentage. The opacity # of the colors is also considered when weighting the components. # # The weight specifies the amount of the first color that should be included # in the returned color. The default, `50%`, means that half the first color # and half the second color should be used. `25%` means that a quarter of # the first color and three quarters of the second color should be used. # # @example # mix(#f00, #00f) => #7f007f # mix(#f00, #00f, 25%) => #3f00bf # mix(rgba(255, 0, 0, 0.5), #00f) => rgba(63, 0, 191, 0.75) # @overload mix($color1, $color2, $weight: 50%) # @param $color1 [Sass::Script::Value::Color] # @param $color2 [Sass::Script::Value::Color] # @param $weight [Sass::Script::Value::Number] The relative weight of each # color. Closer to `100%` gives more weight to `$color1`, closer to `0%` # gives more weight to `$color2` # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if `$weight` is out of bounds or any parameter is # the wrong type def mix(color1, color2, weight = number(50)) assert_type color1, :Color, :color1 assert_type color2, :Color, :color2 assert_type weight, :Number, :weight Sass::Util.check_range("Weight", 0..100, weight, '%') # This algorithm factors in both the user-provided weight (w) and the # difference between the alpha values of the two colors (a) to decide how # to perform the weighted average of the two RGB values. # # It works by first normalizing both parameters to be within [-1, 1], # where 1 indicates "only use color1", -1 indicates "only use color2", and # all values in between indicated a proportionately weighted average. # # Once we have the normalized variables w and a, we apply the formula # (w + a)/(1 + w*a) to get the combined weight (in [-1, 1]) of color1. # This formula has two especially nice properties: # # * When either w or a are -1 or 1, the combined weight is also that number # (cases where w * a == -1 are undefined, and handled as a special case). # # * When a is 0, the combined weight is w, and vice versa. # # Finally, the weight of color1 is renormalized to be within [0, 1] # and the weight of color2 is given by 1 minus the weight of color1. p = (weight.value / 100.0).to_f w = p * 2 - 1 a = color1.alpha - color2.alpha w1 = ((w * a == -1 ? w : (w + a) / (1 + w * a)) + 1) / 2.0 w2 = 1 - w1 rgba = color1.rgb.zip(color2.rgb).map {|v1, v2| v1 * w1 + v2 * w2} rgba << color1.alpha * p + color2.alpha * (1 - p) rgb_color(*rgba) end declare :mix, [:color1, :color2] declare :mix, [:color1, :color2, :weight] # Converts a color to grayscale. This is identical to `desaturate(color, # 100%)`. # # @see #desaturate # @overload grayscale($color) # @param $color [Sass::Script::Value::Color] # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if `$color` isn't a color def grayscale(color) if color.is_a?(Sass::Script::Value::Number) return identifier("grayscale(#{color})") end desaturate color, number(100) end declare :grayscale, [:color] # Returns the complement of a color. This is identical to `adjust-hue(color, # 180deg)`. # # @see #adjust_hue #adjust-hue # @overload complement($color) # @param $color [Sass::Script::Value::Color] # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if `$color` isn't a color def complement(color) adjust_hue color, number(180) end declare :complement, [:color] # Returns the inverse (negative) of a color. The red, green, and blue values # are inverted, while the opacity is left alone. # # @overload invert($color) # @param $color [Sass::Script::Value::Color] # @overload invert($color, $weight: 100%) # @param $color [Sass::Script::Value::Color] # @param $weight [Sass::Script::Value::Number] The relative weight of the # color color's inverse # @return [Sass::Script::Value::Color] # @raise [ArgumentError] if `$color` isn't a color or `$weight` # isn't a percentage between 0% and 100% def invert(color, weight = number(100)) if color.is_a?(Sass::Script::Value::Number) return identifier("invert(#{color})") end assert_type color, :Color, :color inv = color.with( :red => (255 - color.red), :green => (255 - color.green), :blue => (255 - color.blue)) mix(inv, color, weight) end declare :invert, [:color] declare :invert, [:color, :weight] # Removes quotes from a string. If the string is already unquoted, this will # return it unmodified. # # @see #quote # @example # unquote("foo") => foo # unquote(foo) => foo # @overload unquote($string) # @param $string [Sass::Script::Value::String] # @return [Sass::Script::Value::String] # @raise [ArgumentError] if `$string` isn't a string def unquote(string) unless string.is_a?(Sass::Script::Value::String) # Don't warn multiple times for the same source line. $_sass_warned_for_unquote ||= Set.new frame = environment.stack.frames.last key = [frame.filename, frame.line] if frame return string if frame && $_sass_warned_for_unquote.include?(key) $_sass_warned_for_unquote << key if frame Sass::Util.sass_warn(< "foo" # quote(foo) => "foo" # @overload quote($string) # @param $string [Sass::Script::Value::String] # @return [Sass::Script::Value::String] # @raise [ArgumentError] if `$string` isn't a string def quote(string) assert_type string, :String, :string if string.type != :string quoted_string(string.value) else string end end declare :quote, [:string] # Returns the number of characters in a string. # # @example # str-length("foo") => 3 # @overload str_length($string) # @param $string [Sass::Script::Value::String] # @return [Sass::Script::Value::Number] # @raise [ArgumentError] if `$string` isn't a string def str_length(string) assert_type string, :String, :string number(string.value.size) end declare :str_length, [:string] # Inserts `$insert` into `$string` at `$index`. # # Note that unlike some languages, the first character in a Sass string is # number 1, the second number 2, and so forth. # # @example # str-insert("abcd", "X", 1) => "Xabcd" # str-insert("abcd", "X", 4) => "abcXd" # str-insert("abcd", "X", 5) => "abcdX" # # @overload str_insert($string, $insert, $index) # @param $string [Sass::Script::Value::String] # @param $insert [Sass::Script::Value::String] # @param $index [Sass::Script::Value::Number] The position at which # `$insert` will be inserted. Negative indices count from the end of # `$string`. An index that's outside the bounds of the string will insert # `$insert` at the front or back of the string # @return [Sass::Script::Value::String] The result string. This will be # quoted if and only if `$string` was quoted # @raise [ArgumentError] if any parameter is the wrong type def str_insert(original, insert, index) assert_type original, :String, :string assert_type insert, :String, :insert assert_integer index, :index assert_unit index, nil, :index insertion_point = if index.to_i > 0 [index.to_i - 1, original.value.size].min else [index.to_i, -original.value.size - 1].max end result = original.value.dup.insert(insertion_point, insert.value) Sass::Script::Value::String.new(result, original.type) end declare :str_insert, [:string, :insert, :index] # Returns the index of the first occurrence of `$substring` in `$string`. If # there is no such occurrence, returns `null`. # # Note that unlike some languages, the first character in a Sass string is # number 1, the second number 2, and so forth. # # @example # str-index(abcd, a) => 1 # str-index(abcd, ab) => 1 # str-index(abcd, X) => null # str-index(abcd, c) => 3 # # @overload str_index($string, $substring) # @param $string [Sass::Script::Value::String] # @param $substring [Sass::Script::Value::String] # @return [Sass::Script::Value::Number, Sass::Script::Value::Null] # @raise [ArgumentError] if any parameter is the wrong type def str_index(string, substring) assert_type string, :String, :string assert_type substring, :String, :substring index = string.value.index(substring.value) index ? number(index + 1) : null end declare :str_index, [:string, :substring] # Extracts a substring from `$string`. The substring will begin at index # `$start-at` and ends at index `$end-at`. # # Note that unlike some languages, the first character in a Sass string is # number 1, the second number 2, and so forth. # # @example # str-slice("abcd", 2, 3) => "bc" # str-slice("abcd", 2) => "bcd" # str-slice("abcd", -3, -2) => "bc" # str-slice("abcd", 2, -2) => "bc" # # @overload str_slice($string, $start-at, $end-at: -1) # @param $start-at [Sass::Script::Value::Number] The index of the first # character of the substring. If this is negative, it counts from the end # of `$string` # @param $end-at [Sass::Script::Value::Number] The index of the last # character of the substring. If this is negative, it counts from the end # of `$string`. Defaults to -1 # @return [Sass::Script::Value::String] The substring. This will be quoted # if and only if `$string` was quoted # @raise [ArgumentError] if any parameter is the wrong type def str_slice(string, start_at, end_at = nil) assert_type string, :String, :string assert_unit start_at, nil, "start-at" end_at = number(-1) if end_at.nil? assert_unit end_at, nil, "end-at" return Sass::Script::Value::String.new("", string.type) if end_at.value == 0 s = start_at.value > 0 ? start_at.value - 1 : start_at.value e = end_at.value > 0 ? end_at.value - 1 : end_at.value s = string.value.length + s if s < 0 s = 0 if s < 0 e = string.value.length + e if e < 0 return Sass::Script::Value::String.new("", string.type) if e < 0 extracted = string.value.slice(s..e) Sass::Script::Value::String.new(extracted || "", string.type) end declare :str_slice, [:string, :start_at] declare :str_slice, [:string, :start_at, :end_at] # Converts a string to upper case. # # @example # to-upper-case(abcd) => ABCD # # @overload to_upper_case($string) # @param $string [Sass::Script::Value::String] # @return [Sass::Script::Value::String] # @raise [ArgumentError] if `$string` isn't a string def to_upper_case(string) assert_type string, :String, :string Sass::Script::Value::String.new(Sass::Util.upcase(string.value), string.type) end declare :to_upper_case, [:string] # Convert a string to lower case, # # @example # to-lower-case(ABCD) => abcd # # @overload to_lower_case($string) # @param $string [Sass::Script::Value::String] # @return [Sass::Script::Value::String] # @raise [ArgumentError] if `$string` isn't a string def to_lower_case(string) assert_type string, :String, :string Sass::Script::Value::String.new(Sass::Util.downcase(string.value), string.type) end declare :to_lower_case, [:string] # Returns the type of a value. # # @example # type-of(100px) => number # type-of(asdf) => string # type-of("asdf") => string # type-of(true) => bool # type-of(#fff) => color # type-of(blue) => color # type-of(null) => null # type-of(a b c) => list # type-of((a: 1, b: 2)) => map # type-of(get-function("foo")) => function # # @overload type_of($value) # @param $value [Sass::Script::Value::Base] The value to inspect # @return [Sass::Script::Value::String] The unquoted string name of the # value's type def type_of(value) value.check_deprecated_interp if value.is_a?(Sass::Script::Value::String) identifier(value.class.name.gsub(/Sass::Script::Value::/, '').downcase) end declare :type_of, [:value] # Returns whether a feature exists in the current Sass runtime. # # The following features are supported: # # * `global-variable-shadowing` indicates that a local variable will shadow # a global variable unless `!global` is used. # # * `extend-selector-pseudoclass` indicates that `@extend` will reach into # selector pseudoclasses like `:not`. # # * `units-level-3` indicates full support for unit arithmetic using units # defined in the [Values and Units Level 3][] spec. # # [Values and Units Level 3]: http://www.w3.org/TR/css3-values/ # # * `at-error` indicates that the Sass `@error` directive is supported. # # * `custom-property` indicates that the [Custom Properties Level 1][] spec # is supported. This means that custom properties are parsed statically, # with only interpolation treated as SassScript. # # [Custom Properties Level 1]: https://www.w3.org/TR/css-variables-1/ # # @example # feature-exists(some-feature-that-exists) => true # feature-exists(what-is-this-i-dont-know) => false # # @overload feature_exists($feature) # @param $feature [Sass::Script::Value::String] The name of the feature # @return [Sass::Script::Value::Bool] Whether the feature is supported in this version of Sass # @raise [ArgumentError] if `$feature` isn't a string def feature_exists(feature) assert_type feature, :String, :feature bool(Sass.has_feature?(feature.value)) end declare :feature_exists, [:feature] # Returns a reference to a function for later invocation with the `call()` function. # # If `$css` is `false`, the function reference may refer to a function # defined in your stylesheet or built-in to the host environment. If it's # `true` it will refer to a plain-CSS function. # # @example # get-function("rgb") # # @function myfunc { @return "something"; } # get-function("myfunc") # # @overload get_function($name, $css: false) # @param name [Sass::Script::Value::String] The name of the function being referenced. # @param css [Sass::Script::Value::Bool] Whether to get a plain CSS function. # # @return [Sass::Script::Value::Function] A function reference. def get_function(name, kwargs = {}) assert_type name, :String, :name css = if kwargs.has_key?("css") v = kwargs.delete("css") assert_type v, :Bool, :css v.value else false end if kwargs.any? raise ArgumentError.new("Illegal keyword argument '#{kwargs.keys.first}'") end if css return Sass::Script::Value::Function.new( Sass::Callable.new(name.value, nil, nil, nil, nil, nil, "function", :css)) end callable = environment.caller.function(name.value) || (Sass::Script::Functions.callable?(name.value.tr("-", "_")) && Sass::Callable.new(name.value, nil, nil, nil, nil, nil, "function", :builtin)) if callable Sass::Script::Value::Function.new(callable) else raise Sass::SyntaxError.new("Function not found: #{name}") end end declare :get_function, [:name], :var_kwargs => true # Returns the unit(s) associated with a number. Complex units are sorted in # alphabetical order by numerator and denominator. # # @example # unit(100) => "" # unit(100px) => "px" # unit(3em) => "em" # unit(10px * 5em) => "em*px" # unit(10px * 5em / 30cm / 1rem) => "em*px/cm*rem" # @overload unit($number) # @param $number [Sass::Script::Value::Number] # @return [Sass::Script::Value::String] The unit(s) of the number, as a # quoted string # @raise [ArgumentError] if `$number` isn't a number def unit(number) assert_type number, :Number, :number quoted_string(number.unit_str) end declare :unit, [:number] # Returns whether a number has units. # # @example # unitless(100) => true # unitless(100px) => false # @overload unitless($number) # @param $number [Sass::Script::Value::Number] # @return [Sass::Script::Value::Bool] # @raise [ArgumentError] if `$number` isn't a number def unitless(number) assert_type number, :Number, :number bool(number.unitless?) end declare :unitless, [:number] # Returns whether two numbers can added, subtracted, or compared. # # @example # comparable(2px, 1px) => true # comparable(100px, 3em) => false # comparable(10cm, 3mm) => true # @overload comparable($number1, $number2) # @param $number1 [Sass::Script::Value::Number] # @param $number2 [Sass::Script::Value::Number] # @return [Sass::Script::Value::Bool] # @raise [ArgumentError] if either parameter is the wrong type def comparable(number1, number2) assert_type number1, :Number, :number1 assert_type number2, :Number, :number2 bool(number1.comparable_to?(number2)) end declare :comparable, [:number1, :number2] # Converts a unitless number to a percentage. # # @example # percentage(0.2) => 20% # percentage(100px / 50px) => 200% # @overload percentage($number) # @param $number [Sass::Script::Value::Number] # @return [Sass::Script::Value::Number] # @raise [ArgumentError] if `$number` isn't a unitless number def percentage(number) unless number.is_a?(Sass::Script::Value::Number) && number.unitless? raise ArgumentError.new("$number: #{number.inspect} is not a unitless number") end number(number.value * 100, '%') end declare :percentage, [:number] # Rounds a number to the nearest whole number. # # @example # round(10.4px) => 10px # round(10.6px) => 11px # @overload round($number) # @param $number [Sass::Script::Value::Number] # @return [Sass::Script::Value::Number] # @raise [ArgumentError] if `$number` isn't a number def round(number) numeric_transformation(number) {|n| Sass::Util.round(n)} end declare :round, [:number] # Rounds a number up to the next whole number. # # @example # ceil(10.4px) => 11px # ceil(10.6px) => 11px # @overload ceil($number) # @param $number [Sass::Script::Value::Number] # @return [Sass::Script::Value::Number] # @raise [ArgumentError] if `$number` isn't a number def ceil(number) numeric_transformation(number) {|n| n.ceil} end declare :ceil, [:number] # Rounds a number down to the previous whole number. # # @example # floor(10.4px) => 10px # floor(10.6px) => 10px # @overload floor($number) # @param $number [Sass::Script::Value::Number] # @return [Sass::Script::Value::Number] # @raise [ArgumentError] if `$number` isn't a number def floor(number) numeric_transformation(number) {|n| n.floor} end declare :floor, [:number] # Returns the absolute value of a number. # # @example # abs(10px) => 10px # abs(-10px) => 10px # @overload abs($number) # @param $number [Sass::Script::Value::Number] # @return [Sass::Script::Value::Number] # @raise [ArgumentError] if `$number` isn't a number def abs(number) numeric_transformation(number) {|n| n.abs} end declare :abs, [:number] # Finds the minimum of several numbers. This function takes any number of # arguments. # # @example # min(1px, 4px) => 1px # min(5em, 3em, 4em) => 3em # @overload min($numbers...) # @param $numbers [[Sass::Script::Value::Number]] # @return [Sass::Script::Value::Number] # @raise [ArgumentError] if any argument isn't a number, or if not all of # the arguments have comparable units def min(*numbers) numbers.each {|n| assert_type n, :Number} numbers.inject {|min, num| min.lt(num).to_bool ? min : num} end declare :min, [], :var_args => :true # Finds the maximum of several numbers. This function takes any number of # arguments. # # @example # max(1px, 4px) => 4px # max(5em, 3em, 4em) => 5em # @overload max($numbers...) # @param $numbers [[Sass::Script::Value::Number]] # @return [Sass::Script::Value::Number] # @raise [ArgumentError] if any argument isn't a number, or if not all of # the arguments have comparable units def max(*values) values.each {|v| assert_type v, :Number} values.inject {|max, val| max.gt(val).to_bool ? max : val} end declare :max, [], :var_args => :true # Return the length of a list. # # This can return the number of pairs in a map as well. # # @example # length(10px) => 1 # length(10px 20px 30px) => 3 # length((width: 10px, height: 20px)) => 2 # @overload length($list) # @param $list [Sass::Script::Value::Base] # @return [Sass::Script::Value::Number] def length(list) number(list.to_a.size) end declare :length, [:list] # Return a new list, based on the list provided, but with the nth # element changed to the value given. # # Note that unlike some languages, the first item in a Sass list is number # 1, the second number 2, and so forth. # # Negative index values address elements in reverse order, starting with the last element # in the list. # # @example # set-nth($list: 10px 20px 30px, $n: 2, $value: -20px) => 10px -20px 30px # @overload set-nth($list, $n, $value) # @param $list [Sass::Script::Value::Base] The list that will be copied, having the element # at index `$n` changed. # @param $n [Sass::Script::Value::Number] The index of the item to set. # Negative indices count from the end of the list. # @param $value [Sass::Script::Value::Base] The new value at index `$n`. # @return [Sass::Script::Value::List] # @raise [ArgumentError] if `$n` isn't an integer between 1 and the length # of `$list` def set_nth(list, n, value) assert_type n, :Number, :n Sass::Script::Value::List.assert_valid_index(list, n) index = n.to_i > 0 ? n.to_i - 1 : n.to_i new_list = list.to_a.dup new_list[index] = value list.with_contents(new_list) end declare :set_nth, [:list, :n, :value] # Gets the nth item in a list. # # Note that unlike some languages, the first item in a Sass list is number # 1, the second number 2, and so forth. # # This can return the nth pair in a map as well. # # Negative index values address elements in reverse order, starting with the last element in # the list. # # @example # nth(10px 20px 30px, 1) => 10px # nth((Helvetica, Arial, sans-serif), 3) => sans-serif # nth((width: 10px, length: 20px), 2) => length, 20px # @overload nth($list, $n) # @param $list [Sass::Script::Value::Base] # @param $n [Sass::Script::Value::Number] The index of the item to get. # Negative indices count from the end of the list. # @return [Sass::Script::Value::Base] # @raise [ArgumentError] if `$n` isn't an integer between 1 and the length # of `$list` def nth(list, n) assert_type n, :Number, :n Sass::Script::Value::List.assert_valid_index(list, n) index = n.to_i > 0 ? n.to_i - 1 : n.to_i list.to_a[index] end declare :nth, [:list, :n] # Joins together two lists into one. # # Unless `$separator` is passed, if one list is comma-separated and one is # space-separated, the first parameter's separator is used for the resulting # list. If both lists have fewer than two items, spaces are used for the # resulting list. # # Unless `$bracketed` is passed, the resulting list is bracketed if the # first parameter is. # # Like all list functions, `join()` returns a new list rather than modifying # its arguments in place. # # @example # join(10px 20px, 30px 40px) => 10px 20px 30px 40px # join((blue, red), (#abc, #def)) => blue, red, #abc, #def # join(10px, 20px) => 10px 20px # join(10px, 20px, comma) => 10px, 20px # join((blue, red), (#abc, #def), space) => blue red #abc #def # join([10px], 20px) => [10px 20px] # @overload join($list1, $list2, $separator: auto, $bracketed: auto) # @param $list1 [Sass::Script::Value::Base] # @param $list2 [Sass::Script::Value::Base] # @param $separator [Sass::Script::Value::String] The list separator to use. # If this is `comma` or `space`, that separator will be used. If this is # `auto` (the default), the separator is determined as explained above. # @param $bracketed [Sass::Script::Value::Base] Whether the resulting list # will be bracketed. If this is `auto` (the default), the separator is # determined as explained above. # @return [Sass::Script::Value::List] def join(list1, list2, separator = identifier("auto"), bracketed = identifier("auto"), kwargs = nil, *rest) if separator.is_a?(Hash) kwargs = separator separator = identifier("auto") elsif bracketed.is_a?(Hash) kwargs = bracketed bracketed = identifier("auto") elsif rest.last.is_a?(Hash) rest.unshift kwargs kwargs = rest.pop end unless rest.empty? # Add 4 to rest.length because we don't want to count the kwargs hash, # which is always passed. raise ArgumentError.new("wrong number of arguments (#{rest.length + 4} for 2..4)") end if kwargs separator = kwargs.delete("separator") || separator bracketed = kwargs.delete("bracketed") || bracketed unless kwargs.empty? name, val = kwargs.to_a.first raise ArgumentError.new("Unknown argument $#{name} (#{val})") end end assert_type separator, :String, :separator unless %w(auto space comma).include?(separator.value) raise ArgumentError.new("Separator name must be space, comma, or auto") end list(list1.to_a + list2.to_a, separator: if separator.value == 'auto' list1.separator || list2.separator || :space else separator.value.to_sym end, bracketed: if bracketed.is_a?(Sass::Script::Value::String) && bracketed.value == 'auto' list1.bracketed else bracketed.to_bool end) end # We don't actually take variable arguments or keyword arguments, but this # is the best way to take either `$separator` or `$bracketed` as keywords # without complaining about the other missing. declare :join, [:list1, :list2], :var_args => true, :var_kwargs => true # Appends a single value onto the end of a list. # # Unless the `$separator` argument is passed, if the list had only one item, # the resulting list will be space-separated. # # Like all list functions, `append()` returns a new list rather than # modifying its argument in place. # # @example # append(10px 20px, 30px) => 10px 20px 30px # append((blue, red), green) => blue, red, green # append(10px 20px, 30px 40px) => 10px 20px (30px 40px) # append(10px, 20px, comma) => 10px, 20px # append((blue, red), green, space) => blue red green # @overload append($list, $val, $separator: auto) # @param $list [Sass::Script::Value::Base] # @param $val [Sass::Script::Value::Base] # @param $separator [Sass::Script::Value::String] The list separator to use. # If this is `comma` or `space`, that separator will be used. If this is # `auto` (the default), the separator is determined as explained above. # @return [Sass::Script::Value::List] def append(list, val, separator = identifier("auto")) assert_type separator, :String, :separator unless %w(auto space comma).include?(separator.value) raise ArgumentError.new("Separator name must be space, comma, or auto") end list.with_contents(list.to_a + [val], separator: if separator.value == 'auto' list.separator || :space else separator.value.to_sym end) end declare :append, [:list, :val] declare :append, [:list, :val, :separator] # Combines several lists into a single multidimensional list. The nth value # of the resulting list is a space separated list of the source lists' nth # values. # # The length of the resulting list is the length of the # shortest list. # # @example # zip(1px 1px 3px, solid dashed solid, red green blue) # => 1px solid red, 1px dashed green, 3px solid blue # @overload zip($lists...) # @param $lists [[Sass::Script::Value::Base]] # @return [Sass::Script::Value::List] def zip(*lists) length = nil values = [] lists.each do |list| array = list.to_a values << array.dup length = length.nil? ? array.length : [length, array.length].min end values.each do |value| value.slice!(length) end new_list_value = values.first.zip(*values[1..-1]) list(new_list_value.map {|list| list(list, :space)}, :comma) end declare :zip, [], :var_args => true # Returns the position of a value within a list. If the value isn't found, # returns `null` instead. # # Note that unlike some languages, the first item in a Sass list is number # 1, the second number 2, and so forth. # # This can return the position of a pair in a map as well. # # @example # index(1px solid red, solid) => 2 # index(1px solid red, dashed) => null # index((width: 10px, height: 20px), (height 20px)) => 2 # @overload index($list, $value) # @param $list [Sass::Script::Value::Base] # @param $value [Sass::Script::Value::Base] # @return [Sass::Script::Value::Number, Sass::Script::Value::Null] The # 1-based index of `$value` in `$list`, or `null` def index(list, value) index = list.to_a.index {|e| e.eq(value).to_bool} index ? number(index + 1) : null end declare :index, [:list, :value] # Returns the separator of a list. If the list doesn't have a separator due # to having fewer than two elements, returns `space`. # # @example # list-separator(1px 2px 3px) => space # list-separator(1px, 2px, 3px) => comma # list-separator('foo') => space # @overload list_separator($list) # @param $list [Sass::Script::Value::Base] # @return [Sass::Script::Value::String] `comma` or `space` def list_separator(list) identifier((list.separator || :space).to_s) end declare :list_separator, [:list] # Returns whether a list uses square brackets. # # @example # is-bracketed(1px 2px 3px) => false # is-bracketed([1px, 2px, 3px]) => true # @overload is_bracketed($list) # @param $list [Sass::Script::Value::Base] # @return [Sass::Script::Value::Bool] def is_bracketed(list) bool(list.bracketed) end declare :is_bracketed, [:list] # Returns the value in a map associated with the given key. If the map # doesn't have such a key, returns `null`. # # @example # map-get(("foo": 1, "bar": 2), "foo") => 1 # map-get(("foo": 1, "bar": 2), "bar") => 2 # map-get(("foo": 1, "bar": 2), "baz") => null # @overload map_get($map, $key) # @param $map [Sass::Script::Value::Map] # @param $key [Sass::Script::Value::Base] # @return [Sass::Script::Value::Base] The value indexed by `$key`, or `null` # if the map doesn't contain the given key # @raise [ArgumentError] if `$map` is not a map def map_get(map, key) assert_type map, :Map, :map map.to_h[key] || null end declare :map_get, [:map, :key] # Merges two maps together into a new map. Keys in `$map2` will take # precedence over keys in `$map1`. # # This is the best way to add new values to a map. # # All keys in the returned map that also appear in `$map1` will have the # same order as in `$map1`. New keys from `$map2` will be placed at the end # of the map. # # Like all map functions, `map-merge()` returns a new map rather than # modifying its arguments in place. # # @example # map-merge(("foo": 1), ("bar": 2)) => ("foo": 1, "bar": 2) # map-merge(("foo": 1, "bar": 2), ("bar": 3)) => ("foo": 1, "bar": 3) # @overload map_merge($map1, $map2) # @param $map1 [Sass::Script::Value::Map] # @param $map2 [Sass::Script::Value::Map] # @return [Sass::Script::Value::Map] # @raise [ArgumentError] if either parameter is not a map def map_merge(map1, map2) assert_type map1, :Map, :map1 assert_type map2, :Map, :map2 map(map1.to_h.merge(map2.to_h)) end declare :map_merge, [:map1, :map2] # Returns a new map with keys removed. # # Like all map functions, `map-merge()` returns a new map rather than # modifying its arguments in place. # # @example # map-remove(("foo": 1, "bar": 2), "bar") => ("foo": 1) # map-remove(("foo": 1, "bar": 2, "baz": 3), "bar", "baz") => ("foo": 1) # map-remove(("foo": 1, "bar": 2), "baz") => ("foo": 1, "bar": 2) # @overload map_remove($map, $keys...) # @param $map [Sass::Script::Value::Map] # @param $keys [[Sass::Script::Value::Base]] # @return [Sass::Script::Value::Map] # @raise [ArgumentError] if `$map` is not a map def map_remove(map, *keys) assert_type map, :Map, :map hash = map.to_h.dup hash.delete_if {|key, _| keys.include?(key)} map(hash) end declare :map_remove, [:map, :key], :var_args => true # Returns a list of all keys in a map. # # @example # map-keys(("foo": 1, "bar": 2)) => "foo", "bar" # @overload map_keys($map) # @param $map [Map] # @return [List] the list of keys, comma-separated # @raise [ArgumentError] if `$map` is not a map def map_keys(map) assert_type map, :Map, :map list(map.to_h.keys, :comma) end declare :map_keys, [:map] # Returns a list of all values in a map. This list may include duplicate # values, if multiple keys have the same value. # # @example # map-values(("foo": 1, "bar": 2)) => 1, 2 # map-values(("foo": 1, "bar": 2, "baz": 1)) => 1, 2, 1 # @overload map_values($map) # @param $map [Map] # @return [List] the list of values, comma-separated # @raise [ArgumentError] if `$map` is not a map def map_values(map) assert_type map, :Map, :map list(map.to_h.values, :comma) end declare :map_values, [:map] # Returns whether a map has a value associated with a given key. # # @example # map-has-key(("foo": 1, "bar": 2), "foo") => true # map-has-key(("foo": 1, "bar": 2), "baz") => false # @overload map_has_key($map, $key) # @param $map [Sass::Script::Value::Map] # @param $key [Sass::Script::Value::Base] # @return [Sass::Script::Value::Bool] # @raise [ArgumentError] if `$map` is not a map def map_has_key(map, key) assert_type map, :Map, :map bool(map.to_h.has_key?(key)) end declare :map_has_key, [:map, :key] # Returns the map of named arguments passed to a function or mixin that # takes a variable argument list. The argument names are strings, and they # do not contain the leading `$`. # # @example # @mixin foo($args...) { # @debug keywords($args); //=> (arg1: val, arg2: val) # } # # @include foo($arg1: val, $arg2: val); # @overload keywords($args) # @param $args [Sass::Script::Value::ArgList] # @return [Sass::Script::Value::Map] # @raise [ArgumentError] if `$args` isn't a variable argument list def keywords(args) assert_type args, :ArgList, :args map(Sass::Util.map_keys(args.keywords.as_stored) {|k| Sass::Script::Value::String.new(k)}) end declare :keywords, [:args] # Returns one of two values, depending on whether or not `$condition` is # true. Just like in `@if`, all values other than `false` and `null` are # considered to be true. # # @example # if(true, 1px, 2px) => 1px # if(false, 1px, 2px) => 2px # @overload if($condition, $if-true, $if-false) # @param $condition [Sass::Script::Value::Base] Whether the `$if-true` or # `$if-false` will be returned # @param $if-true [Sass::Script::Tree::Node] # @param $if-false [Sass::Script::Tree::Node] # @return [Sass::Script::Value::Base] `$if-true` or `$if-false` def if(condition, if_true, if_false) if condition.to_bool perform(if_true) else perform(if_false) end end declare :if, [:condition, :"&if_true", :"&if_false"] # Returns a unique CSS identifier. The identifier is returned as an unquoted # string. The identifier returned is only guaranteed to be unique within the # scope of a single Sass run. # # @overload unique_id() # @return [Sass::Script::Value::String] def unique_id generator = Sass::Script::Functions.random_number_generator Thread.current[:sass_last_unique_id] ||= generator.rand(36**8) # avoid the temptation of trying to guess the next unique value. value = (Thread.current[:sass_last_unique_id] += (generator.rand(10) + 1)) # the u makes this a legal identifier if it would otherwise start with a number. identifier("u" + value.to_s(36).rjust(8, '0')) end declare :unique_id, [] # Dynamically calls a function. This can call user-defined # functions, built-in functions, or plain CSS functions. It will # pass along all arguments, including keyword arguments, to the # called function. # # @example # call(rgb, 10, 100, 255) => #0a64ff # call(scale-color, #0a64ff, $lightness: -10%) => #0058ef # # $fn: nth; # call($fn, (a b c), 2) => b # # @overload call($function, $args...) # @param $function [Sass::Script::Value::Function] The function to call. def call(name, *args) unless name.is_a?(Sass::Script::Value::String) || name.is_a?(Sass::Script::Value::Function) assert_type name, :Function, :function end if name.is_a?(Sass::Script::Value::String) name = if function_exists(name).to_bool get_function(name) else get_function(name, "css" => bool(true)) end Sass::Util.sass_warn(< true, :var_kwargs => true # This function only exists as a workaround for IE7's [`content: # counter` bug](http://jes.st/2013/ie7s-css-breaking-content-counter-bug/). # It works identically to any other plain-CSS function, except it # avoids adding spaces between the argument commas. # # @example # counter(item, ".") => counter(item,".") # @overload counter($args...) # @return [Sass::Script::Value::String] def counter(*args) identifier("counter(#{args.map {|a| a.to_s(options)}.join(',')})") end declare :counter, [], :var_args => true # This function only exists as a workaround for IE7's [`content: # counter` bug](http://jes.st/2013/ie7s-css-breaking-content-counter-bug/). # It works identically to any other plain-CSS function, except it # avoids adding spaces between the argument commas. # # @example # counters(item, ".") => counters(item,".") # @overload counters($args...) # @return [Sass::Script::Value::String] def counters(*args) identifier("counters(#{args.map {|a| a.to_s(options)}.join(',')})") end declare :counters, [], :var_args => true # Check whether a variable with the given name exists in the current # scope or in the global scope. # # @example # $a-false-value: false; # variable-exists(a-false-value) => true # variable-exists(a-null-value) => true # # variable-exists(nonexistent) => false # # @overload variable_exists($name) # @param $name [Sass::Script::Value::String] The name of the variable to # check. The name should not include the `$`. # @return [Sass::Script::Value::Bool] Whether the variable is defined in # the current scope. def variable_exists(name) assert_type name, :String, :name bool(environment.caller.var(name.value)) end declare :variable_exists, [:name] # Check whether a variable with the given name exists in the global # scope (at the top level of the file). # # @example # $a-false-value: false; # global-variable-exists(a-false-value) => true # global-variable-exists(a-null-value) => true # # .foo { # $some-var: false; # @if global-variable-exists(some-var) { /* false, doesn't run */ } # } # # @overload global_variable_exists($name) # @param $name [Sass::Script::Value::String] The name of the variable to # check. The name should not include the `$`. # @return [Sass::Script::Value::Bool] Whether the variable is defined in # the global scope. def global_variable_exists(name) assert_type name, :String, :name bool(environment.global_env.var(name.value)) end declare :global_variable_exists, [:name] # Check whether a function with the given name exists. # # @example # function-exists(lighten) => true # # @function myfunc { @return "something"; } # function-exists(myfunc) => true # # @overload function_exists($name) # @param name [Sass::Script::Value::String] The name of the function to # check or a function reference. # @return [Sass::Script::Value::Bool] Whether the function is defined. def function_exists(name) assert_type name, :String, :name exists = Sass::Script::Functions.callable?(name.value.tr("-", "_")) exists ||= environment.caller.function(name.value) bool(exists) end declare :function_exists, [:name] # Check whether a mixin with the given name exists. # # @example # mixin-exists(nonexistent) => false # # @mixin red-text { color: red; } # mixin-exists(red-text) => true # # @overload mixin_exists($name) # @param name [Sass::Script::Value::String] The name of the mixin to # check. # @return [Sass::Script::Value::Bool] Whether the mixin is defined. def mixin_exists(name) assert_type name, :String, :name bool(environment.mixin(name.value)) end declare :mixin_exists, [:name] # Check whether a mixin was passed a content block. # # Unless `content-exists()` is called directly from a mixin, an error will be raised. # # @example # @mixin needs-content { # @if not content-exists() { # @error "You must pass a content block!" # } # @content; # } # # @overload content_exists() # @return [Sass::Script::Value::Bool] Whether a content block was passed to the mixin. def content_exists # frames.last is the stack frame for this function, # so we use frames[-2] to get the frame before that. mixin_frame = environment.stack.frames[-2] unless mixin_frame && mixin_frame.type == :mixin raise Sass::SyntaxError.new("Cannot call content-exists() except within a mixin.") end bool(!environment.caller.content.nil?) end declare :content_exists, [] # Return a string containing the value as its Sass representation. # # @overload inspect($value) # @param $value [Sass::Script::Value::Base] The value to inspect. # @return [Sass::Script::Value::String] A representation of the value as # it would be written in Sass. def inspect(value) value.check_deprecated_interp if value.is_a?(Sass::Script::Value::String) unquoted_string(value.to_sass) end declare :inspect, [:value] # @overload random() # Return a decimal between 0 and 1, inclusive of 0 but not 1. # @return [Sass::Script::Value::Number] A decimal value. # @overload random($limit) # Return an integer between 1 and `$limit`, inclusive of both 1 and `$limit`. # @param $limit [Sass::Script::Value::Number] The maximum of the random integer to be # returned, a positive integer. # @return [Sass::Script::Value::Number] An integer. # @raise [ArgumentError] if the `$limit` is not 1 or greater def random(limit = nil) generator = Sass::Script::Functions.random_number_generator if limit assert_integer limit, "limit" if limit.to_i < 1 raise ArgumentError.new("$limit #{limit} must be greater than or equal to 1") end number(1 + generator.rand(limit.to_i)) else number(generator.rand) end end declare :random, [] declare :random, [:limit] # Parses a user-provided selector into a list of lists of strings # as returned by `&`. # # @example # selector-parse(".foo .bar, .baz .bang") => ('.foo' '.bar', '.baz' '.bang') # # @overload selector_parse($selector) # @param $selector [Sass::Script::Value::String, Sass::Script::Value::List] # The selector to parse. This can be either a string, a list of # strings, or a list of lists of strings as returned by `&`. # @return [Sass::Script::Value::List] # A list of lists of strings representing `$selector`. This is # in the same format as a selector returned by `&`. def selector_parse(selector) parse_selector(selector, :selector).to_sass_script end declare :selector_parse, [:selector] # Return a new selector with all selectors in `$selectors` nested beneath # one another as though they had been nested in the stylesheet as # `$selector1 { $selector2 { ... } }`. # # Unlike most selector functions, `selector-nest` allows the # parent selector `&` to be used in any selector but the first. # # @example # selector-nest(".foo", ".bar", ".baz") => .foo .bar .baz # selector-nest(".a .foo", ".b .bar") => .a .foo .b .bar # selector-nest(".foo", "&.bar") => .foo.bar # # @overload selector_nest($selectors...) # @param $selectors [[Sass::Script::Value::String, Sass::Script::Value::List]] # The selectors to nest. At least one selector must be passed. Each of # these can be either a string, a list of strings, or a list of lists of # strings as returned by `&`. # @return [Sass::Script::Value::List] # A list of lists of strings representing the result of nesting # `$selectors`. This is in the same format as a selector returned by # `&`. def selector_nest(*selectors) if selectors.empty? raise ArgumentError.new("$selectors: At least one selector must be passed") end parsed = [parse_selector(selectors.first, :selectors)] parsed += selectors[1..-1].map {|sel| parse_selector(sel, :selectors, true)} parsed.inject {|result, child| child.resolve_parent_refs(result)}.to_sass_script end declare :selector_nest, [], :var_args => true # Return a new selector with all selectors in `$selectors` appended one # another as though they had been nested in the stylesheet as `$selector1 { # &$selector2 { ... } }`. # # @example # selector-append(".foo", ".bar", ".baz") => .foo.bar.baz # selector-append(".a .foo", ".b .bar") => "a .foo.b .bar" # selector-append(".foo", "-suffix") => ".foo-suffix" # # @overload selector_append($selectors...) # @param $selectors [[Sass::Script::Value::String, Sass::Script::Value::List]] # The selectors to append. At least one selector must be passed. Each of # these can be either a string, a list of strings, or a list of lists of # strings as returned by `&`. # @return [Sass::Script::Value::List] # A list of lists of strings representing the result of appending # `$selectors`. This is in the same format as a selector returned by # `&`. # @raise [ArgumentError] if a selector could not be appended. def selector_append(*selectors) if selectors.empty? raise ArgumentError.new("$selectors: At least one selector must be passed") end selectors.map {|sel| parse_selector(sel, :selectors)}.inject do |parent, child| child.members.each do |seq| sseq = seq.members.first unless sseq.is_a?(Sass::Selector::SimpleSequence) raise ArgumentError.new("Can't append \"#{seq}\" to \"#{parent}\"") end base = sseq.base case base when Sass::Selector::Universal raise ArgumentError.new("Can't append \"#{seq}\" to \"#{parent}\"") when Sass::Selector::Element unless base.namespace.nil? raise ArgumentError.new("Can't append \"#{seq}\" to \"#{parent}\"") end sseq.members[0] = Sass::Selector::Parent.new(base.name) else sseq.members.unshift Sass::Selector::Parent.new end end child.resolve_parent_refs(parent) end.to_sass_script end declare :selector_append, [], :var_args => true # Returns a new version of `$selector` with `$extendee` extended # with `$extender`. This works just like the result of # # $selector { ... } # $extender { @extend $extendee } # # @example # selector-extend(".a .b", ".b", ".foo .bar") => .a .b, .a .foo .bar, .foo .a .bar # # @overload selector_extend($selector, $extendee, $extender) # @param $selector [Sass::Script::Value::String, Sass::Script::Value::List] # The selector within which `$extendee` is extended with # `$extender`. This can be either a string, a list of strings, # or a list of lists of strings as returned by `&`. # @param $extendee [Sass::Script::Value::String, Sass::Script::Value::List] # The selector being extended. This can be either a string, a # list of strings, or a list of lists of strings as returned # by `&`. # @param $extender [Sass::Script::Value::String, Sass::Script::Value::List] # The selector being injected into `$selector`. This can be # either a string, a list of strings, or a list of lists of # strings as returned by `&`. # @return [Sass::Script::Value::List] # A list of lists of strings representing the result of the # extension. This is in the same format as a selector returned # by `&`. # @raise [ArgumentError] if the extension fails def selector_extend(selector, extendee, extender) selector = parse_selector(selector, :selector) extendee = parse_selector(extendee, :extendee) extender = parse_selector(extender, :extender) extends = Sass::Util::SubsetMap.new begin extender.populate_extends(extends, extendee, nil, [], true) selector.do_extend(extends).to_sass_script rescue Sass::SyntaxError => e raise ArgumentError.new(e.to_s) end end declare :selector_extend, [:selector, :extendee, :extender] # Replaces all instances of `$original` with `$replacement` in `$selector` # # This works by using `@extend` and throwing away the original # selector. This means that it can be used to do very advanced # replacements; see the examples below. # # @example # selector-replace(".foo .bar", ".bar", ".baz") => ".foo .baz" # selector-replace(".foo.bar.baz", ".foo.baz", ".qux") => ".bar.qux" # # @overload selector_replace($selector, $original, $replacement) # @param $selector [Sass::Script::Value::String, Sass::Script::Value::List] # The selector within which `$original` is replaced with # `$replacement`. This can be either a string, a list of # strings, or a list of lists of strings as returned by `&`. # @param $original [Sass::Script::Value::String, Sass::Script::Value::List] # The selector being replaced. This can be either a string, a # list of strings, or a list of lists of strings as returned # by `&`. # @param $replacement [Sass::Script::Value::String, Sass::Script::Value::List] # The selector that `$original` is being replaced with. This # can be either a string, a list of strings, or a list of # lists of strings as returned by `&`. # @return [Sass::Script::Value::List] # A list of lists of strings representing the result of the # extension. This is in the same format as a selector returned # by `&`. # @raise [ArgumentError] if the replacement fails def selector_replace(selector, original, replacement) selector = parse_selector(selector, :selector) original = parse_selector(original, :original) replacement = parse_selector(replacement, :replacement) extends = Sass::Util::SubsetMap.new begin replacement.populate_extends(extends, original, nil, [], true) selector.do_extend(extends, [], true).to_sass_script rescue Sass::SyntaxError => e raise ArgumentError.new(e.to_s) end end declare :selector_replace, [:selector, :original, :replacement] # Unifies two selectors into a single selector that matches only # elements matched by both input selectors. Returns `null` if # there is no such selector. # # Like the selector unification done for `@extend`, this doesn't # guarantee that the output selector will match *all* elements # matched by both input selectors. For example, if `.a .b` is # unified with `.x .y`, `.a .x .b.y, .x .a .b.y` will be returned, # but `.a.x .b.y` will not. This avoids exponential output size # while matching all elements that are likely to exist in # practice. # # @example # selector-unify(".a", ".b") => .a.b # selector-unify(".a .b", ".x .y") => .a .x .b.y, .x .a .b.y # selector-unify(".a.b", ".b.c") => .a.b.c # selector-unify("#a", "#b") => null # # @overload selector_unify($selector1, $selector2) # @param $selector1 [Sass::Script::Value::String, Sass::Script::Value::List] # The first selector to be unified. This can be either a # string, a list of strings, or a list of lists of strings as # returned by `&`. # @param $selector2 [Sass::Script::Value::String, Sass::Script::Value::List] # The second selector to be unified. This can be either a # string, a list of strings, or a list of lists of strings as # returned by `&`. # @return [Sass::Script::Value::List, Sass::Script::Value::Null] # A list of lists of strings representing the result of the # unification, or null if no unification exists. This is in # the same format as a selector returned by `&`. def selector_unify(selector1, selector2) selector1 = parse_selector(selector1, :selector1) selector2 = parse_selector(selector2, :selector2) return null unless (unified = selector1.unify(selector2)) unified.to_sass_script end declare :selector_unify, [:selector1, :selector2] # Returns the [simple # selectors](http://dev.w3.org/csswg/selectors4/#simple) that # comprise the compound selector `$selector`. # # Note that `$selector` **must be** a [compound # selector](http://dev.w3.org/csswg/selectors4/#compound). That # means it cannot contain commas or spaces. It also means that # unlike other selector functions, this takes only strings, not # lists. # # @example # simple-selectors(".foo.bar") => ".foo", ".bar" # simple-selectors(".foo.bar.baz") => ".foo", ".bar", ".baz" # # @overload simple_selectors($selector) # @param $selector [Sass::Script::Value::String] # The compound selector whose simple selectors will be extracted. # @return [Sass::Script::Value::List] # A list of simple selectors in the compound selector. def simple_selectors(selector) selector = parse_compound_selector(selector, :selector) list(selector.members.map {|simple| unquoted_string(simple.to_s)}, :comma) end declare :simple_selectors, [:selector] # Returns whether `$super` is a superselector of `$sub`. This means that # `$super` matches all the elements that `$sub` matches, as well as possibly # additional elements. In general, simpler selectors tend to be # superselectors of more complex oned. # # @example # is-superselector(".foo", ".foo.bar") => true # is-superselector(".foo.bar", ".foo") => false # is-superselector(".bar", ".foo .bar") => true # is-superselector(".foo .bar", ".bar") => false # # @overload is_superselector($super, $sub) # @param $super [Sass::Script::Value::String, Sass::Script::Value::List] # The potential superselector. This can be either a string, a list of # strings, or a list of lists of strings as returned by `&`. # @param $sub [Sass::Script::Value::String, Sass::Script::Value::List] # The potential subselector. This can be either a string, a list of # strings, or a list of lists of strings as returned by `&`. # @return [Sass::Script::Value::Bool] # Whether `$selector1` is a superselector of `$selector2`. def is_superselector(sup, sub) sup = parse_selector(sup, :super) sub = parse_selector(sub, :sub) bool(sup.superselector?(sub)) end declare :is_superselector, [:super, :sub] private # This method implements the pattern of transforming a numeric value into # another numeric value with the same units. # It yields a number to a block to perform the operation and return a number def numeric_transformation(value) assert_type value, :Number, :value Sass::Script::Value::Number.new( yield(value.value), value.numerator_units, value.denominator_units) end def _adjust(color, amount, attr, range, op, units = "") assert_type color, :Color, :color assert_type amount, :Number, :amount Sass::Util.check_range('Amount', range, amount, units) color.with(attr => color.send(attr).send(op, amount.value)) end def percentage_or_unitless(number, max, name) if number.unitless? number.value elsif number.is_unit?("%") max * number.value / 100.0; else raise ArgumentError.new( "$#{name}: Expected #{number} to have no units or \"%\""); end end end end ruby-sass-3.7.4/lib/sass/script/lexer.rb000066400000000000000000000414511345125207600201560ustar00rootroot00000000000000require 'sass/scss/rx' module Sass module Script # The lexical analyzer for SassScript. # It takes a raw string and converts it to individual tokens # that are easier to parse. class Lexer include Sass::SCSS::RX # A struct containing information about an individual token. # # `type`: \[`Symbol`\] # : The type of token. # # `value`: \[`Object`\] # : The Ruby object corresponding to the value of the token. # # `source_range`: \[`Sass::Source::Range`\] # : The range in the source file in which the token appeared. # # `pos`: \[`Integer`\] # : The scanner position at which the SassScript token appeared. Token = Struct.new(:type, :value, :source_range, :pos) # The line number of the lexer's current position. # # @return [Integer] def line return @line unless @tok @tok.source_range.start_pos.line end # The number of bytes into the current line # of the lexer's current position (1-based). # # @return [Integer] def offset return @offset unless @tok @tok.source_range.start_pos.offset end # A hash from operator strings to the corresponding token types. OPERATORS = { '+' => :plus, '-' => :minus, '*' => :times, '/' => :div, '%' => :mod, '=' => :single_eq, ':' => :colon, '(' => :lparen, ')' => :rparen, '[' => :lsquare, ']' => :rsquare, ',' => :comma, 'and' => :and, 'or' => :or, 'not' => :not, '==' => :eq, '!=' => :neq, '>=' => :gte, '<=' => :lte, '>' => :gt, '<' => :lt, '#{' => :begin_interpolation, '}' => :end_interpolation, ';' => :semicolon, '{' => :lcurly, '...' => :splat, } OPERATORS_REVERSE = Sass::Util.map_hash(OPERATORS) {|k, v| [v, k]} TOKEN_NAMES = Sass::Util.map_hash(OPERATORS_REVERSE) {|k, v| [k, v.inspect]}.merge( :const => "variable (e.g. $foo)", :ident => "identifier (e.g. middle)") # A list of operator strings ordered with longer names first # so that `>` and `<` don't clobber `>=` and `<=`. OP_NAMES = OPERATORS.keys.sort_by {|o| -o.size} # A sub-list of {OP_NAMES} that only includes operators # with identifier names. IDENT_OP_NAMES = OP_NAMES.select {|k, _v| k =~ /^\w+/} PARSEABLE_NUMBER = /(?:(\d*\.\d+)|(\d+))(?:[eE]([+-]?\d+))?(#{UNIT})?/ # A hash of regular expressions that are used for tokenizing. REGULAR_EXPRESSIONS = { :whitespace => /\s+/, :comment => COMMENT, :single_line_comment => SINGLE_LINE_COMMENT, :variable => /(\$)(#{IDENT})/, :ident => /(#{IDENT})(\()?/, :number => PARSEABLE_NUMBER, :unary_minus_number => /-#{PARSEABLE_NUMBER}/, :color => HEXCOLOR, :id => /##{IDENT}/, :selector => /&/, :ident_op => /(#{Regexp.union(*IDENT_OP_NAMES.map do |s| Regexp.new(Regexp.escape(s) + "(?!#{NMCHAR}|\Z)") end)})/, :op => /(#{Regexp.union(*OP_NAMES)})/, } class << self private def string_re(open, close) /#{open}((?:\\.|\#(?!\{)|[^#{close}\\#])*)(#{close}|#\{)/m end end # A hash of regular expressions that are used for tokenizing strings. # # The key is a `[Symbol, Boolean]` pair. # The symbol represents which style of quotation to use, # while the boolean represents whether or not the string # is following an interpolated segment. STRING_REGULAR_EXPRESSIONS = { :double => { false => string_re('"', '"'), true => string_re('', '"') }, :single => { false => string_re("'", "'"), true => string_re('', "'") }, :uri => { false => /url\(#{W}(#{URLCHAR}*?)(#{W}\)|#\{)/, true => /(#{URLCHAR}*?)(#{W}\)|#\{)/ }, # Defined in https://developer.mozilla.org/en/CSS/@-moz-document as a # non-standard version of http://www.w3.org/TR/css3-conditional/ :url_prefix => { false => /url-prefix\(#{W}(#{URLCHAR}*?)(#{W}\)|#\{)/, true => /(#{URLCHAR}*?)(#{W}\)|#\{)/ }, :domain => { false => /domain\(#{W}(#{URLCHAR}*?)(#{W}\)|#\{)/, true => /(#{URLCHAR}*?)(#{W}\)|#\{)/ } } # @param str [String, StringScanner] The source text to lex # @param line [Integer] The 1-based line on which the SassScript appears. # Used for error reporting and sourcemap building # @param offset [Integer] The 1-based character (not byte) offset in the line in the source. # Used for error reporting and sourcemap building # @param options [{Symbol => Object}] An options hash; # see {file:SASS_REFERENCE.md#Options the Sass options documentation} def initialize(str, line, offset, options) @scanner = str.is_a?(StringScanner) ? str : Sass::Util::MultibyteStringScanner.new(str) @line = line @offset = offset @options = options @interpolation_stack = [] @prev = nil @tok = nil @next_tok = nil end # Moves the lexer forward one token. # # @return [Token] The token that was moved past def next @tok ||= read_token @tok, tok = nil, @tok @prev = tok tok end # Returns whether or not there's whitespace before the next token. # # @return [Boolean] def whitespace?(tok = @tok) if tok @scanner.string[0...tok.pos] =~ /\s\Z/ else @scanner.string[@scanner.pos, 1] =~ /^\s/ || @scanner.string[@scanner.pos - 1, 1] =~ /\s\Z/ end end # Returns the given character. # # @return [String] def char(pos = @scanner.pos) @scanner.string[pos, 1] end # Consumes and returns single raw character from the input stream. # # @return [String] def next_char unpeek! scan(/./) end # Returns the next token without moving the lexer forward. # # @return [Token] The next token def peek @tok ||= read_token end # Rewinds the underlying StringScanner # to before the token returned by \{#peek}. def unpeek! raise "[BUG] Can't unpeek before a queued token!" if @next_tok return unless @tok @scanner.pos = @tok.pos @line = @tok.source_range.start_pos.line @offset = @tok.source_range.start_pos.offset end # @return [Boolean] Whether or not there's more source text to lex. def done? return if @next_tok whitespace unless after_interpolation? && !@interpolation_stack.empty? @scanner.eos? && @tok.nil? end # @return [Boolean] Whether or not the last token lexed was `:end_interpolation`. def after_interpolation? @prev && @prev.type == :end_interpolation end # Raise an error to the effect that `name` was expected in the input stream # and wasn't found. # # This calls \{#unpeek!} to rewind the scanner to immediately after # the last returned token. # # @param name [String] The name of the entity that was expected but not found # @raise [Sass::SyntaxError] def expected!(name) unpeek! Sass::SCSS::Parser.expected(@scanner, name, @line) end # Records all non-comment text the lexer consumes within the block # and returns it as a string. # # @yield A block in which text is recorded # @return [String] def str old_pos = @tok ? @tok.pos : @scanner.pos yield new_pos = @tok ? @tok.pos : @scanner.pos @scanner.string[old_pos...new_pos] end # Runs a block, and rewinds the state of the lexer to the beginning of the # block if it returns `nil` or `false`. def try old_pos = @scanner.pos old_line = @line old_offset = @offset old_interpolation_stack = @interpolation_stack.dup old_prev = @prev old_tok = @tok old_next_tok = @next_tok result = yield return result if result @scanner.pos = old_pos @line = old_line @offset = old_offset @interpolation_stack = old_interpolation_stack @prev = old_prev @tok = old_tok @next_tok = old_next_tok nil end private def read_token if (tok = @next_tok) @next_tok = nil return tok end return if done? start_pos = source_position value = token return unless value type, val = value Token.new(type, val, range(start_pos), @scanner.pos - @scanner.matched_size) end def whitespace nil while scan(REGULAR_EXPRESSIONS[:whitespace]) || scan(REGULAR_EXPRESSIONS[:comment]) || scan(REGULAR_EXPRESSIONS[:single_line_comment]) end def token if after_interpolation? interp_type, interp_value = @interpolation_stack.pop if interp_type == :special_fun return special_fun_body(interp_value) elsif interp_type.nil? if @scanner.string[@scanner.pos - 1] == '}' && scan(REGULAR_EXPRESSIONS[:ident]) return [@scanner[2] ? :funcall : :ident, Sass::Util.normalize_ident_escapes(@scanner[1], start: false)] end else raise "[BUG]: Unknown interp_type #{interp_type}" unless interp_type == :string return string(interp_value, true) end end variable || string(:double, false) || string(:single, false) || number || id || color || selector || string(:uri, false) || raw(UNICODERANGE) || special_fun || special_val || ident_op || ident || op end def variable _variable(REGULAR_EXPRESSIONS[:variable]) end def _variable(rx) return unless scan(rx) [:const, Sass::Util.normalize_ident_escapes(@scanner[2])] end def ident return unless scan(REGULAR_EXPRESSIONS[:ident]) [@scanner[2] ? :funcall : :ident, Sass::Util.normalize_ident_escapes(@scanner[1])] end def string(re, open) line, offset = @line, @offset return unless scan(STRING_REGULAR_EXPRESSIONS[re][open]) if @scanner[0] =~ /([^\\]|^)\n/ filename = @options[:filename] Sass::Util.sass_warn < Object}] An options hash; see # {file:SASS_REFERENCE.md#Options the Sass options documentation}. # This supports an additional `:allow_extra_text` option that controls # whether the parser throws an error when extra text is encountered # after the parsed construct. def initialize(str, line, offset, options = {}) @options = options @allow_extra_text = options.delete(:allow_extra_text) @lexer = lexer_class.new(str, line, offset, options) @stop_at = nil end # Parses a SassScript expression within an interpolated segment (`#{}`). # This means that it stops when it comes across an unmatched `}`, # which signals the end of an interpolated segment, # it returns rather than throwing an error. # # @param warn_for_color [Boolean] Whether raw color values passed to # interoplation should cause a warning. # @return [Script::Tree::Node] The root node of the parse tree # @raise [Sass::SyntaxError] if the expression isn't valid SassScript def parse_interpolated(warn_for_color = false) # Start two characters back to compensate for #{ start_pos = Sass::Source::Position.new(line, offset - 2) expr = assert_expr :expr assert_tok :end_interpolation expr = Sass::Script::Tree::Interpolation.new( nil, expr, nil, false, false, :warn_for_color => warn_for_color) check_for_interpolation expr expr.options = @options node(expr, start_pos) rescue Sass::SyntaxError => e e.modify_backtrace :line => @lexer.line, :filename => @options[:filename] raise e end # Parses a SassScript expression. # # @return [Script::Tree::Node] The root node of the parse tree # @raise [Sass::SyntaxError] if the expression isn't valid SassScript def parse expr = assert_expr :expr assert_done expr.options = @options check_for_interpolation expr expr rescue Sass::SyntaxError => e e.modify_backtrace :line => @lexer.line, :filename => @options[:filename] raise e end # Parses a SassScript expression, # ending it when it encounters one of the given identifier tokens. # # @param tokens [#include?(String | Symbol)] A set of strings or symbols that delimit the expression. # @return [Script::Tree::Node] The root node of the parse tree # @raise [Sass::SyntaxError] if the expression isn't valid SassScript def parse_until(tokens) @stop_at = tokens expr = assert_expr :expr assert_done expr.options = @options check_for_interpolation expr expr rescue Sass::SyntaxError => e e.modify_backtrace :line => @lexer.line, :filename => @options[:filename] raise e end # Parses the argument list for a mixin include. # # @return [(Array, # {String => Script::Tree::Node}, # Script::Tree::Node, # Script::Tree::Node)] # The root nodes of the positional arguments, keyword arguments, and # splat argument(s). Keyword arguments are in a hash from names to values. # @raise [Sass::SyntaxError] if the argument list isn't valid SassScript def parse_mixin_include_arglist args, keywords = [], {} if try_tok(:lparen) args, keywords, splat, kwarg_splat = mixin_arglist assert_tok(:rparen) end assert_done args.each do |a| check_for_interpolation a a.options = @options end keywords.each do |_, v| check_for_interpolation v v.options = @options end if splat check_for_interpolation splat splat.options = @options end if kwarg_splat check_for_interpolation kwarg_splat kwarg_splat.options = @options end return args, keywords, splat, kwarg_splat rescue Sass::SyntaxError => e e.modify_backtrace :line => @lexer.line, :filename => @options[:filename] raise e end # Parses the argument list for a mixin definition. # # @return [(Array, Script::Tree::Node)] # The root nodes of the arguments, and the splat argument. # @raise [Sass::SyntaxError] if the argument list isn't valid SassScript def parse_mixin_definition_arglist args, splat = defn_arglist!(false) assert_done args.each do |k, v| check_for_interpolation k k.options = @options if v check_for_interpolation v v.options = @options end end if splat check_for_interpolation splat splat.options = @options end return args, splat rescue Sass::SyntaxError => e e.modify_backtrace :line => @lexer.line, :filename => @options[:filename] raise e end # Parses the argument list for a function definition. # # @return [(Array, Script::Tree::Node)] # The root nodes of the arguments, and the splat argument. # @raise [Sass::SyntaxError] if the argument list isn't valid SassScript def parse_function_definition_arglist args, splat = defn_arglist!(true) assert_done args.each do |k, v| check_for_interpolation k k.options = @options if v check_for_interpolation v v.options = @options end end if splat check_for_interpolation splat splat.options = @options end return args, splat rescue Sass::SyntaxError => e e.modify_backtrace :line => @lexer.line, :filename => @options[:filename] raise e end # Parse a single string value, possibly containing interpolation. # Doesn't assert that the scanner is finished after parsing. # # @return [Script::Tree::Node] The root node of the parse tree. # @raise [Sass::SyntaxError] if the string isn't valid SassScript def parse_string unless (peek = @lexer.peek) && (peek.type == :string || (peek.type == :funcall && peek.value.downcase == 'url')) lexer.expected!("string") end expr = assert_expr :funcall check_for_interpolation expr expr.options = @options @lexer.unpeek! expr rescue Sass::SyntaxError => e e.modify_backtrace :line => @lexer.line, :filename => @options[:filename] raise e end # Parses a SassScript expression. # # @overload parse(str, line, offset, filename = nil) # @return [Script::Tree::Node] The root node of the parse tree # @see Parser#initialize # @see Parser#parse def self.parse(*args) new(*args).parse end PRECEDENCE = [ :comma, :single_eq, :space, :or, :and, [:eq, :neq], [:gt, :gte, :lt, :lte], [:plus, :minus], [:times, :div, :mod], ] ASSOCIATIVE = [:plus, :times] class << self # Returns an integer representing the precedence # of the given operator. # A lower integer indicates a looser binding. # # @private def precedence_of(op) PRECEDENCE.each_with_index do |e, i| return i if Array(e).include?(op) end raise "[BUG] Unknown operator #{op.inspect}" end # Returns whether or not the given operation is associative. # # @private def associative?(op) ASSOCIATIVE.include?(op) end private # Defines a simple left-associative production. # name is the name of the production, # sub is the name of the production beneath it, # and ops is a list of operators for this precedence level def production(name, sub, *ops) class_eval < true, :deprecation => deprecation), (prev || str).source_range.start_pos) interpolation(first: interp) end def try_ops_after_interp(ops, name, prev = nil) return unless @lexer.after_interpolation? op = peek_toks(*ops) return unless op return if @stop_at && @stop_at.include?(op.type) @lexer.next interp = try_op_before_interp(op, prev, :after_interp) return interp if interp wa = @lexer.whitespace? str = literal_node(Script::Value::String.new(Lexer::OPERATORS_REVERSE[op.type]), op.source_range) str.line = @lexer.line deprecation = case op.type when :comma; :potential when :div, :single_eq; :none when :minus; @lexer.whitespace?(op) ? :immediate : :none else; :immediate end interp = node( Script::Tree::Interpolation.new( prev, str, assert_expr(name), false, wa, :originally_text => true, :deprecation => deprecation), (prev || str).source_range.start_pos) interp end def interpolation(first: nil, inner: :space) e = first || send(inner) while (interp = try_tok(:begin_interpolation)) wb = @lexer.whitespace?(interp) char_before = @lexer.char(interp.pos - 1) mid = without_stop_at {assert_expr :expr} assert_tok :end_interpolation wa = @lexer.whitespace? char_after = @lexer.char after = send(inner) before_deprecation = e.is_a?(Script::Tree::Interpolation) ? e.deprecation : :none after_deprecation = after.is_a?(Script::Tree::Interpolation) ? after.deprecation : :none deprecation = if before_deprecation == :immediate || after_deprecation == :immediate || # Warn for #{foo}$var and #{foo}(1) but not #{$foo}1. (after && !wa && char_after =~ /[$(]/) || # Warn for $var#{foo} and (a)#{foo} but not a#{foo}. (e && !wb && is_unsafe_before?(e, char_before)) :immediate else :potential end e = node( Script::Tree::Interpolation.new(e, mid, after, wb, wa, :deprecation => deprecation), (e || interp).source_range.start_pos) end e end # Returns whether `expr` is unsafe to include before an interpolation. # # @param expr [Node] The expression to check. # @param char_before [String] The character immediately before the # interpolation being checked (and presumably the last character of # `expr`). # @return [Boolean] def is_unsafe_before?(expr, char_before) return char_before == ')' if is_safe_value?(expr) # Otherwise, it's only safe if it was another interpolation. !expr.is_a?(Script::Tree::Interpolation) end # Returns whether `expr` is safe as the value immediately before an # interpolation. # # It's safe as long as the previous expression is an identifier or number, # or a list whose last element is also safe. def is_safe_value?(expr) return is_safe_value?(expr.elements.last) if expr.is_a?(Script::Tree::ListLiteral) return false unless expr.is_a?(Script::Tree::Literal) expr.value.is_a?(Script::Value::Number) || (expr.value.is_a?(Script::Value::String) && expr.value.type == :identifier) end def space start_pos = source_position e = or_expr return unless e arr = [e] while (e = or_expr) arr << e end if arr.size == 1 arr.first else node(Sass::Script::Tree::ListLiteral.new(arr, separator: :space), start_pos) end end production :or_expr, :and_expr, :or production :and_expr, :eq_or_neq, :and production :eq_or_neq, :relational, :eq, :neq production :relational, :plus_or_minus, :gt, :gte, :lt, :lte production :plus_or_minus, :times_div_or_mod, :plus, :minus production :times_div_or_mod, :unary_plus, :times, :div, :mod unary :plus, :unary_minus unary :minus, :unary_div unary :div, :unary_not # For strings, so /foo/bar works unary :not, :ident def ident return css_min_max unless @lexer.peek && @lexer.peek.type == :ident return if @stop_at && @stop_at.include?(@lexer.peek.value) name = @lexer.next if (color = Sass::Script::Value::Color::COLOR_NAMES[name.value.downcase]) literal_node(Sass::Script::Value::Color.new(color, name.value), name.source_range) elsif name.value == "true" literal_node(Sass::Script::Value::Bool.new(true), name.source_range) elsif name.value == "false" literal_node(Sass::Script::Value::Bool.new(false), name.source_range) elsif name.value == "null" literal_node(Sass::Script::Value::Null.new, name.source_range) else literal_node(Sass::Script::Value::String.new(name.value, :identifier), name.source_range) end end def css_min_max @lexer.try do next unless tok = try_tok(:funcall) next unless %w[min max].include?(tok.value.downcase) next unless contents = min_max_contents node(array_to_interpolation(["#{tok.value}(", *contents]), tok.source_range.start_pos, source_position) end || funcall end def min_max_contents(allow_comma: true) result = [] loop do if tok = try_tok(:number) result << tok.value.to_s elsif value = min_max_interpolation result << value elsif value = min_max_calc result << value.value elsif value = min_max_function || min_max_parens || nested_min_max result.concat value else return end if try_tok(:rparen) result << ")" return result elsif tok = try_tok(:plus) || try_tok(:minus) || try_tok(:times) || try_tok(:div) result << " #{Lexer::OPERATORS_REVERSE[tok.type]} " elsif allow_comma && try_tok(:comma) result << ", " else return end end end def min_max_interpolation without_stop_at do tok = try_tok(:begin_interpolation) return unless tok expr = without_stop_at {assert_expr :expr} assert_tok :end_interpolation expr end end def min_max_function return unless tok = peek_tok(:funcall) return unless %w[calc env var].include?(tok.value.downcase) @lexer.next result = [tok.value, '(', *declaration_value, ')'] assert_tok :rparen result end def min_max_calc return unless tok = peek_tok(:special_fun) return unless tok.value.value.downcase.start_with?("calc(") @lexer.next.value end def min_max_parens return unless try_tok :lparen return unless contents = min_max_contents(allow_comma: false) ['(', *contents] end def nested_min_max return unless tok = peek_tok(:funcall) return unless %w[min max].include?(tok.value.downcase) @lexer.next return unless contents = min_max_contents [tok.value, '(', *contents] end def declaration_value result = [] brackets = [] loop do result << @lexer.str do until @lexer.done? || peek_toks(:begin_interpolation, :end_interpolation, :lcurly, :lparen, :lsquare, :rparen, :rsquare) @lexer.next || @lexer.next_char end end if try_tok(:begin_interpolation) result << assert_expr(:expr) assert_tok :end_interpolation elsif tok = try_toks(:lcurly, :lparen, :lsquare) brackets << case tok.type when :lcurly; :end_interpolation when :lparen; :rparen when :lsquare; :rsquare end result << Lexer::OPERATORS_REVERSE[tok.type] elsif brackets.empty? return result else bracket = brackets.pop assert_tok bracket result << Lexer::OPERATORS_REVERSE[bracket] end end end def funcall tok = try_tok(:funcall) return raw unless tok args, keywords, splat, kwarg_splat = fn_arglist assert_tok(:rparen) node(Script::Tree::Funcall.new(tok.value, args, keywords, splat, kwarg_splat), tok.source_range.start_pos, source_position) end def defn_arglist!(must_have_parens) if must_have_parens assert_tok(:lparen) else return [], nil unless try_tok(:lparen) end without_stop_at do res = [] splat = nil must_have_default = false loop do break if peek_tok(:rparen) c = assert_tok(:const) var = node(Script::Tree::Variable.new(c.value), c.source_range) if try_tok(:colon) val = assert_expr(:space) must_have_default = true elsif try_tok(:splat) splat = var break elsif must_have_default raise SyntaxError.new( "Required argument #{var.inspect} must come before any optional arguments.") end res << [var, val] break unless try_tok(:comma) end assert_tok(:rparen) return res, splat end end def fn_arglist arglist(:equals, "function argument") end def mixin_arglist arglist(:interpolation, "mixin argument") end def arglist(subexpr, description) without_stop_at do args = [] keywords = Sass::Util::NormalizedMap.new splat = nil while (e = send(subexpr)) if @lexer.peek && @lexer.peek.type == :colon name = e @lexer.expected!("comma") unless name.is_a?(Tree::Variable) assert_tok(:colon) value = assert_expr(subexpr, description) if keywords[name.name] raise SyntaxError.new("Keyword argument \"#{name.to_sass}\" passed more than once") end keywords[name.name] = value else if try_tok(:splat) return args, keywords, splat, e if splat splat, e = e, nil elsif splat raise SyntaxError.new("Only keyword arguments may follow variable arguments (...).") elsif !keywords.empty? raise SyntaxError.new("Positional arguments must come before keyword arguments.") end args << e if e end return args, keywords, splat unless try_tok(:comma) end return args, keywords end end def raw tok = try_tok(:raw) return special_fun unless tok literal_node(Script::Value::String.new(tok.value), tok.source_range) end def special_fun first = try_tok(:special_fun) return square_list unless first str = literal_node(first.value, first.source_range) return str unless try_tok(:string_interpolation) mid = without_stop_at {assert_expr :expr} assert_tok :end_interpolation last = assert_expr(:special_fun) node( Tree::Interpolation.new(str, mid, last, false, false), first.source_range.start_pos) end def square_list start_pos = source_position return paren unless try_tok(:lsquare) without_stop_at do space_start_pos = source_position e = interpolation(inner: :or_expr) separator = nil if e elements = [e] while (e = interpolation(inner: :or_expr)) elements << e end # If there's a comma after a space-separated list, it's actually a # space-separated list nested in a comma-separated list. if try_tok(:comma) e = if elements.length == 1 elements.first else node( Sass::Script::Tree::ListLiteral.new(elements, separator: :space), space_start_pos) end elements = [e] while (e = space) elements << e break unless try_tok(:comma) end separator = :comma else separator = :space if elements.length > 1 end else elements = [] end assert_tok(:rsquare) end_pos = source_position node(Sass::Script::Tree::ListLiteral.new(elements, separator: separator, bracketed: true), start_pos, end_pos) end end def paren return variable unless try_tok(:lparen) without_stop_at do start_pos = source_position e = map e.force_division! if e end_pos = source_position assert_tok(:rparen) e || node(Sass::Script::Tree::ListLiteral.new([]), start_pos, end_pos) end end def variable start_pos = source_position c = try_tok(:const) return string unless c node(Tree::Variable.new(*c.value), start_pos) end def string first = try_tok(:string) return number unless first str = literal_node(first.value, first.source_range) return str unless try_tok(:string_interpolation) mid = assert_expr :expr assert_tok :end_interpolation last = without_stop_at {assert_expr(:string)} node(Tree::StringInterpolation.new(str, mid, last), first.source_range.start_pos) end def number tok = try_tok(:number) return selector unless tok num = tok.value num.options = @options num.original = num.to_s literal_node(num, tok.source_range.start_pos) end def selector tok = try_tok(:selector) return literal unless tok node(tok.value, tok.source_range.start_pos) end def literal t = try_tok(:color) return literal_node(t.value, t.source_range) if t end # It would be possible to have unified #assert and #try methods, # but detecting the method/token difference turns out to be quite expensive. EXPR_NAMES = { :string => "string", :default => "expression (e.g. 1px, bold)", :mixin_arglist => "mixin argument", :fn_arglist => "function argument", :splat => "...", :special_fun => '")"', } def assert_expr(name, expected = nil) e = send(name) return e if e @lexer.expected!(expected || EXPR_NAMES[name] || EXPR_NAMES[:default]) end def assert_tok(name) # Avoids an array allocation caused by argument globbing in assert_toks. t = try_tok(name) return t if t @lexer.expected!(Lexer::TOKEN_NAMES[name] || name.to_s) end def assert_toks(*names) t = try_toks(*names) return t if t @lexer.expected!(names.map {|tok| Lexer::TOKEN_NAMES[tok] || tok}.join(" or ")) end def peek_tok(name) # Avoids an array allocation caused by argument globbing in the try_toks method. peeked = @lexer.peek peeked && name == peeked.type && peeked end def peek_toks(*names) peeked = @lexer.peek peeked && names.include?(peeked.type) && peeked end def try_tok(name) peek_tok(name) && @lexer.next end def try_toks(*names) peek_toks(*names) && @lexer.next end def assert_done if @allow_extra_text # If extra text is allowed, just rewind the lexer so that the # StringScanner is pointing to the end of the parsed text. @lexer.unpeek! else return if @lexer.done? @lexer.expected!(EXPR_NAMES[:default]) end end def without_stop_at old_stop_at = @stop_at @stop_at = nil yield ensure @stop_at = old_stop_at end # @overload node(value, source_range) # @param value [Sass::Script::Value::Base] # @param source_range [Sass::Source::Range] # @overload node(value, start_pos, end_pos = source_position) # @param value [Sass::Script::Value::Base] # @param start_pos [Sass::Source::Position] # @param end_pos [Sass::Source::Position] def literal_node(value, source_range_or_start_pos, end_pos = source_position) node(Sass::Script::Tree::Literal.new(value), source_range_or_start_pos, end_pos) end # @overload node(node, source_range) # @param node [Sass::Script::Tree::Node] # @param source_range [Sass::Source::Range] # @overload node(node, start_pos, end_pos = source_position) # @param node [Sass::Script::Tree::Node] # @param start_pos [Sass::Source::Position] # @param end_pos [Sass::Source::Position] def node(node, source_range_or_start_pos, end_pos = source_position) source_range = if source_range_or_start_pos.is_a?(Sass::Source::Range) source_range_or_start_pos else range(source_range_or_start_pos, end_pos) end node.line = source_range.start_pos.line node.source_range = source_range node.filename = @options[:filename] node end # Converts an array of strings and expressions to a string interoplation # object. # # @param array [Array] # @return [Script::Tree::StringInterpolation] def array_to_interpolation(array) Sass::Util.merge_adjacent_strings(array).reverse.inject(nil) do |after, value| if value.is_a?(::String) literal = Sass::Script::Tree::Literal.new( Sass::Script::Value::String.new(value)) next literal unless after Sass::Script::Tree::StringInterpolation.new(literal, after.mid, after.after) else Sass::Script::Tree::StringInterpolation.new( Sass::Script::Tree::Literal.new( Sass::Script::Value::String.new('')), value, after || Sass::Script::Tree::Literal.new( Sass::Script::Value::String.new(''))) end end end # Checks a script node for any immediately-deprecated interpolations, and # emits warnings for them. # # @param node [Sass::Script::Tree::Node] def check_for_interpolation(node) nodes = [node] until nodes.empty? node = nodes.pop unless node.is_a?(Sass::Script::Tree::Interpolation) && node.deprecation == :immediate nodes.concat node.children next end interpolation_deprecation(node) end end # Emits a deprecation warning for an interpolation node. # # @param node [Sass::Script::Tree::Node] def interpolation_deprecation(interpolation) return if @options[:_convert] location = "on line #{interpolation.line}" location << " of #{interpolation.filename}" if interpolation.filename Sass::Util.sass_warn <] attr_reader :args # The keyword arguments to the function. # # @return [Sass::Util::NormalizedMap] attr_reader :keywords # The first splat argument for this function, if one exists. # # This could be a list of positional arguments, a map of keyword # arguments, or an arglist containing both. # # @return [Node?] attr_accessor :splat # The second splat argument for this function, if one exists. # # If this exists, it's always a map of keyword arguments, and # \{#splat} is always either a list or an arglist. # # @return [Node?] attr_accessor :kwarg_splat # @param name_or_callable [String, Sass::Callable] See \{#name} # @param args [Array] See \{#args} # @param keywords [Sass::Util::NormalizedMap] See \{#keywords} # @param splat [Node] See \{#splat} # @param kwarg_splat [Node] See \{#kwarg_splat} def initialize(name_or_callable, args, keywords, splat, kwarg_splat) if name_or_callable.is_a?(Sass::Callable) @callable = name_or_callable @name = name_or_callable.name else @callable = nil @name = name_or_callable end @args = args @keywords = keywords @splat = splat @kwarg_splat = kwarg_splat super() end # @return [String] A string representation of the function call def inspect args = @args.map {|a| a.inspect}.join(', ') keywords = @keywords.as_stored.to_a.map {|k, v| "$#{k}: #{v.inspect}"}.join(', ') if self.splat splat = args.empty? && keywords.empty? ? "" : ", " splat = "#{splat}#{self.splat.inspect}..." splat = "#{splat}, #{kwarg_splat.inspect}..." if kwarg_splat end "#{name}(#{args}#{', ' unless args.empty? || keywords.empty?}#{keywords}#{splat})" end # @see Node#to_sass def to_sass(opts = {}) arg_to_sass = lambda do |arg| sass = arg.to_sass(opts) sass = "(#{sass})" if arg.is_a?(Sass::Script::Tree::ListLiteral) && arg.separator == :comma sass end args = @args.map(&arg_to_sass) keywords = @keywords.as_stored.to_a.map {|k, v| "$#{dasherize(k, opts)}: #{arg_to_sass[v]}"} if self.splat splat = "#{arg_to_sass[self.splat]}..." kwarg_splat = "#{arg_to_sass[self.kwarg_splat]}..." if self.kwarg_splat end arglist = [args, splat, keywords, kwarg_splat].flatten.compact.join(', ') "#{dasherize(name, opts)}(#{arglist})" end # Returns the arguments to the function. # # @return [Array] # @see Node#children def children res = @args + @keywords.values res << @splat if @splat res << @kwarg_splat if @kwarg_splat res end # @see Node#deep_copy def deep_copy node = dup node.instance_variable_set('@args', args.map {|a| a.deep_copy}) copied_keywords = Sass::Util::NormalizedMap.new @keywords.as_stored.each {|k, v| copied_keywords[k] = v.deep_copy} node.instance_variable_set('@keywords', copied_keywords) node end protected # Evaluates the function call. # # @param environment [Sass::Environment] The environment in which to evaluate the SassScript # @return [Sass::Script::Value] The SassScript object that is the value of the function call # @raise [Sass::SyntaxError] if the function call raises an ArgumentError def _perform(environment) args = @args.each_with_index. map {|a, i| perform_arg(a, environment, signature && signature.args[i])} keywords = Sass::Util.map_hash(@keywords) do |k, v| [k, perform_arg(v, environment, k.tr('-', '_'))] end splat = Sass::Tree::Visitors::Perform.perform_splat( @splat, keywords, @kwarg_splat, environment) fn = @callable || environment.function(@name) if fn && fn.origin == :stylesheet environment.stack.with_function(filename, line, name) do return without_original(perform_sass_fn(fn, args, splat, environment)) end end args = construct_ruby_args(ruby_name, args, splat, environment) if Sass::Script::Functions.callable?(ruby_name) && (!fn || fn.origin == :builtin) local_environment = Sass::Environment.new(environment.global_env, environment.options) local_environment.caller = Sass::ReadOnlyEnvironment.new(environment, environment.options) result = local_environment.stack.with_function(filename, line, name) do opts(Sass::Script::Functions::EvaluationContext.new( local_environment).send(ruby_name, *args)) end without_original(result) else opts(to_literal(args)) end rescue ArgumentError => e reformat_argument_error(e) end # Compass historically overrode this before it changed name to {Funcall#to_value}. # We should get rid of it in the future. def to_literal(args) to_value(args) end # This method is factored out from `_perform` so that compass can override # it with a cross-browser implementation for functions that require vendor prefixes # in the generated css. def to_value(args) Sass::Script::Value::String.new("#{name}(#{args.join(', ')})") end private def ruby_name @ruby_name ||= @name.tr('-', '_') end def perform_arg(argument, environment, name) return argument if signature && signature.delayed_args.include?(name) argument.perform(environment) end def signature @signature ||= Sass::Script::Functions.signature(name.to_sym, @args.size, @keywords.size) end def without_original(value) return value unless value.is_a?(Sass::Script::Value::Number) value = value.dup value.original = nil value end def construct_ruby_args(name, args, splat, environment) args += splat.to_a if splat # All keywords are contained in splat.keywords for consistency, # even if there were no splats passed in. old_keywords_accessed = splat.keywords_accessed keywords = splat.keywords splat.keywords_accessed = old_keywords_accessed unless (signature = Sass::Script::Functions.signature(name.to_sym, args.size, keywords.size)) return args if keywords.empty? raise Sass::SyntaxError.new("Function #{name} doesn't support keyword arguments") end # If the user passes more non-keyword args than the function expects, # but it does expect keyword args, Ruby's arg handling won't raise an error. # Since we don't want to make functions think about this, # we'll handle it for them here. if signature.var_kwargs && !signature.var_args && args.size > signature.args.size raise Sass::SyntaxError.new( "#{args[signature.args.size].inspect} is not a keyword argument for `#{name}'") elsif keywords.empty? args << {} if signature.var_kwargs return args end argnames = signature.args[args.size..-1] || [] deprecated_argnames = (signature.deprecated && signature.deprecated[args.size..-1]) || [] args += argnames.zip(deprecated_argnames).map do |(argname, deprecated_argname)| if keywords.has_key?(argname) keywords.delete(argname) elsif deprecated_argname && keywords.has_key?(deprecated_argname) deprecated_argname = keywords.denormalize(deprecated_argname) Sass::Util.sass_warn("DEPRECATION WARNING: The `$#{deprecated_argname}' argument for " + "`#{@name}()' has been renamed to `$#{argname}'.") keywords.delete(deprecated_argname) else raise Sass::SyntaxError.new("Function #{name} requires an argument named $#{argname}") end end if keywords.size > 0 if signature.var_kwargs # Don't pass a NormalizedMap to a Ruby function. args << keywords.to_hash else argname = keywords.keys.sort.first if signature.args.include?(argname) raise Sass::SyntaxError.new( "Function #{name} was passed argument $#{argname} both by position and by name") else raise Sass::SyntaxError.new( "Function #{name} doesn't have an argument named $#{argname}") end end end args end def perform_sass_fn(function, args, splat, environment) Sass::Tree::Visitors::Perform.perform_arguments(function, args, splat, environment) do |env| env.caller = Sass::Environment.new(environment) val = catch :_sass_return do function.tree.each {|c| Sass::Tree::Visitors::Perform.visit(c, env)} raise Sass::SyntaxError.new("Function #{@name} finished without @return") end val end end def reformat_argument_error(e) message = e.message # If this is a legitimate Ruby-raised argument error, re-raise it. # Otherwise, it's an error in the user's stylesheet, so wrap it. if Sass::Util.rbx? # Rubinius has a different error report string than vanilla Ruby. It # also doesn't put the actual method for which the argument error was # thrown in the backtrace, nor does it include `send`, so we look for # `_perform`. if e.message =~ /^method '([^']+)': given (\d+), expected (\d+)/ error_name, given, expected = $1, $2, $3 raise e if error_name != ruby_name || e.backtrace[0] !~ /:in `_perform'$/ message = "wrong number of arguments (#{given} for #{expected})" end elsif Sass::Util.jruby? should_maybe_raise = e.message =~ /^wrong number of arguments calling `[^`]+` \((\d+) for (\d+)\)/ given, expected = $1, $2 if should_maybe_raise # JRuby 1.7 includes __send__ before send and _perform. trace = e.backtrace.dup raise e if trace.shift !~ /:in `__send__'$/ # JRuby (as of 1.7.2) doesn't put the actual method # for which the argument error was thrown in the backtrace, so we # detect whether our send threw an argument error. if !(trace[0] =~ /:in `send'$/ && trace[1] =~ /:in `_perform'$/) raise e else # JRuby 1.7 doesn't use standard formatting for its ArgumentErrors. message = "wrong number of arguments (#{given} for #{expected})" end end elsif (md = /^wrong number of arguments \(given (\d+), expected (\d+)\)/.match(e.message)) && e.backtrace[0] =~ /:in `#{ruby_name}'$/ # Handle ruby 2.3 error formatting message = "wrong number of arguments (#{md[1]} for #{md[2]})" elsif e.message =~ /^wrong number of arguments/ && e.backtrace[0] !~ /:in `(block in )?#{ruby_name}'$/ raise e end raise Sass::SyntaxError.new("#{message} for `#{name}'") end end end ruby-sass-3.7.4/lib/sass/script/tree/interpolation.rb000066400000000000000000000175351345125207600226730ustar00rootroot00000000000000module Sass::Script::Tree # A SassScript object representing `#{}` interpolation outside a string. # # @see StringInterpolation class Interpolation < Node # @return [Node] The SassScript before the interpolation attr_reader :before # @return [Node] The SassScript within the interpolation attr_reader :mid # @return [Node] The SassScript after the interpolation attr_reader :after # @return [Boolean] Whether there was whitespace between `before` and `#{` attr_reader :whitespace_before # @return [Boolean] Whether there was whitespace between `}` and `after` attr_reader :whitespace_after # @return [Boolean] Whether the original format of the interpolation was # plain text, not an interpolation. This is used when converting back to # SassScript. attr_reader :originally_text # @return [Boolean] Whether a color value passed to the interpolation should # generate a warning. attr_reader :warn_for_color # The type of interpolation deprecation for this node. # # This can be `:none`, indicating that the node doesn't use deprecated # interpolation; `:immediate`, indicating that a deprecation warning should # be emitted as soon as possible; or `:potential`, indicating that a # deprecation warning should be emitted if the resulting string is used in a # way that would distinguish it from a list. # # @return [Symbol] attr_reader :deprecation # Interpolation in a property is of the form `before #{mid} after`. # # @param before [Node] See {Interpolation#before} # @param mid [Node] See {Interpolation#mid} # @param after [Node] See {Interpolation#after} # @param wb [Boolean] See {Interpolation#whitespace_before} # @param wa [Boolean] See {Interpolation#whitespace_after} # @param originally_text [Boolean] See {Interpolation#originally_text} # @param warn_for_color [Boolean] See {Interpolation#warn_for_color} def initialize(before, mid, after, wb, wa, opts = {}) @before = before @mid = mid @after = after @whitespace_before = wb @whitespace_after = wa @originally_text = opts[:originally_text] || false @warn_for_color = opts[:warn_for_color] || false @deprecation = opts[:deprecation] || :none end # @return [String] A human-readable s-expression representation of the interpolation def inspect "(interpolation #{@before.inspect} #{@mid.inspect} #{@after.inspect})" end # @see Node#to_sass def to_sass(opts = {}) return to_quoted_equivalent.to_sass if deprecation == :immediate res = "" res << @before.to_sass(opts) if @before res << ' ' if @before && @whitespace_before res << '#{' unless @originally_text res << @mid.to_sass(opts) res << '}' unless @originally_text res << ' ' if @after && @whitespace_after res << @after.to_sass(opts) if @after res end # Returns an `unquote()` expression that will evaluate to the same value as # this interpolation. # # @return [Sass::Script::Tree::Node] def to_quoted_equivalent Funcall.new( "unquote", [to_string_interpolation(self)], Sass::Util::NormalizedMap.new, nil, nil) end # Returns the three components of the interpolation, `before`, `mid`, and `after`. # # @return [Array] # @see #initialize # @see Node#children def children [@before, @mid, @after].compact end # @see Node#deep_copy def deep_copy node = dup node.instance_variable_set('@before', @before.deep_copy) if @before node.instance_variable_set('@mid', @mid.deep_copy) node.instance_variable_set('@after', @after.deep_copy) if @after node end protected # Converts a script node into a corresponding string interpolation # expression. # # @param node_or_interp [Sass::Script::Tree::Node] # @return [Sass::Script::Tree::StringInterpolation] def to_string_interpolation(node_or_interp) unless node_or_interp.is_a?(Interpolation) node = node_or_interp return string_literal(node.value.to_s) if node.is_a?(Literal) if node.is_a?(StringInterpolation) return concat(string_literal(node.quote), concat(node, string_literal(node.quote))) end return StringInterpolation.new(string_literal(""), node, string_literal("")) end interp = node_or_interp after_string_or_interp = if interp.after to_string_interpolation(interp.after) else string_literal("") end if interp.after && interp.whitespace_after after_string_or_interp = concat(string_literal(' '), after_string_or_interp) end mid_string_or_interp = to_string_interpolation(interp.mid) before_string_or_interp = if interp.before to_string_interpolation(interp.before) else string_literal("") end if interp.before && interp.whitespace_before before_string_or_interp = concat(before_string_or_interp, string_literal(' ')) end concat(before_string_or_interp, concat(mid_string_or_interp, after_string_or_interp)) end private # Evaluates the interpolation. # # @param environment [Sass::Environment] The environment in which to evaluate the SassScript # @return [Sass::Script::Value::String] # The SassScript string that is the value of the interpolation def _perform(environment) res = "" res << @before.perform(environment).to_s if @before res << " " if @before && @whitespace_before val = @mid.perform(environment) if @warn_for_color && val.is_a?(Sass::Script::Value::Color) && val.name alternative = Operation.new(Sass::Script::Value::String.new("", :string), @mid, :plus) Sass::Util.sass_warn < :none) res << " " if @after && @whitespace_after res << @after.perform(environment).to_s if @after str = Sass::Script::Value::String.new( res, :identifier, (to_quoted_equivalent.to_sass if deprecation == :potential)) str.source_range = source_range opts(str) end # Concatenates two string literals or string interpolation expressions. # # @param string_or_interp1 [Sass::Script::Tree::Literal|Sass::Script::Tree::StringInterpolation] # @param string_or_interp2 [Sass::Script::Tree::Literal|Sass::Script::Tree::StringInterpolation] # @return [Sass::Script::Tree::StringInterpolation] def concat(string_or_interp1, string_or_interp2) if string_or_interp1.is_a?(Literal) && string_or_interp2.is_a?(Literal) return string_literal(string_or_interp1.value.value + string_or_interp2.value.value) end if string_or_interp1.is_a?(Literal) string = string_or_interp1 interp = string_or_interp2 before = string_literal(string.value.value + interp.before.value.value) return StringInterpolation.new(before, interp.mid, interp.after) end StringInterpolation.new( string_or_interp1.before, string_or_interp1.mid, concat(string_or_interp1.after, string_or_interp2)) end # Returns a string literal with the given contents. # # @param string [String] # @return string [Sass::Script::Tree::Literal] def string_literal(string) Literal.new(Sass::Script::Value::String.new(string, :string)) end end end ruby-sass-3.7.4/lib/sass/script/tree/list_literal.rb000066400000000000000000000064731345125207600224720ustar00rootroot00000000000000module Sass::Script::Tree # A parse tree node representing a list literal. When resolved, this returns a # {Sass::Tree::Value::List}. class ListLiteral < Node # The parse nodes for members of this list. # # @return [Array] attr_reader :elements # The operator separating the values of the list. Either `:comma` or # `:space`. # # @return [Symbol] attr_reader :separator # Whether the list is surrounded by square brackets. # # @return [Boolean] attr_reader :bracketed # Creates a new list literal. # # @param elements [Array] See \{#elements} # @param separator [Symbol] See \{#separator} # @param bracketed [Boolean] See \{#bracketed} def initialize(elements, separator: nil, bracketed: false) @elements = elements @separator = separator @bracketed = bracketed end # @see Node#children def children; elements; end # @see Value#to_sass def to_sass(opts = {}) return bracketed ? "[]" : "()" if elements.empty? members = elements.map do |v| if element_needs_parens?(v) "(#{v.to_sass(opts)})" else v.to_sass(opts) end end if separator == :comma && members.length == 1 return "#{bracketed ? '[' : '('}#{members.first},#{bracketed ? ']' : ')'}" end contents = members.join(sep_str(nil)) bracketed ? "[#{contents}]" : contents end # @see Node#deep_copy def deep_copy node = dup node.instance_variable_set('@elements', elements.map {|e| e.deep_copy}) node end def inspect (bracketed ? '[' : '(') + elements.map {|e| e.inspect}.join(separator == :space ? ' ' : ', ') + (bracketed ? ']' : ')') end def force_division! # Do nothing. Lists prevent division propagation. end protected def _perform(environment) list = Sass::Script::Value::List.new( elements.map {|e| e.perform(environment)}, separator: separator, bracketed: bracketed) list.source_range = source_range list.options = options list end private # Returns whether an element in the list should be wrapped in parentheses # when serialized to Sass. def element_needs_parens?(element) if element.is_a?(ListLiteral) return false if element.elements.length < 2 return false if element.bracketed return Sass::Script::Parser.precedence_of(element.separator || :space) <= Sass::Script::Parser.precedence_of(separator || :space) end return false unless separator == :space if element.is_a?(UnaryOperation) return element.operator == :minus || element.operator == :plus end return false unless element.is_a?(Operation) return true unless element.operator == :div !(is_literal_number?(element.operand1) && is_literal_number?(element.operand2)) end # Returns whether a value is a number literal that shouldn't be divided. def is_literal_number?(value) value.is_a?(Literal) && value.value.is_a?((Sass::Script::Value::Number)) && !value.value.original.nil? end def sep_str(opts = options) return ' ' if separator == :space return ',' if opts && opts[:style] == :compressed ', ' end end end ruby-sass-3.7.4/lib/sass/script/tree/literal.rb000066400000000000000000000017661345125207600214370ustar00rootroot00000000000000module Sass::Script::Tree # The parse tree node for a literal scalar value. This wraps an instance of # {Sass::Script::Value::Base}. # # List literals should use {ListLiteral} instead. class Literal < Node # The wrapped value. # # @return [Sass::Script::Value::Base] attr_reader :value # Creates a new literal value. # # @param value [Sass::Script::Value::Base] # @see #value def initialize(value) @value = value end # @see Node#children def children; []; end # @see Node#to_sass def to_sass(opts = {}); value.to_sass(opts); end # @see Node#deep_copy def deep_copy; dup; end # @see Node#options= def options=(options) value.options = options end def inspect value.inspect end def force_division! value.original = nil if value.is_a?(Sass::Script::Value::Number) end protected def _perform(environment) value.source_range = source_range value end end end ruby-sass-3.7.4/lib/sass/script/tree/map_literal.rb000066400000000000000000000031471345125207600222670ustar00rootroot00000000000000module Sass::Script::Tree # A class representing a map literal. When resolved, this returns a # {Sass::Script::Node::Map}. class MapLiteral < Node # The key/value pairs that make up this map node. This isn't a Hash so that # we can detect key collisions once all the keys have been performed. # # @return [Array<(Node, Node)>] attr_reader :pairs # Creates a new map literal. # # @param pairs [Array<(Node, Node)>] See \{#pairs} def initialize(pairs) @pairs = pairs end # @see Node#children def children @pairs.flatten end # @see Node#to_sass def to_sass(opts = {}) return "()" if pairs.empty? to_sass = lambda do |value| if value.is_a?(ListLiteral) && value.separator == :comma "(#{value.to_sass(opts)})" else value.to_sass(opts) end end "(" + pairs.map {|(k, v)| "#{to_sass[k]}: #{to_sass[v]}"}.join(', ') + ")" end alias_method :inspect, :to_sass # @see Node#deep_copy def deep_copy node = dup node.instance_variable_set('@pairs', pairs.map {|(k, v)| [k.deep_copy, v.deep_copy]}) node end protected # @see Node#_perform def _perform(environment) keys = Set.new map = Sass::Script::Value::Map.new(Hash[pairs.map do |(k, v)| k, v = k.perform(environment), v.perform(environment) if keys.include?(k) raise Sass::SyntaxError.new("Duplicate key #{k.inspect} in map #{to_sass}.") end keys << k [k, v] end]) map.options = options map end end end ruby-sass-3.7.4/lib/sass/script/tree/node.rb000066400000000000000000000063241345125207600207230ustar00rootroot00000000000000module Sass::Script::Tree # The abstract superclass for SassScript parse tree nodes. # # Use \{#perform} to evaluate a parse tree. class Node # The options hash for this node. # # @return [{Symbol => Object}] attr_reader :options # The line of the document on which this node appeared. # # @return [Integer] attr_accessor :line # The source range in the document on which this node appeared. # # @return [Sass::Source::Range] attr_accessor :source_range # The file name of the document on which this node appeared. # # @return [String] attr_accessor :filename # Sets the options hash for this node, # as well as for all child nodes. # See {file:SASS_REFERENCE.md#Options the Sass options documentation}. # # @param options [{Symbol => Object}] The options def options=(options) @options = options children.each do |c| if c.is_a? Hash c.values.each {|v| v.options = options} else c.options = options end end end # Evaluates the node. # # \{#perform} shouldn't be overridden directly; # instead, override \{#\_perform}. # # @param environment [Sass::Environment] The environment in which to evaluate the SassScript # @return [Sass::Script::Value] The SassScript object that is the value of the SassScript def perform(environment) _perform(environment) rescue Sass::SyntaxError => e e.modify_backtrace(:line => line) raise e end # Returns all child nodes of this node. # # @return [Array] def children Sass::Util.abstract(self) end # Returns the text of this SassScript expression. # # @options opts :quote [String] # The preferred quote style for quoted strings. If `:none`, strings are # always emitted unquoted. # # @return [String] def to_sass(opts = {}) Sass::Util.abstract(self) end # Returns a deep clone of this node. # The child nodes are cloned, but options are not. # # @return [Node] def deep_copy Sass::Util.abstract(self) end # Forces any division operations with number literals in this expression to # do real division, rather than returning strings. def force_division! children.each {|c| c.force_division!} end protected # Converts underscores to dashes if the :dasherize option is set. def dasherize(s, opts) if opts[:dasherize] s.tr('_', '-') else s end end # Evaluates this node. # Note that all {Sass::Script::Value} objects created within this method # should have their \{#options} attribute set, probably via \{#opts}. # # @param environment [Sass::Environment] The environment in which to evaluate the SassScript # @return [Sass::Script::Value] The SassScript object that is the value of the SassScript # @see #perform def _perform(environment) Sass::Util.abstract(self) end # Sets the \{#options} field on the given value and returns it. # # @param value [Sass::Script::Value] # @return [Sass::Script::Value] def opts(value) value.options = options value end end end ruby-sass-3.7.4/lib/sass/script/tree/operation.rb000066400000000000000000000125351345125207600217770ustar00rootroot00000000000000module Sass::Script::Tree # A SassScript parse node representing a binary operation, # such as `$a + $b` or `"foo" + 1`. class Operation < Node @@color_arithmetic_deprecation = Sass::Deprecation.new @@unitless_equals_deprecation = Sass::Deprecation.new attr_reader :operand1 attr_reader :operand2 attr_reader :operator # @param operand1 [Sass::Script::Tree::Node] The parse-tree node # for the right-hand side of the operator # @param operand2 [Sass::Script::Tree::Node] The parse-tree node # for the left-hand side of the operator # @param operator [Symbol] The operator to perform. # This should be one of the binary operator names in {Sass::Script::Lexer::OPERATORS} def initialize(operand1, operand2, operator) @operand1 = operand1 @operand2 = operand2 @operator = operator super() end # @return [String] A human-readable s-expression representation of the operation def inspect "(#{@operator.inspect} #{@operand1.inspect} #{@operand2.inspect})" end # @see Node#to_sass def to_sass(opts = {}) o1 = operand_to_sass @operand1, :left, opts o2 = operand_to_sass @operand2, :right, opts sep = case @operator when :comma; ", " when :space; " " else; " #{Sass::Script::Lexer::OPERATORS_REVERSE[@operator]} " end "#{o1}#{sep}#{o2}" end # Returns the operands for this operation. # # @return [Array] # @see Node#children def children [@operand1, @operand2] end # @see Node#deep_copy def deep_copy node = dup node.instance_variable_set('@operand1', @operand1.deep_copy) node.instance_variable_set('@operand2', @operand2.deep_copy) node end protected # Evaluates the operation. # # @param environment [Sass::Environment] The environment in which to evaluate the SassScript # @return [Sass::Script::Value] The SassScript object that is the value of the operation # @raise [Sass::SyntaxError] if the operation is undefined for the operands def _perform(environment) value1 = @operand1.perform(environment) # Special-case :and and :or to support short-circuiting. if @operator == :and return value1.to_bool ? @operand2.perform(environment) : value1 elsif @operator == :or return value1.to_bool ? value1 : @operand2.perform(environment) end value2 = @operand2.perform(environment) if (value1.is_a?(Sass::Script::Value::Null) || value2.is_a?(Sass::Script::Value::Null)) && @operator != :eq && @operator != :neq raise Sass::SyntaxError.new( "Invalid null operation: \"#{value1.inspect} #{@operator} #{value2.inspect}\".") end begin result = opts(value1.send(@operator, value2)) rescue NoMethodError => e raise e unless e.name.to_s == @operator.to_s raise Sass::SyntaxError.new("Undefined operation: \"#{value1} #{@operator} #{value2}\".") end warn_for_color_arithmetic(value1, value2) warn_for_unitless_equals(value1, value2, result) result end private def warn_for_color_arithmetic(value1, value2) return unless @operator == :plus || @operator == :times || @operator == :minus || @operator == :div || @operator == :mod if value1.is_a?(Sass::Script::Value::Number) return unless value2.is_a?(Sass::Script::Value::Color) elsif value1.is_a?(Sass::Script::Value::Color) return unless value2.is_a?(Sass::Script::Value::Color) || value2.is_a?(Sass::Script::Value::Number) else return end @@color_arithmetic_deprecation.warn(filename, line, < quote) res = "" res << quote if quote != :none res << _to_sass(before, opts) res << '#{' << @mid.to_sass(opts.merge(:quote => nil)) << '}' res << _to_sass(after, opts) res << quote if quote != :none res end # Returns the three components of the interpolation, `before`, `mid`, and `after`. # # @return [Array] # @see #initialize # @see Node#children def children [@before, @mid, @after].compact end # @see Node#deep_copy def deep_copy node = dup node.instance_variable_set('@before', @before.deep_copy) if @before node.instance_variable_set('@mid', @mid.deep_copy) node.instance_variable_set('@after', @after.deep_copy) if @after node end protected # Evaluates the interpolation. # # @param environment [Sass::Environment] The environment in which to evaluate the SassScript # @return [Sass::Script::Value::String] # The SassScript string that is the value of the interpolation def _perform(environment) res = "" before = @before.perform(environment) res << before.value mid = @mid.perform(environment) res << (mid.is_a?(Sass::Script::Value::String) ? mid.value : mid.to_s(:quote => :none)) res << @after.perform(environment).value opts(Sass::Script::Value::String.new(res, before.type)) end private def _to_sass(string_or_interp, opts) result = string_or_interp.to_sass(opts) opts[:quote] == :none ? result : result.slice(1...-1) end def quote_for(string_or_interp) if string_or_interp.is_a?(Sass::Script::Tree::Literal) return nil if string_or_interp.value.value.empty? return '"' if string_or_interp.value.value.include?("'") return "'" if string_or_interp.value.value.include?('"') return nil end # Double-quotes take precedence over single quotes. before_quote = quote_for(string_or_interp.before) return '"' if before_quote == '"' after_quote = quote_for(string_or_interp.after) return '"' if after_quote == '"' # Returns "'" if either or both insist on single quotes, and nil # otherwise. before_quote || after_quote end end end ruby-sass-3.7.4/lib/sass/script/tree/unary_operation.rb000066400000000000000000000040631345125207600232120ustar00rootroot00000000000000module Sass::Script::Tree # A SassScript parse node representing a unary operation, # such as `-$b` or `not true`. # # Currently only `-`, `/`, and `not` are unary operators. class UnaryOperation < Node # @return [Symbol] The operation to perform attr_reader :operator # @return [Script::Node] The parse-tree node for the object of the operator attr_reader :operand # @param operand [Script::Node] See \{#operand} # @param operator [Symbol] See \{#operator} def initialize(operand, operator) @operand = operand @operator = operator super() end # @return [String] A human-readable s-expression representation of the operation def inspect "(#{@operator.inspect} #{@operand.inspect})" end # @see Node#to_sass def to_sass(opts = {}) operand = @operand.to_sass(opts) if @operand.is_a?(Operation) || (@operator == :minus && (operand =~ Sass::SCSS::RX::IDENT) == 0) operand = "(#{@operand.to_sass(opts)})" end op = Sass::Script::Lexer::OPERATORS_REVERSE[@operator] op + (op =~ /[a-z]/ ? " " : "") + operand end # Returns the operand of the operation. # # @return [Array] # @see Node#children def children [@operand] end # @see Node#deep_copy def deep_copy node = dup node.instance_variable_set('@operand', @operand.deep_copy) node end protected # Evaluates the operation. # # @param environment [Sass::Environment] The environment in which to evaluate the SassScript # @return [Sass::Script::Value] The SassScript object that is the value of the operation # @raise [Sass::SyntaxError] if the operation is undefined for the operand def _perform(environment) operator = "unary_#{@operator}" value = @operand.perform(environment) value.send(operator) rescue NoMethodError => e raise e unless e.name.to_s == operator.to_s raise Sass::SyntaxError.new("Undefined unary operation: \"#{@operator} #{value}\".") end end end ruby-sass-3.7.4/lib/sass/script/tree/variable.rb000066400000000000000000000025771345125207600215710ustar00rootroot00000000000000module Sass::Script::Tree # A SassScript parse node representing a variable. class Variable < Node # The name of the variable. # # @return [String] attr_reader :name # The underscored name of the variable. # # @return [String] attr_reader :underscored_name # @param name [String] See \{#name} def initialize(name) @name = name @underscored_name = name.tr("-", "_") super() end # @return [String] A string representation of the variable def inspect(opts = {}) "$#{dasherize(name, opts)}" end alias_method :to_sass, :inspect # Returns an empty array. # # @return [Array] empty # @see Node#children def children [] end # @see Node#deep_copy def deep_copy dup end protected # Evaluates the variable. # # @param environment [Sass::Environment] The environment in which to evaluate the SassScript # @return [Sass::Script::Value] The SassScript object that is the value of the variable # @raise [Sass::SyntaxError] if the variable is undefined def _perform(environment) val = environment.var(name) raise Sass::SyntaxError.new("Undefined variable: \"$#{name}\".") unless val if val.is_a?(Sass::Script::Value::Number) && val.original val = val.dup val.original = nil end val end end end ruby-sass-3.7.4/lib/sass/script/value.rb000066400000000000000000000006341345125207600201510ustar00rootroot00000000000000module Sass::Script::Value; end require 'sass/script/value/base' require 'sass/script/value/string' require 'sass/script/value/number' require 'sass/script/value/color' require 'sass/script/value/bool' require 'sass/script/value/null' require 'sass/script/value/list' require 'sass/script/value/arg_list' require 'sass/script/value/map' require 'sass/script/value/callable' require 'sass/script/value/function' ruby-sass-3.7.4/lib/sass/script/value/000077500000000000000000000000001345125207600176215ustar00rootroot00000000000000ruby-sass-3.7.4/lib/sass/script/value/arg_list.rb000066400000000000000000000022671345125207600217610ustar00rootroot00000000000000module Sass::Script::Value # A SassScript object representing a variable argument list. This works just # like a normal list, but can also contain keyword arguments. # # The keyword arguments attached to this list are unused except when this is # passed as a glob argument to a function or mixin. class ArgList < List # Whether \{#keywords} has been accessed. If so, we assume that all keywords # were valid for the function that created this ArgList. # # @return [Boolean] attr_accessor :keywords_accessed # Creates a new argument list. # # @param value [Array] See \{List#value}. # @param keywords [Hash, NormalizedMap] See \{#keywords} # @param separator [String] See \{List#separator}. def initialize(value, keywords, separator) super(value, separator: separator) if keywords.is_a?(Sass::Util::NormalizedMap) @keywords = keywords else @keywords = Sass::Util::NormalizedMap.new(keywords) end end # The keyword arguments attached to this list. # # @return [NormalizedMap] def keywords @keywords_accessed = true @keywords end end end ruby-sass-3.7.4/lib/sass/script/value/base.rb000066400000000000000000000204761345125207600210710ustar00rootroot00000000000000module Sass::Script::Value # The abstract superclass for SassScript objects. # # Many of these methods, especially the ones that correspond to SassScript operations, # are designed to be overridden by subclasses which may change the semantics somewhat. # The operations listed here are just the defaults. class Base # Returns the Ruby value of the value. # The type of this value varies based on the subclass. # # @return [Object] attr_reader :value # The source range in the document on which this node appeared. # # @return [Sass::Source::Range] attr_accessor :source_range # Creates a new value. # # @param value [Object] The object for \{#value} def initialize(value = nil) value.freeze unless value.nil? || value == true || value == false @value = value @options = nil end # Sets the options hash for this node, # as well as for all child nodes. # See {file:SASS_REFERENCE.md#Options the Sass options documentation}. # # @param options [{Symbol => Object}] The options attr_writer :options # Returns the options hash for this node. # # @return [{Symbol => Object}] # @raise [Sass::SyntaxError] if the options hash hasn't been set. # This should only happen when the value was created # outside of the parser and \{#to\_s} was called on it def options return @options if @options raise Sass::SyntaxError.new(< :none) + other.to_s(:quote => :none), type) end # The SassScript `-` operation. # # @param other [Value] The right-hand side of the operator # @return [Script::Value::String] A string containing both values # separated by `"-"` def minus(other) Sass::Script::Value::String.new("#{self}-#{other}") end # The SassScript `/` operation. # # @param other [Value] The right-hand side of the operator # @return [Script::Value::String] A string containing both values # separated by `"/"` def div(other) Sass::Script::Value::String.new("#{self}/#{other}") end # The SassScript unary `+` operation (e.g. `+$a`). # # @param other [Value] The right-hand side of the operator # @return [Script::Value::String] A string containing the value # preceded by `"+"` def unary_plus Sass::Script::Value::String.new("+#{self}") end # The SassScript unary `-` operation (e.g. `-$a`). # # @param other [Value] The right-hand side of the operator # @return [Script::Value::String] A string containing the value # preceded by `"-"` def unary_minus Sass::Script::Value::String.new("-#{self}") end # The SassScript unary `/` operation (e.g. `/$a`). # # @param other [Value] The right-hand side of the operator # @return [Script::Value::String] A string containing the value # preceded by `"/"` def unary_div Sass::Script::Value::String.new("/#{self}") end # Returns the hash code of this value. Two objects' hash codes should be # equal if the objects are equal. # # @return [Integer for Ruby 2.4.0+, Fixnum for earlier Ruby versions] The hash code. def hash value.hash end def eql?(other) self == other end # @return [String] A readable representation of the value def inspect value.inspect end # @return [Boolean] `true` (the Ruby boolean value) def to_bool true end # Compares this object with another. # # @param other [Object] The object to compare with # @return [Boolean] Whether or not this value is equivalent to `other` def ==(other) eq(other).to_bool end # @return [Integer] The integer value of this value # @raise [Sass::SyntaxError] if this value isn't an integer def to_i raise Sass::SyntaxError.new("#{inspect} is not an integer.") end # @raise [Sass::SyntaxError] if this value isn't an integer def assert_int!; to_i; end # Returns the separator for this value. For non-list-like values or the # empty list, this will be `nil`. For lists or maps, it will be `:space` or # `:comma`. # # @return [Symbol] def separator; nil; end # Whether the value is surrounded by square brackets. For non-list values, # this will be `false`. # # @return [Boolean] def bracketed; false; end # Returns the value of this value as a list. # Single values are considered the same as single-element lists. # # @return [Array] This value as a list def to_a [self] end # Returns the value of this value as a hash. Most values don't have hash # representations, but [Map]s and empty [List]s do. # # @return [Hash] This value as a hash # @raise [Sass::SyntaxError] if this value doesn't have a hash representation def to_h raise Sass::SyntaxError.new("#{inspect} is not a map.") end # Returns the string representation of this value # as it would be output to the CSS document. # # @options opts :quote [String] # The preferred quote style for quoted strings. If `:none`, strings are # always emitted unquoted. # @return [String] def to_s(opts = {}) Sass::Util.abstract(self) end alias_method :to_sass, :to_s # Returns whether or not this object is null. # # @return [Boolean] `false` def null? false end # Creates a new list containing `contents` but with the same brackets and # separators as this object, when interpreted as a list. # # @param contents [Array] The contents of the new list. # @param separator [Symbol] The separator of the new list. Defaults to \{#separator}. # @param bracketed [Boolean] Whether the new list is bracketed. Defaults to \{#bracketed}. # @return [Sass::Script::Value::List] def with_contents(contents, separator: self.separator, bracketed: self.bracketed) Sass::Script::Value::List.new(contents, separator: separator, bracketed: bracketed) end protected # Evaluates the value. # # @param environment [Sass::Environment] The environment in which to evaluate the SassScript # @return [Value] This value def _perform(environment) self end end end ruby-sass-3.7.4/lib/sass/script/value/bool.rb000066400000000000000000000020151345125207600210770ustar00rootroot00000000000000module Sass::Script::Value # A SassScript object representing a boolean (true or false) value. class Bool < Base # The true value in SassScript. # # This is assigned before new is overridden below so that we use the default implementation. TRUE = new(true) # The false value in SassScript. # # This is assigned before new is overridden below so that we use the default implementation. FALSE = new(false) # We override object creation so that users of the core API # will not need to know that booleans are specific constants. # # @param value A ruby value that will be tested for truthiness. # @return [Bool] TRUE if value is truthy, FALSE if value is falsey def self.new(value) value ? TRUE : FALSE end # The Ruby value of the boolean. # # @return [Boolean] attr_reader :value alias_method :to_bool, :value # @return [String] "true" or "false" def to_s(opts = {}) @value.to_s end alias_method :to_sass, :to_s end end ruby-sass-3.7.4/lib/sass/script/value/callable.rb000066400000000000000000000010461345125207600217060ustar00rootroot00000000000000module Sass::Script::Value # A SassScript object representing a null value. class Callable < Base # Constructs a Callable value for use in SassScript. # # @param callable [Sass::Callable] The callable to be used when the # callable is called. def initialize(callable) super(callable) end def to_s(opts = {}) raise Sass::SyntaxError.new("#{to_sass} isn't a valid CSS value.") end def inspect to_sass end # @abstract def to_sass Sass::Util.abstract(self) end end end ruby-sass-3.7.4/lib/sass/script/value/color.rb000066400000000000000000000576771345125207600213120ustar00rootroot00000000000000module Sass::Script::Value # A SassScript object representing a CSS color. # # A color may be represented internally as RGBA, HSLA, or both. # It's originally represented as whatever its input is; # if it's created with RGB values, it's represented as RGBA, # and if it's created with HSL values, it's represented as HSLA. # Once a property is accessed that requires the other representation -- # for example, \{#red} for an HSL color -- # that component is calculated and cached. # # The alpha channel of a color is independent of its RGB or HSL representation. # It's always stored, as 1 if nothing else is specified. # If only the alpha channel is modified using \{#with}, # the cached RGB and HSL values are retained. class Color < Base # @private # # Convert a ruby integer to a rgba components # @param color [Integer] # @return [Array] Array of 4 numbers representing r,g,b and alpha def self.int_to_rgba(color) rgba = (0..3).map {|n| color >> (n << 3) & 0xff}.reverse rgba[-1] = rgba[-1] / 255.0 rgba end ALTERNATE_COLOR_NAMES = Sass::Util.map_vals( { 'aqua' => 0x00FFFFFF, 'darkgrey' => 0xA9A9A9FF, 'darkslategrey' => 0x2F4F4FFF, 'dimgrey' => 0x696969FF, 'fuchsia' => 0xFF00FFFF, 'grey' => 0x808080FF, 'lightgrey' => 0xD3D3D3FF, 'lightslategrey' => 0x778899FF, 'slategrey' => 0x708090FF, }, &method(:int_to_rgba)) # A hash from color names to `[red, green, blue]` value arrays. COLOR_NAMES = Sass::Util.map_vals( { 'aliceblue' => 0xF0F8FFFF, 'antiquewhite' => 0xFAEBD7FF, 'aquamarine' => 0x7FFFD4FF, 'azure' => 0xF0FFFFFF, 'beige' => 0xF5F5DCFF, 'bisque' => 0xFFE4C4FF, 'black' => 0x000000FF, 'blanchedalmond' => 0xFFEBCDFF, 'blue' => 0x0000FFFF, 'blueviolet' => 0x8A2BE2FF, 'brown' => 0xA52A2AFF, 'burlywood' => 0xDEB887FF, 'cadetblue' => 0x5F9EA0FF, 'chartreuse' => 0x7FFF00FF, 'chocolate' => 0xD2691EFF, 'coral' => 0xFF7F50FF, 'cornflowerblue' => 0x6495EDFF, 'cornsilk' => 0xFFF8DCFF, 'crimson' => 0xDC143CFF, 'cyan' => 0x00FFFFFF, 'darkblue' => 0x00008BFF, 'darkcyan' => 0x008B8BFF, 'darkgoldenrod' => 0xB8860BFF, 'darkgray' => 0xA9A9A9FF, 'darkgreen' => 0x006400FF, 'darkkhaki' => 0xBDB76BFF, 'darkmagenta' => 0x8B008BFF, 'darkolivegreen' => 0x556B2FFF, 'darkorange' => 0xFF8C00FF, 'darkorchid' => 0x9932CCFF, 'darkred' => 0x8B0000FF, 'darksalmon' => 0xE9967AFF, 'darkseagreen' => 0x8FBC8FFF, 'darkslateblue' => 0x483D8BFF, 'darkslategray' => 0x2F4F4FFF, 'darkturquoise' => 0x00CED1FF, 'darkviolet' => 0x9400D3FF, 'deeppink' => 0xFF1493FF, 'deepskyblue' => 0x00BFFFFF, 'dimgray' => 0x696969FF, 'dodgerblue' => 0x1E90FFFF, 'firebrick' => 0xB22222FF, 'floralwhite' => 0xFFFAF0FF, 'forestgreen' => 0x228B22FF, 'gainsboro' => 0xDCDCDCFF, 'ghostwhite' => 0xF8F8FFFF, 'gold' => 0xFFD700FF, 'goldenrod' => 0xDAA520FF, 'gray' => 0x808080FF, 'green' => 0x008000FF, 'greenyellow' => 0xADFF2FFF, 'honeydew' => 0xF0FFF0FF, 'hotpink' => 0xFF69B4FF, 'indianred' => 0xCD5C5CFF, 'indigo' => 0x4B0082FF, 'ivory' => 0xFFFFF0FF, 'khaki' => 0xF0E68CFF, 'lavender' => 0xE6E6FAFF, 'lavenderblush' => 0xFFF0F5FF, 'lawngreen' => 0x7CFC00FF, 'lemonchiffon' => 0xFFFACDFF, 'lightblue' => 0xADD8E6FF, 'lightcoral' => 0xF08080FF, 'lightcyan' => 0xE0FFFFFF, 'lightgoldenrodyellow' => 0xFAFAD2FF, 'lightgreen' => 0x90EE90FF, 'lightgray' => 0xD3D3D3FF, 'lightpink' => 0xFFB6C1FF, 'lightsalmon' => 0xFFA07AFF, 'lightseagreen' => 0x20B2AAFF, 'lightskyblue' => 0x87CEFAFF, 'lightslategray' => 0x778899FF, 'lightsteelblue' => 0xB0C4DEFF, 'lightyellow' => 0xFFFFE0FF, 'lime' => 0x00FF00FF, 'limegreen' => 0x32CD32FF, 'linen' => 0xFAF0E6FF, 'magenta' => 0xFF00FFFF, 'maroon' => 0x800000FF, 'mediumaquamarine' => 0x66CDAAFF, 'mediumblue' => 0x0000CDFF, 'mediumorchid' => 0xBA55D3FF, 'mediumpurple' => 0x9370DBFF, 'mediumseagreen' => 0x3CB371FF, 'mediumslateblue' => 0x7B68EEFF, 'mediumspringgreen' => 0x00FA9AFF, 'mediumturquoise' => 0x48D1CCFF, 'mediumvioletred' => 0xC71585FF, 'midnightblue' => 0x191970FF, 'mintcream' => 0xF5FFFAFF, 'mistyrose' => 0xFFE4E1FF, 'moccasin' => 0xFFE4B5FF, 'navajowhite' => 0xFFDEADFF, 'navy' => 0x000080FF, 'oldlace' => 0xFDF5E6FF, 'olive' => 0x808000FF, 'olivedrab' => 0x6B8E23FF, 'orange' => 0xFFA500FF, 'orangered' => 0xFF4500FF, 'orchid' => 0xDA70D6FF, 'palegoldenrod' => 0xEEE8AAFF, 'palegreen' => 0x98FB98FF, 'paleturquoise' => 0xAFEEEEFF, 'palevioletred' => 0xDB7093FF, 'papayawhip' => 0xFFEFD5FF, 'peachpuff' => 0xFFDAB9FF, 'peru' => 0xCD853FFF, 'pink' => 0xFFC0CBFF, 'plum' => 0xDDA0DDFF, 'powderblue' => 0xB0E0E6FF, 'purple' => 0x800080FF, 'red' => 0xFF0000FF, 'rebeccapurple' => 0x663399FF, 'rosybrown' => 0xBC8F8FFF, 'royalblue' => 0x4169E1FF, 'saddlebrown' => 0x8B4513FF, 'salmon' => 0xFA8072FF, 'sandybrown' => 0xF4A460FF, 'seagreen' => 0x2E8B57FF, 'seashell' => 0xFFF5EEFF, 'sienna' => 0xA0522DFF, 'silver' => 0xC0C0C0FF, 'skyblue' => 0x87CEEBFF, 'slateblue' => 0x6A5ACDFF, 'slategray' => 0x708090FF, 'snow' => 0xFFFAFAFF, 'springgreen' => 0x00FF7FFF, 'steelblue' => 0x4682B4FF, 'tan' => 0xD2B48CFF, 'teal' => 0x008080FF, 'thistle' => 0xD8BFD8FF, 'tomato' => 0xFF6347FF, 'transparent' => 0x00000000, 'turquoise' => 0x40E0D0FF, 'violet' => 0xEE82EEFF, 'wheat' => 0xF5DEB3FF, 'white' => 0xFFFFFFFF, 'whitesmoke' => 0xF5F5F5FF, 'yellow' => 0xFFFF00FF, 'yellowgreen' => 0x9ACD32FF }, &method(:int_to_rgba)) # A hash from `[red, green, blue, alpha]` value arrays to color names. COLOR_NAMES_REVERSE = COLOR_NAMES.invert.freeze # We add the alternate color names after inverting because # different ruby implementations and versions vary on the ordering of the result of invert. COLOR_NAMES.update(ALTERNATE_COLOR_NAMES).freeze # The user's original representation of the color. # # @return [String] attr_reader :representation # Constructs an RGB or HSL color object, # optionally with an alpha channel. # # RGB values are clipped within 0 and 255. # Saturation and lightness values are clipped within 0 and 100. # The alpha value is clipped within 0 and 1. # # @raise [Sass::SyntaxError] if any color value isn't in the specified range # # @overload initialize(attrs) # The attributes are specified as a hash. This hash must contain either # `:hue`, `:saturation`, and `:lightness` keys, or `:red`, `:green`, and # `:blue` keys. It cannot contain both HSL and RGB keys. It may also # optionally contain an `:alpha` key, and a `:representation` key # indicating the original representation of the color that the user wrote # in their stylesheet. # # @param attrs [{Symbol => Numeric}] A hash of color attributes to values # @raise [ArgumentError] if not enough attributes are specified, # or both RGB and HSL attributes are specified # # @overload initialize(rgba, [representation]) # The attributes are specified as an array. # This overload only supports RGB or RGBA colors. # # @param rgba [Array] A three- or four-element array # of the red, green, blue, and optionally alpha values (respectively) # of the color # @param representation [String] The original representation of the color # that the user wrote in their stylesheet. # @raise [ArgumentError] if not enough attributes are specified def initialize(attrs, representation = nil, allow_both_rgb_and_hsl = false) super(nil) if attrs.is_a?(Array) unless (3..4).include?(attrs.size) raise ArgumentError.new("Color.new(array) expects a three- or four-element array") end red, green, blue = attrs[0...3].map {|c| Sass::Util.round(c)} @attrs = {:red => red, :green => green, :blue => blue} @attrs[:alpha] = attrs[3] ? attrs[3].to_f : 1 @representation = representation else attrs = attrs.reject {|_k, v| v.nil?} hsl = [:hue, :saturation, :lightness] & attrs.keys rgb = [:red, :green, :blue] & attrs.keys if !allow_both_rgb_and_hsl && !hsl.empty? && !rgb.empty? raise ArgumentError.new("Color.new(hash) may not have both HSL and RGB keys specified") elsif hsl.empty? && rgb.empty? raise ArgumentError.new("Color.new(hash) must have either HSL or RGB keys specified") elsif !hsl.empty? && hsl.size != 3 raise ArgumentError.new("Color.new(hash) must have all three HSL values specified") elsif !rgb.empty? && rgb.size != 3 raise ArgumentError.new("Color.new(hash) must have all three RGB values specified") end @attrs = attrs @attrs[:hue] %= 360 if @attrs[:hue] @attrs[:alpha] ||= 1 @representation = @attrs.delete(:representation) end [:red, :green, :blue].each do |k| next if @attrs[k].nil? @attrs[k] = Sass::Util.restrict(Sass::Util.round(@attrs[k]), 0..255) end [:saturation, :lightness].each do |k| next if @attrs[k].nil? @attrs[k] = Sass::Util.restrict(@attrs[k], 0..100) end @attrs[:alpha] = Sass::Util.restrict(@attrs[:alpha], 0..1) end # Create a new color from a valid CSS hex string. # # The leading hash is optional. # # @return [Color] def self.from_hex(hex_string, alpha = nil) unless hex_string =~ /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/i || hex_string =~ /^#?([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])?$/i raise ArgumentError.new("#{hex_string.inspect} is not a valid hex color.") end red = $1.ljust(2, $1).to_i(16) green = $2.ljust(2, $2).to_i(16) blue = $3.ljust(2, $3).to_i(16) alpha = $4.ljust(2, $4).to_i(16).to_f / 0xff if $4 hex_string = "##{hex_string}" unless hex_string[0] == ?# attrs = {:red => red, :green => green, :blue => blue, :representation => hex_string} attrs[:alpha] = alpha if alpha new(attrs) end # The red component of the color. # # @return [Integer] def red hsl_to_rgb! @attrs[:red] end # The green component of the color. # # @return [Integer] def green hsl_to_rgb! @attrs[:green] end # The blue component of the color. # # @return [Integer] def blue hsl_to_rgb! @attrs[:blue] end # The hue component of the color. # # @return [Numeric] def hue rgb_to_hsl! @attrs[:hue] end # The saturation component of the color. # # @return [Numeric] def saturation rgb_to_hsl! @attrs[:saturation] end # The lightness component of the color. # # @return [Numeric] def lightness rgb_to_hsl! @attrs[:lightness] end # The alpha channel (opacity) of the color. # This is 1 unless otherwise defined. # # @return [Integer] def alpha @attrs[:alpha].to_f end # Returns whether this color object is translucent; # that is, whether the alpha channel is non-1. # # @return [Boolean] def alpha? alpha < 1 end # Returns the red, green, and blue components of the color. # # @return [Array] A frozen three-element array of the red, green, and blue # values (respectively) of the color def rgb [red, green, blue].freeze end # Returns the red, green, blue, and alpha components of the color. # # @return [Array] A frozen four-element array of the red, green, # blue, and alpha values (respectively) of the color def rgba [red, green, blue, alpha].freeze end # Returns the hue, saturation, and lightness components of the color. # # @return [Array] A frozen three-element array of the # hue, saturation, and lightness values (respectively) of the color def hsl [hue, saturation, lightness].freeze end # Returns the hue, saturation, lightness, and alpha components of the color. # # @return [Array] A frozen four-element array of the hue, # saturation, lightness, and alpha values (respectively) of the color def hsla [hue, saturation, lightness, alpha].freeze end # The SassScript `==` operation. # **Note that this returns a {Sass::Script::Value::Bool} object, # not a Ruby boolean**. # # @param other [Value] The right-hand side of the operator # @return [Bool] True if this value is the same as the other, # false otherwise def eq(other) Sass::Script::Value::Bool.new( other.is_a?(Color) && rgb == other.rgb && alpha == other.alpha) end def hash [rgb, alpha].hash end # Returns a copy of this color with one or more channels changed. # RGB or HSL colors may be changed, but not both at once. # # For example: # # Color.new([10, 20, 30]).with(:blue => 40) # #=> rgb(10, 40, 30) # Color.new([126, 126, 126]).with(:red => 0, :green => 255) # #=> rgb(0, 255, 126) # Color.new([255, 0, 127]).with(:saturation => 60) # #=> rgb(204, 51, 127) # Color.new([1, 2, 3]).with(:alpha => 0.4) # #=> rgba(1, 2, 3, 0.4) # # @param attrs [{Symbol => Numeric}] # A map of channel names (`:red`, `:green`, `:blue`, # `:hue`, `:saturation`, `:lightness`, or `:alpha`) to values # @return [Color] The new Color object # @raise [ArgumentError] if both RGB and HSL keys are specified def with(attrs) attrs = attrs.reject {|_k, v| v.nil?} hsl = !([:hue, :saturation, :lightness] & attrs.keys).empty? rgb = !([:red, :green, :blue] & attrs.keys).empty? if hsl && rgb raise ArgumentError.new("Cannot specify HSL and RGB values for a color at the same time") end if hsl [:hue, :saturation, :lightness].each {|k| attrs[k] ||= send(k)} elsif rgb [:red, :green, :blue].each {|k| attrs[k] ||= send(k)} else # If we're just changing the alpha channel, # keep all the HSL/RGB stuff we've calculated attrs = @attrs.merge(attrs) end attrs[:alpha] ||= alpha Color.new(attrs, nil, :allow_both_rgb_and_hsl) end # The SassScript `+` operation. # Its functionality depends on the type of its argument: # # {Number} # : Adds the number to each of the RGB color channels. # # {Color} # : Adds each of the RGB color channels together. # # {Value} # : See {Value::Base#plus}. # # @param other [Value] The right-hand side of the operator # @return [Color] The resulting color # @raise [Sass::SyntaxError] if `other` is a number with units def plus(other) if other.is_a?(Sass::Script::Value::Number) || other.is_a?(Sass::Script::Value::Color) piecewise(other, :+) else super end end # The SassScript `-` operation. # Its functionality depends on the type of its argument: # # {Number} # : Subtracts the number from each of the RGB color channels. # # {Color} # : Subtracts each of the other color's RGB color channels from this color's. # # {Value} # : See {Value::Base#minus}. # # @param other [Value] The right-hand side of the operator # @return [Color] The resulting color # @raise [Sass::SyntaxError] if `other` is a number with units def minus(other) if other.is_a?(Sass::Script::Value::Number) || other.is_a?(Sass::Script::Value::Color) piecewise(other, :-) else super end end # The SassScript `*` operation. # Its functionality depends on the type of its argument: # # {Number} # : Multiplies the number by each of the RGB color channels. # # {Color} # : Multiplies each of the RGB color channels together. # # @param other [Number, Color] The right-hand side of the operator # @return [Color] The resulting color # @raise [Sass::SyntaxError] if `other` is a number with units def times(other) if other.is_a?(Sass::Script::Value::Number) || other.is_a?(Sass::Script::Value::Color) piecewise(other, :*) else raise NoMethodError.new(nil, :times) end end # The SassScript `/` operation. # Its functionality depends on the type of its argument: # # {Number} # : Divides each of the RGB color channels by the number. # # {Color} # : Divides each of this color's RGB color channels by the other color's. # # {Value} # : See {Value::Base#div}. # # @param other [Value] The right-hand side of the operator # @return [Color] The resulting color # @raise [Sass::SyntaxError] if `other` is a number with units def div(other) if other.is_a?(Sass::Script::Value::Number) || other.is_a?(Sass::Script::Value::Color) piecewise(other, :/) else super end end # The SassScript `%` operation. # Its functionality depends on the type of its argument: # # {Number} # : Takes each of the RGB color channels module the number. # # {Color} # : Takes each of this color's RGB color channels modulo the other color's. # # @param other [Number, Color] The right-hand side of the operator # @return [Color] The resulting color # @raise [Sass::SyntaxError] if `other` is a number with units def mod(other) if other.is_a?(Sass::Script::Value::Number) || other.is_a?(Sass::Script::Value::Color) piecewise(other, :%) else raise NoMethodError.new(nil, :mod) end end # Returns a string representation of the color. # This is usually the color's hex value, # but if the color has a name that's used instead. # # @return [String] The string representation def to_s(opts = {}) return smallest if options[:style] == :compressed return representation if representation # IE10 doesn't properly support the color name "transparent", so we emit # generated transparent colors as rgba(0, 0, 0, 0) in favor of that. See # #1782. return rgba_str if Number.basically_equal?(alpha, 0) return name if name alpha? ? rgba_str : hex_str end alias_method :to_sass, :to_s # Returns a string representation of the color. # # @return [String] The hex value def inspect alpha? ? rgba_str : hex_str end # Returns the color's name, if it has one. # # @return [String, nil] def name COLOR_NAMES_REVERSE[rgba] end private def smallest small_explicit_str = alpha? ? rgba_str : hex_str.gsub(/^#(.)\1(.)\2(.)\3$/, '#\1\2\3') [representation, COLOR_NAMES_REVERSE[rgba], small_explicit_str]. compact.min_by {|str| str.size} end def rgba_str split = options[:style] == :compressed ? ',' : ', ' "rgba(#{rgb.join(split)}#{split}#{Number.round(alpha)})" end def hex_str red, green, blue = rgb.map {|num| num.to_s(16).rjust(2, '0')} "##{red}#{green}#{blue}" end def operation_name(operation) case operation when :+ "add" when :- "subtract" when :* "multiply" when :/ "divide" when :% "modulo" end end def piecewise(other, operation) other_num = other.is_a? Number if other_num && !other.unitless? raise Sass::SyntaxError.new( "Cannot #{operation_name(operation)} a number with units (#{other}) to a color (#{self})." ) end result = [] (0...3).each do |i| res = rgb[i].to_f.send(operation, other_num ? other.value : other.rgb[i]) result[i] = [[res, 255].min, 0].max end if !other_num && other.alpha != alpha raise Sass::SyntaxError.new("Alpha channels must be equal: #{self} #{operation} #{other}") end with(:red => result[0], :green => result[1], :blue => result[2]) end def hsl_to_rgb! return if @attrs[:red] && @attrs[:blue] && @attrs[:green] h = @attrs[:hue] / 360.0 s = @attrs[:saturation] / 100.0 l = @attrs[:lightness] / 100.0 # Algorithm from the CSS3 spec: http://www.w3.org/TR/css3-color/#hsl-color. m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s m1 = l * 2 - m2 @attrs[:red], @attrs[:green], @attrs[:blue] = [ hue_to_rgb(m1, m2, h + 1.0 / 3), hue_to_rgb(m1, m2, h), hue_to_rgb(m1, m2, h - 1.0 / 3) ].map {|c| Sass::Util.round(c * 0xff)} end def hue_to_rgb(m1, m2, h) h += 1 if h < 0 h -= 1 if h > 1 return m1 + (m2 - m1) * h * 6 if h * 6 < 1 return m2 if h * 2 < 1 return m1 + (m2 - m1) * (2.0 / 3 - h) * 6 if h * 3 < 2 m1 end def rgb_to_hsl! return if @attrs[:hue] && @attrs[:saturation] && @attrs[:lightness] r, g, b = [:red, :green, :blue].map {|k| @attrs[k] / 255.0} # Algorithm from http://en.wikipedia.org/wiki/HSL_and_HSV#Conversion_from_RGB_to_HSL_or_HSV max = [r, g, b].max min = [r, g, b].min d = max - min h = case max when min; 0 when r; 60 * (g - b) / d when g; 60 * (b - r) / d + 120 when b; 60 * (r - g) / d + 240 end l = (max + min) / 2.0 s = if max == min 0 elsif l < 0.5 d / (2 * l) else d / (2 - 2 * l) end @attrs[:hue] = h % 360 @attrs[:saturation] = s * 100 @attrs[:lightness] = l * 100 end end end ruby-sass-3.7.4/lib/sass/script/value/function.rb000066400000000000000000000010051345125207600217670ustar00rootroot00000000000000module Sass::Script::Value # A SassScript object representing a function. class Function < Callable # Constructs a Function value for use in SassScript. # # @param function [Sass::Callable] The callable to be used when the # function is invoked. def initialize(function) unless function.type == "function" raise ArgumentError.new("A callable of type function was expected.") end super end def to_sass %{get-function("#{value.name}")} end end end ruby-sass-3.7.4/lib/sass/script/value/helpers.rb000066400000000000000000000271661345125207600216240ustar00rootroot00000000000000module Sass::Script::Value # Provides helper functions for creating sass values from within ruby methods. # @since `3.3.0` module Helpers # Construct a Sass Boolean. # # @param value [Object] A ruby object that will be tested for truthiness. # @return [Sass::Script::Value::Bool] whether the ruby value is truthy. def bool(value) Bool.new(value) end # Construct a Sass Color from a hex color string. # # @param value [::String] A string representing a hex color. # The leading hash ("#") is optional. # @param alpha [::Number] The alpha channel. A number between 0 and 1. # @return [Sass::Script::Value::Color] the color object def hex_color(value, alpha = nil) Color.from_hex(value, alpha) end # Construct a Sass Color from hsl values. # # @param hue [::Number] The hue of the color in degrees. # A non-negative number, usually less than 360. # @param saturation [::Number] The saturation of the color. # Must be between 0 and 100 inclusive. # @param lightness [::Number] The lightness of the color. # Must be between 0 and 100 inclusive. # @param alpha [::Number] The alpha channel. A number between 0 and 1. # # @return [Sass::Script::Value::Color] the color object def hsl_color(hue, saturation, lightness, alpha = nil) attrs = {:hue => hue, :saturation => saturation, :lightness => lightness} attrs[:alpha] = alpha if alpha Color.new(attrs) end # Construct a Sass Color from rgb values. # # @param red [::Number] The red component. Must be between 0 and 255 inclusive. # @param green [::Number] The green component. Must be between 0 and 255 inclusive. # @param blue [::Number] The blue component. Must be between 0 and 255 inclusive. # @param alpha [::Number] The alpha channel. A number between 0 and 1. # # @return [Sass::Script::Value::Color] the color object def rgb_color(red, green, blue, alpha = nil) attrs = {:red => red, :green => green, :blue => blue} attrs[:alpha] = alpha if alpha Color.new(attrs) end # Construct a Sass Number from a ruby number. # # @param number [::Number] A numeric value. # @param unit_string [::String] A unit string of the form # `numeral_unit1 * numeral_unit2 ... / denominator_unit1 * denominator_unit2 ...` # this is the same format that is returned by # {Sass::Script::Value::Number#unit_str the `unit_str` method} # # @see Sass::Script::Value::Number#unit_str # # @return [Sass::Script::Value::Number] The sass number representing the given ruby number. def number(number, unit_string = nil) Number.new(number, *parse_unit_string(unit_string)) end # @overload list(*elements, separator:, bracketed: false) # Create a space-separated list from the arguments given. # @param elements [Array] Each argument will be a list element. # @param separator [Symbol] Either :space or :comma. # @param bracketed [Boolean] Whether the list uses square brackets. # @return [Sass::Script::Value::List] The space separated list. # # @overload list(array, separator:, bracketed: false) # Create a space-separated list from the array given. # @param array [Array] A ruby array of Sass values # to make into a list. # @param separator [Symbol] Either :space or :comma. # @param bracketed [Boolean] Whether the list uses square brackets. # @return [Sass::Script::Value::List] The space separated list. def list(*elements, separator: nil, bracketed: false) # Support passing separator as the last value in elements for # backwards-compatibility. if separator.nil? if elements.last.is_a?(Symbol) separator = elements.pop else raise ArgumentError.new("A separator of :space or :comma must be specified.") end end if elements.size == 1 && elements.first.is_a?(Array) elements = elements.first end Sass::Script::Value::List.new(elements, separator: separator, bracketed: bracketed) end # Construct a Sass map. # # @param hash [Hash] A Ruby map to convert to a Sass map. # @return [Sass::Script::Value::Map] The map. def map(hash) Map.new(hash) end # Create a sass null value. # # @return [Sass::Script::Value::Null] def null Sass::Script::Value::Null.new end # Create a quoted string. # # @param str [::String] A ruby string. # @return [Sass::Script::Value::String] A quoted string. def quoted_string(str) Sass::Script::String.new(str, :string) end # Create an unquoted string. # # @param str [::String] A ruby string. # @return [Sass::Script::Value::String] An unquoted string. def unquoted_string(str) Sass::Script::String.new(str, :identifier) end alias_method :identifier, :unquoted_string # Parses a user-provided selector. # # @param value [Sass::Script::Value::String, Sass::Script::Value::List] # The selector to parse. This can be either a string, a list of # strings, or a list of lists of strings as returned by `&`. # @param name [Symbol, nil] # If provided, the name of the selector argument. This is used # for error reporting. # @param allow_parent_ref [Boolean] # Whether the parsed selector should allow parent references. # @return [Sass::Selector::CommaSequence] The parsed selector. # @throw [ArgumentError] if the parse failed for any reason. def parse_selector(value, name = nil, allow_parent_ref = false) str = normalize_selector(value, name) begin Sass::SCSS::StaticParser.new(str, nil, nil, 1, 1, allow_parent_ref).parse_selector rescue Sass::SyntaxError => e err = "#{value.inspect} is not a valid selector: #{e}" err = "$#{name.to_s.tr('_', '-')}: #{err}" if name raise ArgumentError.new(err) end end # Parses a user-provided complex selector. # # A complex selector can contain combinators but cannot contain commas. # # @param value [Sass::Script::Value::String, Sass::Script::Value::List] # The selector to parse. This can be either a string or a list of # strings. # @param name [Symbol, nil] # If provided, the name of the selector argument. This is used # for error reporting. # @param allow_parent_ref [Boolean] # Whether the parsed selector should allow parent references. # @return [Sass::Selector::Sequence] The parsed selector. # @throw [ArgumentError] if the parse failed for any reason. def parse_complex_selector(value, name = nil, allow_parent_ref = false) selector = parse_selector(value, name, allow_parent_ref) return seq if selector.members.length == 1 err = "#{value.inspect} is not a complex selector" err = "$#{name.to_s.tr('_', '-')}: #{err}" if name raise ArgumentError.new(err) end # Parses a user-provided compound selector. # # A compound selector cannot contain combinators or commas. # # @param value [Sass::Script::Value::String] The selector to parse. # @param name [Symbol, nil] # If provided, the name of the selector argument. This is used # for error reporting. # @param allow_parent_ref [Boolean] # Whether the parsed selector should allow parent references. # @return [Sass::Selector::SimpleSequence] The parsed selector. # @throw [ArgumentError] if the parse failed for any reason. def parse_compound_selector(value, name = nil, allow_parent_ref = false) assert_type value, :String, name selector = parse_selector(value, name, allow_parent_ref) seq = selector.members.first sseq = seq.members.first if selector.members.length == 1 && seq.members.length == 1 && sseq.is_a?(Sass::Selector::SimpleSequence) return sseq end err = "#{value.inspect} is not a compound selector" err = "$#{name.to_s.tr('_', '-')}: #{err}" if name raise ArgumentError.new(err) end # Returns true when the literal is a string containing a calc(). # # Use \{#special_number?} in preference to this. # # @param literal [Sass::Script::Value::Base] The value to check # @return Boolean def calc?(literal) literal.is_a?(Sass::Script::Value::String) && literal.value =~ /calc\(/ end # Returns true when the literal is a string containing a var(). # # @param literal [Sass::Script::Value::Base] The value to check # @return Boolean def var?(literal) literal.is_a?(Sass::Script::Value::String) && literal.value =~ /var\(/ end # Returns whether the literal is a special CSS value that may evaluate to a # number, such as `calc()` or `var()`. # # @param literal [Sass::Script::Value::Base] The value to check # @return Boolean def special_number?(literal) literal.is_a?(Sass::Script::Value::String) && literal.value =~ /(calc|var)\(/ end private # Converts a user-provided selector into string form or throws an # ArgumentError if it's in an invalid format. def normalize_selector(value, name) if (str = selector_to_str(value)) return str end err = "#{value.inspect} is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings" err = "$#{name.to_s.tr('_', '-')}: #{err}" if name raise ArgumentError.new(err) end # Converts a user-provided selector into string form or returns # `nil` if it's in an invalid format. def selector_to_str(value) return value.value if value.is_a?(Sass::Script::String) return unless value.is_a?(Sass::Script::List) if value.separator == :comma return value.to_a.map do |complex| next complex.value if complex.is_a?(Sass::Script::String) return unless complex.is_a?(Sass::Script::List) && complex.separator == :space return unless (str = selector_to_str(complex)) str end.join(', ') end value.to_a.map do |compound| return unless compound.is_a?(Sass::Script::String) compound.value end.join(' ') end # @private VALID_UNIT = /#{Sass::SCSS::RX::NMSTART}#{Sass::SCSS::RX::NMCHAR}|%*/ # @example # parse_unit_string("em*px/in*%") # => [["em", "px], ["in", "%"]] # # @param unit_string [String] A string adhering to the output of a number with complex # units. E.g. "em*px/in*%" # @return [Array>] A list of numerator units and a list of denominator units. def parse_unit_string(unit_string) denominator_units = numerator_units = Sass::Script::Value::Number::NO_UNITS return numerator_units, denominator_units unless unit_string && unit_string.length > 0 num_over_denominator = unit_string.split(%r{ */ *}) unless (1..2).include?(num_over_denominator.size) raise ArgumentError.new("Malformed unit string: #{unit_string}") end numerator_units = num_over_denominator[0].split(/ *\* */) denominator_units = (num_over_denominator[1] || "").split(/ *\* */) [[numerator_units, "numerator"], [denominator_units, "denominator"]].each do |units, name| if unit_string =~ %r{/} && units.size == 0 raise ArgumentError.new("Malformed unit string: #{unit_string}") end if units.any? {|unit| unit !~ VALID_UNIT} raise ArgumentError.new("Malformed #{name} in unit string: #{unit_string}") end end [numerator_units, denominator_units] end end end ruby-sass-3.7.4/lib/sass/script/value/list.rb000066400000000000000000000074001345125207600211220ustar00rootroot00000000000000module Sass::Script::Value # A SassScript object representing a CSS list. # This includes both comma-separated lists and space-separated lists. class List < Base # The Ruby array containing the contents of the list. # # @return [Array] attr_reader :value alias_method :to_a, :value # The operator separating the values of the list. # Either `:comma` or `:space`. # # @return [Symbol] attr_reader :separator # Whether the list is surrounded by square brackets. # # @return [Boolean] attr_reader :bracketed # Creates a new list. # # @param value [Array] See \{#value} # @param separator [Symbol] See \{#separator} # @param bracketed [Boolean] See \{#bracketed} def initialize(value, separator: nil, bracketed: false) super(value) @separator = separator @bracketed = bracketed end # @see Value#options= def options=(options) super value.each {|v| v.options = options} end # @see Value#eq def eq(other) Sass::Script::Value::Bool.new( other.is_a?(List) && value == other.value && separator == other.separator && bracketed == other.bracketed) end def hash @hash ||= [value, separator, bracketed].hash end # @see Value#to_s def to_s(opts = {}) if !bracketed && value.empty? raise Sass::SyntaxError.new("#{inspect} isn't a valid CSS value.") end members = value. reject {|e| e.is_a?(Null) || e.is_a?(List) && e.value.empty?}. map {|e| e.to_s(opts)} contents = members.join(sep_str) bracketed ? "[#{contents}]" : contents end # @see Value#to_sass def to_sass(opts = {}) return bracketed ? "[]" : "()" if value.empty? members = value.map do |v| if element_needs_parens?(v) "(#{v.to_sass(opts)})" else v.to_sass(opts) end end if separator == :comma && members.length == 1 return "#{bracketed ? '[' : '('}#{members.first},#{bracketed ? ']' : ')'}" end contents = members.join(sep_str(nil)) bracketed ? "[#{contents}]" : contents end # @see Value#to_h def to_h return {} if value.empty? super end # @see Value#inspect def inspect (bracketed ? '[' : '(') + value.map {|e| e.inspect}.join(sep_str(nil)) + (bracketed ? ']' : ')') end # Asserts an index is within the list. # # @private # # @param list [Sass::Script::Value::List] The list for which the index should be checked. # @param n [Sass::Script::Value::Number] The index being checked. def self.assert_valid_index(list, n) if !n.int? || n.to_i == 0 raise ArgumentError.new("List index #{n} must be a non-zero integer") elsif list.to_a.size == 0 raise ArgumentError.new("List index is #{n} but list has no items") elsif n.to_i.abs > (size = list.to_a.size) raise ArgumentError.new( "List index is #{n} but list is only #{size} item#{'s' if size != 1} long") end end private def element_needs_parens?(element) if element.is_a?(List) return false if element.value.length < 2 return false if element.bracketed precedence = Sass::Script::Parser.precedence_of(separator || :space) return Sass::Script::Parser.precedence_of(element.separator || :space) <= precedence end return false unless separator == :space return false unless element.is_a?(Sass::Script::Tree::UnaryOperation) element.operator == :minus || element.operator == :plus end def sep_str(opts = options) return ' ' if separator == :space return ',' if opts && opts[:style] == :compressed ', ' end end end ruby-sass-3.7.4/lib/sass/script/value/map.rb000066400000000000000000000027741345125207600207350ustar00rootroot00000000000000module Sass::Script::Value # A SassScript object representing a map from keys to values. Both keys and # values can be any SassScript object. class Map < Base # The Ruby hash containing the contents of this map. # # @return [Hash] attr_reader :value alias_method :to_h, :value # Creates a new map. # # @param hash [Hash] def initialize(hash) super(hash) end # @see Value#options= def options=(options) super value.each do |k, v| k.options = options v.options = options end end # @see Value#separator def separator :comma unless value.empty? end # @see Value#to_a def to_a value.map do |k, v| list = List.new([k, v], separator: :space) list.options = options list end end # @see Value#eq def eq(other) Bool.new(other.is_a?(Map) && value == other.value) end def hash @hash ||= value.hash end # @see Value#to_s def to_s(opts = {}) raise Sass::SyntaxError.new("#{inspect} isn't a valid CSS value.") end def to_sass(opts = {}) return "()" if value.empty? to_sass = lambda do |value| if value.is_a?(List) && value.separator == :comma "(#{value.to_sass(opts)})" else value.to_sass(opts) end end "(#{value.map {|(k, v)| "#{to_sass[k]}: #{to_sass[v]}"}.join(', ')})" end alias_method :inspect, :to_sass end end ruby-sass-3.7.4/lib/sass/script/value/null.rb000066400000000000000000000016101345125207600211160ustar00rootroot00000000000000module Sass::Script::Value # A SassScript object representing a null value. class Null < Base # The null value in SassScript. # # This is assigned before new is overridden below so that we use the default implementation. NULL = new(nil) # We override object creation so that users of the core API # will not need to know that null is a specific constant. # # @private # @return [Null] the {NULL} constant. def self.new NULL end # @return [Boolean] `false` (the Ruby boolean value) def to_bool false end # @return [Boolean] `true` def null? true end # @return [String] '' (An empty string) def to_s(opts = {}) '' end def to_sass(opts = {}) 'null' end # Returns a string representing a null value. # # @return [String] def inspect 'null' end end end ruby-sass-3.7.4/lib/sass/script/value/number.rb000066400000000000000000000442401345125207600214420ustar00rootroot00000000000000module Sass::Script::Value # A SassScript object representing a number. # SassScript numbers can have decimal values, # and can also have units. # For example, `12`, `1px`, and `10.45em` # are all valid values. # # Numbers can also have more complex units, such as `1px*em/in`. # These cannot be inputted directly in Sass code at the moment. class Number < Base # The Ruby value of the number. # # @return [Numeric] attr_reader :value # A list of units in the numerator of the number. # For example, `1px*em/in*cm` would return `["px", "em"]` # @return [Array] attr_reader :numerator_units # A list of units in the denominator of the number. # For example, `1px*em/in*cm` would return `["in", "cm"]` # @return [Array] attr_reader :denominator_units # The original representation of this number. # For example, although the result of `1px/2px` is `0.5`, # the value of `#original` is `"1px/2px"`. # # This is only non-nil when the original value should be used as the CSS value, # as in `font: 1px/2px`. # # @return [Boolean, nil] attr_accessor :original def self.precision Thread.current[:sass_numeric_precision] || Thread.main[:sass_numeric_precision] || 10 end # Sets the number of digits of precision # For example, if this is `3`, # `3.1415926` will be printed as `3.142`. # The numeric precision is stored as a thread local for thread safety reasons. # To set for all threads, be sure to set the precision on the main thread. def self.precision=(digits) Thread.current[:sass_numeric_precision] = digits.round Thread.current[:sass_numeric_precision_factor] = nil Thread.current[:sass_numeric_epsilon] = nil end # the precision factor used in numeric output # it is derived from the `precision` method. def self.precision_factor Thread.current[:sass_numeric_precision_factor] ||= 10.0**precision end # Used in checking equality of floating point numbers. Any # numbers within an `epsilon` of each other are considered functionally equal. # The value for epsilon is one tenth of the current numeric precision. def self.epsilon Thread.current[:sass_numeric_epsilon] ||= 1 / (precision_factor * 10) end # Used so we don't allocate two new arrays for each new number. NO_UNITS = [] # @param value [Numeric] The value of the number # @param numerator_units [::String, Array<::String>] See \{#numerator\_units} # @param denominator_units [::String, Array<::String>] See \{#denominator\_units} def initialize(value, numerator_units = NO_UNITS, denominator_units = NO_UNITS) numerator_units = [numerator_units] if numerator_units.is_a?(::String) denominator_units = [denominator_units] if denominator_units.is_a?(::String) super(value) @numerator_units = numerator_units @denominator_units = denominator_units @options = nil normalize! end # The SassScript `+` operation. # Its functionality depends on the type of its argument: # # {Number} # : Adds the two numbers together, converting units if possible. # # {Color} # : Adds this number to each of the RGB color channels. # # {Value} # : See {Value::Base#plus}. # # @param other [Value] The right-hand side of the operator # @return [Value] The result of the operation # @raise [Sass::UnitConversionError] if `other` is a number with incompatible units def plus(other) if other.is_a? Number operate(other, :+) elsif other.is_a?(Color) other.plus(self) else super end end # The SassScript binary `-` operation (e.g. `$a - $b`). # Its functionality depends on the type of its argument: # # {Number} # : Subtracts this number from the other, converting units if possible. # # {Value} # : See {Value::Base#minus}. # # @param other [Value] The right-hand side of the operator # @return [Value] The result of the operation # @raise [Sass::UnitConversionError] if `other` is a number with incompatible units def minus(other) if other.is_a? Number operate(other, :-) else super end end # The SassScript unary `+` operation (e.g. `+$a`). # # @return [Number] The value of this number def unary_plus self end # The SassScript unary `-` operation (e.g. `-$a`). # # @return [Number] The negative value of this number def unary_minus Number.new(-value, @numerator_units, @denominator_units) end # The SassScript `*` operation. # Its functionality depends on the type of its argument: # # {Number} # : Multiplies the two numbers together, converting units appropriately. # # {Color} # : Multiplies each of the RGB color channels by this number. # # @param other [Number, Color] The right-hand side of the operator # @return [Number, Color] The result of the operation # @raise [NoMethodError] if `other` is an invalid type def times(other) if other.is_a? Number operate(other, :*) elsif other.is_a? Color other.times(self) else raise NoMethodError.new(nil, :times) end end # The SassScript `/` operation. # Its functionality depends on the type of its argument: # # {Number} # : Divides this number by the other, converting units appropriately. # # {Value} # : See {Value::Base#div}. # # @param other [Value] The right-hand side of the operator # @return [Value] The result of the operation def div(other) if other.is_a? Number res = operate(other, :/) if original && other.original res.original = "#{original}/#{other.original}" end res else super end end # The SassScript `%` operation. # # @param other [Number] The right-hand side of the operator # @return [Number] This number modulo the other # @raise [NoMethodError] if `other` is an invalid type # @raise [Sass::UnitConversionError] if `other` has incompatible units def mod(other) if other.is_a?(Number) return Number.new(Float::NAN) if other.value == 0 operate(other, :%) else raise NoMethodError.new(nil, :mod) end end # The SassScript `==` operation. # # @param other [Value] The right-hand side of the operator # @return [Boolean] Whether this number is equal to the other object def eq(other) return Bool::FALSE unless other.is_a?(Sass::Script::Value::Number) this = self begin if unitless? this = this.coerce(other.numerator_units, other.denominator_units) else other = other.coerce(@numerator_units, @denominator_units) end rescue Sass::UnitConversionError return Bool::FALSE end Bool.new(basically_equal?(this.value, other.value)) end def hash [value, numerator_units, denominator_units].hash end # Hash-equality works differently than `==` equality for numbers. # Hash-equality must be transitive, so it just compares the exact value, # numerator units, and denominator units. def eql?(other) basically_equal?(value, other.value) && numerator_units == other.numerator_units && denominator_units == other.denominator_units end # The SassScript `>` operation. # # @param other [Number] The right-hand side of the operator # @return [Boolean] Whether this number is greater than the other # @raise [NoMethodError] if `other` is an invalid type def gt(other) raise NoMethodError.new(nil, :gt) unless other.is_a?(Number) operate(other, :>) end # The SassScript `>=` operation. # # @param other [Number] The right-hand side of the operator # @return [Boolean] Whether this number is greater than or equal to the other # @raise [NoMethodError] if `other` is an invalid type def gte(other) raise NoMethodError.new(nil, :gte) unless other.is_a?(Number) operate(other, :>=) end # The SassScript `<` operation. # # @param other [Number] The right-hand side of the operator # @return [Boolean] Whether this number is less than the other # @raise [NoMethodError] if `other` is an invalid type def lt(other) raise NoMethodError.new(nil, :lt) unless other.is_a?(Number) operate(other, :<) end # The SassScript `<=` operation. # # @param other [Number] The right-hand side of the operator # @return [Boolean] Whether this number is less than or equal to the other # @raise [NoMethodError] if `other` is an invalid type def lte(other) raise NoMethodError.new(nil, :lte) unless other.is_a?(Number) operate(other, :<=) end # @return [String] The CSS representation of this number # @raise [Sass::SyntaxError] if this number has units that can't be used in CSS # (e.g. `px*in`) def to_s(opts = {}) return original if original raise Sass::SyntaxError.new("#{inspect} isn't a valid CSS value.") unless legal_units? inspect end # Returns a readable representation of this number. # # This representation is valid CSS (and valid SassScript) # as long as there is only one unit. # # @return [String] The representation def inspect(opts = {}) return original if original value = self.class.round(self.value) str = value.to_s # Ruby will occasionally print in scientific notation if the number is # small enough. That's technically valid CSS, but it's not well-supported # and confusing. str = ("%0.#{self.class.precision}f" % value).gsub(/0*$/, '') if str.include?('e') # Sometimes numeric formatting will result in a decimal number with a trailing zero (x.0) if str =~ /(.*)\.0$/ str = $1 end # We omit a leading zero before the decimal point in compressed mode. if @options && options[:style] == :compressed str.sub!(/^(-)?0\./, '\1.') end unitless? ? str : "#{str}#{unit_str}" end alias_method :to_sass, :inspect # @return [Integer] The integer value of the number # @raise [Sass::SyntaxError] if the number isn't an integer def to_i super unless int? value.to_i end # @return [Boolean] Whether or not this number is an integer. def int? basically_equal?(value % 1, 0.0) end # @return [Boolean] Whether or not this number has no units. def unitless? @numerator_units.empty? && @denominator_units.empty? end # Checks whether the number has the numerator unit specified. # # @example # number = Sass::Script::Value::Number.new(10, "px") # number.is_unit?("px") => true # number.is_unit?(nil) => false # # @param unit [::String, nil] The unit the number should have or nil if the number # should be unitless. # @see Number#unitless? The unitless? method may be more readable. def is_unit?(unit) if unit denominator_units.size == 0 && numerator_units.size == 1 && numerator_units.first == unit else unitless? end end # @return [Boolean] Whether or not this number has units that can be represented in CSS # (that is, zero or one \{#numerator\_units}). def legal_units? (@numerator_units.empty? || @numerator_units.size == 1) && @denominator_units.empty? end # Returns this number converted to other units. # The conversion takes into account the relationship between e.g. mm and cm, # as well as between e.g. in and cm. # # If this number has no units, it will simply return itself # with the given units. # # An incompatible coercion, e.g. between px and cm, will raise an error. # # @param num_units [Array] The numerator units to coerce this number into. # See {\#numerator\_units} # @param den_units [Array] The denominator units to coerce this number into. # See {\#denominator\_units} # @return [Number] The number with the new units # @raise [Sass::UnitConversionError] if the given units are incompatible with the number's # current units def coerce(num_units, den_units) Number.new(if unitless? value else value * coercion_factor(@numerator_units, num_units) / coercion_factor(@denominator_units, den_units) end, num_units, den_units) end # @param other [Number] A number to decide if it can be compared with this number. # @return [Boolean] Whether or not this number can be compared with the other. def comparable_to?(other) operate(other, :+) true rescue Sass::UnitConversionError false end # Returns a human readable representation of the units in this number. # For complex units this takes the form of: # numerator_unit1 * numerator_unit2 / denominator_unit1 * denominator_unit2 # @return [String] a string that represents the units in this number def unit_str rv = @numerator_units.sort.join("*") if @denominator_units.any? rv << "/" rv << @denominator_units.sort.join("*") end rv end private # @private # @see Sass::Script::Number.basically_equal? def basically_equal?(num1, num2) self.class.basically_equal?(num1, num2) end # Checks whether two numbers are within an epsilon of each other. # @return [Boolean] def self.basically_equal?(num1, num2) (num1 - num2).abs < epsilon end # @private def self.round(num) if num.is_a?(Float) && (num.infinite? || num.nan?) num elsif basically_equal?(num % 1, 0.0) num.round else ((num * precision_factor).round / precision_factor).to_f end end OPERATIONS = [:+, :-, :<=, :<, :>, :>=, :%] def operate(other, operation) this = self if OPERATIONS.include?(operation) if unitless? this = this.coerce(other.numerator_units, other.denominator_units) else other = other.coerce(@numerator_units, @denominator_units) end end # avoid integer division value = :/ == operation ? this.value.to_f : this.value result = value.send(operation, other.value) if result.is_a?(Numeric) Number.new(result, *compute_units(this, other, operation)) else # Boolean op Bool.new(result) end end def coercion_factor(from_units, to_units) # get a list of unmatched units from_units, to_units = sans_common_units(from_units, to_units) if from_units.size != to_units.size || !convertable?(from_units | to_units) raise Sass::UnitConversionError.new( "Incompatible units: '#{from_units.join('*')}' and '#{to_units.join('*')}'.") end from_units.zip(to_units).inject(1) {|m, p| m * conversion_factor(p[0], p[1])} end def compute_units(this, other, operation) case operation when :* [this.numerator_units + other.numerator_units, this.denominator_units + other.denominator_units] when :/ [this.numerator_units + other.denominator_units, this.denominator_units + other.numerator_units] else [this.numerator_units, this.denominator_units] end end def normalize! return if unitless? @numerator_units, @denominator_units = sans_common_units(@numerator_units, @denominator_units) @denominator_units.each_with_index do |d, i| next unless convertable?(d) && (u = @numerator_units.find {|n| convertable?([n, d])}) @value /= conversion_factor(d, u) @denominator_units.delete_at(i) @numerator_units.delete_at(@numerator_units.index(u)) end end # This is the source data for all the unit logic. It's pre-processed to make # it efficient to figure out whether a set of units is mutually compatible # and what the conversion ratio is between two units. # # These come from http://www.w3.org/TR/2012/WD-css3-values-20120308/. relative_sizes = [ { 'in' => Rational(1), 'cm' => Rational(1, 2.54), 'pc' => Rational(1, 6), 'mm' => Rational(1, 25.4), 'q' => Rational(1, 101.6), 'pt' => Rational(1, 72), 'px' => Rational(1, 96) }, { 'deg' => Rational(1, 360), 'grad' => Rational(1, 400), 'rad' => Rational(1, 2 * Math::PI), 'turn' => Rational(1) }, { 's' => Rational(1), 'ms' => Rational(1, 1000) }, { 'Hz' => Rational(1), 'kHz' => Rational(1000) }, { 'dpi' => Rational(1), 'dpcm' => Rational(254, 100), 'dppx' => Rational(96) } ] # A hash from each known unit to the set of units that it's mutually # convertible with. MUTUALLY_CONVERTIBLE = {} relative_sizes.map do |values| set = values.keys.to_set values.keys.each {|name| MUTUALLY_CONVERTIBLE[name] = set} end # A two-dimensional hash from two units to the conversion ratio between # them. Multiply `X` by `CONVERSION_TABLE[X][Y]` to convert it to `Y`. CONVERSION_TABLE = {} relative_sizes.each do |values| values.each do |(name1, value1)| CONVERSION_TABLE[name1] ||= {} values.each do |(name2, value2)| value = value1 / value2 CONVERSION_TABLE[name1][name2] = value.denominator == 1 ? value.to_i : value.to_f end end end def conversion_factor(from_unit, to_unit) CONVERSION_TABLE[from_unit][to_unit] end def convertable?(units) units = Array(units).to_set return true if units.empty? return false unless (mutually_convertible = MUTUALLY_CONVERTIBLE[units.first]) units.subset?(mutually_convertible) end def sans_common_units(units1, units2) units2 = units2.dup # Can't just use -, because we want px*px to coerce properly to px*mm units1 = units1.map do |u| j = units2.index(u) next u unless j units2.delete_at(j) nil end units1.compact! return units1, units2 end end end ruby-sass-3.7.4/lib/sass/script/value/string.rb000066400000000000000000000077261345125207600214700ustar00rootroot00000000000000# -*- coding: utf-8 -*- module Sass::Script::Value # A SassScript object representing a CSS string *or* a CSS identifier. class String < Base @@interpolation_deprecation = Sass::Deprecation.new # The Ruby value of the string. # # @return [String] attr_reader :value # Whether this is a CSS string or a CSS identifier. # The difference is that strings are written with double-quotes, # while identifiers aren't. # # @return [Symbol] `:string` or `:identifier` attr_reader :type def self.value(contents) contents.gsub("\\\n", "").gsub(/\\(?:([0-9a-fA-F]{1,6})\s?|(.))/) do next $2 if $2 # Handle unicode escapes as per CSS Syntax Level 3 section 4.3.8. code_point = $1.to_i(16) if code_point == 0 || code_point > 0x10FFFF || (code_point >= 0xD800 && code_point <= 0xDFFF) '�' else [code_point].pack("U") end end end # Returns the quoted string representation of `contents`. # # @options opts :quote [String] # The preferred quote style for quoted strings. If `:none`, strings are # always emitted unquoted. If `nil`, quoting is determined automatically. # @options opts :sass [String] # Whether to quote strings for Sass source, as opposed to CSS. Defaults to `false`. def self.quote(contents, opts = {}) quote = opts[:quote] # Short-circuit if there are no characters that need quoting. unless contents =~ /[\n\\"']|\#\{/ quote ||= '"' return "#{quote}#{contents}#{quote}" end if quote.nil? if contents.include?('"') if contents.include?("'") quote = '"' else quote = "'" end else quote = '"' end end # Replace single backslashes with multiples. contents = contents.gsub("\\", "\\\\\\\\") # Escape interpolation. contents = contents.gsub('#{', "\\\#{") if opts[:sass] if quote == '"' contents = contents.gsub('"', "\\\"") else contents = contents.gsub("'", "\\'") end contents = contents.gsub(/\n(?![a-fA-F0-9\s])/, "\\a").gsub("\n", "\\a ") "#{quote}#{contents}#{quote}" end # Creates a new string. # # @param value [String] See \{#value} # @param type [Symbol] See \{#type} # @param deprecated_interp_equivalent [String?] # If this was created via a potentially-deprecated string interpolation, # this is the replacement expression that should be suggested to the user. def initialize(value, type = :identifier, deprecated_interp_equivalent = nil) super(value) @type = type @deprecated_interp_equivalent = deprecated_interp_equivalent end # @see Value#plus def plus(other) other_value = if other.is_a?(Sass::Script::Value::String) other.value else other.to_s(:quote => :none) end Sass::Script::Value::String.new(value + other_value, type) end # @see Value#to_s def to_s(opts = {}) return @value.gsub(/\n\s*/, ' ') if opts[:quote] == :none || @type == :identifier String.quote(value, opts) end # @see Value#to_sass def to_sass(opts = {}) to_s(opts.merge(:sass => true)) end def separator check_deprecated_interp super end def to_a check_deprecated_interp super end # Prints a warning if this string was created using potentially-deprecated # interpolation. def check_deprecated_interp return unless @deprecated_interp_equivalent @@interpolation_deprecation.warn(source_range.file, source_range.start_pos.line, <, nil] # The interpolated identifier, or nil if none could be parsed def parse_interp_ident init_scanner! interp_ident end # Parses a supports clause for an @import directive def parse_supports_clause init_scanner! ss clause = supports_clause ss clause end # Parses a media query list. # # @return [Sass::Media::QueryList] The parsed query list # @raise [Sass::SyntaxError] if there's a syntax error in the query list, # or if it doesn't take up the entire input string. def parse_media_query_list init_scanner! ql = media_query_list expected("media query list") unless ql && @scanner.eos? ql end # Parses an at-root query. # # @return [Array] The interpolated query. # @raise [Sass::SyntaxError] if there's a syntax error in the query, # or if it doesn't take up the entire input string. def parse_at_root_query init_scanner! query = at_root_query expected("@at-root query list") unless query && @scanner.eos? query end # Parses a supports query condition. # # @return [Sass::Supports::Condition] The parsed condition # @raise [Sass::SyntaxError] if there's a syntax error in the condition, # or if it doesn't take up the entire input string. def parse_supports_condition init_scanner! condition = supports_condition expected("supports condition") unless condition && @scanner.eos? condition end # Parses a custom property value. # # @return [Array] The interpolated value. # @raise [Sass::SyntaxError] if there's a syntax error in the value, # or if it doesn't take up the entire input string. def parse_declaration_value init_scanner! value = declaration_value expected('"}"') unless value && @scanner.eos? value end private include Sass::SCSS::RX def source_position Sass::Source::Position.new(@line, @offset) end def range(start_pos, end_pos = source_position) Sass::Source::Range.new(start_pos, end_pos, @filename, @importer) end def init_scanner! @scanner = if @template.is_a?(StringScanner) @template else Sass::Util::MultibyteStringScanner.new(@template.tr("\r", "")) end end def stylesheet node = node(Sass::Tree::RootNode.new(@scanner.string), source_position) block_contents(node, :stylesheet) {s(node)} end def s(node) while tok(S) || tok(CDC) || tok(CDO) || (c = tok(SINGLE_LINE_COMMENT)) || (c = tok(COMMENT)) next unless c process_comment c, node c = nil end true end def ss nil while tok(S) || tok(SINGLE_LINE_COMMENT) || tok(COMMENT) true end def ss_comments(node) while tok(S) || (c = tok(SINGLE_LINE_COMMENT)) || (c = tok(COMMENT)) next unless c process_comment c, node c = nil end true end def whitespace return unless tok(S) || tok(SINGLE_LINE_COMMENT) || tok(COMMENT) ss end def process_comment(text, node) silent = text =~ %r{\A//} loud = !silent && text =~ %r{\A/[/*]!} line = @line - text.count("\n") comment_start = @scanner.pos - text.length index_before_line = @scanner.string.rindex("\n", comment_start) || -1 offset = comment_start - index_before_line if silent value = [text.sub(%r{\A\s*//}, '/*').gsub(%r{^\s*//}, ' *') + ' */'] else value = Sass::Engine.parse_interp(text, line, offset, :filename => @filename) line_before_comment = @scanner.string[index_before_line + 1...comment_start] value.unshift(line_before_comment.gsub(/[^\s]/, ' ')) end type = if silent :silent elsif loud :loud else :normal end start_pos = Sass::Source::Position.new(line, offset) comment = node(Sass::Tree::CommentNode.new(value, type), start_pos) node << comment end DIRECTIVES = Set[:mixin, :include, :function, :return, :debug, :warn, :for, :each, :while, :if, :else, :extend, :import, :media, :charset, :content, :_moz_document, :at_root, :error] PREFIXED_DIRECTIVES = Set[:supports] def directive start_pos = source_position return unless tok(/@/) name = ident! ss if (dir = special_directive(name, start_pos)) return dir elsif (dir = prefixed_directive(name, start_pos)) return dir end val = almost_any_value val = val ? ["@#{name} "] + Sass::Util.strip_string_array(val) : ["@#{name}"] directive_body(val, start_pos) end def directive_body(value, start_pos) node = Sass::Tree::DirectiveNode.new(value) if tok(/\{/) node.has_children = true block_contents(node, :directive) tok!(/\}/) end node(node, start_pos) end def special_directive(name, start_pos) sym = name.tr('-', '_').to_sym DIRECTIVES.include?(sym) && send("#{sym}_directive", start_pos) end def prefixed_directive(name, start_pos) sym = deprefix(name).tr('-', '_').to_sym PREFIXED_DIRECTIVES.include?(sym) && send("#{sym}_directive", name, start_pos) end def mixin_directive(start_pos) name = ident! args, splat = sass_script(:parse_mixin_definition_arglist) ss block(node(Sass::Tree::MixinDefNode.new(name, args, splat), start_pos), :directive) end def include_directive(start_pos) name = ident! args, keywords, splat, kwarg_splat = sass_script(:parse_mixin_include_arglist) ss include_node = node( Sass::Tree::MixinNode.new(name, args, keywords, splat, kwarg_splat), start_pos) if tok?(/\{/) include_node.has_children = true block(include_node, :directive) else include_node end end def content_directive(start_pos) ss node(Sass::Tree::ContentNode.new, start_pos) end def function_directive(start_pos) name = ident! args, splat = sass_script(:parse_function_definition_arglist) ss block(node(Sass::Tree::FunctionNode.new(name, args, splat), start_pos), :function) end def return_directive(start_pos) node(Sass::Tree::ReturnNode.new(sass_script(:parse)), start_pos) end def debug_directive(start_pos) node(Sass::Tree::DebugNode.new(sass_script(:parse)), start_pos) end def warn_directive(start_pos) node(Sass::Tree::WarnNode.new(sass_script(:parse)), start_pos) end def for_directive(start_pos) tok!(/\$/) var = ident! ss tok!(/from/) from = sass_script(:parse_until, Set["to", "through"]) ss @expected = '"to" or "through"' exclusive = (tok(/to/) || tok!(/through/)) == 'to' to = sass_script(:parse) ss block(node(Sass::Tree::ForNode.new(var, from, to, exclusive), start_pos), :directive) end def each_directive(start_pos) tok!(/\$/) vars = [ident!] ss while tok(/,/) ss tok!(/\$/) vars << ident! ss end tok!(/in/) list = sass_script(:parse) ss block(node(Sass::Tree::EachNode.new(vars, list), start_pos), :directive) end def while_directive(start_pos) expr = sass_script(:parse) ss block(node(Sass::Tree::WhileNode.new(expr), start_pos), :directive) end def if_directive(start_pos) expr = sass_script(:parse) ss node = block(node(Sass::Tree::IfNode.new(expr), start_pos), :directive) pos = @scanner.pos line = @line ss else_block(node) || begin # Backtrack in case there are any comments we want to parse @scanner.pos = pos @line = line node end end def else_block(node) start_pos = source_position return unless tok(/@else/) ss else_node = block( node(Sass::Tree::IfNode.new((sass_script(:parse) if tok(/if/))), start_pos), :directive) node.add_else(else_node) pos = @scanner.pos line = @line ss else_block(node) || begin # Backtrack in case there are any comments we want to parse @scanner.pos = pos @line = line node end end def else_directive(start_pos) err("Invalid CSS: @else must come after @if") end def extend_directive(start_pos) selector_start_pos = source_position @expected = "selector" selector = Sass::Util.strip_string_array(expr!(:almost_any_value)) optional = tok(OPTIONAL) ss node(Sass::Tree::ExtendNode.new(selector, !!optional, range(selector_start_pos)), start_pos) end def import_directive(start_pos) values = [] loop do values << expr!(:import_arg) break if use_css_import? break unless tok(/,/) ss end values end def import_arg start_pos = source_position return unless (str = string) || (uri = tok?(/url\(/i)) if uri str = sass_script(:parse_string) ss supports = supports_clause ss media = media_query_list ss return node(Tree::CssImportNode.new(str, media.to_a, supports), start_pos) end ss supports = supports_clause ss media = media_query_list if str =~ %r{^(https?:)?//} || media || supports || use_css_import? return node( Sass::Tree::CssImportNode.new( Sass::Script::Value::String.quote(str), media.to_a, supports), start_pos) end node(Sass::Tree::ImportNode.new(str.strip), start_pos) end def use_css_import?; false; end def media_directive(start_pos) block(node(Sass::Tree::MediaNode.new(expr!(:media_query_list).to_a), start_pos), :directive) end # http://www.w3.org/TR/css3-mediaqueries/#syntax def media_query_list query = media_query return unless query queries = [query] ss while tok(/,/) ss; queries << expr!(:media_query) end ss Sass::Media::QueryList.new(queries) end def media_query if (ident1 = interp_ident) ss ident2 = interp_ident ss if ident2 && ident2.length == 1 && ident2[0].is_a?(String) && ident2[0].downcase == 'and' query = Sass::Media::Query.new([], ident1, []) else if ident2 query = Sass::Media::Query.new(ident1, ident2, []) else query = Sass::Media::Query.new([], ident1, []) end return query unless tok(/and/i) ss end end if query expr = expr!(:media_expr) else expr = media_expr return unless expr end query ||= Sass::Media::Query.new([], [], []) query.expressions << expr ss while tok(/and/i) ss; query.expressions << expr!(:media_expr) end query end def query_expr interp = interpolation return interp if interp return unless tok(/\(/) res = ['('] ss stop_at = Set[:single_eq, :lt, :lte, :gt, :gte] res << sass_script(:parse_until, stop_at) if tok(/:/) res << ': ' ss res << sass_script(:parse) elsif comparison1 = tok(/=|[<>]=?/) res << ' ' << comparison1 << ' ' ss res << sass_script(:parse_until, stop_at) if ((comparison1 == ">" || comparison1 == ">=") && comparison2 = tok(/>=?/)) || ((comparison1 == "<" || comparison1 == "<=") && comparison2 = tok(/<=?/)) res << ' ' << comparison2 << ' ' ss res << sass_script(:parse_until, stop_at) end end res << tok!(/\)/) ss res end # Aliases allow us to use different descriptions if the same # expression fails in different contexts. alias_method :media_expr, :query_expr alias_method :at_root_query, :query_expr def charset_directive(start_pos) name = expr!(:string) ss node(Sass::Tree::CharsetNode.new(name), start_pos) end # The document directive is specified in # http://www.w3.org/TR/css3-conditional/, but Gecko allows the # `url-prefix` and `domain` functions to omit quotation marks, contrary to # the standard. # # We could parse all document directives according to Mozilla's syntax, # but if someone's using e.g. @-webkit-document we don't want them to # think WebKit works sans quotes. def _moz_document_directive(start_pos) res = ["@-moz-document "] loop do res << str {ss} << expr!(:moz_document_function) if (c = tok(/,/)) res << c else break end end directive_body(res.flatten, start_pos) end def moz_document_function val = interp_uri || _interp_string(:url_prefix) || _interp_string(:domain) || function(false) || interpolation return unless val ss val end def at_root_directive(start_pos) if tok?(/\(/) && (expr = at_root_query) return block(node(Sass::Tree::AtRootNode.new(expr), start_pos), :directive) end at_root_node = node(Sass::Tree::AtRootNode.new, start_pos) rule_node = ruleset return block(at_root_node, :stylesheet) unless rule_node at_root_node << rule_node at_root_node end def at_root_directive_list return unless (first = ident) arr = [first] ss while (e = ident) arr << e ss end arr end def error_directive(start_pos) node(Sass::Tree::ErrorNode.new(sass_script(:parse)), start_pos) end # http://www.w3.org/TR/css3-conditional/ def supports_directive(name, start_pos) condition = expr!(:supports_condition) node = Sass::Tree::SupportsNode.new(name, condition) tok!(/\{/) node.has_children = true block_contents(node, :directive) tok!(/\}/) node(node, start_pos) end def supports_clause return unless tok(/supports\(/i) ss supports = import_supports_condition ss tok!(/\)/) supports end def supports_condition supports_negation || supports_operator || supports_interpolation end def import_supports_condition supports_condition || supports_declaration end def supports_negation return unless tok(/not/i) ss Sass::Supports::Negation.new(expr!(:supports_condition_in_parens)) end def supports_operator cond = supports_condition_in_parens return unless cond re = /and|or/i while (op = tok(re)) re = /#{op}/i ss cond = Sass::Supports::Operator.new( cond, expr!(:supports_condition_in_parens), op) end cond end def supports_declaration name = sass_script(:parse) tok!(/:/); ss value = sass_script(:parse) Sass::Supports::Declaration.new(name, value) end def supports_condition_in_parens interp = supports_interpolation return interp if interp return unless tok(/\(/); ss if (cond = supports_condition) tok!(/\)/); ss cond else decl = supports_declaration tok!(/\)/); ss decl end end def supports_interpolation interp = interpolation return unless interp ss Sass::Supports::Interpolation.new(interp) end def variable return unless tok(/\$/) start_pos = source_position name = ident! ss; tok!(/:/); ss expr = sass_script(:parse) while tok(/!/) flag_name = ident! if flag_name == 'default' guarded ||= true elsif flag_name == 'global' global ||= true else raise Sass::SyntaxError.new("Invalid flag \"!#{flag_name}\".", :line => @line) end ss end result = Sass::Tree::VariableNode.new(name, expr, guarded, global) node(result, start_pos) end def operator # Many of these operators (all except / and ,) # are disallowed by the CSS spec, # but they're included here for compatibility # with some proprietary MS properties str {ss if tok(%r{[/,:.=]})} end def ruleset start_pos = source_position return unless (rules = almost_any_value) block( node( Sass::Tree::RuleNode.new(rules, range(start_pos)), start_pos), :ruleset) end def block(node, context) node.has_children = true tok!(/\{/) block_contents(node, context) tok!(/\}/) node end # A block may contain declarations and/or rulesets def block_contents(node, context) block_given? ? yield : ss_comments(node) node << (child = block_child(context)) while tok(/;/) || has_children?(child) block_given? ? yield : ss_comments(node) node << (child = block_child(context)) end node end def block_child(context) return variable || directive if context == :function return variable || directive || ruleset if context == :stylesheet variable || directive || declaration_or_ruleset end def has_children?(child_or_array) return false unless child_or_array return child_or_array.last.has_children if child_or_array.is_a?(Array) child_or_array.has_children end # When parsing the contents of a ruleset, it can be difficult to tell # declarations apart from nested rulesets. Since we don't thoroughly parse # selectors until after resolving interpolation, we can share a bunch of # the parsing of the two, but we need to disambiguate them first. We use # the following criteria: # # * If the entity doesn't start with an identifier followed by a colon, # it's a selector. There are some additional mostly-unimportant cases # here to support various declaration hacks. # # * If the colon is followed by another colon, it's a selector. # # * Otherwise, if the colon is followed by anything other than # interpolation or a character that's valid as the beginning of an # identifier, it's a declaration. # # * If the colon is followed by interpolation or a valid identifier, try # parsing it as a declaration value. If this fails, backtrack and parse # it as a selector. # # * If the declaration value value valid but is followed by "{", backtrack # and parse it as a selector anyway. This ensures that ".foo:bar {" is # always parsed as a selector and never as a property with nested # properties beneath it. def declaration_or_ruleset start_pos = source_position declaration = try_declaration if declaration.nil? return unless (selector = almost_any_value) elsif declaration.is_a?(Array) selector = declaration else # Declaration should be a PropNode. return declaration end if (additional_selector = almost_any_value) selector << additional_selector end block( node( Sass::Tree::RuleNode.new(merge(selector), range(start_pos)), start_pos), :ruleset) end # Tries to parse a declaration, and returns the value parsed so far if it # fails. # # This has three possible return types. It can return `nil`, indicating # that parsing failed completely and the scanner hasn't moved forward at # all. It can return an Array, indicating that parsing failed after # consuming some text (possibly containing interpolation), which is # returned. Or it can return a PropNode, indicating that parsing # succeeded. def try_declaration # This allows the "*prop: val", ":prop: val", "#prop: val", and ".prop: # val" hacks. name_start_pos = source_position if (s = tok(/[:\*\.]|\#(?!\{)/)) name = [s, str {ss}] return name unless (ident = interp_ident) name << ident else return unless (name = interp_ident) name = Array(name) end if (comment = tok(COMMENT)) name << comment end name_end_pos = source_position mid = [str {ss}] return name + mid unless tok(/:/) mid << ':' # If this is a CSS variable, parse it as a property no matter what. if name.first.is_a?(String) && name.first.start_with?("--") return css_variable_declaration(name, name_start_pos, name_end_pos) end return name + mid + [':'] if tok(/:/) mid << str {ss} post_colon_whitespace = !mid.last.empty? could_be_selector = !post_colon_whitespace && (tok?(IDENT_START) || tok?(INTERP_START)) value_start_pos = source_position value = nil error = catch_error do value = value! if tok?(/\{/) # Properties that are ambiguous with selectors can't have additional # properties nested beneath them. tok!(/;/) if could_be_selector elsif !tok?(/[;{}]/) # We want an exception if there's no valid end-of-property character # exists, but we don't want to consume it if it does. tok!(/[;{}]/) end end if error rethrow error unless could_be_selector # If the value would be followed by a semicolon, it's definitely # supposed to be a property, not a selector. additional_selector = almost_any_value rethrow error if tok?(/;/) return name + mid + (additional_selector || []) end value_end_pos = source_position ss require_block = tok?(/\{/) node = node(Sass::Tree::PropNode.new(name.flatten.compact, [value], :new), name_start_pos, value_end_pos) node.name_source_range = range(name_start_pos, name_end_pos) node.value_source_range = range(value_start_pos, value_end_pos) return node unless require_block nested_properties! node end def css_variable_declaration(name, name_start_pos, name_end_pos) value_start_pos = source_position value = declaration_value value_end_pos = source_position node = node(Sass::Tree::PropNode.new(name.flatten.compact, value, :new), name_start_pos, value_end_pos) node.name_source_range = range(name_start_pos, name_end_pos) node.value_source_range = range(value_start_pos, value_end_pos) node end # This production consumes values that could be a selector, an expression, # or a combination of both. It respects strings and comments and supports # interpolation. It will consume up to "{", "}", ";", or "!". # # Values consumed by this production will usually be parsed more # thoroughly once interpolation has been resolved. def almost_any_value return unless (tok = almost_any_value_token) sel = [tok] while (tok = almost_any_value_token) sel << tok end merge(sel) end def almost_any_value_token tok(%r{ ( \\. | (?!url\() [^"'/\#!;\{\}] # " | # interp_uri will handle most url() calls, but not ones that take strings url\(#{W}(?=") | /(?![/*]) | \#(?!\{) | !(?![a-z]) # TODO: never consume "!" when issue 1126 is fixed. )+ }xi) || tok(COMMENT) || tok(SINGLE_LINE_COMMENT) || interp_string || interp_uri || interpolation(:warn_for_color) end def declaration_value(top_level: true) return unless (tok = declaration_value_token(top_level)) value = [tok] while (tok = declaration_value_token(top_level)) value << tok end merge(value) end def declaration_value_token(top_level) # This comes, more or less, from the [token consumption algorithm][]. # However, since we don't have to worry about the token semantics, we # just consume everything until we come across a token with special # semantics. # # [token consumption algorithm]: https://drafts.csswg.org/css-syntax-3/#consume-token. result = tok(%r{ ( (?! url\( ) [^()\[\]{}"'#/ \t\r\n\f#{top_level ? ";" : ""}] | \#(?!\{) | /(?!\*) )+ }xi) || interp_string || interp_uri || interpolation || tok(COMMENT) return result if result # Fold together multiple characters of whitespace that don't include # newlines. The value only cares about the tokenization, so this is safe # as long as we don't delete whitespace entirely. It's important that we # fold here rather than post-processing, since we aren't allowed to fold # whitespace within strings and we lose that context later on. if (ws = tok(S)) return ws.include?("\n") ? ws.gsub(/\A[^\n]*/, '') : ' ' end if tok(/\(/) value = declaration_value(top_level: false) tok!(/\)/) ['(', *value, ')'] elsif tok(/\[/) value = declaration_value(top_level: false) tok!(/\]/) ['[', *value, ']'] elsif tok(/\{/) value = declaration_value(top_level: false) tok!(/\}/) ['{', *value, '}'] end end def declaration # This allows the "*prop: val", ":prop: val", "#prop: val", and ".prop: # val" hacks. name_start_pos = source_position if (s = tok(/[:\*\.]|\#(?!\{)/)) name = [s, str {ss}, *expr!(:interp_ident)] else return unless (name = interp_ident) name = Array(name) end if (comment = tok(COMMENT)) name << comment end name_end_pos = source_position ss tok!(/:/) ss value_start_pos = source_position value = value! value_end_pos = source_position ss require_block = tok?(/\{/) node = node(Sass::Tree::PropNode.new(name.flatten.compact, [value], :new), name_start_pos, value_end_pos) node.name_source_range = range(name_start_pos, name_end_pos) node.value_source_range = range(value_start_pos, value_end_pos) return node unless require_block nested_properties! node end def value! if tok?(/\{/) str = Sass::Script::Tree::Literal.new(Sass::Script::Value::String.new("")) str.line = source_position.line str.source_range = range(source_position) return str end start_pos = source_position # This is a bit of a dirty trick: # if the value is completely static, # we don't parse it at all, and instead return a plain old string # containing the value. # This results in a dramatic speed increase. if (val = tok(STATIC_VALUE)) # If val ends with escaped whitespace, leave it be. str = Sass::Script::Tree::Literal.new( Sass::Script::Value::String.new( Sass::Util.strip_except_escapes(val))) str.line = start_pos.line str.source_range = range(start_pos) return str end sass_script(:parse) end def nested_properties!(node) @expected = 'expression (e.g. 1px, bold) or "{"' block(node, :property) end def expr(allow_var = true) t = term(allow_var) return unless t res = [t, str {ss}] while (o = operator) && (t = term(allow_var)) res << o << t << str {ss} end res.flatten end def term(allow_var) e = tok(NUMBER) || interp_uri || function(allow_var) || interp_string || tok(UNICODERANGE) || interp_ident || tok(HEXCOLOR) || (allow_var && var_expr) return e if e op = tok(/[+-]/) return unless op @expected = "number or function" [op, tok(NUMBER) || function(allow_var) || (allow_var && var_expr) || expr!(:interpolation)] end def function(allow_var) name = tok(FUNCTION) return unless name if name == "expression(" || name == "calc(" str, _ = Sass::Shared.balance(@scanner, ?(, ?), 1) [name, str] else [name, str {ss}, expr(allow_var), tok!(/\)/)] end end def var_expr return unless tok(/\$/) line = @line var = Sass::Script::Tree::Variable.new(ident!) var.line = line var end def interpolation(warn_for_color = false) return unless tok(INTERP_START) sass_script(:parse_interpolated, warn_for_color) end def string return unless tok(STRING) Sass::Script::Value::String.value(@scanner[1] || @scanner[2]) end def interp_string _interp_string(:double) || _interp_string(:single) end def interp_uri _interp_string(:uri) end def _interp_string(type) start = tok(Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[type][false]) return unless start res = [start] mid_re = Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[type][true] # @scanner[2].empty? means we've started an interpolated section while @scanner[2] == '#{' @scanner.pos -= 2 # Don't consume the #{ res.last.slice!(-2..-1) res << expr!(:interpolation) << tok(mid_re) end res end def ident (ident = tok(IDENT)) && Sass::Util.normalize_ident_escapes(ident) end def ident! Sass::Util.normalize_ident_escapes(tok!(IDENT)) end def name (name = tok(NAME)) && Sass::Util.normalize_ident_escapes(name) end def name! Sass::Util.normalize_ident_escapes(tok!(NAME)) end def interp_ident val = ident || interpolation(:warn_for_color) || tok(IDENT_HYPHEN_INTERP) return unless val res = [val] while (val = name || interpolation(:warn_for_color)) res << val end res end def interp_ident_or_var id = interp_ident return id if id var = var_expr return [var] if var end def str @strs.push String.new("") yield @strs.last ensure @strs.pop end def str? pos = @scanner.pos line = @line offset = @offset @strs.push "" throw_error {yield} && @strs.last rescue Sass::SyntaxError @scanner.pos = pos @line = line @offset = offset nil ensure @strs.pop end def node(node, start_pos, end_pos = source_position) node.line = start_pos.line node.source_range = range(start_pos, end_pos) node end @sass_script_parser = Sass::Script::Parser class << self # @private attr_accessor :sass_script_parser end def sass_script(*args) parser = self.class.sass_script_parser.new(@scanner, @line, @offset, :filename => @filename, :importer => @importer, :allow_extra_text => true) result = parser.send(*args) unless @strs.empty? # Convert to CSS manually so that comments are ignored. src = result.to_sass @strs.each {|s| s << src} end @line = parser.line @offset = parser.offset result rescue Sass::SyntaxError => e throw(:_sass_parser_error, true) if @throw_error raise e end def merge(arr) arr && Sass::Util.merge_adjacent_strings([arr].flatten) end EXPR_NAMES = { :media_query => "media query (e.g. print, screen, print and screen)", :media_query_list => "media query (e.g. print, screen, print and screen)", :media_expr => "media expression (e.g. (min-device-width: 800px))", :at_root_query => "@at-root query (e.g. (without: media))", :at_root_directive_list => '* or identifier', :declaration_value => "expression (e.g. fr, 2n+1)", :interp_ident => "identifier", :qualified_name => "identifier", :expr => "expression (e.g. 1px, bold)", :selector_comma_sequence => "selector", :string => "string", :import_arg => "file to import (string or url())", :moz_document_function => "matching function (e.g. url-prefix(), domain())", :supports_condition => "@supports condition (e.g. (display: flexbox))", :supports_condition_in_parens => "@supports condition (e.g. (display: flexbox))", :a_n_plus_b => "An+B expression", :keyframes_selector_component => "from, to, or a percentage", :keyframes_selector => "keyframes selector (e.g. 10%)" } TOK_NAMES = Hash[Sass::SCSS::RX.constants.map do |c| [Sass::SCSS::RX.const_get(c), c.downcase] end].merge( IDENT => "identifier", /[;{}]/ => '";"', /\b(without|with)\b/ => '"with" or "without"' ) def tok?(rx) @scanner.match?(rx) end def expr!(name) e = send(name) return e if e expected(EXPR_NAMES[name] || name.to_s) end def tok!(rx) t = tok(rx) return t if t name = TOK_NAMES[rx] unless name # Display basic regexps as plain old strings source = rx.source.gsub(%r{\\/}, '/') string = rx.source.gsub(/\\(.)/, '\1') name = source == Regexp.escape(string) ? string.inspect : rx.inspect end expected(name) end def expected(name) throw(:_sass_parser_error, true) if @throw_error self.class.expected(@scanner, @expected || name, @line) end def err(msg) throw(:_sass_parser_error, true) if @throw_error raise Sass::SyntaxError.new(msg, :line => @line) end def throw_error old_throw_error, @throw_error = @throw_error, false yield ensure @throw_error = old_throw_error end def catch_error(&block) old_throw_error, @throw_error = @throw_error, true pos = @scanner.pos line = @line offset = @offset expected = @expected logger = Sass::Logger::Delayed.install! if catch(:_sass_parser_error) {yield; false} @scanner.pos = pos @line = line @offset = offset @expected = expected {:pos => pos, :line => line, :expected => @expected, :block => block} else logger.flush nil end ensure logger.uninstall! if logger @throw_error = old_throw_error end def rethrow(err) if @throw_error throw :_sass_parser_error, err else @scanner = Sass::Util::MultibyteStringScanner.new(@scanner.string) @scanner.pos = err[:pos] @line = err[:line] @expected = err[:expected] err[:block].call end end # @private def self.expected(scanner, expected, line) pos = scanner.pos after = scanner.string[0...pos] # Get rid of whitespace between pos and the last token, # but only if there's a newline in there after.gsub!(/\s*\n\s*$/, '') # Also get rid of stuff before the last newline after.gsub!(/.*\n/, '') after = "..." + after[-15..-1] if after.size > 18 was = scanner.rest.dup # Get rid of whitespace between pos and the next token, # but only if there's a newline in there was.gsub!(/^\s*\n\s*/, '') # Also get rid of stuff after the next newline was.gsub!(/\n.*/, '') was = was[0...15] + "..." if was.size > 18 raise Sass::SyntaxError.new( "Invalid CSS after \"#{after}\": expected #{expected}, was \"#{was}\"", :line => line) end # Avoid allocating lots of new strings for `#tok`. # This is important because `#tok` is called all the time. NEWLINE = "\n" def tok(rx) res = @scanner.scan(rx) return unless res newline_count = res.count(NEWLINE) if newline_count > 0 @line += newline_count @offset = res[res.rindex(NEWLINE)..-1].size else @offset += res.size end @expected = nil if !@strs.empty? && rx != COMMENT && rx != SINGLE_LINE_COMMENT @strs.each {|s| s << res} end res end # Remove a vendor prefix from `str`. def deprefix(str) str.gsub(/^-[a-zA-Z0-9]+-/, '') end end end end ruby-sass-3.7.4/lib/sass/scss/rx.rb000066400000000000000000000112311345125207600171300ustar00rootroot00000000000000# -*- coding: utf-8 -*- module Sass module SCSS # A module containing regular expressions used # for lexing tokens in an SCSS document. # Most of these are taken from [the CSS3 spec](http://www.w3.org/TR/css3-syntax/#lexical), # although some have been modified for various reasons. module RX # Takes a string and returns a CSS identifier # that will have the value of the given string. # # @param str [String] The string to escape # @return [String] The escaped string def self.escape_ident(str) return "" if str.empty? return "\\#{str}" if str == '-' || str == '_' out = "" value = str.dup out << value.slice!(0...1) if value =~ /^[-_]/ if value[0...1] =~ NMSTART out << value.slice!(0...1) else out << escape_char(value.slice!(0...1)) end out << value.gsub(/[^a-zA-Z0-9_-]/) {|c| escape_char c} out end # Escapes a single character for a CSS identifier. # # @param c [String] The character to escape. Should have length 1 # @return [String] The escaped character # @private def self.escape_char(c) return "\\%06x" % c.ord unless c =~ %r{[ -/:-~]} "\\#{c}" end # Creates a Regexp from a plain text string, # escaping all significant characters. # # @param str [String] The text of the regexp # @param flags [Integer] Flags for the created regular expression # @return [Regexp] # @private def self.quote(str, flags = 0) Regexp.new(Regexp.quote(str), flags) end H = /[0-9a-fA-F]/ NL = /\n|\r\n|\r|\f/ UNICODE = /\\#{H}{1,6}[ \t\r\n\f]?/ s = '\u{80}-\u{D7FF}\u{E000}-\u{FFFD}\u{10000}-\u{10FFFF}' NONASCII = /[#{s}]/ ESCAPE = /#{UNICODE}|\\[^0-9a-fA-F\r\n\f]/ NMSTART = /[_a-zA-Z]|#{NONASCII}|#{ESCAPE}/ NMCHAR = /[a-zA-Z0-9_-]|#{NONASCII}|#{ESCAPE}/ STRING1 = /\"((?:[^\n\r\f\\"]|\\#{NL}|#{ESCAPE})*)\"/ STRING2 = /\'((?:[^\n\r\f\\']|\\#{NL}|#{ESCAPE})*)\'/ IDENT = /-*#{NMSTART}#{NMCHAR}*/ NAME = /#{NMCHAR}+/ STRING = /#{STRING1}|#{STRING2}/ URLCHAR = /[#%&*-~]|#{NONASCII}|#{ESCAPE}/ URL = /(#{URLCHAR}*)/ W = /[ \t\r\n\f]*/ VARIABLE = /(\$)(#{Sass::SCSS::RX::IDENT})/ # This is more liberal than the spec's definition, # but that definition didn't work well with the greediness rules RANGE = /(?:#{H}|\?){1,6}/ ## S = /[ \t\r\n\f]+/ COMMENT = %r{/\*([^*]|\*+[^/*])*\**\*/} SINGLE_LINE_COMMENT = %r{//.*(\n[ \t]*//.*)*} CDO = quote("") INCLUDES = quote("~=") DASHMATCH = quote("|=") PREFIXMATCH = quote("^=") SUFFIXMATCH = quote("$=") SUBSTRINGMATCH = quote("*=") HASH = /##{NAME}/ IMPORTANT = /!#{W}important/i # A unit is like an IDENT, but disallows a hyphen followed by a digit. # This allows "1px-2px" to be interpreted as subtraction rather than "1" # with the unit "px-2px". It also allows "%". UNIT = /-?#{NMSTART}(?:[a-zA-Z0-9_]|#{NONASCII}|#{ESCAPE}|-(?!\.?\d))*|%/ UNITLESS_NUMBER = /(?:[0-9]+|[0-9]*\.[0-9]+)(?:[eE][+-]?\d+)?/ NUMBER = /#{UNITLESS_NUMBER}(?:#{UNIT})?/ PERCENTAGE = /#{UNITLESS_NUMBER}%/ URI = /url\(#{W}(?:#{STRING}|#{URL})#{W}\)/i FUNCTION = /#{IDENT}\(/ UNICODERANGE = /u\+(?:#{H}{1,6}-#{H}{1,6}|#{RANGE})/i # Defined in http://www.w3.org/TR/css3-selectors/#lex PLUS = /#{W}\+/ GREATER = /#{W}>/ TILDE = /#{W}~/ NOT = quote(":not(", Regexp::IGNORECASE) # Defined in https://developer.mozilla.org/en/CSS/@-moz-document as a # non-standard version of http://www.w3.org/TR/css3-conditional/ URL_PREFIX = /url-prefix\(#{W}(?:#{STRING}|#{URL})#{W}\)/i DOMAIN = /domain\(#{W}(?:#{STRING}|#{URL})#{W}\)/i # Custom HEXCOLOR = /\#[0-9a-fA-F]+/ INTERP_START = /#\{/ ANY = /:(-[-\w]+-)?any\(/i OPTIONAL = /!#{W}optional/i IDENT_START = /-|#{NMSTART}/ IDENT_HYPHEN_INTERP = /-+(?=#\{)/ STRING1_NOINTERP = /\"((?:[^\n\r\f\\"#]|#(?!\{)|#{ESCAPE})*)\"/ STRING2_NOINTERP = /\'((?:[^\n\r\f\\'#]|#(?!\{)|#{ESCAPE})*)\'/ STRING_NOINTERP = /#{STRING1_NOINTERP}|#{STRING2_NOINTERP}/ STATIC_COMPONENT = /#{IDENT}|#{STRING_NOINTERP}|#{HEXCOLOR}|[+-]?#{NUMBER}|\!important/i STATIC_VALUE = %r(#{STATIC_COMPONENT}(\s*[\s,\/]\s*#{STATIC_COMPONENT})*(?=[;}]))i STATIC_SELECTOR = /(#{NMCHAR}|[ \t]|[,>+*]|[:#.]#{NMSTART}){1,50}([{])/i end end end ruby-sass-3.7.4/lib/sass/scss/static_parser.rb000066400000000000000000000236521345125207600213540ustar00rootroot00000000000000require 'sass/script/css_parser' module Sass module SCSS # A parser for a static SCSS tree. # Parses with SCSS extensions, like nested rules and parent selectors, # but without dynamic SassScript. # This is useful for e.g. \{#parse\_selector parsing selectors} # after resolving the interpolation. class StaticParser < Parser # Parses the text as a selector. # # @param filename [String, nil] The file in which the selector appears, # or nil if there is no such file. # Used for error reporting. # @return [Selector::CommaSequence] The parsed selector # @raise [Sass::SyntaxError] if there's a syntax error in the selector def parse_selector init_scanner! seq = expr!(:selector_comma_sequence) expected("selector") unless @scanner.eos? seq.line = @line seq.filename = @filename seq end # Parses a static at-root query. # # @return [(Symbol, Array)] The type of the query # (`:with` or `:without`) and the values that are being filtered. # @raise [Sass::SyntaxError] if there's a syntax error in the query, # or if it doesn't take up the entire input string. def parse_static_at_root_query init_scanner! tok!(/\(/); ss type = tok!(/\b(without|with)\b/).to_sym; ss tok!(/:/); ss directives = expr!(:at_root_directive_list); ss tok!(/\)/) expected("@at-root query list") unless @scanner.eos? return type, directives end def parse_keyframes_selector init_scanner! sel = expr!(:keyframes_selector) expected("keyframes selector") unless @scanner.eos? sel end # @see Parser#initialize # @param allow_parent_ref [Boolean] Whether to allow the # parent-reference selector, `&`, when parsing the document. def initialize(str, filename, importer, line = 1, offset = 1, allow_parent_ref = true) super(str, filename, importer, line, offset) @allow_parent_ref = allow_parent_ref end private def moz_document_function val = tok(URI) || tok(URL_PREFIX) || tok(DOMAIN) || function(false) return unless val ss [val] end def variable; nil; end def script_value; nil; end def interpolation(warn_for_color = false); nil; end def var_expr; nil; end def interp_string; (s = tok(STRING)) && [s]; end def interp_uri; (s = tok(URI)) && [s]; end def interp_ident; (s = ident) && [s]; end def use_css_import?; true; end def special_directive(name, start_pos) return unless %w(media import charset -moz-document).include?(name) super end def selector_comma_sequence sel = selector return unless sel selectors = [sel] ws = '' while tok(/,/) ws << str {ss} next unless (sel = selector) selectors << sel if ws.include?("\n") selectors[-1] = Selector::Sequence.new(["\n"] + selectors.last.members) end ws = '' end Selector::CommaSequence.new(selectors) end def selector_string sel = selector return unless sel sel.to_s end def selector start_pos = source_position # The combinator here allows the "> E" hack val = combinator || simple_selector_sequence return unless val nl = str {ss}.include?("\n") res = [] res << val res << "\n" if nl while (val = combinator || simple_selector_sequence) res << val res << "\n" if str {ss}.include?("\n") end seq = Selector::Sequence.new(res.compact) if seq.members.any? {|sseq| sseq.is_a?(Selector::SimpleSequence) && sseq.subject?} location = " of #{@filename}" if @filename Sass::Util.sass_warn < e e.message << "\n\n\"#{sel}\" may only be used at the beginning of a compound selector." raise e end end Selector::SimpleSequence.new(res, tok(/!/), range(start_pos)) end def parent_selector return unless @allow_parent_ref && tok(/&/) Selector::Parent.new(name) end def class_selector return unless tok(/\./) @expected = "class name" Selector::Class.new(ident!) end def id_selector return unless tok(/#(?!\{)/) @expected = "id name" Selector::Id.new(name!) end def placeholder_selector return unless tok(/%/) @expected = "placeholder name" Selector::Placeholder.new(ident!) end def element_name ns, name = Sass::Util.destructure(qualified_name(:allow_star_name)) return unless ns || name if name == '*' Selector::Universal.new(ns) else Selector::Element.new(name, ns) end end def qualified_name(allow_star_name = false) name = ident || tok(/\*/) || (tok?(/\|/) && "") return unless name return nil, name unless tok(/\|/) return name, ident! unless allow_star_name @expected = "identifier or *" return name, ident || tok!(/\*/) end def attrib return unless tok(/\[/) ss ns, name = attrib_name! ss op = tok(/=/) || tok(INCLUDES) || tok(DASHMATCH) || tok(PREFIXMATCH) || tok(SUFFIXMATCH) || tok(SUBSTRINGMATCH) if op @expected = "identifier or string" ss val = ident || tok!(STRING) ss end flags = ident || tok(STRING) tok!(/\]/) Selector::Attribute.new(name, ns, op, val, flags) end def attrib_name! if (name_or_ns = ident) # E, E|E if tok(/\|(?!=)/) ns = name_or_ns name = ident else name = name_or_ns end else # *|E or |E ns = tok(/\*/) || "" tok!(/\|/) name = ident! end return ns, name end SELECTOR_PSEUDO_CLASSES = %w(not matches current any has host host-context).to_set PREFIXED_SELECTOR_PSEUDO_CLASSES = %w(nth-child nth-last-child).to_set SELECTOR_PSEUDO_ELEMENTS = %w(slotted).to_set def pseudo s = tok(/::?/) return unless s @expected = "pseudoclass or pseudoelement" name = ident! if tok(/\(/) ss deprefixed = deprefix(name) if s == ':' && SELECTOR_PSEUDO_CLASSES.include?(deprefixed) sel = selector_comma_sequence elsif s == ':' && PREFIXED_SELECTOR_PSEUDO_CLASSES.include?(deprefixed) arg, sel = prefixed_selector_pseudo elsif s == '::' && SELECTOR_PSEUDO_ELEMENTS.include?(deprefixed) sel = selector_comma_sequence else arg = expr!(:declaration_value).join end tok!(/\)/) end Selector::Pseudo.new(s == ':' ? :class : :element, name, arg, sel) end def prefixed_selector_pseudo prefix = str do expr = str {expr!(:a_n_plus_b)} ss return expr, nil unless tok(/of/) ss end return prefix, expr!(:selector_comma_sequence) end def a_n_plus_b if (parity = tok(/even|odd/i)) return parity end if tok(/[+-]?[0-9]+/) ss return true unless tok(/n/) else return unless tok(/[+-]?n/i) end ss return true unless tok(/[+-]/) ss @expected = "number" tok!(/[0-9]+/) true end def keyframes_selector ss str do return unless keyframes_selector_component ss while tok(/,/) ss expr!(:keyframes_selector_component) ss end end end def keyframes_selector_component ident || tok(PERCENTAGE) end @sass_script_parser = Class.new(Sass::Script::CssParser) end end end ruby-sass-3.7.4/lib/sass/selector.rb000066400000000000000000000231031345125207600173450ustar00rootroot00000000000000require 'sass/selector/simple' require 'sass/selector/abstract_sequence' require 'sass/selector/comma_sequence' require 'sass/selector/pseudo' require 'sass/selector/sequence' require 'sass/selector/simple_sequence' module Sass # A namespace for nodes in the parse tree for selectors. # # {CommaSequence} is the toplevel selector, # representing a comma-separated sequence of {Sequence}s, # such as `foo bar, baz bang`. # {Sequence} is the next level, # representing {SimpleSequence}s separated by combinators (e.g. descendant or child), # such as `foo bar` or `foo > bar baz`. # {SimpleSequence} is a sequence of selectors that all apply to a single element, # such as `foo.bar[attr=val]`. # Finally, {Simple} is the superclass of the simplest selectors, # such as `.foo` or `#bar`. module Selector # The base used for calculating selector specificity. The spec says this # should be "sufficiently high"; it's extremely unlikely that any single # selector sequence will contain 1,000 simple selectors. SPECIFICITY_BASE = 1_000 # A parent-referencing selector (`&` in Sass). # The function of this is to be replaced by the parent selector # in the nested hierarchy. class Parent < Simple # The identifier following the `&`. `nil` indicates no suffix. # # @return [String, nil] attr_reader :suffix # @param name [String, nil] See \{#suffix} def initialize(suffix = nil) @suffix = suffix end # @see Selector#to_s def to_s(opts = {}) "&" + (@suffix || '') end # Always raises an exception. # # @raise [Sass::SyntaxError] Parent selectors should be resolved before unification # @see Selector#unify def unify(sels) raise Sass::SyntaxError.new("[BUG] Cannot unify parent selectors.") end end # A class selector (e.g. `.foo`). class Class < Simple # The class name. # # @return [String] attr_reader :name # @param name [String] The class name def initialize(name) @name = name end # @see Selector#to_s def to_s(opts = {}) "." + @name end # @see AbstractSequence#specificity def specificity SPECIFICITY_BASE end end # An id selector (e.g. `#foo`). class Id < Simple # The id name. # # @return [String] attr_reader :name # @param name [String] The id name def initialize(name) @name = name end def unique? true end # @see Selector#to_s def to_s(opts = {}) "#" + @name end # Returns `nil` if `sels` contains an {Id} selector # with a different name than this one. # # @see Selector#unify def unify(sels) return if sels.any? {|sel2| sel2.is_a?(Id) && name != sel2.name} super end # @see AbstractSequence#specificity def specificity SPECIFICITY_BASE**2 end end # A placeholder selector (e.g. `%foo`). # This exists to be replaced via `@extend`. # Rulesets using this selector will not be printed, but can be extended. # Otherwise, this acts just like a class selector. class Placeholder < Simple # The placeholder name. # # @return [String] attr_reader :name # @param name [String] The placeholder name def initialize(name) @name = name end # @see Selector#to_s def to_s(opts = {}) "%" + @name end # @see AbstractSequence#specificity def specificity SPECIFICITY_BASE end end # A universal selector (`*` in CSS). class Universal < Simple # The selector namespace. `nil` means the default namespace, `""` means no # namespace, `"*"` means any namespace. # # @return [String, nil] attr_reader :namespace # @param namespace [String, nil] See \{#namespace} def initialize(namespace) @namespace = namespace end # @see Selector#to_s def to_s(opts = {}) @namespace ? "#{@namespace}|*" : "*" end # Unification of a universal selector is somewhat complicated, # especially when a namespace is specified. # If there is no namespace specified # or any namespace is specified (namespace `"*"`), # then `sel` is returned without change # (unless it's empty, in which case `"*"` is required). # # If a namespace is specified # but `sel` does not specify a namespace, # then the given namespace is applied to `sel`, # either by adding this {Universal} selector # or applying this namespace to an existing {Element} selector. # # If both this selector *and* `sel` specify namespaces, # those namespaces are unified via {Simple#unify_namespaces} # and the unified namespace is used, if possible. # # @todo There are lots of cases that this documentation specifies; # make sure we thoroughly test **all of them**. # @todo Keep track of whether a default namespace has been declared # and handle namespace-unspecified selectors accordingly. # @todo If any branch of a CommaSequence ends up being just `"*"`, # then all other branches should be eliminated # # @see Selector#unify def unify(sels) name = case sels.first when Universal; :universal when Element; sels.first.name else return [self] + sels unless namespace.nil? || namespace == '*' return sels unless sels.empty? return [self] end ns, accept = unify_namespaces(namespace, sels.first.namespace) return unless accept [name == :universal ? Universal.new(ns) : Element.new(name, ns)] + sels[1..-1] end # @see AbstractSequence#specificity def specificity 0 end end # An element selector (e.g. `h1`). class Element < Simple # The element name. # # @return [String] attr_reader :name # The selector namespace. `nil` means the default namespace, `""` means no # namespace, `"*"` means any namespace. # # @return [String, nil] attr_reader :namespace # @param name [String] The element name # @param namespace [String, nil] See \{#namespace} def initialize(name, namespace) @name = name @namespace = namespace end # @see Selector#to_s def to_s(opts = {}) @namespace ? "#{@namespace}|#{@name}" : @name end # Unification of an element selector is somewhat complicated, # especially when a namespace is specified. # First, if `sel` contains another {Element} with a different \{#name}, # then the selectors can't be unified and `nil` is returned. # # Otherwise, if `sel` doesn't specify a namespace, # or it specifies any namespace (via `"*"`), # then it's returned with this element selector # (e.g. `.foo` becomes `a.foo` or `svg|a.foo`). # Similarly, if this selector doesn't specify a namespace, # the namespace from `sel` is used. # # If both this selector *and* `sel` specify namespaces, # those namespaces are unified via {Simple#unify_namespaces} # and the unified namespace is used, if possible. # # @todo There are lots of cases that this documentation specifies; # make sure we thoroughly test **all of them**. # @todo Keep track of whether a default namespace has been declared # and handle namespace-unspecified selectors accordingly. # # @see Selector#unify def unify(sels) case sels.first when Universal; when Element; return unless name == sels.first.name else return [self] + sels end ns, accept = unify_namespaces(namespace, sels.first.namespace) return unless accept [Element.new(name, ns)] + sels[1..-1] end # @see AbstractSequence#specificity def specificity 1 end end # An attribute selector (e.g. `[href^="http://"]`). class Attribute < Simple # The attribute name. # # @return [Array] attr_reader :name # The attribute namespace. `nil` means the default namespace, `""` means # no namespace, `"*"` means any namespace. # # @return [String, nil] attr_reader :namespace # The matching operator, e.g. `"="` or `"^="`. # # @return [String] attr_reader :operator # The right-hand side of the operator. # # @return [String] attr_reader :value # Flags for the attribute selector (e.g. `i`). # # @return [String] attr_reader :flags # @param name [String] The attribute name # @param namespace [String, nil] See \{#namespace} # @param operator [String] The matching operator, e.g. `"="` or `"^="` # @param value [String] See \{#value} # @param flags [String] See \{#flags} def initialize(name, namespace, operator, value, flags) @name = name @namespace = namespace @operator = operator @value = value @flags = flags end # @see Selector#to_s def to_s(opts = {}) res = "[" res << @namespace << "|" if @namespace res << @name res << @operator << @value if @value res << " " << @flags if @flags res << "]" end # @see AbstractSequence#specificity def specificity SPECIFICITY_BASE end end end end ruby-sass-3.7.4/lib/sass/selector/000077500000000000000000000000001345125207600170215ustar00rootroot00000000000000ruby-sass-3.7.4/lib/sass/selector/abstract_sequence.rb000066400000000000000000000066001345125207600230430ustar00rootroot00000000000000module Sass module Selector # The abstract parent class of the various selector sequence classes. # # All subclasses should implement a `members` method that returns an array # of object that respond to `#line=` and `#filename=`, as well as a `to_s` # method that returns the string representation of the selector. class AbstractSequence # The line of the Sass template on which this selector was declared. # # @return [Integer] attr_reader :line # The name of the file in which this selector was declared. # # @return [String, nil] attr_reader :filename # Sets the line of the Sass template on which this selector was declared. # This also sets the line for all child selectors. # # @param line [Integer] # @return [Integer] def line=(line) members.each {|m| m.line = line} @line = line end # Sets the name of the file in which this selector was declared, # or `nil` if it was not declared in a file (e.g. on stdin). # This also sets the filename for all child selectors. # # @param filename [String, nil] # @return [String, nil] def filename=(filename) members.each {|m| m.filename = filename} @filename = filename end # Returns a hash code for this sequence. # # Subclasses should define `#_hash` rather than overriding this method, # which automatically handles memoizing the result. # # @return [Integer] def hash @_hash ||= _hash end # Checks equality between this and another object. # # Subclasses should define `#_eql?` rather than overriding this method, # which handles checking class equality and hash equality. # # @param other [Object] The object to test equality against # @return [Boolean] Whether or not this is equal to `other` def eql?(other) other.class == self.class && other.hash == hash && _eql?(other) end alias_method :==, :eql? # Whether or not this selector should be hidden due to containing a # placeholder. def invisible? @invisible ||= members.any? do |m| next m.invisible? if m.is_a?(AbstractSequence) || m.is_a?(Pseudo) m.is_a?(Placeholder) end end # Returns the selector string. # # @param opts [Hash] rendering options. # @option opts [Symbol] :style The css rendering style. # @option placeholders [Boolean] :placeholders # Whether to include placeholder selectors. Defaults to `true`. # @return [String] def to_s(opts = {}) Sass::Util.abstract(self) end # Returns the specificity of the selector. # # The base is given by {Sass::Selector::SPECIFICITY_BASE}. This can be a # number or a range representing possible specificities. # # @return [Integer, Range] def specificity _specificity(members) end protected def _specificity(arr) min = 0 max = 0 arr.each do |m| next if m.is_a?(String) spec = m.specificity if spec.is_a?(Range) min += spec.begin max += spec.end else min += spec max += spec end end min == max ? min : (min..max) end end end end ruby-sass-3.7.4/lib/sass/selector/comma_sequence.rb000066400000000000000000000173661345125207600223470ustar00rootroot00000000000000module Sass module Selector # A comma-separated sequence of selectors. class CommaSequence < AbstractSequence @@compound_extend_deprecation = Sass::Deprecation.new # The comma-separated selector sequences # represented by this class. # # @return [Array] attr_reader :members # @param seqs [Array] See \{#members} def initialize(seqs) @members = seqs end # Resolves the {Parent} selectors within this selector # by replacing them with the given parent selector, # handling commas appropriately. # # @param super_cseq [CommaSequence] The parent selector # @param implicit_parent [Boolean] Whether the the parent # selector should automatically be prepended to the resolved # selector if it contains no parent refs. # @return [CommaSequence] This selector, with parent references resolved # @raise [Sass::SyntaxError] If a parent selector is invalid def resolve_parent_refs(super_cseq, implicit_parent = true) if super_cseq.nil? if contains_parent_ref? raise Sass::SyntaxError.new( "Base-level rules cannot contain the parent-selector-referencing character '&'.") end return self end CommaSequence.new(Sass::Util.flatten_vertically(@members.map do |seq| seq.resolve_parent_refs(super_cseq, implicit_parent).members end)) end # Returns whether there's a {Parent} selector anywhere in this sequence. # # @return [Boolean] def contains_parent_ref? @members.any? {|sel| sel.contains_parent_ref?} end # Non-destrucively extends this selector with the extensions specified in a hash # (which should come from {Sass::Tree::Visitors::Cssize}). # # @todo Link this to the reference documentation on `@extend` # when such a thing exists. # # @param extends [Sass::Util::SubsetMap{Selector::Simple => # Sass::Tree::Visitors::Cssize::Extend}] # The extensions to perform on this selector # @param parent_directives [Array] # The directives containing this selector. # @param replace [Boolean] # Whether to replace the original selector entirely or include # it in the result. # @param seen [Set>] # The set of simple sequences that are currently being replaced. # @param original [Boolean] # Whether this is the original selector being extended, as opposed to # the result of a previous extension that's being re-extended. # @return [CommaSequence] A copy of this selector, # with extensions made according to `extends` def do_extend(extends, parent_directives = [], replace = false, seen = Set.new, original = true) CommaSequence.new(members.map do |seq| seq.do_extend(extends, parent_directives, replace, seen, original) end.flatten) end # Returns whether or not this selector matches all elements # that the given selector matches (as well as possibly more). # # @example # (.foo).superselector?(.foo.bar) #=> true # (.foo).superselector?(.bar) #=> false # @param cseq [CommaSequence] # @return [Boolean] def superselector?(cseq) cseq.members.all? {|seq1| members.any? {|seq2| seq2.superselector?(seq1)}} end # Populates a subset map that can then be used to extend # selectors. This registers an extension with this selector as # the extender and `extendee` as the extendee. # # @param extends [Sass::Util::SubsetMap{Selector::Simple => # Sass::Tree::Visitors::Cssize::Extend}] # The subset map representing the extensions to perform. # @param extendee [CommaSequence] The selector being extended. # @param extend_node [Sass::Tree::ExtendNode] # The node that caused this extension. # @param parent_directives [Array] # The parent directives containing `extend_node`. # @param allow_compound_target [Boolean] # Whether `extendee` is allowed to contain compound selectors. # @raise [Sass::SyntaxError] if this extension is invalid. def populate_extends(extends, extendee, extend_node = nil, parent_directives = [], allow_compound_target = false) extendee.members.each do |seq| if seq.members.size > 1 raise Sass::SyntaxError.new("Can't extend #{seq}: can't extend nested selectors") end sseq = seq.members.first if !sseq.is_a?(Sass::Selector::SimpleSequence) raise Sass::SyntaxError.new("Can't extend #{seq}: invalid selector") elsif sseq.members.any? {|ss| ss.is_a?(Sass::Selector::Parent)} raise Sass::SyntaxError.new("Can't extend #{seq}: can't extend parent selectors") end sel = sseq.members if !allow_compound_target && sel.length > 1 @@compound_extend_deprecation.warn(sseq.filename, sseq.line, <] ACTUALLY_ELEMENTS = %w(after before first-line first-letter).to_set # Like \{#type}, but returns the type of selector this looks like, rather # than the type it is semantically. This only differs from type for # selectors in \{ACTUALLY\_ELEMENTS}. # # @return [Symbol] attr_reader :syntactic_type # The name of the selector. # # @return [String] attr_reader :name # The argument to the selector, # or `nil` if no argument was given. # # @return [String, nil] attr_reader :arg # The selector argument, or `nil` if no selector exists. # # If this and \{#arg\} are both set, \{#arg\} is considered a non-selector # prefix. # # @return [CommaSequence] attr_reader :selector # @param syntactic_type [Symbol] See \{#syntactic_type} # @param name [String] See \{#name} # @param arg [nil, String] See \{#arg} # @param selector [nil, CommaSequence] See \{#selector} def initialize(syntactic_type, name, arg, selector) @syntactic_type = syntactic_type @name = name @arg = arg @selector = selector end def unique? type == :class && normalized_name == 'root' end # Whether or not this selector should be hidden due to containing a # placeholder. def invisible? # :not() is a special case—if you eliminate all the placeholders from # it, it should match anything. name != 'not' && @selector && @selector.members.all? {|s| s.invisible?} end # Returns a copy of this with \{#selector} set to \{#new\_selector}. # # @param new_selector [CommaSequence] # @return [Array] def with_selector(new_selector) result = Pseudo.new(syntactic_type, name, arg, CommaSequence.new(new_selector.members.map do |seq| next seq unless seq.members.length == 1 sseq = seq.members.first next seq unless sseq.is_a?(SimpleSequence) && sseq.members.length == 1 sel = sseq.members.first next seq unless sel.is_a?(Pseudo) && sel.selector case normalized_name when 'not' # In theory, if there's a nested :not its contents should be # unified with the return value. For example, if :not(.foo) # extends .bar, :not(.bar) should become .foo:not(.bar). However, # this is a narrow edge case and supporting it properly would make # this code and the code calling it a lot more complicated, so # it's not supported for now. next [] unless sel.normalized_name == 'matches' sel.selector.members when 'matches', 'any', 'current', 'nth-child', 'nth-last-child' # As above, we could theoretically support :not within :matches, but # doing so would require this method and its callers to handle much # more complex cases that likely aren't worth the pain. next [] unless sel.name == name && sel.arg == arg sel.selector.members when 'has', 'host', 'host-context', 'slotted' # We can't expand nested selectors here, because each layer adds an # additional layer of semantics. For example, `:has(:has(img))` # doesn't match `
` but `:has(img)` does. sel else [] end end.flatten)) # Older browsers support :not but only with a single complex selector. # In order to support those browsers, we break up the contents of a :not # unless it originally contained a selector list. return [result] unless normalized_name == 'not' return [result] if selector.members.length > 1 result.selector.members.map do |seq| Pseudo.new(syntactic_type, name, arg, CommaSequence.new([seq])) end end # The type of the selector. `:class` if this is a pseudoclass selector, # `:element` if it's a pseudoelement. # # @return [Symbol] def type ACTUALLY_ELEMENTS.include?(normalized_name) ? :element : syntactic_type end # Like \{#name\}, but without any vendor prefix. # # @return [String] def normalized_name @normalized_name ||= name.gsub(/^-[a-zA-Z0-9]+-/, '') end # @see Selector#to_s def to_s(opts = {}) # :not() is a special case, because :not() should match # everything. return '' if name == 'not' && @selector && @selector.members.all? {|m| m.invisible?} res = (syntactic_type == :class ? ":" : "::") + @name if @arg || @selector res << "(" res << Sass::Util.strip_except_escapes(@arg) if @arg res << " " if @arg && @selector res << @selector.to_s(opts) if @selector res << ")" end res end # Returns `nil` if this is a pseudoelement selector # and `sels` contains a pseudoelement selector different than this one. # # @see SimpleSequence#unify def unify(sels) return if type == :element && sels.any? do |sel| sel.is_a?(Pseudo) && sel.type == :element && (sel.name != name || sel.arg != arg || sel.selector != selector) end super end # Returns whether or not this selector matches all elements # that the given selector matches (as well as possibly more). # # @example # (.foo).superselector?(.foo.bar) #=> true # (.foo).superselector?(.bar) #=> false # @param their_sseq [SimpleSequence] # @param parents [Array] The parent selectors of `their_sseq`, if any. # @return [Boolean] def superselector?(their_sseq, parents = []) case normalized_name when 'matches', 'any' # :matches can be a superselector of another selector in one of two # ways. Either its constituent selectors can be a superset of those of # another :matches in the other selector, or any of its constituent # selectors can individually be a superselector of the other selector. (their_sseq.selector_pseudo_classes[normalized_name] || []).any? do |their_sel| next false unless their_sel.is_a?(Pseudo) next false unless their_sel.name == name selector.superselector?(their_sel.selector) end || selector.members.any? do |our_seq| their_seq = Sequence.new(parents + [their_sseq]) our_seq.superselector?(their_seq) end when 'has', 'host', 'host-context', 'slotted' # Like :matches, :has (et al) can be a superselector of another # selector if its constituent selectors are a superset of those of # another :has in the other selector. However, the :matches other case # doesn't work, because :has refers to nested elements. (their_sseq.selector_pseudo_classes[normalized_name] || []).any? do |their_sel| next false unless their_sel.is_a?(Pseudo) next false unless their_sel.name == name selector.superselector?(their_sel.selector) end when 'not' selector.members.all? do |our_seq| their_sseq.members.any? do |their_sel| if their_sel.is_a?(Element) || their_sel.is_a?(Id) # `:not(a)` is a superselector of `h1` and `:not(#foo)` is a # superselector of `#bar`. our_sseq = our_seq.members.last next false unless our_sseq.is_a?(SimpleSequence) our_sseq.members.any? do |our_sel| our_sel.class == their_sel.class && our_sel != their_sel end else next false unless their_sel.is_a?(Pseudo) next false unless their_sel.name == name # :not(X) is a superselector of :not(Y) exactly when Y is a # superselector of X. their_sel.selector.superselector?(CommaSequence.new([our_seq])) end end end when 'current' (their_sseq.selector_pseudo_classes['current'] || []).any? do |their_current| next false if their_current.name != name # Explicitly don't check for nested superselector relationships # here. :current(.foo) isn't always a superselector of # :current(.foo.bar), since it matches the *innermost* ancestor of # the current element that matches the selector. For example: # #
#

# current element #

#
# # Here :current(.foo) would match the p element and *not* the div # element, whereas :current(.foo.bar) would match the div and not # the p. selector == their_current.selector end when 'nth-child', 'nth-last-child' their_sseq.members.any? do |their_sel| # This misses a few edge cases. For example, `:nth-child(n of X)` # is a superselector of `X`, and `:nth-child(2n of X)` is a # superselector of `:nth-child(4n of X)`. These seem rare enough # not to be worth worrying about, though. next false unless their_sel.is_a?(Pseudo) next false unless their_sel.name == name next false unless their_sel.arg == arg selector.superselector?(their_sel.selector) end else throw "[BUG] Unknown selector pseudo class #{name}" end end # @see AbstractSequence#specificity def specificity return 1 if type == :element return SPECIFICITY_BASE unless selector @specificity ||= if normalized_name == 'not' min = 0 max = 0 selector.members.each do |seq| spec = seq.specificity if spec.is_a?(Range) min = Sass::Util.max(spec.begin, min) max = Sass::Util.max(spec.end, max) else min = Sass::Util.max(spec, min) max = Sass::Util.max(spec, max) end end min == max ? max : (min..max) else min = 0 max = 0 selector.members.each do |seq| spec = seq.specificity if spec.is_a?(Range) min = Sass::Util.min(spec.begin, min) max = Sass::Util.max(spec.end, max) else min = Sass::Util.min(spec, min) max = Sass::Util.max(spec, max) end end min == max ? max : (min..max) end end end end end ruby-sass-3.7.4/lib/sass/selector/sequence.rb000066400000000000000000000637601345125207600211720ustar00rootroot00000000000000module Sass module Selector # An operator-separated sequence of # {SimpleSequence simple selector sequences}. class Sequence < AbstractSequence # Sets the line of the Sass template on which this selector was declared. # This also sets the line for all child selectors. # # @param line [Integer] # @return [Integer] def line=(line) members.each {|m| m.line = line if m.is_a?(SimpleSequence)} @line = line end # Sets the name of the file in which this selector was declared, # or `nil` if it was not declared in a file (e.g. on stdin). # This also sets the filename for all child selectors. # # @param filename [String, nil] # @return [String, nil] def filename=(filename) members.each {|m| m.filename = filename if m.is_a?(SimpleSequence)} filename end # The array of {SimpleSequence simple selector sequences}, operators, and # newlines. The operators are strings such as `"+"` and `">"` representing # the corresponding CSS operators, or interpolated SassScript. Newlines # are also newline strings; these aren't semantically relevant, but they # do affect formatting. # # @return [Array>] attr_reader :members # @param seqs_and_ops [Array>] # See \{#members} def initialize(seqs_and_ops) @members = seqs_and_ops end # Resolves the {Parent} selectors within this selector # by replacing them with the given parent selector, # handling commas appropriately. # # @param super_cseq [CommaSequence] The parent selector # @param implicit_parent [Boolean] Whether the the parent # selector should automatically be prepended to the resolved # selector if it contains no parent refs. # @return [CommaSequence] This selector, with parent references resolved # @raise [Sass::SyntaxError] If a parent selector is invalid def resolve_parent_refs(super_cseq, implicit_parent) members = @members.dup nl = (members.first == "\n" && members.shift) contains_parent_ref = contains_parent_ref? return CommaSequence.new([self]) if !implicit_parent && !contains_parent_ref unless contains_parent_ref old_members, members = members, [] members << nl if nl members << SimpleSequence.new([Parent.new], false) members += old_members end CommaSequence.new(Sass::Util.paths(members.map do |sseq_or_op| next [sseq_or_op] unless sseq_or_op.is_a?(SimpleSequence) sseq_or_op.resolve_parent_refs(super_cseq).members end).map do |path| path_members = path.map do |seq_or_op| next seq_or_op unless seq_or_op.is_a?(Sequence) seq_or_op.members end if path_members.length == 2 && path_members[1][0] == "\n" path_members[0].unshift path_members[1].shift end Sequence.new(path_members.flatten) end) end # Returns whether there's a {Parent} selector anywhere in this sequence. # # @return [Boolean] def contains_parent_ref? members.any? do |sseq_or_op| next false unless sseq_or_op.is_a?(SimpleSequence) next true if sseq_or_op.members.first.is_a?(Parent) sseq_or_op.members.any? do |sel| sel.is_a?(Pseudo) && sel.selector && sel.selector.contains_parent_ref? end end end # Non-destructively extends this selector with the extensions specified in a hash # (which should come from {Sass::Tree::Visitors::Cssize}). # # @param extends [Sass::Util::SubsetMap{Selector::Simple => # Sass::Tree::Visitors::Cssize::Extend}] # The extensions to perform on this selector # @param parent_directives [Array] # The directives containing this selector. # @param replace [Boolean] # Whether to replace the original selector entirely or include # it in the result. # @param seen [Set>] # The set of simple sequences that are currently being replaced. # @param original [Boolean] # Whether this is the original selector being extended, as opposed to # the result of a previous extension that's being re-extended. # @return [Array] A list of selectors generated # by extending this selector with `extends`. # These correspond to a {CommaSequence}'s {CommaSequence#members members array}. # @see CommaSequence#do_extend def do_extend(extends, parent_directives, replace, seen, original) extended_not_expanded = members.map do |sseq_or_op| next [[sseq_or_op]] unless sseq_or_op.is_a?(SimpleSequence) extended = sseq_or_op.do_extend(extends, parent_directives, replace, seen) # The First Law of Extend says that the generated selector should have # specificity greater than or equal to that of the original selector. # In order to ensure that, we record the original selector's # (`extended.first`) original specificity. extended.first.add_sources!([self]) if original && !invisible? extended.map {|seq| seq.members} end weaves = Sass::Util.paths(extended_not_expanded).map {|path| weave(path)} trim(weaves).map {|p| Sequence.new(p)} end # Unifies this with another selector sequence to produce a selector # that matches (a subset of) the intersection of the two inputs. # # @param other [Sequence] # @return [CommaSequence, nil] The unified selector, or nil if unification failed. # @raise [Sass::SyntaxError] If this selector cannot be unified. # This will only ever occur when a dynamic selector, # such as {Parent} or {Interpolation}, is used in unification. # Since these selectors should be resolved # by the time extension and unification happen, # this exception will only ever be raised as a result of programmer error def unify(other) base = members.last other_base = other.members.last return unless base.is_a?(SimpleSequence) && other_base.is_a?(SimpleSequence) return unless (unified = other_base.unify(base)) woven = weave([members[0...-1], other.members[0...-1] + [unified]]) CommaSequence.new(woven.map {|w| Sequence.new(w)}) end # Returns whether or not this selector matches all elements # that the given selector matches (as well as possibly more). # # @example # (.foo).superselector?(.foo.bar) #=> true # (.foo).superselector?(.bar) #=> false # @param cseq [Sequence] # @return [Boolean] def superselector?(seq) _superselector?(members, seq.members) end # @see AbstractSequence#to_s def to_s(opts = {}) @members.map {|m| m.is_a?(String) ? m : m.to_s(opts)}.join(" ").gsub(/ ?\n ?/, "\n") end # Returns a string representation of the sequence. # This is basically the selector string. # # @return [String] def inspect members.map {|m| m.inspect}.join(" ") end # Add to the {SimpleSequence#sources} sets of the child simple sequences. # This destructively modifies this sequence's members array, but not the # child simple sequences. # # @param sources [Set] def add_sources!(sources) members.map! {|m| m.is_a?(SimpleSequence) ? m.with_more_sources(sources) : m} end # Converts the subject operator "!", if it exists, into a ":has()" # selector. # # @retur [Sequence] def subjectless pre_subject = [] has = [] subject = nil members.each do |sseq_or_op| if subject has << sseq_or_op elsif sseq_or_op.is_a?(String) || !sseq_or_op.subject? pre_subject << sseq_or_op else subject = sseq_or_op.dup subject.members = sseq_or_op.members.dup subject.subject = false has = [] end end return self unless subject unless has.empty? subject.members << Pseudo.new(:class, 'has', nil, CommaSequence.new([Sequence.new(has)])) end Sequence.new(pre_subject + [subject]) end private # Conceptually, this expands "parenthesized selectors". That is, if we # have `.A .B {@extend .C}` and `.D .C {...}`, this conceptually expands # into `.D .C, .D (.A .B)`, and this function translates `.D (.A .B)` into # `.D .A .B, .A .D .B`. For thoroughness, `.A.D .B` would also be # required, but including merged selectors results in exponential output # for very little gain. # # @param path [Array>] # A list of parenthesized selector groups. # @return [Array>] A list of fully-expanded selectors. def weave(path) # This function works by moving through the selector path left-to-right, # building all possible prefixes simultaneously. prefixes = [[]] path.each do |current| next if current.empty? current = current.dup last_current = [current.pop] prefixes = prefixes.map do |prefix| sub = subweave(prefix, current) next [] unless sub sub.map {|seqs| seqs + last_current} end.flatten(1) end prefixes end # This interweaves two lists of selectors, # returning all possible orderings of them (including using unification) # that maintain the relative ordering of the input arrays. # # For example, given `.foo .bar` and `.baz .bang`, # this would return `.foo .bar .baz .bang`, `.foo .bar.baz .bang`, # `.foo .baz .bar .bang`, `.foo .baz .bar.bang`, `.foo .baz .bang .bar`, # and so on until `.baz .bang .foo .bar`. # # Semantically, for selectors A and B, this returns all selectors `AB_i` # such that the union over all i of elements matched by `AB_i X` is # identical to the intersection of all elements matched by `A X` and all # elements matched by `B X`. Some `AB_i` are elided to reduce the size of # the output. # # @param seq1 [Array] # @param seq2 [Array] # @return [Array>] def subweave(seq1, seq2) return [seq2] if seq1.empty? return [seq1] if seq2.empty? seq1, seq2 = seq1.dup, seq2.dup return unless (init = merge_initial_ops(seq1, seq2)) return unless (fin = merge_final_ops(seq1, seq2)) # Make sure there's only one root selector in the output. root1 = has_root?(seq1.first) && seq1.shift root2 = has_root?(seq2.first) && seq2.shift if root1 && root2 return unless (root = root1.unify(root2)) seq1.unshift root seq2.unshift root elsif root1 seq2.unshift root1 elsif root2 seq1.unshift root2 end seq1 = group_selectors(seq1) seq2 = group_selectors(seq2) lcs = Sass::Util.lcs(seq2, seq1) do |s1, s2| next s1 if s1 == s2 next unless s1.first.is_a?(SimpleSequence) && s2.first.is_a?(SimpleSequence) next s2 if parent_superselector?(s1, s2) next s1 if parent_superselector?(s2, s1) next unless must_unify?(s1, s2) next unless (unified = Sequence.new(s1).unify(Sequence.new(s2))) unified.members.first.members if unified.members.length == 1 end diff = [[init]] until lcs.empty? diff << chunks(seq1, seq2) {|s| parent_superselector?(s.first, lcs.first)} << [lcs.shift] seq1.shift seq2.shift end diff << chunks(seq1, seq2) {|s| s.empty?} diff += fin.map {|sel| sel.is_a?(Array) ? sel : [sel]} diff.reject! {|c| c.empty?} Sass::Util.paths(diff).map {|p| p.flatten}.reject {|p| path_has_two_subjects?(p)} end # Extracts initial selector combinators (`"+"`, `">"`, `"~"`, and `"\n"`) # from two sequences and merges them together into a single array of # selector combinators. # # @param seq1 [Array] # @param seq2 [Array] # @return [Array, nil] If there are no operators in the merged # sequence, this will be the empty array. If the operators cannot be # merged, this will be nil. def merge_initial_ops(seq1, seq2) ops1, ops2 = [], [] ops1 << seq1.shift while seq1.first.is_a?(String) ops2 << seq2.shift while seq2.first.is_a?(String) newline = false newline ||= !!ops1.shift if ops1.first == "\n" newline ||= !!ops2.shift if ops2.first == "\n" # If neither sequence is a subsequence of the other, they cannot be # merged successfully lcs = Sass::Util.lcs(ops1, ops2) return unless lcs == ops1 || lcs == ops2 (newline ? ["\n"] : []) + (ops1.size > ops2.size ? ops1 : ops2) end # Extracts final selector combinators (`"+"`, `">"`, `"~"`) and the # selectors to which they apply from two sequences and merges them # together into a single array. # # @param seq1 [Array] # @param seq2 [Array] # @return [Array>] # If there are no trailing combinators to be merged, this will be the # empty array. If the trailing combinators cannot be merged, this will # be nil. Otherwise, this will contained the merged selector. Array # elements are [Sass::Util#paths]-style options; conceptually, an "or" # of multiple selectors. def merge_final_ops(seq1, seq2, res = []) ops1, ops2 = [], [] ops1 << seq1.pop while seq1.last.is_a?(String) ops2 << seq2.pop while seq2.last.is_a?(String) # Not worth the headache of trying to preserve newlines here. The most # important use of newlines is at the beginning of the selector to wrap # across lines anyway. ops1.reject! {|o| o == "\n"} ops2.reject! {|o| o == "\n"} return res if ops1.empty? && ops2.empty? if ops1.size > 1 || ops2.size > 1 # If there are multiple operators, something hacky's going on. If one # is a supersequence of the other, use that, otherwise give up. lcs = Sass::Util.lcs(ops1, ops2) return unless lcs == ops1 || lcs == ops2 res.unshift(*(ops1.size > ops2.size ? ops1 : ops2).reverse) return res end # This code looks complicated, but it's actually just a bunch of special # cases for interactions between different combinators. op1, op2 = ops1.first, ops2.first if op1 && op2 sel1 = seq1.pop sel2 = seq2.pop if op1 == '~' && op2 == '~' if sel1.superselector?(sel2) res.unshift sel2, '~' elsif sel2.superselector?(sel1) res.unshift sel1, '~' else merged = sel1.unify(sel2) res.unshift [ [sel1, '~', sel2, '~'], [sel2, '~', sel1, '~'], ([merged, '~'] if merged) ].compact end elsif (op1 == '~' && op2 == '+') || (op1 == '+' && op2 == '~') if op1 == '~' tilde_sel, plus_sel = sel1, sel2 else tilde_sel, plus_sel = sel2, sel1 end if tilde_sel.superselector?(plus_sel) res.unshift plus_sel, '+' else merged = plus_sel.unify(tilde_sel) res.unshift [ [tilde_sel, '~', plus_sel, '+'], ([merged, '+'] if merged) ].compact end elsif op1 == '>' && %w(~ +).include?(op2) res.unshift sel2, op2 seq1.push sel1, op1 elsif op2 == '>' && %w(~ +).include?(op1) res.unshift sel1, op1 seq2.push sel2, op2 elsif op1 == op2 merged = sel1.unify(sel2) return unless merged res.unshift merged, op1 else # Unknown selector combinators can't be unified return end return merge_final_ops(seq1, seq2, res) elsif op1 seq2.pop if op1 == '>' && seq2.last && seq2.last.superselector?(seq1.last) res.unshift seq1.pop, op1 return merge_final_ops(seq1, seq2, res) else # op2 seq1.pop if op2 == '>' && seq1.last && seq1.last.superselector?(seq2.last) res.unshift seq2.pop, op2 return merge_final_ops(seq1, seq2, res) end end # Takes initial subsequences of `seq1` and `seq2` and returns all # orderings of those subsequences. The initial subsequences are determined # by a block. # # Destructively removes the initial subsequences of `seq1` and `seq2`. # # For example, given `(A B C | D E)` and `(1 2 | 3 4 5)` (with `|` # denoting the boundary of the initial subsequence), this would return # `[(A B C 1 2), (1 2 A B C)]`. The sequences would then be `(D E)` and # `(3 4 5)`. # # @param seq1 [Array] # @param seq2 [Array] # @yield [a] Used to determine when to cut off the initial subsequences. # Called repeatedly for each sequence until it returns true. # @yieldparam a [Array] A final subsequence of one input sequence after # cutting off some initial subsequence. # @yieldreturn [Boolean] Whether or not to cut off the initial subsequence # here. # @return [Array] All possible orderings of the initial subsequences. def chunks(seq1, seq2) chunk1 = [] chunk1 << seq1.shift until yield seq1 chunk2 = [] chunk2 << seq2.shift until yield seq2 return [] if chunk1.empty? && chunk2.empty? return [chunk2] if chunk1.empty? return [chunk1] if chunk2.empty? [chunk1 + chunk2, chunk2 + chunk1] end # Groups a sequence into subsequences. The subsequences are determined by # strings; adjacent non-string elements will be put into separate groups, # but any element adjacent to a string will be grouped with that string. # # For example, `(A B "C" D E "F" G "H" "I" J)` will become `[(A) (B "C" D) # (E "F" G "H" "I" J)]`. # # @param seq [Array] # @return [Array] def group_selectors(seq) newseq = [] tail = seq.dup until tail.empty? head = [] begin head << tail.shift end while !tail.empty? && head.last.is_a?(String) || tail.first.is_a?(String) newseq << head end newseq end # Given two selector sequences, returns whether `seq1` is a # superselector of `seq2`; that is, whether `seq1` matches every # element `seq2` matches. # # @param seq1 [Array] # @param seq2 [Array] # @return [Boolean] def _superselector?(seq1, seq2) seq1 = seq1.reject {|e| e == "\n"} seq2 = seq2.reject {|e| e == "\n"} # Selectors with leading or trailing operators are neither # superselectors nor subselectors. return if seq1.last.is_a?(String) || seq2.last.is_a?(String) || seq1.first.is_a?(String) || seq2.first.is_a?(String) # More complex selectors are never superselectors of less complex ones return if seq1.size > seq2.size return seq1.first.superselector?(seq2.last, seq2[0...-1]) if seq1.size == 1 _, si = seq2.each_with_index.find do |e, i| return if i == seq2.size - 1 next if e.is_a?(String) seq1.first.superselector?(e, seq2[0...i]) end return unless si if seq1[1].is_a?(String) return unless seq2[si + 1].is_a?(String) # .foo ~ .bar is a superselector of .foo + .bar return unless seq1[1] == "~" ? seq2[si + 1] != ">" : seq1[1] == seq2[si + 1] # .foo > .baz is not a superselector of .foo > .bar > .baz or .foo > # .bar .baz, despite the fact that .baz is a superselector of .bar > # .baz and .bar .baz. Same goes for + and ~. return if seq1.length == 3 && seq2.length > 3 return _superselector?(seq1[2..-1], seq2[si + 2..-1]) elsif seq2[si + 1].is_a?(String) return unless seq2[si + 1] == ">" return _superselector?(seq1[1..-1], seq2[si + 2..-1]) else return _superselector?(seq1[1..-1], seq2[si + 1..-1]) end end # Like \{#_superselector?}, but compares the selectors in the # context of parent selectors, as though they shared an implicit # base simple selector. For example, `B` is not normally a # superselector of `B A`, since it doesn't match `A` elements. # However, it is a parent superselector, since `B X` is a # superselector of `B A X`. # # @param seq1 [Array] # @param seq2 [Array] # @return [Boolean] def parent_superselector?(seq1, seq2) base = Sass::Selector::SimpleSequence.new([Sass::Selector::Placeholder.new('')], false) _superselector?(seq1 + [base], seq2 + [base]) end # Returns whether two selectors must be unified to produce a valid # combined selector. This is true when both selectors contain the same # unique simple selector such as an id. # # @param seq1 [Array] # @param seq2 [Array] # @return [Boolean] def must_unify?(seq1, seq2) unique_selectors = seq1.map do |sseq| next [] if sseq.is_a?(String) sseq.members.select {|sel| sel.unique?} end.flatten.to_set return false if unique_selectors.empty? seq2.any? do |sseq| next false if sseq.is_a?(String) sseq.members.any? do |sel| next unless sel.unique? unique_selectors.include?(sel) end end end # Removes redundant selectors from between multiple lists of # selectors. This takes a list of lists of selector sequences; # each individual list is assumed to have no redundancy within # itself. A selector is only removed if it's redundant with a # selector in another list. # # "Redundant" here means that one selector is a superselector of # the other. The more specific selector is removed. # # @param seqses [Array>>] # @return [Array>] def trim(seqses) # Avoid truly horrific quadratic behavior. TODO: I think there # may be a way to get perfect trimming without going quadratic. return seqses.flatten(1) if seqses.size > 100 # Keep the results in a separate array so we can be sure we aren't # comparing against an already-trimmed selector. This ensures that two # identical selectors don't mutually trim one another. result = seqses.dup # This is n^2 on the sequences, but only comparing between # separate sequences should limit the quadratic behavior. seqses.each_with_index do |seqs1, i| result[i] = seqs1.reject do |seq1| # The maximum specificity of the sources that caused [seq1] to be # generated. In order for [seq1] to be removed, there must be # another selector that's a superselector of it *and* that has # specificity greater or equal to this. max_spec = _sources(seq1).map do |seq| spec = seq.specificity spec.is_a?(Range) ? spec.max : spec end.max || 0 result.any? do |seqs2| next if seqs1.equal?(seqs2) # Second Law of Extend: the specificity of a generated selector # should never be less than the specificity of the extending # selector. # # See https://github.com/nex3/sass/issues/324. seqs2.any? do |seq2| spec2 = _specificity(seq2) spec2 = spec2.begin if spec2.is_a?(Range) spec2 >= max_spec && _superselector?(seq2, seq1) end end end end result.flatten(1) end def _hash members.reject {|m| m == "\n"}.hash end def _eql?(other) other.members.reject {|m| m == "\n"}.eql?(members.reject {|m| m == "\n"}) end def path_has_two_subjects?(path) subject = false path.each do |sseq_or_op| next unless sseq_or_op.is_a?(SimpleSequence) next unless sseq_or_op.subject? return true if subject subject = true end false end def _sources(seq) s = Set.new seq.map {|sseq_or_op| s.merge sseq_or_op.sources if sseq_or_op.is_a?(SimpleSequence)} s end def extended_not_expanded_to_s(extended_not_expanded) extended_not_expanded.map do |choices| choices = choices.map do |sel| next sel.first.to_s if sel.size == 1 "#{sel.join ' '}" end next choices.first if choices.size == 1 && !choices.include?(' ') "(#{choices.join ', '})" end.join ' ' end def has_root?(sseq) sseq.is_a?(SimpleSequence) && sseq.members.any? {|sel| sel.is_a?(Pseudo) && sel.normalized_name == "root"} end end end end ruby-sass-3.7.4/lib/sass/selector/simple.rb000066400000000000000000000107731345125207600206470ustar00rootroot00000000000000module Sass module Selector # The abstract superclass for simple selectors # (that is, those that don't compose multiple selectors). class Simple # The line of the Sass template on which this selector was declared. # # @return [Integer] attr_accessor :line # The name of the file in which this selector was declared, # or `nil` if it was not declared in a file (e.g. on stdin). # # @return [String, nil] attr_accessor :filename # Whether only one instance of this simple selector is allowed in a given # complex selector. # # @return [Boolean] def unique? false end # @see #to_s # # @return [String] def inspect to_s end # Returns the selector string. # # @param opts [Hash] rendering options. # @option opts [Symbol] :style The css rendering style. # @return [String] def to_s(opts = {}) Sass::Util.abstract(self) end # Returns a hash code for this selector object. # # By default, this is based on the value of \{#to\_a}, # so if that contains information irrelevant to the identity of the selector, # this should be overridden. # # @return [Integer] def hash @_hash ||= equality_key.hash end # Checks equality between this and another object. # # By default, this is based on the value of \{#to\_a}, # so if that contains information irrelevant to the identity of the selector, # this should be overridden. # # @param other [Object] The object to test equality against # @return [Boolean] Whether or not this is equal to `other` def eql?(other) other.class == self.class && other.hash == hash && other.equality_key == equality_key end alias_method :==, :eql? # Unifies this selector with a {SimpleSequence}'s {SimpleSequence#members members array}, # returning another `SimpleSequence` members array # that matches both this selector and the input selector. # # By default, this just appends this selector to the end of the array # (or returns the original array if this selector already exists in it). # # @param sels [Array] A {SimpleSequence}'s {SimpleSequence#members members array} # @return [Array, nil] A {SimpleSequence} {SimpleSequence#members members array} # matching both `sels` and this selector, # or `nil` if this is impossible (e.g. unifying `#foo` and `#bar`) # @raise [Sass::SyntaxError] If this selector cannot be unified. # This will only ever occur when a dynamic selector, # such as {Parent} or {Interpolation}, is used in unification. # Since these selectors should be resolved # by the time extension and unification happen, # this exception will only ever be raised as a result of programmer error def unify(sels) return sels.first.unify([self]) if sels.length == 1 && sels.first.is_a?(Universal) return sels if sels.any? {|sel2| eql?(sel2)} if !is_a?(Pseudo) || (sels.last.is_a?(Pseudo) && sels.last.type == :element) _, i = sels.each_with_index.find {|sel, _| sel.is_a?(Pseudo)} end return sels + [self] unless i sels[0...i] + [self] + sels[i..-1] end protected # Returns the key used for testing whether selectors are equal. # # This is a cached version of \{#to\_s}. # # @return [String] def equality_key @equality_key ||= to_s end # Unifies two namespaces, # returning a namespace that works for both of them if possible. # # @param ns1 [String, nil] The first namespace. # `nil` means none specified, e.g. `foo`. # The empty string means no namespace specified, e.g. `|foo`. # `"*"` means any namespace is allowed, e.g. `*|foo`. # @param ns2 [String, nil] The second namespace. See `ns1`. # @return [Array(String or nil, Boolean)] # The first value is the unified namespace, or `nil` for no namespace. # The second value is whether or not a namespace that works for both inputs # could be found at all. # If the second value is `false`, the first should be ignored. def unify_namespaces(ns1, ns2) return ns2, true if ns1 == '*' return ns1, true if ns2 == '*' return nil, false unless ns1 == ns2 [ns1, true] end end end end ruby-sass-3.7.4/lib/sass/selector/simple_sequence.rb000066400000000000000000000327641345125207600225430ustar00rootroot00000000000000module Sass module Selector # A unseparated sequence of selectors # that all apply to a single element. # For example, `.foo#bar[attr=baz]` is a simple sequence # of the selectors `.foo`, `#bar`, and `[attr=baz]`. class SimpleSequence < AbstractSequence # The array of individual selectors. # # @return [Array] attr_accessor :members # The extending selectors that caused this selector sequence to be # generated. For example: # # a.foo { ... } # b.bar {@extend a} # c.baz {@extend b} # # The generated selector `b.foo.bar` has `{b.bar}` as its `sources` set, # and the generated selector `c.foo.bar.baz` has `{b.bar, c.baz}` as its # `sources` set. # # This is populated during the {Sequence#do_extend} process. # # @return {Set} attr_accessor :sources # This sequence source range. # # @return [Sass::Source::Range] attr_accessor :source_range # @see \{#subject?} attr_writer :subject # Returns the element or universal selector in this sequence, # if it exists. # # @return [Element, Universal, nil] def base @base ||= (members.first if members.first.is_a?(Element) || members.first.is_a?(Universal)) end def pseudo_elements @pseudo_elements ||= members.select {|sel| sel.is_a?(Pseudo) && sel.type == :element} end def selector_pseudo_classes @selector_pseudo_classes ||= members. select {|sel| sel.is_a?(Pseudo) && sel.type == :class && sel.selector}. group_by {|sel| sel.normalized_name} end # Returns the non-base, non-pseudo-element selectors in this sequence. # # @return [Set] def rest @rest ||= Set.new(members - [base] - pseudo_elements) end # Whether or not this compound selector is the subject of the parent # selector; that is, whether it is prepended with `$` and represents the # actual element that will be selected. # # @return [Boolean] def subject? @subject end # @param selectors [Array] See \{#members} # @param subject [Boolean] See \{#subject?} # @param source_range [Sass::Source::Range] def initialize(selectors, subject, source_range = nil) @members = selectors @subject = subject @sources = Set.new @source_range = source_range end # Resolves the {Parent} selectors within this selector # by replacing them with the given parent selector, # handling commas appropriately. # # @param super_cseq [CommaSequence] The parent selector # @return [CommaSequence] This selector, with parent references resolved # @raise [Sass::SyntaxError] If a parent selector is invalid def resolve_parent_refs(super_cseq) resolved_members = @members.map do |sel| next sel unless sel.is_a?(Pseudo) && sel.selector sel.with_selector(sel.selector.resolve_parent_refs(super_cseq, false)) end.flatten # Parent selector only appears as the first selector in the sequence unless (parent = resolved_members.first).is_a?(Parent) return CommaSequence.new([Sequence.new([SimpleSequence.new(resolved_members, subject?)])]) end return super_cseq if @members.size == 1 && parent.suffix.nil? CommaSequence.new(super_cseq.members.map do |super_seq| members = super_seq.members.dup newline = members.pop if members.last == "\n" unless members.last.is_a?(SimpleSequence) raise Sass::SyntaxError.new("Invalid parent selector for \"#{self}\": \"" + super_seq.to_s + '"') end parent_sub = members.last.members unless parent.suffix.nil? parent_sub = parent_sub.dup parent_sub[-1] = parent_sub.last.dup case parent_sub.last when Sass::Selector::Class, Sass::Selector::Id, Sass::Selector::Placeholder parent_sub[-1] = parent_sub.last.class.new(parent_sub.last.name + parent.suffix) when Sass::Selector::Element parent_sub[-1] = parent_sub.last.class.new( parent_sub.last.name + parent.suffix, parent_sub.last.namespace) when Sass::Selector::Pseudo if parent_sub.last.arg || parent_sub.last.selector raise Sass::SyntaxError.new("Invalid parent selector for \"#{self}\": \"" + super_seq.to_s + '"') end parent_sub[-1] = Sass::Selector::Pseudo.new( parent_sub.last.type, parent_sub.last.name + parent.suffix, nil, nil) else raise Sass::SyntaxError.new("Invalid parent selector for \"#{self}\": \"" + super_seq.to_s + '"') end end Sequence.new(members[0...-1] + [SimpleSequence.new(parent_sub + resolved_members[1..-1], subject?)] + [newline].compact) end) end # Non-destructively extends this selector with the extensions specified in a hash # (which should come from {Sass::Tree::Visitors::Cssize}). # # @param extends [{Selector::Simple => # Sass::Tree::Visitors::Cssize::Extend}] # The extensions to perform on this selector # @param parent_directives [Array] # The directives containing this selector. # @param seen [Set>] # The set of simple sequences that are currently being replaced. # @param original [Boolean] # Whether this is the original selector being extended, as opposed to # the result of a previous extension that's being re-extended. # @return [Array] A list of selectors generated # by extending this selector with `extends`. # @see CommaSequence#do_extend def do_extend(extends, parent_directives, replace, seen) seen_with_pseudo_selectors = seen.dup modified_original = false members = self.members.map do |sel| next sel unless sel.is_a?(Pseudo) && sel.selector next sel if seen.include?([sel]) extended = sel.selector.do_extend(extends, parent_directives, replace, seen, false) next sel if extended == sel.selector extended.members.reject! {|seq| seq.invisible?} # For `:not()`, we usually want to get rid of any complex # selectors because that will cause the selector to fail to # parse on all browsers at time of writing. We can keep them # if either the original selector had a complex selector, or # the result of extending has only complex selectors, # because either way we aren't breaking anything that isn't # already broken. if sel.normalized_name == 'not' && (sel.selector.members.none? {|seq| seq.members.length > 1} && extended.members.any? {|seq| seq.members.length == 1}) extended.members.reject! {|seq| seq.members.length > 1} end modified_original = true result = sel.with_selector(extended) result.each {|new_sel| seen_with_pseudo_selectors << [new_sel]} result end.flatten groups = extends[members.to_set].group_by {|ex| ex.extender}.to_a groups.map! do |seq, group| sels = group.map {|e| e.target}.flatten # If A {@extend B} and C {...}, # seq is A, sels is B, and self is C self_without_sel = Sass::Util.array_minus(members, sels) group.each {|e| e.success = true} unified = seq.members.last.unify(SimpleSequence.new(self_without_sel, subject?)) next unless unified group.each {|e| check_directives_match!(e, parent_directives)} new_seq = Sequence.new(seq.members[0...-1] + [unified]) new_seq.add_sources!(sources + [seq]) [sels, new_seq] end groups.compact! groups.map! do |sels, seq| next [] if seen.include?(sels) seq.do_extend( extends, parent_directives, false, seen_with_pseudo_selectors + [sels], false) end groups.flatten! if modified_original || !replace || groups.empty? # First Law of Extend: the result of extending a selector should # (almost) always contain the base selector. # # See https://github.com/nex3/sass/issues/324. original = Sequence.new([SimpleSequence.new(members, @subject, source_range)]) original.add_sources! sources groups.unshift original end groups.uniq! groups end # Unifies this selector with another {SimpleSequence}, returning # another `SimpleSequence` that is a subselector of both input # selectors. # # @param other [SimpleSequence] # @return [SimpleSequence, nil] A {SimpleSequence} matching both `sels` and this selector, # or `nil` if this is impossible (e.g. unifying `#foo` and `#bar`) # @raise [Sass::SyntaxError] If this selector cannot be unified. # This will only ever occur when a dynamic selector, # such as {Parent} or {Interpolation}, is used in unification. # Since these selectors should be resolved # by the time extension and unification happen, # this exception will only ever be raised as a result of programmer error def unify(other) sseq = members.inject(other.members) do |member, sel| return unless member sel.unify(member) end return unless sseq SimpleSequence.new(sseq, other.subject? || subject?) end # Returns whether or not this selector matches all elements # that the given selector matches (as well as possibly more). # # @example # (.foo).superselector?(.foo.bar) #=> true # (.foo).superselector?(.bar) #=> false # @param their_sseq [SimpleSequence] # @param parents [Array] The parent selectors of `their_sseq`, if any. # @return [Boolean] def superselector?(their_sseq, parents = []) return false unless base.nil? || base.eql?(their_sseq.base) return false unless pseudo_elements.eql?(their_sseq.pseudo_elements) our_spcs = selector_pseudo_classes their_spcs = their_sseq.selector_pseudo_classes # Some psuedo-selectors can be subselectors of non-pseudo selectors. # Pull those out here so we can efficiently check against them below. their_subselector_pseudos = %w(matches any nth-child nth-last-child). map {|name| their_spcs[name] || []}.flatten # If `self`'s non-pseudo simple selectors aren't a subset of `their_sseq`'s, # it's definitely not a superselector. This also considers being matched # by `:matches` or `:any`. return false unless rest.all? do |our_sel| next true if our_sel.is_a?(Pseudo) && our_sel.selector next true if their_sseq.rest.include?(our_sel) their_subselector_pseudos.any? do |their_pseudo| their_pseudo.selector.members.all? do |their_seq| next false unless their_seq.members.length == 1 their_sseq = their_seq.members.first next false unless their_sseq.is_a?(SimpleSequence) their_sseq.rest.include?(our_sel) end end end our_spcs.all? do |_name, pseudos| pseudos.all? {|pseudo| pseudo.superselector?(their_sseq, parents)} end end # @see Simple#to_s def to_s(opts = {}) res = @members.map {|m| m.to_s(opts)}.join # :not(%foo) may resolve to the empty string, but it should match every # selector so we replace it with "*". res = '*' if res.empty? res << '!' if subject? res end # Returns a string representation of the sequence. # This is basically the selector string. # # @return [String] def inspect res = members.map {|m| m.inspect}.join res << '!' if subject? res end # Return a copy of this simple sequence with `sources` merged into the # {SimpleSequence#sources} set. # # @param sources [Set] # @return [SimpleSequence] def with_more_sources(sources) sseq = dup sseq.members = members.dup sseq.sources = self.sources | sources sseq end private def check_directives_match!(extend, parent_directives) dirs1 = extend.directives.map {|d| d.resolved_value} dirs2 = parent_directives.map {|d| d.resolved_value} return if Sass::Util.subsequence?(dirs1, dirs2) line = extend.node.line filename = extend.node.filename # TODO(nweiz): this should use the Sass stack trace of the extend node, # not the selector. raise Sass::SyntaxError.new(< #{output.inspect}" end end # The mapping data ordered by the location in the target. # # @return [Array] attr_reader :data def initialize @data = [] end # Adds a new mapping from one source range to another. Multiple invocations # of this method should have each `output` range come after all previous ranges. # # @param input [Sass::Source::Range] # The source range in the input document. # @param output [Sass::Source::Range] # The source range in the output document. def add(input, output) @data.push(Mapping.new(input, output)) end # Shifts all output source ranges forward one or more lines. # # @param delta [Integer] The number of lines to shift the ranges forward. def shift_output_lines(delta) return if delta == 0 @data.each do |m| m.output.start_pos.line += delta m.output.end_pos.line += delta end end # Shifts any output source ranges that lie on the first line forward one or # more characters on that line. # # @param delta [Integer] The number of characters to shift the ranges # forward. def shift_output_offsets(delta) return if delta == 0 @data.each do |m| break if m.output.start_pos.line > 1 m.output.start_pos.offset += delta m.output.end_pos.offset += delta if m.output.end_pos.line > 1 end end # Returns the standard JSON representation of the source map. # # If the `:css_uri` option isn't specified, the `:css_path` and # `:sourcemap_path` options must both be specified. Any options may also be # specified alongside the `:css_uri` option. If `:css_uri` isn't specified, # it will be inferred from `:css_path` and `:sourcemap_path` using the # assumption that the local file system has the same layout as the server. # # Regardless of which options are passed to this method, source stylesheets # that are imported using a non-default importer will only be linked to in # the source map if their importers implement # \{Sass::Importers::Base#public\_url\}. # # @option options :css_uri [String] # The publicly-visible URI of the CSS output file. # @option options :css_path [String] # The local path of the CSS output file. # @option options :sourcemap_path [String] # The (eventual) local path of the sourcemap file. # @option options :type [Symbol] # `:auto` (default), `:file`, or `:inline`. # @return [String] The JSON string. # @raise [ArgumentError] If neither `:css_uri` nor `:css_path` and # `:sourcemap_path` are specified. def to_json(options) css_uri, css_path, sourcemap_path = options[:css_uri], options[:css_path], options[:sourcemap_path] unless css_uri || (css_path && sourcemap_path) raise ArgumentError.new("Sass::Source::Map#to_json requires either " \ "the :css_uri option or both the :css_path and :soucemap_path options.") end css_path &&= Sass::Util.pathname(File.absolute_path(css_path)) sourcemap_path &&= Sass::Util.pathname(File.absolute_path(sourcemap_path)) css_uri ||= Sass::Util.file_uri_from_path( Sass::Util.relative_path_from(css_path, sourcemap_path.dirname)) result = "{\n" write_json_field(result, "version", 3, true) source_uri_to_id = {} id_to_source_uri = {} id_to_contents = {} if options[:type] == :inline next_source_id = 0 line_data = [] segment_data_for_line = [] # These track data necessary for the delta coding. previous_target_line = nil previous_target_offset = 1 previous_source_line = 1 previous_source_offset = 1 previous_source_id = 0 @data.each do |m| file, importer = m.input.file, m.input.importer next unless importer if options[:type] == :inline source_uri = file else sourcemap_dir = sourcemap_path && sourcemap_path.dirname.to_s sourcemap_dir = nil if options[:type] == :file source_uri = importer.public_url(file, sourcemap_dir) next unless source_uri end current_source_id = source_uri_to_id[source_uri] unless current_source_id current_source_id = next_source_id next_source_id += 1 source_uri_to_id[source_uri] = current_source_id id_to_source_uri[current_source_id] = source_uri if options[:type] == :inline id_to_contents[current_source_id] = importer.find(file, {}).instance_variable_get('@template') end end [ [m.input.start_pos, m.output.start_pos], [m.input.end_pos, m.output.end_pos] ].each do |source_pos, target_pos| if previous_target_line != target_pos.line line_data.push(segment_data_for_line.join(",")) unless segment_data_for_line.empty? (target_pos.line - 1 - (previous_target_line || 0)).times {line_data.push("")} previous_target_line = target_pos.line previous_target_offset = 1 segment_data_for_line = [] end # `segment` is a data chunk for a single position mapping. segment = "" # Field 1: zero-based starting offset. segment << Sass::Util.encode_vlq(target_pos.offset - previous_target_offset) previous_target_offset = target_pos.offset # Field 2: zero-based index into the "sources" list. segment << Sass::Util.encode_vlq(current_source_id - previous_source_id) previous_source_id = current_source_id # Field 3: zero-based starting line in the original source. segment << Sass::Util.encode_vlq(source_pos.line - previous_source_line) previous_source_line = source_pos.line # Field 4: zero-based starting offset in the original source. segment << Sass::Util.encode_vlq(source_pos.offset - previous_source_offset) previous_source_offset = source_pos.offset segment_data_for_line.push(segment) previous_target_line = target_pos.line end end line_data.push(segment_data_for_line.join(",")) write_json_field(result, "mappings", line_data.join(";")) source_names = [] (0...next_source_id).each {|id| source_names.push(id_to_source_uri[id].to_s)} write_json_field(result, "sources", source_names) if options[:type] == :inline write_json_field(result, "sourcesContent", (0...next_source_id).map {|id| id_to_contents[id]}) end write_json_field(result, "names", []) write_json_field(result, "file", css_uri) result << "\n}" result end private def write_json_field(out, name, value, is_first = false) out << (is_first ? "" : ",\n") << "\"" << Sass::Util.json_escape_string(name) << "\": " << Sass::Util.json_value_of(value) end end end ruby-sass-3.7.4/lib/sass/source/position.rb000066400000000000000000000017531345125207600207000ustar00rootroot00000000000000module Sass::Source class Position # The one-based line of the document associated with the position. # # @return [Integer] attr_accessor :line # The one-based offset in the line of the document associated with the # position. # # @return [Integer] attr_accessor :offset # @param line [Integer] The source line # @param offset [Integer] The source offset def initialize(line, offset) @line = line @offset = offset end # @return [String] A string representation of the source position. def inspect "#{line.inspect}:#{offset.inspect}" end # @param str [String] The string to move through. # @return [Position] The source position after proceeding forward through # `str`. def after(str) newlines = str.count("\n") Position.new(line + newlines, if newlines == 0 offset + str.length else str.length - str.rindex("\n") - 1 end) end end end ruby-sass-3.7.4/lib/sass/source/range.rb000066400000000000000000000023561345125207600201300ustar00rootroot00000000000000module Sass::Source class Range # The starting position of the range in the document (inclusive). # # @return [Sass::Source::Position] attr_accessor :start_pos # The ending position of the range in the document (exclusive). # # @return [Sass::Source::Position] attr_accessor :end_pos # The file in which this source range appears. This can be nil if the file # is unknown or not yet generated. # # @return [String] attr_accessor :file # The importer that imported the file in which this source range appears. # This is nil for target ranges. # # @return [Sass::Importers::Base] attr_accessor :importer # @param start_pos [Sass::Source::Position] See \{#start_pos} # @param end_pos [Sass::Source::Position] See \{#end_pos} # @param file [String] See \{#file} # @param importer [Sass::Importers::Base] See \{#importer} def initialize(start_pos, end_pos, file, importer = nil) @start_pos = start_pos @end_pos = end_pos @file = file @importer = importer end # @return [String] A string representation of the source range. def inspect "(#{start_pos.inspect} to #{end_pos.inspect}#{" in #{@file}" if @file})" end end end ruby-sass-3.7.4/lib/sass/stack.rb000066400000000000000000000100711345125207600166320ustar00rootroot00000000000000module Sass # A class representing the stack when compiling a Sass file. class Stack # TODO: use this to generate stack information for Sass::SyntaxErrors. # A single stack frame. class Frame # The filename of the file in which this stack frame was created. # # @return [String] attr_reader :filename # The line number on which this stack frame was created. # # @return [String] attr_reader :line # The type of this stack frame. This can be `:import`, `:mixin`, or # `:base`. # # `:base` indicates that this is the bottom-most frame, meaning that it # represents a single line of code rather than a nested context. The stack # will only ever have one base frame, and it will always be the most # deeply-nested frame. # # @return [Symbol?] attr_reader :type # The name of the stack frame. For mixin frames, this is the mixin name; # otherwise, it's `nil`. # # @return [String?] attr_reader :name def initialize(filename, line, type, name = nil) @filename = filename @line = line @type = type @name = name end # Whether this frame represents an import. # # @return [Boolean] def is_import? type == :import end # Whether this frame represents a mixin. # # @return [Boolean] def is_mixin? type == :mixin end # Whether this is the base frame. # # @return [Boolean] def is_base? type == :base end end # The stack frames. The last frame is the most deeply-nested. # # @return [Array] attr_reader :frames def initialize @frames = [] end # Pushes a base frame onto the stack. # # @param filename [String] See \{Frame#filename}. # @param line [String] See \{Frame#line}. # @yield [] A block in which the new frame is on the stack. def with_base(filename, line) with_frame(filename, line, :base) {yield} end # Pushes an import frame onto the stack. # # @param filename [String] See \{Frame#filename}. # @param line [String] See \{Frame#line}. # @yield [] A block in which the new frame is on the stack. def with_import(filename, line) with_frame(filename, line, :import) {yield} end # Pushes a mixin frame onto the stack. # # @param filename [String] See \{Frame#filename}. # @param line [String] See \{Frame#line}. # @param name [String] See \{Frame#name}. # @yield [] A block in which the new frame is on the stack. def with_mixin(filename, line, name) with_frame(filename, line, :mixin, name) {yield} end # Pushes a function frame onto the stack. # # @param filename [String] See \{Frame#filename}. # @param line [String] See \{Frame#line}. # @param name [String] See \{Frame#name}. # @yield [] A block in which the new frame is on the stack. def with_function(filename, line, name) with_frame(filename, line, :function, name) {yield} end # Pushes a function frame onto the stack. # # @param filename [String] See \{Frame#filename}. # @param line [String] See \{Frame#line}. # @param name [String] See \{Frame#name}. # @yield [] A block in which the new frame is on the stack. def with_directive(filename, line, name) with_frame(filename, line, :directive, name) {yield} end def to_s (frames.reverse + [nil]).each_cons(2).each_with_index. map do |(frame, caller), i| "#{i == 0 ? 'on' : 'from'} line #{frame.line}" + " of #{frame.filename || 'an unknown file'}" + (caller && caller.name ? ", in `#{caller.name}'" : "") end.join("\n") end private def with_frame(filename, line, type, name = nil) @frames.pop if @frames.last && @frames.last.type == :base @frames.push(Frame.new(filename, line, type, name)) yield ensure @frames.pop unless type == :base && @frames.last && @frames.last.type != :base end end end ruby-sass-3.7.4/lib/sass/supports.rb000066400000000000000000000120721345125207600174270ustar00rootroot00000000000000# A namespace for the `@supports` condition parse tree. module Sass::Supports # The abstract superclass of all Supports conditions. class Condition # Runs the SassScript in the supports condition. # # @param environment [Sass::Environment] The environment in which to run the script. def perform(environment); Sass::Util.abstract(self); end # Returns the CSS for this condition. # # @return [String] def to_css; Sass::Util.abstract(self); end # Returns the Sass/CSS code for this condition. # # @param options [{Symbol => Object}] An options hash (see {Sass::CSS#initialize}). # @return [String] def to_src(options); Sass::Util.abstract(self); end # Returns a deep copy of this condition and all its children. # # @return [Condition] def deep_copy; Sass::Util.abstract(self); end # Sets the options hash for the script nodes in the supports condition. # # @param options [{Symbol => Object}] The options has to set. def options=(options); Sass::Util.abstract(self); end end # An operator condition (e.g. `CONDITION1 and CONDITION2`). class Operator < Condition # The left-hand condition. # # @return [Sass::Supports::Condition] attr_accessor :left # The right-hand condition. # # @return [Sass::Supports::Condition] attr_accessor :right # The operator ("and" or "or"). # # @return [String] attr_accessor :op def initialize(left, right, op) @left = left @right = right @op = op end def perform(env) @left.perform(env) @right.perform(env) end def to_css "#{parens @left, @left.to_css} #{op} #{parens @right, @right.to_css}" end def to_src(options) "#{parens @left, @left.to_src(options)} #{op} #{parens @right, @right.to_src(options)}" end def deep_copy copy = dup copy.left = @left.deep_copy copy.right = @right.deep_copy copy end def options=(options) @left.options = options @right.options = options end private def parens(condition, str) if condition.is_a?(Negation) || (condition.is_a?(Operator) && condition.op != op) return "(#{str})" else return str end end end # A negation condition (`not CONDITION`). class Negation < Condition # The condition being negated. # # @return [Sass::Supports::Condition] attr_accessor :condition def initialize(condition) @condition = condition end def perform(env) @condition.perform(env) end def to_css "not #{parens @condition.to_css}" end def to_src(options) "not #{parens @condition.to_src(options)}" end def deep_copy copy = dup copy.condition = condition.deep_copy copy end def options=(options) condition.options = options end private def parens(str) return "(#{str})" if @condition.is_a?(Negation) || @condition.is_a?(Operator) str end end # A declaration condition (e.g. `(feature: value)`). class Declaration < Condition # @return [Sass::Script::Tree::Node] The feature name. attr_accessor :name # @!attribute resolved_name # The name of the feature after any SassScript has been resolved. # Only set once \{Tree::Visitors::Perform} has been run. # # @return [String] attr_accessor :resolved_name # The feature value. # # @return [Sass::Script::Tree::Node] attr_accessor :value # The value of the feature after any SassScript has been resolved. # Only set once \{Tree::Visitors::Perform} has been run. # # @return [String] attr_accessor :resolved_value def initialize(name, value) @name = name @value = value end def perform(env) @resolved_name = name.perform(env) @resolved_value = value.perform(env) end def to_css "(#{@resolved_name}: #{@resolved_value})" end def to_src(options) "(#{@name.to_sass(options)}: #{@value.to_sass(options)})" end def deep_copy copy = dup copy.name = @name.deep_copy copy.value = @value.deep_copy copy end def options=(options) @name.options = options @value.options = options end end # An interpolation condition (e.g. `#{$var}`). class Interpolation < Condition # The SassScript expression in the interpolation. # # @return [Sass::Script::Tree::Node] attr_accessor :value # The value of the expression after it's been resolved. # Only set once \{Tree::Visitors::Perform} has been run. # # @return [String] attr_accessor :resolved_value def initialize(value) @value = value end def perform(env) @resolved_value = value.perform(env).to_s(:quote => :none) end def to_css @resolved_value end def to_src(options) @value.to_sass(options) end def deep_copy copy = dup copy.value = @value.deep_copy copy end def options=(options) @value.options = options end end end ruby-sass-3.7.4/lib/sass/tree/000077500000000000000000000000001345125207600161405ustar00rootroot00000000000000ruby-sass-3.7.4/lib/sass/tree/at_root_node.rb000066400000000000000000000052031345125207600211410ustar00rootroot00000000000000module Sass module Tree # A dynamic node representing an `@at-root` directive. # # An `@at-root` directive with a selector is converted to an \{AtRootNode} # containing a \{RuleNode} at parse time. # # @see Sass::Tree class AtRootNode < Node # The query for this node (e.g. `(without: media)`), # interspersed with {Sass::Script::Tree::Node}s representing # `#{}`-interpolation. Any adjacent strings will be merged # together. # # This will be nil if the directive didn't have a query. In this # case, {#resolved\_type} will automatically be set to # `:without` and {#resolved\_rule} will automatically be set to `["rule"]`. # # @return [Array] attr_accessor :query # The resolved type of this directive. `:with` or `:without`. # # @return [Symbol] attr_accessor :resolved_type # The resolved value of this directive -- a list of directives # to either include or exclude. # # @return [Array] attr_accessor :resolved_value # The number of additional tabs that the contents of this node # should be indented. # # @return [Number] attr_accessor :tabs # Whether the last child of this node should be considered the # end of a group. # # @return [Boolean] attr_accessor :group_end def initialize(query = nil) super() @query = Sass::Util.strip_string_array(Sass::Util.merge_adjacent_strings(query)) if query @tabs = 0 end # Returns whether or not the given directive is excluded by this # node. `directive` may be "rule", which indicates whether # normal CSS rules should be excluded. # # @param directive [String] # @return [Boolean] def exclude?(directive) if resolved_type == :with return false if resolved_value.include?('all') !resolved_value.include?(directive) else # resolved_type == :without return true if resolved_value.include?('all') resolved_value.include?(directive) end end # Returns whether the given node is excluded by this node. # # @param node [Sass::Tree::Node] # @return [Boolean] def exclude_node?(node) return exclude?(node.name.gsub(/^@/, '')) if node.is_a?(Sass::Tree::DirectiveNode) return exclude?('keyframes') if node.is_a?(Sass::Tree::KeyframeRuleNode) exclude?('rule') && node.is_a?(Sass::Tree::RuleNode) end # @see Node#bubbles? def bubbles? true end end end end ruby-sass-3.7.4/lib/sass/tree/charset_node.rb000066400000000000000000000006311345125207600211230ustar00rootroot00000000000000module Sass::Tree # A static node representing an unprocessed Sass `@charset` directive. # # @see Sass::Tree class CharsetNode < Node # The name of the charset. # # @return [String] attr_accessor :name # @param name [String] see \{#name} def initialize(name) @name = name super() end # @see Node#invisible? def invisible? true end end end ruby-sass-3.7.4/lib/sass/tree/comment_node.rb000066400000000000000000000045121345125207600211360ustar00rootroot00000000000000require 'sass/tree/node' module Sass::Tree # A static node representing a Sass comment (silent or loud). # # @see Sass::Tree class CommentNode < Node # The text of the comment, not including `/*` and `*/`. # Interspersed with {Sass::Script::Tree::Node}s representing `#{}`-interpolation # if this is a loud comment. # # @return [Array] attr_accessor :value # The text of the comment # after any interpolated SassScript has been resolved. # Only set once \{Tree::Visitors::Perform} has been run. # # @return [String] attr_accessor :resolved_value # The type of the comment. `:silent` means it's never output to CSS, # `:normal` means it's output in every compile mode except `:compressed`, # and `:loud` means it's output even in `:compressed`. # # @return [Symbol] attr_accessor :type # @param value [Array] See \{#value} # @param type [Symbol] See \{#type} def initialize(value, type) @value = Sass::Util.with_extracted_values(value) {|str| normalize_indentation str} @type = type super() end # Compares the contents of two comments. # # @param other [Object] The object to compare with # @return [Boolean] Whether or not this node and the other object # are the same def ==(other) self.class == other.class && value == other.value && type == other.type end # Returns `true` if this is a silent comment # or the current style doesn't render comments. # # Comments starting with ! are never invisible (and the ! is removed from the output.) # # @return [Boolean] def invisible? case @type when :loud; false when :silent; true else; style == :compressed end end # Returns the number of lines in the comment. # # @return [Integer] def lines @value.inject(0) do |s, e| next s + e.count("\n") if e.is_a?(String) next s end end private def normalize_indentation(str) ind = str.split("\n").inject(str[/^[ \t]*/].split("")) do |pre, line| line[/^[ \t]*/].split("").zip(pre).inject([]) do |arr, (a, b)| break arr if a != b arr << a end end.join str.gsub(/^#{ind}/, '') end end end ruby-sass-3.7.4/lib/sass/tree/content_node.rb000066400000000000000000000003001345125207600211350ustar00rootroot00000000000000module Sass module Tree # A node representing the placement within a mixin of the include statement's content. # # @see Sass::Tree class ContentNode < Node end end end ruby-sass-3.7.4/lib/sass/tree/css_import_node.rb000066400000000000000000000041351345125207600216570ustar00rootroot00000000000000module Sass::Tree # A node representing an `@import` rule that's importing plain CSS. # # @see Sass::Tree class CssImportNode < DirectiveNode # The URI being imported, either as a plain string or an interpolated # script string. # # @return [String, Sass::Script::Tree::Node] attr_accessor :uri # The text of the URI being imported after any interpolated SassScript has # been resolved. Only set once {Tree::Visitors::Perform} has been run. # # @return [String] attr_accessor :resolved_uri # The supports condition for this import. # # @return [Sass::Supports::Condition] attr_accessor :supports_condition # The media query for this rule, interspersed with # {Sass::Script::Tree::Node}s representing `#{}`-interpolation. Any adjacent # strings will be merged together. # # @return [Array] attr_accessor :query # The media query for this rule, without any unresolved interpolation. # It's only set once {Tree::Visitors::Perform} has been run. # # @return [Sass::Media::QueryList] attr_accessor :resolved_query # @param uri [String, Sass::Script::Tree::Node] See \{#uri} # @param query [Array] See \{#query} # @param supports_condition [Sass::Supports::Condition] See \{#supports_condition} def initialize(uri, query = [], supports_condition = nil) @uri = uri @query = query @supports_condition = supports_condition super('') end # @param uri [String] See \{#resolved_uri} # @return [CssImportNode] def self.resolved(uri) node = new(uri) node.resolved_uri = uri node end # @see DirectiveNode#value def value; raise NotImplementedError; end # @see DirectiveNode#resolved_value def resolved_value @resolved_value ||= begin str = "@import #{resolved_uri}" str << " supports(#{supports_condition.to_css})" if supports_condition str << " #{resolved_query.to_css}" if resolved_query str end end end end ruby-sass-3.7.4/lib/sass/tree/debug_node.rb000066400000000000000000000006141345125207600205610ustar00rootroot00000000000000module Sass module Tree # A dynamic node representing a Sass `@debug` statement. # # @see Sass::Tree class DebugNode < Node # The expression to print. # @return [Script::Tree::Node] attr_accessor :expr # @param expr [Script::Tree::Node] The expression to print def initialize(expr) @expr = expr super() end end end end ruby-sass-3.7.4/lib/sass/tree/directive_node.rb000066400000000000000000000032311345125207600214470ustar00rootroot00000000000000module Sass::Tree # A static node representing an unprocessed Sass `@`-directive. # Directives known to Sass, like `@for` and `@debug`, # are handled by their own nodes; # only CSS directives like `@media` and `@font-face` become {DirectiveNode}s. # # `@import` and `@charset` are special cases; # they become {ImportNode}s and {CharsetNode}s, respectively. # # @see Sass::Tree class DirectiveNode < Node # The text of the directive, `@` and all, with interpolation included. # # @return [Array] attr_accessor :value # The text of the directive after any interpolated SassScript has been resolved. # Only set once \{Tree::Visitors::Perform} has been run. # # @return [String] attr_accessor :resolved_value # @see RuleNode#tabs attr_accessor :tabs # @see RuleNode#group_end attr_accessor :group_end # @param value [Array] See \{#value} def initialize(value) @value = value @tabs = 0 super() end # @param value [String] See \{#resolved_value} # @return [DirectiveNode] def self.resolved(value) node = new([value]) node.resolved_value = value node end # @return [String] The name of the directive, including `@`. def name @name ||= value.first.gsub(/ .*$/, '') end # Strips out any vendor prefixes and downcases the directive name. # @return [String] The normalized name of the directive. def normalized_name @normalized_name ||= name.gsub(/^(@)(?:-[a-zA-Z0-9]+-)?/, '\1').downcase end def bubbles? has_children end end end ruby-sass-3.7.4/lib/sass/tree/each_node.rb000066400000000000000000000010661345125207600203750ustar00rootroot00000000000000require 'sass/tree/node' module Sass::Tree # A dynamic node representing a Sass `@each` loop. # # @see Sass::Tree class EachNode < Node # The names of the loop variables. # @return [Array] attr_reader :vars # The parse tree for the list. # @return [Script::Tree::Node] attr_accessor :list # @param vars [Array] The names of the loop variables # @param list [Script::Tree::Node] The parse tree for the list def initialize(vars, list) @vars = vars @list = list super() end end end ruby-sass-3.7.4/lib/sass/tree/error_node.rb000066400000000000000000000006141345125207600206240ustar00rootroot00000000000000module Sass module Tree # A dynamic node representing a Sass `@error` statement. # # @see Sass::Tree class ErrorNode < Node # The expression to print. # @return [Script::Tree::Node] attr_accessor :expr # @param expr [Script::Tree::Node] The expression to print def initialize(expr) @expr = expr super() end end end end ruby-sass-3.7.4/lib/sass/tree/extend_node.rb000066400000000000000000000025431345125207600207650ustar00rootroot00000000000000require 'sass/tree/node' module Sass::Tree # A static node representing an `@extend` directive. # # @see Sass::Tree class ExtendNode < Node # The parsed selector after interpolation has been resolved. # Only set once {Tree::Visitors::Perform} has been run. # # @return [Selector::CommaSequence] attr_accessor :resolved_selector # The CSS selector to extend, interspersed with {Sass::Script::Tree::Node}s # representing `#{}`-interpolation. # # @return [Array] attr_accessor :selector # The extended selector source range. # # @return [Sass::Source::Range] attr_accessor :selector_source_range # Whether the `@extend` is allowed to match no selectors or not. # # @return [Boolean] def optional?; @optional; end # @param selector [Array] # The CSS selector to extend, # interspersed with {Sass::Script::Tree::Node}s # representing `#{}`-interpolation. # @param optional [Boolean] See \{ExtendNode#optional?} # @param selector_source_range [Sass::Source::Range] The extended selector source range. def initialize(selector, optional, selector_source_range) @selector = selector @optional = optional @selector_source_range = selector_source_range super() end end end ruby-sass-3.7.4/lib/sass/tree/for_node.rb000066400000000000000000000016041345125207600202610ustar00rootroot00000000000000require 'sass/tree/node' module Sass::Tree # A dynamic node representing a Sass `@for` loop. # # @see Sass::Tree class ForNode < Node # The name of the loop variable. # @return [String] attr_reader :var # The parse tree for the initial expression. # @return [Script::Tree::Node] attr_accessor :from # The parse tree for the final expression. # @return [Script::Tree::Node] attr_accessor :to # Whether to include `to` in the loop or stop just before. # @return [Boolean] attr_reader :exclusive # @param var [String] See \{#var} # @param from [Script::Tree::Node] See \{#from} # @param to [Script::Tree::Node] See \{#to} # @param exclusive [Boolean] See \{#exclusive} def initialize(var, from, to, exclusive) @var = var @from = from @to = to @exclusive = exclusive super() end end end ruby-sass-3.7.4/lib/sass/tree/function_node.rb000066400000000000000000000024401345125207600213170ustar00rootroot00000000000000module Sass module Tree # A dynamic node representing a function definition. # # @see Sass::Tree class FunctionNode < Node # The name of the function. # @return [String] attr_reader :name # The arguments to the function. Each element is a tuple # containing the variable for argument and the parse tree for # the default value of the argument # # @return [Array] attr_accessor :args # The splat argument for this function, if one exists. # # @return [Script::Tree::Node?] attr_accessor :splat # Strips out any vendor prefixes. # @return [String] The normalized name of the directive. def normalized_name @normalized_name ||= name.gsub(/^(?:-[a-zA-Z0-9]+-)?/, '\1') end # @param name [String] The function name # @param args [Array<(Script::Tree::Node, Script::Tree::Node)>] # The arguments for the function. # @param splat [Script::Tree::Node] See \{#splat} def initialize(name, args, splat) @name = name @args = args @splat = splat super() return unless %w(and or not).include?(name) raise Sass::SyntaxError.new("Invalid function name \"#{name}\".") end end end end ruby-sass-3.7.4/lib/sass/tree/if_node.rb000066400000000000000000000024471345125207600200770ustar00rootroot00000000000000require 'sass/tree/node' module Sass::Tree # A dynamic node representing a Sass `@if` statement. # # {IfNode}s are a little odd, in that they also represent `@else` and `@else if`s. # This is done as a linked list: # each {IfNode} has a link (\{#else}) to the next {IfNode}. # # @see Sass::Tree class IfNode < Node # The conditional expression. # If this is nil, this is an `@else` node, not an `@else if`. # # @return [Script::Expr] attr_accessor :expr # The next {IfNode} in the if-else list, or `nil`. # # @return [IfNode] attr_accessor :else # @param expr [Script::Expr] See \{#expr} def initialize(expr) @expr = expr @last_else = self super() end # Append an `@else` node to the end of the list. # # @param node [IfNode] The `@else` node to append def add_else(node) @last_else.else = node @last_else = node end def _dump(f) Marshal.dump([expr, self.else, children]) end def self._load(data) expr, else_, children = Marshal.load(data) node = IfNode.new(expr) node.else = else_ node.children = children node.instance_variable_set('@last_else', node.else ? node.else.instance_variable_get('@last_else') : node) node end end end ruby-sass-3.7.4/lib/sass/tree/import_node.rb000066400000000000000000000042521345125207600210070ustar00rootroot00000000000000module Sass module Tree # A static node that wraps the {Sass::Tree} for an `@import`ed file. # It doesn't have a functional purpose other than to add the `@import`ed file # to the backtrace if an error occurs. class ImportNode < RootNode # The name of the imported file as it appears in the Sass document. # # @return [String] attr_reader :imported_filename # Sets the imported file. attr_writer :imported_file # @param imported_filename [String] The name of the imported file def initialize(imported_filename) @imported_filename = imported_filename super(nil) end def invisible?; to_s.empty?; end # Returns the imported file. # # @return [Sass::Engine] # @raise [Sass::SyntaxError] If no file could be found to import. def imported_file @imported_file ||= import end # Returns whether or not this import should emit a CSS @import declaration # # @return [Boolean] Whether or not this is a simple CSS @import declaration. def css_import? if @imported_filename =~ /\.css$/ @imported_filename elsif imported_file.is_a?(String) && imported_file =~ /\.css$/ imported_file end end private def import paths = @options[:load_paths] if @options[:importer] f = @options[:importer].find_relative( @imported_filename, @options[:filename], options_for_importer) return f if f end paths.each do |p| f = p.find(@imported_filename, options_for_importer) return f if f end lines = ["File to import not found or unreadable: #{@imported_filename}."] if paths.size == 1 lines << "Load path: #{paths.first}" elsif !paths.empty? lines << "Load paths:\n #{paths.join("\n ")}" end raise SyntaxError.new(lines.join("\n")) rescue SyntaxError => e raise SyntaxError.new(e.message, :line => line, :filename => @filename) end def options_for_importer @options.merge(:_from_import_node => true) end end end end ruby-sass-3.7.4/lib/sass/tree/keyframe_rule_node.rb000066400000000000000000000006661345125207600223340ustar00rootroot00000000000000module Sass::Tree class KeyframeRuleNode < Node # The text of the directive after any interpolated SassScript has been resolved. # Since this is only a static node, this is the only value property. # # @return [String] attr_accessor :resolved_value # @param resolved_value [String] See \{#resolved_value} def initialize(resolved_value) @resolved_value = resolved_value super() end end end ruby-sass-3.7.4/lib/sass/tree/media_node.rb000066400000000000000000000025741345125207600205610ustar00rootroot00000000000000module Sass::Tree # A static node representing a `@media` rule. # `@media` rules behave differently from other directives # in that when they're nested within rules, # they bubble up to top-level. # # @see Sass::Tree class MediaNode < DirectiveNode # TODO: parse and cache the query immediately if it has no dynamic elements # The media query for this rule, interspersed with {Sass::Script::Tree::Node}s # representing `#{}`-interpolation. Any adjacent strings will be merged # together. # # @return [Array] attr_accessor :query # The media query for this rule, without any unresolved interpolation. It's # only set once {Tree::Visitors::Perform} has been run. # # @return [Sass::Media::QueryList] attr_accessor :resolved_query # @param query [Array] See \{#query} def initialize(query) @query = query super('') end # @see DirectiveNode#value def value; raise NotImplementedError; end # @see DirectiveNode#name def name; '@media'; end # @see DirectiveNode#resolved_value def resolved_value @resolved_value ||= "@media #{resolved_query.to_css}" end # True when the directive has no visible children. # # @return [Boolean] def invisible? children.all? {|c| c.invisible?} end end end ruby-sass-3.7.4/lib/sass/tree/mixin_def_node.rb000066400000000000000000000021001345125207600214250ustar00rootroot00000000000000module Sass module Tree # A dynamic node representing a mixin definition. # # @see Sass::Tree class MixinDefNode < Node # The mixin name. # @return [String] attr_reader :name # The arguments for the mixin. # Each element is a tuple containing the variable for argument # and the parse tree for the default value of the argument. # # @return [Array<(Script::Tree::Node, Script::Tree::Node)>] attr_accessor :args # The splat argument for this mixin, if one exists. # # @return [Script::Tree::Node?] attr_accessor :splat # Whether the mixin uses `@content`. Set during the nesting check phase. # @return [Boolean] attr_accessor :has_content # @param name [String] The mixin name # @param args [Array<(Script::Tree::Node, Script::Tree::Node)>] See \{#args} # @param splat [Script::Tree::Node] See \{#splat} def initialize(name, args, splat) @name = name @args = args @splat = splat super() end end end end ruby-sass-3.7.4/lib/sass/tree/mixin_node.rb000066400000000000000000000030551345125207600206210ustar00rootroot00000000000000require 'sass/tree/node' module Sass::Tree # A static node representing a mixin include. # When in a static tree, the sole purpose is to wrap exceptions # to add the mixin to the backtrace. # # @see Sass::Tree class MixinNode < Node # The name of the mixin. # @return [String] attr_reader :name # The arguments to the mixin. # @return [Array] attr_accessor :args # A hash from keyword argument names to values. # @return [Sass::Util::NormalizedMap] attr_accessor :keywords # The first splat argument for this mixin, if one exists. # # This could be a list of positional arguments, a map of keyword # arguments, or an arglist containing both. # # @return [Node?] attr_accessor :splat # The second splat argument for this mixin, if one exists. # # If this exists, it's always a map of keyword arguments, and # \{#splat} is always either a list or an arglist. # # @return [Node?] attr_accessor :kwarg_splat # @param name [String] The name of the mixin # @param args [Array] See \{#args} # @param splat [Script::Tree::Node] See \{#splat} # @param kwarg_splat [Script::Tree::Node] See \{#kwarg_splat} # @param keywords [Sass::Util::NormalizedMap] See \{#keywords} def initialize(name, args, keywords, splat, kwarg_splat) @name = name @args = args @keywords = keywords @splat = splat @kwarg_splat = kwarg_splat super() end end end ruby-sass-3.7.4/lib/sass/tree/node.rb000066400000000000000000000171641345125207600174230ustar00rootroot00000000000000module Sass # A namespace for nodes in the Sass parse tree. # # The Sass parse tree has three states: dynamic, static Sass, and static CSS. # # When it's first parsed, a Sass document is in the dynamic state. # It has nodes for mixin definitions and `@for` loops and so forth, # in addition to nodes for CSS rules and properties. # Nodes that only appear in this state are called **dynamic nodes**. # # {Tree::Visitors::Perform} creates a static Sass tree, which is # different. It still has nodes for CSS rules and properties but it # doesn't have any dynamic-generation-related nodes. The nodes in # this state are in a similar structure to the Sass document: rules # and properties are nested beneath one another, although the # {Tree::RuleNode} selectors are already in their final state. Nodes # that can be in this state or in the dynamic state are called # **static nodes**; nodes that can only be in this state are called # **solely static nodes**. # # {Tree::Visitors::Cssize} is then used to create a static CSS tree. # This is like a static Sass tree, # but the structure exactly mirrors that of the generated CSS. # Rules and properties can't be nested beneath one another in this state. # # Finally, {Tree::Visitors::ToCss} can be called on a static CSS tree # to get the actual CSS code as a string. module Tree # The abstract superclass of all parse-tree nodes. class Node include Enumerable def self.inherited(base) node_name = base.name.gsub(/.*::(.*?)Node$/, '\\1').downcase base.instance_eval <<-METHODS # @return [Symbol] The name that is used for this node when visiting. def node_name :#{node_name} end # @return [Symbol] The method that is used on the visitor to visit nodes of this type. def visit_method :visit_#{node_name} end # @return [Symbol] The method name that determines if the parent is invalid. def invalid_child_method_name :"invalid_#{node_name}_child?" end # @return [Symbol] The method name that determines if the node is an invalid parent. def invalid_parent_method_name :"invalid_#{node_name}_parent?" end METHODS end # The child nodes of this node. # # @return [Array] attr_reader :children # Whether or not this node has child nodes. # This may be true even when \{#children} is empty, # in which case this node has an empty block (e.g. `{}`). # # @return [Boolean] attr_accessor :has_children # The line of the document on which this node appeared. # # @return [Integer] attr_accessor :line # The source range in the document on which this node appeared. # # @return [Sass::Source::Range] attr_accessor :source_range # The name of the document on which this node appeared. # # @return [String] attr_writer :filename # The options hash for the node. # See {file:SASS_REFERENCE.md#Options the Sass options documentation}. # # @return [{Symbol => Object}] attr_reader :options def initialize @children = [] @filename = nil @options = nil end # Sets the options hash for the node and all its children. # # @param options [{Symbol => Object}] The options # @see #options def options=(options) Sass::Tree::Visitors::SetOptions.visit(self, options) end # @private def children=(children) self.has_children ||= !children.empty? @children = children end # The name of the document on which this node appeared. # # @return [String] def filename @filename || (@options && @options[:filename]) end # Appends a child to the node. # # @param child [Tree::Node, Array] The child node or nodes # @raise [Sass::SyntaxError] if `child` is invalid def <<(child) return if child.nil? if child.is_a?(Array) child.each {|c| self << c} else self.has_children = true @children << child end end # Compares this node and another object (only other {Tree::Node}s will be equal). # This does a structural comparison; # if the contents of the nodes and all the child nodes are equivalent, # then the nodes are as well. # # Only static nodes need to override this. # # @param other [Object] The object to compare with # @return [Boolean] Whether or not this node and the other object # are the same # @see Sass::Tree def ==(other) self.class == other.class && other.children == children end # True if \{#to\_s} will return `nil`; # that is, if the node shouldn't be rendered. # Should only be called in a static tree. # # @return [Boolean] def invisible?; false; end # The output style. See {file:SASS_REFERENCE.md#Options the Sass options documentation}. # # @return [Symbol] def style @options[:style] end # Computes the CSS corresponding to this static CSS tree. # # @return [String] The resulting CSS # @see Sass::Tree def css Sass::Tree::Visitors::ToCss.new.visit(self) end # Computes the CSS corresponding to this static CSS tree, along with # the respective source map. # # @return [(String, Sass::Source::Map)] The resulting CSS and the source map # @see Sass::Tree def css_with_sourcemap visitor = Sass::Tree::Visitors::ToCss.new(:build_source_mapping) result = visitor.visit(self) return result, visitor.source_mapping end # Returns a representation of the node for debugging purposes. # # @return [String] def inspect return self.class.to_s unless has_children "(#{self.class} #{children.map {|c| c.inspect}.join(' ')})" end # Iterates through each node in the tree rooted at this node # in a pre-order walk. # # @yield node # @yieldparam node [Node] a node in the tree def each yield self children.each {|c| c.each {|n| yield n}} end # Converts a node to Sass code that will generate it. # # @param options [{Symbol => Object}] An options hash (see {Sass::CSS#initialize}) # @return [String] The Sass code corresponding to the node def to_sass(options = {}) Sass::Tree::Visitors::Convert.visit(self, options, :sass) end # Converts a node to SCSS code that will generate it. # # @param options [{Symbol => Object}] An options hash (see {Sass::CSS#initialize}) # @return [String] The Sass code corresponding to the node def to_scss(options = {}) Sass::Tree::Visitors::Convert.visit(self, options, :scss) end # Return a deep clone of this node. # The child nodes are cloned, but options are not. # # @return [Node] def deep_copy Sass::Tree::Visitors::DeepCopy.visit(self) end # Whether or not this node bubbles up through RuleNodes. # # @return [Boolean] def bubbles? false end protected # @see Sass::Shared.balance # @raise [Sass::SyntaxError] if the brackets aren't balanced def balance(*args) res = Sass::Shared.balance(*args) return res if res raise Sass::SyntaxError.new("Unbalanced brackets.", :line => line) end end end end ruby-sass-3.7.4/lib/sass/tree/prop_node.rb000066400000000000000000000132611345125207600204550ustar00rootroot00000000000000module Sass::Tree # A static node representing a CSS property. # # @see Sass::Tree class PropNode < Node # The name of the property, # interspersed with {Sass::Script::Tree::Node}s # representing `#{}`-interpolation. # Any adjacent strings will be merged together. # # @return [Array] attr_accessor :name # The name of the property # after any interpolated SassScript has been resolved. # Only set once \{Tree::Visitors::Perform} has been run. # # @return [String] attr_accessor :resolved_name # The value of the property. # # For most properties, this will just contain a single Node. However, for # CSS variables, it will contain multiple strings and nodes representing # interpolation. Any adjacent strings will be merged together. # # @return [Array] attr_accessor :value # The value of the property # after any interpolated SassScript has been resolved. # Only set once \{Tree::Visitors::Perform} has been run. # # @return [String] attr_accessor :resolved_value # How deep this property is indented # relative to a normal property. # This is only greater than 0 in the case that: # # * This node is in a CSS tree # * The style is :nested # * This is a child property of another property # * The parent property has a value, and thus will be rendered # # @return [Integer] attr_accessor :tabs # The source range in which the property name appears. # # @return [Sass::Source::Range] attr_accessor :name_source_range # The source range in which the property value appears. # # @return [Sass::Source::Range] attr_accessor :value_source_range # Whether this represents a CSS custom property. # # @return [Boolean] def custom_property? name.first.is_a?(String) && name.first.start_with?("--") end # @param name [Array] See \{#name} # @param value [Array] See \{#value} # @param prop_syntax [Symbol] `:new` if this property uses `a: b`-style syntax, # `:old` if it uses `:a b`-style syntax def initialize(name, value, prop_syntax) @name = Sass::Util.strip_string_array( Sass::Util.merge_adjacent_strings(name)) @value = Sass::Util.merge_adjacent_strings(value) @value = Sass::Util.strip_string_array(@value) unless custom_property? @tabs = 0 @prop_syntax = prop_syntax super() end # Compares the names and values of two properties. # # @param other [Object] The object to compare with # @return [Boolean] Whether or not this node and the other object # are the same def ==(other) self.class == other.class && name == other.name && value == other.value && super end # Returns a appropriate message indicating how to escape pseudo-class selectors. # This only applies for old-style properties with no value, # so returns the empty string if this is new-style. # # @return [String] The message def pseudo_class_selector_message if @prop_syntax == :new || custom_property? || !value.first.is_a?(Sass::Script::Tree::Literal) || !value.first.value.is_a?(Sass::Script::Value::String) || !value.first.value.value.empty? return "" end "\nIf #{declaration.dump} should be a selector, use \"\\#{declaration}\" instead." end # Computes the Sass or SCSS code for the variable declaration. # This is like \{#to\_scss} or \{#to\_sass}, # except it doesn't print any child properties or a trailing semicolon. # # @param opts [{Symbol => Object}] The options hash for the tree. # @param fmt [Symbol] `:scss` or `:sass`. def declaration(opts = {:old => @prop_syntax == :old}, fmt = :sass) name = self.name.map {|n| n.is_a?(String) ? n : n.to_sass(opts)}.join value = self.value.map {|n| n.is_a?(String) ? n : n.to_sass(opts)}.join value = "(#{value})" if value_needs_parens? if name[0] == ?: raise Sass::SyntaxError.new("The \"#{name}: #{value}\"" + " hack is not allowed in the Sass indented syntax") end # The indented syntax doesn't support newlines in custom property values, # but we can losslessly convert them to spaces instead. value = value.tr("\n", " ") if fmt == :sass old = opts[:old] && fmt == :sass "#{old ? ':' : ''}#{name}#{old ? '' : ':'}#{custom_property? ? '' : ' '}#{value}".rstrip end # A property node is invisible if its value is empty. # # @return [Boolean] def invisible? !custom_property? && resolved_value.empty? end private # Returns whether \{#value} neesd parentheses in order to be parsed # properly as division. def value_needs_parens? return false if custom_property? root = value.first root.is_a?(Sass::Script::Tree::Operation) && root.operator == :div && root.operand1.is_a?(Sass::Script::Tree::Literal) && root.operand1.value.is_a?(Sass::Script::Value::Number) && root.operand1.value.original.nil? && root.operand2.is_a?(Sass::Script::Tree::Literal) && root.operand2.value.is_a?(Sass::Script::Value::Number) && root.operand2.value.original.nil? end def check! return unless @options[:property_syntax] && @options[:property_syntax] != @prop_syntax raise Sass::SyntaxError.new( "Illegal property syntax: can't use #{@prop_syntax} syntax when " + ":property_syntax => #{@options[:property_syntax].inspect} is set.") end end end ruby-sass-3.7.4/lib/sass/tree/return_node.rb000066400000000000000000000006271345125207600210160ustar00rootroot00000000000000module Sass module Tree # A dynamic node representing returning from a function. # # @see Sass::Tree class ReturnNode < Node # The expression to return. # # @return [Script::Tree::Node] attr_accessor :expr # @param expr [Script::Tree::Node] The expression to return def initialize(expr) @expr = expr super() end end end end ruby-sass-3.7.4/lib/sass/tree/root_node.rb000066400000000000000000000023311345125207600204540ustar00rootroot00000000000000module Sass module Tree # A static node that is the root node of the Sass document. class RootNode < Node # The Sass template from which this node was created # # @param template [String] attr_reader :template # @param template [String] The Sass template from which this node was created def initialize(template) super() @template = template end # Runs the dynamic Sass code and computes the CSS for the tree. # # @return [String] The compiled CSS. def render css_tree.css end # Runs the dynamic Sass code and computes the CSS for the tree, along with # the sourcemap. # # @return [(String, Sass::Source::Map)] The compiled CSS, as well as # the source map. @see #render def render_with_sourcemap css_tree.css_with_sourcemap end private def css_tree Visitors::CheckNesting.visit(self) result = Visitors::Perform.visit(self) Visitors::CheckNesting.visit(result) # Check again to validate mixins result, extends = Visitors::Cssize.visit(result) Visitors::Extend.visit(result, extends) result end end end end ruby-sass-3.7.4/lib/sass/tree/rule_node.rb000066400000000000000000000116361345125207600204500ustar00rootroot00000000000000require 'pathname' module Sass::Tree # A static node representing a CSS rule. # # @see Sass::Tree class RuleNode < Node # The character used to include the parent selector PARENT = '&' # The CSS selector for this rule, # interspersed with {Sass::Script::Tree::Node}s # representing `#{}`-interpolation. # Any adjacent strings will be merged together. # # @return [Array] attr_accessor :rule # The CSS selector for this rule, without any unresolved # interpolation but with parent references still intact. It's only # guaranteed to be set once {Tree::Visitors::Perform} has been # run, but it may be set before then for optimization reasons. # # @return [Selector::CommaSequence] attr_accessor :parsed_rules # The CSS selector for this rule, without any unresolved # interpolation or parent references. It's only set once # {Tree::Visitors::Perform} has been run. # # @return [Selector::CommaSequence] attr_accessor :resolved_rules # How deep this rule is indented # relative to a base-level rule. # This is only greater than 0 in the case that: # # * This node is in a CSS tree # * The style is :nested # * This is a child rule of another rule # * The parent rule has properties, and thus will be rendered # # @return [Integer] attr_accessor :tabs # The entire selector source range for this rule. # @return [Sass::Source::Range] attr_accessor :selector_source_range # Whether or not this rule is the last rule in a nested group. # This is only set in a CSS tree. # # @return [Boolean] attr_accessor :group_end # The stack trace. # This is only readable in a CSS tree as it is written during the perform step # and only when the :trace_selectors option is set. # # @return [String] attr_accessor :stack_trace # @param rule [Array, Sass::Selector::CommaSequence] # The CSS rule, either unparsed or parsed. # @param selector_source_range [Sass::Source::Range] def initialize(rule, selector_source_range = nil) if rule.is_a?(Sass::Selector::CommaSequence) @rule = [rule.to_s] @parsed_rules = rule else merged = Sass::Util.merge_adjacent_strings(rule) @rule = Sass::Util.strip_string_array(merged) try_to_parse_non_interpolated_rules end @selector_source_range = selector_source_range @tabs = 0 super() end # If we've precached the parsed selector, set the line on it, too. def line=(line) @parsed_rules.line = line if @parsed_rules super end # If we've precached the parsed selector, set the filename on it, too. def filename=(filename) @parsed_rules.filename = filename if @parsed_rules super end # Compares the contents of two rules. # # @param other [Object] The object to compare with # @return [Boolean] Whether or not this node and the other object # are the same def ==(other) self.class == other.class && rule == other.rule && super end # Adds another {RuleNode}'s rules to this one's. # # @param node [RuleNode] The other node def add_rules(node) @rule = Sass::Util.strip_string_array( Sass::Util.merge_adjacent_strings(@rule + ["\n"] + node.rule)) try_to_parse_non_interpolated_rules end # @return [Boolean] Whether or not this rule is continued on the next line def continued? last = @rule.last last.is_a?(String) && last[-1] == ?, end # A hash that will be associated with this rule in the CSS document # if the {file:SASS_REFERENCE.md#debug_info-option `:debug_info` option} is enabled. # This data is used by e.g. [the FireSass Firebug # extension](https://addons.mozilla.org/en-US/firefox/addon/103988). # # @return [{#to_s => #to_s}] def debug_info {:filename => filename && ("file://" + URI::DEFAULT_PARSER.escape(File.expand_path(filename))), :line => line} end # A rule node is invisible if it has only placeholder selectors. def invisible? resolved_rules.members.all? {|seq| seq.invisible?} end private def try_to_parse_non_interpolated_rules @parsed_rules = nil return unless @rule.all? {|t| t.is_a?(String)} # We don't use real filename/line info because we don't have it yet. # When we get it, we'll set it on the parsed rules if possible. parser = nil warnings = Sass.logger.capture do parser = Sass::SCSS::StaticParser.new( Sass::Util.strip_except_escapes(@rule.join), nil, nil, 1) @parsed_rules = parser.parse_selector rescue nil end # If parsing produces a warning, throw away the result so we can parse # later with the real filename info. @parsed_rules = nil unless warnings.empty? end end end ruby-sass-3.7.4/lib/sass/tree/supports_node.rb000066400000000000000000000015721345125207600213760ustar00rootroot00000000000000module Sass::Tree # A static node representing a `@supports` rule. # # @see Sass::Tree class SupportsNode < DirectiveNode # The name, which may include a browser prefix. # # @return [String] attr_accessor :name # The supports condition. # # @return [Sass::Supports::Condition] attr_accessor :condition # @param condition [Sass::Supports::Condition] See \{#condition} def initialize(name, condition) @name = name @condition = condition super('') end # @see DirectiveNode#value def value; raise NotImplementedError; end # @see DirectiveNode#resolved_value def resolved_value @resolved_value ||= "@#{name} #{condition.to_css}" end # True when the directive has no visible children. # # @return [Boolean] def invisible? children.all? {|c| c.invisible?} end end end ruby-sass-3.7.4/lib/sass/tree/trace_node.rb000066400000000000000000000015771345125207600206020ustar00rootroot00000000000000require 'sass/tree/node' module Sass::Tree # A solely static node left over after a mixin include or @content has been performed. # Its sole purpose is to wrap exceptions to add to the backtrace. # # @see Sass::Tree class TraceNode < Node # The name of the trace entry to add. # # @return [String] attr_reader :name # @param name [String] The name of the trace entry to add. def initialize(name) @name = name self.has_children = true super() end # Initializes this node from an existing node. # @param name [String] The name of the trace entry to add. # @param node [Node] The node to copy information from. # @return [TraceNode] def self.from_node(name, node) trace = new(name) trace.line = node.line trace.filename = node.filename trace.options = node.options trace end end end ruby-sass-3.7.4/lib/sass/tree/variable_node.rb000066400000000000000000000017111345125207600212570ustar00rootroot00000000000000module Sass module Tree # A dynamic node representing a variable definition. # # @see Sass::Tree class VariableNode < Node # The name of the variable. # @return [String] attr_reader :name # The parse tree for the variable value. # @return [Script::Tree::Node] attr_accessor :expr # Whether this is a guarded variable assignment (`!default`). # @return [Boolean] attr_reader :guarded # Whether this is a global variable assignment (`!global`). # @return [Boolean] attr_reader :global # @param name [String] The name of the variable # @param expr [Script::Tree::Node] See \{#expr} # @param guarded [Boolean] See \{#guarded} # @param global [Boolean] See \{#global} def initialize(name, expr, guarded, global) @name = name @expr = expr @guarded = guarded @global = global super() end end end end ruby-sass-3.7.4/lib/sass/tree/visitors/000077500000000000000000000000001345125207600200225ustar00rootroot00000000000000ruby-sass-3.7.4/lib/sass/tree/visitors/base.rb000066400000000000000000000050441345125207600212640ustar00rootroot00000000000000# Visitors are used to traverse the Sass parse tree. # Visitors should extend {Visitors::Base}, # which provides a small amount of scaffolding for traversal. module Sass::Tree::Visitors # The abstract base class for Sass visitors. # Visitors should extend this class, # then implement `visit_*` methods for each node they care about # (e.g. `visit_rule` for {RuleNode} or `visit_for` for {ForNode}). # These methods take the node in question as argument. # They may `yield` to visit the child nodes of the current node. # # *Note*: due to the unusual nature of {Sass::Tree::IfNode}, # special care must be taken to ensure that it is properly handled. # In particular, there is no built-in scaffolding # for dealing with the return value of `@else` nodes. # # @abstract class Base # Runs the visitor on a tree. # # @param root [Tree::Node] The root node of the Sass tree. # @return [Object] The return value of \{#visit} for the root node. def self.visit(root) new.send(:visit, root) end protected # Runs the visitor on the given node. # This can be overridden by subclasses that need to do something for each node. # # @param node [Tree::Node] The node to visit. # @return [Object] The return value of the `visit_*` method for this node. def visit(node) if respond_to?(node.class.visit_method, true) send(node.class.visit_method, node) {visit_children(node)} else visit_children(node) end end # Visit the child nodes for a given node. # This can be overridden by subclasses that need to do something # with the child nodes' return values. # # This method is run when `visit_*` methods `yield`, # and its return value is returned from the `yield`. # # @param parent [Tree::Node] The parent node of the children to visit. # @return [Array] The return values of the `visit_*` methods for the children. def visit_children(parent) parent.children.map {|c| visit(c)} end # Returns the name of a node as used in the `visit_*` method. # # @param [Tree::Node] node The node. # @return [String] The name. def self.node_name(node) Sass::Util.deprecated(self, "Call node.class.node_name instead.") node.class.node_name end # `yield`s, then runs the visitor on the `@else` clause if the node has one. # This exists to ensure that the contents of the `@else` clause get visited. def visit_if(node) yield visit(node.else) if node.else node end end end ruby-sass-3.7.4/lib/sass/tree/visitors/check_nesting.rb000066400000000000000000000127631345125207600231640ustar00rootroot00000000000000# A visitor for checking that all nodes are properly nested. class Sass::Tree::Visitors::CheckNesting < Sass::Tree::Visitors::Base protected def initialize @parents = [] @parent = nil @current_mixin_def = nil end def visit(node) if (error = @parent && ( try_send(@parent.class.invalid_child_method_name, @parent, node) || try_send(node.class.invalid_parent_method_name, @parent, node))) raise Sass::SyntaxError.new(error) end super rescue Sass::SyntaxError => e e.modify_backtrace(:filename => node.filename, :line => node.line) raise e end CONTROL_NODES = [Sass::Tree::EachNode, Sass::Tree::ForNode, Sass::Tree::IfNode, Sass::Tree::WhileNode, Sass::Tree::TraceNode] SCRIPT_NODES = [Sass::Tree::ImportNode] + CONTROL_NODES def visit_children(parent) old_parent = @parent # When checking a static tree, resolve at-roots to be sure they won't send # nodes where they don't belong. if parent.is_a?(Sass::Tree::AtRootNode) && parent.resolved_value old_parents = @parents @parents = @parents.reject {|p| parent.exclude_node?(p)} @parent = @parents.reverse.each_with_index. find {|p, i| !transparent_parent?(p, @parents[-i - 2])}.first begin return super ensure @parents = old_parents @parent = old_parent end end unless transparent_parent?(parent, old_parent) @parent = parent end @parents.push parent begin super ensure @parent = old_parent @parents.pop end end def visit_root(node) yield rescue Sass::SyntaxError => e e.sass_template ||= node.template raise e end def visit_import(node) yield rescue Sass::SyntaxError => e e.modify_backtrace(:filename => node.children.first.filename) e.add_backtrace(:filename => node.filename, :line => node.line) raise e end def visit_mixindef(node) @current_mixin_def, old_mixin_def = node, @current_mixin_def yield ensure @current_mixin_def = old_mixin_def end def invalid_content_parent?(parent, child) if @current_mixin_def @current_mixin_def.has_content = true nil else "@content may only be used within a mixin." end end def invalid_charset_parent?(parent, child) "@charset may only be used at the root of a document." unless parent.is_a?(Sass::Tree::RootNode) end VALID_EXTEND_PARENTS = [Sass::Tree::RuleNode, Sass::Tree::MixinDefNode, Sass::Tree::MixinNode] def invalid_extend_parent?(parent, child) return if is_any_of?(parent, VALID_EXTEND_PARENTS) "Extend directives may only be used within rules." end INVALID_IMPORT_PARENTS = CONTROL_NODES + [Sass::Tree::MixinDefNode, Sass::Tree::MixinNode] def invalid_import_parent?(parent, child) unless (@parents.map {|p| p.class} & INVALID_IMPORT_PARENTS).empty? return "Import directives may not be used within control directives or mixins." end return if parent.is_a?(Sass::Tree::RootNode) return "CSS import directives may only be used at the root of a document." if child.css_import? rescue Sass::SyntaxError => e e.modify_backtrace(:filename => child.imported_file.options[:filename]) e.add_backtrace(:filename => child.filename, :line => child.line) raise e end def invalid_mixindef_parent?(parent, child) return if (@parents.map {|p| p.class} & INVALID_IMPORT_PARENTS).empty? "Mixins may not be defined within control directives or other mixins." end def invalid_function_parent?(parent, child) return if (@parents.map {|p| p.class} & INVALID_IMPORT_PARENTS).empty? "Functions may not be defined within control directives or other mixins." end VALID_FUNCTION_CHILDREN = [ Sass::Tree::CommentNode, Sass::Tree::DebugNode, Sass::Tree::ReturnNode, Sass::Tree::VariableNode, Sass::Tree::WarnNode, Sass::Tree::ErrorNode ] + CONTROL_NODES def invalid_function_child?(parent, child) return if is_any_of?(child, VALID_FUNCTION_CHILDREN) "Functions can only contain variable declarations and control directives." end VALID_PROP_CHILDREN = CONTROL_NODES + [Sass::Tree::CommentNode, Sass::Tree::PropNode, Sass::Tree::MixinNode] def invalid_prop_child?(parent, child) return if is_any_of?(child, VALID_PROP_CHILDREN) "Illegal nesting: Only properties may be nested beneath properties." end VALID_PROP_PARENTS = [Sass::Tree::RuleNode, Sass::Tree::KeyframeRuleNode, Sass::Tree::PropNode, Sass::Tree::MixinDefNode, Sass::Tree::DirectiveNode, Sass::Tree::MixinNode] def invalid_prop_parent?(parent, child) return if is_any_of?(parent, VALID_PROP_PARENTS) "Properties are only allowed within rules, directives, mixin includes, or other properties." + child.pseudo_class_selector_message end def invalid_return_parent?(parent, child) "@return may only be used within a function." unless parent.is_a?(Sass::Tree::FunctionNode) end private # Whether `parent` should be assigned to `@parent`. def transparent_parent?(parent, grandparent) is_any_of?(parent, SCRIPT_NODES) || (parent.bubbles? && !grandparent.is_a?(Sass::Tree::RootNode) && !grandparent.is_a?(Sass::Tree::AtRootNode)) end def is_any_of?(val, classes) classes.each do |c| return true if val.is_a?(c) end false end def try_send(method, *args) return unless respond_to?(method, true) send(method, *args) end end ruby-sass-3.7.4/lib/sass/tree/visitors/convert.rb000066400000000000000000000231431345125207600220320ustar00rootroot00000000000000# A visitor for converting a Sass tree into a source string. class Sass::Tree::Visitors::Convert < Sass::Tree::Visitors::Base # Runs the visitor on a tree. # # @param root [Tree::Node] The root node of the Sass tree. # @param options [{Symbol => Object}] An options hash (see {Sass::CSS#initialize}). # @param format [Symbol] `:sass` or `:scss`. # @return [String] The Sass or SCSS source for the tree. def self.visit(root, options, format) new(options, format).send(:visit, root) end protected def initialize(options, format) @options = options @format = format @tabs = 0 # 2 spaces by default @tab_chars = @options[:indent] || " " @is_else = false end def visit_children(parent) @tabs += 1 return @format == :sass ? "\n" : " {}\n" if parent.children.empty? res = visit_rule_level(parent.children) if @format == :sass "\n" + res.rstrip + "\n" else " {\n" + res.rstrip + "\n#{@tab_chars * (@tabs - 1)}}\n" end ensure @tabs -= 1 end # Ensures proper spacing between top-level nodes. def visit_root(node) visit_rule_level(node.children) end def visit_charset(node) "#{tab_str}@charset \"#{node.name}\"#{semi}\n" end def visit_comment(node) value = interp_to_src(node.value) if @format == :sass content = value.gsub(%r{\*/$}, '').rstrip if content =~ /\A[ \t]/ # Re-indent SCSS comments like this: # /* foo # bar # baz */ content.gsub!(/^/, ' ') content.sub!(%r{\A([ \t]*)/\*}, '/*\1') end if content.include?("\n") content.gsub!(/\n \*/, "\n ") spaces = content.scan(/\n( *)/).map {|s| s.first.size}.min sep = node.type == :silent ? "\n//" : "\n *" if spaces >= 2 content.gsub!(/\n /, sep) else content.gsub!(/\n#{' ' * spaces}/, sep) end end content.gsub!(%r{\A/\*}, '//') if node.type == :silent content.gsub!(/^/, tab_str) content = content.rstrip + "\n" else spaces = (@tab_chars * [@tabs - value[/^ */].size, 0].max) content = if node.type == :silent value.gsub(%r{^[/ ]\*}, '//').gsub(%r{ *\*/$}, '') else value end.gsub(/^/, spaces) + "\n" end content end def visit_debug(node) "#{tab_str}@debug #{node.expr.to_sass(@options)}#{semi}\n" end def visit_error(node) "#{tab_str}@error #{node.expr.to_sass(@options)}#{semi}\n" end def visit_directive(node) res = "#{tab_str}#{interp_to_src(node.value)}" res.gsub!(/^@import \#\{(.*)\}([^}]*)$/, '@import \1\2') return res + "#{semi}\n" unless node.has_children res + yield end def visit_each(node) vars = node.vars.map {|var| "$#{dasherize(var)}"}.join(", ") "#{tab_str}@each #{vars} in #{node.list.to_sass(@options)}#{yield}" end def visit_extend(node) "#{tab_str}@extend #{selector_to_src(node.selector).lstrip}" + "#{' !optional' if node.optional?}#{semi}\n" end def visit_for(node) "#{tab_str}@for $#{dasherize(node.var)} from #{node.from.to_sass(@options)} " + "#{node.exclusive ? 'to' : 'through'} #{node.to.to_sass(@options)}#{yield}" end def visit_function(node) args = node.args.map do |v, d| d ? "#{v.to_sass(@options)}: #{d.to_sass(@options)}" : v.to_sass(@options) end.join(", ") if node.splat args << ", " unless node.args.empty? args << node.splat.to_sass(@options) << "..." end "#{tab_str}@function #{dasherize(node.name)}(#{args})#{yield}" end def visit_if(node) name = if !@is_else "if" elsif node.expr "else if" else "else" end @is_else = false str = "#{tab_str}@#{name}" str << " #{node.expr.to_sass(@options)}" if node.expr str << yield @is_else = true str << visit(node.else) if node.else str ensure @is_else = false end def visit_import(node) quote = @format == :scss ? '"' : '' "#{tab_str}@import #{quote}#{node.imported_filename}#{quote}#{semi}\n" end def visit_media(node) "#{tab_str}@media #{query_interp_to_src(node.query)}#{yield}" end def visit_supports(node) "#{tab_str}@#{node.name} #{node.condition.to_src(@options)}#{yield}" end def visit_cssimport(node) if node.uri.is_a?(Sass::Script::Tree::Node) str = "#{tab_str}@import #{node.uri.to_sass(@options)}" else str = "#{tab_str}@import #{node.uri}" end str << " supports(#{node.supports_condition.to_src(@options)})" if node.supports_condition str << " #{interp_to_src(node.query)}" unless node.query.empty? "#{str}#{semi}\n" end def visit_mixindef(node) args = if node.args.empty? && node.splat.nil? "" else str = '(' str << node.args.map do |v, d| if d "#{v.to_sass(@options)}: #{d.to_sass(@options)}" else v.to_sass(@options) end end.join(", ") if node.splat str << ", " unless node.args.empty? str << node.splat.to_sass(@options) << '...' end str << ')' end "#{tab_str}#{@format == :sass ? '=' : '@mixin '}#{dasherize(node.name)}#{args}#{yield}" end def visit_mixin(node) arg_to_sass = lambda do |arg| sass = arg.to_sass(@options) sass = "(#{sass})" if arg.is_a?(Sass::Script::Tree::ListLiteral) && arg.separator == :comma sass end unless node.args.empty? && node.keywords.empty? && node.splat.nil? args = node.args.map(&arg_to_sass) keywords = node.keywords.as_stored.to_a.map {|k, v| "$#{dasherize(k)}: #{arg_to_sass[v]}"} if node.splat splat = "#{arg_to_sass[node.splat]}..." kwarg_splat = "#{arg_to_sass[node.kwarg_splat]}..." if node.kwarg_splat end arglist = "(#{[args, splat, keywords, kwarg_splat].flatten.compact.join(', ')})" end "#{tab_str}#{@format == :sass ? '+' : '@include '}" + "#{dasherize(node.name)}#{arglist}#{node.has_children ? yield : semi}\n" end def visit_content(node) "#{tab_str}@content#{semi}\n" end def visit_prop(node) res = tab_str + node.declaration(@options, @format) return res + semi + "\n" if node.children.empty? res + yield.rstrip + semi + "\n" end def visit_return(node) "#{tab_str}@return #{node.expr.to_sass(@options)}#{semi}\n" end def visit_rule(node) rule = node.parsed_rules ? [node.parsed_rules.to_s] : node.rule if @format == :sass name = selector_to_sass(rule) name = "\\" + name if name[0] == ?: name.gsub(/^/, tab_str) + yield elsif @format == :scss name = selector_to_scss(rule) res = name + yield if node.children.last.is_a?(Sass::Tree::CommentNode) && node.children.last.type == :silent res.slice!(-3..-1) res << "\n" << tab_str << "}\n" end res end end def visit_variable(node) "#{tab_str}$#{dasherize(node.name)}: #{node.expr.to_sass(@options)}" + "#{' !global' if node.global}#{' !default' if node.guarded}#{semi}\n" end def visit_warn(node) "#{tab_str}@warn #{node.expr.to_sass(@options)}#{semi}\n" end def visit_while(node) "#{tab_str}@while #{node.expr.to_sass(@options)}#{yield}" end def visit_atroot(node) if node.query "#{tab_str}@at-root #{query_interp_to_src(node.query)}#{yield}" elsif node.children.length == 1 && node.children.first.is_a?(Sass::Tree::RuleNode) rule = node.children.first "#{tab_str}@at-root #{selector_to_src(rule.rule).lstrip}#{visit_children(rule)}" else "#{tab_str}@at-root#{yield}" end end def visit_keyframerule(node) "#{tab_str}#{node.resolved_value}#{yield}" end private # Visit rule-level nodes and return their conversion with appropriate # whitespace added. def visit_rule_level(nodes) (nodes + [nil]).each_cons(2).map do |child, nxt| visit(child) + if nxt && (child.is_a?(Sass::Tree::CommentNode) && child.line + child.lines + 1 == nxt.line) || (child.is_a?(Sass::Tree::ImportNode) && nxt.is_a?(Sass::Tree::ImportNode) && child.line + 1 == nxt.line) || (child.is_a?(Sass::Tree::VariableNode) && nxt.is_a?(Sass::Tree::VariableNode) && child.line + 1 == nxt.line) || (child.is_a?(Sass::Tree::PropNode) && nxt.is_a?(Sass::Tree::PropNode)) || (child.is_a?(Sass::Tree::MixinNode) && nxt.is_a?(Sass::Tree::MixinNode) && child.line + 1 == nxt.line) "" else "\n" end end.join.rstrip + "\n" end def interp_to_src(interp) interp.map {|r| r.is_a?(String) ? r : r.to_sass(@options)}.join end # Like interp_to_src, but removes the unnecessary `#{}` around the keys and # values in query expressions. def query_interp_to_src(interp) interp = interp.map do |e| next e unless e.is_a?(Sass::Script::Tree::Literal) next e unless e.value.is_a?(Sass::Script::Value::String) e.value.value end interp_to_src(interp) end def selector_to_src(sel) @format == :sass ? selector_to_sass(sel) : selector_to_scss(sel) end def selector_to_sass(sel) sel.map do |r| if r.is_a?(String) r.gsub(/(,)?([ \t]*)\n\s*/) {$1 ? "#{$1}#{$2}\n" : " "} else r.to_sass(@options) end end.join end def selector_to_scss(sel) interp_to_src(sel).gsub(/^[ \t]*/, tab_str).gsub(/[ \t]*$/, '') end def semi @format == :sass ? "" : ";" end def tab_str @tab_chars * @tabs end def dasherize(s) if @options[:dasherize] s.tr('_', '-') else s end end end ruby-sass-3.7.4/lib/sass/tree/visitors/cssize.rb000066400000000000000000000263241345125207600216560ustar00rootroot00000000000000# A visitor for converting a static Sass tree into a static CSS tree. class Sass::Tree::Visitors::Cssize < Sass::Tree::Visitors::Base # @param root [Tree::Node] The root node of the tree to visit. # @return [(Tree::Node, Sass::Util::SubsetMap)] The resulting tree of static nodes # *and* the extensions defined for this tree def self.visit(root); super; end protected # Returns the immediate parent of the current node. # @return [Tree::Node] def parent @parents.last end def initialize @parents = [] @extends = Sass::Util::SubsetMap.new end # If an exception is raised, this adds proper metadata to the backtrace. def visit(node) super(node) rescue Sass::SyntaxError => e e.modify_backtrace(:filename => node.filename, :line => node.line) raise e end # Keeps track of the current parent node. def visit_children(parent) with_parent parent do parent.children = visit_children_without_parent(parent) parent end end # Like {#visit\_children}, but doesn't set {#parent}. # # @param node [Sass::Tree::Node] # @return [Array] the flattened results of # visiting all the children of `node` def visit_children_without_parent(node) node.children.map {|c| visit(c)}.flatten end # Runs a block of code with the current parent node # replaced with the given node. # # @param parent [Tree::Node] The new parent for the duration of the block. # @yield A block in which the parent is set to `parent`. # @return [Object] The return value of the block. def with_parent(parent) @parents.push parent yield ensure @parents.pop end # Converts the entire document to CSS. # # @return [(Tree::Node, Sass::Util::SubsetMap)] The resulting tree of static nodes # *and* the extensions defined for this tree def visit_root(node) yield if parent.nil? imports_to_move = [] import_limit = nil i = -1 node.children.reject! do |n| i += 1 if import_limit next false unless n.is_a?(Sass::Tree::CssImportNode) imports_to_move << n next true end if !n.is_a?(Sass::Tree::CommentNode) && !n.is_a?(Sass::Tree::CharsetNode) && !n.is_a?(Sass::Tree::CssImportNode) import_limit = i end false end if import_limit node.children = node.children[0...import_limit] + imports_to_move + node.children[import_limit..-1] end end return node, @extends rescue Sass::SyntaxError => e e.sass_template ||= node.template raise e end # A simple struct wrapping up information about a single `@extend` instance. A # single {ExtendNode} can have multiple Extends if either the parent node or # the extended selector is a comma sequence. # # @attr extender [Sass::Selector::Sequence] # The selector of the CSS rule containing the `@extend`. # @attr target [Array] The selector being `@extend`ed. # @attr node [Sass::Tree::ExtendNode] The node that produced this extend. # @attr directives [Array] # The directives containing the `@extend`. # @attr success [Boolean] # Whether this extend successfully matched a selector. Extend = Struct.new(:extender, :target, :node, :directives, :success) # Registers an extension in the `@extends` subset map. def visit_extend(node) parent.resolved_rules.populate_extends(@extends, node.resolved_selector, node, @parents.select {|p| p.is_a?(Sass::Tree::DirectiveNode)}) [] end # Modifies exception backtraces to include the imported file. def visit_import(node) visit_children_without_parent(node) rescue Sass::SyntaxError => e e.modify_backtrace(:filename => node.children.first.filename) e.add_backtrace(:filename => node.filename, :line => node.line) raise e end # Asserts that all the traced children are valid in their new location. def visit_trace(node) visit_children_without_parent(node) rescue Sass::SyntaxError => e e.modify_backtrace(:mixin => node.name, :filename => node.filename, :line => node.line) e.add_backtrace(:filename => node.filename, :line => node.line) raise e end # Converts nested properties into flat properties # and updates the indentation of the prop node based on the nesting level. def visit_prop(node) if parent.is_a?(Sass::Tree::PropNode) node.resolved_name = "#{parent.resolved_name}-#{node.resolved_name}" node.tabs = parent.tabs + (parent.resolved_value.empty? ? 0 : 1) if node.style == :nested end yield result = node.children.dup if !node.resolved_value.empty? || node.children.empty? node.send(:check!) result.unshift(node) end result end def visit_atroot(node) # If there aren't any more directives or rules that this @at-root needs to # exclude, we can get rid of it and just evaluate the children. if @parents.none? {|n| node.exclude_node?(n)} results = visit_children_without_parent(node) results.each {|c| c.tabs += node.tabs if bubblable?(c)} if !results.empty? && bubblable?(results.last) results.last.group_end = node.group_end end return results end # If this @at-root excludes the immediate parent, return it as-is so that it # can be bubbled up by the parent node. return Bubble.new(node) if node.exclude_node?(parent) # Otherwise, duplicate the current parent and move it into the @at-root # node. As above, returning an @at-root node signals to the parent directive # that it should be bubbled upwards. bubble(node) end # The following directives are visible and have children. This means they need # to be able to handle bubbling up nodes such as @at-root and @media. # Updates the indentation of the rule node based on the nesting # level. The selectors were resolved in {Perform}. def visit_rule(node) yield rules = node.children.select {|c| bubblable?(c)} props = node.children.reject {|c| bubblable?(c) || c.invisible?} unless props.empty? node.children = props rules.each {|r| r.tabs += 1} if node.style == :nested rules.unshift(node) end rules = debubble(rules) unless parent.is_a?(Sass::Tree::RuleNode) || rules.empty? || !bubblable?(rules.last) rules.last.group_end = true end rules end def visit_keyframerule(node) return node unless node.has_children yield debubble(node.children, node) end # Bubbles a directive up through RuleNodes. def visit_directive(node) return node unless node.has_children if parent.is_a?(Sass::Tree::RuleNode) # @keyframes shouldn't include the rule nodes, so we manually create a # bubble that doesn't have the parent's contents for them. return node.normalized_name == '@keyframes' ? Bubble.new(node) : bubble(node) end yield # Since we don't know if the mere presence of an unknown directive may be # important, we should keep an empty version around even if all the contents # are removed via @at-root. However, if the contents are just bubbled out, # we don't need to do so. directive_exists = node.children.any? do |child| next true unless child.is_a?(Bubble) next false unless child.node.is_a?(Sass::Tree::DirectiveNode) child.node.resolved_value == node.resolved_value end # We know empty @keyframes directives do nothing. if directive_exists || node.name == '@keyframes' [] else empty_node = node.dup empty_node.children = [] [empty_node] end + debubble(node.children, node) end # Bubbles the `@media` directive up through RuleNodes # and merges it with other `@media` directives. def visit_media(node) return bubble(node) if parent.is_a?(Sass::Tree::RuleNode) return Bubble.new(node) if parent.is_a?(Sass::Tree::MediaNode) yield debubble(node.children, node) do |child| next child unless child.is_a?(Sass::Tree::MediaNode) # Copies of `node` can be bubbled, and we don't want to merge it with its # own query. next child if child.resolved_query == node.resolved_query next child if child.resolved_query = child.resolved_query.merge(node.resolved_query) end end # Bubbles the `@supports` directive up through RuleNodes. def visit_supports(node) return node unless node.has_children return bubble(node) if parent.is_a?(Sass::Tree::RuleNode) yield debubble(node.children, node) end private # "Bubbles" `node` one level by copying the parent and wrapping `node`'s # children with it. # # @param node [Sass::Tree::Node]. # @return [Bubble] def bubble(node) new_rule = parent.dup new_rule.children = node.children node.children = [new_rule] Bubble.new(node) end # Pops all bubbles in `children` and intersperses the results with the other # values. # # If `parent` is passed, it's copied and used as the parent node for the # nested portions of `children`. # # @param children [List] # @param parent [Sass::Tree::Node] # @yield [node] An optional block for processing bubbled nodes. Each bubbled # node will be passed to this block. # @yieldparam node [Sass::Tree::Node] A bubbled node. # @yieldreturn [Sass::Tree::Node?] A node to use in place of the bubbled node. # This can be the node itself, or `nil` to indicate that the node should be # omitted. # @return [List] def debubble(children, parent = nil) # Keep track of the previous parent so that we don't divide `parent` # unnecessarily if the `@at-root` doesn't produce any new nodes (e.g. # `@at-root {@extend %foo}`). previous_parent = nil Sass::Util.slice_by(children) {|c| c.is_a?(Bubble)}.map do |(is_bubble, slice)| unless is_bubble next slice unless parent if previous_parent previous_parent.children.push(*slice) next [] else previous_parent = new_parent = parent.dup new_parent.children = slice next new_parent end end slice.map do |bubble| next unless (node = block_given? ? yield(bubble.node) : bubble.node) node.tabs += bubble.tabs node.group_end = bubble.group_end results = [visit(node)].flatten previous_parent = nil unless results.empty? results end.compact end.flatten end # Returns whether or not a node can be bubbled up through the syntax tree. # # @param node [Sass::Tree::Node] # @return [Boolean] def bubblable?(node) node.is_a?(Sass::Tree::RuleNode) || node.bubbles? end # A wrapper class for a node that indicates to the parent that it should # treat the wrapped node as a sibling rather than a child. # # Nodes should be wrapped before they're passed to \{Cssize.visit}. They will # be automatically visited upon calling \{#pop}. # # This duck types as a [Sass::Tree::Node] for the purposes of # tree-manipulation operations. class Bubble attr_accessor :node attr_accessor :tabs attr_accessor :group_end def initialize(node) @node = node @tabs = 0 end def bubbles? true end def inspect "(Bubble #{node.inspect})" end end end ruby-sass-3.7.4/lib/sass/tree/visitors/deep_copy.rb000066400000000000000000000044331345125207600223220ustar00rootroot00000000000000# A visitor for copying the full structure of a Sass tree. class Sass::Tree::Visitors::DeepCopy < Sass::Tree::Visitors::Base protected def visit(node) super(node.dup) end def visit_children(parent) parent.children = parent.children.map {|c| visit(c)} parent end def visit_debug(node) node.expr = node.expr.deep_copy yield end def visit_error(node) node.expr = node.expr.deep_copy yield end def visit_each(node) node.list = node.list.deep_copy yield end def visit_extend(node) node.selector = node.selector.map {|c| c.is_a?(Sass::Script::Tree::Node) ? c.deep_copy : c} yield end def visit_for(node) node.from = node.from.deep_copy node.to = node.to.deep_copy yield end def visit_function(node) node.args = node.args.map {|k, v| [k.deep_copy, v && v.deep_copy]} yield end def visit_if(node) node.expr = node.expr.deep_copy if node.expr node.else = visit(node.else) if node.else yield end def visit_mixindef(node) node.args = node.args.map {|k, v| [k.deep_copy, v && v.deep_copy]} yield end def visit_mixin(node) node.args = node.args.map {|a| a.deep_copy} node.keywords = Sass::Util::NormalizedMap.new(Hash[node.keywords.map {|k, v| [k, v.deep_copy]}]) yield end def visit_prop(node) node.name = node.name.map {|c| c.is_a?(Sass::Script::Tree::Node) ? c.deep_copy : c} node.value = node.value.map {|c| c.is_a?(Sass::Script::Tree::Node) ? c.deep_copy : c} yield end def visit_return(node) node.expr = node.expr.deep_copy yield end def visit_rule(node) node.rule = node.rule.map {|c| c.is_a?(Sass::Script::Tree::Node) ? c.deep_copy : c} yield end def visit_variable(node) node.expr = node.expr.deep_copy yield end def visit_warn(node) node.expr = node.expr.deep_copy yield end def visit_while(node) node.expr = node.expr.deep_copy yield end def visit_directive(node) node.value = node.value.map {|c| c.is_a?(Sass::Script::Tree::Node) ? c.deep_copy : c} yield end def visit_media(node) node.query = node.query.map {|c| c.is_a?(Sass::Script::Tree::Node) ? c.deep_copy : c} yield end def visit_supports(node) node.condition = node.condition.deep_copy yield end end ruby-sass-3.7.4/lib/sass/tree/visitors/extend.rb000066400000000000000000000040671345125207600216450ustar00rootroot00000000000000# A visitor for performing selector inheritance on a static CSS tree. # # Destructively modifies the tree. class Sass::Tree::Visitors::Extend < Sass::Tree::Visitors::Base # Performs the given extensions on the static CSS tree based in `root`, then # validates that all extends matched some selector. # # @param root [Tree::Node] The root node of the tree to visit. # @param extends [Sass::Util::SubsetMap{Selector::Simple => # Sass::Tree::Visitors::Cssize::Extend}] # The extensions to perform on this tree. # @return [Object] The return value of \{#visit} for the root node. def self.visit(root, extends) return if extends.empty? new(extends).send(:visit, root) check_extends_fired! extends end protected def initialize(extends) @parent_directives = [] @extends = extends end # If an exception is raised, this adds proper metadata to the backtrace. def visit(node) super(node) rescue Sass::SyntaxError => e e.modify_backtrace(:filename => node.filename, :line => node.line) raise e end # Keeps track of the current parent directives. def visit_children(parent) @parent_directives.push parent if parent.is_a?(Sass::Tree::DirectiveNode) super ensure @parent_directives.pop if parent.is_a?(Sass::Tree::DirectiveNode) end # Applies the extend to a single rule's selector. def visit_rule(node) node.resolved_rules = node.resolved_rules.do_extend(@extends, @parent_directives) end class << self private def check_extends_fired!(extends) extends.each_value do |ex| next if ex.success || ex.node.optional? message = "\"#{ex.extender}\" failed to @extend \"#{ex.target.join}\"." # TODO(nweiz): this should use the Sass stack trace of the extend node. raise Sass::SyntaxError.new(< ex.node.filename, :line => ex.node.line) #{message} The selector "#{ex.target.join}" was not found. Use "@extend #{ex.target.join} !optional" if the extend should be able to fail. MESSAGE end end end end ruby-sass-3.7.4/lib/sass/tree/visitors/perform.rb000066400000000000000000000474031345125207600220310ustar00rootroot00000000000000# A visitor for converting a dynamic Sass tree into a static Sass tree. class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base @@function_name_deprecation = Sass::Deprecation.new class << self # @param root [Tree::Node] The root node of the tree to visit. # @param environment [Sass::Environment] The lexical environment. # @return [Tree::Node] The resulting tree of static nodes. def visit(root, environment = nil) new(environment).send(:visit, root) end # @api private def perform_arguments(callable, args, splat, environment) desc = "#{callable.type.capitalize} #{callable.name}" downcase_desc = "#{callable.type} #{callable.name}" # All keywords are contained in splat.keywords for consistency, # even if there were no splats passed in. old_keywords_accessed = splat.keywords_accessed keywords = splat.keywords splat.keywords_accessed = old_keywords_accessed begin unless keywords.empty? unknown_args = Sass::Util.array_minus(keywords.keys, callable.args.map {|var| var.first.underscored_name}) if callable.splat && unknown_args.include?(callable.splat.underscored_name) raise Sass::SyntaxError.new("Argument $#{callable.splat.name} of #{downcase_desc} " + "cannot be used as a named argument.") elsif unknown_args.any? description = unknown_args.length > 1 ? 'the following arguments:' : 'an argument named' raise Sass::SyntaxError.new("#{desc} doesn't have #{description} " + "#{unknown_args.map {|name| "$#{name}"}.join ', '}.") end end rescue Sass::SyntaxError => keyword_exception end # If there's no splat, raise the keyword exception immediately. The actual # raising happens in the ensure clause at the end of this function. return if keyword_exception && !callable.splat splat_sep = :comma if splat args += splat.to_a splat_sep = splat.separator end if args.size > callable.args.size && !callable.splat extra_args_because_of_splat = splat && args.size - splat.to_a.size <= callable.args.size takes = callable.args.size passed = args.size message = "#{desc} takes #{takes} argument#{'s' unless takes == 1} " + "but #{passed} #{passed == 1 ? 'was' : 'were'} passed." raise Sass::SyntaxError.new(message) unless extra_args_because_of_splat # TODO: when the deprecation period is over, make this an error. Sass::Util.sass_warn("WARNING: #{message}\n" + environment.stack.to_s.gsub(/^/m, " " * 8) + "\n" + "This will be an error in future versions of Sass.") end env = Sass::Environment.new(callable.environment) callable.args.zip(args[0...callable.args.length]) do |(var, default), value| if value && keywords.has_key?(var.name) raise Sass::SyntaxError.new("#{desc} was passed argument $#{var.name} " + "both by position and by name.") end value ||= keywords.delete(var.name) value ||= default && default.perform(env) raise Sass::SyntaxError.new("#{desc} is missing argument #{var.inspect}.") unless value env.set_local_var(var.name, value) end if callable.splat rest = args[callable.args.length..-1] || [] arg_list = Sass::Script::Value::ArgList.new(rest, keywords, splat_sep) arg_list.options = env.options env.set_local_var(callable.splat.name, arg_list) end yield env rescue StandardError => e ensure # If there's a keyword exception, we don't want to throw it immediately, # because the invalid keywords may be part of a glob argument that should be # passed on to another function. So we only raise it if we reach the end of # this function *and* the keywords attached to the argument list glob object # haven't been accessed. # # The keyword exception takes precedence over any Sass errors, but not over # non-Sass exceptions. if keyword_exception && !(arg_list && arg_list.keywords_accessed) && (e.nil? || e.is_a?(Sass::SyntaxError)) raise keyword_exception elsif e raise e end end # @api private # @return [Sass::Script::Value::ArgList] def perform_splat(splat, performed_keywords, kwarg_splat, environment) args, kwargs, separator = [], nil, :comma if splat splat = splat.perform(environment) separator = splat.separator || separator if splat.is_a?(Sass::Script::Value::ArgList) args = splat.to_a kwargs = splat.keywords elsif splat.is_a?(Sass::Script::Value::Map) kwargs = arg_hash(splat) else args = splat.to_a end end kwargs ||= Sass::Util::NormalizedMap.new kwargs.update(performed_keywords) if kwarg_splat kwarg_splat = kwarg_splat.perform(environment) unless kwarg_splat.is_a?(Sass::Script::Value::Map) raise Sass::SyntaxError.new("Variable keyword arguments must be a map " + "(was #{kwarg_splat.inspect}).") end kwargs.update(arg_hash(kwarg_splat)) end Sass::Script::Value::ArgList.new(args, kwargs, separator) end private def arg_hash(map) Sass::Util.map_keys(map.to_h) do |key| next key.value if key.is_a?(Sass::Script::Value::String) raise Sass::SyntaxError.new("Variable keyword argument map must have string keys.\n" + "#{key.inspect} is not a string in #{map.inspect}.") end end end protected def initialize(env) @environment = env @in_keyframes = false @at_root_without_rule = false end # If an exception is raised, this adds proper metadata to the backtrace. def visit(node) return super(node.dup) unless @environment @environment.stack.with_base(node.filename, node.line) {super(node.dup)} rescue Sass::SyntaxError => e e.modify_backtrace(:filename => node.filename, :line => node.line) raise e end # Keeps track of the current environment. def visit_children(parent) with_environment Sass::Environment.new(@environment, parent.options) do parent.children = super.flatten parent end end # Runs a block of code with the current environment replaced with the given one. # # @param env [Sass::Environment] The new environment for the duration of the block. # @yield A block in which the environment is set to `env`. # @return [Object] The return value of the block. def with_environment(env) old_env, @environment = @environment, env yield ensure @environment = old_env end # Sets the options on the environment if this is the top-level root. def visit_root(node) yield rescue Sass::SyntaxError => e e.sass_template ||= node.template raise e end # Removes this node from the tree if it's a silent comment. def visit_comment(node) return [] if node.invisible? node.resolved_value = run_interp_no_strip(node.value) node.resolved_value.gsub!(/\\([\\#])/, '\1') node end # Prints the expression to STDERR. def visit_debug(node) res = node.expr.perform(@environment) if res.is_a?(Sass::Script::Value::String) res = res.value else res = res.to_sass end if node.filename Sass::Util.sass_warn "#{node.filename}:#{node.line} DEBUG: #{res}" else Sass::Util.sass_warn "Line #{node.line} DEBUG: #{res}" end [] end # Throws the expression as an error. def visit_error(node) res = node.expr.perform(@environment) if res.is_a?(Sass::Script::Value::String) res = res.value else res = res.to_sass end raise Sass::SyntaxError.new(res) end # Runs the child nodes once for each value in the list. def visit_each(node) list = node.list.perform(@environment) with_environment Sass::SemiGlobalEnvironment.new(@environment) do list.to_a.map do |value| if node.vars.length == 1 @environment.set_local_var(node.vars.first, value) else node.vars.zip(value.to_a) do |(var, sub_value)| @environment.set_local_var(var, sub_value || Sass::Script::Value::Null.new) end end node.children.map {|c| visit(c)} end.flatten end end # Runs SassScript interpolation in the selector, # and then parses the result into a {Sass::Selector::CommaSequence}. def visit_extend(node) parser = Sass::SCSS::StaticParser.new(run_interp(node.selector), node.filename, node.options[:importer], node.line) node.resolved_selector = parser.parse_selector node end # Runs the child nodes once for each time through the loop, varying the variable each time. def visit_for(node) from = node.from.perform(@environment) to = node.to.perform(@environment) from.assert_int! to.assert_int! to = to.coerce(from.numerator_units, from.denominator_units) direction = from.to_i > to.to_i ? -1 : 1 range = Range.new(direction * from.to_i, direction * to.to_i, node.exclusive) with_environment Sass::SemiGlobalEnvironment.new(@environment) do range.map do |i| @environment.set_local_var(node.var, Sass::Script::Value::Number.new(direction * i, from.numerator_units, from.denominator_units)) node.children.map {|c| visit(c)} end.flatten end end # Loads the function into the environment. def visit_function(node) env = Sass::Environment.new(@environment, node.options) if node.normalized_name == 'calc' || node.normalized_name == 'element' || node.name == 'expression' || node.name == 'url' @@function_name_deprecation.warn(node.filename, node.line, < e e.modify_backtrace(:filename => node.imported_file.options[:filename]) e.add_backtrace(:filename => node.filename, :line => node.line) raise e end end # Loads a mixin into the environment. def visit_mixindef(node) env = Sass::Environment.new(@environment, node.options) @environment.set_local_mixin(node.name, Sass::Callable.new(node.name, node.args, node.splat, env, node.children, node.has_content, "mixin", :stylesheet)) [] end # Runs a mixin. def visit_mixin(node) @environment.stack.with_mixin(node.filename, node.line, node.name) do mixin = @environment.mixin(node.name) raise Sass::SyntaxError.new("Undefined mixin '#{node.name}'.") unless mixin if node.has_children && !mixin.has_content raise Sass::SyntaxError.new(%(Mixin "#{node.name}" does not accept a content block.)) end args = node.args.map {|a| a.perform(@environment)} keywords = Sass::Util.map_vals(node.keywords) {|v| v.perform(@environment)} splat = self.class.perform_splat(node.splat, keywords, node.kwarg_splat, @environment) self.class.perform_arguments(mixin, args, splat, @environment) do |env| env.caller = Sass::Environment.new(@environment) env.content = [node.children, @environment] if node.has_children trace_node = Sass::Tree::TraceNode.from_node(node.name, node) with_environment(env) {trace_node.children = mixin.tree.map {|c| visit(c)}.flatten} trace_node end end rescue Sass::SyntaxError => e e.modify_backtrace(:mixin => node.name, :line => node.line) e.add_backtrace(:line => node.line) raise e end def visit_content(node) content, content_env = @environment.content return [] unless content @environment.stack.with_mixin(node.filename, node.line, '@content') do trace_node = Sass::Tree::TraceNode.from_node('@content', node) content_env = Sass::Environment.new(content_env) content_env.caller = Sass::Environment.new(@environment) with_environment(content_env) do trace_node.children = content.map {|c| visit(c.dup)}.flatten end trace_node end rescue Sass::SyntaxError => e e.modify_backtrace(:mixin => '@content', :line => node.line) e.add_backtrace(:line => node.line) raise e end # Runs any SassScript that may be embedded in a property. def visit_prop(node) node.resolved_name = run_interp(node.name) # If the node's value is just a variable or similar, we may get a useful # source range from evaluating it. if node.value.length == 1 && node.value.first.is_a?(Sass::Script::Tree::Node) result = node.value.first.perform(@environment) node.resolved_value = result.to_s node.value_source_range = result.source_range if result.source_range elsif node.custom_property? node.resolved_value = run_interp_no_strip(node.value) else node.resolved_value = run_interp(node.value) end yield end # Returns the value of the expression. def visit_return(node) throw :_sass_return, node.expr.perform(@environment) end # Runs SassScript interpolation in the selector, # and then parses the result into a {Sass::Selector::CommaSequence}. def visit_rule(node) old_at_root_without_rule = @at_root_without_rule parser = Sass::SCSS::StaticParser.new(run_interp(node.rule), node.filename, node.options[:importer], node.line) if @in_keyframes keyframe_rule_node = Sass::Tree::KeyframeRuleNode.new(parser.parse_keyframes_selector) keyframe_rule_node.options = node.options keyframe_rule_node.line = node.line keyframe_rule_node.filename = node.filename keyframe_rule_node.source_range = node.source_range keyframe_rule_node.has_children = node.has_children with_environment Sass::Environment.new(@environment, node.options) do keyframe_rule_node.children = node.children.map {|c| visit(c)}.flatten end keyframe_rule_node else @at_root_without_rule = false node.parsed_rules ||= parser.parse_selector node.resolved_rules = node.parsed_rules.resolve_parent_refs( @environment.selector, !old_at_root_without_rule) node.stack_trace = @environment.stack.to_s if node.options[:trace_selectors] with_environment Sass::Environment.new(@environment, node.options) do @environment.selector = node.resolved_rules node.children = node.children.map {|c| visit(c)}.flatten end node end ensure @at_root_without_rule = old_at_root_without_rule end # Sets a variable that indicates that the first level of rule nodes # shouldn't include the parent selector by default. def visit_atroot(node) if node.query parser = Sass::SCSS::StaticParser.new(run_interp(node.query), node.filename, node.options[:importer], node.line) node.resolved_type, node.resolved_value = parser.parse_static_at_root_query else node.resolved_type, node.resolved_value = :without, ['rule'] end old_at_root_without_rule = @at_root_without_rule old_in_keyframes = @in_keyframes @at_root_without_rule = true if node.exclude?('rule') @in_keyframes = false if node.exclude?('keyframes') yield ensure @in_keyframes = old_in_keyframes @at_root_without_rule = old_at_root_without_rule end # Loads the new variable value into the environment. def visit_variable(node) env = @environment env = env.global_env if node.global if node.guarded var = env.var(node.name) return [] if var && !var.null? end val = node.expr.perform(@environment) if node.expr.source_range val.source_range = node.expr.source_range else val.source_range = node.source_range end env.set_var(node.name, val) [] end # Prints the expression to STDERR with a stylesheet trace. def visit_warn(node) res = node.expr.perform(@environment) res = res.value if res.is_a?(Sass::Script::Value::String) @environment.stack.with_directive(node.filename, node.line, "@warn") do msg = "WARNING: #{res}\n " msg << @environment.stack.to_s.gsub("\n", "\n ") << "\n" Sass::Util.sass_warn msg end [] end # Runs the child nodes until the continuation expression becomes false. def visit_while(node) children = [] with_environment Sass::SemiGlobalEnvironment.new(@environment) do children += node.children.map {|c| visit(c)} while node.expr.perform(@environment).to_bool end children.flatten end def visit_directive(node) node.resolved_value = run_interp(node.value) old_in_keyframes, @in_keyframes = @in_keyframes, node.normalized_name == "@keyframes" with_environment Sass::Environment.new(@environment) do node.children = node.children.map {|c| visit(c)}.flatten node end ensure @in_keyframes = old_in_keyframes end def visit_media(node) parser = Sass::SCSS::StaticParser.new(run_interp(node.query), node.filename, node.options[:importer], node.line) node.resolved_query ||= parser.parse_media_query_list yield end def visit_supports(node) node.condition = node.condition.deep_copy node.condition.perform(@environment) yield end def visit_cssimport(node) node.resolved_uri = run_interp([node.uri]) if node.query && !node.query.empty? parser = Sass::SCSS::StaticParser.new(run_interp(node.query), node.filename, node.options[:importer], node.line) node.resolved_query ||= parser.parse_media_query_list end if node.supports_condition node.supports_condition.perform(@environment) end yield end private def run_interp_no_strip(text) text.map do |r| next r if r.is_a?(String) r.perform(@environment).to_s(:quote => :none) end.join end def run_interp(text) Sass::Util.strip_except_escapes(run_interp_no_strip(text)) end def handle_import_loop!(node) msg = "An @import loop has been found:" files = @environment.stack.frames.select {|f| f.is_import?}.map {|f| f.filename}.compact if node.filename == node.imported_file.options[:filename] raise Sass::SyntaxError.new("#{msg} #{node.filename} imports itself") end files << node.filename << node.imported_file.options[:filename] msg << "\n" << files.each_cons(2).map do |m1, m2| " #{m1} imports #{m2}" end.join("\n") raise Sass::SyntaxError.new(msg) end end ruby-sass-3.7.4/lib/sass/tree/visitors/set_options.rb000066400000000000000000000061651345125207600227250ustar00rootroot00000000000000# A visitor for setting options on the Sass tree class Sass::Tree::Visitors::SetOptions < Sass::Tree::Visitors::Base # @param root [Tree::Node] The root node of the tree to visit. # @param options [{Symbol => Object}] The options has to set. def self.visit(root, options); new(options).send(:visit, root); end protected def initialize(options) @options = options end def visit(node) node.instance_variable_set('@options', @options) super end def visit_comment(node) node.value.each {|c| c.options = @options if c.is_a?(Sass::Script::Tree::Node)} yield end def visit_debug(node) node.expr.options = @options yield end def visit_error(node) node.expr.options = @options yield end def visit_each(node) node.list.options = @options yield end def visit_extend(node) node.selector.each {|c| c.options = @options if c.is_a?(Sass::Script::Tree::Node)} yield end def visit_for(node) node.from.options = @options node.to.options = @options yield end def visit_function(node) node.args.each do |k, v| k.options = @options v.options = @options if v end node.splat.options = @options if node.splat yield end def visit_if(node) node.expr.options = @options if node.expr visit(node.else) if node.else yield end def visit_import(node) # We have no good way of propagating the new options through an Engine # instance, so we just null it out. This also lets us avoid caching an # imported Engine along with the importing source tree. node.imported_file = nil yield end def visit_mixindef(node) node.args.each do |k, v| k.options = @options v.options = @options if v end node.splat.options = @options if node.splat yield end def visit_mixin(node) node.args.each {|a| a.options = @options} node.keywords.each {|_k, v| v.options = @options} node.splat.options = @options if node.splat node.kwarg_splat.options = @options if node.kwarg_splat yield end def visit_prop(node) node.name.each {|c| c.options = @options if c.is_a?(Sass::Script::Tree::Node)} node.value.each {|c| c.options = @options if c.is_a?(Sass::Script::Tree::Node)} yield end def visit_return(node) node.expr.options = @options yield end def visit_rule(node) node.rule.each {|c| c.options = @options if c.is_a?(Sass::Script::Tree::Node)} yield end def visit_variable(node) node.expr.options = @options yield end def visit_warn(node) node.expr.options = @options yield end def visit_while(node) node.expr.options = @options yield end def visit_directive(node) node.value.each {|c| c.options = @options if c.is_a?(Sass::Script::Tree::Node)} yield end def visit_media(node) node.query.each {|c| c.options = @options if c.is_a?(Sass::Script::Tree::Node)} yield end def visit_cssimport(node) node.query.each {|c| c.options = @options if c.is_a?(Sass::Script::Tree::Node)} if node.query yield end def visit_supports(node) node.condition.options = @options yield end end ruby-sass-3.7.4/lib/sass/tree/visitors/to_css.rb000066400000000000000000000316761345125207600216560ustar00rootroot00000000000000# A visitor for converting a Sass tree into CSS. class Sass::Tree::Visitors::ToCss < Sass::Tree::Visitors::Base # The source mapping for the generated CSS file. This is only set if # `build_source_mapping` is passed to the constructor and \{Sass::Engine#render} has been # run. attr_reader :source_mapping # @param build_source_mapping [Boolean] Whether to build a # \{Sass::Source::Map} while creating the CSS output. The mapping will # be available from \{#source\_mapping} after the visitor has completed. def initialize(build_source_mapping = false) @tabs = 0 @line = 1 @offset = 1 @result = String.new("") @source_mapping = build_source_mapping ? Sass::Source::Map.new : nil @lstrip = nil @in_directive = false end # Runs the visitor on `node`. # # @param node [Sass::Tree::Node] The root node of the tree to convert to CSS> # @return [String] The CSS output. def visit(node) super rescue Sass::SyntaxError => e e.modify_backtrace(:filename => node.filename, :line => node.line) raise e end protected def with_tabs(tabs) old_tabs, @tabs = @tabs, tabs yield ensure @tabs = old_tabs end # Associate all output produced in a block with a given node. Used for source # mapping. def for_node(node, attr_prefix = nil) return yield unless @source_mapping start_pos = Sass::Source::Position.new(@line, @offset) yield range_attr = attr_prefix ? :"#{attr_prefix}_source_range" : :source_range return if node.invisible? || !node.send(range_attr) source_range = node.send(range_attr) target_end_pos = Sass::Source::Position.new(@line, @offset) target_range = Sass::Source::Range.new(start_pos, target_end_pos, nil) @source_mapping.add(source_range, target_range) end def trailing_semicolon? @result.end_with?(";") && !@result.end_with?('\;') end # Move the output cursor back `chars` characters. def erase!(chars) return if chars == 0 str = @result.slice!(-chars..-1) newlines = str.count("\n") if newlines > 0 @line -= newlines @offset = @result[@result.rindex("\n") || 0..-1].size else @offset -= chars end end # Avoid allocating lots of new strings for `#output`. This is important # because `#output` is called all the time. NEWLINE = "\n" # Add `s` to the output string and update the line and offset information # accordingly. def output(s) if @lstrip s = s.gsub(/\A\s+/, "") @lstrip = false end newlines = s.count(NEWLINE) if newlines > 0 @line += newlines @offset = s[s.rindex(NEWLINE)..-1].size else @offset += s.size end @result << s end # Strip all trailing whitespace from the output string. def rstrip! erase! @result.length - 1 - (@result.rindex(/[^\s]/) || -1) end # lstrip the first output in the given block. def lstrip old_lstrip = @lstrip @lstrip = true yield ensure @lstrip &&= old_lstrip end # Prepend `prefix` to the output string. def prepend!(prefix) @result.insert 0, prefix return unless @source_mapping line_delta = prefix.count("\n") offset_delta = prefix.gsub(/.*\n/, '').size @source_mapping.shift_output_offsets(offset_delta) @source_mapping.shift_output_lines(line_delta) end def visit_root(node) node.children.each do |child| next if child.invisible? visit(child) next if node.style == :compressed output "\n" next unless child.is_a?(Sass::Tree::DirectiveNode) && child.has_children && !child.bubbles? output "\n" end rstrip! if node.style == :compressed && trailing_semicolon? erase! 1 end return "" if @result.empty? output "\n" unless @result.ascii_only? if node.style == :compressed # A byte order mark is sufficient to tell browsers that this # file is UTF-8 encoded, and will override any other detection # methods as per http://encoding.spec.whatwg.org/#decode-and-encode. prepend! "\uFEFF" else prepend! "@charset \"UTF-8\";\n" end end @result rescue Sass::SyntaxError => e e.sass_template ||= node.template raise e end def visit_charset(node) for_node(node) {output("@charset \"#{node.name}\";")} end def visit_comment(node) return if node.invisible? spaces = (' ' * [@tabs - node.resolved_value[/^ */].size, 0].max) output(spaces) content = node.resolved_value.split("\n").join("\n" + spaces) if node.type == :silent content.gsub!(%r{^(\s*)//(.*)$}) {"#{$1}/*#{$2} */"} end if (node.style == :compact || node.style == :compressed) && node.type != :loud content.gsub!(%r{\n +(\* *(?!/))?}, ' ') end for_node(node) {output(content)} end def visit_directive(node) was_in_directive = @in_directive tab_str = ' ' * @tabs if !node.has_children || node.children.empty? output(tab_str) for_node(node) {output(node.resolved_value)} if node.has_children output("#{' ' unless node.style == :compressed}{}") elsif node.children.empty? output(";") end return end @in_directive ||= !node.is_a?(Sass::Tree::MediaNode) output(tab_str) if node.style != :compressed for_node(node) {output(node.resolved_value)} output(node.style == :compressed ? "{" : " {") output(node.style == :compact ? ' ' : "\n") if node.style != :compressed had_children = true first = true node.children.each do |child| next if child.invisible? if node.style == :compact if child.is_a?(Sass::Tree::PropNode) with_tabs(first || !had_children ? 0 : @tabs + 1) do visit(child) output(' ') end else unless had_children erase! 1 output "\n" end if first lstrip {with_tabs(@tabs + 1) {visit(child)}} else with_tabs(@tabs + 1) {visit(child)} end rstrip! output "\n" end had_children = child.has_children first = false elsif node.style == :compressed unless had_children output(";") unless trailing_semicolon? end with_tabs(0) {visit(child)} had_children = child.has_children else with_tabs(@tabs + 1) {visit(child)} output "\n" end end rstrip! if node.style == :compressed && trailing_semicolon? erase! 1 end if node.style == :expanded output("\n#{tab_str}") elsif node.style != :compressed output(" ") end output("}") ensure @in_directive = was_in_directive end def visit_media(node) with_tabs(@tabs + node.tabs) {visit_directive(node)} output("\n") if node.style != :compressed && node.group_end end def visit_supports(node) visit_media(node) end def visit_cssimport(node) visit_directive(node) end def visit_prop(node) return if node.resolved_value.empty? && !node.custom_property? tab_str = ' ' * (@tabs + node.tabs) output(tab_str) for_node(node, :name) {output(node.resolved_name)} output(":") output(" ") unless node.style == :compressed || node.custom_property? for_node(node, :value) do output(if node.custom_property? format_custom_property_value(node) else node.resolved_value end) end output(";") unless node.style == :compressed end def visit_rule(node) with_tabs(@tabs + node.tabs) do rule_separator = node.style == :compressed ? ',' : ', ' line_separator = case node.style when :nested, :expanded; "\n" when :compressed; "" else; " " end rule_indent = ' ' * @tabs per_rule_indent, total_indent = if [:nested, :expanded].include?(node.style) [rule_indent, ''] else ['', rule_indent] end joined_rules = node.resolved_rules.members.map do |seq| next if seq.invisible? rule_part = seq.to_s(style: node.style, placeholder: false) if node.style == :compressed rule_part.gsub!(/([^,])\s*\n\s*/m, '\1 ') rule_part.gsub!(/\s*([+>])\s*/m, '\1') rule_part.gsub!(/nth([^( ]*)\(([^)]*)\)/m) do |match| match.tr(" \t\n", "") end rule_part = Sass::Util.strip_except_escapes(rule_part) end rule_part end.compact.join(rule_separator) joined_rules.lstrip! joined_rules.gsub!(/\s*\n\s*/, "#{line_separator}#{per_rule_indent}") old_spaces = ' ' * @tabs if node.style != :compressed if node.options[:debug_info] && !@in_directive visit(debug_info_rule(node.debug_info, node.options)) output "\n" elsif node.options[:trace_selectors] output("#{old_spaces}/* ") output(node.stack_trace.gsub("\n", "\n #{old_spaces}")) output(" */\n") elsif node.options[:line_comments] output("#{old_spaces}/* line #{node.line}") if node.filename relative_filename = if node.options[:css_filename] begin Sass::Util.relative_path_from( node.filename, File.dirname(node.options[:css_filename])).to_s rescue ArgumentError nil end end relative_filename ||= node.filename output(", #{relative_filename}") end output(" */\n") end end end_props, trailer, tabs = '', '', 0 if node.style == :compact separator, end_props, bracket = ' ', ' ', ' { ' trailer = "\n" if node.group_end elsif node.style == :compressed separator, bracket = ';', '{' else tabs = @tabs + 1 separator, bracket = "\n", " {\n" trailer = "\n" if node.group_end end_props = (node.style == :expanded ? "\n" + old_spaces : ' ') end output(total_indent + per_rule_indent) for_node(node, :selector) {output(joined_rules)} output(bracket) with_tabs(tabs) do node.children.each_with_index do |child, i| if i > 0 if separator.start_with?(";") && trailing_semicolon? erase! 1 end output(separator) end visit(child) end end if node.style == :compressed && trailing_semicolon? erase! 1 end output(end_props) output("}" + trailer) end end def visit_keyframerule(node) visit_directive(node) end private # Reformats the value of `node` so that it's nicely indented, preserving its # existing relative indentation. # # @param node [Sass::Script::Tree::PropNode] A custom property node. # @return [String] def format_custom_property_value(node) value = node.resolved_value.sub(/\n[ \t\r\f\n]*\Z/, ' ') if node.style == :compact || node.style == :compressed || !value.include?("\n") # Folding not involving newlines was done in the parser. We can safely # fold newlines here because tokens like strings can't contain literal # newlines, so we know any adjacent whitespace is tokenized as whitespace. return node.resolved_value.gsub(/[ \t\r\f]*\n[ \t\r\f\n]*/, ' ') end # Find the smallest amount of indentation in the custom property and use # that as the base indentation level. lines = value.split("\n") indented_lines = lines[1..-1] min_indentation = indented_lines. map {|line| line[/^[ \t]*/]}. reject {|line| line.empty?}. min_by {|line| line.length} # Limit the base indentation to the same indentation level as the node name # so that if *every* line is indented relative to the property name that's # preserved. if node.name_source_range base_indentation = min_indentation[0...node.name_source_range.start_pos.offset - 1] end lines.first + "\n" + indented_lines.join("\n").gsub(/^#{base_indentation}/, ' ' * @tabs) end def debug_info_rule(debug_info, options) node = Sass::Tree::DirectiveNode.resolved("@media -sass-debug-info") debug_info.map {|k, v| [k.to_s, v.to_s]}.to_a.each do |k, v| rule = Sass::Tree::RuleNode.new([""]) rule.resolved_rules = Sass::Selector::CommaSequence.new( [Sass::Selector::Sequence.new( [Sass::Selector::SimpleSequence.new( [Sass::Selector::Element.new(k.to_s.gsub(/[^\w-]/, "\\\\\\0"), nil)], false) ]) ]) prop = Sass::Tree::PropNode.new([""], [""], :new) prop.resolved_name = "font-family" prop.resolved_value = Sass::SCSS::RX.escape_ident(v.to_s) rule << prop node << rule end node.options = options.merge(:debug_info => false, :line_comments => false, :style => :compressed) node end end ruby-sass-3.7.4/lib/sass/tree/warn_node.rb000066400000000000000000000006121345125207600204400ustar00rootroot00000000000000module Sass module Tree # A dynamic node representing a Sass `@warn` statement. # # @see Sass::Tree class WarnNode < Node # The expression to print. # @return [Script::Tree::Node] attr_accessor :expr # @param expr [Script::Tree::Node] The expression to print def initialize(expr) @expr = expr super() end end end end ruby-sass-3.7.4/lib/sass/tree/while_node.rb000066400000000000000000000006051345125207600206030ustar00rootroot00000000000000require 'sass/tree/node' module Sass::Tree # A dynamic node representing a Sass `@while` loop. # # @see Sass::Tree class WhileNode < Node # The parse tree for the continuation expression. # @return [Script::Tree::Node] attr_accessor :expr # @param expr [Script::Tree::Node] See \{#expr} def initialize(expr) @expr = expr super() end end end ruby-sass-3.7.4/lib/sass/util.rb000066400000000000000000001117611345125207600165120ustar00rootroot00000000000000# -*- coding: utf-8 -*- require 'erb' require 'set' require 'enumerator' require 'stringio' require 'rbconfig' require 'uri' require 'thread' require 'pathname' require 'sass/root' require 'sass/util/subset_map' module Sass # A module containing various useful functions. module Util extend self # An array of ints representing the Ruby version number. # @api public RUBY_VERSION_COMPONENTS = RUBY_VERSION.split(".").map {|s| s.to_i} # The Ruby engine we're running under. Defaults to `"ruby"` # if the top-level constant is undefined. # @api public RUBY_ENGINE = defined?(::RUBY_ENGINE) ? ::RUBY_ENGINE : "ruby" # Returns the path of a file relative to the Sass root directory. # # @param file [String] The filename relative to the Sass root # @return [String] The filename relative to the the working directory def scope(file) File.join(Sass::ROOT_DIR, file) end # Maps the keys in a hash according to a block. # # @example # map_keys({:foo => "bar", :baz => "bang"}) {|k| k.to_s} # #=> {"foo" => "bar", "baz" => "bang"} # @param hash [Hash] The hash to map # @yield [key] A block in which the keys are transformed # @yieldparam key [Object] The key that should be mapped # @yieldreturn [Object] The new value for the key # @return [Hash] The mapped hash # @see #map_vals # @see #map_hash def map_keys(hash) map_hash(hash) {|k, v| [yield(k), v]} end # Maps the values in a hash according to a block. # # @example # map_values({:foo => "bar", :baz => "bang"}) {|v| v.to_sym} # #=> {:foo => :bar, :baz => :bang} # @param hash [Hash] The hash to map # @yield [value] A block in which the values are transformed # @yieldparam value [Object] The value that should be mapped # @yieldreturn [Object] The new value for the value # @return [Hash] The mapped hash # @see #map_keys # @see #map_hash def map_vals(hash) # We don't delegate to map_hash for performance here # because map_hash does more than is necessary. rv = hash.class.new hash = hash.as_stored if hash.is_a?(NormalizedMap) hash.each do |k, v| rv[k] = yield(v) end rv end # Maps the key-value pairs of a hash according to a block. # # @example # map_hash({:foo => "bar", :baz => "bang"}) {|k, v| [k.to_s, v.to_sym]} # #=> {"foo" => :bar, "baz" => :bang} # @param hash [Hash] The hash to map # @yield [key, value] A block in which the key-value pairs are transformed # @yieldparam [key] The hash key # @yieldparam [value] The hash value # @yieldreturn [(Object, Object)] The new value for the `[key, value]` pair # @return [Hash] The mapped hash # @see #map_keys # @see #map_vals def map_hash(hash) # Copy and modify is more performant than mapping to an array and using # to_hash on the result. rv = hash.class.new hash.each do |k, v| new_key, new_value = yield(k, v) new_key = hash.denormalize(new_key) if hash.is_a?(NormalizedMap) && new_key == k rv[new_key] = new_value end rv end # Computes the powerset of the given array. # This is the set of all subsets of the array. # # @example # powerset([1, 2, 3]) #=> # Set[Set[], Set[1], Set[2], Set[3], Set[1, 2], Set[2, 3], Set[1, 3], Set[1, 2, 3]] # @param arr [Enumerable] # @return [Set] The subsets of `arr` def powerset(arr) arr.inject([Set.new].to_set) do |powerset, el| new_powerset = Set.new powerset.each do |subset| new_powerset << subset new_powerset << subset + [el] end new_powerset end end # Restricts a number to falling within a given range. # Returns the number if it falls within the range, # or the closest value in the range if it doesn't. # # @param value [Numeric] # @param range [Range] # @return [Numeric] def restrict(value, range) [[value, range.first].max, range.last].min end # Like [Fixnum.round], but leaves rooms for slight floating-point # differences. # # @param value [Numeric] # @return [Numeric] def round(value) # If the number is within epsilon of X.5, round up (or down for negative # numbers). mod = value % 1 mod_is_half = (mod - 0.5).abs < Script::Value::Number.epsilon if value > 0 !mod_is_half && mod < 0.5 ? value.floor : value.ceil else mod_is_half || mod < 0.5 ? value.floor : value.ceil end end # Concatenates all strings that are adjacent in an array, # while leaving other elements as they are. # # @example # merge_adjacent_strings([1, "foo", "bar", 2, "baz"]) # #=> [1, "foobar", 2, "baz"] # @param arr [Array] # @return [Array] The enumerable with strings merged def merge_adjacent_strings(arr) # Optimize for the common case of one element return arr if arr.size < 2 arr.inject([]) do |a, e| if e.is_a?(String) if a.last.is_a?(String) a.last << e else a << e.dup end else a << e end a end end # Non-destructively replaces all occurrences of a subsequence in an array # with another subsequence. # # @example # replace_subseq([1, 2, 3, 4, 5], [2, 3], [:a, :b]) # #=> [1, :a, :b, 4, 5] # # @param arr [Array] The array whose subsequences will be replaced. # @param subseq [Array] The subsequence to find and replace. # @param replacement [Array] The sequence that `subseq` will be replaced with. # @return [Array] `arr` with `subseq` replaced with `replacement`. def replace_subseq(arr, subseq, replacement) new = [] matched = [] i = 0 arr.each do |elem| if elem != subseq[i] new.push(*matched) matched = [] i = 0 new << elem next end if i == subseq.length - 1 matched = [] i = 0 new.push(*replacement) else matched << elem i += 1 end end new.push(*matched) new end # Intersperses a value in an enumerable, as would be done with `Array#join` # but without concatenating the array together afterwards. # # @param enum [Enumerable] # @param val # @return [Array] def intersperse(enum, val) enum.inject([]) {|a, e| a << e << val}[0...-1] end def slice_by(enum) results = [] enum.each do |value| key = yield(value) if !results.empty? && results.last.first == key results.last.last << value else results << [key, [value]] end end results end # Substitutes a sub-array of one array with another sub-array. # # @param ary [Array] The array in which to make the substitution # @param from [Array] The sequence of elements to replace with `to` # @param to [Array] The sequence of elements to replace `from` with def substitute(ary, from, to) res = ary.dup i = 0 while i < res.size if res[i...i + from.size] == from res[i...i + from.size] = to end i += 1 end res end # Destructively strips whitespace from the beginning and end of the first # and last elements, respectively, in the array (if those elements are # strings). Preserves CSS escapes at the end of the array. # # @param arr [Array] # @return [Array] `arr` def strip_string_array(arr) arr.first.lstrip! if arr.first.is_a?(String) arr[-1] = Sass::Util.rstrip_except_escapes(arr[-1]) if arr.last.is_a?(String) arr end # Normalizes identifier escapes. # # See https://github.com/sass/language/blob/master/accepted/identifier-escapes.md. # # @param ident [String] # @return [String] def normalize_ident_escapes(ident, start: true) ident.gsub(/(^)?(#{Sass::SCSS::RX::ESCAPE})/) do |s| at_start = start && $1 char = escaped_char(s) next char if char =~ (at_start ? Sass::SCSS::RX::NMSTART : Sass::SCSS::RX::NMCHAR) if char =~ (at_start ? /[\x0-\x1F\x7F0-9]/ : /[\x0-\x1F\x7F]/) "\\#{char.ord.to_s(16)} " else "\\#{char}" end end end # Returns the character encoded by the given escape sequence. # # @param escape [String] # @return [String] def escaped_char(escape) if escape =~ /^\\([0-9a-fA-F]{1,6})[ \t\r\n\f]?/ $1.to_i(16).chr(Encoding::UTF_8) else escape[1] end end # Like [String#strip], but preserves escaped whitespace at the end of the # string. # # @param string [String] # @return [String] def strip_except_escapes(string) rstrip_except_escapes(string.lstrip) end # Like [String#rstrip], but preserves escaped whitespace at the end of the # string. # # @param string [String] # @return [String] def rstrip_except_escapes(string) string.sub(/(?] # @return [Array] # # @example # paths([[1, 2], [3, 4], [5]]) #=> # # [[1, 3, 5], # # [2, 3, 5], # # [1, 4, 5], # # [2, 4, 5]] def paths(arrs) arrs.inject([[]]) do |paths, arr| arr.map {|e| paths.map {|path| path + [e]}}.flatten(1) end end # Computes a single longest common subsequence for `x` and `y`. # If there are more than one longest common subsequences, # the one returned is that which starts first in `x`. # # @param x [Array] # @param y [Array] # @yield [a, b] An optional block to use in place of a check for equality # between elements of `x` and `y`. # @yieldreturn [Object, nil] If the two values register as equal, # this will return the value to use in the LCS array. # @return [Array] The LCS def lcs(x, y, &block) x = [nil, *x] y = [nil, *y] block ||= proc {|a, b| a == b && a} lcs_backtrace(lcs_table(x, y, &block), x, y, x.size - 1, y.size - 1, &block) end # Like `String.upcase`, but only ever upcases ASCII letters. def upcase(string) return string.upcase unless ruby2_4? string.upcase(:ascii) end # Like `String.downcase`, but only ever downcases ASCII letters. def downcase(string) return string.downcase unless ruby2_4? string.downcase(:ascii) end # Returns a sub-array of `minuend` containing only elements that are also in # `subtrahend`. Ensures that the return value has the same order as # `minuend`, even on Rubinius where that's not guaranteed by `Array#-`. # # @param minuend [Array] # @param subtrahend [Array] # @return [Array] def array_minus(minuend, subtrahend) return minuend - subtrahend unless rbx? set = Set.new(minuend) - subtrahend minuend.select {|e| set.include?(e)} end # Returns the maximum of `val1` and `val2`. We use this over \{Array.max} to # avoid unnecessary garbage collection. def max(val1, val2) val1 > val2 ? val1 : val2 end # Returns the minimum of `val1` and `val2`. We use this over \{Array.min} to # avoid unnecessary garbage collection. def min(val1, val2) val1 <= val2 ? val1 : val2 end # Returns a string description of the character that caused an # `Encoding::UndefinedConversionError`. # # @param e [Encoding::UndefinedConversionError] # @return [String] def undefined_conversion_error_char(e) # Rubinius (as of 2.0.0.rc1) pre-quotes the error character. return e.error_char if rbx? # JRuby (as of 1.7.2) doesn't have an error_char field on # Encoding::UndefinedConversionError. return e.error_char.dump unless jruby? e.message[/^"[^"]+"/] # " end # Asserts that `value` falls within `range` (inclusive), leaving # room for slight floating-point errors. # # @param name [String] The name of the value. Used in the error message. # @param range [Range] The allowed range of values. # @param value [Numeric, Sass::Script::Value::Number] The value to check. # @param unit [String] The unit of the value. Used in error reporting. # @return [Numeric] `value` adjusted to fall within range, if it # was outside by a floating-point margin. def check_range(name, range, value, unit = '') grace = (-0.00001..0.00001) str = value.to_s value = value.value if value.is_a?(Sass::Script::Value::Number) return value if range.include?(value) return range.first if grace.include?(value - range.first) return range.last if grace.include?(value - range.last) raise ArgumentError.new( "#{name} #{str} must be between #{range.first}#{unit} and #{range.last}#{unit}") end # Returns whether or not `seq1` is a subsequence of `seq2`. That is, whether # or not `seq2` contains every element in `seq1` in the same order (and # possibly more elements besides). # # @param seq1 [Array] # @param seq2 [Array] # @return [Boolean] def subsequence?(seq1, seq2) i = j = 0 loop do return true if i == seq1.size return false if j == seq2.size i += 1 if seq1[i] == seq2[j] j += 1 end end # Returns information about the caller of the previous method. # # @param entry [String] An entry in the `#caller` list, or a similarly formatted string # @return [[String, Integer, (String, nil)]] # An array containing the filename, line, and method name of the caller. # The method name may be nil def caller_info(entry = nil) # JRuby evaluates `caller` incorrectly when it's in an actual default argument. entry ||= caller[1] info = entry.scan(/^((?:[A-Za-z]:)?.*?):(-?.*?)(?::.*`(.+)')?$/).first info[1] = info[1].to_i # This is added by Rubinius to designate a block, but we don't care about it. info[2].sub!(/ \{\}\Z/, '') if info[2] info end # Returns whether one version string represents a more recent version than another. # # @param v1 [String] A version string. # @param v2 [String] Another version string. # @return [Boolean] def version_gt(v1, v2) # Construct an array to make sure the shorter version is padded with nil Array.new([v1.length, v2.length].max).zip(v1.split("."), v2.split(".")) do |_, p1, p2| p1 ||= "0" p2 ||= "0" release1 = p1 =~ /^[0-9]+$/ release2 = p2 =~ /^[0-9]+$/ if release1 && release2 # Integer comparison if both are full releases p1, p2 = p1.to_i, p2.to_i next if p1 == p2 return p1 > p2 elsif !release1 && !release2 # String comparison if both are prereleases next if p1 == p2 return p1 > p2 else # If only one is a release, that one is newer return release1 end end end # Returns whether one version string represents the same or a more # recent version than another. # # @param v1 [String] A version string. # @param v2 [String] Another version string. # @return [Boolean] def version_geq(v1, v2) version_gt(v1, v2) || !version_gt(v2, v1) end # Throws a NotImplementedError for an abstract method. # # @param obj [Object] `self` # @raise [NotImplementedError] def abstract(obj) raise NotImplementedError.new("#{obj.class} must implement ##{caller_info[2]}") end # Prints a deprecation warning for the caller method. # # @param obj [Object] `self` # @param message [String] A message describing what to do instead. def deprecated(obj, message = nil) obj_class = obj.is_a?(Class) ? "#{obj}." : "#{obj.class}#" full_message = "DEPRECATION WARNING: #{obj_class}#{caller_info[2]} " + "will be removed in a future version of Sass.#{("\n" + message) if message}" Sass::Util.sass_warn full_message end # Silences all Sass warnings within a block. # # @yield A block in which no Sass warnings will be printed def silence_sass_warnings old_level, Sass.logger.log_level = Sass.logger.log_level, :error yield ensure Sass.logger.log_level = old_level end # The same as `Kernel#warn`, but is silenced by \{#silence\_sass\_warnings}. # # @param msg [String] def sass_warn(msg) Sass.logger.warn("#{msg}\n") end ## Cross Rails Version Compatibility # Returns the root of the Rails application, # if this is running in a Rails context. # Returns `nil` if no such root is defined. # # @return [String, nil] def rails_root if defined?(::Rails.root) return ::Rails.root.to_s if ::Rails.root raise "ERROR: Rails.root is nil!" end return RAILS_ROOT.to_s if defined?(RAILS_ROOT) nil end # Returns the environment of the Rails application, # if this is running in a Rails context. # Returns `nil` if no such environment is defined. # # @return [String, nil] def rails_env return ::Rails.env.to_s if defined?(::Rails.env) return RAILS_ENV.to_s if defined?(RAILS_ENV) nil end # Returns whether this environment is using ActionPack # version 3.0.0 or greater. # # @return [Boolean] def ap_geq_3? ap_geq?("3.0.0.beta1") end # Returns whether this environment is using ActionPack # of a version greater than or equal to that specified. # # @param version [String] The string version number to check against. # Should be greater than or equal to Rails 3, # because otherwise ActionPack::VERSION isn't autoloaded # @return [Boolean] def ap_geq?(version) # The ActionPack module is always loaded automatically in Rails >= 3 return false unless defined?(ActionPack) && defined?(ActionPack::VERSION) && defined?(ActionPack::VERSION::STRING) version_geq(ActionPack::VERSION::STRING, version) end # Returns an ActionView::Template* class. # In pre-3.0 versions of Rails, most of these classes # were of the form `ActionView::TemplateFoo`, # while afterwards they were of the form `ActionView;:Template::Foo`. # # @param name [#to_s] The name of the class to get. # For example, `:Error` will return `ActionView::TemplateError` # or `ActionView::Template::Error`. def av_template_class(name) return ActionView.const_get("Template#{name}") if ActionView.const_defined?("Template#{name}") ActionView::Template.const_get(name.to_s) end ## Cross-OS Compatibility # # These methods are cached because some of them are called quite frequently # and even basic checks like String#== are too costly to be called repeatedly. # Whether or not this is running on Windows. # # @return [Boolean] def windows? return @windows if defined?(@windows) @windows = (RbConfig::CONFIG['host_os'] =~ /mswin|windows|mingw/i) end # Whether or not this is running on IronRuby. # # @return [Boolean] def ironruby? return @ironruby if defined?(@ironruby) @ironruby = RUBY_ENGINE == "ironruby" end # Whether or not this is running on Rubinius. # # @return [Boolean] def rbx? return @rbx if defined?(@rbx) @rbx = RUBY_ENGINE == "rbx" end # Whether or not this is running on JRuby. # # @return [Boolean] def jruby? return @jruby if defined?(@jruby) @jruby = RUBY_PLATFORM =~ /java/ end # Returns an array of ints representing the JRuby version number. # # @return [Array] def jruby_version @jruby_version ||= ::JRUBY_VERSION.split(".").map {|s| s.to_i} end # Like `Dir.glob`, but works with backslash-separated paths on Windows. # # @param path [String] def glob(path) path = path.tr('\\', '/') if windows? if block_given? Dir.glob(path) {|f| yield(f)} else Dir.glob(path) end end # Like `Pathname.new`, but normalizes Windows paths to always use backslash # separators. # # `Pathname#relative_path_from` can break if the two pathnames aren't # consistent in their slash style. # # @param path [String] # @return [Pathname] def pathname(path) path = path.tr("/", "\\") if windows? Pathname.new(path) end # Like `Pathname#cleanpath`, but normalizes Windows paths to always use # backslash separators. Normally, `Pathname#cleanpath` actually does the # reverse -- it will convert backslashes to forward slashes, which can break # `Pathname#relative_path_from`. # # @param path [String, Pathname] # @return [Pathname] def cleanpath(path) path = Pathname.new(path) unless path.is_a?(Pathname) pathname(path.cleanpath.to_s) end # Returns `path` with all symlinks resolved. # # @param path [String, Pathname] # @return [Pathname] def realpath(path) path = Pathname.new(path) unless path.is_a?(Pathname) # Explicitly DON'T run #pathname here. We don't want to convert # to Windows directory separators because we're comparing these # against the paths returned by Listen, which use forward # slashes everywhere. begin path.realpath rescue SystemCallError # If [path] doesn't actually exist, don't bail, just # return the original. path end end # Returns `path` relative to `from`. # # This is like `Pathname#relative_path_from` except it accepts both strings # and pathnames, it handles Windows path separators correctly, and it throws # an error rather than crashing if the paths use different encodings # (https://github.com/ruby/ruby/pull/713). # # @param path [String, Pathname] # @param from [String, Pathname] # @return [Pathname?] def relative_path_from(path, from) pathname(path.to_s).relative_path_from(pathname(from.to_s)) rescue NoMethodError => e raise e unless e.name == :zero? # Work around https://github.com/ruby/ruby/pull/713. path = path.to_s from = from.to_s raise ArgumentError("Incompatible path encodings: #{path.inspect} is #{path.encoding}, " + "#{from.inspect} is #{from.encoding}") end # Converts `path` to a "file:" URI. This handles Windows paths correctly. # # @param path [String, Pathname] # @return [String] def file_uri_from_path(path) path = path.to_s if path.is_a?(Pathname) path = path.tr('\\', '/') if windows? path = URI::DEFAULT_PARSER.escape(path) return path.start_with?('/') ? "file://" + path : path unless windows? return "file:///" + path.tr("\\", "/") if path =~ %r{^[a-zA-Z]:[/\\]} return "file:" + path.tr("\\", "/") if path =~ %r{\\\\[^\\]+\\[^\\/]+} path.tr("\\", "/") end # Retries a filesystem operation if it fails on Windows. Windows # has weird and flaky locking rules that can cause operations to fail. # # @yield [] The filesystem operation. def retry_on_windows return yield unless windows? begin yield rescue SystemCallError sleep 0.1 yield end end # Prepare a value for a destructuring assignment (e.g. `a, b = # val`). This works around a performance bug when using # ActiveSupport, and only needs to be called when `val` is likely # to be `nil` reasonably often. # # See [this bug report](http://redmine.ruby-lang.org/issues/4917). # # @param val [Object] # @return [Object] def destructure(val) val || [] end CHARSET_REGEXP = /\A@charset "([^"]+)"/ bom = "\uFEFF" UTF_8_BOM = bom.encode("UTF-8").force_encoding('BINARY') UTF_16BE_BOM = bom.encode("UTF-16BE").force_encoding('BINARY') UTF_16LE_BOM = bom.encode("UTF-16LE").force_encoding('BINARY') ## Cross-Ruby-Version Compatibility # Whether or not this is running under Ruby 2.4 or higher. # # @return [Boolean] def ruby2_4? return @ruby2_4 if defined?(@ruby2_4) @ruby2_4 = if RUBY_VERSION_COMPONENTS[0] == 2 RUBY_VERSION_COMPONENTS[1] >= 4 else RUBY_VERSION_COMPONENTS[0] > 2 end end # Like {\#check\_encoding}, but also checks for a `@charset` declaration # at the beginning of the file and uses that encoding if it exists. # # Sass follows CSS's decoding rules. # # @param str [String] The string of which to check the encoding # @return [(String, Encoding)] The original string encoded as UTF-8, # and the source encoding of the string # @raise [Encoding::UndefinedConversionError] if the source encoding # cannot be converted to UTF-8 # @raise [ArgumentError] if the document uses an unknown encoding with `@charset` # @raise [Sass::SyntaxError] If the document declares an encoding that # doesn't match its contents, or it doesn't declare an encoding and its # contents are invalid in the native encoding. def check_sass_encoding(str) # Determine the fallback encoding following section 3.2 of CSS Syntax Level 3 and Encodings: # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#determine-the-fallback-encoding # http://encoding.spec.whatwg.org/#decode binary = str.dup.force_encoding("BINARY") if binary.start_with?(UTF_8_BOM) binary.slice! 0, UTF_8_BOM.length str = binary.force_encoding('UTF-8') elsif binary.start_with?(UTF_16BE_BOM) binary.slice! 0, UTF_16BE_BOM.length str = binary.force_encoding('UTF-16BE') elsif binary.start_with?(UTF_16LE_BOM) binary.slice! 0, UTF_16LE_BOM.length str = binary.force_encoding('UTF-16LE') elsif binary =~ CHARSET_REGEXP charset = $1.force_encoding('US-ASCII') encoding = Encoding.find(charset) if encoding.name == 'UTF-16' || encoding.name == 'UTF-16BE' encoding = Encoding.find('UTF-8') end str = binary.force_encoding(encoding) elsif str.encoding.name == "ASCII-8BIT" # Normally we want to fall back on believing the Ruby string # encoding, but if that's just binary we want to make sure # it's valid UTF-8. str = str.force_encoding('utf-8') end find_encoding_error(str) unless str.valid_encoding? begin # If the string is valid, preprocess it according to section 3.3 of CSS Syntax Level 3. return str.encode("UTF-8").gsub(/\r\n?|\f/, "\n").tr("\u0000", "�"), str.encoding rescue EncodingError find_encoding_error(str) end end # Destructively removes all elements from an array that match a block, and # returns the removed elements. # # @param array [Array] The array from which to remove elements. # @yield [el] Called for each element. # @yieldparam el [*] The element to test. # @yieldreturn [Boolean] Whether or not to extract the element. # @return [Array] The extracted elements. def extract!(array) out = [] array.reject! do |e| next false unless yield e out << e true end out end # Flattens the first level of nested arrays in `arrs`. Unlike # `Array#flatten`, this orders the result by taking the first # values from each array in order, then the second, and so on. # # @param arrs [Array] The array to flatten. # @return [Array] The flattened array. def flatten_vertically(arrs) result = [] arrs = arrs.map {|sub| sub.is_a?(Array) ? sub.dup : Array(sub)} until arrs.empty? arrs.reject! do |arr| result << arr.shift arr.empty? end end result end # Like `Object#inspect`, but preserves non-ASCII characters rather than # escaping them under Ruby 1.9.2. This is necessary so that the # precompiled Haml template can be `#encode`d into `@options[:encoding]` # before being evaluated. # # @param obj {Object} # @return {String} def inspect_obj(obj) return obj.inspect unless version_geq(RUBY_VERSION, "1.9.2") return ':' + inspect_obj(obj.to_s) if obj.is_a?(Symbol) return obj.inspect unless obj.is_a?(String) '"' + obj.gsub(/[\x00-\x7F]+/) {|s| s.inspect[1...-1]} + '"' end # Extracts the non-string vlaues from an array containing both strings and non-strings. # These values are replaced with escape sequences. # This can be undone using \{#inject\_values}. # # This is useful e.g. when we want to do string manipulation # on an interpolated string. # # The precise format of the resulting string is not guaranteed. # However, it is guaranteed that newlines and whitespace won't be affected. # # @param arr [Array] The array from which values are extracted. # @return [(String, Array)] The resulting string, and an array of extracted values. def extract_values(arr) values = [] mapped = arr.map do |e| next e.gsub('{', '{{') if e.is_a?(String) values << e next "{#{values.count - 1}}" end return mapped.join, values end # Undoes \{#extract\_values} by transforming a string with escape sequences # into an array of strings and non-string values. # # @param str [String] The string with escape sequences. # @param values [Array] The array of values to inject. # @return [Array] The array of strings and values. def inject_values(str, values) return [str.gsub('{{', '{')] if values.empty? # Add an extra { so that we process the tail end of the string result = (str + '{{').scan(/(.*?)(?:(\{\{)|\{(\d+)\})/m).map do |(pre, esc, n)| [pre, esc ? '{' : '', n ? values[n.to_i] : ''] end.flatten(1) result[-2] = '' # Get rid of the extra { merge_adjacent_strings(result).reject {|s| s == ''} end # Allows modifications to be performed on the string form # of an array containing both strings and non-strings. # # @param arr [Array] The array from which values are extracted. # @yield [str] A block in which string manipulation can be done to the array. # @yieldparam str [String] The string form of `arr`. # @yieldreturn [String] The modified string. # @return [Array] The modified, interpolated array. def with_extracted_values(arr) str, vals = extract_values(arr) str = yield str inject_values(str, vals) end # Builds a sourcemap file name given the generated CSS file name. # # @param css [String] The generated CSS file name. # @return [String] The source map file name. def sourcemap_name(css) css + ".map" end # Escapes certain characters so that the result can be used # as the JSON string value. Returns the original string if # no escaping is necessary. # # @param s [String] The string to be escaped # @return [String] The escaped string def json_escape_string(s) return s if s !~ /["\\\b\f\n\r\t]/ result = "" s.split("").each do |c| case c when '"', "\\" result << "\\" << c when "\n" then result << "\\n" when "\t" then result << "\\t" when "\r" then result << "\\r" when "\f" then result << "\\f" when "\b" then result << "\\b" else result << c end end result end # Converts the argument into a valid JSON value. # # @param v [Integer, String, Array, Boolean, nil] # @return [String] def json_value_of(v) case v when Integer v.to_s when String "\"" + json_escape_string(v) + "\"" when Array "[" + v.map {|x| json_value_of(x)}.join(",") + "]" when NilClass "null" when TrueClass "true" when FalseClass "false" else raise ArgumentError.new("Unknown type: #{v.class.name}") end end VLQ_BASE_SHIFT = 5 VLQ_BASE = 1 << VLQ_BASE_SHIFT VLQ_BASE_MASK = VLQ_BASE - 1 VLQ_CONTINUATION_BIT = VLQ_BASE BASE64_DIGITS = ('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a + ['+', '/'] BASE64_DIGIT_MAP = begin map = {} BASE64_DIGITS.each_with_index.map do |digit, i| map[digit] = i end map end # Encodes `value` as VLQ (http://en.wikipedia.org/wiki/VLQ). # # @param value [Integer] # @return [String] The encoded value def encode_vlq(value) if value < 0 value = ((-value) << 1) | 1 else value <<= 1 end result = '' begin digit = value & VLQ_BASE_MASK value >>= VLQ_BASE_SHIFT if value > 0 digit |= VLQ_CONTINUATION_BIT end result << BASE64_DIGITS[digit] end while value > 0 result end ## Static Method Stuff # The context in which the ERB for \{#def\_static\_method} will be run. class StaticConditionalContext # @param set [#include?] The set of variables that are defined for this context. def initialize(set) @set = set end # Checks whether or not a variable is defined for this context. # # @param name [Symbol] The name of the variable # @return [Boolean] def method_missing(name, *args) super unless args.empty? && !block_given? @set.include?(name) end end # @private ATOMIC_WRITE_MUTEX = Mutex.new # This creates a temp file and yields it for writing. When the # write is complete, the file is moved into the desired location. # The atomicity of this operation is provided by the filesystem's # rename operation. # # @param filename [String] The file to write to. # @param perms [Integer] The permissions used for creating this file. # Will be masked by the process umask. Defaults to readable/writeable # by all users however the umask usually changes this to only be writable # by the process's user. # @yieldparam tmpfile [Tempfile] The temp file that can be written to. # @return The value returned by the block. def atomic_create_and_write_file(filename, perms = 0666) require 'tempfile' tmpfile = Tempfile.new(File.basename(filename), File.dirname(filename)) tmpfile.binmode if tmpfile.respond_to?(:binmode) result = yield tmpfile tmpfile.close ATOMIC_WRITE_MUTEX.synchronize do begin File.chmod(perms & ~File.umask, tmpfile.path) rescue Errno::EPERM # If we don't have permissions to chmod the file, don't let that crash # the compilation. See issue 1215. end File.rename tmpfile.path, filename end result ensure # close and remove the tempfile if it still exists, # presumably due to an error during write tmpfile.close if tmpfile tmpfile.unlink if tmpfile end private def find_encoding_error(str) encoding = str.encoding cr = Regexp.quote("\r".encode(encoding).force_encoding('BINARY')) lf = Regexp.quote("\n".encode(encoding).force_encoding('BINARY')) ff = Regexp.quote("\f".encode(encoding).force_encoding('BINARY')) line_break = /#{cr}#{lf}?|#{ff}|#{lf}/ str.force_encoding("binary").split(line_break).each_with_index do |line, i| begin line.encode(encoding) rescue Encoding::UndefinedConversionError => e raise Sass::SyntaxError.new( "Invalid #{encoding.name} character #{undefined_conversion_error_char(e)}", :line => i + 1) end end # We shouldn't get here, but it's possible some weird encoding stuff causes it. return str, str.encoding end # Calculates the memoization table for the Least Common Subsequence algorithm. # Algorithm from [Wikipedia](http://en.wikipedia.org/wiki/Longest_common_subsequence_problem#Computing_the_length_of_the_LCS) def lcs_table(x, y) # This method does not take a block as an explicit parameter for performance reasons. c = Array.new(x.size) {[]} x.size.times {|i| c[i][0] = 0} y.size.times {|j| c[0][j] = 0} (1...x.size).each do |i| (1...y.size).each do |j| c[i][j] = if yield x[i], y[j] c[i - 1][j - 1] + 1 else [c[i][j - 1], c[i - 1][j]].max end end end c end # Computes a single longest common subsequence for arrays x and y. # Algorithm from [Wikipedia](http://en.wikipedia.org/wiki/Longest_common_subsequence_problem#Reading_out_an_LCS) def lcs_backtrace(c, x, y, i, j, &block) return [] if i == 0 || j == 0 if (v = yield(x[i], y[j])) return lcs_backtrace(c, x, y, i - 1, j - 1, &block) << v end return lcs_backtrace(c, x, y, i, j - 1, &block) if c[i][j - 1] > c[i - 1][j] lcs_backtrace(c, x, y, i - 1, j, &block) end singleton_methods.each {|method| module_function method} end end require 'sass/util/multibyte_string_scanner' require 'sass/util/normalized_map' ruby-sass-3.7.4/lib/sass/util/000077500000000000000000000000001345125207600161565ustar00rootroot00000000000000ruby-sass-3.7.4/lib/sass/util/multibyte_string_scanner.rb000066400000000000000000000077051345125207600236310ustar00rootroot00000000000000require 'strscan' if Sass::Util.rbx? # Rubinius's StringScanner class implements some of its methods in terms of # others, which causes us to double-count bytes in some cases if we do # straightforward inheritance. To work around this, we use a delegate class. require 'delegate' class Sass::Util::MultibyteStringScanner < DelegateClass(StringScanner) def initialize(str) super(StringScanner.new(str)) @mb_pos = 0 @mb_matched_size = nil @mb_last_pos = nil end def is_a?(klass) __getobj__.is_a?(klass) || super end end else class Sass::Util::MultibyteStringScanner < StringScanner def initialize(str) super @mb_pos = 0 @mb_matched_size = nil @mb_last_pos = nil end end end # A wrapper of the native StringScanner class that works correctly with # multibyte character encodings. The native class deals only in bytes, not # characters, for methods like [#pos] and [#matched_size]. This class deals # only in characters, instead. class Sass::Util::MultibyteStringScanner def self.new(str) return StringScanner.new(str) if str.ascii_only? super end alias_method :byte_pos, :pos alias_method :byte_matched_size, :matched_size def check(pattern); _match super; end def check_until(pattern); _matched super; end def getch; _forward _match super; end def match?(pattern); _size check(pattern); end def matched_size; @mb_matched_size; end def peek(len); string[@mb_pos, len]; end alias_method :peep, :peek def pos; @mb_pos; end alias_method :pointer, :pos def rest_size; rest.size; end def scan(pattern); _forward _match super; end def scan_until(pattern); _forward _matched super; end def skip(pattern); _size scan(pattern); end def skip_until(pattern); _matched _size scan_until(pattern); end def get_byte raise "MultibyteStringScanner doesn't support #get_byte." end def getbyte raise "MultibyteStringScanner doesn't support #getbyte." end def pos=(n) @mb_last_pos = nil # We set position kind of a lot during parsing, so we want it to be as # efficient as possible. This is complicated by the fact that UTF-8 is a # variable-length encoding, so it's difficult to find the byte length that # corresponds to a given character length. # # Our heuristic here is to try to count the fewest possible characters. So # if the new position is close to the current one, just count the # characters between the two; if the new position is closer to the # beginning of the string, just count the characters from there. if @mb_pos - n < @mb_pos / 2 # New position is close to old position byte_delta = @mb_pos > n ? -string[n...@mb_pos].bytesize : string[@mb_pos...n].bytesize super(byte_pos + byte_delta) else # New position is close to BOS super(string[0...n].bytesize) end @mb_pos = n end def reset @mb_pos = 0 @mb_matched_size = nil @mb_last_pos = nil super end def scan_full(pattern, advance_pointer_p, return_string_p) res = _match super(pattern, advance_pointer_p, true) _forward res if advance_pointer_p return res if return_string_p end def search_full(pattern, advance_pointer_p, return_string_p) res = super(pattern, advance_pointer_p, true) _forward res if advance_pointer_p _matched((res if return_string_p)) end def string=(str) @mb_pos = 0 @mb_matched_size = nil @mb_last_pos = nil super end def terminate @mb_pos = string.size @mb_matched_size = nil @mb_last_pos = nil super end alias_method :clear, :terminate def unscan super @mb_pos = @mb_last_pos @mb_last_pos = @mb_matched_size = nil end private def _size(str) str && str.size end def _match(str) @mb_matched_size = str && str.size str end def _matched(res) _match matched res end def _forward(str) @mb_last_pos = @mb_pos @mb_pos += str.size if str str end end ruby-sass-3.7.4/lib/sass/util/normalized_map.rb000066400000000000000000000051221345125207600215040ustar00rootroot00000000000000require 'delegate' module Sass module Util # A hash that normalizes its string keys while still allowing you to get back # to the original keys that were stored. If several different values normalize # to the same value, whichever is stored last wins. class NormalizedMap # Create a normalized map def initialize(map = nil) @key_strings = {} @map = {} map.each {|key, value| self[key] = value} if map end # Specifies how to transform the key. # # This can be overridden to create other normalization behaviors. def normalize(key) key.tr("-", "_") end # Returns the version of `key` as it was stored before # normalization. If `key` isn't in the map, returns it as it was # passed in. # # @return [String] def denormalize(key) @key_strings[normalize(key)] || key end # @private def []=(k, v) normalized = normalize(k) @map[normalized] = v @key_strings[normalized] = k v end # @private def [](k) @map[normalize(k)] end # @private def has_key?(k) @map.has_key?(normalize(k)) end # @private def delete(k) normalized = normalize(k) @key_strings.delete(normalized) @map.delete(normalized) end # @return [Hash] Hash with the keys as they were stored (before normalization). def as_stored Sass::Util.map_keys(@map) {|k| @key_strings[k]} end def empty? @map.empty? end def values @map.values end def keys @map.keys end def each @map.each {|k, v| yield(k, v)} end def size @map.size end def to_hash @map.dup end def to_a @map.to_a end def map @map.map {|k, v| yield(k, v)} end def dup d = super d.send(:instance_variable_set, "@map", @map.dup) d end def sort_by @map.sort_by {|k, v| yield k, v} end def update(map) map = map.as_stored if map.is_a?(NormalizedMap) map.each {|k, v| self[k] = v} end def method_missing(method, *args, &block) if Sass.tests_running raise ArgumentError.new("The method #{method} must be implemented explicitly") end @map.send(method, *args, &block) end def respond_to_missing?(method, include_private = false) @map.respond_to?(method, include_private) end end end end ruby-sass-3.7.4/lib/sass/util/subset_map.rb000066400000000000000000000067461345125207600206620ustar00rootroot00000000000000require 'set' module Sass module Util # A map from sets to values. # A value is \{#\[]= set} by providing a set (the "set-set") and a value, # which is then recorded as corresponding to that set. # Values are \{#\[] accessed} by providing a set (the "get-set") # and returning all values that correspond to set-sets # that are subsets of the get-set. # # SubsetMap preserves the order of values as they're inserted. # # @example # ssm = SubsetMap.new # ssm[Set[1, 2]] = "Foo" # ssm[Set[2, 3]] = "Bar" # ssm[Set[1, 2, 3]] = "Baz" # # ssm[Set[1, 2, 3]] #=> ["Foo", "Bar", "Baz"] class SubsetMap # Creates a new, empty SubsetMap. def initialize @hash = {} @vals = [] end # Whether or not this SubsetMap has any key-value pairs. # # @return [Boolean] def empty? @hash.empty? end # Associates a value with a set. # When `set` or any of its supersets is accessed, # `value` will be among the values returned. # # Note that if the same `set` is passed to this method multiple times, # all given `value`s will be associated with that `set`. # # This runs in `O(n)` time, where `n` is the size of `set`. # # @param set [#to_set] The set to use as the map key. May not be empty. # @param value [Object] The value to associate with `set`. # @raise [ArgumentError] If `set` is empty. def []=(set, value) raise ArgumentError.new("SubsetMap keys may not be empty.") if set.empty? index = @vals.size @vals << value set.each do |k| @hash[k] ||= [] @hash[k] << [set, set.to_set, index] end end # Returns all values associated with subsets of `set`. # # In the worst case, this runs in `O(m*max(n, log m))` time, # where `n` is the size of `set` # and `m` is the number of associations in the map. # However, unless many keys in the map overlap with `set`, # `m` will typically be much smaller. # # @param set [Set] The set to use as the map key. # @return [Array<(Object, #to_set)>] An array of pairs, # where the first value is the value associated with a subset of `set`, # and the second value is that subset of `set` # (or whatever `#to_set` object was used to set the value) # This array is in insertion order. # @see #[] def get(set) res = set.map do |k| subsets = @hash[k] next unless subsets subsets.map do |subenum, subset, index| next unless subset.subset?(set) [index, subenum] end end.flatten(1) res.compact! res.uniq! res.sort! res.map! {|i, s| [@vals[i], s]} res end # Same as \{#get}, but doesn't return the subsets of the argument # for which values were found. # # @param set [Set] The set to use as the map key. # @return [Array] The array of all values # associated with subsets of `set`, in insertion order. # @see #get def [](set) get(set).map {|v, _| v} end # Iterates over each value in the subset map. Ignores keys completely. If # multiple keys have the same value, this will return them multiple times. # # @yield [Object] Each value in the map. def each_value @vals.each {|v| yield v} end end end end ruby-sass-3.7.4/lib/sass/util/test.rb000066400000000000000000000002161345125207600174610ustar00rootroot00000000000000module Sass module Util module Test def skip(msg = nil, bt = caller) super if defined?(super) end end end end ruby-sass-3.7.4/lib/sass/version.rb000066400000000000000000000076351345125207600172260ustar00rootroot00000000000000require 'date' require 'sass/util' module Sass # Handles Sass version-reporting. # Sass not only reports the standard three version numbers, # but its Git revision hash as well, # if it was installed from Git. module Version # Returns a hash representing the version of Sass. # The `:major`, `:minor`, and `:teeny` keys have their respective numbers as Integers. # The `:name` key has the name of the version. # The `:string` key contains a human-readable string representation of the version. # The `:number` key is the major, minor, and teeny keys separated by periods. # The `:date` key, which is not guaranteed to be defined, is the `DateTime` # at which this release was cut. # If Sass is checked out from Git, the `:rev` key will have the revision hash. # For example: # # { # :string => "2.1.0.9616393", # :rev => "9616393b8924ef36639c7e82aa88a51a24d16949", # :number => "2.1.0", # :date => DateTime.parse("Apr 30 13:52:01 2009 -0700"), # :major => 2, :minor => 1, :teeny => 0 # } # # If a prerelease version of Sass is being used, # the `:string` and `:number` fields will reflect the full version # (e.g. `"2.2.beta.1"`), and the `:teeny` field will be `-1`. # A `:prerelease` key will contain the name of the prerelease (e.g. `"beta"`), # and a `:prerelease_number` key will contain the rerelease number. # For example: # # { # :string => "3.0.beta.1", # :number => "3.0.beta.1", # :date => DateTime.parse("Mar 31 00:38:04 2010 -0700"), # :major => 3, :minor => 0, :teeny => -1, # :prerelease => "beta", # :prerelease_number => 1 # } # # @return [{Symbol => String/Integer}] The version hash def version return @@version if defined?(@@version) numbers = File.read(Sass::Util.scope('VERSION')).strip.split('.'). map {|n| n =~ /^[0-9]+$/ ? n.to_i : n} name = File.read(Sass::Util.scope('VERSION_NAME')).strip @@version = { :major => numbers[0], :minor => numbers[1], :teeny => numbers[2], :name => name } if (date = version_date) @@version[:date] = date end if numbers[3].is_a?(String) @@version[:teeny] = -1 @@version[:prerelease] = numbers[3] @@version[:prerelease_number] = numbers[4] end @@version[:number] = numbers.join('.') @@version[:string] = @@version[:number].dup if (rev = revision_number) @@version[:rev] = rev unless rev[0] == ?( @@version[:string] << "." << rev[0...7] end end @@version end private def revision_number if File.exist?(Sass::Util.scope('REVISION')) rev = File.read(Sass::Util.scope('REVISION')).strip return rev unless rev =~ /^([a-f0-9]+|\(.*\))$/ || rev == '(unknown)' end return unless File.exist?(Sass::Util.scope('.git/HEAD')) rev = File.read(Sass::Util.scope('.git/HEAD')).strip return rev unless rev =~ /^ref: (.*)$/ ref_name = $1 ref_file = Sass::Util.scope(".git/#{ref_name}") info_file = Sass::Util.scope(".git/info/refs") return File.read(ref_file).strip if File.exist?(ref_file) return unless File.exist?(info_file) File.open(info_file) do |f| f.each do |l| sha, ref = l.strip.split("\t", 2) next unless ref == ref_name return sha end end nil end def version_date return unless File.exist?(Sass::Util.scope('VERSION_DATE')) DateTime.parse(File.read(Sass::Util.scope('VERSION_DATE')).strip) end end extend Sass::Version # A string representing the version of Sass. # A more fine-grained representation is available from Sass.version. # @api public VERSION = version[:string] unless defined?(Sass::VERSION) end ruby-sass-3.7.4/rails/000077500000000000000000000000001345125207600145745ustar00rootroot00000000000000ruby-sass-3.7.4/rails/init.rb000066400000000000000000000000771345125207600160700ustar00rootroot00000000000000Kernel.load File.join(File.dirname(__FILE__), '..', 'init.rb') ruby-sass-3.7.4/sass.gemspec000066400000000000000000000043331345125207600160030ustar00rootroot00000000000000require 'rubygems' # Note that Sass's gem-compilation process requires access to the filesystem. # This means that it cannot be automatically run by e.g. GitHub's gem system. # However, a build server automatically packages the master branch # every time it's pushed to; this is made available as a prerelease gem. SASS_GEMSPEC = Gem::Specification.new do |spec| spec.rubyforge_project = 'sass' spec.name = 'sass' spec.summary = "A powerful but elegant CSS compiler that makes CSS fun again." spec.version = File.read(File.dirname(__FILE__) + '/VERSION').strip spec.authors = ['Natalie Weizenbaum', 'Chris Eppstein', 'Hampton Catlin'] spec.email = 'sass-lang@googlegroups.com' spec.description = <<-END Ruby Sass is deprecated! See https://sass-lang.com/ruby-sass for details. Sass makes CSS fun again. Sass is an extension of CSS, adding nested rules, variables, mixins, selector inheritance, and more. It's translated to well-formatted, standard CSS using the command line tool or a web-framework plugin. END spec.required_ruby_version = '>= 2.0.0' spec.add_runtime_dependency 'sass-listen', '~> 4.0.0' spec.add_development_dependency 'yard', '~> 0.8.7.6' spec.add_development_dependency 'redcarpet', '~> 3.3' spec.add_development_dependency 'nokogiri', '~> 1.6.0' spec.add_development_dependency 'minitest', '>= 5' readmes = Dir['*'].reject{ |x| x =~ /(^|[^.a-z])[a-z]+/ || x == "TODO" } spec.executables = ['sass', 'sass-convert', 'scss'] spec.files = Dir['rails/init.rb', '{lib,bin,extra}/**/*', 'init.rb', '.yardopts'] + readmes spec.homepage = 'https://sass-lang.com/' spec.license = "MIT" if spec.respond_to?(:metadata) spec.metadata['source_code_uri'] = 'https://github.com/sass/ruby-sass' end spec.post_install_message = < :bar} cache.store("an_object", "", an_object) assert_equal an_object, cache.retrieve("an_object", "") end def test_cache_node_with_unmarshalable_option engine_with_unmarshalable_options("foo {a: b + c}").to_tree end # Regression tests def test_cache_mixin_def_splat_sass_node_with_unmarshalable_option engine_with_unmarshalable_options(< :sass).to_tree =color($args...) color: red SASS end def test_cache_mixin_def_splat_scss_node_with_unmarshalable_option engine_with_unmarshalable_options(< :scss).to_tree @mixin color($args...) { color: red; } SCSS end def test_cache_function_splat_sass_node_with_unmarshalable_option engine_with_unmarshalable_options(< :sass).to_tree @function color($args...) @return red SASS end def test_cache_function_splat_scss_node_with_unmarshalable_option engine_with_unmarshalable_options(< :scss).to_tree @function color($args...) { @return red; } SCSS end def test_cache_include_splat_sass_node_with_unmarshalable_option engine_with_unmarshalable_options(< :sass).to_tree @include color($args..., $kwargs...) SASS end def test_cache_include_splat_scss_node_with_unmarshalable_option engine_with_unmarshalable_options(< :scss).to_tree @include color($args..., $kwargs...); SCSS end private def root_node Sass::Engine.new(<<-SCSS, :syntax => :scss).to_tree @mixin color($c) { color: $c} div { @include color(red); } SCSS end def engine_with_unmarshalable_options(src, options={}) Sass::Engine.new(src, { :syntax => :scss, :object => Class.new.new, :filename => 'file.scss', :importer => Sass::Importers::Filesystem.new(absolutize('templates')) }.merge(options)) end end ruby-sass-3.7.4/test/sass/callbacks_test.rb000077500000000000000000000021111345125207600207130ustar00rootroot00000000000000require File.dirname(__FILE__) + '/../test_helper' require 'sass/callbacks' class CallerBack extend Sass::Callbacks define_callback :foo define_callback :bar def do_foo run_foo end def do_bar run_bar 12 end end module ClassLevelCallerBack extend Sass::Callbacks define_callback :foo extend self def do_foo run_foo end end class SassCallbacksTest < MiniTest::Test def test_simple_callback cb = CallerBack.new there = false cb.on_foo {there = true} cb.do_foo assert there, "Expected callback to be called." end def test_multiple_callbacks cb = CallerBack.new str = "" cb.on_foo {str += "first"} cb.on_foo {str += " second"} cb.do_foo assert_equal "first second", str end def test_callback_with_arg cb = CallerBack.new val = nil cb.on_bar {|a| val = a} cb.do_bar assert_equal 12, val end def test_class_level_callback there = false ClassLevelCallerBack.on_foo {there = true} ClassLevelCallerBack.do_foo assert there, "Expected callback to be called." end end ruby-sass-3.7.4/test/sass/compiler_test.rb000077500000000000000000000127001345125207600206130ustar00rootroot00000000000000require 'minitest/autorun' require File.dirname(__FILE__) + '/../test_helper' require 'sass/plugin' require 'sass/plugin/compiler' class CompilerTest < MiniTest::Test class FakeListener attr_accessor :options attr_accessor :directories attr_reader :start_called attr_reader :thread def initialize(*args, &on_filesystem_event) self.options = args.last.is_a?(Hash) ? args.pop : {} self.directories = args @on_filesystem_event = on_filesystem_event @start_called = false reset_events! end def fire_events!(*args) @on_filesystem_event.call(@modified, @added, @removed) reset_events! end def changed(filename) @modified << File.expand_path(filename) end def added(filename) @added << File.expand_path(filename) end def removed(filename) @removed << File.expand_path(filename) end def on_start!(&run_during_start) @run_during_start = run_during_start end def start! @run_during_start.call(self) if @run_during_start end def start parent = Thread.current @thread = Thread.new do @run_during_start.call(self) if @run_during_start parent.raise Interrupt end end def stop end def reset_events! @modified = [] @added = [] @removed = [] end end module MockWatcher attr_accessor :run_during_start attr_accessor :update_stylesheets_times attr_accessor :update_stylesheets_called_with attr_accessor :deleted_css_files def fake_listener @fake_listener end def update_stylesheets(individual_files) @update_stylesheets_times ||= 0 @update_stylesheets_times += 1 (@update_stylesheets_called_with ||= []) << individual_files end def try_delete_css(css_filename) (@deleted_css_files ||= []) << css_filename end private def create_listener(*args, &on_filesystem_event) args.pop if args.last.is_a?(Hash) @fake_listener = FakeListener.new(*args, &on_filesystem_event) @fake_listener.on_start!(&run_during_start) @fake_listener end end def test_initialize watcher end def test_watch_starts_the_listener start_called = false c = watcher do |listener| start_called = true end c.watch assert start_called, "start! was not called" end def test_sass_callbacks_fire_from_listener_events c = watcher do |listener| listener.changed "changed.scss" listener.added "added.scss" listener.removed "removed.scss" listener.fire_events! end modified_fired = false c.on_template_modified do |sass_file| modified_fired = true assert_equal "changed.scss", sass_file end added_fired = false c.on_template_created do |sass_file| added_fired = true assert_equal "added.scss", sass_file end removed_fired = false c.on_template_deleted do |sass_file| removed_fired = true assert_equal "removed.scss", sass_file end c.watch assert_equal 2, c.update_stylesheets_times assert modified_fired assert added_fired assert removed_fired end def test_removing_a_sass_file_removes_corresponding_css_file c = watcher do |listener| listener.removed "remove_me.scss" listener.fire_events! end c.watch assert_equal "./remove_me.css", c.deleted_css_files.first end def test_an_importer_can_watch_an_image image_importer = Sass::Importers::Filesystem.new(".") class << image_importer def watched_file?(filename) filename =~ /\.png$/ end end c = watcher(:load_paths => [image_importer]) do |listener| listener.changed "image.png" listener.fire_events! end modified_fired = false c.on_template_modified do |f| modified_fired = true assert_equal "image.png", f end c.watch assert_equal 2, c.update_stylesheets_times assert modified_fired end def test_watching_specific_files_and_one_is_deleted directories = nil c = watcher do |listener| directories = listener.directories listener.removed File.expand_path("./foo.scss") listener.fire_events! end c.watch([[File.expand_path("./foo.scss"), File.expand_path("./foo.css"), nil]]) assert directories.include?(File.expand_path(".")), directories.inspect assert_equal File.expand_path("./foo.css"), c.deleted_css_files.first, "the corresponding css file was not deleted" assert_equal [], c.update_stylesheets_called_with[1], "the sass file should not have been compiled" end def test_watched_directories_are_dedupped directories = nil c = watcher(:load_paths => [".", "./foo", "."]) do |listener| directories = listener.directories end c.watch assert_equal [File.expand_path(".")], directories end def test_a_changed_css_in_a_watched_directory_does_not_force_a_compile c = watcher do |listener| listener.changed "foo.css" listener.fire_events! end c.on_template_modified do |f| assert false, "Should not have been called" end c.watch assert_equal 1, c.update_stylesheets_times end private def default_options {:template_location => [[".","."]]} end def watcher(options = {}, &run_during_start) options = default_options.merge(options) watcher = Sass::Plugin::Compiler.new(options) watcher.extend(MockWatcher) watcher.run_during_start = run_during_start watcher end end ruby-sass-3.7.4/test/sass/conversion_test.rb000077500000000000000000000647561345125207600212100ustar00rootroot00000000000000require File.dirname(__FILE__) + '/../test_helper' class ConversionTest < MiniTest::Test def test_basic assert_converts < E'] assert_selector_renders['+ E'] assert_selector_renders['~ E'] assert_selector_renders['> > E'] assert_selector_renders['E*'] assert_selector_renders['E*.foo'] assert_selector_renders['E*:hover'] end def test_disallowed_colon_hack assert_raise_message(Sass::SyntaxError, 'The ":name: val" hack is not allowed in the Sass indented syntax') do to_sass("foo {:name: val;}", :syntax => :scss) end end def test_nested_properties assert_converts < true))} h1 :color red SASS end def test_nesting assert_equal(< b SASS a {color: red} a > b {} CSS end def test_nesting_within_media assert_equal(< .bar a: b .baz c: d SASS .foo>.bar {a: b} .foo>.baz {c: d} CSS assert_equal(< .baz c: d SASS .bar > .baz {c: d} CSS end # Error reporting def test_error_reporting css2sass("foo") assert(false, "Expected exception") rescue Sass::SyntaxError => err assert_equal(1, err.sass_line) assert_equal('Invalid CSS after "foo": expected "{", was ""', err.message) end def test_error_reporting_in_line css2sass("foo\nbar }\nbaz") assert(false, "Expected exception") rescue Sass::SyntaxError => err assert_equal(2, err.sass_line) assert_equal('Invalid CSS after "bar ": expected "{", was "}"', err.message) end def test_error_truncate_after css2sass("#{"a" * 16}foo") assert(false, "Expected exception") rescue Sass::SyntaxError => err assert_equal(1, err.sass_line) assert_equal('Invalid CSS after "...aaaaaaaaaaaafoo": expected "{", was ""', err.message) end def test_error_truncate_was css2sass("foo }foo#{"a" * 15}") assert(false, "Expected exception") rescue Sass::SyntaxError => err assert_equal(1, err.sass_line) assert_equal('Invalid CSS after "foo ": expected "{", was "}fooaaaaaaaaaaa..."', err.message) end def test_error_doesnt_truncate_after_when_elipsis_would_add_length css2sass("#{"a" * 15}foo") assert(false, "Expected exception") rescue Sass::SyntaxError => err assert_equal(1, err.sass_line) assert_equal('Invalid CSS after "aaaaaaaaaaaaaaafoo": expected "{", was ""', err.message) end def test_error_doesnt_truncate_was_when_elipsis_would_add_length css2sass("foo }foo#{"a" * 14}") assert(false, "Expected exception") rescue Sass::SyntaxError => err assert_equal(1, err.sass_line) assert_equal('Invalid CSS after "foo ": expected "{", was "}fooaaaaaaaaaaaaaa"', err.message) end def test_error_gets_rid_of_trailing_newline_for_after css2sass("foo \n ") assert(false, "Expected exception") rescue Sass::SyntaxError => err assert_equal(2, err.sass_line) assert_equal('Invalid CSS after "foo": expected "{", was ""', err.message) end def test_error_gets_rid_of_trailing_newline_for_was css2sass("foo \n }foo") assert(false, "Expected exception") rescue Sass::SyntaxError => err assert_equal(2, err.sass_line) assert_equal('Invalid CSS after "foo": expected "{", was "}foo"', err.message) end # Encodings def test_encoding_error css2sass("foo\nbar\nb\xFEaz".force_encoding("utf-8")) assert(false, "Expected exception") rescue Sass::SyntaxError => e assert_equal(3, e.sass_line) assert_equal('Invalid UTF-8 character "\xFE"', e.message) end def test_ascii_incompatible_encoding_error template = "foo\nbar\nb_z".encode("utf-16le") template[9] = "\xFE".force_encoding("utf-16le") css2sass(template) assert(false, "Expected exception") rescue Sass::SyntaxError => e assert_equal(3, e.sass_line) assert_equal('Invalid UTF-16LE character "\xFE"', e.message) end private def css2sass(string, opts={}) Sass::CSS.new(string, opts).render end end ruby-sass-3.7.4/test/sass/css_variable_test.rb000077500000000000000000000073471345125207600214510ustar00rootroot00000000000000 require File.dirname(__FILE__) + '/../test_helper' require 'sass/engine' # Most CSS variable tests are in sass-spec, but a few relate to formatting or # conversion and so belong here. class CssVariableTest < MiniTest::Test def test_folded_inline_whitespace assert_variable_value "foo bar baz", "foo bar baz" assert_variable_value "foo bar", "foo \t bar" end def test_folded_multiline_whitespace # We don't want to reformat newlines in nested and expanded mode, so we just # remove trailing whitespace before them. assert_equal < syntax) x { --variable: #{source}; } SCSS x --variable: #{source} SASS end def render(sass, options = {}) options[:syntax] ||= :scss options[:cache] = false munge_filename options Sass::Engine.new(sass, options).render end def resolve(str, opts = {}, environment = env) munge_filename opts val = eval(str, opts, environment) assert_kind_of Sass::Script::Value::Base, val val.options = opts val.is_a?(Sass::Script::Value::String) ? val.value : val.to_s end def eval(str, opts = {}, environment = env) munge_filename opts Sass::Script.parse(str, opts.delete(:line) || 1, opts.delete(:offset) || 0, opts). perform(Sass::Environment.new(environment, opts)) end def env(hash = {}) env = Sass::Environment.new hash.each {|k, v| env.set_var(k, v)} env end end ruby-sass-3.7.4/test/sass/data/000077500000000000000000000000001345125207600163235ustar00rootroot00000000000000ruby-sass-3.7.4/test/sass/data/hsl-rgb.txt000066400000000000000000000121621345125207600204240ustar00rootroot00000000000000hsl(0, 100%, 50%) hsl(60, 100%, 50%) hsl(120, 100%, 50%) hsl(180, 100%, 50%) hsl(240, 100%, 50%) hsl(300, 100%, 50%) ==== rgb(255, 0, 0) rgb(255, 255, 0) rgb(0, 255, 0) rgb(0, 255, 255) rgb(0, 0, 255) rgb(255, 0, 255) hsl(-360, 100%, 50%) hsl(-300, 100%, 50%) hsl(-240, 100%, 50%) hsl(-180, 100%, 50%) hsl(-120, 100%, 50%) hsl(-60, 100%, 50%) ==== rgb(255, 0, 0) rgb(255, 255, 0) rgb(0, 255, 0) rgb(0, 255, 255) rgb(0, 0, 255) rgb(255, 0, 255) hsl(360, 100%, 50%) hsl(420, 100%, 50%) hsl(480, 100%, 50%) hsl(540, 100%, 50%) hsl(600, 100%, 50%) hsl(660, 100%, 50%) ==== rgb(255, 0, 0) rgb(255, 255, 0) rgb(0, 255, 0) rgb(0, 255, 255) rgb(0, 0, 255) rgb(255, 0, 255) hsl(6120, 100%, 50%) hsl(-9660, 100%, 50%) hsl(99840, 100%, 50%) hsl(-900, 100%, 50%) hsl(-104880, 100%, 50%) hsl(2820, 100%, 50%) ==== rgb(255, 0, 0) rgb(255, 255, 0) rgb(0, 255, 0) rgb(0, 255, 255) rgb(0, 0, 255) rgb(255, 0, 255) hsl(0, 100%, 50%) hsl(12, 100%, 50%) hsl(24, 100%, 50%) hsl(36, 100%, 50%) hsl(48, 100%, 50%) hsl(60, 100%, 50%) hsl(72, 100%, 50%) hsl(84, 100%, 50%) hsl(96, 100%, 50%) hsl(108, 100%, 50%) hsl(120, 100%, 50%) ==== rgb(255, 0, 0) rgb(255, 51, 0) rgb(255, 102, 0) rgb(255, 153, 0) rgb(255, 204, 0) rgb(255, 255, 0) rgb(204, 255, 0) rgb(153, 255, 0) rgb(102, 255, 0) rgb(51, 255, 0) rgb(0, 255, 0) hsl(120, 100%, 50%) hsl(132, 100%, 50%) hsl(144, 100%, 50%) hsl(156, 100%, 50%) hsl(168, 100%, 50%) hsl(180, 100%, 50%) hsl(192, 100%, 50%) hsl(204, 100%, 50%) hsl(216, 100%, 50%) hsl(228, 100%, 50%) hsl(240, 100%, 50%) ==== rgb(0, 255, 0) rgb(0, 255, 51) rgb(0, 255, 102) rgb(0, 255, 153) rgb(0, 255, 204) rgb(0, 255, 255) rgb(0, 204, 255) rgb(0, 153, 255) rgb(0, 102, 255) rgb(0, 51, 255) rgb(0, 0, 255) hsl(240, 100%, 50%) hsl(252, 100%, 50%) hsl(264, 100%, 50%) hsl(276, 100%, 50%) hsl(288, 100%, 50%) hsl(300, 100%, 50%) hsl(312, 100%, 50%) hsl(324, 100%, 50%) hsl(336, 100%, 50%) hsl(348, 100%, 50%) hsl(360, 100%, 50%) ==== rgb(0, 0, 255) rgb(51, 0, 255) rgb(102, 0, 255) rgb(153, 0, 255) rgb(204, 0, 255) rgb(255, 0, 255) rgb(255, 0, 204) rgb(255, 0, 153) rgb(255, 0, 102) rgb(255, 0, 51) rgb(255, 0, 0) hsl(0, 20%, 50%) hsl(0, 60%, 50%) hsl(0, 100%, 50%) ==== rgb(153, 102, 102) rgb(204, 51, 51) rgb(255, 0, 0) hsl(60, 20%, 50%) hsl(60, 60%, 50%) hsl(60, 100%, 50%) ==== rgb(153, 153, 102) rgb(204, 204, 51) rgb(255, 255, 0) hsl(120, 20%, 50%) hsl(120, 60%, 50%) hsl(120, 100%, 50%) ==== rgb(102, 153, 102) rgb(51, 204, 51) rgb(0, 255, 0) hsl(180, 20%, 50%) hsl(180, 60%, 50%) hsl(180, 100%, 50%) ==== rgb(102, 153, 153) rgb(51, 204, 204) rgb(0, 255, 255) hsl(240, 20%, 50%) hsl(240, 60%, 50%) hsl(240, 100%, 50%) ==== rgb(102, 102, 153) rgb(51, 51, 204) rgb(0, 0, 255) hsl(300, 20%, 50%) hsl(300, 60%, 50%) hsl(300, 100%, 50%) ==== rgb(153, 102, 153) rgb(204, 51, 204) rgb(255, 0, 255) hsl(0, 100%, 0%) hsl(0, 100%, 10%) hsl(0, 100%, 20%) hsl(0, 100%, 30%) hsl(0, 100%, 40%) hsl(0, 100%, 50%) hsl(0, 100%, 60%) hsl(0, 100%, 70%) hsl(0, 100%, 80%) hsl(0, 100%, 90%) hsl(0, 100%, 100%) ==== rgb(0, 0, 0) rgb(51, 0, 0) rgb(102, 0, 0) rgb(153, 0, 0) rgb(204, 0, 0) rgb(255, 0, 0) rgb(255, 51, 51) rgb(255, 102, 102) rgb(255, 153, 153) rgb(255, 204, 204) rgb(255, 255, 255) hsl(60, 100%, 0%) hsl(60, 100%, 10%) hsl(60, 100%, 20%) hsl(60, 100%, 30%) hsl(60, 100%, 40%) hsl(60, 100%, 50%) hsl(60, 100%, 60%) hsl(60, 100%, 70%) hsl(60, 100%, 80%) hsl(60, 100%, 90%) hsl(60, 100%, 100%) ==== rgb(0, 0, 0) rgb(51, 51, 0) rgb(102, 102, 0) rgb(153, 153, 0) rgb(204, 204, 0) rgb(255, 255, 0) rgb(255, 255, 51) rgb(255, 255, 102) rgb(255, 255, 153) rgb(255, 255, 204) rgb(255, 255, 255) hsl(120, 100%, 0%) hsl(120, 100%, 10%) hsl(120, 100%, 20%) hsl(120, 100%, 30%) hsl(120, 100%, 40%) hsl(120, 100%, 50%) hsl(120, 100%, 60%) hsl(120, 100%, 70%) hsl(120, 100%, 80%) hsl(120, 100%, 90%) hsl(120, 100%, 100%) ==== rgb(0, 0, 0) rgb(0, 51, 0) rgb(0, 102, 0) rgb(0, 153, 0) rgb(0, 204, 0) rgb(0, 255, 0) rgb(51, 255, 51) rgb(102, 255, 102) rgb(153, 255, 153) rgb(204, 255, 204) rgb(255, 255, 255) hsl(180, 100%, 0%) hsl(180, 100%, 10%) hsl(180, 100%, 20%) hsl(180, 100%, 30%) hsl(180, 100%, 40%) hsl(180, 100%, 50%) hsl(180, 100%, 60%) hsl(180, 100%, 70%) hsl(180, 100%, 80%) hsl(180, 100%, 90%) hsl(180, 100%, 100%) ==== rgb(0, 0, 0) rgb(0, 51, 51) rgb(0, 102, 102) rgb(0, 153, 153) rgb(0, 204, 204) rgb(0, 255, 255) rgb(51, 255, 255) rgb(102, 255, 255) rgb(153, 255, 255) rgb(204, 255, 255) rgb(255, 255, 255) hsl(240, 100%, 0%) hsl(240, 100%, 10%) hsl(240, 100%, 20%) hsl(240, 100%, 30%) hsl(240, 100%, 40%) hsl(240, 100%, 50%) hsl(240, 100%, 60%) hsl(240, 100%, 70%) hsl(240, 100%, 80%) hsl(240, 100%, 90%) hsl(240, 100%, 100%) ==== rgb(0, 0, 0) rgb(0, 0, 51) rgb(0, 0, 102) rgb(0, 0, 153) rgb(0, 0, 204) rgb(0, 0, 255) rgb(51, 51, 255) rgb(102, 102, 255) rgb(153, 153, 255) rgb(204, 204, 255) rgb(255, 255, 255) hsl(300, 100%, 0%) hsl(300, 100%, 10%) hsl(300, 100%, 20%) hsl(300, 100%, 30%) hsl(300, 100%, 40%) hsl(300, 100%, 50%) hsl(300, 100%, 60%) hsl(300, 100%, 70%) hsl(300, 100%, 80%) hsl(300, 100%, 90%) hsl(300, 100%, 100%) ==== rgb(0, 0, 0) rgb(51, 0, 51) rgb(102, 0, 102) rgb(153, 0, 153) rgb(204, 0, 204) rgb(255, 0, 255) rgb(255, 51, 255) rgb(255, 102, 255) rgb(255, 153, 255) rgb(255, 204, 255) rgb(255, 255, 255) ruby-sass-3.7.4/test/sass/encoding_test.rb000077500000000000000000000103611345125207600205700ustar00rootroot00000000000000# -*- coding: utf-8 -*- require File.dirname(__FILE__) + '/../test_helper' require File.dirname(__FILE__) + '/test_helper' require 'sass/util/test' class EncodingTest < MiniTest::Test include Sass::Util::Test def test_encoding_error render("foo\nbar\nb\xFEaz".force_encoding("utf-8")) assert(false, "Expected exception") rescue Sass::SyntaxError => e assert_equal(3, e.sass_line) assert_equal('Invalid UTF-8 character "\xFE"', e.message) end def test_ascii_incompatible_encoding_error template = "foo\nbar\nb_z".encode("utf-16le") template[9] = "\xFE".force_encoding("utf-16le") render(template) assert(false, "Expected exception") rescue Sass::SyntaxError => e assert_equal(3, e.sass_line) assert_equal('Invalid UTF-16LE character "\xFE"', e.message) end def test_prefers_charset_to_ruby_encoding assert_renders_encoded(< :compressed)) fóó a: b SASS end def test_newline_normalization assert_equal("/* foo\nbar\nbaz\nbang\nqux */\n", render("/* foo\nbar\r\nbaz\fbang\rqux */", :syntax => :scss)) end def test_null_normalization assert_equal(< :scss)) @charset "UTF-8"; /* foo�bar�baz */ CSS end # Regression def test_multibyte_prop_name assert_equal(< :scss)) #bar { background: a 0%; } CSS #bar { //  background: \#{a} 0%; } SCSS end private def assert_renders_encoded(css, sass) result = render(sass) assert_equal css.encoding, result.encoding assert_equal css, result end def render(sass, options = {}) munge_filename options Sass::Engine.new(sass, options).render end end ruby-sass-3.7.4/test/sass/engine_test.rb000077500000000000000000002353631345125207600202620ustar00rootroot00000000000000# -*- coding: utf-8 -*- require File.dirname(__FILE__) + '/../test_helper' require File.dirname(__FILE__) + '/test_helper' require 'sass/engine' require 'stringio' require 'mock_importer' require 'pathname' module Sass::Script::Functions::UserFunctions def option(name) Sass::Script::Value::String.new(@options[name.value.to_sym].to_s) end def set_a_variable(name, value) environment.set_var(name.value, value) return Sass::Script::Value::Null.new end def set_a_global_variable(name, value) environment.set_global_var(name.value, value) return Sass::Script::Value::Null.new end def get_a_variable(name) environment.var(name.value) || Sass::Script::Value::String.new("undefined") end end module Sass::Script::Functions include Sass::Script::Functions::UserFunctions end class SassEngineTest < MiniTest::Test FAKE_FILE_NAME = __FILE__.gsub(/rb$/,"sass") # A map of erroneous Sass documents to the error messages they should produce. # The error messages may be arrays; # if so, the second element should be the line number that should be reported for the error. # If this isn't provided, the tests will assume the line number should be the last line of the document. EXCEPTION_MAP = { "$a: 1 + " => 'Invalid CSS after "1 +": expected expression (e.g. 1px, bold), was ""', "$a: 1 + 2 +" => 'Invalid CSS after "1 + 2 +": expected expression (e.g. 1px, bold), was ""', "$a: 1 + 2 + %" => 'Invalid CSS after "1 + 2 + ": expected expression (e.g. 1px, bold), was "%"', "$a: foo(\"bar\"" => 'Invalid CSS after "foo("bar"": expected ")", was ""', "$a: 1 }" => 'Invalid CSS after "1 ": expected expression (e.g. 1px, bold), was "}"', "$a: 1 }foo\"" => 'Invalid CSS after "1 ": expected expression (e.g. 1px, bold), was "}foo""', ":" => 'Invalid property: ":".', ": a" => 'Invalid property: ": a".', "a\n :b" => < 'Invalid property: "b:" (no value).', "a\n :b: c" => 'Invalid property: ":b: c".', "a\n :b:c d" => 'Invalid property: ":b:c d".', "a\n :b c;" => 'Invalid CSS after "c": expected expression (e.g. 1px, bold), was ";"', "a\n b: c;" => 'Invalid CSS after "c": expected expression (e.g. 1px, bold), was ";"', ".foo ^bar\n a: b" => ['Invalid CSS after ".foo ": expected selector, was "^bar"', 1], "a\n @extend .foo ^bar" => 'Invalid CSS after ".foo ": expected selector, was "^bar"', "a\n @extend .foo .bar" => "Can't extend .foo .bar: can't extend nested selectors", "a\n @extend >" => "Can't extend >: invalid selector", "a\n @extend &.foo" => "Can't extend &.foo: can't extend parent selectors", "a: b" => 'Properties are only allowed within rules, directives, mixin includes, or other properties.', ":a b" => 'Properties are only allowed within rules, directives, mixin includes, or other properties.', "$" => 'Invalid variable: "$".', "$a" => 'Invalid variable: "$a".', "$ a" => 'Invalid variable: "$ a".', "$a b" => 'Invalid variable: "$a b".', "$a: 1b + 2c" => "Incompatible units: 'c' and 'b'.", "$a: 1b < 2c" => "Incompatible units: 'c' and 'b'.", "$a: 1b > 2c" => "Incompatible units: 'c' and 'b'.", "$a: 1b <= 2c" => "Incompatible units: 'c' and 'b'.", "$a: 1b >= 2c" => "Incompatible units: 'c' and 'b'.", "a\n b: 1b * 2c" => "2b*c isn't a valid CSS value.", "a\n b: 1b % 2c" => "Incompatible units: 'c' and 'b'.", "$a: 2px + #ccc" => "Cannot add a number with units (2px) to a color (#ccc).", "$a: #ccc + 2px" => "Cannot add a number with units (2px) to a color (#ccc).", "& a\n :b c" => ["Base-level rules cannot contain the parent-selector-referencing character '&'.", 1], "a\n :b\n c" => "Illegal nesting: Only properties may be nested beneath properties.", "$a: b\n :c d\n" => "Illegal nesting: Nothing may be nested beneath variable declarations.", "@import templates/basic\n foo" => "Illegal nesting: Nothing may be nested beneath import directives.", "foo\n @import foo.css" => "CSS import directives may only be used at the root of a document.", "@if true\n @import foo" => "Import directives may not be used within control directives or mixins.", "@if true\n .foo\n @import foo" => "Import directives may not be used within control directives or mixins.", "@mixin foo\n @import foo" => "Import directives may not be used within control directives or mixins.", "@mixin foo\n .foo\n @import foo" => "Import directives may not be used within control directives or mixins.", "@import foo;" => "Invalid @import: expected end of line, was \";\".", '$foo: "bar" "baz" !' => %Q{Invalid CSS after ""bar" "baz" ": expected expression (e.g. 1px, bold), was "!"}, '$foo: "bar" "baz" $' => %Q{Invalid CSS after ""bar" "baz" ": expected expression (e.g. 1px, bold), was "$"}, #' "=foo\n :color red\n.bar\n +bang" => "Undefined mixin 'bang'.", "=foo\n :color red\n.bar\n +bang_bop" => "Undefined mixin 'bang_bop'.", "=foo\n :color red\n.bar\n +bang-bop" => "Undefined mixin 'bang-bop'.", ".foo\n =foo\n :color red\n.bar\n +foo" => "Undefined mixin 'foo'.", " a\n b: c" => ["Indenting at the beginning of the document is illegal.", 1], " \n \n\t\n a\n b: c" => ["Indenting at the beginning of the document is illegal.", 4], "a\n b: c\n b: c" => ["Inconsistent indentation: 1 space was used for indentation, but the rest of the document was indented using 2 spaces.", 3], "a\n b: c\na\n b: c" => ["Inconsistent indentation: 1 space was used for indentation, but the rest of the document was indented using 2 spaces.", 4], "a\n\t\tb: c\n\tb: c" => ["Inconsistent indentation: 1 tab was used for indentation, but the rest of the document was indented using 2 tabs.", 3], "a\n b: c\n b: c" => ["Inconsistent indentation: 3 spaces were used for indentation, but the rest of the document was indented using 2 spaces.", 3], "a\n b: c\n a\n d: e" => ["Inconsistent indentation: 3 spaces were used for indentation, but the rest of the document was indented using 2 spaces.", 4], "a\n \tb: c" => ["Indentation can't use both tabs and spaces.", 2], "=a(" => 'Invalid CSS after "(": expected variable (e.g. $foo), was ""', "=a(b)" => 'Invalid CSS after "(": expected variable (e.g. $foo), was "b)"', "=a(,)" => 'Invalid CSS after "(": expected variable (e.g. $foo), was ",)"', "=a($)" => 'Invalid CSS after "(": expected variable (e.g. $foo), was "$)"', "=a($foo bar)" => 'Invalid CSS after "($foo ": expected ")", was "bar)"', "=foo\n bar: baz\n+foo" => ["Properties are only allowed within rules, directives, mixin includes, or other properties.", 2], "a-\#{$b\n c: d" => ['Invalid CSS after "a-#{$b": expected "}", was ""', 1], "=a($b: 1, $c)" => "Required argument $c must come before any optional arguments.", "=a($b: 1)\n a: $b\ndiv\n +a(1,2)" => "Mixin a takes 1 argument but 2 were passed.", "=a($b: 1)\n a: $b\ndiv\n +a(1,$c: 3)" => "Mixin a doesn't have an argument named $c.", "=a($b)\n a: $b\ndiv\n +a" => "Mixin a is missing argument $b.", "@function foo()\n 1 + 2" => "Functions can only contain variable declarations and control directives.", "@function foo()\n foo: bar" => "Functions can only contain variable declarations and control directives.", "@function foo()\n foo: bar\n @return 3" => ["Functions can only contain variable declarations and control directives.", 2], "@function foo\n @return 1" => ['Invalid CSS after "": expected "(", was ""', 1], "@function foo(\n @return 1" => ['Invalid CSS after "(": expected variable (e.g. $foo), was ""', 1], "@function foo(b)\n @return 1" => ['Invalid CSS after "(": expected variable (e.g. $foo), was "b)"', 1], "@function foo(,)\n @return 1" => ['Invalid CSS after "(": expected variable (e.g. $foo), was ",)"', 1], "@function foo($)\n @return 1" => ['Invalid CSS after "(": expected variable (e.g. $foo), was "$)"', 1], "@function foo()\n @return" => 'Invalid @return: expected expression.', "@function foo()\n @return 1\n $var: val" => 'Illegal nesting: Nothing may be nested beneath return directives.', "@function foo($a)\n @return 1\na\n b: foo()" => 'Function foo is missing argument $a.', "@function foo()\n @return 1\na\n b: foo(2)" => 'Function foo takes 0 arguments but 1 was passed.', "@function foo()\n @return 1\na\n b: foo($a: 1)" => "Function foo doesn't have an argument named $a.", "@function foo()\n @return 1\na\n b: foo($a: 1, $b: 2)" => "Function foo doesn't have the following arguments: $a, $b.", "@return 1" => '@return may only be used within a function.', "@if true\n @return 1" => '@return may only be used within a function.', "@mixin foo\n @return 1\n@include foo" => ['@return may only be used within a function.', 2], "@else\n a\n b: c" => ["@else must come after @if.", 1], "@if false\n@else foo" => "Invalid else directive '@else foo': expected 'if '.", "@if false\n@else if " => "Invalid else directive '@else if': expected 'if '.", "a\n $b: 12\nc\n d: $b" => 'Undefined variable: "$b".', "=foo\n $b: 12\nc\n +foo\n d: $b" => 'Undefined variable: "$b".', "c\n d: $b-foo" => 'Undefined variable: "$b-foo".', "c\n d: $b_foo" => 'Undefined variable: "$b_foo".', '@for $a from "foo" to 1' => '"foo" is not an integer.', '@for $a from 1 to "2"' => '"2" is not an integer.', '@for $a from 1 to "foo"' => '"foo" is not an integer.', '@for $a from 1 to 1.23232323232' => '1.2323232323 is not an integer.', '@for $a from 1px to 3em' => "Incompatible units: 'em' and 'px'.", '@if' => "Invalid if directive '@if': expected expression.", '@while' => "Invalid while directive '@while': expected expression.", '@debug' => "Invalid debug directive '@debug': expected expression.", %Q{@debug "a message"\n "nested message"} => "Illegal nesting: Nothing may be nested beneath debug directives.", '@error' => "Invalid error directive '@error': expected expression.", %Q{@error "a message"\n "nested message"} => "Illegal nesting: Nothing may be nested beneath error directives.", '@warn' => "Invalid warn directive '@warn': expected expression.", %Q{@warn "a message"\n "nested message"} => "Illegal nesting: Nothing may be nested beneath warn directives.", "/* foo\n bar\n baz" => "Inconsistent indentation: previous line was indented by 4 spaces, but this line was indented by 2 spaces.", '+foo(1 + 1: 2)' => 'Invalid CSS after "(1 + 1": expected comma, was ": 2)"', '+foo($var: )' => 'Invalid CSS after "($var: ": expected mixin argument, was ")"', '+foo($var: a, $var: b)' => 'Keyword argument "$var" passed more than once', '+foo($var-var: a, $var_var: b)' => 'Keyword argument "$var_var" passed more than once', '+foo($var_var: a, $var-var: b)' => 'Keyword argument "$var-var" passed more than once', "a\n b: foo(1 + 1: 2)" => 'Invalid CSS after "foo(1 + 1": expected comma, was ": 2)"', "a\n b: foo($var: )" => 'Invalid CSS after "foo($var: ": expected function argument, was ")"', "a\n b: foo($var: a, $var: b)" => 'Keyword argument "$var" passed more than once', "a\n b: foo($var-var: a, $var_var: b)" => 'Keyword argument "$var_var" passed more than once', "a\n b: foo($var_var: a, $var-var: b)" => 'Keyword argument "$var-var" passed more than once', "@if foo\n @extend .bar" => ["Extend directives may only be used within rules.", 2], "$var: true\n@while $var\n @extend .bar\n $var: false" => ["Extend directives may only be used within rules.", 3], "@for $i from 0 to 1\n @extend .bar" => ["Extend directives may only be used within rules.", 2], "@mixin foo\n @extend .bar\n@include foo" => ["Extend directives may only be used within rules.", 2], "foo %\n a: b" => ['Invalid CSS after "foo %": expected placeholder name, was ""', 1], "=foo\n @content error" => "Invalid content directive. Trailing characters found: \"error\".", "=foo\n @content\n b: c" => "Illegal nesting: Nothing may be nested beneath @content directives.", "@content" => '@content may only be used within a mixin.', "=simple\n .simple\n color: red\n+simple\n color: blue" => ['Mixin "simple" does not accept a content block.', 4], "@import \"foo\" // bar" => "Invalid CSS after \"\"foo\" \": expected media query list, was \"// bar\"", "@at-root\n a: b" => "Properties are only allowed within rules, directives, mixin includes, or other properties.", # Regression tests "a\n b:\n c\n d" => ["Illegal nesting: Only properties may be nested beneath properties.", 3], "& foo\n bar: baz\n blat: bang" => ["Base-level rules cannot contain the parent-selector-referencing character '&'.", 1], "a\n b: c\n& foo\n bar: baz\n blat: bang" => ["Base-level rules cannot contain the parent-selector-referencing character '&'.", 3], "@" => "Invalid directive: '@'.", "$r: 20em * #ccc" => ["Cannot multiply a number with units (20em) to a color (#ccc).", 1], "$r: #ccc / 1em" => ["Cannot divide a number with units (1em) to a color (#ccc).", 1], } def teardown clean_up_sassc end def test_basic_render renders_correctly "basic", { :style => :compact } end def test_empty_render assert_equal "", render("") end def test_multiple_calls_to_render sass = Sass::Engine.new("a\n b: c") assert_equal sass.render, sass.render end def test_alternate_styles renders_correctly "expanded", { :style => :expanded } renders_correctly "compact", { :style => :compact } renders_correctly "nested", { :style => :nested } renders_correctly "compressed", { :style => :compressed } end def test_compile assert_equal "div { hello: world; }\n", Sass.compile("$who: world\ndiv\n hello: $who", :syntax => :sass, :style => :compact) assert_equal "div { hello: world; }\n", Sass.compile("$who: world; div { hello: $who }", :style => :compact) end def test_compile_file FileUtils.mkdir_p(absolutize("tmp")) open(absolutize("tmp/test_compile_file.sass"), "w") {|f| f.write("$who: world\ndiv\n hello: $who")} open(absolutize("tmp/test_compile_file.scss"), "w") {|f| f.write("$who: world; div { hello: $who }")} assert_equal "div { hello: world; }\n", Sass.compile_file(absolutize("tmp/test_compile_file.sass"), :style => :compact) assert_equal "div { hello: world; }\n", Sass.compile_file(absolutize("tmp/test_compile_file.scss"), :style => :compact) ensure FileUtils.rm_rf(absolutize("tmp")) end def test_compile_file_to_css_file FileUtils.mkdir_p(absolutize("tmp")) open(absolutize("tmp/test_compile_file.sass"), "w") {|f| f.write("$who: world\ndiv\n hello: $who")} open(absolutize("tmp/test_compile_file.scss"), "w") {|f| f.write("$who: world; div { hello: $who }")} Sass.compile_file(absolutize("tmp/test_compile_file.sass"), absolutize("tmp/test_compile_file_sass.css"), :style => :compact) Sass.compile_file(absolutize("tmp/test_compile_file.scss"), absolutize("tmp/test_compile_file_scss.css"), :style => :compact) assert_equal "div { hello: world; }\n", File.read(absolutize("tmp/test_compile_file_sass.css")) assert_equal "div { hello: world; }\n", File.read(absolutize("tmp/test_compile_file_scss.css")) ensure FileUtils.rm_rf(absolutize("tmp")) end def test_flexible_tabulation assert_equal("p {\n a: b; }\n p q {\n c: d; }\n", render("p\n a: b\n q\n c: d\n")) assert_equal("p {\n a: b; }\n p q {\n c: d; }\n", render("p\n\ta: b\n\tq\n\t\tc: d\n")) end def test_import_same_name_different_ext assert_raise_message Sass::SyntaxError, < [File.dirname(__FILE__) + '/templates/']} munge_filename options Sass::Engine.new("@import 'same_name_different_ext'", options).render end end def test_import_same_name_different_partiality assert_raise_message Sass::SyntaxError, < [File.dirname(__FILE__) + '/templates/']} munge_filename options Sass::Engine.new("@import 'same_name_different_partiality'", options).render end end EXCEPTION_MAP.each do |key, value| define_method("test_exception (#{key.inspect})") do line = 10 begin silence_warnings {Sass::Engine.new(key, :filename => FAKE_FILE_NAME, :line => line).render} rescue Sass::SyntaxError => err value = [value] unless value.is_a?(Array) assert_equal(value.first.rstrip, err.message, "Line: #{key}") assert_equal(FAKE_FILE_NAME, err.sass_filename) assert_equal((value[1] || key.split("\n").length) + line - 1, err.sass_line, "Line: #{key}") assert_match(/#{Regexp.escape(FAKE_FILE_NAME)}:[0-9]+/, err.backtrace[0], "Line: #{key}") else assert(false, "Exception not raised for\n#{key}") end end end def test_exception_line to_render = < err assert_equal(5, err.sass_line) else assert(false, "Exception not raised for '#{to_render}'!") end end def test_exception_location to_render = < FAKE_FILE_NAME, :line => (__LINE__-7)).render rescue Sass::SyntaxError => err assert_equal(FAKE_FILE_NAME, err.sass_filename) assert_equal((__LINE__-6), err.sass_line) else assert(false, "Exception not raised for '#{to_render}'!") end end def test_imported_exception [1, 2, 3, 4].each do |i| begin Sass::Engine.new("@import bork#{i}", :load_paths => [File.dirname(__FILE__) + '/templates/']).render rescue Sass::SyntaxError => err assert_equal(2, err.sass_line) assert_match(/(\/|^)bork#{i}\.sass$/, err.sass_filename) assert_hash_has(err.sass_backtrace.first, :filename => err.sass_filename, :line => err.sass_line) assert_nil(err.sass_backtrace[1][:filename]) assert_equal(1, err.sass_backtrace[1][:line]) assert_match(/(\/|^)bork#{i}\.sass:2$/, err.backtrace.first) assert_equal("(sass):1", err.backtrace[1]) else assert(false, "Exception not raised for imported template: bork#{i}") end end end def test_double_imported_exception [1, 2, 3, 4].each do |i| begin Sass::Engine.new("@import nested_bork#{i}", :load_paths => [File.dirname(__FILE__) + '/templates/']).render rescue Sass::SyntaxError => err assert_equal(2, err.sass_line) assert_match(/(\/|^)bork#{i}\.sass$/, err.sass_filename) assert_hash_has(err.sass_backtrace.first, :filename => err.sass_filename, :line => err.sass_line) assert_match(/(\/|^)nested_bork#{i}\.sass$/, err.sass_backtrace[1][:filename]) assert_equal(2, err.sass_backtrace[1][:line]) assert_nil(err.sass_backtrace[2][:filename]) assert_equal(1, err.sass_backtrace[2][:line]) assert_match(/(\/|^)bork#{i}\.sass:2$/, err.backtrace.first) assert_match(/(\/|^)nested_bork#{i}\.sass:2$/, err.backtrace[1]) assert_equal("(sass):1", err.backtrace[2]) else assert(false, "Exception not raised for imported template: bork#{i}") end end end def test_selector_tracing actual_css = render(<<-SCSS, :syntax => :scss, :trace_selectors => true) @mixin mixed { .mixed { color: red; } } .context { @include mixed; } SCSS assert_equal(< err assert_equal(2, err.sass_line) assert_equal(filename_for_test, err.sass_filename) assert_equal("error-mixin", err.sass_mixin) assert_hash_has(err.sass_backtrace.first, :line => err.sass_line, :filename => err.sass_filename, :mixin => err.sass_mixin) assert_hash_has(err.sass_backtrace[1], :line => 5, :filename => filename_for_test, :mixin => "outer-mixin") assert_hash_has(err.sass_backtrace[2], :line => 8, :filename => filename_for_test, :mixin => nil) assert_equal("#{filename_for_test}:2:in `error-mixin'", err.backtrace.first) assert_equal("#{filename_for_test}:5:in `outer-mixin'", err.backtrace[1]) assert_equal("#{filename_for_test}:8", err.backtrace[2]) end def test_mixin_callsite_exception render(< err assert_hash_has(err.sass_backtrace.first, :line => 5, :filename => filename_for_test, :mixin => "one-arg-mixin") assert_hash_has(err.sass_backtrace[1], :line => 5, :filename => filename_for_test, :mixin => "outer-mixin") assert_hash_has(err.sass_backtrace[2], :line => 8, :filename => filename_for_test, :mixin => nil) end def test_mixin_exception_cssize render(< err assert_hash_has(err.sass_backtrace.first, :line => 2, :filename => filename_for_test, :mixin => "parent-ref-mixin") assert_hash_has(err.sass_backtrace[1], :line => 6, :filename => filename_for_test, :mixin => "outer-mixin") assert_hash_has(err.sass_backtrace[2], :line => 8, :filename => filename_for_test, :mixin => nil) end def test_mixin_and_import_exception Sass::Engine.new("@import nested_mixin_bork", :load_paths => [File.dirname(__FILE__) + '/templates/']).render assert(false, "Exception not raised") rescue Sass::SyntaxError => err assert_match(/(\/|^)nested_mixin_bork\.sass$/, err.sass_backtrace.first[:filename]) assert_hash_has(err.sass_backtrace.first, :mixin => "error-mixin", :line => 4) assert_match(/(\/|^)mixin_bork\.sass$/, err.sass_backtrace[1][:filename]) assert_hash_has(err.sass_backtrace[1], :mixin => "outer-mixin", :line => 2) assert_match(/(\/|^)mixin_bork\.sass$/, err.sass_backtrace[2][:filename]) assert_hash_has(err.sass_backtrace[2], :mixin => nil, :line => 5) assert_match(/(\/|^)nested_mixin_bork\.sass$/, err.sass_backtrace[3][:filename]) assert_hash_has(err.sass_backtrace[3], :mixin => nil, :line => 6) assert_hash_has(err.sass_backtrace[4], :filename => nil, :mixin => nil, :line => 1) end def test_recursive_mixin assert_equal < filename_for_test, :load_paths => [importer], :importer => importer) assert_raise_message(Sass::SyntaxError, < filename_for_test, :load_paths => [importer], :importer => importer) assert_raise_message(Sass::SyntaxError, < true, :line => 362} render(("a\n b: c\n" * 10) + "d\n e:\n" + ("f\n g: h\n" * 10), opts) rescue Sass::SyntaxError => e assert_equal(< true) =error-mixin($a) color: $a * 1em * 1px =outer-mixin($a) +error-mixin($a) .error +outer-mixin(12) SASS rescue Sass::SyntaxError => e assert_equal(< true) .filler stuff: "stuff!" a: b .more.filler a: b SASS rescue Sass::SyntaxError => e assert_equal(< :compact, :load_paths => [File.dirname(__FILE__) + "/templates"] } assert File.exist?(sassc_file) end def test_sass_pathname_import sassc_file = sassc_path("importee") assert !File.exist?(sassc_file) renders_correctly("import", :style => :compact, :load_paths => [Pathname.new(File.dirname(__FILE__) + "/templates")]) assert File.exist?(sassc_file) end def test_import_from_global_load_paths importer = MockImporter.new importer.add_import("imported", "div{color:red}") Sass.load_paths << importer assert_equal "div {\n color: red; }\n", Sass::Engine.new('@import "imported"', :importer => importer).render ensure Sass.load_paths.clear end def test_nonexistent_import assert_raise_message(Sass::SyntaxError, < :compact, :cache => false, :load_paths => [File.dirname(__FILE__) + "/templates"], }) assert !File.exist?(sassc_path("importee")) end def test_import_in_rule assert_equal(< [File.dirname(__FILE__) + '/templates/'])) .foo #foo { background-color: #baf; } .bar { a: b; } .bar #foo { background-color: #baf; } CSS .foo @import partial .bar a: b @import partial SASS end def test_units renders_correctly "units" end def test_default_function assert_equal(< :compact)) assert_equal("#foo #bar,#baz #boom{foo:bar}\n", render("#foo #bar,\n#baz #boom\n foo: bar", :style => :compressed)) assert_equal("#foo #bar,\n#baz #boom {\n foo: bar; }\n", render("#foo #bar,,\n,#baz #boom,\n foo: bar")) assert_equal("#bip #bop {\n foo: bar; }\n", render("#bip #bop,, ,\n foo: bar")) end def test_complex_multiline_selector renders_correctly "multiline" end def test_colon_only begin render("a\n b: c", :property_syntax => :old) rescue Sass::SyntaxError => e assert_equal("Illegal property syntax: can't use new syntax when :property_syntax => :old is set.", e.message) assert_equal(2, e.sass_line) else assert(false, "SyntaxError not raised for :property_syntax => :old") end begin silence_warnings {render("a\n :b c", :property_syntax => :new)} assert_equal(2, e.sass_line) rescue Sass::SyntaxError => e assert_equal("Illegal property syntax: can't use old syntax when :property_syntax => :new is set.", e.message) else assert(false, "SyntaxError not raised for :property_syntax => :new") end end def test_pseudo_elements assert_equal(< :compact)) assert_equal("@a {\n b: c;\n}\n", render("@a\n b: c", :style => :expanded)) assert_equal("@a{b:c}\n", render("@a\n b: c", :style => :compressed)) assert_equal("@a {\n b: c;\n d: e; }\n", render("@a\n b: c\n d: e")) assert_equal("@a { b: c; d: e; }\n", render("@a\n b: c\n d: e", :style => :compact)) assert_equal("@a {\n b: c;\n d: e;\n}\n", render("@a\n b: c\n d: e", :style => :expanded)) assert_equal("@a{b:c;d:e}\n", render("@a\n b: c\n d: e", :style => :compressed)) assert_equal("@a {\n #b {\n c: d; } }\n", render("@a\n #b\n c: d")) assert_equal("@a { #b { c: d; } }\n", render("@a\n #b\n c: d", :style => :compact)) assert_equal("@a {\n #b {\n c: d;\n }\n}\n", render("@a\n #b\n c: d", :style => :expanded)) assert_equal("@a{#b{c:d}}\n", render("@a\n #b\n c: d", :style => :compressed)) assert_equal("@a {\n #b {\n a: b; }\n #b #c {\n d: e; } }\n", render("@a\n #b\n a: b\n #c\n d: e")) assert_equal("@a { #b { a: b; }\n #b #c { d: e; } }\n", render("@a\n #b\n a: b\n #c\n d: e", :style => :compact)) assert_equal("@a {\n #b {\n a: b;\n }\n #b #c {\n d: e;\n }\n}\n", render("@a\n #b\n a: b\n #c\n d: e", :style => :expanded)) assert_equal("@a{#b{a:b}#b #c{d:e}}\n", render("@a\n #b\n a: b\n #c\n d: e", :style => :compressed)) assert_equal("@a {\n #foo,\n #bar {\n b: c; } }\n", render("@a\n #foo, \n #bar\n b: c")) assert_equal("@a { #foo, #bar { b: c; } }\n", render("@a\n #foo, \n #bar\n b: c", :style => :compact)) assert_equal("@a {\n #foo,\n #bar {\n b: c;\n }\n}\n", render("@a\n #foo, \n #bar\n b: c", :style => :expanded)) assert_equal("@a{#foo,#bar{b:c}}\n", render("@a\n #foo, \n #bar\n b: c", :style => :compressed)) to_render = < :compact)) assert_equal("@a{b:c;#d{e:f}g:h}\n", render(to_render, :style => :compressed)) end def test_property_hacks assert_equal(< true, :style => :compact)) /* line 2, test_line_annotations_inline.sass */ foo bar { foo: bar; } /* line 5, test_line_annotations_inline.sass */ foo baz { blip: blop; } /* line 9, test_line_annotations_inline.sass */ floodle { flop: blop; } /* line 18, test_line_annotations_inline.sass */ bup { mix: on; } /* line 15, test_line_annotations_inline.sass */ bup mixin { moop: mup; } /* line 22, test_line_annotations_inline.sass */ bip hop, skip hop { a: b; } CSS foo bar foo: bar baz blip: blop floodle flop: blop =mxn mix: on mixin moop: mup bup +mxn bip, skip hop a: b SASS end def test_line_annotations_with_filename renders_correctly "line_numbers", :line_comments => true, :load_paths => [File.dirname(__FILE__) + "/templates"] end def test_debug_info esc_file_name = Sass::SCSS::RX.escape_ident(Sass::Util.scope("test_debug_info_inline.sass")) assert_equal(< true, :style => :compact)) @media -sass-debug-info{filename{font-family:file\\:\\/\\/#{esc_file_name}}line{font-family:\\000032}} foo bar { foo: bar; } @media -sass-debug-info{filename{font-family:file\\:\\/\\/#{esc_file_name}}line{font-family:\\000035}} foo baz { blip: blop; } @media -sass-debug-info{filename{font-family:file\\:\\/\\/#{esc_file_name}}line{font-family:\\000039}} floodle { flop: blop; } @media -sass-debug-info{filename{font-family:file\\:\\/\\/#{esc_file_name}}line{font-family:\\0000318}} bup { mix: on; } @media -sass-debug-info{filename{font-family:file\\:\\/\\/#{esc_file_name}}line{font-family:\\0000315}} bup mixin { moop: mup; } @media -sass-debug-info{filename{font-family:file\\:\\/\\/#{esc_file_name}}line{font-family:\\0000322}} bip hop, skip hop { a: b; } CSS foo bar foo: bar baz blip: blop floodle flop: blop =mxn mix: on mixin moop: mup bup +mxn bip, skip hop a: b SASS end def test_debug_info_without_filename assert_equal(< true).render) @media -sass-debug-info{filename{}line{font-family:\\000031}} foo { a: b; } CSS foo a: b SASS end def test_debug_info_with_compressed assert_equal(< true, :style => :compressed)) foo{a:b} CSS foo a: b SASS end def test_debug_info_with_line_annotations esc_file_name = Sass::SCSS::RX.escape_ident(Sass::Util.scope("test_debug_info_with_line_annotations_inline.sass")) assert_equal(< true, :line_comments => true)) @media -sass-debug-info{filename{font-family:file\\:\\/\\/#{esc_file_name}}line{font-family:\\000031}} foo { a: b; } CSS foo a: b SASS end def test_debug_info_in_keyframes assert_equal(< true)) @-webkit-keyframes warm { from { color: black; } to { color: red; } } CSS @-webkit-keyframes warm from color: black to color: red SASS end def test_empty_first_line assert_equal("#a {\n b: c; }\n", render("#a\n\n b: c")) end def test_escaped_rule assert_equal(":focus {\n a: b; }\n", render("\\:focus\n a: b")) assert_equal("a {\n b: c; }\n a :focus {\n d: e; }\n", render("\\a\n b: c\n \\:focus\n d: e")) end def test_cr_newline assert_equal("foo {\n a: b;\n c: d;\n e: f; }\n", render("foo\r a: b\r\n c: d\n\r e: f")) end def test_property_with_content_and_nested_props assert_equal(< :expanded } end def test_directive_style_mixins assert_equal < e assert_equal("Function plus is missing argument $var1.", e.message) end def test_function_with_extra_argument render(< e assert_equal("Function plus doesn't have an argument named $var3.", e.message) end def test_function_with_positional_and_keyword_argument render(< e assert_equal("Function plus was passed argument $var2 both by position and by name.", e.message) end def test_function_with_keyword_before_positional_argument render(< e assert_equal("Positional arguments must come before keyword arguments.", e.message) end def test_function_with_if assert_equal(< e assert_equal('Undefined variable: "$variable".', e.message) end def test_user_defined_function_can_change_global_variable assert_equal(< :compressed) foo{color:blue;/*! foo * bar */} CSS foo color: blue /*! foo * bar */ SASS end def test_loud_comment_is_evaluated assert_equal < :new)) :focus { outline: 0; } CSS :focus outline: 0 SASS end def test_pseudo_class_with_new_properties assert_equal(< :new)) p :focus { outline: 0; } CSS p :focus outline: 0 SASS end def test_nil_option assert_equal(< nil)) foo { a: b; } CSS foo a: b SASS end def test_interpolation_in_raw_functions assert_equal(< true) CSS @warn "this is a warning" SASS end end def test_warn_with_imports prefix = Sass::Util.cleanpath(File.dirname(__FILE__)).to_s expected_warning = < :compact, :load_paths => ["#{prefix}/templates"] end end def test_media_bubbling assert_equal < :compact) .foo { a: b; } @media bar { .foo { c: d; } } .foo .baz { e: f; } @media bip { .foo .baz { g: h; } } .other { i: j; } CSS .foo a: b @media bar c: d .baz e: f @media bip g: h .other i: j SASS assert_equal < :expanded) .foo { a: b; } @media bar { .foo { c: d; } } .foo .baz { e: f; } @media bip { .foo .baz { g: h; } } .other { i: j; } CSS .foo a: b @media bar c: d .baz e: f @media bip g: h .other i: j SASS end def test_double_media_bubbling assert_equal < true) /* line 5, test_line_numbers_with_dos_line_endings_inline.sass */ .foo { a: b; } CSS \r \r \r \r .foo a: b SASS end def test_variable_in_media_in_mixin assert_equal < :compressed)) .box{border-style:solid} RESULT .box border: /*color: black style: solid SOURCE end def test_compressed_comment_beneath_directive assert_equal(< :compressed)) @foo{a:b} RESULT @foo a: b /*b: c SOURCE end def test_comment_with_crazy_indentation assert_equal(< :compressed) a>b,c+d,:-moz-any(e,f,g){h:i} CSS a > b, c + d, :-moz-any(e, f, g) h: i SASS end def test_comment_like_selector assert_raise_message(Sass::SyntaxError, 'Invalid CSS after "/": expected identifier, was " foo"') {render(< original_filename, :load_paths => [importer], :syntax => :scss, :importer => importer) engine.render assert_equal original_filename, engine.options[:original_filename] assert_equal original_filename, importer.engine("imported").options[:original_filename] end def test_changing_precision old_precision = Sass::Script::Value::Number.precision begin Sass::Script::Value::Number.precision = 8 assert_equal < e assert_equal([ {:mixin => '@content', :line => 6, :filename => 'test_content_backtrace_for_perform_inline.sass'}, {:mixin => 'foo', :line => 2, :filename => 'test_content_backtrace_for_perform_inline.sass'}, {:line => 5, :filename => 'test_content_backtrace_for_perform_inline.sass'}, ], e.sass_backtrace) end def test_content_backtrace_for_cssize render(< e assert_equal([ {:mixin => '@content', :line => 6, :filename => 'test_content_backtrace_for_cssize_inline.sass'}, {:mixin => 'foo', :line => 2, :filename => 'test_content_backtrace_for_cssize_inline.sass'}, {:line => 5, :filename => 'test_content_backtrace_for_cssize_inline.sass'}, ], e.sass_backtrace) end def test_mixin_with_args_and_varargs_passed_no_var_args assert_equal < :scss) .foo { a: 1; b: 2; c: 3; } CSS @mixin three-or-more-args($a, $b, $c, $rest...) { a: $a; b: $b; c: $c; } .foo { @include three-or-more-args($a: 1, $b: 2, $c: 3); } SASS end def test_debug_inspects_sass_objects assert_warning(< :scss)} test_debug_inspects_sass_objects_inline.scss:1 DEBUG: (a: 1, b: 2) END end def test_error_throws_sass_objects assert_raise_message(Sass::SyntaxError, "(a: 1, b: 2)") {render("@error (a: 1, b: 2)")} assert_raise_message(Sass::SyntaxError, "(a: 1, b: 2)") do render("$map: (a: 1, b: 2); @error $map", :syntax => :scss) end end def test_default_arg_before_splat assert_equal < :scss) .foo-positional { a: 1; b: 2; positional-arguments: 3, 4; keyword-arguments: (); } .foo-keywords { a: true; positional-arguments: (); keyword-arguments: (c: c, d: d); } CSS @mixin foo($a: true, $b: null, $arguments...) { a: $a; b: $b; positional-arguments: inspect($arguments); keyword-arguments: inspect(keywords($arguments)); } .foo-positional { @include foo(1, 2, 3, 4); } .foo-keywords { @include foo($c: c, $d: d); } SASS end def test_keyframes assert_equal < :compressed)) x{@foo;a:b;@bar} CSS x @foo a: b @bar SASS end def test_compressed_unknown_directive_in_directive assert_equal(< :compressed)) @x{@foo;a:b;@bar} CSS @x @foo a: b @bar SASS end def test_compressed_unknown_directive_with_children_in_directive assert_equal(< :compressed)) @x{@foo{a:b}c:d;@bar{e:f}} CSS @x @foo a: b c: d @bar e: f SASS end def test_compressed_rule_in_directive assert_equal(< :compressed)) @x{foo{a:b}c:d;bar{e:f}} CSS @x foo a: b c: d bar e: f SASS end def test_import_two_css_files_issue_1806 assert_equal(< :scss, :style => :compressed)) @import url(\"foo.css\");@import url(\"bar.css\");@import url(\"baz.css\") CSS @import url("foo.css"); @import url("bar.css"); @import url("baz.css"); SASS end def test_numeric_formatting_of_integers assert_equal(< :scss, :style => :compressed)) a{near:3.0000000001;plus:3;minus:3;negative:-3} CSS a { near: (3 + 0.0000000001); plus: (3 + 0.000000000001); minus: (3 - 0.000000000001); negative: (-3 + 0.000000000001); } SASS end def test_escaped_semicolons_are_not_compressed assert_equal(<<'CSS', render(<<'SASS', :syntax => :scss, :style => :compressed)) div{color:#f00000\9 \0 \;} CSS div { color: #f00000\9\0\; } SASS end def test_compressed_output_of_nth_selectors assert_equal(< :scss, :style => :compressed)) :nth-of-type(2n-1),:nth-child(2n-1),:nth(2n-1),:nth-of-type(2n-1),:nth-of-type(2n-1){color:red}:nth-of-type(2n+1),:nth-child(2n+1),:nth(2n+1),:nth-of-type(2n+1),:nth-of-type(2n+1){color:red} CSS :nth-of-type(2n-1), :nth-child(2n- 1), :nth(2n -1), :nth-of-type(2n - 1), :nth-of-type( 2n - 1 ) { color: red } :nth-of-type(2n+1), :nth-child(2n+ 1), :nth(2n +1), :nth-of-type(2n + 1), :nth-of-type( 2n + 1 ) { color: red } SASS end def test_descendant_selectors_with_leading_dash assert_equal(< :scss, :style => :compressed)) a -b{color:red} CSS a -b { color: red } SASS end def test_import_with_supports_clause_interp assert_equal(< :compressed)) @import url("fallback-layout.css") supports(not (display: flex)) CSS $display-type: flex @import url("fallback-layout.css") supports(not (display: #{$display-type})) SASS end def test_import_with_supports_clause assert_equal(< :compressed)) @import url("fallback-layout.css") supports(not (display: flex)) CSS @import url("fallback-layout.css") supports(not (display: flex)) SASS end def test_compressed_commas_in_attribute_selectors assert_equal(< :compressed)) .classname[a="1, 2, 3"],.another[b="4, 5, 6"]{color:red} CSS .classname[a="1, 2, 3"], .another[b="4, 5, 6"] color: red SASS end def test_trailing_commas_in_arglists assert_equal(< :nested)) .includes { one-positional-arg: positional 1 a; two-positional-args: positional 2 a b; one-keyword-arg: keyword 1 z; two-keyword-args: keyword 2 y z; mixed-args: mixed 2 y z; } .calls { one-positional-arg: positional 1 a; two-positional-args: positional 2 a b; one-keyword-arg: keyword 1 z; two-keyword-args: keyword 2 y z; mixed-args: mixed 2 y z; } CSS =one-positional-arg($a,) one-positional-arg: positional 1 $a =two-positional-args($a, $b,) two-positional-args: positional 2 $a $b =one-keyword-arg($a: a,) one-keyword-arg: keyword 1 $a =two-keyword-args($a: a, $b: b,) two-keyword-args: keyword 2 $a $b =mixed-args($a, $b: b,) mixed-args: mixed 2 $a $b @function one-positional-arg($a) @return positional 1 $a @function two-positional-args($a, $b) @return positional 2 $a $b @function one-keyword-arg($a: a) @return keyword 1 $a @function two-keyword-args($a: a, $b: b) @return keyword 2 $a $b @function mixed-args($a, $b: b) @return mixed 2 $a $b .includes +one-positional-arg(a,) +two-positional-args(a, b,) +one-keyword-arg($a: z,) +two-keyword-args($a: y, $b: z,) +mixed-args(y, $b: z,) .calls one-positional-arg: one-positional-arg(a) two-positional-args: two-positional-args(a, b) one-keyword-arg: one-keyword-arg($a: z) two-keyword-args: two-keyword-args($a: y, $b: z) mixed-args: mixed-args(y, $b: z) SASS end private def assert_hash_has(hash, expected) expected.each do |k, v| if v.nil? assert_nil(hash[k]) else assert_equal(v, hash[k]) end end end def assert_renders_encoded(css, sass) result = render(sass) assert_equal css.encoding, result.encoding assert_equal css, result end def render(sass, options = {}) munge_filename options options[:importer] ||= MockImporter.new Sass::Engine.new(sass, options).render end def renders_correctly(name, options={}) sass_file = load_file(name, "sass") css_file = load_file(name, "css") options[:filename] ||= filename(name, "sass") options[:syntax] ||= :sass options[:css_filename] ||= filename(name, "css") css_result = Sass::Engine.new(sass_file, options).render assert_equal css_file, css_result end def load_file(name, type = "sass") @result = '' File.new(filename(name, type)).each_line { |l| @result += l } @result end def filename(name, type) path = File.dirname(__FILE__) + "/#{type == 'sass' ? 'templates' : 'results'}/#{name}.#{type}" Sass::Util.cleanpath(path).to_s end def sassc_path(template) sassc_path = File.join(File.dirname(__FILE__) + "/templates/#{template}.sass") engine = Sass::Engine.new("", :filename => sassc_path, :importer => Sass::Importers::Filesystem.new(".")) key = engine.send(:sassc_key) File.join(engine.options[:cache_location], key) end end ruby-sass-3.7.4/test/sass/exec_test.rb000077500000000000000000000054421345125207600177320ustar00rootroot00000000000000require File.dirname(__FILE__) + '/../test_helper' require 'fileutils' require 'sass/util/test' require 'tmpdir' class ExecTest < MiniTest::Test include Sass::Util::Test def setup @dir = Dir.mktmpdir end def teardown FileUtils.rm_rf(@dir) clean_up_sassc end def test_scss_t_expanded src = get_path("src.scss") dest = get_path("dest.css") write(src, ".ruleset { margin: 0 }") assert(exec(*%w[scss --sourcemap=none -t expanded --unix-newlines].push(src, dest))) assert_equal(".ruleset {\n margin: 0;\n}\n", read(dest)) end def test_sass_convert_T_sass src = get_path("src.scss") dest = get_path("dest.css") write(src, ".ruleset { margin: 0 }") assert(exec(*%w[sass-convert -T sass --unix-newlines].push(src, dest))) assert_equal(".ruleset\n margin: 0\n", read(dest)) end def test_sass_convert_T_sass_in_place src = get_path("src.scss") write(src, ".ruleset { margin: 0 }") assert(exec(*%w[sass-convert -T sass --in-place --unix-newlines].push(src))) assert_equal(".ruleset\n margin: 0\n", read(src)) end def test_scss_t_expanded_no_unix_newlines return skip "Can be run on Windows only" unless Sass::Util.windows? src = get_path("src.scss") dest = get_path("dest.css") write(src, ".ruleset { margin: 0 }") assert(exec(*%w[scss -t expanded].push(src, dest))) assert_equal(".ruleset {\r\n margin: 0;\r\n}\r\n", read(dest)) end def test_sass_convert_T_sass_no_unix_newlines return skip "Can be run on Windows only" unless Sass::Util.windows? src = get_path("src.scss") dest = get_path("dest.sass") write(src, ".ruleset { margin: 0 }") assert(exec(*%w[sass-convert -T sass].push(src, dest))) assert_equal(".ruleset\r\n margin: 0\r\n", read(dest)) end def test_sass_convert_T_sass_in_place_no_unix_newlines return skip "Can be run on Windows only" unless Sass::Util.windows? src = get_path("src.scss") write(src, ".ruleset { margin: 0 }") assert(exec(*%w[sass-convert -T sass --in-place].push(src))) assert_equal(".ruleset\r\n margin: 0\r\n", read(src)) end def test_sass_convert_R Dir.chdir(@dir) do src = get_path("styles/src.css") write(src, ".ruleset { margin: 0 }") assert(exec(*%w[sass-convert -Rq --from css --to scss --trace styles])) end end private def get_path(name) File.join(@dir, name) end def read(file) open(file, 'rb') {|f| f.read} end def write(file, content) FileUtils.mkdir_p(File.dirname(file)) open(file, 'wb') {|f| f.write(content)} end def exec(script, *args) script = File.dirname(__FILE__) + '/../../bin/' + script ruby = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'] + RbConfig::CONFIG['EXEEXT']) system(ruby, script, *args) end end ruby-sass-3.7.4/test/sass/extend_test.rb000077500000000000000000001320701345125207600202730ustar00rootroot00000000000000require File.dirname(__FILE__) + '/../test_helper' class ExtendTest < MiniTest::Test def test_basic assert_equal < :sass) .foo, .bar { a: b; } CSS .foo a: b .bar @extend .foo SASS assert_equal < :sass) .foo, .bar { a: b; } CSS .foo a: b .bar @extend \#{".foo"} SASS end def test_multiple_targets assert_equal < .bar .x', '.baz:root .bang .y {@extend .x}', '.foo:root > .bar .x, .baz.foo:root > .bar .bang .y') end def test_comma_extendee assert_equal < bar {@extend .foo}', '.baz .foo, .baz foo > bar' end def test_nested_extender_finds_common_selectors_around_child_selector assert_extends 'a > b c .c1', 'a c .c2 {@extend .c1}', 'a > b c .c1, a > b c .c2' assert_extends 'a > b c .c1', 'b c .c2 {@extend .c1}', 'a > b c .c1, a > b c .c2' end def test_nested_extender_doesnt_find_common_selectors_around_adjacent_sibling_selector assert_extends 'a + b c .c1', 'a c .c2 {@extend .c1}', 'a + b c .c1, a + b a c .c2, a a + b c .c2' assert_extends 'a + b c .c1', 'a b .c2 {@extend .c1}', 'a + b c .c1, a a + b c .c2' assert_extends 'a + b c .c1', 'b c .c2 {@extend .c1}', 'a + b c .c1, a + b c .c2' end def test_nested_extender_doesnt_find_common_selectors_around_sibling_selector assert_extends 'a ~ b c .c1', 'a c .c2 {@extend .c1}', 'a ~ b c .c1, a ~ b a c .c2, a a ~ b c .c2' assert_extends 'a ~ b c .c1', 'a b .c2 {@extend .c1}', 'a ~ b c .c1, a a ~ b c .c2' assert_extends 'a ~ b c .c1', 'b c .c2 {@extend .c1}', 'a ~ b c .c1, a ~ b c .c2' end def test_nested_extender_doesnt_find_common_selectors_around_reference_selector silence_warnings {assert_extends 'a /for/ b c .c1', 'a c .c2 {@extend .c1}', 'a /for/ b c .c1, a /for/ b a c .c2, a a /for/ b c .c2'} silence_warnings {assert_extends 'a /for/ b c .c1', 'a b .c2 {@extend .c1}', 'a /for/ b c .c1, a a /for/ b c .c2'} silence_warnings {assert_extends 'a /for/ b c .c1', 'b c .c2 {@extend .c1}', 'a /for/ b c .c1, a /for/ b c .c2'} end def test_nested_extender_with_early_child_selectors_doesnt_subseq_them assert_extends('.bip > .bap .foo', '.grip > .bap .bar {@extend .foo}', '.bip > .bap .foo, .bip > .bap .grip > .bap .bar, .grip > .bap .bip > .bap .bar') assert_extends('.bap > .bip .foo', '.bap > .grip .bar {@extend .foo}', '.bap > .bip .foo, .bap > .bip .bap > .grip .bar, .bap > .grip .bap > .bip .bar') end def test_nested_extender_with_child_selector_unifies assert_extends '.baz.foo', 'foo > bar {@extend .foo}', '.baz.foo, foo > bar.baz' assert_equal < .foo, .baz > .bar { a: b; } CSS .baz > { .foo {a: b} .bar {@extend .foo} } SCSS assert_equal < .baz { a: b; } CSS .foo { .bar {a: b} > .baz {@extend .bar} } SCSS end def test_nested_extender_with_early_child_selector assert_equal < .baz { a: b; } CSS .foo { .bar {a: b} .bip > .baz {@extend .bar} } SCSS assert_equal < .baz { a: b; } CSS .foo { .bip .bar {a: b} > .baz {@extend .bar} } SCSS assert_extends '.foo > .bar', '.bip + .baz {@extend .bar}', '.foo > .bar, .foo > .bip + .baz' assert_extends '.foo + .bar', '.bip > .baz {@extend .bar}', '.foo + .bar, .bip > .foo + .baz' assert_extends '.foo > .bar', '.bip > .baz {@extend .bar}', '.foo > .bar, .bip.foo > .baz' end def test_nested_extender_with_trailing_child_selector assert_raises(Sass::SyntaxError, "bar > can't extend: invalid selector") do render("bar > {@extend .baz}") end end def test_nested_extender_with_sibling_selector assert_extends '.baz .foo', 'foo + bar {@extend .foo}', '.baz .foo, .baz foo + bar' end def test_nested_extender_with_hacky_selector assert_extends('.baz .foo', 'foo + > > + bar {@extend .foo}', '.baz .foo, .baz foo + > > + bar, foo .baz + > > + bar') assert_extends '.baz .foo', '> > bar {@extend .foo}', '.baz .foo, > > .baz bar' end def test_nested_extender_merges_with_same_selector assert_equal < .bar .baz', '.foo > .bar .bang {@extend .baz}', '.foo > .bar .baz, .foo > .bar .bang') end # Combinator Unification def test_combinator_unification_for_hacky_combinators assert_extends '.a > + x', '.b y {@extend x}', '.a > + x, .a .b > + y, .b .a > + y' assert_extends '.a x', '.b > + y {@extend x}', '.a x, .a .b > + y, .b .a > + y' assert_extends '.a > + x', '.b > + y {@extend x}', '.a > + x, .a .b > + y, .b .a > + y' assert_extends '.a ~ > + x', '.b > + y {@extend x}', '.a ~ > + x, .a .b ~ > + y, .b .a ~ > + y' assert_extends '.a + > x', '.b > + y {@extend x}', '.a + > x' assert_extends '.a + > x', '.b > + y {@extend x}', '.a + > x' assert_extends '.a ~ > + .b > x', '.c > + .d > y {@extend x}', '.a ~ > + .b > x, .a .c ~ > + .d.b > y, .c .a ~ > + .d.b > y' end def test_combinator_unification_double_tilde assert_extends '.a.b ~ x', '.a ~ y {@extend x}', '.a.b ~ x, .a.b ~ y' assert_extends '.a ~ x', '.a.b ~ y {@extend x}', '.a ~ x, .a.b ~ y' assert_extends '.a ~ x', '.b ~ y {@extend x}', '.a ~ x, .a ~ .b ~ y, .b ~ .a ~ y, .b.a ~ y' assert_extends 'a.a ~ x', 'b.b ~ y {@extend x}', 'a.a ~ x, a.a ~ b.b ~ y, b.b ~ a.a ~ y' end def test_combinator_unification_tilde_plus assert_extends '.a.b + x', '.a ~ y {@extend x}', '.a.b + x, .a.b + y' assert_extends '.a + x', '.a.b ~ y {@extend x}', '.a + x, .a.b ~ .a + y, .a.b + y' assert_extends '.a + x', '.b ~ y {@extend x}', '.a + x, .b ~ .a + y, .b.a + y' assert_extends 'a.a + x', 'b.b ~ y {@extend x}', 'a.a + x, b.b ~ a.a + y' assert_extends '.a.b ~ x', '.a + y {@extend x}', '.a.b ~ x, .a.b ~ .a + y, .a.b + y' assert_extends '.a ~ x', '.a.b + y {@extend x}', '.a ~ x, .a.b + y' assert_extends '.a ~ x', '.b + y {@extend x}', '.a ~ x, .a ~ .b + y, .a.b + y' assert_extends 'a.a ~ x', 'b.b + y {@extend x}', 'a.a ~ x, a.a ~ b.b + y' end def test_combinator_unification_angle_sibling assert_extends '.a > x', '.b ~ y {@extend x}', '.a > x, .a > .b ~ y' assert_extends '.a > x', '.b + y {@extend x}', '.a > x, .a > .b + y' assert_extends '.a ~ x', '.b > y {@extend x}', '.a ~ x, .b > .a ~ y' assert_extends '.a + x', '.b > y {@extend x}', '.a + x, .b > .a + y' end def test_combinator_unification_double_angle assert_extends '.a.b > x', '.b > y {@extend x}', '.a.b > x, .b.a > y' assert_extends '.a > x', '.a.b > y {@extend x}', '.a > x, .a.b > y' assert_extends '.a > x', '.b > y {@extend x}', '.a > x, .b.a > y' assert_extends 'a.a > x', 'b.b > y {@extend x}', 'a.a > x' end def test_combinator_unification_double_plus assert_extends '.a.b + x', '.b + y {@extend x}', '.a.b + x, .b.a + y' assert_extends '.a + x', '.a.b + y {@extend x}', '.a + x, .a.b + y' assert_extends '.a + x', '.b + y {@extend x}', '.a + x, .b.a + y' assert_extends 'a.a + x', 'b.b + y {@extend x}', 'a.a + x' end def test_combinator_unification_angle_space assert_extends '.a.b > x', '.a y {@extend x}', '.a.b > x, .a.b > y' assert_extends '.a > x', '.a.b y {@extend x}', '.a > x, .a.b .a > y' assert_extends '.a > x', '.b y {@extend x}', '.a > x, .b .a > y' assert_extends '.a.b x', '.a > y {@extend x}', '.a.b x, .a.b .a > y' assert_extends '.a x', '.a.b > y {@extend x}', '.a x, .a.b > y' assert_extends '.a x', '.b > y {@extend x}', '.a x, .a .b > y' end def test_combinator_unification_plus_space assert_extends '.a.b + x', '.a y {@extend x}', '.a.b + x, .a .a.b + y' assert_extends '.a + x', '.a.b y {@extend x}', '.a + x, .a.b .a + y' assert_extends '.a + x', '.b y {@extend x}', '.a + x, .b .a + y' assert_extends '.a.b x', '.a + y {@extend x}', '.a.b x, .a.b .a + y' assert_extends '.a x', '.a.b + y {@extend x}', '.a x, .a .a.b + y' assert_extends '.a x', '.b + y {@extend x}', '.a x, .a .b + y' end def test_combinator_unification_nested assert_extends '.a > .b + x', '.c > .d + y {@extend x}', '.a > .b + x, .c.a > .d.b + y' assert_extends '.a > .b + x', '.c > y {@extend x}', '.a > .b + x, .c.a > .b + y' end def test_combinator_unification_with_newlines assert_equal < .b + x, .c.a > .d.b + y { a: b; } CSS .a > .b + x {a: b} .c > .d + y {@extend x} SCSS end # Loops def test_extend_self_loop assert_equal < .foo', 'foo bar {@extend .foo}', '> .foo, > foo bar' end def test_nested_selector_with_child_selector_hack_extender assert_extends '.foo .bar', '> foo bar {@extend .bar}', '.foo .bar, > .foo foo bar, > foo .foo bar' end def test_nested_selector_with_child_selector_hack_extender_and_extendee assert_extends '> .foo', '> foo bar {@extend .foo}', '> .foo, > foo bar' end def test_nested_selector_with_child_selector_hack_extender_and_sibling_selector_extendee assert_extends '~ .foo', '> foo bar {@extend .foo}', '~ .foo' end def test_nested_selector_with_child_selector_hack_extender_and_extendee_and_newline assert_equal < .foo, > flip, > foo bar { a: b; } CSS > .foo {a: b} flip, > foo bar {@extend .foo} SCSS end def test_extended_parent_and_child_redundancy_elimination assert_equal < :scss}.merge(options) munge_filename options Sass::Engine.new(sass, options).render end end ruby-sass-3.7.4/test/sass/fixtures/000077500000000000000000000000001345125207600172635ustar00rootroot00000000000000ruby-sass-3.7.4/test/sass/fixtures/test_staleness_check_across_importers.css000066400000000000000000000000301345125207600276410ustar00rootroot00000000000000.pear { color: green; } ruby-sass-3.7.4/test/sass/fixtures/test_staleness_check_across_importers.scss000066400000000000000000000000211345125207600300240ustar00rootroot00000000000000@import "apple"; ruby-sass-3.7.4/test/sass/functions_test.rb000077500000000000000000002510621345125207600210170ustar00rootroot00000000000000require 'minitest/autorun' require File.dirname(__FILE__) + '/../test_helper' require File.dirname(__FILE__) + '/test_helper' require 'sass/script' require 'mock_importer' module Sass::Script::Functions def no_kw_args Sass::Script::Value::String.new("no-kw-args") end def only_var_args(*args) Sass::Script::Value::String.new("only-var-args("+args.map{|a| a.plus(Sass::Script::Value::Number.new(1)).to_s }.join(", ")+")") end declare :only_var_args, [], :var_args => true def only_kw_args(kwargs) Sass::Script::Value::String.new("only-kw-args(" + kwargs.keys.map {|a| a.to_s}.sort.join(", ") + ")") end declare :only_kw_args, [], :var_kwargs => true def deprecated_arg_fn(arg1, arg2, arg3 = nil) Sass::Script::Value::List.new( [arg1, arg2, arg3 || Sass::Script::Value::Null.new], separator: :space) end declare :deprecated_arg_fn, [:arg1, :arg2, :arg3], :deprecated => [:arg_1, :arg_2, :arg3] declare :deprecated_arg_fn, [:arg1, :arg2], :deprecated => [:arg_1, :arg_2] end module Sass::Script::Functions::UserFunctions def call_options_on_new_value str = Sass::Script::Value::String.new("foo") str.options[:foo] str end def user_defined Sass::Script::Value::String.new("I'm a user-defined string!") end def _preceding_underscore Sass::Script::Value::String.new("I'm another user-defined string!") end def fetch_the_variable environment.var('variable') end end module Sass::Script::Functions include Sass::Script::Functions::UserFunctions end class SassFunctionTest < MiniTest::Test # Tests taken from: # http://www.w3.org/Style/CSS/Test/CSS3/Color/20070927/html4/t040204-hsl-h-rotating-b.htm # http://www.w3.org/Style/CSS/Test/CSS3/Color/20070927/html4/t040204-hsl-values-b.htm File.read(File.dirname(__FILE__) + "/data/hsl-rgb.txt").split("\n\n").each do |chunk| hsls, rgbs = chunk.strip.split("====") hsls.strip.split("\n").zip(rgbs.strip.split("\n")) do |hsl, rgb| hsl_method = "test_hsl: #{hsl} = #{rgb}" unless method_defined?(hsl_method) define_method(hsl_method) do assert_equal(evaluate(rgb), evaluate(hsl)) end end rgb_to_hsl_method = "test_rgb_to_hsl: #{rgb} = #{hsl}" unless method_defined?(rgb_to_hsl_method) define_method(rgb_to_hsl_method) do rgb_color = perform(rgb) hsl_color = perform(hsl) white = hsl_color.lightness == 100 black = hsl_color.lightness == 0 grayscale = white || black || hsl_color.saturation == 0 assert_in_delta(hsl_color.hue, rgb_color.hue, 0.0001, "Hues should be equal") unless grayscale assert_in_delta(hsl_color.saturation, rgb_color.saturation, 0.0001, "Saturations should be equal") unless white || black assert_in_delta(hsl_color.lightness, rgb_color.lightness, 0.0001, "Lightnesses should be equal") end end end end def test_hsl_kwargs assert_equal "#33cccc", evaluate("hsl($hue: 180, $saturation: 60%, $lightness: 50%)") end def test_hsl_clamps_bounds assert_equal("#1f1f1f", evaluate("hsl(10, -114, 12)")) assert_equal("white", evaluate("hsl(10, 10, 256%)")) end def test_hsl_checks_types assert_error_message("$hue: \"foo\" is not a number for `hsl'", "hsl(\"foo\", 10, 12)"); assert_error_message("$saturation: \"foo\" is not a number for `hsl'", "hsl(10, \"foo\", 12)"); assert_error_message("$lightness: \"foo\" is not a number for `hsl'", "hsl(10, 10, \"foo\")"); end def test_hsla assert_equal "rgba(51, 204, 204, 0.4)", evaluate("hsla(180, 60%, 50%, 0.4)") assert_equal "#33cccc", evaluate("hsla(180, 60%, 50%, 1)") assert_equal "rgba(51, 204, 204, 0)", evaluate("hsla(180, 60%, 50%, 0)") assert_equal "rgba(51, 204, 204, 0.4)", evaluate("hsla($hue: 180, $saturation: 60%, $lightness: 50%, $alpha: 0.4)") end def test_hsla_clamps_bounds assert_equal("#1f1f1f", evaluate("hsla(10, -114, 12, 1)")) assert_equal("rgba(255, 255, 255, 0)", evaluate("hsla(10, 10, 256%, 0)")) assert_equal("rgba(28, 24, 23, 0)", evaluate("hsla(10, 10, 10, -0.1)")) assert_equal("#1c1817", evaluate("hsla(10, 10, 10, 1.1)")) end def test_hsla_checks_types assert_error_message("$hue: \"foo\" is not a number for `hsla'", "hsla(\"foo\", 10, 12, 0.3)"); assert_error_message("$saturation: \"foo\" is not a number for `hsla'", "hsla(10, \"foo\", 12, 0)"); assert_error_message("$lightness: \"foo\" is not a number for `hsla'", "hsla(10, 10, \"foo\", 1)"); assert_error_message("$alpha: \"foo\" is not a number for `hsla'", "hsla(10, 10, 10, \"foo\")"); end def test_percentage assert_equal("50%", evaluate("percentage(.5)")) assert_equal("100%", evaluate("percentage(1)")) assert_equal("25%", evaluate("percentage(25px / 100px)")) assert_equal("50%", evaluate("percentage($number: 0.5)")) end def test_percentage_checks_types assert_error_message("$number: 25px is not a unitless number for `percentage'", "percentage(25px)") assert_error_message("$number: #cccccc is not a unitless number for `percentage'", "percentage(#ccc)") assert_error_message("$number: \"string\" is not a unitless number for `percentage'", %Q{percentage("string")}) end def test_round assert_equal("5", evaluate("round(4.8)")) assert_equal("5px", evaluate("round(4.8px)")) assert_equal("5px", evaluate("round(5.49px)")) assert_equal("5px", evaluate("round($number: 5.49px)")) assert_equal("-6", evaluate("round(-5.5)")) end def test_round_checks_types assert_error_message("$value: #cccccc is not a number for `round'", "round(#ccc)") end def test_floor assert_equal("4", evaluate("floor(4.8)")) assert_equal("4px", evaluate("floor(4.8px)")) assert_equal("4px", evaluate("floor($number: 4.8px)")) end def test_floor_checks_types assert_error_message("$value: \"foo\" is not a number for `floor'", "floor(\"foo\")") end def test_ceil assert_equal("5", evaluate("ceil(4.1)")) assert_equal("5px", evaluate("ceil(4.8px)")) assert_equal("5px", evaluate("ceil($number: 4.8px)")) end def test_ceil_checks_types assert_error_message("$value: \"a\" is not a number for `ceil'", "ceil(\"a\")") end def test_abs assert_equal("5", evaluate("abs(-5)")) assert_equal("5px", evaluate("abs(-5px)")) assert_equal("5", evaluate("abs(5)")) assert_equal("5px", evaluate("abs(5px)")) assert_equal("5px", evaluate("abs($number: 5px)")) end def test_abs_checks_types assert_error_message("$value: #aaaaaa is not a number for `abs'", "abs(#aaa)") end def test_min # A trailing comma forces the function to be parsed as a Sass function, # rather than a CSS math function. assert_equal("1", evaluate("min(1, 2, 3,)")) assert_equal("1", evaluate("min(3px, 2px, 1,)")) assert_equal("4em", evaluate("min(4em,)")) assert_equal("10cm", evaluate("min(10cm, 6in,)")) assert_equal("1q", evaluate("min(1cm, 1q,)")) assert_error_message("#aaaaaa is not a number for `min'", "min(#aaa)") assert_error_message("Incompatible units: 'px' and 'em'.", "min(3em, 4em, 1px,)") end def test_max # A trailing comma forces the function to be parsed as a Sass function, # rather than a CSS math function. assert_equal("3", evaluate("max(1, 2, 3,)")) assert_equal("3", evaluate("max(3, 2px, 1px,)")) assert_equal("4em", evaluate("max(4em,)")) assert_equal("6in", evaluate("max(10cm, 6in,)")) assert_equal("11mm", evaluate("max(11mm, 10q,)")) assert_error_message("#aaaaaa is not a number for `max'", "max(#aaa)") assert_error_message("Incompatible units: 'px' and 'em'.", "max(3em, 4em, 1px,)") end def test_rgb assert_equal("#123456", evaluate("rgb(18, 52, 86)")) assert_equal("#beaded", evaluate("rgb(190, 173, 237)")) assert_equal("springgreen", evaluate("rgb(0, 255, 127)")) assert_equal("springgreen", evaluate("rgb($red: 0, $green: 255, $blue: 127)")) end def test_rgb_percent assert_equal("#123457", evaluate("rgb(7.1%, 20.4%, 34%)")) assert_equal("#beaded", evaluate("rgb(74.7%, 173, 93%)")) assert_equal("#beaded", evaluate("rgb(190, 68%, 237)")) assert_equal("#00ff80", evaluate("rgb(0%, 100%, 50%)")) end def test_rgb_clamps_bounds assert_equal("#ff0101", evaluate("rgb(256, 1, 1)")) assert_equal("#01ff01", evaluate("rgb(1, 256, 1)")) assert_equal("#0101ff", evaluate("rgb(1, 1, 256)")) assert_equal("#01ffff", evaluate("rgb(1, 256, 257)")) assert_equal("#000101", evaluate("rgb(-1, 1, 1)")) end def test_rgb_clamps_percent_bounds assert_equal("red", evaluate("rgb(100.1%, 0, 0)")) assert_equal("black", evaluate("rgb(0, -0.1%, 0)")) assert_equal("blue", evaluate("rgb(0, 0, 101%)")) end def test_rgb_tests_types assert_error_message("$red: \"foo\" is not a number for `rgb'", "rgb(\"foo\", 10, 12)"); assert_error_message("$green: \"foo\" is not a number for `rgb'", "rgb(10, \"foo\", 12)"); assert_error_message("$blue: \"foo\" is not a number for `rgb'", "rgb(10, 10, \"foo\")"); end def test_rgba assert_equal("rgba(18, 52, 86, 0.5)", evaluate("rgba(18, 52, 86, 0.5)")) assert_equal("#beaded", evaluate("rgba(190, 173, 237, 1)")) assert_equal("rgba(0, 255, 127, 0)", evaluate("rgba(0, 255, 127, 0)")) assert_equal("rgba(0, 255, 127, 0)", evaluate("rgba($red: 0, $green: 255, $blue: 127, $alpha: 0)")) end def test_rgba_clamps_bounds assert_equal("rgba(255, 1, 1, 0.3)", evaluate("rgba(256, 1, 1, 0.3)")) assert_equal("rgba(1, 255, 1, 0.3)", evaluate("rgba(1, 256, 1, 0.3)")) assert_equal("rgba(1, 1, 255, 0.3)", evaluate("rgba(1, 1, 256, 0.3)")) assert_equal("rgba(1, 255, 255, 0.3)", evaluate("rgba(1, 256, 257, 0.3)")) assert_equal("rgba(0, 1, 1, 0.3)", evaluate("rgba(-1, 1, 1, 0.3)")) assert_equal("rgba(1, 1, 1, 0)", evaluate("rgba(1, 1, 1, -0.2)")) assert_equal("#010101", evaluate("rgba(1, 1, 1, 1.2)")) end def test_rgba_tests_types assert_error_message("$red: \"foo\" is not a number for `rgba'", "rgba(\"foo\", 10, 12, 0.2)"); assert_error_message("$green: \"foo\" is not a number for `rgba'", "rgba(10, \"foo\", 12, 0.1)"); assert_error_message("$blue: \"foo\" is not a number for `rgba'", "rgba(10, 10, \"foo\", 0)"); assert_error_message("$alpha: \"foo\" is not a number for `rgba'", "rgba(10, 10, 10, \"foo\")"); end def test_rgba_with_color assert_equal "rgba(16, 32, 48, 0.5)", evaluate("rgba(#102030, 0.5)") assert_equal "rgba(0, 0, 255, 0.5)", evaluate("rgba(blue, 0.5)") assert_equal "rgba(0, 0, 255, 0.5)", evaluate("rgba($color: blue, $alpha: 0.5)") end def test_rgba_with_color_tests_types assert_error_message("$color: \"foo\" is not a color for `rgba'", "rgba(\"foo\", 0.2)"); assert_error_message("$alpha: \"foo\" is not a number for `rgba'", "rgba(blue, \"foo\")"); end def test_rgba_tests_num_args assert_error_message("wrong number of arguments (0 for 4) for `rgba'", "rgba()"); assert_error_message("wrong number of arguments (1 for 4) for `rgba'", "rgba(blue)"); assert_error_message("wrong number of arguments (3 for 4) for `rgba'", "rgba(1, 2, 3)"); assert_error_message("wrong number of arguments (5 for 4) for `rgba'", "rgba(1, 2, 3, 0.4, 5)"); end def test_red assert_equal("18", evaluate("red(#123456)")) assert_equal("18", evaluate("red($color: #123456)")) end def test_red_exception assert_error_message("$color: 12 is not a color for `red'", "red(12)") end def test_green assert_equal("52", evaluate("green(#123456)")) assert_equal("52", evaluate("green($color: #123456)")) end def test_green_exception assert_error_message("$color: 12 is not a color for `green'", "green(12)") end def test_blue assert_equal("86", evaluate("blue(#123456)")) assert_equal("86", evaluate("blue($color: #123456)")) end def test_blue_exception assert_error_message("$color: 12 is not a color for `blue'", "blue(12)") end def test_hue assert_equal("18deg", evaluate("hue(hsl(18, 50%, 20%))")) assert_equal("18deg", evaluate("hue($color: hsl(18, 50%, 20%))")) end def test_hue_exception assert_error_message("$color: 12 is not a color for `hue'", "hue(12)") end def test_saturation assert_equal("52%", evaluate("saturation(hsl(20, 52%, 20%))")) assert_equal("52%", evaluate("saturation(hsl(20, 52, 20%))")) assert_equal("52%", evaluate("saturation($color: hsl(20, 52, 20%))")) end def test_saturation_exception assert_error_message("$color: 12 is not a color for `saturation'", "saturation(12)") end def test_lightness assert_equal("86%", evaluate("lightness(hsl(120, 50%, 86%))")) assert_equal("86%", evaluate("lightness(hsl(120, 50%, 86))")) assert_equal("86%", evaluate("lightness($color: hsl(120, 50%, 86))")) end def test_lightness_exception assert_error_message("$color: 12 is not a color for `lightness'", "lightness(12)") end def test_alpha assert_equal("1", evaluate("alpha(#123456)")) assert_equal("0.34", evaluate("alpha(rgba(0, 1, 2, 0.34))")) assert_equal("0", evaluate("alpha(hsla(0, 1, 2, 0))")) assert_equal("0", evaluate("alpha($color: hsla(0, 1, 2, 0))")) end def test_alpha_exception assert_error_message("$color: 12 is not a color for `alpha'", "alpha(12)") end def test_opacity assert_equal("1", evaluate("opacity(#123456)")) assert_equal("0.34", evaluate("opacity(rgba(0, 1, 2, 0.34))")) assert_equal("0", evaluate("opacity(hsla(0, 1, 2, 0))")) assert_equal("0", evaluate("opacity($color: hsla(0, 1, 2, 0))")) assert_equal("opacity(20%)", evaluate("opacity(20%)")) end def test_opacity_exception assert_error_message("$color: \"foo\" is not a color for `opacity'", "opacity(foo)") end def test_opacify assert_equal("rgba(0, 0, 0, 0.75)", evaluate("opacify(rgba(0, 0, 0, 0.5), 0.25)")) assert_equal("rgba(0, 0, 0, 0.3)", evaluate("opacify(rgba(0, 0, 0, 0.2), 0.1)")) assert_equal("rgba(0, 0, 0, 0.7)", evaluate("fade-in(rgba(0, 0, 0, 0.2), 0.5px)")) assert_equal("black", evaluate("fade_in(rgba(0, 0, 0, 0.2), 0.8)")) assert_equal("black", evaluate("opacify(rgba(0, 0, 0, 0.2), 1)")) assert_equal("rgba(0, 0, 0, 0.2)", evaluate("opacify(rgba(0, 0, 0, 0.2), 0%)")) assert_equal("rgba(0, 0, 0, 0.2)", evaluate("opacify($color: rgba(0, 0, 0, 0.2), $amount: 0%)")) assert_equal("rgba(0, 0, 0, 0.2)", evaluate("fade-in($color: rgba(0, 0, 0, 0.2), $amount: 0%)")) end def test_opacify_tests_bounds assert_error_message("Amount -0.001 must be between 0 and 1 for `opacify'", "opacify(rgba(0, 0, 0, 0.2), -0.001)") assert_error_message("Amount 1.001 must be between 0 and 1 for `opacify'", "opacify(rgba(0, 0, 0, 0.2), 1.001)") end def test_opacify_tests_types assert_error_message("$color: \"foo\" is not a color for `opacify'", "opacify(\"foo\", 10%)") assert_error_message("$amount: \"foo\" is not a number for `opacify'", "opacify(#fff, \"foo\")") end def test_transparentize assert_equal("rgba(0, 0, 0, 0.3)", evaluate("transparentize(rgba(0, 0, 0, 0.5), 0.2)")) assert_equal("rgba(0, 0, 0, 0.1)", evaluate("transparentize(rgba(0, 0, 0, 0.2), 0.1)")) assert_equal("rgba(0, 0, 0, 0.2)", evaluate("fade-out(rgba(0, 0, 0, 0.5), 0.3px)")) assert_equal("rgba(0, 0, 0, 0)", evaluate("fade_out(rgba(0, 0, 0, 0.2), 0.2)")) assert_equal("rgba(0, 0, 0, 0)", evaluate("transparentize(rgba(0, 0, 0, 0.2), 1)")) assert_equal("rgba(0, 0, 0, 0.2)", evaluate("transparentize(rgba(0, 0, 0, 0.2), 0)")) assert_equal("rgba(0, 0, 0, 0.2)", evaluate("transparentize($color: rgba(0, 0, 0, 0.2), $amount: 0)")) assert_equal("rgba(0, 0, 0, 0.2)", evaluate("fade-out($color: rgba(0, 0, 0, 0.2), $amount: 0)")) end def test_transparentize_tests_bounds assert_error_message("Amount -0.001 must be between 0 and 1 for `transparentize'", "transparentize(rgba(0, 0, 0, 0.2), -0.001)") assert_error_message("Amount 1.001 must be between 0 and 1 for `transparentize'", "transparentize(rgba(0, 0, 0, 0.2), 1.001)") end def test_transparentize_tests_types assert_error_message("$color: \"foo\" is not a color for `transparentize'", "transparentize(\"foo\", 10%)") assert_error_message("$amount: \"foo\" is not a number for `transparentize'", "transparentize(#fff, \"foo\")") end def test_lighten assert_equal("#4d4d4d", evaluate("lighten(hsl(0, 0, 0), 30%)")) assert_equal("#ee0000", evaluate("lighten(#800, 20%)")) assert_equal("white", evaluate("lighten(#fff, 20%)")) assert_equal("white", evaluate("lighten(#800, 100%)")) assert_equal("#880000", evaluate("lighten(#800, 0%)")) assert_equal("rgba(238, 0, 0, 0.5)", evaluate("lighten(rgba(136, 0, 0, 0.5), 20%)")) assert_equal("rgba(238, 0, 0, 0.5)", evaluate("lighten($color: rgba(136, 0, 0, 0.5), $amount: 20%)")) end def test_lighten_tests_bounds assert_error_message("Amount -0.001 must be between 0% and 100% for `lighten'", "lighten(#123, -0.001)") assert_error_message("Amount 100.001 must be between 0% and 100% for `lighten'", "lighten(#123, 100.001)") end def test_lighten_tests_types assert_error_message("$color: \"foo\" is not a color for `lighten'", "lighten(\"foo\", 10%)") assert_error_message("$amount: \"foo\" is not a number for `lighten'", "lighten(#fff, \"foo\")") end def test_darken assert_equal("#ff6a00", evaluate("darken(hsl(25, 100, 80), 30%)")) assert_equal("#220000", evaluate("darken(#800, 20%)")) assert_equal("black", evaluate("darken(#000, 20%)")) assert_equal("black", evaluate("darken(#800, 100%)")) assert_equal("#880000", evaluate("darken(#800, 0%)")) assert_equal("rgba(34, 0, 0, 0.5)", evaluate("darken(rgba(136, 0, 0, 0.5), 20%)")) assert_equal("rgba(34, 0, 0, 0.5)", evaluate("darken($color: rgba(136, 0, 0, 0.5), $amount: 20%)")) end def test_darken_tests_bounds assert_error_message("Amount -0.001 must be between 0% and 100% for `darken'", "darken(#123, -0.001)") assert_error_message("Amount 100.001 must be between 0% and 100% for `darken'", "darken(#123, 100.001)") end def test_darken_tests_types assert_error_message("$color: \"foo\" is not a color for `darken'", "darken(\"foo\", 10%)") assert_error_message("$amount: \"foo\" is not a number for `darken'", "darken(#fff, \"foo\")") end def test_saturate assert_equal("#d9f2d9", evaluate("saturate(hsl(120, 30, 90), 20%)")) assert_equal("#9e3f3f", evaluate("saturate(#855, 20%)")) assert_equal("black", evaluate("saturate(#000, 20%)")) assert_equal("white", evaluate("saturate(#fff, 20%)")) assert_equal("#33ff33", evaluate("saturate(#8a8, 100%)")) assert_equal("#88aa88", evaluate("saturate(#8a8, 0%)")) assert_equal("rgba(158, 63, 63, 0.5)", evaluate("saturate(rgba(136, 85, 85, 0.5), 20%)")) assert_equal("rgba(158, 63, 63, 0.5)", evaluate("saturate($color: rgba(136, 85, 85, 0.5), $amount: 20%)")) assert_equal("saturate(50%)", evaluate("saturate(50%)")) end def test_saturate_tests_bounds assert_error_message("Amount -0.001 must be between 0% and 100% for `saturate'", "saturate(#123, -0.001)") assert_error_message("Amount 100.001 must be between 0% and 100% for `saturate'", "saturate(#123, 100.001)") end def test_saturate_tests_types assert_error_message("$color: \"foo\" is not a color for `saturate'", "saturate(\"foo\", 10%)") assert_error_message("$amount: \"foo\" is not a number for `saturate'", "saturate(#fff, \"foo\")") end def test_desaturate assert_equal("#e3e8e3", evaluate("desaturate(hsl(120, 30, 90), 20%)")) assert_equal("#726b6b", evaluate("desaturate(#855, 20%)")) assert_equal("black", evaluate("desaturate(#000, 20%)")) assert_equal("white", evaluate("desaturate(#fff, 20%)")) assert_equal("#999999", evaluate("desaturate(#8a8, 100%)")) assert_equal("#88aa88", evaluate("desaturate(#8a8, 0%)")) assert_equal("rgba(114, 107, 107, 0.5)", evaluate("desaturate(rgba(136, 85, 85, 0.5), 20%)")) assert_equal("rgba(114, 107, 107, 0.5)", evaluate("desaturate($color: rgba(136, 85, 85, 0.5), $amount: 20%)")) end def test_desaturate_tests_bounds assert_error_message("Amount -0.001 must be between 0% and 100% for `desaturate'", "desaturate(#123, -0.001)") assert_error_message("Amount 100.001 must be between 0% and 100% for `desaturate'", "desaturate(#123, 100.001)") end def test_desaturate_tests_types assert_error_message("$color: \"foo\" is not a color for `desaturate'", "desaturate(\"foo\", 10%)") assert_error_message("$amount: \"foo\" is not a number for `desaturate'", "desaturate(#fff, \"foo\")") end def test_adjust_hue assert_equal("#deeded", evaluate("adjust-hue(hsl(120, 30, 90), 60deg)")) assert_equal("#ededde", evaluate("adjust-hue(hsl(120, 30, 90), -60deg)")) assert_equal("#886a11", evaluate("adjust-hue(#811, 45deg)")) assert_equal("black", evaluate("adjust-hue(#000, 45deg)")) assert_equal("white", evaluate("adjust-hue(#fff, 45deg)")) assert_equal("#88aa88", evaluate("adjust-hue(#8a8, 360deg)")) assert_equal("#88aa88", evaluate("adjust-hue(#8a8, 0deg)")) assert_equal("rgba(136, 106, 17, 0.5)", evaluate("adjust-hue(rgba(136, 17, 17, 0.5), 45deg)")) assert_equal("rgba(136, 106, 17, 0.5)", evaluate("adjust-hue($color: rgba(136, 17, 17, 0.5), $degrees: 45deg)")) end def test_adjust_hue_tests_types assert_error_message("$color: \"foo\" is not a color for `adjust-hue'", "adjust-hue(\"foo\", 10%)") assert_error_message("$degrees: \"foo\" is not a number for `adjust-hue'", "adjust-hue(#fff, \"foo\")") end def test_adjust_color # HSL assert_equal(evaluate("hsl(180, 30, 90)"), evaluate("adjust-color(hsl(120, 30, 90), $hue: 60deg)")) assert_equal(evaluate("hsl(120, 50, 90)"), evaluate("adjust-color(hsl(120, 30, 90), $saturation: 20%)")) assert_equal(evaluate("hsl(120, 30, 60)"), evaluate("adjust-color(hsl(120, 30, 90), $lightness: -30%)")) # RGB assert_equal(evaluate("rgb(15, 20, 30)"), evaluate("adjust-color(rgb(10, 20, 30), $red: 5)")) assert_equal(evaluate("rgb(10, 15, 30)"), evaluate("adjust-color(rgb(10, 20, 30), $green: -5)")) assert_equal(evaluate("rgb(10, 20, 40)"), evaluate("adjust-color(rgb(10, 20, 30), $blue: 10)")) # Alpha assert_equal(evaluate("hsla(120, 30, 90, 0.65)"), evaluate("adjust-color(hsl(120, 30, 90), $alpha: -0.35)")) assert_equal(evaluate("rgba(10, 20, 30, 0.9)"), evaluate("adjust-color(rgba(10, 20, 30, 0.4), $alpha: 0.5)")) # HSL composability assert_equal(evaluate("hsl(180, 20, 90)"), evaluate("adjust-color(hsl(120, 30, 90), $hue: 60deg, $saturation: -10%)")) assert_equal(evaluate("hsl(180, 20, 95)"), evaluate("adjust-color(hsl(120, 30, 90), $hue: 60deg, $saturation: -10%, $lightness: 5%)")) assert_equal(evaluate("hsla(120, 20, 95, 0.3)"), evaluate("adjust-color(hsl(120, 30, 90), $saturation: -10%, $lightness: 5%, $alpha: -0.7)")) # RGB composability assert_equal(evaluate("rgb(15, 20, 29)"), evaluate("adjust-color(rgb(10, 20, 30), $red: 5, $blue: -1)")) assert_equal(evaluate("rgb(15, 45, 29)"), evaluate("adjust-color(rgb(10, 20, 30), $red: 5, $green: 25, $blue: -1)")) assert_equal(evaluate("rgba(10, 25, 29, 0.7)"), evaluate("adjust-color(rgb(10, 20, 30), $green: 5, $blue: -1, $alpha: -0.3)")) # HSL range restriction assert_equal(evaluate("hsl(120, 30, 90)"), evaluate("adjust-color(hsl(120, 30, 90), $hue: 720deg)")) assert_equal(evaluate("hsl(120, 0, 90)"), evaluate("adjust-color(hsl(120, 30, 90), $saturation: -90%)")) assert_equal(evaluate("hsl(120, 30, 100)"), evaluate("adjust-color(hsl(120, 30, 90), $lightness: 30%)")) # RGB range restriction assert_equal(evaluate("rgb(255, 20, 30)"), evaluate("adjust-color(rgb(10, 20, 30), $red: 250)")) assert_equal(evaluate("rgb(10, 0, 30)"), evaluate("adjust-color(rgb(10, 20, 30), $green: -30)")) assert_equal(evaluate("rgb(10, 20, 0)"), evaluate("adjust-color(rgb(10, 20, 30), $blue: -40)")) end def test_adjust_color_tests_types assert_error_message("$color: \"foo\" is not a color for `adjust-color'", "adjust-color(foo, $hue: 10)") # HSL assert_error_message("$hue: \"foo\" is not a number for `adjust-color'", "adjust-color(blue, $hue: foo)") assert_error_message("$saturation: \"foo\" is not a number for `adjust-color'", "adjust-color(blue, $saturation: foo)") assert_error_message("$lightness: \"foo\" is not a number for `adjust-color'", "adjust-color(blue, $lightness: foo)") # RGB assert_error_message("$red: \"foo\" is not a number for `adjust-color'", "adjust-color(blue, $red: foo)") assert_error_message("$green: \"foo\" is not a number for `adjust-color'", "adjust-color(blue, $green: foo)") assert_error_message("$blue: \"foo\" is not a number for `adjust-color'", "adjust-color(blue, $blue: foo)") # Alpha assert_error_message("$alpha: \"foo\" is not a number for `adjust-color'", "adjust-color(blue, $alpha: foo)") end def test_adjust_color_tests_arg_range # HSL assert_error_message("$saturation: Amount 101% must be between -100% and 100% for `adjust-color'", "adjust-color(blue, $saturation: 101%)") assert_error_message("$saturation: Amount -101% must be between -100% and 100% for `adjust-color'", "adjust-color(blue, $saturation: -101%)") assert_error_message("$lightness: Amount 101% must be between -100% and 100% for `adjust-color'", "adjust-color(blue, $lightness: 101%)") assert_error_message("$lightness: Amount -101% must be between -100% and 100% for `adjust-color'", "adjust-color(blue, $lightness: -101%)") # RGB assert_error_message("$red: Amount 256 must be between -255 and 255 for `adjust-color'", "adjust-color(blue, $red: 256)") assert_error_message("$red: Amount -256 must be between -255 and 255 for `adjust-color'", "adjust-color(blue, $red: -256)") assert_error_message("$green: Amount 256 must be between -255 and 255 for `adjust-color'", "adjust-color(blue, $green: 256)") assert_error_message("$green: Amount -256 must be between -255 and 255 for `adjust-color'", "adjust-color(blue, $green: -256)") assert_error_message("$blue: Amount 256 must be between -255 and 255 for `adjust-color'", "adjust-color(blue, $blue: 256)") assert_error_message("$blue: Amount -256 must be between -255 and 255 for `adjust-color'", "adjust-color(blue, $blue: -256)") # Alpha assert_error_message("$alpha: Amount 1.1 must be between -1 and 1 for `adjust-color'", "adjust-color(blue, $alpha: 1.1)") assert_error_message("$alpha: Amount -1.1 must be between -1 and 1 for `adjust-color'", "adjust-color(blue, $alpha: -1.1)") end def test_adjust_color_argument_errors assert_error_message("Unknown argument $hoo (260deg) for `adjust-color'", "adjust-color(blue, $hoo: 260deg)") assert_error_message("Cannot specify HSL and RGB values for a color at the same time for `adjust-color'", "adjust-color(blue, $hue: 120deg, $red: 10)"); assert_error_message("10px is not a keyword argument for `adjust_color'", "adjust-color(blue, 10px)") assert_error_message("10px is not a keyword argument for `adjust_color'", "adjust-color(blue, 10px, 20px)") assert_error_message("10px is not a keyword argument for `adjust_color'", "adjust-color(blue, 10px, $hue: 180deg)") end def test_scale_color # HSL assert_equal(evaluate("hsl(120, 51, 90)"), evaluate("scale-color(hsl(120, 30, 90), $saturation: 30%)")) assert_equal(evaluate("hsl(120, 30, 76.5)"), evaluate("scale-color(hsl(120, 30, 90), $lightness: -15%)")) # RGB assert_equal(evaluate("rgb(157, 20, 30)"), evaluate("scale-color(rgb(10, 20, 30), $red: 60%)")) assert_equal(evaluate("rgb(10, 38.8, 30)"), evaluate("scale-color(rgb(10, 20, 30), $green: 8%)")) assert_equal(evaluate("rgb(10, 20, 20)"), evaluate("scale-color(rgb(10, 20, 30), $blue: -(1/3)*100%)")) # Alpha assert_equal(evaluate("hsla(120, 30, 90, 0.86)"), evaluate("scale-color(hsl(120, 30, 90), $alpha: -14%)")) assert_equal(evaluate("rgba(10, 20, 30, 0.82)"), evaluate("scale-color(rgba(10, 20, 30, 0.8), $alpha: 10%)")) # HSL composability assert_equal(evaluate("hsl(120, 51, 76.5)"), evaluate("scale-color(hsl(120, 30, 90), $saturation: 30%, $lightness: -15%)")) assert_equal(evaluate("hsla(120, 51, 90, 0.2)"), evaluate("scale-color(hsl(120, 30, 90), $saturation: 30%, $alpha: -80%)")) # RGB composability assert_equal(evaluate("rgb(157, 38.8, 30)"), evaluate("scale-color(rgb(10, 20, 30), $red: 60%, $green: 8%)")) assert_equal(evaluate("rgb(157, 38.8, 20)"), evaluate("scale-color(rgb(10, 20, 30), $red: 60%, $green: 8%, $blue: -(1/3)*100%)")) assert_equal(evaluate("rgba(10, 38.8, 20, 0.55)"), evaluate("scale-color(rgba(10, 20, 30, 0.5), $green: 8%, $blue: -(1/3)*100%, $alpha: 10%)")) # Extremes assert_equal(evaluate("hsl(120, 100, 90)"), evaluate("scale-color(hsl(120, 30, 90), $saturation: 100%)")) assert_equal(evaluate("hsl(120, 30, 90)"), evaluate("scale-color(hsl(120, 30, 90), $saturation: 0%)")) assert_equal(evaluate("hsl(120, 0, 90)"), evaluate("scale-color(hsl(120, 30, 90), $saturation: -100%)")) end def test_scale_color_tests_types assert_error_message("$color: \"foo\" is not a color for `scale-color'", "scale-color(foo, $red: 10%)") # HSL assert_error_message("$saturation: \"foo\" is not a number for `scale-color'", "scale-color(blue, $saturation: foo)") assert_error_message("$lightness: \"foo\" is not a number for `scale-color'", "scale-color(blue, $lightness: foo)") # RGB assert_error_message("$red: \"foo\" is not a number for `scale-color'", "scale-color(blue, $red: foo)") assert_error_message("$green: \"foo\" is not a number for `scale-color'", "scale-color(blue, $green: foo)") assert_error_message("$blue: \"foo\" is not a number for `scale-color'", "scale-color(blue, $blue: foo)") # Alpha assert_error_message("$alpha: \"foo\" is not a number for `scale-color'", "scale-color(blue, $alpha: foo)") end def test_scale_color_argument_errors # Range assert_error_message("$saturation: Amount 101% must be between -100% and 100% for `scale-color'", "scale-color(blue, $saturation: 101%)") assert_error_message("$red: Amount -101% must be between -100% and 100% for `scale-color'", "scale-color(blue, $red: -101%)") assert_error_message("$alpha: Amount -101% must be between -100% and 100% for `scale-color'", "scale-color(blue, $alpha: -101%)") # Unit assert_error_message("Expected $saturation to have a unit of % but got 80 for `scale-color'", "scale-color(blue, $saturation: 80)") assert_error_message("Expected $alpha to have a unit of % but got 0.5 for `scale-color'", "scale-color(blue, $alpha: 0.5)") # Unknown argument assert_error_message("Unknown argument $hue (80%) for `scale-color'", "scale-color(blue, $hue: 80%)") # Non-keyword arg assert_error_message("10px is not a keyword argument for `scale_color'", "scale-color(blue, 10px)") # HSL/RGB assert_error_message("Cannot specify HSL and RGB values for a color at the same time for `scale-color'", "scale-color(blue, $lightness: 10%, $red: 20%)"); end def test_change_color # HSL assert_equal(evaluate("hsl(195, 30, 90)"), evaluate("change-color(hsl(120, 30, 90), $hue: 195deg)")) assert_equal(evaluate("hsl(120, 50, 90)"), evaluate("change-color(hsl(120, 30, 90), $saturation: 50%)")) assert_equal(evaluate("hsl(120, 30, 40)"), evaluate("change-color(hsl(120, 30, 90), $lightness: 40%)")) # RGB assert_equal(evaluate("rgb(123, 20, 30)"), evaluate("change-color(rgb(10, 20, 30), $red: 123)")) assert_equal(evaluate("rgb(10, 234, 30)"), evaluate("change-color(rgb(10, 20, 30), $green: 234)")) assert_equal(evaluate("rgb(10, 20, 198)"), evaluate("change-color(rgb(10, 20, 30), $blue: 198)")) # Alpha assert_equal(evaluate("rgba(10, 20, 30, 0.76)"), evaluate("change-color(rgb(10, 20, 30), $alpha: 0.76)")) # HSL composability assert_equal(evaluate("hsl(56, 30, 47)"), evaluate("change-color(hsl(120, 30, 90), $hue: 56deg, $lightness: 47%)")) assert_equal(evaluate("hsla(56, 30, 47, 0.9)"), evaluate("change-color(hsl(120, 30, 90), $hue: 56deg, $lightness: 47%, $alpha: 0.9)")) end def test_change_color_tests_types assert_error_message("$color: \"foo\" is not a color for `change-color'", "change-color(foo, $red: 10%)") # HSL assert_error_message("$saturation: \"foo\" is not a number for `change-color'", "change-color(blue, $saturation: foo)") assert_error_message("$lightness: \"foo\" is not a number for `change-color'", "change-color(blue, $lightness: foo)") # RGB assert_error_message("$red: \"foo\" is not a number for `change-color'", "change-color(blue, $red: foo)") assert_error_message("$green: \"foo\" is not a number for `change-color'", "change-color(blue, $green: foo)") assert_error_message("$blue: \"foo\" is not a number for `change-color'", "change-color(blue, $blue: foo)") # Alpha assert_error_message("$alpha: \"foo\" is not a number for `change-color'", "change-color(blue, $alpha: foo)") end def test_change_color_argument_errors # Range assert_error_message("Saturation 101% must be between 0% and 100% for `change-color'", "change-color(blue, $saturation: 101%)") assert_error_message("Lightness 101% must be between 0% and 100% for `change-color'", "change-color(blue, $lightness: 101%)") assert_error_message("Red value -1 must be between 0 and 255 for `change-color'", "change-color(blue, $red: -1)") assert_error_message("Green value 256 must be between 0 and 255 for `change-color'", "change-color(blue, $green: 256)") assert_error_message("Blue value 500 must be between 0 and 255 for `change-color'", "change-color(blue, $blue: 500)") # Unknown argument assert_error_message("Unknown argument $hoo (80%) for `change-color'", "change-color(blue, $hoo: 80%)") # Non-keyword arg assert_error_message("10px is not a keyword argument for `change_color'", "change-color(blue, 10px)") # HSL/RGB assert_error_message("Cannot specify HSL and RGB values for a color at the same time for `change-color'", "change-color(blue, $lightness: 10%, $red: 120)"); end def test_ie_hex_str assert_equal("#FFAA11CC", evaluate('ie-hex-str(#aa11cc)')) assert_equal("#FFAA11CC", evaluate('ie-hex-str(#a1c)')) assert_equal("#FFAA11CC", evaluate('ie-hex-str(#A1c)')) assert_equal("#80FF0000", evaluate('ie-hex-str(rgba(255, 0, 0, 0.5))')) end def test_mix assert_equal("purple", evaluate("mix(#f00, #00f)")) assert_equal("gray", evaluate("mix(#f00, #0ff)")) assert_equal("#809155", evaluate("mix(#f70, #0aa)")) assert_equal("#4000bf", evaluate("mix(#f00, #00f, 25%)")) assert_equal("rgba(64, 0, 191, 0.75)", evaluate("mix(rgba(255, 0, 0, 0.5), #00f)")) assert_equal("red", evaluate("mix(#f00, #00f, 100%)")) assert_equal("blue", evaluate("mix(#f00, #00f, 0%)")) assert_equal("rgba(255, 0, 0, 0.5)", evaluate("mix(#f00, transparentize(#00f, 1))")) assert_equal("rgba(0, 0, 255, 0.5)", evaluate("mix(transparentize(#f00, 1), #00f)")) assert_equal("red", evaluate("mix(#f00, transparentize(#00f, 1), 100%)")) assert_equal("blue", evaluate("mix(transparentize(#f00, 1), #00f, 0%)")) assert_equal("rgba(0, 0, 255, 0)", evaluate("mix(#f00, transparentize(#00f, 1), 0%)")) assert_equal("rgba(255, 0, 0, 0)", evaluate("mix(transparentize(#f00, 1), #00f, 100%)")) assert_equal("rgba(255, 0, 0, 0)", evaluate("mix($color1: transparentize(#f00, 1), $color2: #00f, $weight: 100%)")) end def test_mix_tests_types assert_error_message("$color1: \"foo\" is not a color for `mix'", "mix(\"foo\", #f00, 10%)") assert_error_message("$color2: \"foo\" is not a color for `mix'", "mix(#f00, \"foo\", 10%)") assert_error_message("$weight: \"foo\" is not a number for `mix'", "mix(#f00, #baf, \"foo\")") end def test_mix_tests_bounds assert_error_message("Weight -0.001 must be between 0% and 100% for `mix'", "mix(#123, #456, -0.001)") assert_error_message("Weight 100.001 must be between 0% and 100% for `mix'", "mix(#123, #456, 100.001)") end def test_grayscale assert_equal("#bbbbbb", evaluate("grayscale(#abc)")) assert_equal("gray", evaluate("grayscale(#f00)")) assert_equal("gray", evaluate("grayscale(#00f)")) assert_equal("white", evaluate("grayscale(white)")) assert_equal("black", evaluate("grayscale(black)")) assert_equal("black", evaluate("grayscale($color: black)")) assert_equal("grayscale(2)", evaluate("grayscale(2)")) assert_equal("grayscale(-5px)", evaluate("grayscale(-5px)")) end def tets_grayscale_tests_types assert_error_message("$color: \"foo\" is not a color for `grayscale'", "grayscale(\"foo\")") end def test_complement assert_equal("#ccbbaa", evaluate("complement(#abc)")) assert_equal("cyan", evaluate("complement(red)")) assert_equal("red", evaluate("complement(cyan)")) assert_equal("white", evaluate("complement(white)")) assert_equal("black", evaluate("complement(black)")) assert_equal("black", evaluate("complement($color: black)")) end def tets_complement_tests_types assert_error_message("$color: \"foo\" is not a color for `complement'", "complement(\"foo\")") end def test_invert assert_equal("#112233", evaluate("invert(#edc)")) assert_equal("#d8cabd", evaluate("invert(#edc, 10%)")) assert_equal("rgba(245, 235, 225, 0.5)", evaluate("invert(rgba(10, 20, 30, 0.5))")) assert_equal("rgba(34, 42, 50, 0.5)", evaluate("invert(rgba(10, 20, 30, 0.5), 10%)")) assert_equal("invert(20%)", evaluate("invert(20%)")) end def test_invert_tests_types assert_error_message("$color: \"foo\" is not a color for `invert'", "invert(\"foo\")") assert_error_message("$weight: \"foo\" is not a number for `invert'", "invert(#edc, \"foo\")") end def test_invert_tests_bounds assert_error_message("Weight -0.001 must be between 0% and 100% for `invert'", "invert(#edc, -0.001)") assert_error_message("Weight 100.001 must be between 0% and 100% for `invert'", "invert(#edc, 100.001)") end def test_unquote assert_equal('foo', evaluate('unquote("foo")')) assert_equal('foo', evaluate('unquote(foo)')) assert_equal('foo', evaluate('unquote($string: foo)')) assert_warning < Sass::Script::Value::String.new('The variable')) assert_equal("The variable", evaluate("fetch_the_variable()", environment)) end def test_options_on_new_values_fails assert_error_message(< e assert_equal("Function rgba doesn't have an argument named $extra", e.message) end def test_keyword_args_must_have_signature evaluate("no-kw-args($fake: value)") flunk("Expected exception") rescue Sass::SyntaxError => e assert_equal("Function no_kw_args doesn't support keyword arguments", e.message) end def test_keyword_args_with_missing_argument evaluate("rgb($red: 255, $green: 255)") flunk("Expected exception") rescue Sass::SyntaxError => e assert_equal("wrong number of arguments (2 for 3) for `rgb'", e.message) end def test_keyword_args_with_extra_argument evaluate("rgb($red: 255, $green: 255, $blue: 255, $purple: 255)") flunk("Expected exception") rescue Sass::SyntaxError => e assert_equal("Function rgb doesn't have an argument named $purple", e.message) end def test_keyword_args_with_positional_and_keyword_argument evaluate("rgb(255, 255, 255, $red: 255)") flunk("Expected exception") rescue Sass::SyntaxError => e assert_equal("Function rgb was passed argument $red both by position and by name", e.message) end def test_keyword_args_with_keyword_before_positional_argument evaluate("rgb($red: 255, 255, 255)") flunk("Expected exception") rescue Sass::SyntaxError => e assert_equal("Positional arguments must come before keyword arguments.", e.message) end def test_only_var_args assert_equal "only-var-args(2px, 3px, 4px)", evaluate("only-var-args(1px, 2px, 3px)") end def test_only_kw_args assert_equal "only-kw-args(a, b, c)", evaluate("only-kw-args($a: 1, $b: 2, $c: 3)") end def test_unique_id last_id, current_id = nil, evaluate("unique-id()") 50.times do last_id, current_id = current_id, evaluate("unique-id()") assert_match(/u[a-z0-9]{8}/, current_id) refute_equal last_id, current_id end end def test_map_get assert_equal "1", evaluate("map-get((foo: 1, bar: 2), foo)") assert_equal "2", evaluate("map-get((foo: 1, bar: 2), bar)") assert_equal "null", perform("map-get((foo: 1, bar: 2), baz)").to_sass assert_equal "null", perform("map-get((), foo)").to_sass end def test_map_get_checks_type assert_error_message("$map: 12 is not a map for `map-get'", "map-get(12, bar)") end def test_map_merge assert_equal("(foo: 1, bar: 2, baz: 3)", perform("map-merge((foo: 1, bar: 2), (baz: 3))").to_sass) assert_equal("(foo: 1, bar: 2)", perform("map-merge((), (foo: 1, bar: 2))").to_sass) assert_equal("(foo: 1, bar: 2)", perform("map-merge((foo: 1, bar: 2), ())").to_sass) end def test_map_merge_checks_type assert_error_message("$map1: 12 is not a map for `map-merge'", "map-merge(12, (foo: 1))") assert_error_message("$map2: 12 is not a map for `map-merge'", "map-merge((foo: 1), 12)") end def test_map_remove assert_equal("(foo: 1, baz: 3)", perform("map-remove((foo: 1, bar: 2, baz: 3), bar)").to_sass) assert_equal("(foo: 1, baz: 3)", perform("map-remove($map: (foo: 1, bar: 2, baz: 3), $key: bar)").to_sass) assert_equal("()", perform("map-remove((foo: 1, bar: 2, baz: 3), foo, bar, baz)").to_sass) assert_equal("()", perform("map-remove((), foo)").to_sass) assert_equal("()", perform("map-remove((), foo, bar)").to_sass) end def test_map_remove_checks_type assert_error_message("$map: 12 is not a map for `map-remove'", "map-remove(12, foo)") end def test_map_keys assert_equal("foo, bar", perform("map-keys((foo: 1, bar: 2))").to_sass) assert_equal("()", perform("map-keys(())").to_sass) end def test_map_keys_checks_type assert_error_message("$map: 12 is not a map for `map-keys'", "map-keys(12)") end def test_map_values assert_equal("1, 2", perform("map-values((foo: 1, bar: 2))").to_sass) assert_equal("1, 2, 2", perform("map-values((foo: 1, bar: 2, baz: 2))").to_sass) assert_equal("()", perform("map-values(())").to_sass) end def test_map_values_checks_type assert_error_message("$map: 12 is not a map for `map-values'", "map-values(12)") end def test_map_has_key assert_equal "true", evaluate("map-has-key((foo: 1, bar: 1), foo)") assert_equal "false", evaluate("map-has-key((foo: 1, bar: 1), baz)") assert_equal "false", evaluate("map-has-key((), foo)") end def test_map_has_key_checks_type assert_error_message("$map: 12 is not a map for `map-has-key'", "map-has-key(12, foo)") end def test_keywords # The actual functionality is tested in tests where real arglists are passed. assert_error_message("$args: 12 is not a variable argument list for `keywords'", "keywords(12)") assert_error_message( "$args: (1 2 3) is not a variable argument list for `keywords'", "keywords(1 2 3)") end def test_partial_list_of_pairs_doesnt_work_as_a_map assert_raises(Sass::SyntaxError) {evaluate("map-get((foo bar, baz bang, bip), 1)")} assert_raises(Sass::SyntaxError) {evaluate("map-get((foo bar, baz bang, bip bap bop), 1)")} assert_raises(Sass::SyntaxError) {evaluate("map-get((foo bar), 1)")} end def test_assert_unit ctx = Sass::Script::Functions::EvaluationContext.new(Sass::Environment.new(nil, {})) ctx.assert_unit Sass::Script::Value::Number.new(10, ["px"], []), "px" ctx.assert_unit Sass::Script::Value::Number.new(10, [], []), nil begin ctx.assert_unit Sass::Script::Value::Number.new(10, [], []), "px" fail rescue ArgumentError => e assert_equal "Expected 10 to have a unit of px", e.message end begin ctx.assert_unit Sass::Script::Value::Number.new(10, ["px"], []), nil fail rescue ArgumentError => e assert_equal "Expected 10px to be unitless", e.message end begin ctx.assert_unit Sass::Script::Value::Number.new(10, [], []), "px", "arg" fail rescue ArgumentError => e assert_equal "Expected $arg to have a unit of px but got 10", e.message end begin ctx.assert_unit Sass::Script::Value::Number.new(10, ["px"], []), nil, "arg" fail rescue ArgumentError => e assert_equal "Expected $arg to be unitless but got 10px", e.message end end def test_call_with_positional_arguments # TODO: Remove this block in 4.0 Sass::Util.silence_sass_warnings do assert_equal evaluate("lighten(blue, 5%)"), evaluate("call(lighten, blue, 5%)") end assert_equal evaluate("lighten(blue, 5%)"), evaluate("call(get-function(lighten), blue, 5%)") end def test_call_with_keyword_arguments # TODO: Remove this block in 4.0 Sass::Util.silence_sass_warnings do assert_equal( evaluate("lighten($color: blue, $amount: 5%)"), evaluate("call(lighten, $color: blue, $amount: 5%)")) end assert_equal( evaluate("lighten($color: blue, $amount: 5%)"), evaluate("call(get-function(lighten), $color: blue, $amount: 5%)")) end def test_call_with_keyword_and_positional_arguments # TODO: Remove this block in 4.0 Sass::Util.silence_sass_warnings do assert_equal( evaluate("lighten(blue, $amount: 5%)"), evaluate("call(lighten, blue, $amount: 5%)")) end assert_equal( evaluate("lighten(blue, $amount: 5%)"), evaluate("call(get-function(lighten), blue, $amount: 5%)")) end def test_call_with_dynamic_name # TODO: Remove this block in 4.0 Sass::Util.silence_sass_warnings do assert_equal( evaluate("lighten($color: blue, $amount: 5%)"), evaluate("call($fn, $color: blue, $amount: 5%)", env("fn" => Sass::Script::Value::String.new("lighten")))) end assert_equal( evaluate("lighten($color: blue, $amount: 5%)"), evaluate("call($fn, $color: blue, $amount: 5%)", env("fn" => Sass::Script::Value::Function.new( Sass::Callable.new("lighten", nil, nil, nil, nil, nil, "function", :builtin))))) end # TODO: Remove this test in 4.0 def test_call_uses_local_scope Sass::Util.silence_sass_warnings do assert_equal <= 0, "Random number was below 0" assert result.value <= 1, "Random number was above 1" end def test_random_with_limit_one # Passing 1 as the limit should always return 1, since limit calls return # integers from 1 to the argument, so when the argument is 1, its a predicatble # outcome assert "1", evaluate("random(1)") end def test_random_with_limit_too_low assert_error_message("$limit 0 must be greater than or equal to 1 for `random'", "random(0)") end def test_random_with_non_integer_limit assert_error_message("Expected $limit to be an integer but got 1.5 for `random'", "random(1.5)") end # Regression test for #1638. def test_random_with_float_integer_limit result = perform("random(1.0)") assert_kind_of Sass::Script::Number, result assert result.value >= 0, "Random number was below 0" assert result.value <= 1, "Random number was above 1" end # This could *possibly* fail, but exceedingly unlikely def test_random_is_semi_unique if Sass::Script::Functions.instance_variable_defined?("@random_number_generator") Sass::Script::Functions.send(:remove_instance_variable, "@random_number_generator") end refute_equal evaluate("random()"), evaluate("random()") end def test_deprecated_arg_names assert_warning < .bar\" to \".foo\" for `selector-append'", "selector-append('.foo', '> .bar')") assert_error_message("Can't append \"*.bar\" to \".foo\" for `selector-append'", "selector-append('.foo', '*.bar')") assert_error_message("Can't append \"ns|suffix\" to \".foo\" for `selector-append'", "selector-append('.foo', 'ns|suffix')") end def test_selector_extend assert_equal(".foo .x, .foo .a .bar, .a .foo .bar", evaluate("selector-extend('.foo .x', '.x', '.a .bar')")) assert_equal(".foo .x, .foo .bang, .x.bar, .bar.bang", evaluate("selector-extend('.foo .x, .x.bar', '.x', '.bang')")) assert_equal(".y .x, .foo .x, .y .foo, .foo .foo", evaluate("selector-extend('.y .x', '.x, .y', '.foo')")) assert_equal(".foo .x, .foo .bar, .foo .bang", evaluate("selector-extend('.foo .x', '.x', '.bar, .bang')")) assert_equal(".foo.x, .foo", evaluate("selector-extend('.foo.x', '.x', '.foo')")) end def test_selector_extend_checks_types assert_error_message("$selector: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `selector-extend'", "selector-extend(12, '.foo', '.bar')") assert_error_message("$extendee: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `selector-extend'", "selector-extend('.foo', 12, '.bar')") assert_error_message("$extender: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `selector-extend'", "selector-extend('.foo', '.bar', 12)") end def test_selector_extend_errors assert_error_message("Can't extend .bar .baz: can't extend nested selectors for " + "`selector-extend'", "selector-extend('.foo', '.bar .baz', '.bang')") assert_error_message("Can't extend >: invalid selector for `selector-extend'", "selector-extend('.foo', '>', '.bang')") assert_error_message(".bang > can't extend: invalid selector for `selector-extend'", "selector-extend('.foo', '.bar', '.bang >')") end def test_selector_replace assert_equal(".bar", evaluate("selector-replace('.foo', '.foo', '.bar')")) assert_equal(".foo.baz", evaluate("selector-replace('.foo.bar', '.bar', '.baz')")) assert_equal(".a .foo.baz", evaluate("selector-replace('.foo.bar', '.bar', '.a .baz')")) # These shouldn't warn since we still support componud targets for selector # functions. assert_no_warning {assert_equal(".foo.bar", evaluate("selector-replace('.foo.bar', '.baz.bar', '.qux')"))} assert_no_warning {assert_equal(".bar.qux", evaluate("selector-replace('.foo.bar.baz', '.foo.baz', '.qux')"))} assert_equal(":not(.bar)", evaluate("selector-replace(':not(.foo)', '.foo', '.bar')")) assert_equal(".bar", evaluate("selector-replace(':not(.foo)', ':not(.foo)', '.bar')")) end def test_selector_replace_checks_types assert_error_message("$selector: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `selector-replace'", "selector-replace(12, '.foo', '.bar')") assert_error_message("$original: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `selector-replace'", "selector-replace('.foo', 12, '.bar')") assert_error_message("$replacement: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `selector-replace'", "selector-replace('.foo', '.bar', 12)") end def test_selector_replace_errors assert_error_message("Can't extend .bar .baz: can't extend nested selectors for " + "`selector-replace'", "selector-replace('.foo', '.bar .baz', '.bang')") assert_error_message("Can't extend >: invalid selector for `selector-replace'", "selector-replace('.foo', '>', '.bang')") assert_error_message(".bang > can't extend: invalid selector for `selector-replace'", "selector-replace('.foo', '.bar', '.bang >')") end def test_selector_unify assert_equal(".foo", evaluate("selector-unify('.foo', '.foo')")) assert_equal(".foo.bar", evaluate("selector-unify('.foo', '.bar')")) assert_equal(".foo.bar.baz", evaluate("selector-unify('.foo.bar', '.bar.baz')")) assert_equal(".a .b .foo.bar, .b .a .foo.bar", evaluate("selector-unify('.a .foo', '.b .bar')")) assert_equal(".a .foo.bar", evaluate("selector-unify('.a .foo', '.a .bar')")) assert_equal("", evaluate("selector-unify('p', 'a')")) assert_equal("", evaluate("selector-unify('.foo >', '.bar')")) assert_equal("", evaluate("selector-unify('.foo', '.bar >')")) assert_equal(".foo.baz, .foo.bang, .bar.baz, .bar.bang", evaluate("selector-unify('.foo, .bar', '.baz, .bang')")) end def test_selector_unify_checks_types assert_error_message("$selector1: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `selector-unify'", "selector-unify(12, '.foo')") assert_error_message("$selector2: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `selector-unify'", "selector-unify('.foo', 12)") end def test_simple_selectors assert_equal('(.foo,)', evaluate("inspect(simple-selectors('.foo'))")) assert_equal('.foo, .bar', evaluate("inspect(simple-selectors('.foo.bar'))")) assert_equal('.foo, .bar, :pseudo("flip, flap")', evaluate("inspect(simple-selectors('.foo.bar:pseudo(\"flip, flap\")'))")) end def test_simple_selectors_checks_types assert_error_message("$selector: 12 is not a string for `simple-selectors'", "simple-selectors(12)") end def test_simple_selectors_errors assert_error_message("$selector: \".foo .bar\" is not a compound selector for `simple-selectors'", "simple-selectors('.foo .bar')") assert_error_message("$selector: \".foo,.bar\" is not a compound selector for `simple-selectors'", "simple-selectors('.foo,.bar')") assert_error_message("$selector: \".#\" is not a valid selector: Invalid CSS after \".\": " + "expected class name, was \"#\" for `simple-selectors'", "simple-selectors('.#')") end def test_is_superselector assert_equal("true", evaluate("is-superselector('.foo', '.foo.bar')")) assert_equal("false", evaluate("is-superselector('.foo.bar', '.foo')")) assert_equal("true", evaluate("is-superselector('.foo', '.foo')")) assert_equal("true", evaluate("is-superselector('.bar', '.foo .bar')")) assert_equal("false", evaluate("is-superselector('.foo .bar', '.bar')")) assert_equal("true", evaluate("is-superselector('.foo .bar', '.foo > .bar')")) assert_equal("false", evaluate("is-superselector('.foo > .bar', '.foo .bar')")) end def test_is_superselector_checks_types assert_error_message("$super: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `is-superselector'", "is-superselector(12, '.foo')") assert_error_message("$sub: 12 is not a valid selector: it must be a string,\n" + "a list of strings, or a list of lists of strings for `is-superselector'", "is-superselector('.foo', 12)") end ## Regression Tests def test_inspect_nested_empty_lists assert_equal "() ()", evaluate("inspect(() ())") end def test_saturation_bounds assert_equal "#fbfdff", evaluate("hsl(hue(#fbfdff), saturation(#fbfdff), lightness(#fbfdff))") end private def env(hash = {}, parent = nil) env = Sass::Environment.new(parent) hash.each {|k, v| env.set_var(k, v)} env end def evaluate(value, environment = env) result = perform(value, environment) assert_kind_of Sass::Script::Value::Base, result return result.to_s end def perform(value, environment = env) Sass::Script::Parser.parse(value, 1, 0, {:filename => "#{test_name}_inline.scss"}).perform(environment) end def render(sass, options = {}) options[:syntax] ||= :scss munge_filename options options[:importer] ||= MockImporter.new Sass::Engine.new(sass, options).render end def assert_error_message(message, value) evaluate(value) flunk("Error message expected but not raised: #{message}") rescue Sass::SyntaxError => e assert_equal(message, e.message) end end ruby-sass-3.7.4/test/sass/importer_test.rb000077500000000000000000000275751345125207600206620ustar00rootroot00000000000000require File.dirname(__FILE__) + '/../test_helper' require File.dirname(__FILE__) + '/test_helper' require 'mock_importer' require 'sass/plugin' class ImporterTest < MiniTest::Test class FruitImporter < Sass::Importers::Base def find(name, context = nil) fruit = parse(name) return unless fruit color = case fruit when "apple" "red" when "orange" "orange" else "blue" end contents = %Q{ $#{fruit}-color: #{color} !default; @mixin #{fruit} { color: $#{fruit}-color; } } Sass::Engine.new(contents, :filename => name, :syntax => :scss, :importer => self) end def key(name, context) [self.class.name, name] end def public_url(name, sourcemap_directory = nil) "http://#{parse(name)}.example.com/style.scss" end private def parse(name) name[%r{fruits/(\w+)(\.s[ac]ss)?}, 1] end end class NoPublicUrlImporter < FruitImporter def public_url(name, sourcemap_directory = nil) nil end private def parse(name) name[%r{ephemeral/(\w+)(\.s[ac]ss)?}, 1] end end # This class proves that you can override the extension scheme for importers class ReversedExtImporter < Sass::Importers::Filesystem def extensions {"sscs" => :scss, "ssas" => :sass} end end # This importer maps one import to another import # based on the mappings passed to importer's constructor. class IndirectImporter < Sass::Importers::Base def initialize(mappings, mtimes) @mappings = mappings @mtimes = mtimes end def find_relative(uri, base, options) nil end def find(name, options) if @mappings.has_key?(name) Sass::Engine.new( %Q[@import "#{@mappings[name]}";], options.merge( :filename => name, :syntax => :scss, :importer => self ) ) end end def mtime(uri, options) @mtimes.fetch(uri, @mtimes.has_key?(uri) ? Time.now : nil) end def key(uri, options) [self.class.name, uri] end def to_s "IndirectImporter(#{@mappings.keys.join(", ")})" end end # This importer maps the import to single class # based on the mappings passed to importer's constructor. class ClassImporter < Sass::Importers::Base def initialize(mappings, mtimes) @mappings = mappings @mtimes = mtimes end def find_relative(uri, base, options) nil end def find(name, options) if @mappings.has_key?(name) Sass::Engine.new( %Q[.#{name}{#{@mappings[name]}}], options.merge( :filename => name, :syntax => :scss, :importer => self ) ) end end def mtime(uri, options) @mtimes.fetch(uri, @mtimes.has_key?(uri) ? Time.now : nil) end def key(uri, options) [self.class.name, uri] end def to_s "ClassImporter(#{@mappings.keys.join(", ")})" end end def test_can_resolve_generated_imports scss_file = %Q{ $pear-color: green; @import "fruits/apple"; @import "fruits/orange"; @import "fruits/pear"; .apple { @include apple; } .orange { @include orange; } .pear { @include pear; } } css_file = < :compact, :load_paths => [FruitImporter.new], :syntax => :scss} assert_equal css_file, Sass::Engine.new(scss_file, options).render end def test_extension_overrides FileUtils.mkdir_p(absolutize("tmp")) open(absolutize("tmp/foo.ssas"), "w") {|f| f.write(".foo\n reversed: true\n")} open(absolutize("tmp/bar.sscs"), "w") {|f| f.write(".bar {reversed: true}\n")} scss_file = %Q{ @import "foo", "bar"; @import "foo.ssas", "bar.sscs"; } css_file = < :compact, :load_paths => [ReversedExtImporter.new(absolutize("tmp"))], :syntax => :scss} assert_equal css_file, Sass::Engine.new(scss_file, options).render ensure FileUtils.rm_rf(absolutize("tmp")) end def test_staleness_check_across_importers file_system_importer = Sass::Importers::Filesystem.new(fixture_dir) # Make sure the first import is older indirect_importer = IndirectImporter.new({"apple" => "pear"}, {"apple" => Time.now - 1}) # Make css file is newer so the dependencies are the only way for the css file to be out of date. FileUtils.touch(fixture_file("test_staleness_check_across_importers.css")) # Make sure the first import is older class_importer = ClassImporter.new({"pear" => %Q{color: green;}}, {"pear" => Time.now + 1}) options = { :style => :compact, :filename => fixture_file("test_staleness_check_across_importers.scss"), :importer => file_system_importer, :load_paths => [file_system_importer, indirect_importer, class_importer], :syntax => :scss } assert_equal File.read(fixture_file("test_staleness_check_across_importers.css")), Sass::Engine.new(File.read(fixture_file("test_staleness_check_across_importers.scss")), options).render checker = Sass::Plugin::StalenessChecker.new(options) assert checker.stylesheet_needs_update?( fixture_file("test_staleness_check_across_importers.css"), fixture_file("test_staleness_check_across_importers.scss"), file_system_importer ) end def test_source_map_with_only_css_uri_supports_public_url_imports fruit_importer = FruitImporter.new options = { :filename => 'fruits/orange', :importer => fruit_importer, :syntax => :scss } engine = Sass::Engine.new(< 'css_uri') { "version": 3, "mappings": "AAAA,QAAS;EACP,KAAK,EAAE,IAAI", "sources": ["http://orange.example.com/style.scss"], "names": [], "file": "css_uri" } JSON end def test_source_map_with_only_css_uri_can_have_no_public_url ephemeral_importer = NoPublicUrlImporter.new mock_importer = MockImporter.new def mock_importer.public_url(name, sourcemap_directory = nil) "source_uri" end options = { :filename => filename_for_test, :sourcemap_filename => sourcemap_filename_for_test, :importer => mock_importer, :syntax => :scss, :load_paths => [ephemeral_importer], :cache => false } engine = Sass::Engine.new(< 'css_uri') assert_equal < filename_for_test(:scss), :sourcemap_filename => sourcemap_filename_for_test, :importer => file_system_importer, :syntax => :scss } engine = Sass::Engine.new(< 'css_uri') { "version": 3, "mappings": "AAAA,IAAK;EAAC,CAAC,EAAE,CAAC", "sources": ["#{uri}"], "names": [], "file": "css_uri" } JSON end def test_source_map_with_css_uri_and_css_path_falls_back_to_file_uris file_system_importer = Sass::Importers::Filesystem.new('.') options = { :filename => filename_for_test(:scss), :sourcemap_filename => sourcemap_filename_for_test, :importer => file_system_importer, :syntax => :scss } engine = Sass::Engine.new(< 'css_uri', :css_path => 'css_path') { "version": 3, "mappings": "AAAA,IAAK;EAAC,CAAC,EAAE,CAAC", "sources": ["#{uri}"], "names": [], "file": "css_uri" } JSON end def test_source_map_with_css_uri_and_sourcemap_path_supports_filesystem_importer file_system_importer = Sass::Importers::Filesystem.new('.') css_uri = 'css_uri' sourcemap_path = 'map/style.map' options = { :filename => 'sass/style.scss', :sourcemap_filename => sourcemap_path, :importer => file_system_importer, :syntax => :scss } engine = Sass::Engine.new(< css_uri, :sourcemap_path => sourcemap_path) { "version": 3, "mappings": "AAAA,IAAK;EAAC,CAAC,EAAE,CAAC", "sources": ["../sass/style.scss"], "names": [], "file": "css_uri" } JSON end def test_source_map_with_css_path_and_sourcemap_path_supports_file_system_importer file_system_importer = Sass::Importers::Filesystem.new('.') sass_path = 'sass/style.scss' css_path = 'static/style.css' sourcemap_path = 'map/style.map' options = { :filename => sass_path, :sourcemap_filename => sourcemap_path, :importer => file_system_importer, :syntax => :scss } engine = Sass::Engine.new(< css_path, :sourcemap_path => sourcemap_path) { "version": 3, "mappings": "AAAA,IAAK;EAAC,CAAC,EAAE,CAAC", "sources": ["../sass/style.scss"], "names": [], "file": "../static/style.css" } JSON end def test_render_with_sourcemap_requires_filename file_system_importer = Sass::Importers::Filesystem.new('.') engine = Sass::Engine.new(".foo {a: b}", :syntax => :scss, :importer => file_system_importer) assert_raise_message(Sass::SyntaxError, < "color: green;"}, {"pear" => Time.now}) assert_raise_message(Sass::SyntaxError, < [ [], [:trace, :debug, :info, :warn, :error]], :debug => [ [:trace], [:debug, :info, :warn, :error]], :info => [ [:trace, :debug], [:info, :warn, :error]], :warn => [ [:trace, :debug, :info], [:warn, :error]], :error => [ [:trace, :debug, :info, :warn], [:error]] } logged_levels.each do |level, (should_not_be_logged, should_be_logged)| logger = Sass::Logger::Base.new(level) should_not_be_logged.each do |should_level| assert !logger.logging_level?(should_level) end should_be_logged.each do |should_level| assert logger.logging_level?(should_level) end end end def test_logging_can_be_disabled logger = InterceptedLogger.new logger.error("message #1") assert_equal 1, logger.messages.size logger.reset! logger.disabled = true logger.error("message #2") assert_equal 0, logger.messages.size end end ruby-sass-3.7.4/test/sass/mock_importer.rb000066400000000000000000000016161345125207600206150ustar00rootroot00000000000000class MockImporter < Sass::Importers::Base def initialize(name = "mock") @name = name @imports = Hash.new({}) end def find_relative(uri, base, options) nil end def find(uri, options) contents = @imports[uri][:contents] return unless contents options[:syntax] = @imports[uri][:syntax] options[:filename] = uri options[:importer] = self @imports[uri][:engine] = Sass::Engine.new(contents, options) end def mtime(uri, options) @imports[uri][:mtime] end def key(uri, options) ["mock", uri] end def to_s @name end # Methods for testing def add_import(uri, contents, syntax = :scss, mtime = Time.now - 10) @imports[uri] = { :contents => contents, :mtime => mtime, :syntax => syntax } end def touch(uri) @imports[uri][:mtime] = Time.now end def engine(uri) @imports[uri][:engine] end end ruby-sass-3.7.4/test/sass/more_results/000077500000000000000000000000001345125207600201355ustar00rootroot00000000000000ruby-sass-3.7.4/test/sass/more_results/more1.css000066400000000000000000000005421345125207600216730ustar00rootroot00000000000000body { font: Arial; background: blue; } #page { width: 700px; height: 100; } #page #header { height: 300px; } #page #header h1 { font-size: 50px; color: blue; } #content.user.show #container.top #column.left { width: 100px; } #content.user.show #container.top #column.right { width: 600px; } #content.user.show #container.bottom { background: brown; } ruby-sass-3.7.4/test/sass/more_results/more1_with_line_comments.css000066400000000000000000000012761345125207600256470ustar00rootroot00000000000000/* line 3, ../more_templates/more1.sass */ body { font: Arial; background: blue; } /* line 7, ../more_templates/more1.sass */ #page { width: 700px; height: 100; } /* line 10, ../more_templates/more1.sass */ #page #header { height: 300px; } /* line 12, ../more_templates/more1.sass */ #page #header h1 { font-size: 50px; color: blue; } /* line 18, ../more_templates/more1.sass */ #content.user.show #container.top #column.left { width: 100px; } /* line 20, ../more_templates/more1.sass */ #content.user.show #container.top #column.right { width: 600px; } /* line 22, ../more_templates/more1.sass */ #content.user.show #container.bottom { background: brown; } ruby-sass-3.7.4/test/sass/more_results/more_import.css000066400000000000000000000017141345125207600232060ustar00rootroot00000000000000@import url(basic.css); @import url(../results/complex.css); imported { otherconst: hello; myconst: goodbye; pre-mixin: here; } body { font: Arial; background: blue; } #page { width: 700px; height: 100; } #page #header { height: 300px; } #page #header h1 { font-size: 50px; color: blue; } #content.user.show #container.top #column.left { width: 100px; } #content.user.show #container.top #column.right { width: 600px; } #content.user.show #container.bottom { background: brown; } midrule { inthe: middle; } body { font: Arial; background: blue; } #page { width: 700px; height: 100; } #page #header { height: 300px; } #page #header h1 { font-size: 50px; color: blue; } #content.user.show #container.top #column.left { width: 100px; } #content.user.show #container.top #column.right { width: 600px; } #content.user.show #container.bottom { background: brown; } #foo { background-color: #baf; } nonimported { myconst: hello; otherconst: goodbye; post-mixin: here; } ruby-sass-3.7.4/test/sass/more_templates/000077500000000000000000000000001345125207600204325ustar00rootroot00000000000000ruby-sass-3.7.4/test/sass/more_templates/_more_partial.sass000066400000000000000000000000361345125207600241410ustar00rootroot00000000000000#foo :background-color #baf ruby-sass-3.7.4/test/sass/more_templates/more1.sass000066400000000000000000000004651345125207600223550ustar00rootroot00000000000000 body font: Arial background: blue #page width: 700px height: 100 #header height: 300px h1 font-size: 50px color: blue #content.user.show #container.top #column.left width: 100px #column.right width: 600px #container.bottom background: brown ruby-sass-3.7.4/test/sass/more_templates/more_import.sass000066400000000000000000000003001345125207600236520ustar00rootroot00000000000000$preconst: hello =premixin pre-mixin: here @import importee, basic, basic.css, ../results/complex.css, more_partial nonimported myconst: $preconst otherconst: $postconst +postmixin ruby-sass-3.7.4/test/sass/plugin_test.rb000077500000000000000000000413741345125207600203100ustar00rootroot00000000000000require File.dirname(__FILE__) + '/../test_helper' require File.dirname(__FILE__) + '/test_helper' require 'sass/plugin' require 'fileutils' module Sass::Script::Functions def filename filename = options[:filename].gsub(%r{.*((/[^/]+){4})}, '\1') Sass::Script::Value::String.new(filename) end def whatever custom = options[:custom] whatever = custom && custom[:whatever] Sass::Script::Value::String.new(whatever || "incorrect") end end class SassPluginTest < MiniTest::Test @@templates = %w{ complex script parent_ref import scss_import alt subdir/subdir subdir/nested_subdir/nested_subdir options import_content filename_fn import_charset import_charset_ibm866 } @@cache_store = Sass::CacheStores::Memory.new def setup Sass::Util.retry_on_windows {FileUtils.mkdir_p tempfile_loc} Sass::Util.retry_on_windows {FileUtils.mkdir_p tempfile_loc(nil,"more_")} set_plugin_opts check_for_updates! reset_mtimes end def teardown clean_up_sassc Sass::Plugin.reset! Sass::Util.retry_on_windows {FileUtils.rm_r tempfile_loc} Sass::Util.retry_on_windows {FileUtils.rm_r tempfile_loc(nil,"more_")} end @@templates.each do |name| define_method("test_template_renders_correctly (#{name})") do silence_warnings {assert_renders_correctly(name)} end end def test_no_update File.delete(tempfile_loc('basic')) assert_needs_update 'basic' check_for_updates! assert_stylesheet_updated 'basic' end def test_update_needed_when_modified touch 'basic' assert_needs_update 'basic' check_for_updates! assert_stylesheet_updated 'basic' end def test_update_needed_when_dependency_modified touch 'basic' assert_needs_update 'import' check_for_updates! assert_stylesheet_updated 'basic' assert_stylesheet_updated 'import' end def test_update_needed_when_scss_dependency_modified touch 'scss_importee' assert_needs_update 'import' check_for_updates! assert_stylesheet_updated 'scss_importee' assert_stylesheet_updated 'import' end def test_scss_update_needed_when_dependency_modified touch 'basic' assert_needs_update 'scss_import' check_for_updates! assert_stylesheet_updated 'basic' assert_stylesheet_updated 'scss_import' end def test_update_needed_when_nested_import_dependency_modified touch 'basic' assert_needs_update 'nested_import' check_for_updates! assert_stylesheet_updated 'basic' assert_stylesheet_updated 'scss_import' end def test_no_updates_when_always_check_and_always_update_both_false Sass::Plugin.options[:always_update] = false Sass::Plugin.options[:always_check] = false touch 'basic' assert_needs_update 'basic' check_for_updates! # Check it's still stale assert_needs_update 'basic' end def test_full_exception_handling File.delete(tempfile_loc('bork1')) check_for_updates! File.open(tempfile_loc('bork1')) do |file| assert_equal(< { template_loc => tempfile_loc, template_loc(nil,'more_') => tempfile_loc(nil,'more_') } check_for_updates! ['more1', 'more_import'].each { |name| assert_renders_correctly(name, :prefix => 'more_') } end def test_two_template_directories_with_line_annotations set_plugin_opts :line_comments => true, :style => :nested, :template_location => { template_loc => tempfile_loc, template_loc(nil,'more_') => tempfile_loc(nil,'more_') } check_for_updates! assert_renders_correctly('more1_with_line_comments', 'more1', :prefix => 'more_') end def test_doesnt_render_partials assert !File.exist?(tempfile_loc('_partial')) end def test_template_location_array assert_equal [[template_loc, tempfile_loc]], Sass::Plugin.template_location_array end def test_add_template_location Sass::Plugin.add_template_location(template_loc(nil, "more_"), tempfile_loc(nil, "more_")) assert_equal( [[template_loc, tempfile_loc], [template_loc(nil, "more_"), tempfile_loc(nil, "more_")]], Sass::Plugin.template_location_array) touch 'more1', 'more_' touch 'basic' assert_needs_update "more1", "more_" assert_needs_update "basic" check_for_updates! assert_doesnt_need_update "more1", "more_" assert_doesnt_need_update "basic" end def test_remove_template_location Sass::Plugin.add_template_location(template_loc(nil, "more_"), tempfile_loc(nil, "more_")) Sass::Plugin.remove_template_location(template_loc, tempfile_loc) assert_equal( [[template_loc(nil, "more_"), tempfile_loc(nil, "more_")]], Sass::Plugin.template_location_array) touch 'more1', 'more_' touch 'basic' assert_needs_update "more1", "more_" assert_needs_update "basic" check_for_updates! assert_doesnt_need_update "more1", "more_" assert_needs_update "basic" end def test_import_same_name assert_warning < [template_loc(nil, "more_")] touch 'basic', 'more_' assert_needs_update "import" check_for_updates! assert_renders_correctly("import") ensure FileUtils.mv(template_loc("basic", "more_"), template_loc("basic")) end def test_cached_relative_import old_always_update = Sass::Plugin.options[:always_update] Sass::Plugin.options[:always_update] = true check_for_updates! assert_renders_correctly('subdir/subdir') ensure Sass::Plugin.options[:always_update] = old_always_update end def test_cached_if set_plugin_opts :cache_store => Sass::CacheStores::Filesystem.new(tempfile_loc + '/cache') check_for_updates! assert_renders_correctly 'if' check_for_updates! assert_renders_correctly 'if' ensure set_plugin_opts end def test_cached_import_option set_plugin_opts :custom => {:whatever => "correct"} check_for_updates! assert_renders_correctly "cached_import_option" @@cache_store.reset! set_plugin_opts :custom => nil, :always_update => false check_for_updates! assert_renders_correctly "cached_import_option" set_plugin_opts :custom => {:whatever => "correct"}, :always_update => true check_for_updates! assert_renders_correctly "cached_import_option" ensure set_plugin_opts :custom => nil end private def assert_renders_correctly(*arguments) options = arguments.last.is_a?(Hash) ? arguments.pop : {} prefix = options[:prefix] result_name = arguments.shift tempfile_name = arguments.shift || result_name expected_str = File.read(result_loc(result_name, prefix)) actual_str = File.read(tempfile_loc(tempfile_name, prefix)) expected_str = expected_str.force_encoding('IBM866') if result_name == 'import_charset_ibm866' actual_str = actual_str.force_encoding('IBM866') if tempfile_name == 'import_charset_ibm866' expected_lines = expected_str.split("\n") actual_lines = actual_str.split("\n") if actual_lines.first == "/*" && expected_lines.first != "/*" assert(false, actual_lines[0..actual_lines.each_with_index.find {|l, i| l == "*/"}.last].join("\n")) end expected_lines.zip(actual_lines).each_with_index do |pair, line| message = "template: #{result_name}\nline: #{line + 1}" assert_equal(pair.first, pair.last, message) end if expected_lines.size < actual_lines.size assert(false, "#{actual_lines.size - expected_lines.size} Trailing lines found in #{tempfile_name}.css: #{actual_lines[expected_lines.size..-1].join('\n')}") end end def assert_stylesheet_updated(name) assert_doesnt_need_update name # Make sure it isn't an exception expected_lines = File.read(result_loc(name)).split("\n") actual_lines = File.read(tempfile_loc(name)).split("\n") if actual_lines.first == "/*" && expected_lines.first != "/*" assert(false, actual_lines[0..actual_lines.each_with_index.find {|l, i| l == "*/"}.last].join("\n")) end end def assert_callback(name, *expected_args) run = false received_args = nil Sass::Plugin.send("on_#{name}") do |*args| received_args = args run ||= expected_args.zip(received_args).all? do |ea, ra| ea.respond_to?(:call) ? ea.call(ra) : ea == ra end end if block_given? Sass::Util.silence_sass_warnings {yield} else check_for_updates! end assert run, "Expected #{name} callback to be run with arguments:\n #{expected_args.inspect}\nHowever, it got:\n #{received_args.inspect}" end def assert_no_callback(name, *unexpected_args) Sass::Plugin.send("on_#{name}") do |*a| next unless unexpected_args.empty? || a == unexpected_args msg = "Expected #{name} callback not to be run" if !unexpected_args.empty? msg << " with arguments #{unexpected_args.inspect}" elsif !a.empty? msg << ",\n was run with arguments #{a.inspect}" end flunk msg end if block_given? yield else check_for_updates! end end def assert_callbacks(*args) return check_for_updates! if args.empty? assert_callback(*args.pop) {assert_callbacks(*args)} end def assert_no_callbacks(*args) return check_for_updates! if args.empty? assert_no_callback(*args.pop) {assert_no_callbacks(*args)} end def check_for_updates! Sass::Util.silence_sass_warnings do Sass::Plugin.check_for_updates end end def assert_needs_update(*args) assert(Sass::Plugin::StalenessChecker.stylesheet_needs_update?(tempfile_loc(*args), template_loc(*args)), "Expected #{template_loc(*args)} to need an update.") end def assert_doesnt_need_update(*args) assert(!Sass::Plugin::StalenessChecker.stylesheet_needs_update?(tempfile_loc(*args), template_loc(*args)), "Expected #{template_loc(*args)} not to need an update.") end def touch(*args) FileUtils.touch(template_loc(*args)) end def reset_mtimes Sass::Plugin::StalenessChecker.dependencies_cache = {} atime = Time.now mtime = Time.now - 5 Dir["{#{template_loc},#{tempfile_loc}}/**/*.{css,sass,scss}"].each do |f| Sass::Util.retry_on_windows {File.utime(atime, mtime, f)} end end def template_loc(name = nil, prefix = nil) if name scss = absolutize "#{prefix}templates/#{name}.scss" File.exist?(scss) ? scss : absolutize("#{prefix}templates/#{name}.sass") else absolutize "#{prefix}templates" end end def tempfile_loc(name = nil, prefix = nil) if name absolutize "#{prefix}tmp/#{name}.css" else absolutize "#{prefix}tmp" end end def result_loc(name = nil, prefix = nil) if name absolutize "#{prefix}results/#{name}.css" else absolutize "#{prefix}results" end end def set_plugin_opts(overrides = {}) Sass::Plugin.options.merge!( :template_location => template_loc, :css_location => tempfile_loc, :style => :compact, :always_update => true, :never_update => false, :full_exception => true, :cache_store => @@cache_store, :sourcemap => :none ) Sass::Plugin.options.merge!(overrides) end end class Sass::Engine alias_method :old_render, :render def render raise "bork bork bork!" if @template[0] == "{bork now!}" old_render end end ruby-sass-3.7.4/test/sass/results/000077500000000000000000000000001345125207600171135ustar00rootroot00000000000000ruby-sass-3.7.4/test/sass/results/alt.css000066400000000000000000000003551345125207600204100ustar00rootroot00000000000000h1 { float: left; width: 274px; height: 75px; margin: 0; background-repeat: no-repeat; background-image: none; } h1 a:hover, h1 a:visited { color: green; } h1 b:hover { color: red; background-color: green; } h1 const { nosp: 3; sp: 3; } ruby-sass-3.7.4/test/sass/results/basic.css000066400000000000000000000005421345125207600207070ustar00rootroot00000000000000body { font: Arial; background: blue; } #page { width: 700px; height: 100; } #page #header { height: 300px; } #page #header h1 { font-size: 50px; color: blue; } #content.user.show #container.top #column.left { width: 100px; } #content.user.show #container.top #column.right { width: 600px; } #content.user.show #container.bottom { background: brown; } ruby-sass-3.7.4/test/sass/results/cached_import_option.css000066400000000000000000000000661345125207600240200ustar00rootroot00000000000000partial { value: correct; } main { value: correct; } ruby-sass-3.7.4/test/sass/results/compact.css000066400000000000000000000003341345125207600212530ustar00rootroot00000000000000#main { width: 15em; color: #0000ff; } #main p { border-style: dotted; /* Nested comment More nested stuff */ border-width: 2px; } #main .cool { width: 100px; } #left { font-size: 2em; font-weight: bold; float: left; } ruby-sass-3.7.4/test/sass/results/complex.css000066400000000000000000000153371345125207600213050ustar00rootroot00000000000000body { margin: 0; font: 0.85em "Lucida Grande", "Trebuchet MS", Verdana, sans-serif; color: #fff; background: url(/images/global_bg.gif); } #page { width: 900px; margin: 0 auto; background: #440008; border-top-width: 5px; border-top-style: solid; border-top-color: #ff8500; } #header { height: 75px; padding: 0; } #header h1 { float: left; width: 274px; height: 75px; margin: 0; background-image: url(/images/global_logo.gif); /* Crazy nested comment */ background-repeat: no-repeat; text-indent: -9999px; } #header .status { float: right; padding-top: 0.5em; padding-left: 0.5em; padding-right: 0.5em; padding-bottom: 0; } #header .status p { float: left; margin-top: 0; margin-right: 0.5em; margin-bottom: 0; margin-left: 0; } #header .status ul { float: left; margin: 0; padding: 0; } #header .status li { list-style-type: none; display: inline; margin: 0 5px; } #header .status a:link, #header .status a:visited { color: #ff8500; text-decoration: none; } #header .status a:hover { text-decoration: underline; } #header .search { float: right; clear: right; margin: 12px 0 0 0; } #header .search form { margin: 0; } #header .search input { margin: 0 3px 0 0; padding: 2px; border: none; } #menu { clear: both; text-align: right; height: 20px; border-bottom: 5px solid #006b95; background: #00a4e4; } #menu .contests ul { margin: 0 5px 0 0; padding: 0; } #menu .contests ul li { list-style-type: none; margin: 0 5px; padding: 5px 5px 0 5px; display: inline; font-size: 1.1em; color: #fff; background: #00a4e4; } #menu .contests a:link, #menu .contests a:visited { color: #fff; text-decoration: none; font-weight: bold; } #menu .contests a:hover { text-decoration: underline; } #content { clear: both; } #content .container { clear: both; } #content .container .column { float: left; } #content .container .column .right { float: right; } #content a:link, #content a:visited { color: #93d700; text-decoration: none; } #content a:hover { text-decoration: underline; } #content p, #content div { width: 40em; } #content p li, #content p dt, #content p dd, #content div li, #content div dt, #content div dd { color: #ddffdd; background-color: #4792bb; } #content .container.video .column.left { width: 200px; } #content .container.video .column.left .box { margin-top: 10px; } #content .container.video .column.left .box p { margin: 0 1em auto 1em; } #content .container.video .column.left .box.participants img { float: left; margin: 0 1em auto 1em; border: 1px solid #6e000d; border-style: solid; } #content .container.video .column.left .box.participants h2 { margin: 0 0 10px 0; padding: 0.5em; /* The background image is a gif! */ background: #6e000d url(/images/hdr_participant.gif) 2px 2px no-repeat; /* Okay check this out Multiline comments Wow dude I mean seriously, WOW */ text-indent: -9999px; border-top-width: 5px; border-top-style: solid; border-top-color: #a20013; border-right-width: 1px; border-right-style: dotted; } #content .container.video .column.middle { width: 500px; } #content .container.video .column.right { width: 200px; } #content .container.video .column.right .box { margin-top: 0; } #content .container.video .column.right .box p { margin: 0 1em auto 1em; } #content .container.video .column p { margin-top: 0; } #content.contests .container.information .column.right .box { margin: 1em 0; } #content.contests .container.information .column.right .box.videos .thumbnail img { width: 200px; height: 150px; margin-bottom: 5px; } #content.contests .container.information .column.right .box.videos a:link, #content.contests .container.information .column.right .box.videos a:visited { color: #93d700; text-decoration: none; } #content.contests .container.information .column.right .box.videos a:hover { text-decoration: underline; } #content.contests .container.information .column.right .box.votes a { display: block; width: 200px; height: 60px; margin: 15px 0; background: url(/images/btn_votenow.gif) no-repeat; text-indent: -9999px; outline: none; border: none; } #content.contests .container.information .column.right .box.votes h2 { margin: 52px 0 10px 0; padding: 0.5em; background: #6e000d url(/images/hdr_videostats.gif) 2px 2px no-repeat; text-indent: -9999px; border-top: 5px solid #a20013; } #content.contests .container.video .box.videos h2 { margin: 0; padding: 0.5em; background: #6e000d url(/images/hdr_newestclips.gif) 2px 2px no-repeat; text-indent: -9999px; border-top: 5px solid #a20013; } #content.contests .container.video .box.videos table { width: 100; } #content.contests .container.video .box.videos table td { padding: 1em; width: 25; vertical-align: top; } #content.contests .container.video .box.videos table td p { margin: 0 0 5px 0; } #content.contests .container.video .box.videos table td a:link, #content.contests .container.video .box.videos table td a:visited { color: #93d700; text-decoration: none; } #content.contests .container.video .box.videos table td a:hover { text-decoration: underline; } #content.contests .container.video .box.videos .thumbnail { float: left; } #content.contests .container.video .box.videos .thumbnail img { width: 80px; height: 60px; margin: 0 10px 0 0; border: 1px solid #6e000d; } #content .container.comments .column { margin-top: 15px; } #content .container.comments .column.left { width: 600px; } #content .container.comments .column.left .box ol { margin: 0; padding: 0; } #content .container.comments .column.left .box li { list-style-type: none; padding: 10px; margin: 0 0 1em 0; background: #6e000d; border-top: 5px solid #a20013; } #content .container.comments .column.left .box li div { margin-bottom: 1em; } #content .container.comments .column.left .box li ul { text-align: right; } #content .container.comments .column.left .box li ul li { display: inline; border: none; padding: 0; } #content .container.comments .column.right { width: 290px; padding-left: 10px; } #content .container.comments .column.right h2 { margin: 0; padding: 0.5em; background: #6e000d url(/images/hdr_addcomment.gif) 2px 2px no-repeat; text-indent: -9999px; border-top: 5px solid #a20013; } #content .container.comments .column.right .box textarea { width: 290px; height: 100px; border: none; } #footer { margin-top: 10px; padding: 1.2em 1.5em; background: #ff8500; } #footer ul { margin: 0; padding: 0; list-style-type: none; } #footer ul li { display: inline; margin: 0 0.5em; color: #440008; } #footer ul.links { float: left; } #footer ul.links a:link, #footer ul.links a:visited { color: #440008; text-decoration: none; } #footer ul.links a:hover { text-decoration: underline; } #footer ul.copyright { float: right; } .clear { clear: both; } .centered { text-align: center; } img { border: none; } button.short { width: 60px; height: 22px; padding: 0 0 2px 0; color: #fff; border: none; background: url(/images/btn_short.gif) no-repeat; } table { border-collapse: collapse; } ruby-sass-3.7.4/test/sass/results/compressed.css000066400000000000000000000002221345125207600217650ustar00rootroot00000000000000#main{width:15em;color:blue}#main p{border-style:dotted;border-width:2px}#main .cool{width:100px}#left{font-size:2em;font-weight:bold;float:left} ruby-sass-3.7.4/test/sass/results/expanded.css000066400000000000000000000003631345125207600214170ustar00rootroot00000000000000#main { width: 15em; color: #0000ff; } #main p { border-style: dotted; /* Nested comment * More nested stuff */ border-width: 2px; } #main .cool { width: 100px; } #left { font-size: 2em; font-weight: bold; float: left; } ruby-sass-3.7.4/test/sass/results/filename_fn.css000066400000000000000000000005561345125207600220760ustar00rootroot00000000000000filename { imported: /test/sass/templates/_filename_fn_import.scss; } filename { local: /test/sass/templates/filename_fn.scss; local-mixin: /test/sass/templates/filename_fn.scss; local-function: /test/sass/templates/filename_fn.scss; imported-mixin: /test/sass/templates/_filename_fn_import.scss; imported-function: /test/sass/templates/_filename_fn_import.scss; } ruby-sass-3.7.4/test/sass/results/if.css000066400000000000000000000000471345125207600202240ustar00rootroot00000000000000a { branch: if; } b { branch: else; } ruby-sass-3.7.4/test/sass/results/import.css000066400000000000000000000017451345125207600211460ustar00rootroot00000000000000@import url(basic.css); @import url(../results/complex.css); imported { otherconst: hello; myconst: goodbye; pre-mixin: here; } body { font: Arial; background: blue; } #page { width: 700px; height: 100; } #page #header { height: 300px; } #page #header h1 { font-size: 50px; color: blue; } #content.user.show #container.top #column.left { width: 100px; } #content.user.show #container.top #column.right { width: 600px; } #content.user.show #container.bottom { background: brown; } midrule { inthe: middle; } scss { imported: yes; } body { font: Arial; background: blue; } #page { width: 700px; height: 100; } #page #header { height: 300px; } #page #header h1 { font-size: 50px; color: blue; } #content.user.show #container.top #column.left { width: 100px; } #content.user.show #container.top #column.right { width: 600px; } #content.user.show #container.bottom { background: brown; } #foo { background-color: #baf; } nonimported { myconst: hello; otherconst: goodbye; post-mixin: here; } ruby-sass-3.7.4/test/sass/results/import_charset.css000066400000000000000000000001101345125207600226400ustar00rootroot00000000000000@charset "UTF-8"; @import url(foo.css); .foo { a: b; } .bar { a: щ; } ruby-sass-3.7.4/test/sass/results/import_charset_ibm866.css000066400000000000000000000001101345125207600237330ustar00rootroot00000000000000@charset "UTF-8"; @import url(foo.css); .foo { a: b; } .bar { a: щ; } ruby-sass-3.7.4/test/sass/results/import_content.css000066400000000000000000000000131345125207600226630ustar00rootroot00000000000000a { b: c; }ruby-sass-3.7.4/test/sass/results/line_numbers.css000066400000000000000000000021471345125207600223130ustar00rootroot00000000000000/* line 1, ../templates/line_numbers.sass */ foo { bar: baz; } /* line 6, ../templates/importee.sass */ imported { otherconst: 12; myconst: goodbye; } /* line 5, ../templates/line_numbers.sass */ imported squggle { blat: bang; } /* line 3, ../templates/basic.sass */ body { font: Arial; background: blue; } /* line 7, ../templates/basic.sass */ #page { width: 700px; height: 100; } /* line 10, ../templates/basic.sass */ #page #header { height: 300px; } /* line 12, ../templates/basic.sass */ #page #header h1 { font-size: 50px; color: blue; } /* line 18, ../templates/basic.sass */ #content.user.show #container.top #column.left { width: 100px; } /* line 20, ../templates/basic.sass */ #content.user.show #container.top #column.right { width: 600px; } /* line 22, ../templates/basic.sass */ #content.user.show #container.bottom { background: brown; } /* line 13, ../templates/importee.sass */ midrule { inthe: middle; } /* line 12, ../templates/line_numbers.sass */ umph { foo: bar; } /* line 18, ../templates/importee.sass */ umph baz { blat: bang; } ruby-sass-3.7.4/test/sass/results/mixins.css000066400000000000000000000031241345125207600211340ustar00rootroot00000000000000#main { width: 15em; color: #0000ff; } #main p { border-top-width: 2px; border-top-color: #fc0; border-left-width: 1px; border-left-color: #000; -moz-border-radius: 10px; border-style: dotted; border-width: 2px; } #main .cool { width: 100px; } #left { border-top-width: 2px; border-top-color: #fc0; border-left-width: 1px; border-left-color: #000; -moz-border-radius: 10px; font-size: 2em; font-weight: bold; float: left; } #right { border-top-width: 2px; border-top-color: #fc0; border-left-width: 1px; border-left-color: #000; -moz-border-radius: 10px; color: #f00; font-size: 20px; float: right; } .bordered { border-top-width: 2px; border-top-color: #fc0; border-left-width: 1px; border-left-color: #000; -moz-border-radius: 10px; } .complex { color: #f00; font-size: 20px; text-decoration: none; } .complex:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .complex { height: 1px; color: #f00; font-size: 20px; } .more-complex { color: #f00; font-size: 20px; text-decoration: none; display: inline; -webkit-nonsense-top-right: 1px; -webkit-nonsense-bottom-left: 1px; } .more-complex:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } * html .more-complex { height: 1px; color: #f00; font-size: 20px; } .more-complex a:hover { text-decoration: underline; color: #f00; font-size: 20px; border-top-width: 2px; border-top-color: #fc0; border-left-width: 1px; border-left-color: #000; -moz-border-radius: 10px; } ruby-sass-3.7.4/test/sass/results/multiline.css000066400000000000000000000006441345125207600216330ustar00rootroot00000000000000#main, #header { height: 50px; } #main div, #header div { width: 100px; } #main div a span, #main div em span, #header div a span, #header div em span { color: pink; } #one div.nested, #one span.nested, #one p.nested, #two div.nested, #two span.nested, #two p.nested, #three div.nested, #three span.nested, #three p.nested { font-weight: bold; border-color: red; display: block; } ruby-sass-3.7.4/test/sass/results/nested.css000066400000000000000000000006001345125207600211030ustar00rootroot00000000000000#main { width: 15em; color: #0000ff; } #main p { border-style: dotted; /* Nested comment * More nested stuff */ border-width: 2px; } #main .cool { width: 100px; } #left { font-size: 2em; font-weight: bold; float: left; } #right .header { border-style: solid; } #right .body { border-style: dotted; } #right .footer { border-style: dashed; } ruby-sass-3.7.4/test/sass/results/options.css000066400000000000000000000000271345125207600213170ustar00rootroot00000000000000foo { style: compact; }ruby-sass-3.7.4/test/sass/results/parent_ref.css000066400000000000000000000006731345125207600217600ustar00rootroot00000000000000a { color: #000; } a:hover { color: #f00; } p, div { width: 100em; } p foo, div foo { width: 10em; } p:hover, p bar, div:hover, div bar { height: 20em; } #cool { border-style: solid; border-width: 2em; } .ie7 #cool, .ie6 #cool { content: string("Totally not cool."); } .firefox #cool { content: string("Quite cool."); } .wow, .snazzy { font-family: fantasy; } .wow:hover, .wow:visited, .snazzy:hover, .snazzy:visited { font-weight: bold; } ruby-sass-3.7.4/test/sass/results/script.css000066400000000000000000000021331345125207600211300ustar00rootroot00000000000000#main { content: Hello\!; qstr: 'Quo"ted"!'; hstr: Hyph-en\!; width: 30em; background-color: #000; color: #ffa; short-color: #123; named-color: olive; con: "foo" bar 9 hi there "boom"; con2: "noquo" quo; } #main #sidebar { background-color: #00ff98; num-normal: 10; num-dec: 10.2; num-dec0: 99; num-neg: -10; esc: 10\+12; many: 6; order: 7; complex: #4c9db1hi16; } #plus { num-num: 7; num-num-un: 25em; num-num-un2: 23em; num-num-neg: 9.87; num-str: 100px; num-col: #b7b7b7; num-perc: 31%; str-str: "hi there"; str-str2: "hi there"; str-col: "14em solid #123"; str-num: "times: 13"; col-num: #ff7c9e; col-col: #5173ff; } #minus { num-num: 900; col-num: #fafaf5; col-col: #000035; unary-num: -1; unary-const: 10; unary-paren: -11; unary-two: 12; unary-many: 12; unary-crazy: -15; } #times { num-num: 7; num-col: #7496b8; col-num: #092345; col-col: #243648; } #div { num-num: 3.3333333333; num-num2: 3.3333333333; col-num: #092345; col-col: #0b0e10; comp: 1px; } #mod { num-num: 2; col-col: #0f0e05; col-num: #020001; } #const { escaped-quote: \$foo \!bar; default: Hello\! !important; } #regression { a: 4; } ruby-sass-3.7.4/test/sass/results/scss_import.css000066400000000000000000000017451345125207600222010ustar00rootroot00000000000000@import url(basic.css); @import url(../results/complex.css); imported { otherconst: hello; myconst: goodbye; pre-mixin: here; } body { font: Arial; background: blue; } #page { width: 700px; height: 100; } #page #header { height: 300px; } #page #header h1 { font-size: 50px; color: blue; } #content.user.show #container.top #column.left { width: 100px; } #content.user.show #container.top #column.right { width: 600px; } #content.user.show #container.bottom { background: brown; } midrule { inthe: middle; } scss { imported: yes; } body { font: Arial; background: blue; } #page { width: 700px; height: 100; } #page #header { height: 300px; } #page #header h1 { font-size: 50px; color: blue; } #content.user.show #container.top #column.left { width: 100px; } #content.user.show #container.top #column.right { width: 600px; } #content.user.show #container.bottom { background: brown; } #foo { background-color: #baf; } nonimported { myconst: hello; otherconst: goodbye; post-mixin: here; } ruby-sass-3.7.4/test/sass/results/scss_importee.css000066400000000000000000000000321345125207600224770ustar00rootroot00000000000000scss { imported: yes; } ruby-sass-3.7.4/test/sass/results/subdir/000077500000000000000000000000001345125207600204035ustar00rootroot00000000000000ruby-sass-3.7.4/test/sass/results/subdir/nested_subdir/000077500000000000000000000000001345125207600232355ustar00rootroot00000000000000ruby-sass-3.7.4/test/sass/results/subdir/nested_subdir/nested_subdir.css000066400000000000000000000000251345125207600265760ustar00rootroot00000000000000#pi { width: 314px; }ruby-sass-3.7.4/test/sass/results/subdir/subdir.css000066400000000000000000000001141345125207600224010ustar00rootroot00000000000000#nested { relative: true; } #subdir { font-size: 20px; font-weight: bold; }ruby-sass-3.7.4/test/sass/results/units.css000066400000000000000000000002541345125207600207700ustar00rootroot00000000000000b { foo: 5px; bar: 24px; baz: 66.6666666667%; many-units: 32em; mm: 15mm; pc: 2pc; pt: -72pt; inches: 2in; more-inches: 3.5in; mixed: 2.0416666667in; } ruby-sass-3.7.4/test/sass/results/warn.css000066400000000000000000000000001345125207600205620ustar00rootroot00000000000000ruby-sass-3.7.4/test/sass/results/warn_imported.css000066400000000000000000000000001345125207600224650ustar00rootroot00000000000000ruby-sass-3.7.4/test/sass/script_conversion_test.rb000077500000000000000000000257451345125207600225670ustar00rootroot00000000000000# -*- coding: utf-8 -*- require File.dirname(__FILE__) + '/../test_helper' require 'sass/engine' class SassScriptConversionTest < MiniTest::Test def test_bool assert_renders "true" assert_renders "false" end def test_color assert_renders "#abcdef" assert_renders "blue" assert_renders "rgba(0, 1, 2, 0.2)" assert_renders "#abc" assert_renders "#0000ff" end def test_number assert_renders "10" assert_renders "10.35" assert_renders "12px" assert_renders "12.45px" assert_equal "12.3456789013", render("12.34567890129") end def test_string assert_renders '"foo"' assert_renders '"bar baz"' assert_equal '"baz bang"', render("'baz bang'") end def test_string_quotes assert_equal "'quote\"quote'", render('"quote\\"quote"') assert_equal '"quote\'quote"', render("'quote\\'quote'") assert_renders '"quote\'quote\\"quote"' assert_equal '"quote\'quote\\"quote"', render("'quote\\'quote\"quote'") end def test_string_escapes assert_renders '"foo\\\\bar"' end def test_funcall assert_renders "foo(true, blue)" assert_renders "hsla(20deg, 30%, 50%, 0.3)" assert_renders "blam()" assert_renders "-\xC3\xBFoo(12px)" assert_renders "-foo(12px)" end def test_funcall_with_keyword_args assert_renders "foo(arg1, arg2, $karg1: val, $karg2: val2)" assert_renders "foo($karg1: val, $karg2: val2)" end def test_funcall_with_hyphen_conversion_keyword_arg assert_renders "foo($a-b_c: val)" end def test_url assert_renders "url(foo.gif)" assert_renders "url($var)" assert_renders "url(\#{$var}/flip.gif)" end def test_variable assert_renders "$foo-bar" assert_renders "$flaznicate" end def test_null assert_renders "null" end def test_space_list assert_renders "foo bar baz" assert_renders "foo (bar baz) bip" assert_renders "foo (bar, baz) bip" end def test_comma_list assert_renders "foo, bar, baz" assert_renders "foo, (bar, baz), bip" assert_renders "foo, bar baz, bip" end def test_space_list_adds_parens_for_clarity assert_renders "(1 + 1) (2 / 4) (3 * 5)" end def test_comma_list_doesnt_add_parens assert_renders "1 + 1, 2 / 4, 3 * 5" end def test_empty_list assert_renders "()" end def test_list_in_args assert_renders "foo((a, b, c))" assert_renders "foo($arg: (a, b, c))" assert_renders "foo(a, b, (a, b, c)...)" end def test_singleton_list assert_renders "(1,)" assert_renders "(1 2 3,)" assert_renders "((1, 2, 3),)" end def test_map assert_renders "(foo: bar)" assert_renders "(foo: bar, baz: bip)" assert_renders "(foo: bar, baz: (bip: bap))" end def test_map_in_list assert_renders "(foo: bar) baz" assert_renders "(foo: bar), (baz: bip)" end def test_list_in_map assert_renders "(foo: bar baz)" assert_renders "(foo: (bar, baz), bip: bop)" end def test_selector assert_renders "&" end def self.test_precedence(outer, inner) op_outer = Sass::Script::Lexer::OPERATORS_REVERSE[outer] op_inner = Sass::Script::Lexer::OPERATORS_REVERSE[inner] class_eval < true)) node.to_sass end end ruby-sass-3.7.4/test/sass/script_test.rb000077500000000000000000001443321345125207600203140ustar00rootroot00000000000000# -*- coding: utf-8 -*- require File.dirname(__FILE__) + '/../test_helper' require 'sass/engine' module Sass::Script::Functions::UserFunctions def assert_options(val) val.options[:foo] Sass::Script::Value::String.new("Options defined!") end def arg_error assert_options end end module Sass::Script::Functions include Sass::Script::Functions::UserFunctions end class SassScriptTest < MiniTest::Test include Sass::Script def test_color_clamps_input assert_equal 0, Sass::Script::Value::Color.new([1, 2, -1]).blue assert_equal 255, Sass::Script::Value::Color.new([256, 2, 3]).red end def test_color_clamps_rgba_input assert_equal 1, Sass::Script::Value::Color.new([1, 2, 3, 1.1]).alpha assert_equal 0, Sass::Script::Value::Color.new([1, 2, 3, -0.1]).alpha end def test_color_from_hex assert_equal Sass::Script::Value::Color.new([0,0,0]), Sass::Script::Value::Color.from_hex('000000') assert_equal Sass::Script::Value::Color.new([0,0,0]), Sass::Script::Value::Color.from_hex('#000000') end def test_string_escapes assert_equal "'", resolve("\"'\"") assert_equal '"', resolve("\"\\\"\"") assert_equal "\\", resolve("\"\\\\\"") assert_equal "☃", resolve("\"\\2603\"") assert_equal "☃f", resolve("\"\\2603 f\"") assert_equal "☃x", resolve("\"\\2603x\"") assert_equal "\\2603", resolve("\"\\\\2603\"") assert_equal "\#{foo}", resolve("\"\\\#{foo}\"") # U+FFFD is the replacement character, "�". assert_equal [0xFFFD].pack("U"), resolve("\"\\0\"") assert_equal [0xFFFD].pack("U"), resolve("\"\\FFFFFF\"") assert_equal [0xFFFD].pack("U"), resolve("\"\\D800\"") assert_equal [0xD7FF].pack("U"), resolve("\"\\D7FF\"") assert_equal [0xFFFD].pack("U"), resolve("\"\\DFFF\"") assert_equal [0xE000].pack("U"), resolve("\"\\E000\"") end def test_string_escapes_are_resolved_before_operators assert_equal "true", resolve('"abc" == "\61\62\63"') end def test_string_quote assert_equal '"foo"', resolve_quoted('"foo"') assert_equal "'f\"oo'", resolve_quoted('"f\"oo"') assert_equal "\"f'oo\"", resolve_quoted("'f\\'oo'") assert_equal "\"f'o\\\"o\"", resolve_quoted("'f\\'o\"o'") assert_equal '"foo bar"', resolve_quoted('"foo\20 bar"') assert_equal '"foo\a bar"', resolve_quoted('"foo\a bar"') assert_equal '"x\ay"', resolve_quoted('"x\a y"') assert_equal '"\a "', resolve_quoted('"\a\20"') assert_equal '"\a abcdef"', resolve_quoted('"\a abcdef"') assert_equal '"☃abcdef"', resolve_quoted('"\2603 abcdef"') assert_equal '"\\\\"', resolve_quoted('"\\\\"') assert_equal '"foobar"', resolve_quoted("\"foo\\\nbar\"") assert_equal '"#{foo}"', resolve_quoted("\"\\\#{foo}\"") end def test_color_names assert_equal "white", resolve("white") assert_equal "#ffffff", resolve("#ffffff") silence_warnings {assert_equal "#fffffe", resolve("white - #000001")} assert_equal "transparent", resolve("transparent") assert_equal "rgba(0, 0, 0, 0)", resolve("rgba(0, 0, 0, 0)") end def test_rgba_color_literals assert_equal Sass::Script::Value::Color.new([1, 2, 3, 0.75]), eval("rgba(1, 2, 3, 0.75)") assert_equal "rgba(1, 2, 3, 0.75)", resolve("rgba(1, 2, 3, 0.75)") assert_equal Sass::Script::Value::Color.new([1, 2, 3, 0]), eval("rgba(1, 2, 3, 0)") assert_equal "rgba(1, 2, 3, 0)", resolve("rgba(1, 2, 3, 0)") assert_equal Sass::Script::Value::Color.new([1, 2, 3]), eval("rgba(1, 2, 3, 1)") assert_equal Sass::Script::Value::Color.new([1, 2, 3, 1]), eval("rgba(1, 2, 3, 1)") assert_equal "#010203", resolve("rgba(1, 2, 3, 1)") assert_equal "white", resolve("rgba(255, 255, 255, 1)") end def test_rgba_color_math silence_warnings {assert_equal "rgba(50, 50, 100, 0.35)", resolve("rgba(1, 1, 2, 0.35) * rgba(50, 50, 50, 0.35)")} silence_warnings {assert_equal "rgba(52, 52, 52, 0.25)", resolve("rgba(2, 2, 2, 0.25) + rgba(50, 50, 50, 0.25)")} assert_raise_message(Sass::SyntaxError, "Alpha channels must be equal: rgba(1, 2, 3, 0.15) + rgba(50, 50, 50, 0.75)") do silence_warnings {resolve("rgba(1, 2, 3, 0.15) + rgba(50, 50, 50, 0.75)")} end assert_raise_message(Sass::SyntaxError, "Alpha channels must be equal: #123456 * rgba(50, 50, 50, 0.75)") do silence_warnings {resolve("#123456 * rgba(50, 50, 50, 0.75)")} end assert_raise_message(Sass::SyntaxError, "Alpha channels must be equal: rgba(50, 50, 50, 0.75) / #123456") do silence_warnings {resolve("rgba(50, 50, 50, 0.75) / #123456")} end end def test_rgba_number_math silence_warnings {assert_equal "rgba(49, 49, 49, 0.75)", resolve("rgba(50, 50, 50, 0.75) - 1")} silence_warnings {assert_equal "rgba(100, 100, 100, 0.75)", resolve("rgba(50, 50, 50, 0.75) * 2")} end def test_rgba_rounding assert_equal "rgba(10, 1, 0, 0.1234567892)", resolve("rgba(10.0, 1.23456789, 0.0, 0.12345678919)") end def test_rgb_calc assert_equal "rgb(calc(255 - 5), 0, 0)", resolve("rgb(calc(255 - 5), 0, 0)") end def test_rgba_calc assert_equal "rgba(calc(255 - 5), 0, 0, 0.1)", resolve("rgba(calc(255 - 5), 0, 0, 0.1)") assert_equal "rgba(127, 0, 0, calc(0.1 + 0.5))", resolve("rgba(127, 0, 0, calc(0.1 + 0.5))") end def test_rgba_shorthand_calc assert_equal "rgba(255, 0, 0, calc(0.1 + 0.5))", resolve("rgba(red, calc(0.1 + 0.5))") end def test_hsl_calc assert_equal "hsl(calc(360 * 5 / 6), 50%, 50%)", resolve("hsl(calc(360 * 5 / 6), 50%, 50%)") end def test_hsla_calc assert_equal "hsla(calc(360 * 5 / 6), 50%, 50%, 0.1)", resolve("hsla(calc(360 * 5 / 6), 50%, 50%, 0.1)") assert_equal "hsla(270, 50%, 50%, calc(0.1 + 0.1))", resolve("hsla(270, 50%, 50%, calc(0.1 + 0.1))") end def test_compressed_colors assert_equal "#123456", resolve("#123456", :style => :compressed) assert_equal "rgba(1,2,3,0.5)", resolve("rgba(1, 2, 3, 0.5)", :style => :compressed) assert_equal "#123", resolve("#112233", :style => :compressed) assert_equal "#000", resolve("black", :style => :compressed) assert_equal "red", resolve("#f00", :style => :compressed) assert_equal "blue", resolve("blue", :style => :compressed) assert_equal "navy", resolve("#000080", :style => :compressed) assert_equal "navy #fff", resolve("#000080 white", :style => :compressed) assert_equal "This color is #fff", resolve('"This color is #{ white }"', :style => :compressed) assert_equal "transparent", resolve("rgba(0, 0, 0, 0)", :style => :compressed) end def test_compressed_comma # assert_equal "foo,bar,baz", resolve("foo, bar, baz", :style => :compressed) # assert_equal "foo,#baf,baz", resolve("foo, #baf, baz", :style => :compressed) assert_equal "foo,#baf,red", resolve("foo, #baf, #f00", :style => :compressed) end def test_implicit_strings assert_equal Sass::Script::Value::String.new("foo"), eval("foo") assert_equal Sass::Script::Value::String.new("foo/bar"), eval("foo/bar") end def test_basic_interpolation assert_equal "foo3bar", resolve("foo\#{1 + 2}bar") assert_equal "foo3 bar", resolve("foo\#{1 + 2} bar") assert_equal "foo 3bar", resolve("foo \#{1 + 2}bar") assert_equal "foo 3 bar", resolve("foo \#{1 + 2} bar") assert_equal "foo 35 bar", resolve("foo \#{1 + 2}\#{2 + 3} bar") assert_equal "foo 3 5 bar", resolve("foo \#{1 + 2} \#{2 + 3} bar") assert_equal "3bar", resolve("\#{1 + 2}bar") assert_equal "foo3", resolve("foo\#{1 + 2}") assert_equal "3", resolve("\#{1 + 2}") end def test_interpolation_in_function assert_equal 'flabnabbit(1foo)', resolve('flabnabbit(#{1 + "foo"})') assert_equal 'flabnabbit(foo 1foobaz)', resolve('flabnabbit(foo #{1 + "foo"}baz)') assert_equal('flabnabbit(foo 1foo2bar baz)', resolve('flabnabbit(foo #{1 + "foo"}#{2 + "bar"} baz)')) end def test_interpolation_near_operators silence_warnings do assert_equal '3 , 7', resolve('#{1 + 2} , #{3 + 4}') assert_equal '3, 7', resolve('#{1 + 2}, #{3 + 4}') assert_equal '3 ,7', resolve('#{1 + 2} ,#{3 + 4}') assert_equal '3,7', resolve('#{1 + 2},#{3 + 4}') assert_equal '3, 7, 11', resolve('#{1 + 2}, #{3 + 4}, #{5 + 6}') assert_equal '3, 7, 11', resolve('3, #{3 + 4}, 11') assert_equal '3, 7, 11', resolve('3, 7, #{5 + 6}') assert_equal '3 / 7', resolve('3 / #{3 + 4}') assert_equal '3 /7', resolve('3 /#{3 + 4}') assert_equal '3/ 7', resolve('3/ #{3 + 4}') assert_equal '3/7', resolve('3/#{3 + 4}') assert_equal '3 * 7', resolve('#{1 + 2} * 7') assert_equal '3* 7', resolve('#{1 + 2}* 7') assert_equal '3 *7', resolve('#{1 + 2} *7') assert_equal '3*7', resolve('#{1 + 2}*7') assert_equal '-3', resolve('-#{1 + 2}') assert_equal '- 3', resolve('- #{1 + 2}') assert_equal '5 + 3 * 7', resolve('5 + #{1 + 2} * #{3 + 4}') assert_equal '5 +3 * 7', resolve('5 +#{1 + 2} * #{3 + 4}') assert_equal '5+3 * 7', resolve('5+#{1 + 2} * #{3 + 4}') assert_equal '3 * 7 + 5', resolve('#{1 + 2} * #{3 + 4} + 5') assert_equal '3 * 7+ 5', resolve('#{1 + 2} * #{3 + 4}+ 5') assert_equal '3 * 7+5', resolve('#{1 + 2} * #{3 + 4}+5') assert_equal '5/3 + 7', resolve('5 / (#{1 + 2} + #{3 + 4})') assert_equal '5/3 + 7', resolve('5 /(#{1 + 2} + #{3 + 4})') assert_equal '5/3 + 7', resolve('5 /( #{1 + 2} + #{3 + 4} )') assert_equal '3 + 7/5', resolve('(#{1 + 2} + #{3 + 4}) / 5') assert_equal '3 + 7/5', resolve('(#{1 + 2} + #{3 + 4})/ 5') assert_equal '3 + 7/5', resolve('( #{1 + 2} + #{3 + 4} )/ 5') assert_equal '3 + 5', resolve('#{1 + 2} + 2 + 3') assert_equal '3 +5', resolve('#{1 + 2} +2 + 3') end end def test_string_interpolation assert_equal "foo bar, baz bang", resolve('"foo #{"bar"}, #{"baz"} bang"') assert_equal "foo bar baz bang", resolve('"foo #{"#{"ba" + "r"} baz"} bang"') assert_equal 'foo #{bar baz} bang', resolve('"foo \#{#{"ba" + "r"} baz} bang"') assert_equal 'foo #{baz bang', resolve('"foo #{"\#{" + "baz"} bang"') assert_equal "foo2bar", resolve('\'foo#{1 + 1}bar\'') assert_equal "foo2bar", resolve('"foo#{1 + 1}bar"') assert_equal "foo1bar5baz4bang", resolve('\'foo#{1 + "bar#{2 + 3}baz" + 4}bang\'') end def test_interpolation_in_interpolation assert_equal 'foo', resolve('#{#{foo}}') assert_equal 'foo', resolve('"#{#{foo}}"') assert_equal 'foo', resolve('#{"#{foo}"}') assert_equal 'foo', resolve('"#{"#{foo}"}"') end def test_interpolation_with_newline assert_equal "\nbang", resolve('"#{"\a "}bang"') assert_equal "\n\nbang", resolve('"#{"\a "}\a bang"') end def test_rule_interpolation assert_equal(< 2) assert_equal "public_instance_methods()", resolve("public_instance_methods()") end def test_adding_functions_directly_to_functions_module assert !Functions.callable?('nonexistent') Functions.class_eval { def nonexistent; end } assert Functions.callable?('nonexistent') Functions.send :remove_method, :nonexistent end def test_default_functions assert_equal "url(12)", resolve("url(12)") assert_equal 'blam("foo")', resolve('blam("foo")') end def test_function_results_have_options assert_equal "Options defined!", resolve("assert_options(abs(1))") assert_equal "Options defined!", resolve("assert_options(round(1.2))") end def test_funcall_requires_no_whitespace_before_lparen assert_equal "no-repeat 15px", resolve("no-repeat (7px + 8px)") assert_equal "no-repeat(15px)", resolve("no-repeat(7px + 8px)") end def test_dynamic_url assert_equal "url(foo-bar)", resolve("url($foo)", {}, env('foo' => Sass::Script::Value::String.new("foo-bar"))) assert_equal "url(foo-bar baz)", resolve("url($foo $bar)", {}, env('foo' => Sass::Script::Value::String.new("foo-bar"), 'bar' => Sass::Script::Value::String.new("baz"))) assert_equal "url(foo baz)", resolve("url(foo $bar)", {}, env('bar' => Sass::Script::Value::String.new("baz"))) assert_equal "url(foo bar)", resolve("url(foo bar)") end def test_url_with_interpolation assert_equal "url(https://sass-lang.com/images/foo-bar)", resolve("url(https://sass-lang.com/images/\#{foo-bar})") assert_equal 'url("https://sass-lang.com/images/foo-bar")', resolve("url('https://sass-lang.com/images/\#{foo-bar}')") assert_equal 'url("https://sass-lang.com/images/foo-bar")', resolve('url("https://sass-lang.com/images/#{foo-bar}")') assert_unquoted "url(https://sass-lang.com/images/\#{foo-bar})" end def test_hyphenated_variables assert_equal("a-b", resolve("$a-b", {}, env("a-b" => Sass::Script::Value::String.new("a-b")))) end def test_ruby_equality assert_equal eval('"foo"'), eval('"foo"') assert_equal eval('1'), eval('1.0') assert_equal eval('1 2 3.0'), eval('1 2 3') assert_equal eval('1, 2, 3.0'), eval('1, 2, 3') assert_equal eval('(1 2), (3, 4), (5 6)'), eval('(1 2), (3, 4), (5 6)') refute_equal eval('1, 2, 3'), eval('1 2 3') refute_equal eval('1'), eval('"1"') end def test_booleans assert_equal "true", resolve("true") assert_equal "false", resolve("false") end def test_null assert_equal "", resolve("null") end def test_boolean_ops assert_equal "true", resolve("true and true") assert_equal "true", resolve("false or true") assert_equal "true", resolve("true or false") assert_equal "true", resolve("true or true") assert_equal "false", resolve("false or false") assert_equal "false", resolve("false and true") assert_equal "false", resolve("true and false") assert_equal "false", resolve("false and false") assert_equal "true", resolve("not false") assert_equal "false", resolve("not true") assert_equal "true", resolve("not not true") assert_equal "1", resolve("false or 1") assert_equal "false", resolve("false and 1") assert_equal "2", resolve("2 or 3") assert_equal "3", resolve("2 and 3") assert_equal "true", resolve("null or true") assert_equal "true", resolve("true or null") assert_equal "", resolve("null or null") assert_equal "", resolve("null and true") assert_equal "", resolve("true and null") assert_equal "", resolve("null and null") assert_equal "true", resolve("not null") assert_equal "1", resolve("null or 1") assert_equal "", resolve("null and 1") end def test_arithmetic_ops assert_equal "2", resolve("1 + 1") assert_equal "0", resolve("1 - 1") assert_equal "8", resolve("2 * 4") assert_equal "0.5", resolve("(2 / 4)") assert_equal "2", resolve("(4 / 2)") assert_equal "-1", resolve("-1") end def test_subtraction_vs_minus_vs_identifier assert_equal "0.25em", resolve("1em-.75") assert_equal "0.25em", resolve("1em-0.75") assert_equal "1em -0.75", resolve("1em -.75") assert_equal "1em -0.75", resolve("1em -0.75") assert_equal "1em- 0.75", resolve("1em- .75") assert_equal "1em- 0.75", resolve("1em- 0.75") assert_equal "0.25em", resolve("1em - .75") assert_equal "0.25em", resolve("1em - 0.75") end def test_string_ops assert_equal '"foo" "bar"', resolve('"foo" "bar"') assert_equal "true 1", resolve('true 1') assert_equal '"foo", "bar"', resolve("'foo' , 'bar'") assert_equal "true, 1", resolve('true , 1') assert_equal "foobar", resolve('"foo" + "bar"') assert_equal "\nfoo\nxyz", resolve('"\a foo" + "\axyz"') assert_equal "true1", resolve('true + 1') assert_equal '"foo"-"bar"', resolve("'foo' - 'bar'") assert_equal "true-1", resolve('true - 1') assert_equal '"foo"/"bar"', resolve('"foo" / "bar"') assert_equal "true/1", resolve('true / 1') assert_equal '-"bar"', resolve("- 'bar'") assert_equal "-true", resolve('- true') assert_equal '/"bar"', resolve('/ "bar"') assert_equal "/true", resolve('/ true') end def test_relational_ops assert_equal "false", resolve("1 > 2") assert_equal "false", resolve("2 > 2") assert_equal "true", resolve("3 > 2") assert_equal "false", resolve("1 >= 2") assert_equal "true", resolve("2 >= 2") assert_equal "true", resolve("3 >= 2") assert_equal "true", resolve("1 < 2") assert_equal "false", resolve("2 < 2") assert_equal "false", resolve("3 < 2") assert_equal "true", resolve("1 <= 2") assert_equal "true", resolve("2 <= 2") assert_equal "false", resolve("3 <= 2") end def test_null_ops assert_raise_message(Sass::SyntaxError, 'Invalid null operation: "null plus 1".') {eval("null + 1")} assert_raise_message(Sass::SyntaxError, 'Invalid null operation: "null minus 1".') {eval("null - 1")} assert_raise_message(Sass::SyntaxError, 'Invalid null operation: "null times 1".') {eval("null * 1")} assert_raise_message(Sass::SyntaxError, 'Invalid null operation: "null div 1".') {eval("null / 1")} assert_raise_message(Sass::SyntaxError, 'Invalid null operation: "null mod 1".') {eval("null % 1")} assert_raise_message(Sass::SyntaxError, 'Invalid null operation: "1 plus null".') {eval("1 + null")} assert_raise_message(Sass::SyntaxError, 'Invalid null operation: "1 minus null".') {eval("1 - null")} assert_raise_message(Sass::SyntaxError, 'Invalid null operation: "1 times null".') {eval("1 * null")} assert_raise_message(Sass::SyntaxError, 'Invalid null operation: "1 div null".') {eval("1 / null")} assert_raise_message(Sass::SyntaxError, 'Invalid null operation: "1 mod null".') {eval("1 % null")} assert_raise_message(Sass::SyntaxError, 'Invalid null operation: "1 gt null".') {eval("1 > null")} assert_raise_message(Sass::SyntaxError, 'Invalid null operation: "null lt 1".') {eval("null < 1")} assert_raise_message(Sass::SyntaxError, 'Invalid null operation: "null plus null".') {eval("null + null")} assert_raise_message(Sass::SyntaxError, 'Invalid null operation: ""foo" plus null".') {eval("foo + null")} end def test_equals assert_equal("true", resolve('"foo" == $foo', {}, env("foo" => Sass::Script::Value::String.new("foo")))) assert_equal "true", resolve("1 == 1.0") assert_equal "true", resolve("false != true") assert_equal "false", resolve("1em == 1px") assert_equal "false", resolve("12 != 12") assert_equal "true", resolve("(foo bar baz) == (foo bar baz)") assert_equal "true", resolve("(foo, bar, baz) == (foo, bar, baz)") assert_equal "true", resolve('((1 2), (3, 4), (5 6)) == ((1 2), (3, 4), (5 6))') assert_equal "true", resolve('((1 2), (3 4)) == (1 2, 3 4)') assert_equal "false", resolve('((1 2) 3) == (1 2 3)') assert_equal "false", resolve('(1 (2 3)) == (1 2 3)') assert_equal "false", resolve('((1, 2) (3, 4)) == (1, 2 3, 4)') assert_equal "false", resolve('(1 2 3) == (1, 2, 3)') assert_equal "true", resolve('null == null') assert_equal "false", resolve('"null" == null') assert_equal "false", resolve('0 == null') assert_equal "false", resolve('() == null') assert_equal "false", resolve('null != null') assert_equal "true", resolve('"null" != null') assert_equal "true", resolve('0 != null') assert_equal "true", resolve('() != null') end def test_mod assert_equal "5", resolve("29 % 12") assert_equal "5px", resolve("29px % 12") assert_equal "5px", resolve("29px % 12px") end def test_operation_precedence assert_equal "false true", resolve("true and false false or true") assert_equal "true", resolve("false and true or true and true") assert_equal "true", resolve("1 == 2 or 3 == 3") assert_equal "true", resolve("1 < 2 == 3 >= 3") assert_equal "true", resolve("1 + 3 > 4 - 2") assert_equal "11", resolve("1 + 2 * 3 + 4") end def test_functions assert_equal "#80ff80", resolve("hsl(120, 100%, 75%)") silence_warnings {assert_equal "#81ff81", resolve("hsl(120, 100%, 75%) + #010001")} end def test_operator_unit_conversion assert_equal "1.1cm", resolve("1cm + 1mm") assert_equal "8q", resolve("4q + 1mm") assert_equal "40.025cm", resolve("40cm + 1q") assert_equal "2in", resolve("1in + 96px") assert_equal "true", resolve("2mm < 1cm") assert_equal "true", resolve("10mm == 1cm") assert_equal "true", resolve("1.1cm == 11mm") assert_equal "true", resolve("2mm == 8q") assert_equal "false", resolve("2px > 3q") Sass::Deprecation.allow_double_warnings do assert_warning(< eval("1px"))) assert_equal "0.5", resolve("1px/$var", {}, env("var" => eval("2px"))) assert_equal "0.5", resolve("$var", {}, env("var" => eval("1px/2px"))) end # Regression test for issue 1786. def test_slash_division_within_list assert_equal "1 1/2 1/2", resolve("(1 1/2 1/2)") assert_equal "1/2 1/2", resolve("(1/2 1/2)") assert_equal "1/2", resolve("(1/2,)") end def test_non_ident_colors_with_wrong_number_of_digits assert_raise_message(Sass::SyntaxError, 'Invalid CSS after "": expected expression (e.g. 1px, bold), was "#1"') {eval("#1")} assert_raise_message(Sass::SyntaxError, 'Invalid CSS after "": expected expression (e.g. 1px, bold), was "#12"') {eval("#12")} assert_raise_message(Sass::SyntaxError, 'Invalid CSS after "": expected expression (e.g. 1px, bold), was "#12345"') {eval("#12345")} assert_raise_message(Sass::SyntaxError, 'Invalid CSS after "": expected expression (e.g. ' \ '1px, bold), was "#1234567"') {eval("#1234567")} end def test_case_insensitive_color_names assert_equal "BLUE", resolve("BLUE") assert_equal "rEd", resolve("rEd") assert_equal "#804000", resolve("mix(GrEeN, ReD)") end def test_empty_list assert_equal "1 2 3", resolve("1 2 () 3") assert_equal "1 2 3", resolve("1 2 3 ()") assert_equal "1 2 3", resolve("() 1 2 3") assert_raise_message(Sass::SyntaxError, "() isn't a valid CSS value.") {resolve("()")} assert_raise_message(Sass::SyntaxError, "() isn't a valid CSS value.") {resolve("nth(append((), ()), 1)")} end def test_list_with_nulls assert_equal "1, 2, 3", resolve("1, 2, null, 3") assert_equal "1 2 3", resolve("1 2 null 3") assert_equal "1, 2, 3", resolve("1, 2, 3, null") assert_equal "1 2 3", resolve("1 2 3 null") assert_equal "1, 2, 3", resolve("null, 1, 2, 3") assert_equal "1 2 3", resolve("null 1 2 3") end def test_map_can_have_trailing_comma assert_equal("(foo: 1, bar: 2)", eval("(foo: 1, bar: 2,)").to_sass) end def test_list_can_have_trailing_comma assert_equal("1, 2, 3", resolve("1, 2, 3,")) end def test_trailing_comma_defines_singleton_list assert_equal("1 2 3", resolve("nth((1 2 3,), 1)")) end def test_map_cannot_have_duplicate_keys assert_raise_message(Sass::SyntaxError, 'Duplicate key "foo" in map (foo: bar, foo: baz).') do eval("(foo: bar, foo: baz)") end assert_raise_message(Sass::SyntaxError, 'Duplicate key "foo" in map (foo: bar, fo + o: baz).') do eval("(foo: bar, fo + o: baz)") end assert_raise_message(Sass::SyntaxError, 'Duplicate key "foo" in map (foo: bar, "foo": baz).') do eval("(foo: bar, 'foo': baz)") end assert_raise_message(Sass::SyntaxError, 'Duplicate key 2px in map (2px: bar, 1px + 1px: baz).') do eval("(2px: bar, 1px + 1px: baz)") end assert_raise_message(Sass::SyntaxError, 'Duplicate key #0000ff in map (blue: bar, #00f: baz).') do eval("(blue: bar, #00f: baz)") end end def test_non_duplicate_map_keys # These shouldn't throw errors eval("(foo: foo, bar: bar)") eval("(2px: foo, 2: bar)") eval("(2px: foo, 2em: bar)") eval("('2px': foo, 2px: bar)") end def test_map_syntax_errors assert_raise_message(Sass::SyntaxError, 'Invalid CSS after "(foo:": expected expression (e.g. 1px, bold), was ")"') do eval("(foo:)") end assert_raise_message(Sass::SyntaxError, 'Invalid CSS after "(": expected ")", was ":bar)"') do eval("(:bar)") end assert_raise_message(Sass::SyntaxError, 'Invalid CSS after "(foo, bar": expected ")", was ": baz)"') do eval("(foo, bar: baz)") end assert_raise_message(Sass::SyntaxError, 'Invalid CSS after "(foo: bar, baz": expected ":", was ")"') do eval("(foo: bar, baz)") end end def test_deep_argument_error_not_unwrapped # JRuby (as of 1.6.7.2) offers no way of distinguishing between # argument errors caused by programming errors in a function and # argument errors explicitly thrown within that function. return if RUBY_PLATFORM =~ /java/ # Don't validate the message; it's different on Rubinius. assert_raises(ArgumentError) {resolve("arg-error()")} end def test_shallow_argument_error_unwrapped assert_raise_message(Sass::SyntaxError, "wrong number of arguments (1 for 0) for `arg-error'") {resolve("arg-error(1)")} end def test_boolean_ops_short_circuit assert_equal "false", resolve("$ie and $ie <= 7", {}, env('ie' => Sass::Script::Value::Bool.new(false))) assert_equal "true", resolve("$ie or $undef", {}, env('ie' => Sass::Script::Value::Bool.new(true))) end def test_selector env = Sass::Environment.new assert_equal "true", resolve("& == null", {}, env) env.selector = selector('.foo.bar .baz.bang, .bip.bop') assert_equal ".foo.bar .baz.bang, .bip.bop", resolve("&", {}, env) assert_equal ".foo.bar .baz.bang", resolve("nth(&, 1)", {}, env) assert_equal ".bip.bop", resolve("nth(&, 2)", {}, env) assert_equal ".foo.bar", resolve("nth(nth(&, 1), 1)", {}, env) assert_equal ".baz.bang", resolve("nth(nth(&, 1), 2)", {}, env) assert_equal ".bip.bop", resolve("nth(nth(&, 2), 1)", {}, env) assert_equal "string", resolve("type-of(nth(nth(&, 1), 1))", {}, env) env.selector = selector('.foo > .bar') assert_equal ".foo > .bar", resolve("&", {}, env) assert_equal ".foo > .bar", resolve("nth(&, 1)", {}, env) assert_equal ".foo", resolve("nth(nth(&, 1), 1)", {}, env) assert_equal ">", resolve("nth(nth(&, 1), 2)", {}, env) assert_equal ".bar", resolve("nth(nth(&, 1), 3)", {}, env) end def test_selector_with_newlines env = Sass::Environment.new env.selector = selector(".foo.bar\n.baz.bang,\n\n.bip.bop") assert_equal ".foo.bar .baz.bang, .bip.bop", resolve("&", {}, env) assert_equal ".foo.bar .baz.bang", resolve("nth(&, 1)", {}, env) assert_equal ".bip.bop", resolve("nth(&, 2)", {}, env) assert_equal ".foo.bar", resolve("nth(nth(&, 1), 1)", {}, env) assert_equal ".baz.bang", resolve("nth(nth(&, 1), 2)", {}, env) assert_equal ".bip.bop", resolve("nth(nth(&, 2), 1)", {}, env) assert_equal "string", resolve("type-of(nth(nth(&, 1), 1))", {}, env) end def test_setting_global_variable_globally assert_no_warning {assert_equal(< :scss))} .foo { a: 1; } .bar { b: 2; } CSS $var: 1; .foo { a: $var; } $var: 2; .bar { b: $var; } SCSS end def test_setting_global_variable_locally assert_no_warning {assert_equal(< :scss))} .bar { a: x; b: y; c: z; } CSS $var1: 1; $var3: 3; .foo { $var1: x !global; $var2: y !global; @each $var3 in _ { $var3: z !global; } } .bar { a: $var1; b: $var2; c: $var3; } SCSS end def test_setting_global_variable_locally_with_default assert_equal(< :scss)) .bar { a: 1; b: y; c: z; } CSS $var1: 1; .foo { $var1: x !global !default; $var2: y !global !default; @each $var3 in _ { $var3: z !global !default; } } .bar { a: $var1; b: $var2; c: $var3; } SCSS end def test_setting_local_variable assert_equal(< :scss)) .a { value: inside; } .b { value: outside; } CSS $var: outside; .a { $var: inside; value: $var; } .b { value: $var; } SCSS end def test_setting_local_variable_from_inner_scope assert_equal(< :scss)) .a .b { value: inside; } .a .c { value: inside; } CSS .a { $var: outside; .b { $var: inside; value: $var; } .c { value: $var; } } SCSS end def test_if_can_assign_to_global_variables assert_equal < :scss) .a { b: 2; } CSS $var: 1; @if true {$var: 2} .a {b: $var} SCSS end def test_else_can_assign_to_global_variables assert_equal < :scss) .a { b: 2; } CSS $var: 1; @if false {} @else {$var: 2} .a {b: $var} SCSS end def test_for_can_assign_to_global_variables assert_equal < :scss) .a { b: 2; } CSS $var: 1; @for $i from 1 to 2 {$var: 2} .a {b: $var} SCSS end def test_each_can_assign_to_global_variables assert_equal < :scss) .a { b: 2; } CSS $var: 1; @each $a in 1 {$var: 2} .a {b: $var} SCSS end def test_while_can_assign_to_global_variables assert_equal < :scss) .a { b: 2; } CSS $var: 1; @while $var != 2 {$var: 2} .a {b: $var} SCSS end def test_if_doesnt_leak_local_variables assert_raise_message(Sass::SyntaxError, 'Undefined variable: "$var".') do render(< :scss) @if true {$var: 1} .a {b: $var} SCSS end end def test_else_doesnt_leak_local_variables assert_raise_message(Sass::SyntaxError, 'Undefined variable: "$var".') do render(< :scss) @if false {} @else {$var: 1} .a {b: $var} SCSS end end def test_for_doesnt_leak_local_variables assert_raise_message(Sass::SyntaxError, 'Undefined variable: "$var".') do render(< :scss) @for $i from 1 to 2 {$var: 1} .a {b: $var} SCSS end end def test_each_doesnt_leak_local_variables assert_raise_message(Sass::SyntaxError, 'Undefined variable: "$var".') do render(< :scss) @each $a in 1 {$var: 1} .a {b: $var} SCSS end end def test_while_doesnt_leak_local_variables assert_raise_message(Sass::SyntaxError, 'Undefined variable: "$var".') do render(< :scss) $iter: true; @while $iter { $var: 1; $iter: false; } .a {b: $var} SCSS end end def test_color_format_is_preserved_by_default assert_equal "blue", resolve("blue") assert_equal "bLuE", resolve("bLuE") assert_equal "#00f", resolve("#00f") assert_equal "blue #00F", resolve("blue #00F") assert_equal "blue", resolve("nth(blue #00F, 1)") assert_equal "#00F", resolve("nth(blue #00F, 2)") end def test_color_format_isnt_always_preserved_in_compressed_style assert_equal "red", resolve("red", :style => :compressed) assert_equal "red", resolve("#f00", :style => :compressed) assert_equal "red red", resolve("red #f00", :style => :compressed) assert_equal "red", resolve("nth(red #f00, 2)", :style => :compressed) end def test_color_format_is_sometimes_preserved_in_compressed_style assert_equal "ReD", resolve("ReD", :style => :compressed) assert_equal "blue", resolve("blue", :style => :compressed) assert_equal "#00f", resolve("#00f", :style => :compressed) end def test_color_format_isnt_preserved_when_modified assert_equal "magenta", resolve("change-color(#f00, $blue: 255)") end def test_ids assert_equal "#foo", resolve("#foo") silence_warnings {assert_equal "#abcd", resolve("#abcd")} assert_equal "#abc-def", resolve("#abc-def") assert_equal "#abc_def", resolve("#abc_def") assert_equal "#uvw-xyz", resolve("#uvw-xyz") assert_equal "#uvw_xyz", resolve("#uvw_xyz") assert_equal "#uvwxyz", resolve("#uvw + xyz") end def test_scientific_notation assert_equal "2000", resolve("2e3") assert_equal "2000", resolve("2E3") assert_equal "2000", resolve("2e+3") assert_equal "2000em", resolve("2e3em") assert_equal "25000000000", resolve("2.5e10") assert_equal "0.1234", resolve("1234e-4") assert_equal "12.34", resolve("1.234e1") end def test_identifier_units assert_equal "5-foo", resolve("2-foo + 3-foo") assert_equal "5-foo-", resolve("2-foo- + 3-foo-") assert_equal "5-u2603", resolve("2-\\u2603 + 3-\\u2603") end def test_backslash_newline_in_string assert_equal 'foobar', resolve("\"foo\\\nbar\"") assert_equal 'foobar', resolve("'foo\\\nbar'") end def test_unclosed_special_fun assert_raise_message(Sass::SyntaxError, 'Invalid CSS after "calc(foo()": expected ")", was ""') do resolve("calc(foo()") end assert_raise_message(Sass::SyntaxError, 'Invalid CSS after "calc(#{\')\'}": expected ")", was ""') do resolve("calc(\#{')'}") end assert_raise_message(Sass::SyntaxError, 'Invalid CSS after "calc(#{foo": expected "}", was ""') do resolve("calc(\#{foo") end end def test_special_fun_with_interpolation assert_equal "calc())", resolve("calc(\#{')'})") assert_equal "calc(# {foo})", resolve("calc(# {foo})") end # Regression Tests def test_interpolation_after_string assert_equal '"foobar" 2', resolve('"foobar" #{2}') assert_equal "calc(1 + 2) 3", resolve('calc(1 + 2) #{3}') end def test_repeatedly_modified_color assert_equal(< 29.000000000000004 assert_equal "true", resolve("29 == (29 / 7 * 7)") end def test_compressed_output_of_numbers_with_leading_zeros assert_equal "1.5", resolve("1.5", :style => :compressed) assert_equal ".5", resolve("0.5", :style => :compressed) assert_equal "-.5", resolve("-0.5", :style => :compressed) assert_equal "0.5", resolve("0.5", :style => :compact) end def test_interpolation_without_deprecation_warning assert_no_warning {assert_equal "a", resolve('#{a}')} assert_no_warning {assert_equal "abc", resolve('a#{b}c')} assert_no_warning {assert_equal "+ a", resolve('+ #{a}')} assert_no_warning {assert_equal "/ a", resolve('/ #{a}')} assert_no_warning {assert_equal "1 / a", resolve('1 / #{a}')} assert_no_warning {assert_equal "a / b", resolve('#{a} / #{b}')} assert_no_warning {assert_equal "foo(1 = a)", resolve('foo(1 = #{a})')} assert_no_warning {assert_equal "foo(a = b)", resolve('foo(#{a} = #{b})')} assert_no_warning {assert_equal "-a", resolve('-#{a}')} assert_no_warning {assert_equal "1-a", resolve('1-#{a}')} assert_no_warning {assert_equal "a- 1", resolve('#{a}- 1')} assert_no_warning {assert_equal "a-1", resolve('#{a}-1')} assert_no_warning {assert_equal "a-b", resolve('#{a}-#{b}')} assert_no_warning {assert_equal "a1", resolve('#{a}1')} assert_no_warning {assert_equal "ab", resolve('#{a}b')} assert_no_warning {assert_equal "1a", resolve('1#{a}')} assert_no_warning {assert_equal "ba", resolve('b#{a}')} end def test_leading_interpolation_with_deprecation_warning assert_equal "ab == 1", resolve_with_interp_warning('#{a + b} == 1') assert_equal "ab != 1", resolve_with_interp_warning('#{a + b} != 1') assert_equal "ab > 1", resolve_with_interp_warning('#{a + b} > 1') assert_equal "ab >= 1", resolve_with_interp_warning('#{a + b} >= 1') assert_equal "ab < 1", resolve_with_interp_warning('#{a + b} < 1') assert_equal "ab <= 1", resolve_with_interp_warning('#{a + b} <= 1') assert_equal "ab + 1", resolve_with_interp_warning('#{a + b} + 1') assert_equal "ab * 1", resolve_with_interp_warning('#{a + b} * 1') assert_equal "ab - 1", resolve_with_interp_warning('#{a + b} - 1') assert_equal "ab % 1", resolve_with_interp_warning('#{a + b} % 1') assert_equal( "abvar", resolve_with_interp_warning( '#{a + b}$var', '"#{a + b}#{$var}"', env('var' => Sass::Script::Value::String.new("var")))) assert_equal( "varab", resolve_with_interp_warning( '$var#{a + b}', '"#{$var}#{a + b}"', env('var' => Sass::Script::Value::String.new("var")))) assert_equal "ab1", resolve_with_interp_warning('#{a + b}(1)', '"#{a + b}1"') assert_equal "1ab", resolve_with_interp_warning('(1)#{a + b}', '"1#{a + b}"') end def test_trailing_interpolation_with_deprecation_warning assert_equal "not ab", resolve_with_interp_warning('not #{a + b}') assert_equal "1 and ab", resolve_with_interp_warning('1 and #{a + b}') assert_equal "1 or ab", resolve_with_interp_warning('1 or #{a + b}') assert_equal "1 == ab", resolve_with_interp_warning('1 == #{a + b}') assert_equal "1 != ab", resolve_with_interp_warning('1 != #{a + b}') assert_equal "1 > ab", resolve_with_interp_warning('1 > #{a + b}') assert_equal "1 >= ab", resolve_with_interp_warning('1 >= #{a + b}') assert_equal "1 < ab", resolve_with_interp_warning('1 < #{a + b}') assert_equal "1 <= ab", resolve_with_interp_warning('1 <= #{a + b}') assert_equal "1 + ab", resolve_with_interp_warning('1 + #{a + b}') assert_equal "1 * ab", resolve_with_interp_warning('1 * #{a + b}') assert_equal "1 - ab", resolve_with_interp_warning('1 - #{a + b}') assert_equal "1 % ab", resolve_with_interp_warning('1 % #{a + b}') assert_equal "- ab", resolve_with_interp_warning('- #{a + b}') assert_equal "1- ab", resolve_with_interp_warning('1- #{a + b}') assert_equal "- ab 2 3", resolve_with_interp_warning('- #{a + b} 2 3', '"- #{a + b} #{2 3}"') end def test_brackteing_interpolation_with_deprecation_warning assert_equal "ab == cd", resolve_with_interp_warning('#{a + b} == #{c + d}') assert_equal "ab != cd", resolve_with_interp_warning('#{a + b} != #{c + d}') assert_equal "ab > cd", resolve_with_interp_warning('#{a + b} > #{c + d}') assert_equal "ab >= cd", resolve_with_interp_warning('#{a + b} >= #{c + d}') assert_equal "ab < cd", resolve_with_interp_warning('#{a + b} < #{c + d}') assert_equal "ab <= cd", resolve_with_interp_warning('#{a + b} <= #{c + d}') assert_equal "ab + cd", resolve_with_interp_warning('#{a + b} + #{c + d}') assert_equal "ab * cd", resolve_with_interp_warning('#{a + b} * #{c + d}') assert_equal "ab - cd", resolve_with_interp_warning('#{a + b} - #{c + d}') assert_equal "ab % cd", resolve_with_interp_warning('#{a + b} % #{c + d}') end def test_interp_warning_formatting resolve_with_interp_warning('#{1} + 1', '"1 + 1"') resolve_with_interp_warning('#{1} + "foo"', '\'1 + "foo"\'') resolve_with_interp_warning('#{1} + \'foo\'', '\'1 + "foo"\'') resolve_with_interp_warning('#{1} + "#{a + b}"', '\'1 + "#{a + b}"\'') resolve_with_interp_warning('"#{a + b}" + #{1}', '\'"#{a + b}" + 1\'') resolve_with_interp_warning('"#{a + b}" + #{1} + "#{c + d}"', '\'"#{a + b}" + 1 + "#{c + d}"\'') resolve_with_interp_warning('#{1} + "\'"', '"1 + \\"\'\\""') resolve_with_interp_warning('#{1} + \'"\'', '"1 + \'\\"\'"') resolve_with_interp_warning('#{1} + "\'\\""', '"1 + \\"\'\\\\\\"\\""') end def test_inactive_lazy_interpolation_deprecation_warning assert_equal '1, 2, 3', assert_no_warning {resolve('1, #{2}, 3')} assert_equal '1, 2, 3', assert_no_warning {resolve('1, 2, #{3}')} assert_equal '1,2,3', assert_no_warning {resolve('1,#{2},3')} assert_equal '1 2 3', assert_no_warning {resolve('#{1} 2 3')} assert_equal '1 2 3', assert_no_warning {resolve('1 #{2} 3')} assert_equal '1 2 3', assert_no_warning {resolve('1 2 #{3}')} assert_equal '+1 2 3', assert_no_warning {resolve('+#{1} 2 3')} assert_equal '-1 2 3', assert_no_warning {resolve('-#{1} 2 3')} assert_equal '/1 2 3', assert_no_warning {resolve('/#{1} 2 3')} assert_equal '1, 2, 31', assert_no_warning {resolve('(1, #{2}, 3) + 1')} assert_equal '11, 2, 3', assert_no_warning {resolve('1 + (1, #{2}, 3)')} assert_equal 'a, b, c', assert_no_warning {resolve('selector-parse((a, #{b}, c))')} end def test_active_lazy_interpolation_deprecation_warning Sass::Deprecation.allow_double_warnings do assert_equal "1, 2, 3", resolve_with_lazy_interp_warning('quote((1, #{2}, 3))', '"1, 2, 3"') assert_equal "1", resolve_with_lazy_interp_warning('length((1, #{2}, 3))', '"1, 2, 3"') assert_equal "1, 2, 3", resolve_with_lazy_interp_warning('inspect((1, #{2}, 3))', '"1, 2, 3"') assert_equal "string", resolve_with_lazy_interp_warning('type-of((1, #{2}, 3))', '"1, 2, 3"') assert_equal "+1 2 3", resolve_with_lazy_interp_warning('quote((+#{1} 2 3))', '"+1 #{2 3}"') assert_equal "/1 2 3", resolve_with_lazy_interp_warning('quote((/#{1} 2 3))', '"/1 #{2 3}"') assert_equal "-1 2 3", resolve_with_lazy_interp_warning('quote((-#{1} 2 3))', '"-1 #{2 3}"') end end def test_comparison_of_complex_units # Tests for issue #1960 Sass::Deprecation.allow_double_warnings do assert_warning(< baz {bar: baz} SCSS end def test_unicode assert_parses < s1, > s2)') assert_selector_parses('E.warning') assert_selector_parses('E#myid') assert_selector_parses('E[foo]') assert_selector_parses('E[foo="bar"]') assert_selector_parses('E[foo="bar" i]') assert_selector_parses('E[foo~="bar"]') assert_selector_parses('E[foo^="bar"]') assert_selector_parses('E[foo$="bar"]') assert_selector_parses('E[foo*="bar"]') assert_selector_parses('E[foo|="en"]') assert_selector_parses('E:dir(ltr)') assert_selector_parses('E:lang(fr)') assert_selector_parses('E:lang(zh, *-hant)') assert_selector_parses('E:any-link') assert_selector_parses('E:link') assert_selector_parses('E:visited') assert_selector_parses('E:local-link') assert_selector_parses('E:local-link(0)') assert_selector_parses('E:target') assert_selector_parses('E:scope') assert_selector_parses('E:current') assert_selector_parses('E:current(s)') assert_selector_parses('E:past') assert_selector_parses('E:future') assert_selector_parses('E:active') assert_selector_parses('E:hover') assert_selector_parses('E:focus') assert_selector_parses('E:enabled') assert_selector_parses('E:disabled') assert_selector_parses('E:checked') assert_selector_parses('E:indeterminate') assert_selector_parses('E:default') assert_selector_parses('E:in-range') assert_selector_parses('E:out-of-range') assert_selector_parses('E:required') assert_selector_parses('E:optional') assert_selector_parses('E:read-only') assert_selector_parses('E:read-write') assert_selector_parses('E:root') assert_selector_parses('E:empty') assert_selector_parses('E:first-child') assert_selector_parses('E:nth-child(n)') assert_selector_parses('E:last-child') assert_selector_parses('E:nth-last-child(n)') assert_selector_parses('E:only-child') assert_selector_parses('E:first-of-type') assert_selector_parses('E:nth-of-type(n)') assert_selector_parses('E:last-of-type') assert_selector_parses('E:nth-last-of-type(n)') assert_selector_parses('E:only-of-type') assert_selector_parses('E:nth-child(n of selector)') assert_selector_parses('E:nth-last-child(n of selector)') assert_selector_parses('E:nth-child(n)') assert_selector_parses('E:nth-last-child(n)') assert_selector_parses('E F') assert_selector_parses('E > F') assert_selector_parses('E + F') assert_selector_parses('E ~ F') silence_warnings {assert_selector_parses('E /foo/ F')} silence_warnings {assert_selector_parses('E! > F')} silence_warnings {assert_selector_parses('E /ns|foo/ F')} # From http://dev.w3.org/csswg/css-scoping-1/ assert_selector_parses('E:host(s)') assert_selector_parses('E:host-context(s)') end # Taken from http://dev.w3.org/csswg/selectors4/#overview, but without element # names. def test_more_summarized_selectors assert_selector_parses(':not(s)') assert_selector_parses(':not(s1, s2)') assert_selector_parses(':matches(s1, s2)') assert_selector_parses(':has(s1, s2)') assert_selector_parses(':has(> s1, > s2)') assert_selector_parses('.warning') assert_selector_parses('#myid') assert_selector_parses('[foo]') assert_selector_parses('[foo="bar"]') assert_selector_parses('[foo="bar" i]') assert_selector_parses('[foo~="bar"]') assert_selector_parses('[foo^="bar"]') assert_selector_parses('[foo$="bar"]') assert_selector_parses('[foo*="bar"]') assert_selector_parses('[foo|="en"]') assert_selector_parses(':dir(ltr)') assert_selector_parses(':lang(fr)') assert_selector_parses(':lang(zh, *-hant)') assert_selector_parses(':any-link') assert_selector_parses(':link') assert_selector_parses(':visited') assert_selector_parses(':local-link') assert_selector_parses(':local-link(0)') assert_selector_parses(':target') assert_selector_parses(':scope') assert_selector_parses(':current') assert_selector_parses(':current(s)') assert_selector_parses(':past') assert_selector_parses(':future') assert_selector_parses(':active') assert_selector_parses(':hover') assert_selector_parses(':focus') assert_selector_parses(':enabled') assert_selector_parses(':disabled') assert_selector_parses(':checked') assert_selector_parses(':indeterminate') assert_selector_parses(':default') assert_selector_parses(':in-range') assert_selector_parses(':out-of-range') assert_selector_parses(':required') assert_selector_parses(':optional') assert_selector_parses(':read-only') assert_selector_parses(':read-write') assert_selector_parses(':root') assert_selector_parses(':empty') assert_selector_parses(':first-child') assert_selector_parses(':nth-child(n)') assert_selector_parses(':last-child') assert_selector_parses(':nth-last-child(n)') assert_selector_parses(':only-child') assert_selector_parses(':first-of-type') assert_selector_parses(':nth-of-type(n)') assert_selector_parses(':last-of-type') assert_selector_parses(':nth-last-of-type(n)') assert_selector_parses(':only-of-type') assert_selector_parses(':nth-child(n of selector)') assert_selector_parses(':nth-last-child(n of selector)') assert_selector_parses(':nth-child(n)') assert_selector_parses(':nth-last-child(n)') # From http://dev.w3.org/csswg/css-scoping-1/ assert_selector_parses(':host(s)') assert_selector_parses(':host-context(s)') end def test_attribute_selectors_with_identifiers assert_selector_parses('[foo~=bar]') assert_selector_parses('[foo^=bar]') assert_selector_parses('[foo$=bar]') assert_selector_parses('[foo*=bar]') assert_selector_parses('[foo|=en]') end def test_nth_selectors assert_selector_parses(':nth-child(-n)') assert_selector_parses(':nth-child(+n)') assert_selector_parses(':nth-child(even)') assert_selector_parses(':nth-child(odd)') assert_selector_parses(':nth-child(50)') assert_selector_parses(':nth-child(-50)') assert_selector_parses(':nth-child(+50)') assert_selector_parses(':nth-child(2n+3)') assert_selector_parses(':nth-child(2n-3)') assert_selector_parses(':nth-child(+2n-3)') assert_selector_parses(':nth-child(-2n+3)') assert_selector_parses(':nth-child(-2n+ 3)') assert_equal(<)') assert_selector_can_contain_selectors(':current()') assert_selector_can_contain_selectors(':nth-child(n of )') assert_selector_can_contain_selectors(':nth-last-child(n of )') assert_selector_can_contain_selectors(':-moz-any()') assert_selector_can_contain_selectors(':has()') assert_selector_can_contain_selectors(':has(+ )') assert_selector_can_contain_selectors(':host()') assert_selector_can_contain_selectors(':host-context()') end def assert_selector_can_contain_selectors(sel) try = lambda {|subsel| assert_selector_parses(sel.gsub('', subsel))} try['foo|bar'] try['*|bar'] try['foo|*'] try['*|*'] try['#blah'] try['.blah'] try['[foo]'] try['[foo^="bar"]'] try['[baz|foo~="bar"]'] try[':hover'] try[':nth-child(2n + 3)'] try['h1, h2, h3'] try['#foo, bar, [baz]'] # Not technically allowed for most selectors, but what the heck try[':not(#foo)'] try['a#foo.bar'] try['#foo .bar > baz'] end def test_namespaced_selectors assert_selector_parses('foo|E') assert_selector_parses('*|E') assert_selector_parses('foo|*') assert_selector_parses('*|*') end def test_namespaced_attribute_selectors assert_selector_parses('[foo|bar=baz]') assert_selector_parses('[*|bar=baz]') assert_selector_parses('[foo|bar|=baz]') end def test_comma_selectors assert_selector_parses('E, F') assert_selector_parses('E F, G H') assert_selector_parses('E > F, G > H') end def test_selectors_with_newlines assert_selector_parses("E,\nF") assert_selector_parses("E\nF") assert_selector_parses("E, F\nG, H") end def test_expression_fallback_selectors assert_directive_parses('0%') assert_directive_parses('60%') assert_directive_parses('100%') assert_directive_parses('12px') assert_directive_parses('"foo"') end def test_functional_pseudo_selectors assert_selector_parses(':foo("bar")') assert_selector_parses(':foo(bar)') assert_selector_parses(':foo(12px)') assert_selector_parses(':foo(+)') assert_selector_parses(':foo(-)') assert_selector_parses(':foo(+"bar")') assert_selector_parses(':foo(-++--baz-"bar"12px)') end def test_selector_hacks assert_selector_parses('> E') assert_selector_parses('+ E') assert_selector_parses('~ E') assert_selector_parses('> > E') assert_equal < > E { a: b; } CSS >> E { a: b; } SCSS assert_selector_parses('E*') assert_selector_parses('E*.foo') assert_selector_parses('E*:hover') end def test_spaceless_combo_selectors assert_equal "E > F {\n a: b; }\n", render("E>F { a: b;} ") assert_equal "E ~ F {\n a: b; }\n", render("E~F { a: b;} ") assert_equal "E + F {\n a: b; }\n", render("E+F { a: b;} ") end def test_escapes_in_selectors assert_selector_parses('.\!foo') assert_equal ".ffoo {\n a: b; }\n", render('.\66 foo {a: b}') assert_equal ".\\!foo {\n a: b; }\n", render('.\21 foo {a: b}') end def test_subject_selector_deprecation assert_warning(< .baz {a: b}")} DEPRECATION WARNING on line 1, column 1: The subject selector operator "!" is deprecated and will be removed in a future release. This operator has been replaced by ":has()" in the CSS spec. For example: .foo .bar:has(> .baz) WARNING assert_warning(< import "foo";') assert_not_parses("identifier", '@12 "foo";') end def test_invalid_classes assert_not_parses("class name", 'p. foo {a: b}') assert_not_parses("class name", 'p.1foo {a: b}') end def test_invalid_ids assert_not_parses("id name", 'p# foo {a: b}') end def test_no_properties_at_toplevel assert_not_parses('pseudoclass or pseudoelement', 'a: b;') end def test_no_scss_directives assert_parses('@import "foo.sass";') assert_parses <$var = 12;") assert_not_parses('"}"', "foo { !var = 12; }") end def test_no_parent_selectors assert_not_parses('"{"', "foo &.bar {a: b}") end def test_no_selector_interpolation assert_not_parses('"{"', 'foo #{"bar"}.baz {a: b}') end def test_no_prop_name_interpolation assert_not_parses('":"', 'foo {a#{"bar"}baz: b}') end def test_no_prop_val_interpolation assert_not_parses('"}"', 'foo {a: b #{"bar"} c}') end def test_no_string_interpolation assert_parses <* c}') end def test_no_nested_rules assert_not_parses('":"', 'foo {bar {a: b}}') assert_not_parses('"}"', 'foo {[bar=baz] {a: b}}') end def test_no_nested_properties assert_not_parses('expression (e.g. 1px, bold)', 'foo {bar: {a: b}}') assert_not_parses('expression (e.g. 1px, bold)', 'foo {bar: bang {a: b}}') end def test_no_nested_directives assert_not_parses('"}"', 'foo {@bar {a: b}}') end def test_error_with_windows_newlines render < e assert_equal 'Invalid CSS after "foo {bar": expected ":", was "}"', e.message assert_equal 1, e.sass_line end def test_newline_in_property_value assert_equal(<{a: b}") end def test_closing_line_comment_end_with_compact_output assert_equal(< :compact)) /* foo */ bar { baz: bang; } CSS /* * foo */ bar {baz: bang} SCSS end def test_single_line_comment_within_multiline_comment assert_equal(< e assert_equal 'Invalid CSS after "@media ": expected media query (e.g. print, screen, print and screen), was "{"', e.message assert_equal 1, e.sass_line end private def assert_selector_parses(selector) assert_parses < :nested)) @fblthp { .foo .bar { a: b; } } CSS .foo { @fblthp { .bar {a: b} } } SCSS end def test_keyframe_bubbling assert_equal(< :nested)) @keyframes spin { 0% { transform: rotate(0deg); } } @-webkit-keyframes spin { 0% { transform: rotate(0deg); } } CSS .foo { @keyframes spin { 0% {transform: rotate(0deg)} } @-webkit-keyframes spin { 0% {transform: rotate(0deg)} } } SCSS end ## Namespace Properties def test_namespace_properties assert_equal < e assert_equal 'Invalid CSS after "bar:baz calc": expected selector, was "(1 + 2)"', e.message assert_equal 2, e.sass_line end def test_namespace_properties_without_space_allowed_for_non_identifier assert_equal < e assert_equal "Extend directives may only be used within rules.", e.message assert_equal 3, e.sass_line end def test_at_root_doesnt_always_break_blocks assert_equal < e assert_equal 'Invalid CSS after "": expected selector, was "0%"', e.message assert_equal 1, e.sass_line end def test_selector_rule_in_keyframes render < e assert_equal 'Invalid CSS after "": expected keyframes selector (e.g. 10%), was ".foo"', e.message assert_equal 2, e.sass_line end def test_nested_mixin_def_is_scoped render < e assert_equal "Undefined mixin 'bar'.", e.message assert_equal 3, e.sass_line end def test_rules_beneath_properties render < e assert_equal 'Illegal nesting: Only properties may be nested beneath properties.', e.message assert_equal 3, e.sass_line end def test_uses_property_exception_with_star_hack render <; } SCSS assert(false, "Expected syntax error") rescue Sass::SyntaxError => e assert_equal 'Invalid CSS after " *bar:baz ": expected expression (e.g. 1px, bold), was "; }"', e.message assert_equal 2, e.sass_line end def test_uses_property_exception_with_colon_hack render <; } SCSS assert(false, "Expected syntax error") rescue Sass::SyntaxError => e assert_equal 'Invalid CSS after " :bar:baz ": expected expression (e.g. 1px, bold), was "; }"', e.message assert_equal 2, e.sass_line end def test_uses_rule_exception_with_dot_hack render <; } SCSS assert(false, "Expected syntax error") rescue Sass::SyntaxError => e assert_equal 'Invalid CSS after " .bar:baz ": expected expression (e.g. 1px, bold), was "; }"', e.message assert_equal 2, e.sass_line end def test_uses_property_exception_with_space_after_name render <; } SCSS assert(false, "Expected syntax error") rescue Sass::SyntaxError => e assert_equal 'Invalid CSS after " bar: baz ": expected expression (e.g. 1px, bold), was "; }"', e.message assert_equal 2, e.sass_line end def test_uses_property_exception_with_non_identifier_after_name render <; } SCSS assert(false, "Expected syntax error") rescue Sass::SyntaxError => e assert_equal 'Invalid CSS after " bar:1px ": expected expression (e.g. 1px, bold), was "; }"', e.message assert_equal 2, e.sass_line end def test_uses_property_exception_when_followed_by_open_bracket render < e assert_equal 'Invalid CSS after " bar:{baz: ": expected expression (e.g. 1px, bold), was ".fail} }"', e.message assert_equal 2, e.sass_line end def test_script_error render < e assert_equal 'Invalid CSS after " bar: "baz" * ": expected expression (e.g. 1px, bold), was "* }"', e.message assert_equal 2, e.sass_line end def test_multiline_script_syntax_error render < e assert_equal 'Invalid CSS after " "baz" * ": expected expression (e.g. 1px, bold), was "* }"', e.message assert_equal 3, e.sass_line end def test_multiline_script_runtime_error render < e assert_equal "Undefined variable: \"$bang\".", e.message assert_equal 4, e.sass_line end def test_post_multiline_script_runtime_error render < e assert_equal "Undefined variable: \"$bop\".", e.message assert_equal 5, e.sass_line end def test_multiline_property_runtime_error render < e assert_equal "Undefined variable: \"$bang\".", e.message assert_equal 4, e.sass_line end def test_post_resolution_selector_error render "\n\nfoo \#{\") bar\"} {a: b}" assert(false, "Expected syntax error") rescue Sass::SyntaxError => e assert_equal 'Invalid CSS after "foo ": expected selector, was ") bar"', e.message assert_equal 3, e.sass_line end def test_parent_in_mid_selector_error assert_raise_message(Sass::SyntaxError, < :compressed) z a,z b{display:block} CSS a , b { z & { display: block; } } SCSS end def test_if_error_line assert_raise_line(2) {render(< :compressed) foo{color:#000} CSS foo {color: darken(black, 10%)} SCSS end # ref: https://github.com/nex3/sass/issues/104 def test_no_buffer_overflow template = render < :compressed)) @import url("fallback-layout.css") supports(not (display: flex)) CSS $display-type: flex; @import url("fallback-layout.css") supports(not (display: #{$display-type})); SASS end def test_import_with_supports_clause assert_equal(< :compressed)) @import url("fallback-layout.css") supports(not (display: flex));.foo{bar:baz} CSS @import url("fallback-layout.css") supports(not (display: flex)); .foo { bar: baz; } SASS end def test_crlf # Attempt to reproduce https://github.com/sass/sass/issues/1985 assert_equal(< where an error is expected" unless scss.include?("") after, was = scss.split("") line = after.count("\n") + 1 after.gsub!(/\s*\n\s*$/, '') after.gsub!(/.*\n/, '') after = "..." + after[-15..-1] if after.size > 18 was.gsub!(/^\s*\n\s*/, '') was.gsub!(/\n.*/, '') was = was[0...15] + "..." if was.size > 18 to_render = scss.sub("", "") render(to_render) assert(false, "Expected syntax error for:\n#{to_render}\n") rescue Sass::SyntaxError => err assert_equal("Invalid CSS after \"#{after}\": expected #{expected}, was \"#{was}\"", err.message) assert_equal line, err.sass_line end def render(scss, options = {}) options[:syntax] ||= :scss munge_filename options Sass::Engine.new(scss, options).render end end ruby-sass-3.7.4/test/sass/source_map_test.rb000077500000000000000000000715571345125207600211550ustar00rootroot00000000000000# -*- coding: utf-8 -*- require File.dirname(__FILE__) + '/../test_helper' require File.dirname(__FILE__) + '/test_helper' class SourcemapTest < MiniTest::Test def test_to_json_requires_args _, sourcemap = render_with_sourcemap('') assert_raises(ArgumentError) {sourcemap.to_json({})} assert_raises(ArgumentError) {sourcemap.to_json({:css_path => 'foo'})} assert_raises(ArgumentError) {sourcemap.to_json({:sourcemap_path => 'foo'})} end def test_simple_mapping_scss assert_parses_with_sourcemap < :sass} a foo: bar /* SOME COMMENT */ :font-size 12px SASS a { foo: bar; /* SOME COMMENT */ font-size: 12px; } /*# sourceMappingURL=test.css.map */ CSS { "version": 3, "mappings": "AAAA,CAAC;EACC,GAAG,EAAE,GAAG;;EAEP,SAAS,EAAC,IAAI", "sources": ["test_simple_mapping_sass_inline.sass"], "names": [], "file": "test.css" } JSON end def test_simple_mapping_with_file_uris uri = Sass::Util.file_uri_from_path(File.absolute_path(filename_for_test(:scss))) assert_parses_with_sourcemap < :file a { foo: bar; /* SOME COMMENT */ font-size: 12px; } SCSS a { foo: bar; /* SOME COMMENT */ font-size: 12px; } /*# sourceMappingURL=test.css.map */ CSS { "version": 3, "mappings": "AAAA,CAAE;EACA,GAAG,EAAE,GAAG;EACV,kBAAkB;EAChB,SAAS,EAAE,IAAI", "sources": ["#{uri}"], "names": [], "file": "test.css" } JSON end def test_mapping_with_directory_scss options = {:filename => "scss/style.scss", :output => "css/style.css"} assert_parses_with_sourcemap < "sass/style.sass", :output => "css/style.css", :syntax => :sass} silence_warnings {assert_parses_with_sourcemap < :sass a fóó: bár SASS @charset "UTF-8"; a { fóó: bár; } /*# sourceMappingURL=test.css.map */ CSS { "version": 3, "mappings": ";AAAA,CAAC;EACC,GAAG,EAAE,GAAG", "sources": ["test_simple_charset_mapping_sass_inline.sass"], "names": [], "file": "test.css" } JSON end def test_different_charset_than_encoding_scss assert_parses_with_sourcemap(< :sass) @charset "IBM866" f\x86\x86 \x86: b SASS @charset "UTF-8"; fЖЖ { Ж: b; } /*# sourceMappingURL=test.css.map */ CSS { "version": 3, "mappings": ";AACA,GAAG;EACD,CAAC,EAAE,CAAC", "sources": ["test_different_charset_than_encoding_sass_inline.sass"], "names": [], "file": "test.css" } JSON end def test_import_sourcemap_scss assert_parses_with_mapping <<'SCSS', <<'CSS' @import {{1}}url(foo){{/1}},{{2}}url(moo) {{/2}}, {{3}}url(bar) {{/3}}; @import {{4}}url(baz) screen print{{/4}}; SCSS {{1}}@import url(foo){{/1}}; {{2}}@import url(moo){{/2}}; {{3}}@import url(bar){{/3}}; {{4}}@import url(baz) screen print{{/4}}; /*# sourceMappingURL=test.css.map */ CSS end def test_import_sourcemap_sass assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass @import {{1}}foo.css{{/1}},{{2}}moo.css{{/2}}, {{3}}bar.css{{/3}} @import {{4}}url(baz.css){{/4}} @import {{5}}url(qux.css) screen print{{/5}} SASS {{1}}@import url(foo.css){{/1}}; {{2}}@import url(moo.css){{/2}}; {{3}}@import url(bar.css){{/3}}; {{4}}@import url(baz.css){{/4}}; {{5}}@import url(qux.css) screen print{{/5}}; /*# sourceMappingURL=test.css.map */ CSS end def test_media_sourcemap_scss assert_parses_with_mapping <<'SCSS', <<'CSS' {{1}}@media screen, tv {{/1}}{ {{2}}body {{/2}}{ {{3}}max-width{{/3}}: {{4}}1070px{{/4}}; } } SCSS {{1}}@media screen, tv{{/1}} { {{2}}body{{/2}} { {{3}}max-width{{/3}}: {{4}}1070px{{/4}}; } } /*# sourceMappingURL=test.css.map */ CSS end def test_media_sourcemap_sass assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass {{1}}@media screen, tv{{/1}} {{2}}body{{/2}} {{3}}max-width{{/3}}: {{4}}1070px{{/4}} SASS {{1}}@media screen, tv{{/1}} { {{2}}body{{/2}} { {{3}}max-width{{/3}}: {{4}}1070px{{/4}}; } } /*# sourceMappingURL=test.css.map */ CSS end def test_interpolation_and_vars_sourcemap_scss assert_parses_with_mapping <<'SCSS', <<'CSS' $te: "te"; $teal: {{5}}teal{{/5}}; {{1}}p {{/1}}{ {{2}}con#{$te}nt{{/2}}: {{3}}"I a#{$te} #{5 + 10} pies!"{{/3}}; {{4}}color{{/4}}: $teal; } $name: foo; $attr: border; {{6}}p.#{$name} {{/6}}{ {{7}}#{$attr}-color{{/7}}: {{8}}blue{{/8}}; $font-size: 12px; $line-height: 30px; {{9}}font{{/9}}: {{10}}#{$font-size}/#{$line-height}{{/10}}; } SCSS {{1}}p{{/1}} { {{2}}content{{/2}}: {{3}}"I ate 15 pies!"{{/3}}; {{4}}color{{/4}}: {{5}}teal{{/5}}; } {{6}}p.foo{{/6}} { {{7}}border-color{{/7}}: {{8}}blue{{/8}}; {{9}}font{{/9}}: {{10}}12px/30px{{/10}}; } /*# sourceMappingURL=test.css.map */ CSS end def test_interpolation_and_vars_sourcemap_sass assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass $te: "te" $teal: {{5}}teal{{/5}} {{1}}p{{/1}} {{2}}con#{$te}nt{{/2}}: {{3}}"I a#{$te} #{5 + 10} pies!"{{/3}} {{4}}color{{/4}}: $teal $name: foo $attr: border {{6}}p.#{$name}{{/6}} {{7}}#{$attr}-color{{/7}}: {{8}}blue{{/8}} $font-size: 12px $line-height: 30px {{9}}font{{/9}}: {{10}}#{$font-size}/#{$line-height}{{/10}} SASS {{1}}p{{/1}} { {{2}}content{{/2}}: {{3}}"I ate 15 pies!"{{/3}}; {{4}}color{{/4}}: {{5}}teal{{/5}}; } {{6}}p.foo{{/6}} { {{7}}border-color{{/7}}: {{8}}blue{{/8}}; {{9}}font{{/9}}: {{10}}12px/30px{{/10}}; } /*# sourceMappingURL=test.css.map */ CSS end def test_selectors_properties_sourcemap_scss assert_parses_with_mapping <<'SCSS', <<'CSS' $width: 2px; $translucent-red: rgba(255, 0, 0, 0.5); {{1}}a {{/1}}{ {{9}}.special {{/9}}{ {{10}}color{{/10}}: {{11}}red{{/11}}; {{12}}&:hover {{/12}}{ {{13}}foo{{/13}}: {{14}}bar{{/14}}; {{15}}cursor{{/15}}: {{16}}e + -resize{{/16}}; {{17}}color{{/17}}: {{18}}opacify($translucent-red, 0.3){{/18}}; } {{19}}&:after {{/19}}{ {{20}}content{{/20}}: {{21}}"I ate #{5 + 10} pies #{$width} thick!"{{/21}}; } } {{22}}&:active {{/22}}{ {{23}}border{{/23}}: {{24}}$width solid black{{/24}}; } {{2}}/* SOME COMMENT */{{/2}} {{3}}font{{/3}}: {{4}}2px/3px {{/4}}{ {{5}}family{{/5}}: {{6}}fantasy{{/6}}; {{7}}size{{/7}}: {{8}}1em + (2em * 3){{/8}}; } } SCSS {{1}}a{{/1}} { {{2}}/* SOME COMMENT */{{/2}} {{3}}font{{/3}}: {{4}}2px/3px{{/4}}; {{5}}font-family{{/5}}: {{6}}fantasy{{/6}}; {{7}}font-size{{/7}}: {{8}}7em{{/8}}; } {{9}}a .special{{/9}} { {{10}}color{{/10}}: {{11}}red{{/11}}; } {{12}}a .special:hover{{/12}} { {{13}}foo{{/13}}: {{14}}bar{{/14}}; {{15}}cursor{{/15}}: {{16}}e-resize{{/16}}; {{17}}color{{/17}}: {{18}}rgba(255, 0, 0, 0.8){{/18}}; } {{19}}a .special:after{{/19}} { {{20}}content{{/20}}: {{21}}"I ate 15 pies 2px thick!"{{/21}}; } {{22}}a:active{{/22}} { {{23}}border{{/23}}: {{24}}2px solid black{{/24}}; } /*# sourceMappingURL=test.css.map */ CSS end def test_selectors_properties_sourcemap_sass assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass $width: 2px $translucent-red: rgba(255, 0, 0, 0.5) {{1}}a{{/1}} {{9}}.special{{/9}} {{10}}color{{/10}}: {{11}}red{{/11}} {{12}}&:hover{{/12}} {{13}}foo{{/13}}: {{14}}bar{{/14}} {{15}}cursor{{/15}}: {{16}}e + -resize{{/16}} {{17}}color{{/17}}: {{18}}opacify($translucent-red, 0.3){{/18}} {{19}}&:after{{/19}} {{20}}content{{/20}}: {{21}}"I ate #{5 + 10} pies #{$width} thick!"{{/21}} {{22}}&:active{{/22}} {{23}}border{{/23}}: {{24}}$width solid black{{/24}} {{2}}/* SOME COMMENT */{{/2}} {{3}}font{{/3}}: {{4}}2px/3px{{/4}} {{5}}family{{/5}}: {{6}}fantasy{{/6}} {{7}}size{{/7}}: {{8}}1em + (2em * 3){{/8}} SASS {{1}}a{{/1}} { {{2}}/* SOME COMMENT */{{/2}} {{3}}font{{/3}}: {{4}}2px/3px{{/4}}; {{5}}font-family{{/5}}: {{6}}fantasy{{/6}}; {{7}}font-size{{/7}}: {{8}}7em{{/8}}; } {{9}}a .special{{/9}} { {{10}}color{{/10}}: {{11}}red{{/11}}; } {{12}}a .special:hover{{/12}} { {{13}}foo{{/13}}: {{14}}bar{{/14}}; {{15}}cursor{{/15}}: {{16}}e-resize{{/16}}; {{17}}color{{/17}}: {{18}}rgba(255, 0, 0, 0.8){{/18}}; } {{19}}a .special:after{{/19}} { {{20}}content{{/20}}: {{21}}"I ate 15 pies 2px thick!"{{/21}}; } {{22}}a:active{{/22}} { {{23}}border{{/23}}: {{24}}2px solid black{{/24}}; } /*# sourceMappingURL=test.css.map */ CSS end def test_extend_sourcemap_scss assert_parses_with_mapping <<'SCSS', <<'CSS' {{1}}.error {{/1}}{ {{2}}border{{/2}}: {{3}}1px #ff00aa{{/3}}; {{4}}background-color{{/4}}: {{5}}#fdd{{/5}}; } {{6}}.seriousError {{/6}}{ @extend .error; {{7}}border-width{{/7}}: {{8}}3px{{/8}}; } SCSS {{1}}.error, .seriousError{{/1}} { {{2}}border{{/2}}: {{3}}1px #ff00aa{{/3}}; {{4}}background-color{{/4}}: {{5}}#fdd{{/5}}; } {{6}}.seriousError{{/6}} { {{7}}border-width{{/7}}: {{8}}3px{{/8}}; } /*# sourceMappingURL=test.css.map */ CSS end def test_extend_sourcemap_sass assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass {{1}}.error{{/1}} {{2}}border{{/2}}: {{3}}1px #f00{{/3}} {{4}}background-color{{/4}}: {{5}}#fdd{{/5}} {{6}}.seriousError{{/6}} @extend .error {{7}}border-width{{/7}}: {{8}}3px{{/8}} SASS {{1}}.error, .seriousError{{/1}} { {{2}}border{{/2}}: {{3}}1px #f00{{/3}}; {{4}}background-color{{/4}}: {{5}}#fdd{{/5}}; } {{6}}.seriousError{{/6}} { {{7}}border-width{{/7}}: {{8}}3px{{/8}}; } /*# sourceMappingURL=test.css.map */ CSS end def test_for_sourcemap_scss assert_parses_with_mapping <<'SCSS', <<'CSS' @for $i from 1 through 3 { {{1}}{{4}}{{7}}.item-#{$i} {{/1}}{{/4}}{{/7}}{ {{2}}{{5}}{{8}}width{{/2}}{{/5}}{{/8}}: {{3}}{{6}}{{9}}2em * $i{{/3}}{{/6}}{{/9}}; } } SCSS {{1}}.item-1{{/1}} { {{2}}width{{/2}}: {{3}}2em{{/3}}; } {{4}}.item-2{{/4}} { {{5}}width{{/5}}: {{6}}4em{{/6}}; } {{7}}.item-3{{/7}} { {{8}}width{{/8}}: {{9}}6em{{/9}}; } /*# sourceMappingURL=test.css.map */ CSS end def test_for_sourcemap_sass assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass @for $i from 1 through 3 {{1}}{{4}}{{7}}.item-#{$i}{{/1}}{{/4}}{{/7}} {{2}}{{5}}{{8}}width{{/2}}{{/5}}{{/8}}: {{3}}{{6}}{{9}}2em * $i{{/3}}{{/6}}{{/9}} SASS {{1}}.item-1{{/1}} { {{2}}width{{/2}}: {{3}}2em{{/3}}; } {{4}}.item-2{{/4}} { {{5}}width{{/5}}: {{6}}4em{{/6}}; } {{7}}.item-3{{/7}} { {{8}}width{{/8}}: {{9}}6em{{/9}}; } /*# sourceMappingURL=test.css.map */ CSS end def test_while_sourcemap_scss assert_parses_with_mapping <<'SCSS', <<'CSS' $i: 6; @while $i > 0 { {{1}}{{4}}{{7}}.item-#{$i} {{/1}}{{/4}}{{/7}}{ {{2}}{{5}}{{8}}width{{/2}}{{/5}}{{/8}}: {{3}}{{6}}{{9}}2em * $i{{/3}}{{/6}}{{/9}}; } $i: $i - 2 !global; } SCSS {{1}}.item-6{{/1}} { {{2}}width{{/2}}: {{3}}12em{{/3}}; } {{4}}.item-4{{/4}} { {{5}}width{{/5}}: {{6}}8em{{/6}}; } {{7}}.item-2{{/7}} { {{8}}width{{/8}}: {{9}}4em{{/9}}; } /*# sourceMappingURL=test.css.map */ CSS end def test_while_sourcemap_sass assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass $i: 6 @while $i > 0 {{1}}{{4}}{{7}}.item-#{$i}{{/1}}{{/4}}{{/7}} {{2}}{{5}}{{8}}width{{/2}}{{/5}}{{/8}}: {{3}}{{6}}{{9}}2em * $i{{/3}}{{/6}}{{/9}} $i: $i - 2 !global SASS {{1}}.item-6{{/1}} { {{2}}width{{/2}}: {{3}}12em{{/3}}; } {{4}}.item-4{{/4}} { {{5}}width{{/5}}: {{6}}8em{{/6}}; } {{7}}.item-2{{/7}} { {{8}}width{{/8}}: {{9}}4em{{/9}}; } /*# sourceMappingURL=test.css.map */ CSS end def test_each_sourcemap_scss assert_parses_with_mapping <<'SCSS', <<'CSS' @each $animal in puma, sea-slug, egret, salamander { {{1}}{{4}}{{7}}{{10}}.#{$animal}-icon {{/1}}{{/4}}{{/7}}{{/10}}{ {{2}}{{5}}{{8}}{{11}}background-image{{/2}}{{/5}}{{/8}}{{/11}}: {{3}}{{6}}{{9}}{{12}}url('/images/#{$animal}.png'){{/3}}{{/6}}{{/9}}{{/12}}; } } SCSS {{1}}.puma-icon{{/1}} { {{2}}background-image{{/2}}: {{3}}url("/images/puma.png"){{/3}}; } {{4}}.sea-slug-icon{{/4}} { {{5}}background-image{{/5}}: {{6}}url("/images/sea-slug.png"){{/6}}; } {{7}}.egret-icon{{/7}} { {{8}}background-image{{/8}}: {{9}}url("/images/egret.png"){{/9}}; } {{10}}.salamander-icon{{/10}} { {{11}}background-image{{/11}}: {{12}}url("/images/salamander.png"){{/12}}; } /*# sourceMappingURL=test.css.map */ CSS end def test_each_sourcemap_sass assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass @each $animal in puma, sea-slug, egret, salamander {{1}}{{4}}{{7}}{{10}}.#{$animal}-icon{{/1}}{{/4}}{{/7}}{{/10}} {{2}}{{5}}{{8}}{{11}}background-image{{/2}}{{/5}}{{/8}}{{/11}}: {{3}}{{6}}{{9}}{{12}}url('/images/#{$animal}.png'){{/3}}{{/6}}{{/9}}{{/12}} SASS {{1}}.puma-icon{{/1}} { {{2}}background-image{{/2}}: {{3}}url("/images/puma.png"){{/3}}; } {{4}}.sea-slug-icon{{/4}} { {{5}}background-image{{/5}}: {{6}}url("/images/sea-slug.png"){{/6}}; } {{7}}.egret-icon{{/7}} { {{8}}background-image{{/8}}: {{9}}url("/images/egret.png"){{/9}}; } {{10}}.salamander-icon{{/10}} { {{11}}background-image{{/11}}: {{12}}url("/images/salamander.png"){{/12}}; } /*# sourceMappingURL=test.css.map */ CSS end def test_mixin_sourcemap_scss assert_parses_with_mapping <<'SCSS', <<'CSS' @mixin large-text { font: { {{2}}size{{/2}}: {{3}}20px{{/3}}; {{4}}weight{{/4}}: {{5}}bold{{/5}}; } {{6}}color{{/6}}: {{7}}#ff0000{{/7}}; } {{1}}.page-title {{/1}}{ @include large-text; {{8}}padding{{/8}}: {{9}}4px{{/9}}; } @mixin dashed-border($color, $width: {{14}}1in{{/14}}) { border: { {{11}}{{18}}color{{/11}}{{/18}}: $color; {{13}}{{20}}width{{/13}}{{/20}}: $width; {{15}}{{22}}style{{/15}}{{/22}}: {{16}}{{23}}dashed{{/16}}{{/23}}; } } {{10}}p {{/10}}{ @include dashed-border({{12}}blue{{/12}}); } {{17}}h1 {{/17}}{ @include dashed-border({{19}}blue{{/19}}, {{21}}2in{{/21}}); } @mixin box-shadow($shadows...) { {{25}}-moz-box-shadow{{/25}}: {{26}}$shadows{{/26}}; {{27}}-webkit-box-shadow{{/27}}: {{28}}$shadows{{/28}}; {{29}}box-shadow{{/29}}: {{30}}$shadows{{/30}}; } {{24}}.shadows {{/24}}{ @include box-shadow(0px 4px 5px #666, 2px 6px 10px #999); } SCSS {{1}}.page-title{{/1}} { {{2}}font-size{{/2}}: {{3}}20px{{/3}}; {{4}}font-weight{{/4}}: {{5}}bold{{/5}}; {{6}}color{{/6}}: {{7}}#ff0000{{/7}}; {{8}}padding{{/8}}: {{9}}4px{{/9}}; } {{10}}p{{/10}} { {{11}}border-color{{/11}}: {{12}}blue{{/12}}; {{13}}border-width{{/13}}: {{14}}1in{{/14}}; {{15}}border-style{{/15}}: {{16}}dashed{{/16}}; } {{17}}h1{{/17}} { {{18}}border-color{{/18}}: {{19}}blue{{/19}}; {{20}}border-width{{/20}}: {{21}}2in{{/21}}; {{22}}border-style{{/22}}: {{23}}dashed{{/23}}; } {{24}}.shadows{{/24}} { {{25}}-moz-box-shadow{{/25}}: {{26}}0px 4px 5px #666, 2px 6px 10px #999{{/26}}; {{27}}-webkit-box-shadow{{/27}}: {{28}}0px 4px 5px #666, 2px 6px 10px #999{{/28}}; {{29}}box-shadow{{/29}}: {{30}}0px 4px 5px #666, 2px 6px 10px #999{{/30}}; } /*# sourceMappingURL=test.css.map */ CSS end def test_mixin_sourcemap_sass silence_warnings {assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass} =large-text :font {{2}}size{{/2}}: {{3}}20px{{/3}} {{4}}weight{{/4}}: {{5}}bold{{/5}} {{6}}color{{/6}}: {{7}}#ff0000{{/7}} {{1}}.page-title{{/1}} +large-text {{8}}padding{{/8}}: {{9}}4px{{/9}} =dashed-border($color, $width: {{14}}1in{{/14}}) border: {{11}}{{18}}color{{/11}}{{/18}}: $color {{13}}{{20}}width{{/13}}{{/20}}: $width {{15}}{{22}}style{{/15}}{{/22}}: {{16}}{{23}}dashed{{/16}}{{/23}} {{10}}p{{/10}} +dashed-border({{12}}blue{{/12}}) {{17}}h1{{/17}} +dashed-border({{19}}blue{{/19}}, {{21}}2in{{/21}}) =box-shadow($shadows...) {{25}}-moz-box-shadow{{/25}}: {{26}}$shadows{{/26}} {{27}}-webkit-box-shadow{{/27}}: {{28}}$shadows{{/28}} {{29}}box-shadow{{/29}}: {{30}}$shadows{{/30}} {{24}}.shadows{{/24}} +box-shadow(0px 4px 5px #666, 2px 6px 10px #999) SASS {{1}}.page-title{{/1}} { {{2}}font-size{{/2}}: {{3}}20px{{/3}}; {{4}}font-weight{{/4}}: {{5}}bold{{/5}}; {{6}}color{{/6}}: {{7}}#ff0000{{/7}}; {{8}}padding{{/8}}: {{9}}4px{{/9}}; } {{10}}p{{/10}} { {{11}}border-color{{/11}}: {{12}}blue{{/12}}; {{13}}border-width{{/13}}: {{14}}1in{{/14}}; {{15}}border-style{{/15}}: {{16}}dashed{{/16}}; } {{17}}h1{{/17}} { {{18}}border-color{{/18}}: {{19}}blue{{/19}}; {{20}}border-width{{/20}}: {{21}}2in{{/21}}; {{22}}border-style{{/22}}: {{23}}dashed{{/23}}; } {{24}}.shadows{{/24}} { {{25}}-moz-box-shadow{{/25}}: {{26}}0px 4px 5px #666, 2px 6px 10px #999{{/26}}; {{27}}-webkit-box-shadow{{/27}}: {{28}}0px 4px 5px #666, 2px 6px 10px #999{{/28}}; {{29}}box-shadow{{/29}}: {{30}}0px 4px 5px #666, 2px 6px 10px #999{{/30}}; } /*# sourceMappingURL=test.css.map */ CSS end def test_function_sourcemap_scss assert_parses_with_mapping <<'SCSS', <<'CSS' $grid-width: 20px; $gutter-width: 5px; @function grid-width($n) { @return $n * $grid-width + ($n - 1) * $gutter-width; } {{1}}sidebar {{/1}}{ {{2}}width{{/2}}: {{3}}grid-width(5){{/3}}; } SCSS {{1}}sidebar{{/1}} { {{2}}width{{/2}}: {{3}}120px{{/3}}; } /*# sourceMappingURL=test.css.map */ CSS end def test_function_sourcemap_sass assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass $grid-width: 20px $gutter-width: 5px @function grid-width($n) @return $n * $grid-width + ($n - 1) * $gutter-width {{1}}sidebar{{/1}} {{2}}width{{/2}}: {{3}}grid-width(5){{/3}} SASS {{1}}sidebar{{/1}} { {{2}}width{{/2}}: {{3}}120px{{/3}}; } /*# sourceMappingURL=test.css.map */ CSS end # Regression tests def test_properties_sass silence_warnings {assert_parses_with_mapping < :sass} {{1}}.foo{{/1}} :{{2}}name{{/2}} {{3}}value{{/3}} {{4}}name{{/4}}: {{5}}value{{/5}} :{{6}}name{{/6}} {{7}}value{{/7}} {{8}}name{{/8}}: {{9}}value{{/9}} SASS {{1}}.foo{{/1}} { {{2}}name{{/2}}: {{3}}value{{/3}}; {{4}}name{{/4}}: {{5}}value{{/5}}; {{6}}name{{/6}}: {{7}}value{{/7}}; {{8}}name{{/8}}: {{9}}value{{/9}}; } /*# sourceMappingURL=test.css.map */ CSS end def test_multiline_script_scss assert_parses_with_mapping < :scss $var: {{3}}foo + bar{{/3}}; {{1}}x {{/1}}{ {{2}}y{{/2}}: $var } SCSS {{1}}x{{/1}} { {{2}}y{{/2}}: {{3}}foobar{{/3}}; } /*# sourceMappingURL=test.css.map */ CSS end def test_multiline_interpolation_source_range engine = Sass::Engine.new(<<-SCSS, :cache => false, :syntax => :scss) p { filter: progid:DXImageTransform( '\#{123}'); } SCSS interpolated = engine.to_tree.children. first.children. first.value.first.children[1] assert_equal "123", interpolated.to_sass range = interpolated.source_range assert_equal 3, range.start_pos.line assert_equal 14, range.start_pos.offset assert_equal 3, range.end_pos.line assert_equal 17, range.end_pos.offset end def test_list_source_range engine = Sass::Engine.new(<<-SCSS, :cache => false, :syntax => :scss) @each $a, $b in (1, 2), (2, 4), (3, 6) { } SCSS list = engine.to_tree.children.first.list assert_equal 1, list.source_range.start_pos.line assert_equal 1, list.source_range.end_pos.line assert_equal 16, list.source_range.start_pos.offset assert_equal 38, list.source_range.end_pos.offset end def test_map_source_range engine = Sass::Engine.new(<<-SCSS, :cache => false, :syntax => :scss) $margins: (sm: 4px, md: 8px, lg: 16px); SCSS expr = engine.to_tree.children.first.expr assert_equal 1, expr.source_range.start_pos.line assert_equal 1, expr.source_range.end_pos.line assert_equal 12, expr.source_range.start_pos.offset assert_equal 38, expr.source_range.end_pos.offset end def test_sources_array_is_uri_escaped map = Sass::Source::Map.new importer = Sass::Importers::Filesystem.new('.') map.add( Sass::Source::Range.new( Sass::Source::Position.new(0, 0), Sass::Source::Position.new(0, 10), 'source file.scss', importer), Sass::Source::Range.new( Sass::Source::Position.new(0, 0), Sass::Source::Position.new(0, 10), nil, nil)) json = map.to_json(:css_path => 'output file.css', :sourcemap_path => 'output file.css.map') assert_equal json, < :scss $var: val; {{1}}/* text */{{/1}} {{2}}/* multiline comment */{{/2}} SCSS {{1}}/* text */{{/1}} {{2}}/* multiline comment */{{/2}} /*# sourceMappingURL=test.css.map */ CSS end def test_sass_comment_source_range assert_parses_with_mapping < :sass {{1}}body{{/1}} {{2}}/* text */{{/2}} {{3}}/* multiline comment */{{/3}} SASS {{1}}body{{/1}} { {{2}}/* text */{{/2}} } {{3}}/* multiline * comment */{{/3}} /*# sourceMappingURL=test.css.map */ CSS end def test_scss_comment_interpolation_source_range assert_parses_with_mapping < :scss $var: 2; {{1}}/* two \#{$var} and four \#{2 * $var} */{{/1}} {{2}}/* multiline comment \#{ 2 + 2 } and \#{ 2 + 2 } */{{/2}} SCSS {{1}}/* two 2 and four 4 */{{/1}} {{2}}/* multiline comment 4 and 4 */{{/2}} /*# sourceMappingURL=test.css.map */ CSS end def test_sass_comment_interpolation_source_range assert_parses_with_mapping < :sass $var: 2 {{1}}/* two \#{$var} and four \#{2 * $var} */{{/1}} {{2}}/* multiline comment \#{ 2 + 2 } and \#{ 2 + 2 } */{{/2}} SASS {{1}}/* two 2 and four 4 */{{/1}} {{2}}/* multiline * comment 4 and 4 */{{/2}} /*# sourceMappingURL=test.css.map */ CSS end private ANNOTATION_REGEX = /\{\{(\/?)([^}]+)\}\}/ def build_ranges(text, file_name = nil) ranges = Hash.new {|h, k| h[k] = []} start_positions = {} text.split("\n").each_with_index do |line_text, line| line += 1 # lines shoud be 1-based while (match = line_text.match(ANNOTATION_REGEX)) closing = !match[1].empty? name = match[2] match_offsets = match.offset(0) offset = match_offsets[0] + 1 # Offsets are 1-based in source maps. assert(!closing || start_positions[name], "Closing annotation #{name} found before opening one.") position = Sass::Source::Position.new(line, offset) if closing ranges[name] << Sass::Source::Range.new( start_positions[name], position, file_name, Sass::Importers::Filesystem.new('.')) start_positions.delete name else assert(!start_positions[name], "Overlapping range annotation #{name} encountered on line #{line}") start_positions[name] = position end line_text.slice!(match_offsets[0], match_offsets[1] - match_offsets[0]) end end ranges end def build_mapping_from_annotations(source, css, source_file_name) source_ranges = build_ranges(source, source_file_name) target_ranges = build_ranges(css) map = Sass::Source::Map.new source_ranges.map do |(name, sources)| assert(sources.length == 1, "#{sources.length} source ranges encountered for annotation #{name}") assert(target_ranges[name], "No target ranges for annotation #{name}") target_ranges[name].map {|target_range| [sources.first, target_range]} end. flatten(1). sort_by {|(_, target)| [target.start_pos.line, target.start_pos.offset]}. each {|(s2, target)| map.add(s2, target)} map end def assert_parses_with_mapping(source, css, options={}) options[:syntax] ||= :scss input_filename = filename_for_test(options[:syntax]) mapping = build_mapping_from_annotations(source, css, input_filename) source.gsub!(ANNOTATION_REGEX, "") css.gsub!(ANNOTATION_REGEX, "") rendered, sourcemap = render_with_sourcemap(source, options) assert_equal css.rstrip, rendered.rstrip assert_sourcemaps_equal source, css, mapping, sourcemap end def assert_positions_equal(expected, actual, lines, message = nil) prefix = message ? message + ": " : "" expected_location = lines[expected.line - 1] + "\n" + ("-" * (expected.offset - 1)) + "^" actual_location = lines[actual.line - 1] + "\n" + ("-" * (actual.offset - 1)) + "^" assert_equal(expected.line, actual.line, prefix + "Expected #{expected.inspect}\n" + expected_location + "\n\n" + "But was #{actual.inspect}\n" + actual_location) assert_equal(expected.offset, actual.offset, prefix + "Expected #{expected.inspect}\n" + expected_location + "\n\n" + "But was #{actual.inspect}\n" + actual_location) end def assert_ranges_equal(expected, actual, lines, prefix) assert_positions_equal(expected.start_pos, actual.start_pos, lines, prefix + " start position") assert_positions_equal(expected.end_pos, actual.end_pos, lines, prefix + " end position") if expected.file.nil? assert_nil(actual.file) else assert_equal(expected.file, actual.file) end end def assert_sourcemaps_equal(source, css, expected, actual) assert_equal(expected.data.length, actual.data.length, < css_path, :sourcemap_path => sourcemap_path, :type => options[:sourcemap]) assert_equal css.rstrip, rendered.rstrip assert_equal sourcemap_json.rstrip, rendered_json end def render_with_sourcemap(source, options={}) options[:syntax] ||= :scss munge_filename options engine = Sass::Engine.new(source, options) engine.options[:cache] = false sourcemap_path = Sass::Util.sourcemap_name(options[:output] || "test.css") engine.render_with_sourcemap File.basename(sourcemap_path) end def dump_sourcemap_as_expectation(source, css, sourcemap) mappings_to_annotations(source, sourcemap.data.map {|d| d.input}) + "\n\n" + "=" * 20 + " maps to:\n\n" + mappings_to_annotations(css, sourcemap.data.map {|d| d.output}) end def mappings_to_annotations(source, ranges) additional_offsets = Hash.new(0) lines = source.split("\n") add_annotation = lambda do |pos, str| line_num = pos.line - 1 line = lines[line_num] offset = pos.offset + additional_offsets[line_num] - 1 line << " " * (offset - line.length) if offset > line.length line.insert(offset, str) additional_offsets[line_num] += str.length end ranges.each_with_index do |range, i| add_annotation[range.start_pos, "{{#{i + 1}}}"] add_annotation[range.end_pos, "{{/#{i + 1}}}"] end return lines.join("\n") end end ruby-sass-3.7.4/test/sass/superselector_test.rb000077500000000000000000000174561345125207600217150ustar00rootroot00000000000000require File.dirname(__FILE__) + '/../test_helper' class SuperselectorTest < MiniTest::Test def test_superselector_reflexivity assert_superselector 'h1', 'h1' assert_superselector '.foo', '.foo' assert_superselector '#foo > .bar, baz', '#foo > .bar, baz' end def test_smaller_compound_superselector assert_strict_superselector '.foo', '.foo.bar' assert_strict_superselector '.bar', '.foo.bar' assert_strict_superselector 'a', 'a#b' assert_strict_superselector '#b', 'a#b' end def test_smaller_complex_superselector assert_strict_superselector '.bar', '.foo .bar' assert_strict_superselector '.bar', '.foo > .bar' assert_strict_superselector '.bar', '.foo + .bar' assert_strict_superselector '.bar', '.foo ~ .bar' end def test_selector_list_subset_superselector assert_strict_superselector '.foo, .bar', '.foo' assert_strict_superselector '.foo, .bar, .baz', '.foo, .baz' assert_strict_superselector '.foo, .baz, .qux', '.foo.bar, .baz.bang' end def test_leading_combinator_superselector refute_superselector '+ .foo', '.foo' refute_superselector '+ .foo', '.bar + .foo' end def test_trailing_combinator_superselector refute_superselector '.foo +', '.foo' refute_superselector '.foo +', '.foo + .bar' end def test_matching_combinator_superselector assert_strict_superselector '.foo + .bar', '.foo + .bar.baz' assert_strict_superselector '.foo + .bar', '.foo.baz + .bar' assert_strict_superselector '.foo > .bar', '.foo > .bar.baz' assert_strict_superselector '.foo > .bar', '.foo.baz > .bar' assert_strict_superselector '.foo ~ .bar', '.foo ~ .bar.baz' assert_strict_superselector '.foo ~ .bar', '.foo.baz ~ .bar' end def test_following_sibling_is_superselector_of_next_sibling assert_strict_superselector '.foo ~ .bar', '.foo + .bar.baz' assert_strict_superselector '.foo ~ .bar', '.foo.baz + .bar' end def test_descendant_is_superselector_of_child assert_strict_superselector '.foo .bar', '.foo > .bar.baz' assert_strict_superselector '.foo .bar', '.foo.baz > .bar' assert_strict_superselector '.foo .baz', '.foo > .bar > .baz' end def test_child_isnt_superselector_of_longer_child refute_superselector '.foo > .baz', '.foo > .bar > .baz' refute_superselector '.foo > .baz', '.foo > .bar .baz' end def test_following_sibling_isnt_superselector_of_longer_following_sibling refute_superselector '.foo + .baz', '.foo + .bar + .baz' refute_superselector '.foo + .baz', '.foo + .bar .baz' end def test_sibling_isnt_superselector_of_longer_sibling # This actually is a superselector, but it's a very narrow edge case and # detecting it is very difficult and may be exponential in the worst case. refute_superselector '.foo ~ .baz', '.foo ~ .bar ~ .baz' refute_superselector '.foo ~ .baz', '.foo ~ .bar .baz' end def test_matches_is_superselector_of_constituent_selectors %w[matches -moz-any].each do |name| assert_strict_superselector ":#{name}(.foo, .bar)", '.foo.baz' assert_strict_superselector ":#{name}(.foo, .bar)", '.bar.baz' assert_strict_superselector ":#{name}(.foo .bar, .baz)", '.x .foo .bar' end end def test_matches_is_superselector_of_subset_matches assert_strict_superselector ':matches(.foo, .bar, .baz)', '#x:matches(.foo.bip, .baz.bang)' assert_strict_superselector ':-moz-any(.foo, .bar, .baz)', '#x:-moz-any(.foo.bip, .baz.bang)' end def test_matches_is_not_superselector_of_any refute_superselector ':matches(.foo, .bar)', ':-moz-any(.foo, .bar)' refute_superselector ':-moz-any(.foo, .bar)', ':matches(.foo, .bar)' end def test_matches_can_be_subselector %w[matches -moz-any].each do |name| assert_superselector '.foo', ":#{name}(.foo.bar)" assert_superselector '.foo.bar', ":#{name}(.foo.bar.baz)" assert_superselector '.foo', ":#{name}(.foo.bar, .foo.baz)" end end def test_any_is_not_superselector_of_different_prefix refute_superselector ':-moz-any(.foo, .bar)', ':-s-any(.foo, .bar)' end def test_not_is_superselector_of_less_complex_not assert_strict_superselector ':not(.foo.bar)', ':not(.foo)' assert_strict_superselector ':not(.foo .bar)', ':not(.bar)' end def test_not_is_superselector_of_superset assert_strict_superselector ':not(.foo.bip, .baz.bang)', ':not(.foo, .bar, .baz)' assert_strict_superselector ':not(.foo.bip, .baz.bang)', ':not(.foo):not(.bar):not(.baz)' end def test_not_is_superselector_of_unique_selectors assert_strict_superselector ':not(h1.foo)', 'a' assert_strict_superselector ':not(.baz #foo)', '#bar' end def test_not_is_not_superselector_of_non_unique_selectors refute_superselector ':not(.foo)', '.bar' refute_superselector ':not(:hover)', ':visited' end def test_current_is_superselector_with_identical_innards assert_superselector ':current(.foo)', ':current(.foo)' end def test_current_is_superselector_with_subselector_innards refute_superselector ':current(.foo)', ':current(.foo.bar)' refute_superselector ':current(.foo.bar)', ':current(.foo)' end def test_nth_match_is_superselector_of_subset_nth_match assert_strict_superselector( ':nth-child(2n of .foo, .bar, .baz)', '#x:nth-child(2n of .foo.bip, .baz.bang)') assert_strict_superselector( ':nth-last-child(2n of .foo, .bar, .baz)', '#x:nth-last-child(2n of .foo.bip, .baz.bang)') end def test_nth_match_is_not_superselector_of_nth_match_with_different_arg refute_superselector( ':nth-child(2n of .foo, .bar, .baz)', '#x:nth-child(2n + 1 of .foo.bip, .baz.bang)') refute_superselector( ':nth-last-child(2n of .foo, .bar, .baz)', '#x:nth-last-child(2n + 1 of .foo.bip, .baz.bang)') end def test_nth_match_is_not_superselector_of_nth_last_match refute_superselector ':nth-child(2n of .foo, .bar)', ':nth-last-child(2n of .foo, .bar)' refute_superselector ':nth-last-child(2n of .foo, .bar)', ':nth-child(2n of .foo, .bar)' end def test_nth_match_can_be_subselector %w[nth-child nth-last-child].each do |name| assert_superselector '.foo', ":#{name}(2n of .foo.bar)" assert_superselector '.foo.bar', ":#{name}(2n of .foo.bar.baz)" assert_superselector '.foo', ":#{name}(2n of .foo.bar, .foo.baz)" end end def has_is_superselector_of_subset_host assert_strict_superselector ':has(.foo, .bar, .baz)', ':has(.foo.bip, .baz.bang)' end def has_isnt_superselector_of_contained_selector assert_strict_superselector ':has(.foo, .bar, .baz)', '.foo' end def host_is_superselector_of_subset_host assert_strict_superselector ':host(.foo, .bar, .baz)', ':host(.foo.bip, .baz.bang)' end def host_isnt_superselector_of_contained_selector assert_strict_superselector ':host(.foo, .bar, .baz)', '.foo' end def host_context_is_superselector_of_subset_host assert_strict_superselector( ':host-context(.foo, .bar, .baz)', ':host-context(.foo.bip, .baz.bang)') end def host_context_isnt_superselector_of_contained_selector assert_strict_superselector ':host-context(.foo, .bar, .baz)', '.foo' end private def assert_superselector(superselector, subselector) assert(parse_selector(superselector).superselector?(parse_selector(subselector)), "Expected #{superselector} to be a superselector of #{subselector}.") end def refute_superselector(superselector, subselector) assert(!parse_selector(superselector).superselector?(parse_selector(subselector)), "Expected #{superselector} not to be a superselector of #{subselector}.") end def assert_strict_superselector(superselector, subselector) assert_superselector(superselector, subselector) refute_superselector(subselector, superselector) end def parse_selector(selector) Sass::SCSS::CssParser.new(selector, filename_for_test, nil).parse_selector end end ruby-sass-3.7.4/test/sass/templates/000077500000000000000000000000001345125207600174105ustar00rootroot00000000000000ruby-sass-3.7.4/test/sass/templates/_cached_import_option_partial.scss000066400000000000000000000000341345125207600263460ustar00rootroot00000000000000partial {value: whatever()} ruby-sass-3.7.4/test/sass/templates/_double_import_loop2.sass000066400000000000000000000000351345125207600244170ustar00rootroot00000000000000@import "double_import_loop1"ruby-sass-3.7.4/test/sass/templates/_filename_fn_import.scss000066400000000000000000000002271345125207600243020ustar00rootroot00000000000000@mixin imported-mixin { imported-mixin: filename(); } @function imported-function() { @return filename(); } filename { imported: filename(); } ruby-sass-3.7.4/test/sass/templates/_imported_charset_ibm866.sass000066400000000000000000000000371345125207600250710ustar00rootroot00000000000000@charset "IBM866" .bar a: ruby-sass-3.7.4/test/sass/templates/_imported_charset_utf8.sass000066400000000000000000000000371345125207600247440ustar00rootroot00000000000000@charset "UTF-8" .bar a: щ ruby-sass-3.7.4/test/sass/templates/_imported_content.sass000066400000000000000000000000341345125207600240140ustar00rootroot00000000000000@mixin foo a @content ruby-sass-3.7.4/test/sass/templates/_partial.sass000066400000000000000000000000361345125207600220750ustar00rootroot00000000000000#foo background-color: #baf ruby-sass-3.7.4/test/sass/templates/_same_name_different_partiality.scss000066400000000000000000000000231345125207600266540ustar00rootroot00000000000000.foo {partial: yes}ruby-sass-3.7.4/test/sass/templates/alt.sass000066400000000000000000000003631345125207600210650ustar00rootroot00000000000000h1 :float left :width 274px height: 75px margin: 0 background: repeat: no-repeat :image none a:hover, a:visited color: green b:hover color: red :background-color green const nosp: 1 + 2 sp : 1 + 2 ruby-sass-3.7.4/test/sass/templates/basic.sass000066400000000000000000000004631345125207600213670ustar00rootroot00000000000000 body font: Arial background: blue #page width: 700px height: 100 #header height: 300px h1 font-size: 50px color: blue #content.user.show #container.top #column.left width: 100px #column.right width: 600px #container.bottom background: brown ruby-sass-3.7.4/test/sass/templates/bork1.sass000066400000000000000000000000231345125207600213140ustar00rootroot00000000000000bork bork: $bork ruby-sass-3.7.4/test/sass/templates/bork2.sass000066400000000000000000000000241345125207600213160ustar00rootroot00000000000000bork :bork: bork; ruby-sass-3.7.4/test/sass/templates/bork3.sass000066400000000000000000000000151345125207600213170ustar00rootroot00000000000000bork bork: ruby-sass-3.7.4/test/sass/templates/bork4.sass000066400000000000000000000000141345125207600213170ustar00rootroot00000000000000 bork: blah ruby-sass-3.7.4/test/sass/templates/bork5.sass000066400000000000000000000000371345125207600213250ustar00rootroot00000000000000bork /* foo */ bork: $bork ruby-sass-3.7.4/test/sass/templates/cached_import_option.scss000066400000000000000000000001021345125207600244670ustar00rootroot00000000000000@import "cached_import_option_partial"; main {value: whatever()} ruby-sass-3.7.4/test/sass/templates/compact.sass000066400000000000000000000003411345125207600217270ustar00rootroot00000000000000#main width: 15em color: #0000ff p border: style: dotted /* Nested comment More nested stuff width: 2px .cool width: 100px #left font: size: 2em weight: bold float: left ruby-sass-3.7.4/test/sass/templates/complex.sass000066400000000000000000000142251345125207600217560ustar00rootroot00000000000000body margin: 0 font: 0.85em "Lucida Grande", "Trebuchet MS", Verdana, sans-serif color: #fff background: url(/images/global_bg.gif) #page width: 900px margin: 0 auto background: #440008 border-top: width: 5px style: solid color: #ff8500 #header height: 75px padding: 0 h1 float: left width: 274px height: 75px margin: 0 background: image: url(/images/global_logo.gif) /* Crazy nested comment repeat: no-repeat text-indent: -9999px .status float: right padding: top: .5em left: .5em right: .5em bottom: 0 p float: left margin: top: 0 right: 0.5em bottom: 0 left: 0 ul float: left margin: 0 padding: 0 li list-style-type: none display: inline margin: 0 5px a:link, a:visited color: #ff8500 text-decoration: none a:hover text-decoration: underline .search float: right clear: right margin: 12px 0 0 0 form margin: 0 input margin: 0 3px 0 0 padding: 2px border: none #menu clear: both text-align: right height: 20px border-bottom: 5px solid #006b95 background: #00a4e4 .contests ul margin: 0 5px 0 0 padding: 0 li list-style-type: none margin: 0 5px padding: 5px 5px 0 5px display: inline font-size: 1.1em // This comment is properly indented color: #fff background: #00a4e4 a:link, a:visited color: #fff text-decoration: none font-weight: bold a:hover text-decoration: underline //General content information #content clear: both .container clear: both .column float: left .right float: right a:link, a:visited color: #93d700 text-decoration: none a:hover text-decoration: underline // A hard tab: #content p, div width: 40em li, dt, dd color: #ddffdd background-color: #4792bb .container.video .column.left width: 200px .box margin-top: 10px p margin: 0 1em auto 1em .box.participants img float: left margin: 0 1em auto 1em border: 1px solid #6e000d style: solid h2 margin: 0 0 10px 0 padding: 0.5em /* The background image is a gif! background: #6e000d url(/images/hdr_participant.gif) 2px 2px no-repeat /* Okay check this out Multiline comments Wow dude I mean seriously, WOW text-indent: -9999px // And also... Multiline comments that don't output! Snazzy, no? border: top: width: 5px style: solid color: #a20013 right: width: 1px style: dotted .column.middle width: 500px .column.right width: 200px .box margin-top: 0 p margin: 0 1em auto 1em .column p margin-top: 0 #content.contests .container.information .column.right .box margin: 1em 0 .box.videos .thumbnail img width: 200px height: 150px margin-bottom: 5px a:link, a:visited color: #93d700 text-decoration: none a:hover text-decoration: underline .box.votes a display: block width: 200px height: 60px margin: 15px 0 background: url(/images/btn_votenow.gif) no-repeat text-indent: -9999px outline: none border: none h2 margin: 52px 0 10px 0 padding: 0.5em background: #6e000d url(/images/hdr_videostats.gif) 2px 2px no-repeat text-indent: -9999px border-top: 5px solid #a20013 #content.contests .container.video .box.videos h2 margin: 0 padding: 0.5em background: #6e000d url(/images/hdr_newestclips.gif) 2px 2px no-repeat text-indent: -9999px border-top: 5px solid #a20013 table width: 100 td padding: 1em width: 25 vertical-align: top p margin: 0 0 5px 0 a:link, a:visited color: #93d700 text-decoration: none a:hover text-decoration: underline .thumbnail float: left img width: 80px height: 60px margin: 0 10px 0 0 border: 1px solid #6e000d #content .container.comments .column margin-top: 15px .column.left width: 600px .box ol margin: 0 padding: 0 li list-style-type: none padding: 10px margin: 0 0 1em 0 background: #6e000d border-top: 5px solid #a20013 div margin-bottom: 1em ul text-align: right li display: inline border: none padding: 0 .column.right width: 290px padding-left: 10px h2 margin: 0 padding: 0.5em background: #6e000d url(/images/hdr_addcomment.gif) 2px 2px no-repeat text-indent: -9999px border-top: 5px solid #a20013 .box textarea width: 290px height: 100px border: none #footer margin-top: 10px padding: 1.2em 1.5em background: #ff8500 ul margin: 0 padding: 0 list-style-type: none li display: inline margin: 0 0.5em color: #440008 ul.links float: left a:link, a:visited color: #440008 text-decoration: none a:hover text-decoration: underline ul.copyright float: right .clear clear: both .centered text-align: center img border: none button.short width: 60px height: 22px padding: 0 0 2px 0 color: #fff border: none background: url(/images/btn_short.gif) no-repeat table border-collapse: collapse ruby-sass-3.7.4/test/sass/templates/compressed.sass000066400000000000000000000002571345125207600224530ustar00rootroot00000000000000#main width: 15em color: #0000ff p border: style: dotted width: 2px .cool width: 100px #left font: size: 2em weight: bold float: left ruby-sass-3.7.4/test/sass/templates/double_import_loop1.sass000066400000000000000000000000351345125207600242570ustar00rootroot00000000000000@import "double_import_loop2"ruby-sass-3.7.4/test/sass/templates/expanded.sass000066400000000000000000000003411345125207600220710ustar00rootroot00000000000000#main width: 15em color: #0000ff p border: style: dotted /* Nested comment More nested stuff width: 2px .cool width: 100px #left font: size: 2em weight: bold float: left ruby-sass-3.7.4/test/sass/templates/filename_fn.scss000066400000000000000000000004541345125207600225530ustar00rootroot00000000000000@import "filename_fn_import"; @mixin local-mixin { local-mixin: filename(); } @function local-function() { @return filename(); } filename { local: filename(); @include local-mixin; local-function: local-function(); @include imported-mixin; imported-function: imported-function(); } ruby-sass-3.7.4/test/sass/templates/if.sass000066400000000000000000000001541345125207600207010ustar00rootroot00000000000000a @if true branch: if @else branch: else b @if false branch: if @else branch: else ruby-sass-3.7.4/test/sass/templates/import.sass000066400000000000000000000003421345125207600216140ustar00rootroot00000000000000$preconst: hello =premixin pre-mixin: here @import importee.sass, scss_importee, "basic.sass", basic.css, ../results/complex.css @import partial.sass nonimported myconst: $preconst otherconst: $postconst +postmixin ruby-sass-3.7.4/test/sass/templates/import_charset.sass000066400000000000000000000003011345125207600233200ustar00rootroot00000000000000.foo a: b @import "foo.css" // Even though the imported file is in IBM866, // since the root file is in UTF-8/ASCII // the output will end up being UTF-8. @import "imported_charset_ibm866" ruby-sass-3.7.4/test/sass/templates/import_charset_ibm866.sass000066400000000000000000000003151345125207600244200ustar00rootroot00000000000000@charset "IBM866" .foo a: b @import "foo.css" // Even though the imported file is in UTF-8, // since the root file is in IBM866 // the output will end up being IBM866. @import "imported_charset_utf8" ruby-sass-3.7.4/test/sass/templates/import_content.sass000066400000000000000000000000561345125207600233500ustar00rootroot00000000000000@import imported_content @include foo b: c ruby-sass-3.7.4/test/sass/templates/importee.less000066400000000000000000000000361345125207600221230ustar00rootroot00000000000000.foo {a: b} .bar () {c: d} ruby-sass-3.7.4/test/sass/templates/importee.sass000066400000000000000000000003141345125207600221250ustar00rootroot00000000000000$postconst: goodbye =postmixin post-mixin: here imported otherconst: $preconst myconst: $postconst +premixin @import basic midrule inthe: middle =crazymixin foo: bar baz blat: bang ruby-sass-3.7.4/test/sass/templates/line_numbers.sass000066400000000000000000000001471345125207600227670ustar00rootroot00000000000000foo bar: baz =premixin squggle blat: bang $preconst: 12 @import importee umph +crazymixinruby-sass-3.7.4/test/sass/templates/mixin_bork.sass000066400000000000000000000000601345125207600224400ustar00rootroot00000000000000=outer-mixin +error-mixin foo +outer-mixin ruby-sass-3.7.4/test/sass/templates/mixins.sass000066400000000000000000000016161345125207600216160ustar00rootroot00000000000000$yellow: #fc0 =bordered border: top: width: 2px color: $yellow left: width: 1px color: #000 -moz-border-radius: 10px =header-font color: #f00 font: size: 20px =compound +header-font +bordered =complex +header-font text: decoration: none &:after content: "." display: block height: 0 clear: both visibility: hidden * html & height: 1px +header-font =deep a:hover text-decoration: underline +compound #main width: 15em color: #0000ff p +bordered border: style: dotted width: 2px .cool width: 100px #left +bordered font: size: 2em weight: bold float: left #right +bordered +header-font float: right .bordered +bordered .complex +complex .more-complex +complex +deep display: inline -webkit-nonsense: top-right: 1px bottom-left: 1px ruby-sass-3.7.4/test/sass/templates/multiline.sass000066400000000000000000000003441345125207600223060ustar00rootroot00000000000000#main, #header height: 50px div width: 100px a, em span color: pink #one, #two, #three div.nested, span.nested, p.nested font: weight: bold border-color: red display: block ruby-sass-3.7.4/test/sass/templates/nested.sass000066400000000000000000000005171345125207600215700ustar00rootroot00000000000000#main width: 15em color: #0000ff p border: style: dotted /* Nested comment More nested stuff width: 2px .cool width: 100px #left font: size: 2em weight: bold float: left #right .header border-style: solid .body border-style: dotted .footer border-style: dashed ruby-sass-3.7.4/test/sass/templates/nested_bork1.sass000066400000000000000000000000171345125207600226610ustar00rootroot00000000000000 @import bork1 ruby-sass-3.7.4/test/sass/templates/nested_bork2.sass000066400000000000000000000000171345125207600226620ustar00rootroot00000000000000 @import bork2 ruby-sass-3.7.4/test/sass/templates/nested_bork3.sass000066400000000000000000000000171345125207600226630ustar00rootroot00000000000000 @import bork3 ruby-sass-3.7.4/test/sass/templates/nested_bork4.sass000066400000000000000000000000171345125207600226640ustar00rootroot00000000000000 @import bork4 ruby-sass-3.7.4/test/sass/templates/nested_import.sass000066400000000000000000000000251345125207600231540ustar00rootroot00000000000000.foo @import basic ruby-sass-3.7.4/test/sass/templates/nested_mixin_bork.sass000066400000000000000000000000661345125207600240100ustar00rootroot00000000000000 =error-mixin width: 1px * 1em @import mixin_bork ruby-sass-3.7.4/test/sass/templates/options.sass000066400000000000000000000000341345125207600217730ustar00rootroot00000000000000foo style: option("style")ruby-sass-3.7.4/test/sass/templates/parent_ref.sass000066400000000000000000000005451345125207600224340ustar00rootroot00000000000000a color: #000 &:hover color: #f00 p, div width: 100em & foo width: 10em &:hover, bar height: 20em #cool border: style: solid width: 2em .ie7 &, .ie6 & content: string("Totally not cool.") .firefox & content: string("Quite cool.") .wow, .snazzy font-family: fantasy &:hover, &:visited font-weight: bold ruby-sass-3.7.4/test/sass/templates/same_name_different_ext.sass000066400000000000000000000000211345125207600251270ustar00rootroot00000000000000.foo ext: sass ruby-sass-3.7.4/test/sass/templates/same_name_different_ext.scss000066400000000000000000000000211345125207600251310ustar00rootroot00000000000000.foo {ext: scss} ruby-sass-3.7.4/test/sass/templates/same_name_different_partiality.scss000066400000000000000000000000221345125207600265140ustar00rootroot00000000000000.foo {partial: no}ruby-sass-3.7.4/test/sass/templates/script.sass000066400000000000000000000031261345125207600216110ustar00rootroot00000000000000$width: 10em + 20 $color: #00ff98 $main_text: #ffa $num: 10 $dec: 10.2 $dec_0: 99.0 $neg: -10 $esc: 10\+12 $str: Hello\! $qstr: "Quo\"ted\"!" $hstr: Hyph-en\! $space: #{5 + 4} hi there $percent: 11% $complex: 1px/1em #main content: $str qstr: $qstr hstr: $hstr width: $width background-color: #000 color: $main_text short-color: #123 named-color: olive con: "foo" bar ($space "boom") con2: "noquo" quo #sidebar background-color: $color num: normal: $num dec: $dec dec0: $dec_0 neg: $neg esc: $esc many: 1 + 2 + 3 order: 1 + 2 * 3 complex: ((1 + 2) + 15)+#3a8b9f + (hi+(1 +1+ 2)* 4) #plus num: num: 5+2 num-un: 10em + 15em num-un2: 10 + 13em num-neg: 10 + -.13 str: 100 * 1px col: 13 + #aaa perc: $percent + 20% str: str: "hi" + "\ there" str2: "hi" + " there" col: "14em solid " + #123 num: "times: " + 13 col: num: #f02 + 123.5 col: #12A + #405162 #minus num: num: 912 - 12 col: num: #fffffa - 5.2 col: #abcdef - #fedcba unary: num: -1 const: -$neg paren: -(5 + 6) two: --12 many: --------12 crazy: -----(5 + ---$neg) #times num: num: 2 * 3.5 col: 2 * #3a4b5c col: num: #12468a * 0.5 col: #121212 * #020304 #div num: num: (10 / 3.0) num2: (10 / 3) col: num: #12468a / 2 col: #abcdef / #0f0f0f comp: $complex * 1em #mod num: num: 17 % 3 col: col: #5f6e7d % #10200a num: #aaabac % 3 #const escaped: quote: \$foo \!bar default: $str !important #regression a: (3 + 2) - 1 ruby-sass-3.7.4/test/sass/templates/scss_import.scss000066400000000000000000000004061345125207600226520ustar00rootroot00000000000000$preconst: hello; @mixin premixin {pre-mixin: here} @import "importee.sass", "scss_importee", "basic.sass", "basic.css", "../results/complex.css"; @import "part\ ial.sass"; nonimported { myconst: $preconst; otherconst: $postconst; @include postmixin; } ruby-sass-3.7.4/test/sass/templates/scss_importee.scss000066400000000000000000000000251345125207600231610ustar00rootroot00000000000000scss {imported: yes} ruby-sass-3.7.4/test/sass/templates/single_import_loop.sass000066400000000000000000000000341345125207600242040ustar00rootroot00000000000000@import "single_import_loop"ruby-sass-3.7.4/test/sass/templates/subdir/000077500000000000000000000000001345125207600207005ustar00rootroot00000000000000ruby-sass-3.7.4/test/sass/templates/subdir/import_up1.scss000066400000000000000000000000441345125207600236720ustar00rootroot00000000000000@import "../subdir/import_up2.scss";ruby-sass-3.7.4/test/sass/templates/subdir/import_up2.scss000066400000000000000000000000441345125207600236730ustar00rootroot00000000000000@import "../subdir/import_up3.scss";ruby-sass-3.7.4/test/sass/templates/subdir/nested_subdir/000077500000000000000000000000001345125207600235325ustar00rootroot00000000000000ruby-sass-3.7.4/test/sass/templates/subdir/nested_subdir/_nested_partial.sass000066400000000000000000000000311345125207600275540ustar00rootroot00000000000000#nested relative: true ruby-sass-3.7.4/test/sass/templates/subdir/nested_subdir/nested_subdir.sass000066400000000000000000000000301345125207600272500ustar00rootroot00000000000000#pi width: 314px ruby-sass-3.7.4/test/sass/templates/subdir/subdir.sass000066400000000000000000000001331345125207600230600ustar00rootroot00000000000000@import nested_subdir/nested_partial.sass #subdir font: size: 20px weight: bold ruby-sass-3.7.4/test/sass/templates/units.sass000066400000000000000000000005001345125207600214400ustar00rootroot00000000000000b foo: 0.5 * 10px bar: 10zzz * 12px / 5zzz baz: percentage(12.0px / 18px) many-units: 10.0zzz / 3yyy * 12px / 5zzz * 3yyy / 3px * 4em mm: 5mm + 1cm pc: 1pc + 12pt pt: 72pt - 2in inches: 1in + 2.54cm more-inches: 1in + ((72pt * 2in) + (36pt * 1in)) / 2.54cm mixed: (1 + (1em * 6px / 3in)) * 4in / 2em ruby-sass-3.7.4/test/sass/templates/warn.sass000066400000000000000000000001051345125207600212460ustar00rootroot00000000000000@warn "In the main file" @import warn_imported.sass +emits-a-warning ruby-sass-3.7.4/test/sass/templates/warn_imported.sass000066400000000000000000000001021345125207600231460ustar00rootroot00000000000000@warn "Imported" =emits-a-warning @warn "In an imported mixin" ruby-sass-3.7.4/test/sass/test_helper.rb000066400000000000000000000003041345125207600202520ustar00rootroot00000000000000test_dir = File.dirname(__FILE__) $:.unshift test_dir unless $:.include?(test_dir) class MiniTest::Test def absolutize(file) File.expand_path("#{File.dirname(__FILE__)}/#{file}") end end ruby-sass-3.7.4/test/sass/util/000077500000000000000000000000001345125207600163675ustar00rootroot00000000000000ruby-sass-3.7.4/test/sass/util/multibyte_string_scanner_test.rb000077500000000000000000000072551345125207600251040ustar00rootroot00000000000000# -*- coding: utf-8 -*- require File.dirname(__FILE__) + '/../../test_helper' class MultibyteStringScannerTest < MiniTest::Test def setup @scanner = Sass::Util::MultibyteStringScanner.new("cölorfül") end def test_initial assert_scanner_state 0, 0, nil, nil end def test_check assert_equal 'cö', @scanner.check(/../) assert_scanner_state 0, 0, 2, 3 assert_equal 0, @scanner.pos assert_equal 0, @scanner.pos assert_equal 2, @scanner.matched_size assert_equal 3, @scanner.byte_matched_size end def test_check_until assert_equal 'cölorfü', @scanner.check_until(/f./) assert_scanner_state 0, 0, 2, 3 end def test_getch assert_equal 'c', @scanner.getch assert_equal 'ö', @scanner.getch assert_scanner_state 2, 3, 1, 2 end def test_match? assert_equal 2, @scanner.match?(/../) assert_scanner_state 0, 0, 2, 3 end def test_peek assert_equal 'cö', @scanner.peek(2) assert_scanner_state 0, 0, nil, nil end def test_rest_size assert_equal 'cö', @scanner.scan(/../) assert_equal 6, @scanner.rest_size end def test_scan assert_equal 'cö', @scanner.scan(/../) assert_scanner_state 2, 3, 2, 3 end def test_scan_until assert_equal 'cölorfü', @scanner.scan_until(/f./) assert_scanner_state 7, 9, 2, 3 end def test_skip assert_equal 2, @scanner.skip(/../) assert_scanner_state 2, 3, 2, 3 end def test_skip_until assert_equal 7, @scanner.skip_until(/f./) assert_scanner_state 7, 9, 2, 3 end def test_set_pos @scanner.pos = 7 assert_scanner_state 7, 9, nil, nil @scanner.pos = 6 assert_scanner_state 6, 7, nil, nil @scanner.pos = 1 assert_scanner_state 1, 1, nil, nil end def test_reset @scanner.scan(/../) @scanner.reset assert_scanner_state 0, 0, nil, nil end def test_scan_full assert_equal 'cö', @scanner.scan_full(/../, true, true) assert_scanner_state 2, 3, 2, 3 @scanner.reset assert_equal 'cö', @scanner.scan_full(/../, false, true) assert_scanner_state 0, 0, 2, 3 @scanner.reset assert_nil @scanner.scan_full(/../, true, false) assert_scanner_state 2, 3, 2, 3 @scanner.reset assert_nil @scanner.scan_full(/../, false, false) assert_scanner_state 0, 0, 2, 3 end def test_search_full assert_equal 'cölorfü', @scanner.search_full(/f./, true, true) assert_scanner_state 7, 9, 2, 3 @scanner.reset assert_equal 'cölorfü', @scanner.search_full(/f./, false, true) assert_scanner_state 0, 0, 2, 3 @scanner.reset assert_nil @scanner.search_full(/f./, true, false) assert_scanner_state 7, 9, 2, 3 @scanner.reset assert_nil @scanner.search_full(/f./, false, false) assert_scanner_state 0, 0, 2, 3 end def test_set_string @scanner.scan(/../) @scanner.string = 'föóbâr' assert_scanner_state 0, 0, nil, nil end def test_terminate @scanner.scan(/../) @scanner.terminate assert_scanner_state 8, 10, nil, nil end def test_unscan @scanner.scan(/../) @scanner.scan_until(/f./) @scanner.unscan assert_scanner_state 2, 3, nil, nil end private def assert_scanner_state(pos, byte_pos, matched_size, byte_matched_size) assert_equal pos, @scanner.pos, 'pos' assert_equal byte_pos, @scanner.byte_pos, 'byte_pos' if matched_size.nil? assert_nil @scanner.matched_size, 'matched_size' else assert_equal matched_size, @scanner.matched_size, 'matched_size' end if byte_matched_size.nil? assert_nil @scanner.byte_matched_size, 'byte_matched_size' else assert_equal byte_matched_size, @scanner.byte_matched_size, 'byte_matched_size' end end end ruby-sass-3.7.4/test/sass/util/normalized_map_test.rb000077500000000000000000000024031345125207600227560ustar00rootroot00000000000000require File.dirname(__FILE__) + '/../../test_helper' require 'sass/util/normalized_map' class NormalizedMapTest < MiniTest::Test extend PublicApiLinter lint_api Hash, Sass::Util::NormalizedMap def lint_instance Sass::Util::NormalizedMap.new end def test_normalized_map_errors_unless_explicitly_implemented assert Sass.tests_running assert_raise_message(ArgumentError, "The method invert must be implemented explicitly") do Sass::Util::NormalizedMap.new.invert end end def test_normalized_map_does_not_error_when_released Sass.tests_running = false assert_equal({}, Sass::Util::NormalizedMap.new.invert) ensure Sass.tests_running = true end def test_basic_lifecycle m = Sass::Util::NormalizedMap.new m["a-b"] = 1 assert_equal ["a_b"], m.keys assert_equal 1, m["a_b"] assert_equal 1, m["a-b"] assert m.has_key?("a_b") assert m.has_key?("a-b") assert_equal({"a-b" => 1}, m.as_stored) assert_equal 1, m.delete("a-b") assert !m.has_key?("a-b") m["a_b"] = 2 assert_equal({"a_b" => 2}, m.as_stored) end def test_dup m = Sass::Util::NormalizedMap.new m["a-b"] = 1 m2 = m.dup m.delete("a-b") assert !m.has_key?("a-b") assert m2.has_key?("a-b") end end ruby-sass-3.7.4/test/sass/util/subset_map_test.rb000077500000000000000000000051661345125207600221300ustar00rootroot00000000000000require File.dirname(__FILE__) + '/../../test_helper' class SubsetMapTest < MiniTest::Test def setup @ssm = Sass::Util::SubsetMap.new @ssm[Set[1, 2]] = "Foo" @ssm[Set["fizz", "fazz"]] = "Bar" @ssm[Set[:foo, :bar]] = "Baz" @ssm[Set[:foo, :bar, :baz]] = "Bang" @ssm[Set[:bip, :bop, :blip]] = "Qux" @ssm[Set[:bip, :bop]] = "Thram" end def test_equal_keys assert_equal [["Foo", Set[1, 2]]], @ssm.get(Set[1, 2]) assert_equal [["Bar", Set["fizz", "fazz"]]], @ssm.get(Set["fizz", "fazz"]) end def test_subset_keys assert_equal [["Foo", Set[1, 2]]], @ssm.get(Set[1, 2, "fuzz"]) assert_equal [["Bar", Set["fizz", "fazz"]]], @ssm.get(Set["fizz", "fazz", 3]) end def test_superset_keys assert_equal [], @ssm.get(Set[1]) assert_equal [], @ssm.get(Set[2]) assert_equal [], @ssm.get(Set["fizz"]) assert_equal [], @ssm.get(Set["fazz"]) end def test_disjoint_keys assert_equal [], @ssm.get(Set[3, 4]) assert_equal [], @ssm.get(Set["fuzz", "frizz"]) assert_equal [], @ssm.get(Set["gran", 15]) end def test_semi_disjoint_keys assert_equal [], @ssm.get(Set[2, 3]) assert_equal [], @ssm.get(Set["fizz", "fuzz"]) assert_equal [], @ssm.get(Set[1, "fazz"]) end def test_empty_key_set assert_raises(ArgumentError) {@ssm[Set[]] = "Fail"} end def test_empty_key_get assert_equal [], @ssm.get(Set[]) end def test_multiple_subsets assert_equal [["Foo", Set[1, 2]], ["Bar", Set["fizz", "fazz"]]], @ssm.get(Set[1, 2, "fizz", "fazz"]) assert_equal [["Foo", Set[1, 2]], ["Bar", Set["fizz", "fazz"]]], @ssm.get(Set[1, 2, 3, "fizz", "fazz", "fuzz"]) assert_equal [["Baz", Set[:foo, :bar]]], @ssm.get(Set[:foo, :bar]) assert_equal [["Baz", Set[:foo, :bar]], ["Bang", Set[:foo, :bar, :baz]]], @ssm.get(Set[:foo, :bar, :baz]) end def test_bracket_bracket assert_equal ["Foo"], @ssm[Set[1, 2, "fuzz"]] assert_equal ["Baz", "Bang"], @ssm[Set[:foo, :bar, :baz]] end def test_order_preserved @ssm[Set[10, 11, 12]] = 1 @ssm[Set[10, 11]] = 2 @ssm[Set[11]] = 3 @ssm[Set[11, 12]] = 4 @ssm[Set[9, 10, 11, 12, 13]] = 5 @ssm[Set[10, 13]] = 6 assert_equal( [[1, Set[10, 11, 12]], [2, Set[10, 11]], [3, Set[11]], [4, Set[11, 12]], [5, Set[9, 10, 11, 12, 13]], [6, Set[10, 13]]], @ssm.get(Set[9, 10, 11, 12, 13])) end def test_multiple_equal_values @ssm[Set[11, 12]] = 1 @ssm[Set[12, 13]] = 2 @ssm[Set[13, 14]] = 1 @ssm[Set[14, 15]] = 1 assert_equal( [[1, Set[11, 12]], [2, Set[12, 13]], [1, Set[13, 14]], [1, Set[14, 15]]], @ssm.get(Set[11, 12, 13, 14, 15])) end end ruby-sass-3.7.4/test/sass/util_test.rb000077500000000000000000000316601345125207600177640ustar00rootroot00000000000000require File.dirname(__FILE__) + '/../test_helper' require 'pathname' require 'tmpdir' class UtilTest < MiniTest::Test include Sass::Util def test_scope assert(File.exist?(scope("Rakefile"))) end def test_map_keys assert_equal({ "foo" => 1, "bar" => 2, "baz" => 3 }, map_keys({:foo => 1, :bar => 2, :baz => 3}) {|k| k.to_s}) end def test_map_vals assert_equal({ :foo => "1", :bar => "2", :baz => "3" }, map_vals({:foo => 1, :bar => 2, :baz => 3}) {|k| k.to_s}) end def test_map_hash assert_equal({ "foo" => "1", "bar" => "2", "baz" => "3" }, map_hash({:foo => 1, :bar => 2, :baz => 3}) {|k, v| [k.to_s, v.to_s]}) end def test_map_hash_with_normalized_map map = NormalizedMap.new("foo-bar" => 1, "baz_bang" => 2) result = map_hash(map) {|k, v| [k, v.to_s]} assert_equal("1", result["foo-bar"]) assert_equal("1", result["foo_bar"]) assert_equal("2", result["baz-bang"]) assert_equal("2", result["baz_bang"]) end def test_powerset assert_equal([[].to_set].to_set, powerset([])) assert_equal([[].to_set, [1].to_set].to_set, powerset([1])) assert_equal([[].to_set, [1].to_set, [2].to_set, [1, 2].to_set].to_set, powerset([1, 2])) assert_equal([[].to_set, [1].to_set, [2].to_set, [3].to_set, [1, 2].to_set, [2, 3].to_set, [1, 3].to_set, [1, 2, 3].to_set].to_set, powerset([1, 2, 3])) end def test_restrict assert_equal(0.5, restrict(0.5, 0..1)) assert_equal(1, restrict(2, 0..1)) assert_equal(1.3, restrict(2, 0..1.3)) assert_equal(0, restrict(-1, 0..1)) end def test_merge_adjacent_strings assert_equal(["foo bar baz", :bang, "biz bop", 12], merge_adjacent_strings(["foo ", "bar ", "baz", :bang, "biz", " bop", 12])) str = "foo" assert_equal(["foo foo foo", :bang, "foo foo", 12], merge_adjacent_strings([str, " ", str, " ", str, :bang, str, " ", str, 12])) end def test_replace_subseq assert_equal([1, 2, :a, :b, 5], replace_subseq([1, 2, 3, 4, 5], [3, 4], [:a, :b])) assert_equal([1, 2, 3, 4, 5], replace_subseq([1, 2, 3, 4, 5], [3, 4, 6], [:a, :b])) assert_equal([1, 2, 3, 4, 5], replace_subseq([1, 2, 3, 4, 5], [4, 5, 6], [:a, :b])) end def test_intersperse assert_equal(["foo", " ", "bar", " ", "baz"], intersperse(%w[foo bar baz], " ")) assert_equal([], intersperse([], " ")) end def test_substitute assert_equal(["foo", "bar", "baz", 3, 4], substitute([1, 2, 3, 4], [1, 2], ["foo", "bar", "baz"])) assert_equal([1, "foo", "bar", "baz", 4], substitute([1, 2, 3, 4], [2, 3], ["foo", "bar", "baz"])) assert_equal([1, 2, "foo", "bar", "baz"], substitute([1, 2, 3, 4], [3, 4], ["foo", "bar", "baz"])) assert_equal([1, "foo", "bar", "baz", 2, 3, 4], substitute([1, 2, 2, 2, 3, 4], [2, 2], ["foo", "bar", "baz"])) end def test_strip_string_array assert_equal(["foo ", " bar ", " baz"], strip_string_array([" foo ", " bar ", " baz "])) assert_equal([:foo, " bar ", " baz"], strip_string_array([:foo, " bar ", " baz "])) assert_equal(["foo ", " bar ", :baz], strip_string_array([" foo ", " bar ", :baz])) end def test_paths assert_equal([[1, 3, 5], [2, 3, 5], [1, 4, 5], [2, 4, 5]], paths([[1, 2], [3, 4], [5]])) assert_equal([[]], paths([])) assert_equal([[1, 2, 3]], paths([[1], [2], [3]])) end def test_lcs assert_equal([1, 2, 3], lcs([1, 2, 3], [1, 2, 3])) assert_equal([], lcs([], [1, 2, 3])) assert_equal([], lcs([1, 2, 3], [])) assert_equal([1, 2, 3], lcs([5, 1, 4, 2, 3, 17], [0, 0, 1, 2, 6, 3])) assert_equal([1], lcs([1, 2, 3, 4], [4, 3, 2, 1])) assert_equal([1, 2], lcs([1, 2, 3, 4], [3, 4, 1, 2])) end def test_lcs_with_block assert_equal(["1", "2", "3"], lcs([1, 4, 2, 5, 3], [1, 2, 3]) {|a, b| a == b && a.to_s}) assert_equal([-4, 2, 8], lcs([-5, 3, 2, 8], [-4, 1, 8]) {|a, b| (a - b).abs <= 1 && [a, b].max}) end def test_subsequence assert(subsequence?([1, 2, 3], [1, 2, 3])) assert(subsequence?([1, 2, 3], [1, :a, 2, :b, 3])) assert(subsequence?([1, 2, 3], [:a, 1, :b, :c, 2, :d, 3, :e, :f])) assert(!subsequence?([1, 2, 3], [1, 2])) assert(!subsequence?([1, 2, 3], [1, 3, 2])) assert(!subsequence?([1, 2, 3], [3, 2, 1])) end def test_sass_warn assert_warning("Foo!") {sass_warn "Foo!"} end def test_silence_sass_warnings old_stderr, $stderr = $stderr, StringIO.new silence_sass_warnings {warn "Out"} assert_equal("Out\n", $stderr.string) silence_sass_warnings {sass_warn "In"} assert_equal("Out\n", $stderr.string) ensure $stderr = old_stderr end def test_extract arr = [1, 2, 3, 4, 5] assert_equal([1, 3, 5], extract!(arr) {|e| e % 2 == 1}) assert_equal([2, 4], arr) end def test_flatten_vertically assert_equal([1, 2, 3], flatten_vertically([1, 2, 3])) assert_equal([1, 3, 5, 2, 4, 6], flatten_vertically([[1, 2], [3, 4], [5, 6]])) assert_equal([1, 2, 4, 3, 5, 6], flatten_vertically([1, [2, 3], [4, 5, 6]])) assert_equal([1, 4, 6, 2, 5, 3], flatten_vertically([[1, 2, 3], [4, 5], 6])) end def test_extract_and_inject_values test = lambda {|arr| assert_equal(arr, with_extracted_values(arr) {|str| str})} test[['foo bar']] test[['foo {12} bar']] test[['foo {{12} bar']] test[['foo {{1', 12, '2} bar']] test[['foo 1', 2, '{3', 4, 5, 6, '{7}', 8]] test[['foo 1', [2, 3, 4], ' bar']] test[['foo ', 1, "\n bar\n", [2, 3, 4], "\n baz"]] end def nested_caller_info_fn caller_info end def double_nested_caller_info_fn nested_caller_info_fn end def test_caller_info assert_equal(["/tmp/foo.rb", 12, "fizzle"], caller_info("/tmp/foo.rb:12: in `fizzle'")) assert_equal(["/tmp/foo.rb", 12, nil], caller_info("/tmp/foo.rb:12")) assert_equal(["C:/tmp/foo.rb", 12, nil], caller_info("C:/tmp/foo.rb:12")) assert_equal(["(sass)", 12, "blah"], caller_info("(sass):12: in `blah'")) assert_equal(["", 12, "boop"], caller_info(":12: in `boop'")) assert_equal(["/tmp/foo.rb", -12, "fizzle"], caller_info("/tmp/foo.rb:-12: in `fizzle'")) assert_equal(["/tmp/foo.rb", 12, "fizzle"], caller_info("/tmp/foo.rb:12: in `fizzle {}'")) assert_equal(["C:/tmp/foo.rb", 12, "fizzle"], caller_info("C:/tmp/foo.rb:12: in `fizzle {}'")) info = nested_caller_info_fn assert_equal(__FILE__, info[0]) assert_equal("test_caller_info", info[2]) info = proc {nested_caller_info_fn}.call assert_equal(__FILE__, info[0]) assert_match(/^(block in )?test_caller_info$/, info[2]) info = double_nested_caller_info_fn assert_equal(__FILE__, info[0]) assert_equal("double_nested_caller_info_fn", info[2]) info = proc {double_nested_caller_info_fn}.call assert_equal(__FILE__, info[0]) assert_equal("double_nested_caller_info_fn", info[2]) end def test_version_gt assert_version_gt("2.0.0", "1.0.0") assert_version_gt("1.1.0", "1.0.0") assert_version_gt("1.0.1", "1.0.0") assert_version_gt("1.0.0", "1.0.0.rc") assert_version_gt("1.0.0.1", "1.0.0.rc") assert_version_gt("1.0.0.rc", "0.9.9") assert_version_gt("1.0.0.beta", "1.0.0.alpha") assert_version_eq("1.0.0", "1.0.0") assert_version_eq("1.0.0", "1.0.0.0") end def assert_version_gt(v1, v2) #assert(version_gt(v1, v2), "Expected #{v1} > #{v2}") assert(!version_gt(v2, v1), "Expected #{v2} < #{v1}") end def assert_version_eq(v1, v2) assert(!version_gt(v1, v2), "Expected #{v1} = #{v2}") assert(!version_gt(v2, v1), "Expected #{v2} = #{v1}") end class FooBar def foo Sass::Util.abstract(self) end def old_method Sass::Util.deprecated(self) end def old_method_with_custom_message Sass::Util.deprecated(self, "Call FooBar#new_method instead.") end def self.another_old_method Sass::Util.deprecated(self) end end def test_abstract assert_raise_message(NotImplementedError, "UtilTest::FooBar must implement #foo") {FooBar.new.foo} end def test_deprecated assert_warning("DEPRECATION WARNING: UtilTest::FooBar#old_method will be removed in a future version of Sass.") { FooBar.new.old_method } assert_warning(< e assert_instance_of(klass, e) assert_equal(message, e.message) else flunk "Expected exception #{klass}, none raised" end def assert_raise_line(line) yield rescue Sass::SyntaxError => e assert_equal(line, e.sass_line) else flunk "Expected exception on line #{line}, none raised" end def assert_sass_to_sass(sass, options: {}) assert_equal(sass.rstrip, to_sass(sass, options).rstrip, "Expected Sass to transform to itself") end def assert_scss_to_sass(sass, scss, options: {}) assert_equal(sass.rstrip, to_sass(scss, options.merge(:syntax => :scss)).rstrip, "Expected SCSS to transform to Sass") end def assert_scss_to_scss(scss, source: scss, options: {}) assert_equal(scss.rstrip, to_scss(source, options.merge(:syntax => :scss)).rstrip, "Expected SCSS to transform to #{scss == source ? 'itself' : 'SCSS'}") end def assert_sass_to_scss(scss, sass, options: {}) assert_equal(scss.rstrip, to_scss(sass, options).rstrip, "Expected Sass to transform to SCSS") end def assert_converts(sass, scss, options: {}) assert_sass_to_sass(sass, options: options) assert_scss_to_sass(sass, scss, options: options) assert_scss_to_scss(scss, options: options) assert_sass_to_scss(scss, sass, options: options) end def to_sass(scss, options = {}) Sass::Util.silence_sass_warnings do Sass::Engine.new(scss, options).to_tree.to_sass(options) end end def to_scss(sass, options = {}) Sass::Util.silence_sass_warnings do Sass::Engine.new(sass, options).to_tree.to_scss(options) end end end module PublicApiLinter def lint_api(api_class, duck_type_class) define_method :test_lint_instance do assert lint_instance.is_a?(duck_type_class) end api_class.instance_methods.each do |meth| define_method :"test_has_#{meth}" do assert lint_instance.respond_to?(meth), "#{duck_type_class.name} does not implement #{meth}" end end end end ruby-sass-3.7.4/yard/000077500000000000000000000000001345125207600144215ustar00rootroot00000000000000ruby-sass-3.7.4/yard/callbacks.rb000066400000000000000000000014611345125207600166670ustar00rootroot00000000000000class CallbacksHandler < YARD::Handlers::Ruby::Legacy::Base handles /\Adefine_callback(\s|\()/ def process callback_name = tokval(statement.tokens[2]) attr_index = statement.comments.each_with_index {|c, i| break i if c[0] == ?@} if attr_index.is_a?(Integer) docstring = statement.comments[0...attr_index] attrs = statement.comments[attr_index..-1] else docstring = statement.comments attrs = [] end yieldparams = "" attrs.reject! do |a| next unless a =~ /^@yield *(\[.*?\])/ yieldparams = $1 true end o = register(MethodObject.new(namespace, "on_#{callback_name}", scope)) o.docstring = docstring + [ "@return [void]", "@yield #{yieldparams} When the callback is run" ] + attrs o.signature = true end end ruby-sass-3.7.4/yard/default/000077500000000000000000000000001345125207600160455ustar00rootroot00000000000000ruby-sass-3.7.4/yard/default/.gitignore000066400000000000000000000000061345125207600200310ustar00rootroot00000000000000*.css ruby-sass-3.7.4/yard/default/fulldoc/000077500000000000000000000000001345125207600174755ustar00rootroot00000000000000ruby-sass-3.7.4/yard/default/fulldoc/html/000077500000000000000000000000001345125207600204415ustar00rootroot00000000000000ruby-sass-3.7.4/yard/default/fulldoc/html/css/000077500000000000000000000000001345125207600212315ustar00rootroot00000000000000ruby-sass-3.7.4/yard/default/fulldoc/html/css/common.sass000066400000000000000000000007141345125207600234160ustar00rootroot00000000000000.maruku_toc background: #ddd border: 1px solid #ccc margin-right: 2em float: left ul padding: 0 1em #frequently_asked_questions + & float: none margin: 0 2em #filecontents *:target, dt:target + dd background-color: #ccf border: 1px solid #88b dt font-weight: bold dd margin: left: 0 bottom: 0.7em padding-left: 3em dt:target border-bottom-style: none & + dd border-top-style: none ruby-sass-3.7.4/yard/default/layout/000077500000000000000000000000001345125207600173625ustar00rootroot00000000000000ruby-sass-3.7.4/yard/default/layout/html/000077500000000000000000000000001345125207600203265ustar00rootroot00000000000000ruby-sass-3.7.4/yard/default/layout/html/footer.erb000066400000000000000000000007551345125207600223250ustar00rootroot00000000000000<%= superb :footer %> <% if ENV["ANALYTICS"] %> <% end %> ruby-sass-3.7.4/yard/inherited_hash.rb000066400000000000000000000027231345125207600177300ustar00rootroot00000000000000class InheritedHashHandler < YARD::Handlers::Ruby::Legacy::Base handles /\Ainherited_hash(\s|\()/ def process hash_name = tokval(statement.tokens[2]) name = statement.comments.first.strip type = statement.comments[1].strip o = register(MethodObject.new(namespace, hash_name, scope)) o.docstring = [ "Gets a #{name} from this {Environment} or one of its \\{#parent}s.", "@param name [String] The name of the #{name}", "@return [#{type}] The #{name} value", ] o.signature = true o.parameters = ["name"] o = register(MethodObject.new(namespace, "set_#{hash_name}", scope)) o.docstring = [ "Sets a #{name} in this {Environment} or one of its \\{#parent}s.", "If the #{name} is already defined in some environment,", "that one is set; otherwise, a new one is created in this environment.", "@param name [String] The name of the #{name}", "@param value [#{type}] The value of the #{name}", "@return [#{type}] `value`", ] o.signature = true o.parameters = ["name", "value"] o = register(MethodObject.new(namespace, "set_local_#{hash_name}", scope)) o.docstring = [ "Sets a #{name} in this {Environment}.", "Ignores any parent environments.", "@param name [String] The name of the #{name}", "@param value [#{type}] The value of the #{name}", "@return [#{type}] `value`", ] o.signature = true o.parameters = ["name", "value"] end end