1
0
mirror of https://github.com/bitwarden/help synced 2025-12-24 04:04:27 +00:00

Promote to Master (#748)

* initial commit

* adding quotes for the array error

* Create Gemfile

* Create Gemfile.lock

* add .nvmrc and .node-version

* removed /article from URL

* update links to work with netlify

* more fixed links

* link fixes

* update bad links

* Update netlify.toml

toml test for redirects

* article redirect

* link fixes

* Update index.html

* Update netlify.toml

* Update _config.yml

* Update netlify.toml

* Update netlify.toml

* Update netlify.toml

* Update netlify.toml

* Update netlify.toml

* add article back into URL for launch

* Update netlify.toml

* Update netlify.toml

* add order to categories front matter

* Update netlify.toml

* update

* sidemenu update

* Revert "sidemenu update"

This reverts commit 5441c3d35c.

* update order prop

* Navbar updates per Gary and compiler warnings

* font/style tweaks

* Update sidebar.html

* Stage Release Documentation (#739)

* initial drafts

* rewrite Custom Fields article to prioritize new context-menu option & better organize ancillary information

* edit

* edit

* Custom Field Context Menu & CAPTCHA item in release notes

* SSO relink event

* update rn

* small edits

* improve release notes titles

* fix side menu

* Edits courtest of mportune!

* update order

* link fixes

* link cleanup

* image updates and a link

* fix trailing slash

Co-authored-by: DanHillesheim <79476558+DanHillesheim@users.noreply.github.com>
This commit is contained in:
fred_the_tech_writer
2021-09-21 13:21:11 -04:00
committed by GitHub
parent 63f78e8979
commit 906e2ca0dd
3304 changed files with 386714 additions and 8864 deletions

View File

@@ -0,0 +1,17 @@
# frozen_string_literal: true
module SassC
class Dependency
attr_reader :filename
attr_reader :options
def initialize(filename)
@filename = filename
@options = { filename: @filename }
end
def self.from_filenames(filenames)
filenames.map { |f| new(f) }
end
end
end

View File

@@ -0,0 +1,141 @@
# frozen_string_literal: true
require_relative "error"
module SassC
class Engine
OUTPUT_STYLES = %i[
sass_style_nested
sass_style_expanded
sass_style_compact
sass_style_compressed
]
attr_reader :template, :options
def initialize(template, options = {})
@template = template
@options = options
@functions = options.fetch(:functions, Script::Functions)
end
def render
return @template.dup if @template.empty?
data_context = Native.make_data_context(@template)
context = Native.data_context_get_context(data_context)
native_options = Native.context_get_options(context)
Native.option_set_is_indented_syntax_src(native_options, true) if sass?
Native.option_set_input_path(native_options, filename) if filename
Native.option_set_precision(native_options, precision) if precision
Native.option_set_include_path(native_options, load_paths)
Native.option_set_output_style(native_options, output_style_enum)
Native.option_set_source_comments(native_options, true) if line_comments?
Native.option_set_source_map_file(native_options, source_map_file) if source_map_file
Native.option_set_source_map_embed(native_options, true) if source_map_embed?
Native.option_set_source_map_contents(native_options, true) if source_map_contents?
Native.option_set_omit_source_map_url(native_options, true) if omit_source_map_url?
import_handler.setup(native_options)
functions_handler.setup(native_options, functions: @functions)
status = Native.compile_data_context(data_context)
if status != 0
message = Native.context_get_error_message(context)
filename = Native.context_get_error_file(context)
line = Native.context_get_error_line(context)
raise SyntaxError.new(message, filename: filename, line: line)
end
css = Native.context_get_output_string(context)
@dependencies = Native.context_get_included_files(context)
@source_map = Native.context_get_source_map_string(context)
css.force_encoding(@template.encoding)
@source_map.force_encoding(@template.encoding) if @source_map.is_a?(String)
return css unless quiet?
ensure
Native.delete_data_context(data_context) if data_context
end
def dependencies
raise NotRenderedError unless @dependencies
Dependency.from_filenames(@dependencies)
end
def source_map
raise NotRenderedError unless @source_map
@source_map
end
def filename
@options[:filename]
end
private
def quiet?
@options[:quiet]
end
def precision
@options[:precision]
end
def sass?
@options[:syntax] && @options[:syntax].to_sym == :sass
end
def line_comments?
@options[:line_comments]
end
def source_map_embed?
@options[:source_map_embed]
end
def source_map_contents?
@options[:source_map_contents]
end
def omit_source_map_url?
@options[:omit_source_map_url]
end
def source_map_file
@options[:source_map_file]
end
def import_handler
@import_handler ||= ImportHandler.new(@options)
end
def functions_handler
@functions_handler = FunctionsHandler.new(@options)
end
def output_style_enum
@output_style_enum ||= Native::SassOutputStyle[output_style]
end
def output_style
@output_style ||= begin
style = @options.fetch(:style, :sass_style_nested).to_s
style = "sass_style_#{style}" unless style.include?("sass_style_")
style = style.to_sym
raise InvalidStyleError unless Native::SassOutputStyle.symbols.include?(style)
style
end
end
def load_paths
paths = (@options[:load_paths] || []) + SassC.load_paths
paths.join(File::PATH_SEPARATOR) unless paths.empty?
end
end
end

View File

@@ -0,0 +1,37 @@
# frozen_string_literal: true
require "pathname"
module SassC
class BaseError < StandardError; end
class NotRenderedError < BaseError; end
class InvalidStyleError < BaseError; end
class UnsupportedValue < BaseError; end
# 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.
class SyntaxError < BaseError
def initialize(message, filename: nil, line: nil)
@filename = filename
@line = line
super(message)
end
def backtrace
return nil if super.nil?
sass_backtrace + super
end
# The backtrace of the error within Sass files.
def sass_backtrace
return [] unless @filename && @line
["#{@filename}:#{@line}"]
end
end
end

View File

@@ -0,0 +1,73 @@
# frozen_string_literal: true
module SassC
class FunctionsHandler
def initialize(options)
@options = options
end
def setup(native_options, functions: Script::Functions)
@callbacks = {}
@function_names = {}
list = Native.make_function_list(Script.custom_functions(functions: functions).count)
# use an anonymous class wrapper to avoid mutations in a threaded environment
functions_wrapper = Class.new do
attr_accessor :options
include functions
end.new
functions_wrapper.options = @options
Script.custom_functions(functions: functions).each_with_index do |custom_function, i|
@callbacks[custom_function] = FFI::Function.new(:pointer, [:pointer, :pointer]) do |native_argument_list, cookie|
begin
function_arguments = arguments_from_native_list(native_argument_list)
result = functions_wrapper.send(custom_function, *function_arguments)
to_native_value(result)
rescue StandardError => exception
# This rescues any exceptions that occur either in value conversion
# or during the execution of a custom function.
error(exception.message)
end
end
@function_names[custom_function] = Script.formatted_function_name(custom_function, functions: functions)
callback = Native.make_function(
@function_names[custom_function],
@callbacks[custom_function],
nil
)
Native::function_set_list_entry(list, i, callback)
end
Native::option_set_c_functions(native_options, list)
end
private
def arguments_from_native_list(native_argument_list)
native_argument_list_length = Native.list_get_length(native_argument_list)
(0...native_argument_list_length).map do |i|
native_value = Native.list_get_value(native_argument_list, i)
Script::ValueConversion.from_native(native_value, @options)
end.compact
end
def to_native_value(sass_value)
# if the custom function returns nil, we provide a "default" return
# value of an empty string
sass_value ||= SassC::Script::Value::String.new("")
sass_value.options = @options
Script::ValueConversion.to_native(sass_value)
end
def error(message)
$stderr.puts "[SassC::FunctionsHandler] #{message}"
Native.make_error(message)
end
end
end

View File

@@ -0,0 +1,50 @@
# frozen_string_literal: true
module SassC
class ImportHandler
def initialize(options)
@importer = if options[:importer]
options[:importer].new(options)
else
nil
end
end
def setup(native_options)
return unless @importer
importer_callback = Native.make_importer(import_function, nil)
list = Native.make_function_list(1)
Native::function_set_list_entry(list, 0, importer_callback)
Native.option_set_c_importers(native_options, list)
end
private
def import_function
@import_function ||= FFI::Function.new(:pointer, [:string, :pointer, :pointer]) do |path, importer_entry, compiler|
last_import = Native::compiler_get_last_import(compiler)
parent_path = Native::import_get_abs_path(last_import)
imports = [*@importer.imports(path, parent_path)]
imports_to_native(imports)
end
end
def imports_to_native(imports)
import_list = Native.make_import_list(imports.size)
imports.each_with_index do |import, i|
source = import.source ? Native.native_string(import.source) : nil
source_map_path = nil
entry = Native.make_import_entry(import.path, source, source_map_path)
Native.import_set_list_entry(import_list, i, entry)
end
import_list
end
end
end

View File

@@ -0,0 +1,31 @@
# frozen_string_literal: true
module SassC
class Importer
attr_reader :options
def initialize(options)
@options = options
end
def imports(path, parent_path)
# A custom importer must override this method.
# Custom importer may return an Import, or an array of Imports.
raise NotImplementedError
end
class Import
attr_accessor :path, :source, :source_map_path
def initialize(path, source: nil, source_map_path: nil)
@path = path
@source = source
@source_map_path = source_map_path
end
def to_s
"Import: #{path} #{source} #{source_map_path}"
end
end
end
end

View File

@@ -0,0 +1,64 @@
# frozen_string_literal: true
require "ffi"
module SassC
module Native
extend FFI::Library
dl_ext = RbConfig::MAKEFILE_CONFIG['DLEXT']
begin
ffi_lib File.expand_path("libsass.#{dl_ext}", __dir__)
rescue LoadError # Some non-rvm environments don't copy a shared object over to lib/sassc
ffi_lib File.expand_path("libsass.#{dl_ext}", "#{__dir__}/../../ext")
end
require_relative "native/sass_value"
typedef :pointer, :sass_options_ptr
typedef :pointer, :sass_context_ptr
typedef :pointer, :sass_file_context_ptr
typedef :pointer, :sass_data_context_ptr
typedef :pointer, :sass_c_function_list_ptr
typedef :pointer, :sass_c_function_callback_ptr
typedef :pointer, :sass_value_ptr
typedef :pointer, :sass_import_list_ptr
typedef :pointer, :sass_importer
typedef :pointer, :sass_import_ptr
callback :sass_c_function, [:pointer, :pointer], :pointer
callback :sass_c_import_function, [:pointer, :pointer, :pointer], :pointer
require_relative "native/sass_input_style"
require_relative "native/sass_output_style"
require_relative "native/string_list"
# Remove the redundant "sass_" from the beginning of every method name
def self.attach_function(*args)
return super if args.size != 3
if args[0] =~ /^sass_/
args.unshift args[0].to_s.sub(/^sass_/, "")
end
super(*args)
end
# https://github.com/ffi/ffi/wiki/Examples#array-of-strings
def self.return_string_array(ptr)
ptr.null? ? [] : ptr.get_array_of_string(0).compact
end
def self.native_string(string)
m = FFI::MemoryPointer.from_string(string)
m.autorelease = false
m
end
require_relative "native/native_context_api"
require_relative "native/native_functions_api"
require_relative "native/sass2scss_api"
end
end

View File

@@ -0,0 +1,147 @@
# frozen_string_literal: true
module SassC
module Native
attach_function :version, :libsass_version, [], :string
# Create and initialize an option struct
# ADDAPI struct Sass_Options* ADDCALL sass_make_options (void);
attach_function :sass_make_options, [], :sass_options_ptr
# Create and initialize a specific context
# ADDAPI struct Sass_File_Context* ADDCALL sass_make_file_context (const char* input_path);
# ADDAPI struct Sass_Data_Context* ADDCALL sass_make_data_context (char* source_string);
attach_function :sass_make_file_context, [:string], :sass_file_context_ptr
attach_function :_make_data_context, :sass_make_data_context, [:pointer], :sass_data_context_ptr
def self.make_data_context(data)
_make_data_context(Native.native_string(data))
end
# Call the compilation step for the specific context
# ADDAPI int ADDCALL sass_compile_file_context (struct Sass_File_Context* ctx);
# ADDAPI int ADDCALL sass_compile_data_context (struct Sass_Data_Context* ctx);
attach_function :sass_compile_file_context, [:sass_file_context_ptr], :int
attach_function :sass_compile_data_context, [:sass_data_context_ptr], :int
# Create a sass compiler instance for more control
# ADDAPI struct Sass_Compiler* ADDCALL sass_make_file_compiler (struct Sass_File_Context* file_ctx);
# ADDAPI struct Sass_Compiler* ADDCALL sass_make_data_compiler (struct Sass_Data_Context* data_ctx);
# Execute the different compilation steps individually
# Usefull if you only want to query the included files
# ADDAPI int ADDCALL sass_compiler_parse(struct Sass_Compiler* compiler);
# ADDAPI int ADDCALL sass_compiler_execute(struct Sass_Compiler* compiler);
# Release all memory allocated with the compiler
# This does _not_ include any contexts or options
# ADDAPI void ADDCALL sass_delete_compiler(struct Sass_Compiler* compiler);
# Release all memory allocated and also ourself
# ADDAPI void ADDCALL sass_delete_file_context (struct Sass_File_Context* ctx);
# ADDAPI void ADDCALL sass_delete_data_context (struct Sass_Data_Context* ctx);
attach_function :sass_delete_file_context, [:sass_file_context_ptr], :void
attach_function :sass_delete_data_context, [:sass_data_context_ptr], :void
# Getters for context from specific implementation
# ADDAPI struct Sass_Context* ADDCALL sass_file_context_get_context (struct Sass_File_Context* file_ctx);
# ADDAPI struct Sass_Context* ADDCALL sass_data_context_get_context (struct Sass_Data_Context* data_ctx);
attach_function :sass_file_context_get_context, [:sass_file_context_ptr], :sass_context_ptr
attach_function :sass_data_context_get_context, [:sass_data_context_ptr], :sass_context_ptr
# Getters for context options from Sass_Context
# ADDAPI struct Sass_Options* ADDCALL sass_context_get_options (struct Sass_Context* ctx);
# ADDAPI struct Sass_Options* ADDCALL sass_file_context_get_options (struct Sass_File_Context* file_ctx);
# ADDAPI struct Sass_Options* ADDCALL sass_data_context_get_options (struct Sass_Data_Context* data_ctx);
# ADDAPI void ADDCALL sass_file_context_set_options (struct Sass_File_Context* file_ctx, struct Sass_Options* opt);
# ADDAPI void ADDCALL sass_data_context_set_options (struct Sass_Data_Context* data_ctx, struct Sass_Options* opt);
attach_function :sass_context_get_options, [:sass_context_ptr], :sass_options_ptr
attach_function :sass_file_context_get_options, [:sass_file_context_ptr], :sass_options_ptr
attach_function :sass_data_context_get_options, [:sass_data_context_ptr], :sass_options_ptr
attach_function :sass_file_context_set_options, [:sass_file_context_ptr, :sass_options_ptr], :void
attach_function :sass_data_context_set_options, [:sass_data_context_ptr, :sass_options_ptr], :void
# Getters for options
# ADDAPI int ADDCALL sass_option_get_precision (struct Sass_Options* options);
# ADDAPI enum Sass_Output_Style ADDCALL sass_option_get_output_style (struct Sass_Options* options);
# ADDAPI bool ADDCALL sass_option_get_source_comments (struct Sass_Options* options);
# ADDAPI bool ADDCALL sass_option_get_source_map_embed (struct Sass_Options* options);
# ADDAPI bool ADDCALL sass_option_get_source_map_contents (struct Sass_Options* options);
# ADDAPI bool ADDCALL sass_option_get_omit_source_map_url (struct Sass_Options* options);
# ADDAPI bool ADDCALL sass_option_get_is_indented_syntax_src (struct Sass_Options* options);
# ADDAPI const char* ADDCALL sass_option_get_input_path (struct Sass_Options* options);
# ADDAPI const char* ADDCALL sass_option_get_output_path (struct Sass_Options* options);
# ADDAPI const char* ADDCALL sass_option_get_include_path (struct Sass_Options* options);
# ADDAPI const char* ADDCALL sass_option_get_source_map_file (struct Sass_Options* options);
# ADDAPI Sass_C_Function_List ADDCALL sass_option_get_c_functions (struct Sass_Options* options);
attach_function :sass_option_get_precision, [:sass_options_ptr], :int
attach_function :sass_option_get_output_style, [:sass_options_ptr], SassOutputStyle
attach_function :sass_option_get_source_comments, [:sass_options_ptr], :bool
attach_function :sass_option_get_source_map_embed, [:sass_options_ptr], :bool
attach_function :sass_option_get_source_map_contents, [:sass_options_ptr], :bool
attach_function :sass_option_get_omit_source_map_url, [:sass_options_ptr], :bool
attach_function :sass_option_get_is_indented_syntax_src, [:sass_options_ptr], :bool
attach_function :sass_option_get_input_path, [:sass_options_ptr], :string
attach_function :sass_option_get_output_path, [:sass_options_ptr], :string
attach_function :sass_option_get_include_path, [:sass_options_ptr], :string
attach_function :sass_option_get_source_map_file, [:sass_options_ptr], :string
attach_function :sass_option_get_c_functions, [:sass_options_ptr], :sass_c_function_list_ptr
# ADDAPI Sass_C_Import_Callback ADDCALL sass_option_get_importer (struct Sass_Options* options);
# Setters for options
# ADDAPI void ADDCALL sass_option_set_precision (struct Sass_Options* options, int precision);
# ADDAPI void ADDCALL sass_option_set_output_style (struct Sass_Options* options, enum Sass_Output_Style output_style);
# ADDAPI void ADDCALL sass_option_set_source_comments (struct Sass_Options* options, bool source_comments);
# ADDAPI void ADDCALL sass_option_set_source_map_embed (struct Sass_Options* options, bool source_map_embed);
# ADDAPI void ADDCALL sass_option_set_source_map_contents (struct Sass_Options* options, bool source_map_contents);
# ADDAPI void ADDCALL sass_option_set_omit_source_map_url (struct Sass_Options* options, bool omit_source_map_url);
# ADDAPI void ADDCALL sass_option_set_is_indented_syntax_src (struct Sass_Options* options, bool is_indented_syntax_src);
# ADDAPI void ADDCALL sass_option_set_input_path (struct Sass_Options* options, const char* input_path);
# ADDAPI void ADDCALL sass_option_set_output_path (struct Sass_Options* options, const char* output_path);
# ADDAPI void ADDCALL sass_option_set_include_path (struct Sass_Options* options, const char* include_path);
# ADDAPI void ADDCALL sass_option_set_source_map_file (struct Sass_Options* options, const char* source_map_file);
# ADDAPI void ADDCALL sass_option_set_c_functions (struct Sass_Options* options, Sass_C_Function_List c_functions);
# ADDAPI void ADDCALL sass_option_set_c_importers (struct Sass_Options* options, Sass_Importer_List c_importers);
attach_function :sass_option_set_precision, [:sass_options_ptr, :int], :void
attach_function :sass_option_set_output_style, [:sass_options_ptr, SassOutputStyle], :void
attach_function :sass_option_set_source_comments, [:sass_options_ptr, :bool], :void
attach_function :sass_option_set_source_map_embed, [:sass_options_ptr, :bool], :void
attach_function :sass_option_set_source_map_contents, [:sass_options_ptr, :bool], :void
attach_function :sass_option_set_omit_source_map_url, [:sass_options_ptr, :bool], :void
attach_function :sass_option_set_is_indented_syntax_src, [:sass_options_ptr, :bool], :void
attach_function :sass_option_set_input_path, [:sass_options_ptr, :string], :void
attach_function :sass_option_set_output_path, [:sass_options_ptr, :string], :void
attach_function :sass_option_set_include_path, [:sass_options_ptr, :string], :void
attach_function :sass_option_set_source_map_file, [:sass_options_ptr, :string], :void
attach_function :sass_option_set_c_functions, [:sass_options_ptr, :pointer], :void
attach_function :sass_option_set_c_importers, [:sass_options_ptr, :pointer], :void
#attach_function :sass_option_set_c_importers, [:sass_options_ptr, :sass_importer], :void
# Getter for context
# ADDAPI const char* ADDCALL sass_context_get_output_string (struct Sass_Context* ctx);
# ADDAPI int ADDCALL sass_context_get_error_status (struct Sass_Context* ctx);
# ADDAPI const char* ADDCALL sass_context_get_error_json (struct Sass_Context* ctx);
# ADDAPI const char* ADDCALL sass_context_get_error_message (struct Sass_Context* ctx);
# ADDAPI const char* ADDCALL sass_context_get_error_file (struct Sass_Context* ctx);
# ADDAPI size_t ADDCALL sass_context_get_error_line (struct Sass_Context* ctx);
# ADDAPI size_t ADDCALL sass_context_get_error_column (struct Sass_Context* ctx);
# ADDAPI const char* ADDCALL sass_context_get_source_map_string (struct Sass_Context* ctx);
# ADDAPI char** ADDCALL sass_context_get_included_files (struct Sass_Context* ctx);
attach_function :sass_context_get_output_string, [:sass_context_ptr], :string
attach_function :sass_context_get_error_status, [:sass_context_ptr], :int
attach_function :sass_context_get_error_json, [:sass_context_ptr], :string
attach_function :sass_context_get_error_message, [:sass_context_ptr], :string
attach_function :sass_context_get_error_file, [:sass_context_ptr], :string
attach_function :sass_context_get_error_line, [:sass_context_ptr], :size_t
attach_function :sass_context_get_error_column, [:sass_context_ptr], :size_t
attach_function :sass_context_get_source_map_string, [:sass_context_ptr], :string
attach_function :_context_get_included_files, :sass_context_get_included_files, [:sass_context_ptr], :pointer
def self.context_get_included_files(*args)
return_string_array _context_get_included_files(*args)
end
# ADDAPI Sass_Import_Entry ADDCALL sass_compiler_get_last_import(struct Sass_Compiler* compiler);
attach_function :sass_compiler_get_last_import, [:pointer], :pointer
end
end

View File

@@ -0,0 +1,159 @@
# frozen_string_literal: true
module SassC
module Native
# Creators for sass function list and function descriptors
# ADDAPI Sass_C_Function_List ADDCALL sass_make_function_list (size_t length);
# ADDAPI Sass_C_Function_Callback ADDCALL sass_make_function (const char* signature, Sass_C_Function fn, void* cookie);
attach_function :sass_make_function_list, [:size_t], :sass_c_function_list_ptr
attach_function :sass_make_function, [:string, :sass_c_function, :pointer], :sass_c_function_callback_ptr
# Setters and getters for callbacks on function lists
# ADDAPI Sass_C_Function_Callback ADDCALL sass_function_get_list_entry(Sass_C_Function_List list, size_t pos);
# ADDAPI void ADDCALL sass_function_set_list_entry(Sass_C_Function_List list, size_t pos, Sass_C_Function_Callback cb);
attach_function :sass_function_get_list_entry, [:sass_c_function_list_ptr, :size_t], :sass_c_function_callback_ptr
attach_function :sass_function_set_list_entry, [:sass_c_function_list_ptr, :size_t, :sass_c_function_callback_ptr], :void
# ADDAPI union Sass_Value* ADDCALL sass_make_number (double val, const char* unit);
attach_function :sass_make_number, [:double, :string], :sass_value_ptr
# ADDAPI union Sass_Value* ADDCALL sass_make_string (const char* val);
attach_function :sass_make_string, [:string], :sass_value_ptr
# ADDAPI union Sass_Value* ADDCALL sass_make_qstring (const char* val);
attach_function :sass_make_qstring, [:string], :sass_value_ptr
# ADDAPI union Sass_Value* ADDCALL sass_make_color (double r, double g, double b, double a);
attach_function :sass_make_color, [:double, :double, :double, :double], :sass_value_ptr
# ADDAPI union Sass_Value* ADDCALL sass_make_map (size_t len);
attach_function :sass_make_map, [:size_t], :sass_value_ptr
# ADDAPI union Sass_Value* ADDCALL sass_make_list (size_t len, enum Sass_Separator sep)
attach_function :sass_make_list, [:size_t, SassSeparator], :sass_value_ptr
# ADDAPI union Sass_Value* ADDCALL sass_make_boolean (boolean val);
attach_function :sass_make_boolean, [:bool], :sass_value_ptr
# ADDAPI void ADDCALL sass_map_set_key (union Sass_Value* v, size_t i, union Sass_Value*);
attach_function :sass_map_set_key, [:sass_value_ptr, :size_t, :sass_value_ptr], :void
# ADDAPI union Sass_Value* ADDCALL sass_map_get_key (const union Sass_Value* v, size_t i);
attach_function :sass_map_get_key, [:sass_value_ptr, :size_t], :sass_value_ptr
# ADDAPI void ADDCALL sass_map_set_value (union Sass_Value* v, size_t i, union Sass_Value*);
attach_function :sass_map_set_value, [:sass_value_ptr, :size_t, :sass_value_ptr], :void
# ADDAPI union Sass_Value* ADDCALL sass_map_get_value (const union Sass_Value* v, size_t i);
attach_function :sass_map_get_value, [:sass_value_ptr, :size_t], :sass_value_ptr
# ADDAPI size_t ADDCALL sass_map_get_length (const union Sass_Value* v);
attach_function :sass_map_get_length, [:sass_value_ptr], :size_t
# ADDAPI union Sass_Value* ADDCALL sass_list_get_value (const union Sass_Value* v, size_t i);
attach_function :sass_list_get_value, [:sass_value_ptr, :size_t], :sass_value_ptr
# ADDAPI void ADDCALL sass_list_set_value (union Sass_Value* v, size_t i, union Sass_Value* value);
attach_function :sass_list_set_value, [:sass_value_ptr, :size_t, :sass_value_ptr], :void
# ADDAPI size_t ADDCALL sass_list_get_length (const union Sass_Value* v);
attach_function :sass_list_get_length, [:sass_value_ptr], :size_t
# ADDAPI union Sass_Value* ADDCALL sass_make_error (const char* msg);
attach_function :sass_make_error, [:string], :sass_value_ptr
# ADDAPI enum Sass_Tag ADDCALL sass_value_get_tag (const union Sass_Value* v);
attach_function :sass_value_get_tag, [:sass_value_ptr], SassTag
attach_function :sass_value_is_null, [:sass_value_ptr], :bool
# ADDAPI const char* ADDCALL sass_string_get_value (const union Sass_Value* v);
attach_function :sass_string_get_value, [:sass_value_ptr], :string
# ADDAPI bool ADDCALL sass_string_is_quoted(const union Sass_Value* v);
attach_function :sass_string_is_quoted, [:sass_value_ptr], :bool
# ADDAPI const char* ADDCALL sass_number_get_value (const union Sass_Value* v);
attach_function :sass_number_get_value, [:sass_value_ptr], :double
# ADDAPI const char* ADDCALL sass_number_get_unit (const union Sass_Value* v);
attach_function :sass_number_get_unit, [:sass_value_ptr], :string
# ADDAPI const char* ADDCALL sass_boolean_get_value (const union Sass_Value* v);
attach_function :sass_boolean_get_value, [:sass_value_ptr], :bool
def self.string_get_type(native_value)
string_is_quoted(native_value) ? :string : :identifier
end
# ADDAPI double ADDCALL sass_color_get_r (const union Sass_Value* v);
# ADDAPI void ADDCALL sass_color_set_r (union Sass_Value* v, double r);
# ADDAPI double ADDCALL sass_color_get_g (const union Sass_Value* v);
# ADDAPI void ADDCALL sass_color_set_g (union Sass_Value* v, double g);
# ADDAPI double ADDCALL sass_color_get_b (const union Sass_Value* v);
# ADDAPI void ADDCALL sass_color_set_b (union Sass_Value* v, double b);
# ADDAPI double ADDCALL sass_color_get_a (const union Sass_Value* v);
# ADDAPI void ADDCALL sass_color_set_a (union Sass_Value* v, double a);
['r', 'g', 'b', 'a'].each do |color_channel|
attach_function "sass_color_get_#{color_channel}".to_sym, [:sass_value_ptr], :double
attach_function "sass_color_set_#{color_channel}".to_sym, [:sass_value_ptr, :double], :void
end
# ADDAPI char* ADDCALL sass_error_get_message (const union Sass_Value* v);
# ADDAPI void ADDCALL sass_error_set_message (union Sass_Value* v, char* msg);
attach_function :sass_error_get_message, [:sass_value_ptr], :string
attach_function :sass_error_set_message, [:sass_value_ptr, :pointer], :void
# Getters for custom function descriptors
# ADDAPI const char* ADDCALL sass_function_get_signature (Sass_C_Function_Callback fn);
# ADDAPI Sass_C_Function ADDCALL sass_function_get_function (Sass_C_Function_Callback fn);
# ADDAPI void* ADDCALL sass_function_get_cookie (Sass_C_Function_Callback fn);
attach_function :sass_function_get_signature, [:sass_c_function_callback_ptr], :string
attach_function :sass_function_get_function, [:sass_c_function_callback_ptr], :sass_c_function
attach_function :sass_function_get_cookie, [:sass_c_function_callback_ptr], :pointer
# Creators for custom importer callback (with some additional pointer)
# The pointer is mostly used to store the callback into the actual binding
# ADDAPI Sass_C_Import_Callback ADDCALL sass_make_importer (Sass_C_Import_Fn, void* cookie);
attach_function :sass_make_importer, [:sass_c_import_function, :pointer], :sass_importer
# Getters for import function descriptors
# ADDAPI Sass_C_Import_Fn ADDCALL sass_import_get_function (Sass_C_Import_Callback fn);
# ADDAPI void* ADDCALL sass_import_get_cookie (Sass_C_Import_Callback fn);
# Deallocator for associated memory
# ADDAPI void ADDCALL sass_delete_importer (Sass_C_Import_Callback fn);
# Creator for sass custom importer return argument list
# ADDAPI struct Sass_Import** ADDCALL sass_make_import_list (size_t length);
attach_function :sass_make_import_list, [:size_t], :sass_import_list_ptr
# Creator for a single import entry returned by the custom importer inside the list
# ADDAPI struct Sass_Import* ADDCALL sass_make_import_entry (const char* path, char* source, char* srcmap);
# ADDAPI struct Sass_Import* ADDCALL sass_make_import (const char* path, const char* base, char* source, char* srcmap);
attach_function :sass_make_import_entry, [:string, :pointer, :pointer], :sass_import_ptr
# Setters to insert an entry into the import list (you may also use [] access directly)
# Since we are dealing with pointers they should have a guaranteed and fixed size
# ADDAPI void ADDCALL sass_import_set_list_entry (struct Sass_Import** list, size_t idx, struct Sass_Import* entry);
attach_function :sass_import_set_list_entry, [:sass_import_list_ptr, :size_t, :sass_import_ptr], :void
# ADDAPI struct Sass_Import* ADDCALL sass_import_get_list_entry (struct Sass_Import** list, size_t idx);
# Getters for import entry
# ADDAPI const char* ADDCALL sass_import_get_imp_path (struct Sass_Import*);
attach_function :sass_import_get_imp_path, [:sass_import_ptr], :string
# ADDAPI const char* ADDCALL sass_import_get_abs_path (struct Sass_Import*);
attach_function :sass_import_get_abs_path, [:sass_import_ptr], :string
# ADDAPI const char* ADDCALL sass_import_get_source (struct Sass_Import*);
attach_function :sass_import_get_source, [:sass_import_ptr], :string
# ADDAPI const char* ADDCALL sass_import_get_srcmap (struct Sass_Import*);
# Explicit functions to take ownership of these items
# The property on our struct will be reset to NULL
# ADDAPI char* ADDCALL sass_import_take_source (struct Sass_Import*);
# ADDAPI char* ADDCALL sass_import_take_srcmap (struct Sass_Import*);
# Deallocator for associated memory (incl. entries)
# ADDAPI void ADDCALL sass_delete_import_list (struct Sass_Import**);
# Just in case we have some stray import structs
# ADDAPI void ADDCALL sass_delete_import (struct Sass_Import*);
end
end

View File

@@ -0,0 +1,10 @@
# frozen_string_literal: true
module SassC
module Native
# ADDAPI char* ADDCALL sass2scss (const char* sass, const int options);
attach_function :sass2scss, [:string, :int], :string
# ADDAPI const char* ADDCALL sass2scss_version(void);
end
end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
module SassC
module Native
SassInputStyle = enum(
:sass_context_null,
:sass_context_file,
:sass_context_data,
:sass_context_folder
)
end
end

View File

@@ -0,0 +1,12 @@
# frozen_string_literal: true
module SassC
module Native
SassOutputStyle = enum(
:sass_style_nested,
:sass_style_expanded,
:sass_style_compact,
:sass_style_compressed
)
end
end

View File

@@ -0,0 +1,97 @@
# frozen_string_literal: true
module SassC
module Native
class SassValue < FFI::Union; end
SassTag = enum(
:sass_boolean,
:sass_number,
:sass_color,
:sass_string,
:sass_list,
:sass_map,
:sass_null,
:sass_error,
:sass_warning
)
SassSeparator = enum(
:sass_comma,
:sass_space
)
class SassUnknown < FFI::Struct
layout :tag, SassTag
end
class SassBoolean < FFI::Struct
layout :tag, SassTag,
:value, :bool
end
class SassNumber < FFI::Struct
layout :tag, SassTag,
:value, :double,
:unit, :string
end
class SassColor < FFI::Struct
layout :tag, SassTag,
:r, :double,
:g, :double,
:b, :double,
:a, :double
end
class SassString < FFI::Struct
layout :tag, SassTag,
:value, :string
end
class SassList < FFI::Struct
layout :tag, SassTag,
:separator, SassSeparator,
:length, :size_t,
:values, :pointer
end
class SassMapPair < FFI::Struct
layout :key, SassValue.ptr,
:value, SassValue.ptr
end
class SassMap < FFI::Struct
layout :tag, SassTag,
:length, :size_t,
:pairs, SassMapPair.ptr
end
class SassNull < FFI::Struct
layout :tag, SassTag
end
class SassError < FFI::Struct
layout :tag, SassTag,
:message, :string
end
class SassWarning < FFI::Struct
layout :tag, SassTag,
:message, :string
end
class SassValue # < FFI::Union
layout :unknown, SassUnknown,
:boolean, SassBoolean,
:number, SassNumber,
:color, SassColor,
:string, SassString,
:list, SassList,
:map, SassMap,
:null, SassNull,
:error, SassError,
:warning, SassWarning
end
end
end

View File

@@ -0,0 +1,10 @@
# frozen_string_literal: true
module SassC
module Native
class StringList < FFI::Struct
layout :string_list, StringList.ptr,
:string, :string
end
end
end

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
module SassC
class Sass2Scss
def self.convert(sass)
Native.sass2scss(sass, 0)
end
end
end

View File

@@ -0,0 +1,17 @@
# frozen_string_literal: true
module SassC
module Script
def self.custom_functions(functions: Functions)
functions.public_instance_methods
end
def self.formatted_function_name(function_name, functions: Functions)
params = functions.instance_method(function_name).parameters
params = params.map { |param_type, name| "$#{name}#{': null' if param_type == :opt}" }.join(", ")
return "#{function_name}(#{params})"
end
end
end

View File

@@ -0,0 +1,8 @@
# frozen_string_literal: true
module SassC
module Script
module Functions
end
end
end

View File

@@ -0,0 +1,137 @@
# frozen_string_literal: true
# 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 SassC::Script::Value
# Returns the pure Ruby value of the value.
# The type of this value varies based on the subclass.
attr_reader :value
# The source range in the document on which this node appeared.
attr_accessor :source_range
# Creates a new 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 the official Sass reference for options.
attr_writer :options
# Returns the options hash for this node.
# Raises SassC::SyntaxError if the value was created
# outside of the parser and \{#to\_s} was called on it
def options
return @options if @options
raise SassC::SyntaxError.new("The #options attribute is not set on this #{self.class}. This error is probably occurring because #to_s was called on this value within a custom Sass function without first setting the #options attribute.")
end
# Returns the hash code of this value. Two objects' hash codes should be
# equal if the objects are equal.
def hash
value.hash
end
# True if this Value is the same as `other`
def eql?(other)
self == other
end
# Returns a system inspect value for this object
def inspect
value.inspect
end
# Returns `true` (all Values are truthy)
def to_bool
true
end
# Compares this object to `other`
def ==(other)
self.class == other.class && value == other.value
end
# Returns the integer value of this value.
# Raises SassC::SyntaxError if this value doesnt implment integer conversion.
def to_i
raise SassC::SyntaxError.new("#{inspect} is not an integer.")
end
# @raise [SassC::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`.
def separator
nil
end
# Whether the value is surrounded by square brackets. For non-list values,
# this will be `false`.
def bracketed
false
end
# Returns the value of this Value as an array.
# Single Values are considered the same as single-element arrays.
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<Value, Value>] This value as a hash
# @raise [SassC::SyntaxError] if this value doesn't have a hash representation
def to_h
raise SassC::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 = {})
SassC::Util.abstract(self)
end
alias_method :to_sass, :to_s
# Returns `false` (all Values are truthy)
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<Value>] 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)
SassC::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

View File

@@ -0,0 +1,32 @@
# frozen_string_literal: true
# A SassScript object representing a boolean (true or false) value.
class SassC::Script::Value::Bool < SassC::Script::Value
# 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.
# Tests `value` for truthiness and returns the TRUE or FALSE constant.
def self.new(value)
value ? TRUE : FALSE
end
# The pure Ruby value of this Boolean
attr_reader :value
alias_method :to_bool, :value
# Returns the string "true" or "false" for this value
def to_s(opts = {})
@value.to_s
end
alias_method :to_sass, :to_s
end

View File

@@ -0,0 +1,95 @@
# frozen_string_literal: true
# A SassScript object representing a CSS color.
# This class provides a very bare-bones system for storing a RGB(A) or HSL(A)
# color and converting it to a CSS color function.
#
# If your Sass method accepts a color you will need to perform any
# needed color mathematics or transformations yourself.
class SassC::Script::Value::Color < SassC::Script::Value
attr_reader :red
attr_reader :green
attr_reader :blue
attr_reader :hue
attr_reader :saturation
attr_reader :lightness
attr_reader :alpha
# Creates a new color with (`red`, `green`, `blue`) or (`hue`, `saturation`, `lightness`
# values, plus an optional `alpha` transparency value.
def initialize(red:nil, green:nil, blue:nil, hue:nil, saturation:nil, lightness:nil, alpha:1.0)
if red && green && blue && alpha
@mode = :rgba
@red = SassC::Util.clamp(red.to_i, 0, 255)
@green = SassC::Util.clamp(green.to_i, 0, 255)
@blue = SassC::Util.clamp(blue.to_i, 0, 255)
@alpha = SassC::Util.clamp(alpha.to_f, 0.0, 1.0)
elsif hue && saturation && lightness && alpha
@mode = :hsla
@hue = SassC::Util.clamp(hue.to_i, 0, 360)
@saturation = SassC::Util.clamp(saturation.to_i, 0, 100)
@lightness = SassC::Util.clamp(lightness.to_i, 0, 100)
@alpha = SassC::Util.clamp(alpha.to_f, 0.0, 1.0)
else
raise SassC::UnsupportedValue, "Unable to determine color configuration for "
end
end
# Returns a CSS color declaration in the form
# `rgb(…)`, `rgba(…)`, `hsl(…)`, or `hsla(…)`.
def to_s
if rgba? && @alpha == 1.0
return "rgb(#{@red}, #{@green}, #{@blue})"
elsif rgba?
return "rgba(#{@red}, #{@green}, #{@blue}, #{alpha_string})"
elsif hsla? && @alpha == 1.0
return "hsl(#{@hue}, #{@saturation}%, #{@lightness}%)"
else # hsla?
return "hsla(#{@hue}, #{@saturation}%, #{@lightness}%, #{alpha_string})"
end
end
# True if this color has RGBA values
def rgba?
@mode == :rgba
end
# True if this color has HSLA values
def hlsa?
@mode == :hlsa
end
# Returns the alpha value of this color as a string
# and rounded to 8 decimal places.
def alpha_string
alpha.round(8).to_s
end
# Returns the values of this color in an array.
# Provided for compatibility between different SassC::Script::Value classes
def value
return [
red, green, blue,
hue, saturation, lightness,
alpha,
].compact
end
# True if this Color is equal to `other_color`
def eql?(other_color)
unless other_color.is_a?(self.class)
raise ArgumentError, "No implicit conversion of #{other_color.class} to #{self.class}"
end
self.value == other_color.value
end
alias_method :==, :eql?
# Returns a numeric value for comparing two Color objects
# This method is used internally by the Hash class and is not the same as `.to_h`
def hash
value.hash
end
end

View File

@@ -0,0 +1,136 @@
# frozen_string_literal: true
# A SassScript object representing a CSS list.
# This includes both comma-separated lists and space-separated lists.
class SassC::Script::Value::List < SassC::Script::Value
# The Ruby array containing the contents of the list.
#
# @return [Array<Value>]
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<Value>] 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)
SassC::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 SassC::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

View File

@@ -0,0 +1,69 @@
# frozen_string_literal: true
class SassC::Script::Value::Map < SassC::Script::Value
# The Ruby hash containing the contents of this map.
# @return [Hash<Node, Node>]
attr_reader :value
alias_method :to_h, :value
# Creates a new map.
#
# @param hash [Hash<Node, Node>]
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 = SassC::Script::Value::List.new([k, v], separator: :space)
list.options = options
list
end
end
# @see Value#eq
def eq(other)
SassC::Script::Value::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 SassC::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

View File

@@ -0,0 +1,389 @@
# frozen_string_literal: true
# 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 SassC::Script::Value::Number < SassC::Script::Value
# 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<String>]
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<String>]
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
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
# @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<String>] The numerator units to coerce this number into.
# See {\#numerator\_units}
# @param den_units [Array<String>] 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

View File

@@ -0,0 +1,96 @@
# frozen_string_literal: true
class SassC::Script::Value::String < SassC::Script::Value
# The Ruby value of the 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
# 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)
super(value)
@type = type
end
# @see Value#plus
def plus(other)
if other.is_a?(SassC::Script::Value::String)
other_value = other.value
else
other_value = other.to_s(:quote => :none)
end
SassC::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
self.class.quote(value, opts)
end
# @see Value#to_sass
def to_sass(opts = {})
to_s(opts.merge(:sass => true))
end
def inspect
String.quote(value)
end
end

View File

@@ -0,0 +1,69 @@
# frozen_string_literal: true
module SassC::Script::ValueConversion
def self.from_native(native_value, options)
case value_tag = SassC::Native.value_get_tag(native_value)
when :sass_null
# no-op
when :sass_string
value = SassC::Native.string_get_value(native_value)
type = SassC::Native.string_get_type(native_value)
argument = SassC::Script::Value::String.new(value, type)
argument
when :sass_boolean
value = SassC::Native.boolean_get_value(native_value)
argument = SassC::Script::Value::Bool.new(value)
argument
when :sass_number
value = SassC::Native.number_get_value(native_value)
unit = SassC::Native.number_get_unit(native_value)
argument = SassC::Script::Value::Number.new(value, unit)
argument
when :sass_color
red, green, blue, alpha = SassC::Native.color_get_r(native_value), SassC::Native.color_get_g(native_value), SassC::Native.color_get_b(native_value), SassC::Native.color_get_a(native_value)
argument = SassC::Script::Value::Color.new(red:red, green:green, blue:blue, alpha:alpha)
argument.options = options
argument
when :sass_map
values = {}
length = SassC::Native::map_get_length native_value
(0..length-1).each do |index|
key = SassC::Native::map_get_key(native_value, index)
value = SassC::Native::map_get_value(native_value, index)
values[from_native(key, options)] = from_native(value, options)
end
argument = SassC::Script::Value::Map.new values
argument
when :sass_list
length = SassC::Native::list_get_length(native_value)
items = (0...length).map do |index|
native_item = SassC::Native::list_get_value(native_value, index)
from_native(native_item, options)
end
SassC::Script::Value::List.new(items, separator: :space)
else
raise UnsupportedValue.new("Sass argument of type #{value_tag} unsupported")
end
end
def self.to_native(value)
case value_name = value.class.name.split("::").last
when "String"
SassC::Script::ValueConversion::String.new(value).to_native
when "Color"
SassC::Script::ValueConversion::Color.new(value).to_native
when "Number"
SassC::Script::ValueConversion::Number.new(value).to_native
when "Map"
SassC::Script::ValueConversion::Map.new(value).to_native
when "List"
SassC::Script::ValueConversion::List.new(value).to_native
when "Bool"
SassC::Script::ValueConversion::Bool.new(value).to_native
else
raise SassC::UnsupportedValue.new("Sass return type #{value_name} unsupported")
end
end
end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
module SassC
module Script
module ValueConversion
class Base
def initialize(value)
@value = value
end
end
end
end
end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
module SassC
module Script
module ValueConversion
class Bool < Base
def to_native
Native::make_boolean(@value.value)
end
end
end
end
end

View File

@@ -0,0 +1,18 @@
# frozen_string_literal: true
module SassC
module Script
module ValueConversion
class Color < Base
def to_native
Native::make_color(
@value.red,
@value.green,
@value.blue,
@value.alpha
)
end
end
end
end
end

View File

@@ -0,0 +1,25 @@
# frozen_string_literal: true
module SassC
module Script
module ValueConversion
SEPARATORS = {
space: :sass_space,
comma: :sass_comma
}
class List < Base
def to_native
list = @value.to_a
sep = SEPARATORS.fetch(@value.separator)
native_list = Native::make_list(list.size, sep)
list.each_with_index do |item, index|
native_item = ValueConversion.to_native(item)
Native::list_set_value(native_list, index, native_item)
end
native_list
end
end
end
end
end

View File

@@ -0,0 +1,21 @@
# frozen_string_literal: true
module SassC
module Script
module ValueConversion
class Map < Base
def to_native
hash = @value.to_h
native_map = Native::make_map( hash.size )
hash.each_with_index do |(key, value), index|
key = ValueConversion.to_native key
value = ValueConversion.to_native value
Native::map_set_key( native_map, index, key )
Native::map_set_value( native_map, index, value )
end
return native_map
end
end
end
end
end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
module SassC
module Script
module ValueConversion
class Number < Base
def to_native
Native::make_number(@value.value, @value.numerator_units.first)
end
end
end
end
end

View File

@@ -0,0 +1,17 @@
# frozen_string_literal: true
module SassC
module Script
module ValueConversion
class String < Base
def to_native(opts = {})
if opts[:quote] == :none || @value.type == :identifier
Native::make_string(@value.to_s)
else
Native::make_qstring(@value.to_s)
end
end
end
end
end
end

View File

@@ -0,0 +1,231 @@
# frozen_string_literal: true
require "erb"
require "set"
require "enumerator"
require "stringio"
require "rbconfig"
require "uri"
require "thread"
require "pathname"
# A module containing various useful functions.
module SassC::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"
# 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
# Restricts the numeric `value` to be within `min` and `max`, inclusive.
# If the value is lower than `min`
def clamp(value, min, max)
return min if value < min
return max if value > max
return value
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 < SassC::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
# Return an array of all possible paths through the given arrays.
#
# @param arrs [Array<Array>]
# @return [Array<Arrays>]
#
# @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
# 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
# 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}"
SassC::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
SassC.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
## 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<Integer>]
def jruby_version
@jruby_version ||= ::JRUBY_VERSION.split(".").map {|s| s.to_i}
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
singleton_methods.each {|method| module_function method}
end

View File

@@ -0,0 +1,117 @@
# frozen_string_literal: true
require "delegate"
# 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 SassC::Util::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
SassC::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)
@map.send(method, *args, &block)
end
def respond_to_missing?(method, include_private = false)
@map.respond_to?(method, include_private)
end
end

View File

@@ -0,0 +1,5 @@
# frozen_string_literal: true
module SassC
VERSION = "2.4.0"
end