Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
@@ -0,0 +1,22 @@
# frozen_string_literal: true
module Sass
# The type of values that can be arguments to a SassCalculation.
#
# @see https://sass-lang.com/documentation/js-api/types/calculationvalue/
module CalculationValue
private
def assert_calculation_value(value, name = nil)
if !value.is_a?(Sass::CalculationValue) || (value.is_a?(Sass::Value::String) && value.quoted?)
raise Sass::ScriptError.new(
"#{value} must be one of SassNumber, unquoted SassString, SassCalculation, CalculationOperation", name
)
end
value
end
end
end
require_relative 'calculation_value/calculation_operation'
@@ -0,0 +1,49 @@
# frozen_string_literal: true
module Sass
module CalculationValue
# A binary operation that can appear in a SassCalculation.
#
# @see https://sass-lang.com/documentation/js-api/classes/calculationoperation/
class CalculationOperation
include CalculationValue
OPERATORS = %w[+ - * /].freeze
private_constant :OPERATORS
# @param operator [::String]
# @param left [CalculationValue]
# @param right [CalculationValue]
def initialize(operator, left, right)
raise Sass::ScriptError, "Invalid operator: #{operator}" unless OPERATORS.include?(operator)
@operator = operator.freeze
@left = assert_calculation_value(left, 'left')
@right = assert_calculation_value(right, 'right')
end
# @return [::String]
attr_reader :operator
# @return [CalculationValue]
attr_reader :left
# @return [CalculationValue]
attr_reader :right
# @return [::Boolean]
def ==(other)
other.is_a?(Sass::CalculationValue::CalculationOperation) &&
other.operator == operator &&
other.left == left &&
other.right == right
end
# @return [Integer]
def hash
@hash ||= [operator, left, right].hash
end
end
end
end
@@ -0,0 +1,25 @@
# frozen_string_literal: true
module Sass
# Contextual information passed to `canonicalize` and `find_file_url`.
# Not all importers will need this information to resolve loads, but some may find it useful.
#
# @see https://sass-lang.com/documentation/js-api/interfaces/canonicalizecontext/
class CanonicalizeContext
# @return [String, nil]
def containing_url
@containing_url_unused = false
@containing_url
end
# @return [Boolean]
attr_reader :from_import
# @!visibility private
def initialize(canonicalize_request)
@containing_url_unused = true
@containing_url = canonicalize_request.containing_url == '' ? nil : canonicalize_request.containing_url
@from_import = canonicalize_request.from_import
end
end
end
@@ -0,0 +1,19 @@
# frozen_string_literal: true
require 'sass/elf'
module Sass
module CLI
INTERPRETER = '/lib64/ld-linux-x86-64.so.2'
INTERPRETER_SUFFIX = '/ld-linux-x86-64.so.2'
COMMAND = [
*(ELF::INTERPRETER if ELF::INTERPRETER != INTERPRETER && ELF::INTERPRETER&.end_with?(INTERPRETER_SUFFIX)),
File.absolute_path('dart-sass/src/dart', __dir__).freeze,
File.absolute_path('dart-sass/src/sass.snapshot', __dir__).freeze
].freeze
end
private_constant :CLI
end
@@ -0,0 +1,24 @@
# frozen_string_literal: true
module Sass
# The result of compiling Sass to CSS. Returned by {Sass.compile} and {Sass.compile_string}.
#
# @see https://sass-lang.com/documentation/js-api/interfaces/compileresult/
class CompileResult
# @return [String]
attr_reader :css
# @return [String, nil]
attr_reader :source_map
# @return [Array<String>]
attr_reader :loaded_urls
# @!visibility private
def initialize(css, source_map, loaded_urls)
@css = css
@source_map = source_map
@loaded_urls = loaded_urls
end
end
end
@@ -0,0 +1,214 @@
# frozen_string_literal: true
require_relative 'canonicalize_context'
require_relative 'compile_result'
require_relative 'compiler/channel'
require_relative 'compiler/connection'
require_relative 'compiler/dispatcher'
require_relative 'compiler/session'
require_relative 'compiler/varint'
require_relative 'embedded/version'
require_relative 'embedded_protocol'
require_relative 'exception'
require_relative 'fork_tracker'
require_relative 'gem_package_importer'
require_relative 'logger/silent'
require_relative 'logger/source_location'
require_relative 'logger/source_span'
require_relative 'node_package_importer'
require_relative 'serializer'
require_relative 'uri'
require_relative 'value'
module Sass
# A synchronous {Compiler}.
# Each compiler instance exposes the {#compile} and {#compile_string} methods within the lifespan of the compiler.
#
# @example
# sass = Sass::Compiler.new
# result = sass.compile_string('h1 { font-size: 40px; }')
# result = sass.compile('style.scss')
# sass.close
# @see https://sass-lang.com/documentation/js-api/classes/compiler/
class Compiler
def initialize
@channel = Channel.new
end
# Compiles the Sass file at +path+ to CSS.
# @param path [String]
# @param load_paths [Array<String>] Paths in which to look for stylesheets loaded by rules like
# {@use}[https://sass-lang.com/documentation/at-rules/use/] and {@import}[https://sass-lang.com/documentation/at-rules/import/].
# @param charset [Boolean] By default, if the CSS document contains non-ASCII characters, Sass adds a +@charset+
# declaration (in expanded output mode) or a byte-order mark (in compressed mode) to indicate its encoding to
# browsers or other consumers. If +charset+ is +false+, these annotations are omitted.
# @param source_map [Boolean] Whether or not Sass should generate a source map.
# @param source_map_include_sources [Boolean] Whether Sass should include the sources in the generated source map.
# @param style [Symbol] The OutputStyle of the compiled CSS.
# @param functions [Hash<String, Proc>] Additional built-in Sass functions that are available in all stylesheets.
# @param importers [Array<Object>] Custom importers that control how Sass resolves loads from rules like
# {@use}[https://sass-lang.com/documentation/at-rules/use/] and {@import}[https://sass-lang.com/documentation/at-rules/import/].
# @param alert_ascii [Boolean] If this is +true+, the compiler will exclusively use ASCII characters in its error
# and warning messages. Otherwise, it may use non-ASCII Unicode characters as well.
# @param alert_color [Boolean] If this is +true+, the compiler will use ANSI color escape codes in its error and
# warning messages. If it's +false+, it won't use these. If it's +nil+, the compiler will determine whether or
# not to use colors depending on whether the user is using an interactive terminal.
# @param fatal_deprecations [Array<String>] A set of deprecations to treat as fatal.
# @param future_deprecations [Array<String>] A set of future deprecations to opt into early.
# @param logger [Object] An object to use to handle warnings and/or debug messages from Sass.
# @param quiet_deps [Boolean] If this option is set to +true+, Sass won’t print warnings that are caused by
# dependencies. A “dependency” is defined as any file that’s loaded through +load_paths+ or +importer+.
# Stylesheets that are imported relative to the entrypoint are not considered dependencies.
# @param silence_deprecations [Array<String>] A set of active deprecations to ignore.
# @param verbose [Boolean] By default, Dart Sass will print only five instances of the same deprecation warning per
# compilation to avoid deluging users in console noise. If you set verbose to +true+, it will instead print every
# deprecation warning it encounters.
# @return [CompileResult]
# @raise [ArgumentError, CompileError, IOError]
# @see https://sass-lang.com/documentation/js-api/functions/compile/
def compile(path,
load_paths: [],
charset: true,
source_map: false,
source_map_include_sources: false,
style: :expanded,
functions: {},
importers: [],
alert_ascii: false,
alert_color: nil,
fatal_deprecations: [],
future_deprecations: [],
logger: nil,
quiet_deps: false,
silence_deprecations: [],
verbose: false)
raise ArgumentError, 'path must be set' if path.nil?
Session.new(@channel).compile_request(
path:,
source: nil,
importer: nil,
load_paths:,
syntax: nil,
url: nil,
charset:,
source_map:,
source_map_include_sources:,
style:,
functions:,
importers:,
alert_color:,
alert_ascii:,
fatal_deprecations:,
future_deprecations:,
logger:,
quiet_deps:,
silence_deprecations:,
verbose:
)
end
# Compiles a stylesheet whose contents is +source+ to CSS.
# @param source [String]
# @param importer [Object] The importer to use to handle loads that are relative to the entrypoint stylesheet.
# @param load_paths [Array<String>] Paths in which to look for stylesheets loaded by rules like
# {@use}[https://sass-lang.com/documentation/at-rules/use/] and {@import}[https://sass-lang.com/documentation/at-rules/import/].
# @param syntax [Symbol] The Syntax to use to parse the entrypoint stylesheet.
# @param url [String] The canonical URL of the entrypoint stylesheet. If this is passed along with +importer+, it's
# used to resolve relative loads in the entrypoint stylesheet.
# @param charset [Boolean] By default, if the CSS document contains non-ASCII characters, Sass adds a +@charset+
# declaration (in expanded output mode) or a byte-order mark (in compressed mode) to indicate its encoding to
# browsers or other consumers. If +charset+ is +false+, these annotations are omitted.
# @param source_map [Boolean] Whether or not Sass should generate a source map.
# @param source_map_include_sources [Boolean] Whether Sass should include the sources in the generated source map.
# @param style [Symbol] The OutputStyle of the compiled CSS.
# @param functions [Hash<String, Proc>] Additional built-in Sass functions that are available in all stylesheets.
# @param importers [Array<Object>] Custom importers that control how Sass resolves loads from rules like
# {@use}[https://sass-lang.com/documentation/at-rules/use/] and {@import}[https://sass-lang.com/documentation/at-rules/import/].
# @param alert_ascii [Boolean] If this is +true+, the compiler will exclusively use ASCII characters in its error
# and warning messages. Otherwise, it may use non-ASCII Unicode characters as well.
# @param alert_color [Boolean] If this is +true+, the compiler will use ANSI color escape codes in its error and
# warning messages. If it's +false+, it won't use these. If it's +nil+, the compiler will determine whether or
# not to use colors depending on whether the user is using an interactive terminal.
# @param fatal_deprecations [Array<String>] A set of deprecations to treat as fatal.
# @param future_deprecations [Array<String>] A set of future deprecations to opt into early.
# @param logger [Object] An object to use to handle warnings and/or debug messages from Sass.
# @param quiet_deps [Boolean] If this option is set to +true+, Sass won’t print warnings that are caused by
# dependencies. A “dependency” is defined as any file that’s loaded through +load_paths+ or +importer+.
# Stylesheets that are imported relative to the entrypoint are not considered dependencies.
# @param silence_deprecations [Array<String>] A set of active deprecations to ignore.
# @param verbose [Boolean] By default, Dart Sass will print only five instances of the same deprecation warning per
# compilation to avoid deluging users in console noise. If you set verbose to +true+, it will instead print every
# deprecation warning it encounters.
# @return [CompileResult]
# @raise [ArgumentError, CompileError, IOError]
# @see https://sass-lang.com/documentation/js-api/functions/compilestring/
def compile_string(source,
importer: nil,
load_paths: [],
syntax: :scss,
url: nil,
charset: true,
source_map: false,
source_map_include_sources: false,
style: :expanded,
functions: {},
importers: [],
alert_ascii: false,
alert_color: nil,
fatal_deprecations: [],
future_deprecations: [],
logger: nil,
quiet_deps: false,
silence_deprecations: [],
verbose: false)
raise ArgumentError, 'source must be set' if source.nil?
Session.new(@channel).compile_request(
path: nil,
source:,
importer:,
load_paths:,
syntax:,
url:,
charset:,
source_map:,
source_map_include_sources:,
style:,
functions:,
importers:,
alert_color:,
alert_ascii:,
fatal_deprecations:,
future_deprecations:,
logger:,
quiet_deps:,
silence_deprecations:,
verbose:
)
end
# @return [String] Information about the Sass implementation.
# @see https://sass-lang.com/documentation/js-api/variables/info/
def info
@info ||= [
['sass-embedded', Embedded::VERSION, '(Embedded Host)', '[Ruby]'].join("\t"),
Session.new(@channel).version_request.join("\t")
].join("\n").freeze
end
def close
@channel.close
end
def closed?
@channel.closed?
end
end
end
@@ -0,0 +1,70 @@
# frozen_string_literal: true
module Sass
class Compiler
# The {Channel} class.
#
# It manages the lifecycle of {Dispatcher}.
class Channel
def initialize(*args, **kwargs, &block)
@args = args
@kwargs = kwargs
@block = block
@dispatcher = Dispatcher.new(*@args, **@kwargs, &@block)
@mutex = Mutex.new
end
def close
@mutex.synchronize do
unless @dispatcher.nil?
@dispatcher.close
@dispatcher = nil
end
end
end
def closed?
@mutex.synchronize do
@dispatcher.nil?
end
end
def stream(session)
@mutex.synchronize do
raise IOError, 'closed compiler' if @dispatcher.nil?
Stream.new(@dispatcher, session)
rescue Errno::EBUSY
@dispatcher = Dispatcher.new(*@args, **@kwargs, &@block)
Stream.new(@dispatcher, session)
end
end
# The {Stream} between {Dispatcher} and {Session}.
class Stream
attr_reader :id
def initialize(dispatcher, session)
@dispatcher = dispatcher
@id = @dispatcher.subscribe(session)
end
def close
@dispatcher.unsubscribe(@id)
end
def error(...)
@dispatcher.error(...)
end
def send_proto(...)
@dispatcher.send_proto(...)
end
end
private_constant :Stream
end
private_constant :Channel
end
end
@@ -0,0 +1,90 @@
# frozen_string_literal: true
require 'open3'
require 'sass/cli'
module Sass
class Compiler
# The stdio based {Connection} between the {Dispatcher} and the compiler.
#
# It runs the `sass --embedded` command.
class Connection
def initialize
@mutex = Mutex.new
@stdin, @stdout, @stderr, @wait_thread = Open3.popen3(*CLI::COMMAND, '--embedded', chdir: __dir__)
@stdin.binmode
# # https://dart.dev/tools/dart-devtools
# if %w[dart dartvm].include?(File.basename(CLI::COMMAND.first, '.exe')) &&
# %w[--enable-vm-service --observe].intersect?(CLI::COMMAND.map { |argument| argument.partition('=').first })
# Kernel.warn(@stdout.readline, uplevel: 0)
# Kernel.warn(@stdout.readline, uplevel: 0)
# end
@stdout.binmode
@wait_thread.name = "sass-embedded-process-waiter-#{id}"
end
def id
@wait_thread.pid
end
def listen(dispatcher)
Thread.new do
Thread.current.name = "sass-embedded-process-stdout-poller-#{id}"
loop do
length = Varint.read(@stdout)
id = Varint.read(@stdout)
proto = @stdout.read(length - Varint.length(id))
dispatcher.receive_proto(id, proto)
end
rescue IOError, Errno::EBADF, Errno::EPROTO => e
dispatcher.error(e)
@mutex.synchronize do
@stdout.close
end
end
Thread.new do
Thread.current.name = "sass-embedded-process-stderr-poller-#{id}"
loop do
Kernel.warn(@stderr.readline, uplevel: 0)
end
rescue IOError, Errno::EBADF
@mutex.synchronize do
@stderr.close
end
end
end
def close
@mutex.synchronize do
@stdin.close
@wait_thread.join
@stdout.close
@stderr.close
end
end
def closed?
@mutex.synchronize do
@stdin.closed? && !@wait_thread.alive?
end
end
def write(id, proto)
buffer = []
Varint.write(buffer, Varint.length(id) + proto.length)
Varint.write(buffer, id)
@mutex.synchronize do
@stdin.write(buffer.pack('C*'), proto)
end
end
end
private_constant :Connection
end
end
@@ -0,0 +1,142 @@
# frozen_string_literal: true
module Sass
class Compiler
# The {Dispatcher} class.
#
# It dispatches messages between multiple instances of {Session} and a single {Connection} to the compiler.
class Dispatcher
def initialize(idle_timeout: 0)
@id = 1
@observers = {}.compare_by_identity
@mutex = Mutex.new
@connection = Connection.new
@connection.listen(self)
ForkTracker.add(self)
return unless idle_timeout.positive?
@last_accessed_time = current_time
Thread.new do
Thread.current.name = "sass-embedded-connection-reaper-#{@connection.id}"
duration = idle_timeout
loop do
sleep(duration.negative? ? idle_timeout : duration)
break if @mutex.synchronize do
raise Errno::EBUSY if _closed?
duration = idle_timeout - (current_time - @last_accessed_time)
duration.negative? && _idle? && _close
end
end
close
rescue Errno::EBUSY
# do nothing
end
end
def subscribe(observer)
@mutex.synchronize do
raise Errno::EBUSY if _closed?
id = @id
@id = id.next
@observers[id] = observer
id
end
end
def unsubscribe(id)
@mutex.synchronize do
@observers.delete(id)
return unless @observers.empty?
if _closed?
Thread.new do
close
end
else
_idle
end
end
end
def close
@mutex.synchronize do
_close
end
@connection.close
ForkTracker.delete(self)
end
def closed?
@connection.closed?
end
def error(error)
observers = @mutex.synchronize do
_close
@observers.values
end
if observers.empty?
close
else
observers.each do |observer|
observer.error(error)
end
end
end
def receive_proto(id, proto)
case id
when 1...0xffffffff
@mutex.synchronize { @observers[id] }&.receive_proto(proto)
when 0
outbound_message = EmbeddedProtocol::OutboundMessage.decode(proto)
oneof = outbound_message.message
message = outbound_message.public_send(oneof)
@mutex.synchronize { @observers[message.id] }&.public_send(oneof, message)
when 0xffffffff
outbound_message = EmbeddedProtocol::OutboundMessage.decode(proto)
oneof = outbound_message.message
message = outbound_message.public_send(oneof)
raise Errno::EPROTO, message.message
else
raise Errno::EPROTO
end
end
def send_proto(...)
@connection.write(...)
end
private
def current_time
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
def _close
@id = 0xffffffff
end
def _closed?
@id == 0xffffffff
end
def _idle
@last_accessed_time = current_time if defined?(@last_accessed_time)
@id = 1
end
def _idle?
@id == 1
end
end
private_constant :Dispatcher
end
end
@@ -0,0 +1,233 @@
# frozen_string_literal: true
require_relative 'session/function_registry'
require_relative 'session/importer_registry'
require_relative 'session/logger_registry'
require_relative 'session/path'
require_relative 'session/protofier'
require_relative 'session/stack_trace'
require_relative 'session/struct'
module Sass
class Compiler
# The {Session} class.
#
# It communicates with {Dispatcher} and handles the host logic.
class Session
attr_writer :backtrace
def initialize(channel)
@channel = channel
@backtrace = nil
end
def compile_request(path:,
source:,
importer:,
load_paths:,
syntax:,
url:,
charset:,
source_map:,
source_map_include_sources:,
style:,
functions:,
importers:,
alert_ascii:,
alert_color:,
fatal_deprecations:,
future_deprecations:,
logger:,
quiet_deps:,
silence_deprecations:,
verbose:)
alert_color = Exception.to_tty? if alert_color.nil?
@function_registry = FunctionRegistry.new(functions, session: self)
@importer_registry = ImporterRegistry.new(importers, load_paths, session: self)
@logger_registry = LoggerRegistry.new(logger)
compile_request = EmbeddedProtocol::InboundMessage::CompileRequest.new(
string: unless source.nil?
EmbeddedProtocol::InboundMessage::CompileRequest::StringInput.new(
source: source.to_str,
url: url&.to_s,
syntax: @importer_registry.syntax_to_proto(syntax),
importer: (@importer_registry.register(importer) unless importer.nil?)
)
end,
path: (File.absolute_path(path) unless path.nil?),
style: case style&.to_sym
when :expanded
EmbeddedProtocol::OutputStyle::EXPANDED
when :compressed
EmbeddedProtocol::OutputStyle::COMPRESSED
else
raise ArgumentError, 'style must be one of :expanded, :compressed'
end,
charset:,
source_map:,
source_map_include_sources:,
importers: @importer_registry.importers,
global_functions: @function_registry.global_functions,
alert_ascii:,
alert_color:,
fatal_deprecation: fatal_deprecations.map(&:to_s),
future_deprecation: future_deprecations.map(&:to_s),
quiet_deps:,
silent: logger == Logger.silent,
silence_deprecation: silence_deprecations.map(&:to_s),
verbose:
)
compile_response = await do
send_message(compile_request:)
end
oneof = compile_response.result
result = compile_response.public_send(oneof)
case oneof
when :failure
compile_error = CompileError.new(
result.message,
result.formatted == '' ? nil : StackTrace.pretty_formatted!(+result.formatted, result.stack_trace),
result.stack_trace == '' ? nil : result.stack_trace,
result.span.nil? ? nil : Logger::SourceSpan.new(result.span),
compile_response.loaded_urls.to_a
)
compile_error.set_backtrace(@backtrace) unless @backtrace.nil?
raise compile_error
when :success
CompileResult.new(
result.css,
result.source_map == '' ? nil : result.source_map,
compile_response.loaded_urls.to_a
)
else
raise ArgumentError, "Unknown CompileResponse.result #{result}"
end
end
def version_request
version_response = await0 do
send_message0(version_request: EmbeddedProtocol::InboundMessage::VersionRequest.new(
id:
))
end
info = [
version_response.implementation_name,
version_response.implementation_version,
'(Sass Compiler)'
]
case version_response.implementation_name
when 'dart-sass'
info << (File.basename(CLI::COMMAND.first, '.exe') == 'node' ? '[JavaScript]' : '[Dart]')
end
info
end
def compile_response(message)
@result = message
@queue.close
end
def version_response(message)
@result = message
@queue.close
end
def error(message)
case message
when EmbeddedProtocol::ProtocolError
raise Errno::EPROTO, message.message
else
@error ||= message
@queue.close
end
end
def log_event(message)
@logger_registry.log(message)
end
def canonicalize_request(message)
send_message(canonicalize_response: @importer_registry.canonicalize(message))
end
def import_request(message)
send_message(import_response: @importer_registry.import(message))
end
def file_import_request(message)
send_message(file_import_response: @importer_registry.file_import(message))
end
def function_call_request(message)
send_message(function_call_response: @function_registry.function_call(message))
end
def receive_proto(proto)
@queue.push(proto)
end
private
def await0
listen do
yield
@queue.pop
end
end
def await
listen do
yield
while (proto = @queue.pop)
outbound_message = EmbeddedProtocol::OutboundMessage.decode(proto)
oneof = outbound_message.message
message = outbound_message.public_send(oneof)
public_send(oneof, message)
end
rescue Exception => e # rubocop:disable Lint/RescueException
@stream.error(e)
raise
end
end
def listen
@queue = Queue.new
@stream = @channel.stream(self)
yield
raise @error if @error
@result
ensure
@stream&.close
@queue&.close
end
def id
@stream.id
end
def send_message0(...)
inbound_message = EmbeddedProtocol::InboundMessage.new(...)
@stream.send_proto(0, EmbeddedProtocol::InboundMessage.encode(inbound_message))
end
def send_message(...)
inbound_message = EmbeddedProtocol::InboundMessage.new(...)
@stream.send_proto(id, EmbeddedProtocol::InboundMessage.encode(inbound_message))
end
end
private_constant :Session
end
end
@@ -0,0 +1,88 @@
# frozen_string_literal: true
module Sass
class Compiler
class Session
# The {FunctionRegistry} class.
#
# It stores sass custom functions and handles function calls.
class FunctionRegistry
attr_reader :compile_context, :global_functions
def initialize(functions, session:)
@compile_context = Object.new
@global_functions = functions.keys.map!(&:to_s)
@functions_by_name = functions.transform_keys do |signature|
signature = signature.to_s
index = signature.index('(')
if index
signature.slice(0, index)
else
signature
end
end
@id = 0
@functions_by_id = {}.compare_by_identity
@ids_by_function = {}.compare_by_identity
@session = session
end
def register(function)
@ids_by_function.fetch(function) do |fn|
id = @id
@id = id.next
@functions_by_id[id] = fn
@ids_by_function[fn] = id
end
end
def function_call(function_call_request)
oneof = function_call_request.identifier
identifier = function_call_request.public_send(oneof)
function = case oneof
when :name
@functions_by_name[identifier]
when :function_id
@functions_by_id[identifier]
else
raise ArgumentError, "Unknown FunctionCallRequest.identifier #{identifier}"
end
arguments = function_call_request.arguments.map do |argument|
protofier.from_proto(argument)
end
success = protofier.to_proto(function.call(arguments))
accessed_argument_lists = arguments.filter_map do |argument|
if argument.is_a?(Sass::Value::ArgumentList) && argument.instance_variable_get(:@keywords_accessed)
argument.instance_variable_get(:@id)
end
end
EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
id: function_call_request.id,
success:,
accessed_argument_lists:
)
rescue StandardError => e
@session.backtrace = e.backtrace
EmbeddedProtocol::InboundMessage::FunctionCallResponse.new(
id: function_call_request.id,
error: e.detailed_message(highlight: false)
)
end
private
def protofier
@protofier ||= Protofier.new(self)
end
end
private_constant :FunctionRegistry
end
end
end
@@ -0,0 +1,150 @@
# frozen_string_literal: true
module Sass
class Compiler
class Session
# The {ImporterRegistry} class.
#
# It stores importers and handles import requests.
class ImporterRegistry
attr_reader :importers
def initialize(importers, load_paths, session:)
@id = 0
@importers_by_id = {}.compare_by_identity
@importers = importers
.map { |importer| register(importer) }
.concat(
load_paths.map do |load_path|
EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
path: File.absolute_path(load_path)
)
end
)
@session = session
end
IMPORTER_ATTRS = %i[non_canonical_scheme].freeze
IMPORTER_METHODS = %i[canonicalize load find_file_url].freeze
private_constant :IMPORTER_ATTRS, :IMPORTER_METHODS
def register(importer)
if importer.is_a?(Sass::NodePackageImporter)
EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
node_package_importer: EmbeddedProtocol::NodePackageImporter.new(
entry_point_directory: importer.instance_variable_get(:@entry_point_directory)
)
)
else
importer = Struct.new(importer, attrs: IMPORTER_ATTRS, methods: IMPORTER_METHODS) if importer.is_a?(::Hash)
is_importer = importer.respond_to?(:canonicalize) && importer.respond_to?(:load)
is_file_importer = importer.respond_to?(:find_file_url)
raise ArgumentError, 'importer must be an Importer or a FileImporter' if is_importer == is_file_importer
id = @id
@id = id.next
@importers_by_id[id] = importer
if is_importer
EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
importer_id: id,
non_canonical_scheme: if importer.respond_to?(:non_canonical_scheme)
Array(importer.non_canonical_scheme)
else
[]
end
)
else
EmbeddedProtocol::InboundMessage::CompileRequest::Importer.new(
file_importer_id: id
)
end
end
end
def canonicalize(canonicalize_request)
importer = @importers_by_id[canonicalize_request.importer_id]
canonicalize_context = CanonicalizeContext.new(canonicalize_request)
url = importer.canonicalize(canonicalize_request.url,
canonicalize_context)&.to_s
EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
id: canonicalize_request.id,
url:,
containing_url_unused: canonicalize_context.instance_variable_get(:@containing_url_unused)
)
rescue StandardError => e
@session.backtrace = e.backtrace
EmbeddedProtocol::InboundMessage::CanonicalizeResponse.new(
id: canonicalize_request.id,
error: e.detailed_message(highlight: false)
)
end
IMPORTER_RESULT_ATTRS = %i[contents syntax source_map_url].freeze
private_constant :IMPORTER_RESULT_ATTRS
def import(import_request)
importer = @importers_by_id[import_request.importer_id]
importer_result = importer.load(import_request.url)
importer_result = Struct.new(importer_result, attrs: IMPORTER_RESULT_ATTRS) if importer_result.is_a?(::Hash)
EmbeddedProtocol::InboundMessage::ImportResponse.new(
id: import_request.id,
success: EmbeddedProtocol::InboundMessage::ImportResponse::ImportSuccess.new(
contents: importer_result.contents.to_str,
syntax: syntax_to_proto(importer_result.syntax),
source_map_url: (importer_result.source_map_url&.to_s if importer_result.respond_to?(:source_map_url))
)
)
rescue StandardError => e
@session.backtrace = e.backtrace
EmbeddedProtocol::InboundMessage::ImportResponse.new(
id: import_request.id,
error: e.detailed_message(highlight: false)
)
end
def file_import(file_import_request)
importer = @importers_by_id[file_import_request.importer_id]
canonicalize_context = CanonicalizeContext.new(file_import_request)
file_url = importer.find_file_url(file_import_request.url,
canonicalize_context)&.to_s
EmbeddedProtocol::InboundMessage::FileImportResponse.new(
id: file_import_request.id,
file_url:,
containing_url_unused: canonicalize_context.instance_variable_get(:@containing_url_unused)
)
rescue StandardError => e
@session.backtrace = e.backtrace
EmbeddedProtocol::InboundMessage::FileImportResponse.new(
id: file_import_request.id,
error: e.detailed_message(highlight: false)
)
end
def syntax_to_proto(syntax)
case syntax&.to_sym
when :scss
EmbeddedProtocol::Syntax::SCSS
when :indented
EmbeddedProtocol::Syntax::INDENTED
when :css
EmbeddedProtocol::Syntax::CSS
else
raise ArgumentError, 'syntax must be one of :scss, :indented, :css'
end
end
end
private_constant :ImporterRegistry
end
end
end
@@ -0,0 +1,77 @@
# frozen_string_literal: true
module Sass
class Compiler
class Session
# The {LoggerRegistry} class.
#
# It stores logger and handles log events.
class LoggerRegistry
LOGGER_METHODS = %i[debug warn].freeze
private_constant :LOGGER_METHODS
def initialize(logger)
logger = Struct.new(logger, methods: LOGGER_METHODS) if logger.is_a?(::Hash)
@logger = logger
@logger_respond_to_debug = logger.respond_to?(:debug)
@logger_respond_to_warn = logger.respond_to?(:warn)
end
def log(event)
case event.type
when :DEBUG
if @logger_respond_to_debug
@logger.debug(event.message, DebugContext.new(event))
else
Kernel.warn(Path.pretty_formatted!(+event.formatted, event.span.url))
end
when :DEPRECATION_WARNING, :WARNING
if @logger_respond_to_warn
@logger.warn(event.message, WarnContext.new(event))
else
Kernel.warn(StackTrace.pretty_formatted!(+event.formatted, event.stack_trace))
end
else
raise ArgumentError, "Unknown LogEvent.type #{event.type}"
end
end
# Contextual information passed to `debug`.
class DebugContext
# @return [Logger::SourceSpan, nil]
attr_reader :span
def initialize(event)
@span = event.span.nil? ? nil : Logger::SourceSpan.new(event.span)
end
end
private_constant :DebugContext
# Contextual information passed to `warn`.
class WarnContext < DebugContext
# @return [Boolean]
attr_reader :deprecation
# @return [String, nil]
attr_reader :deprecation_type
# @return [String]
attr_reader :stack
def initialize(event)
super
@deprecation = event.type == :DEPRECATION_WARNING
@deprecation_type = (event.deprecation_type if @deprecation)
@stack = event.stack_trace
end
end
private_constant :WarnContext
end
private_constant :LoggerRegistry
end
end
end
@@ -0,0 +1,34 @@
# frozen_string_literal: true
module Sass
class Compiler
class Session
# @see https://pub.dev/documentation/path/latest/path/
module Path
module_function
# @see https://pub.dev/documentation/path/latest/path/Context/prettyUri.html
def pretty_uri(uri)
return uri unless uri&.start_with?('file:')
absolute_path = Uri.file_uri_to_path(uri)
relative_path = Uri.decode_uri_component(Uri.relative(uri, Uri.pwd))
relative_path.count('/') > absolute_path.count('/') ? absolute_path : relative_path
end
def pretty_formatted!(formatted, uri)
index = formatted.index(uri)
return formatted unless index
replacement = pretty_uri(uri)
return formatted if uri == replacement
formatted[index, uri.length] = replacement
formatted
end
end
private_constant :Path
end
end
end
@@ -0,0 +1,376 @@
# frozen_string_literal: true
module Sass
class Compiler
class Session
# The {Protofier} class.
#
# It converts Pure Ruby types and Protobuf Ruby types.
class Protofier
def initialize(function_registry)
@function_registry = function_registry
end
def to_proto(obj)
case obj
when Sass::Value::String
EmbeddedProtocol::Value.new(
string: EmbeddedProtocol::Value::String.new(
text: obj.text.to_str,
quoted: obj.quoted?
)
)
when Sass::Value::Number
EmbeddedProtocol::Value.new(
number: Number.to_proto(obj)
)
when Sass::Value::Color
EmbeddedProtocol::Value.new(
color: EmbeddedProtocol::Value::Color.new(
channel1: obj.send(:channel0_or_nil),
channel2: obj.send(:channel1_or_nil),
channel3: obj.send(:channel2_or_nil),
alpha: obj.send(:alpha_or_nil),
space: obj.space
)
)
when Sass::Value::ArgumentList
if obj.instance_variable_get(:@compile_context) == @function_registry.compile_context
EmbeddedProtocol::Value.new(
argument_list: EmbeddedProtocol::Value::ArgumentList.new(
id: obj.instance_variable_get(:@id)
)
)
else
EmbeddedProtocol::Value.new(
argument_list: EmbeddedProtocol::Value::ArgumentList.new(
contents: obj.to_a.map { |element| to_proto(element) },
keywords: obj.keywords.each_with_object({}) { |(key, value), hash| hash[key.to_s] = to_proto(value) },
separator: ListSeparator.to_proto(obj.separator)
)
)
end
when Sass::Value::List
EmbeddedProtocol::Value.new(
list: EmbeddedProtocol::Value::List.new(
contents: obj.to_a.map { |element| to_proto(element) },
separator: ListSeparator.to_proto(obj.separator),
has_brackets: obj.bracketed?
)
)
when Sass::Value::Map
EmbeddedProtocol::Value.new(
map: EmbeddedProtocol::Value::Map.new(
entries: obj.contents.map do |key, value|
EmbeddedProtocol::Value::Map::Entry.new(
key: to_proto(key),
value: to_proto(value)
)
end
)
)
when Sass::Value::Function
if obj.instance_variable_defined?(:@id)
EmbeddedProtocol::Value.new(
compiler_function: EmbeddedProtocol::Value::CompilerFunction.new(
id: assert_compiler_value(obj).instance_variable_get(:@id)
)
)
else
EmbeddedProtocol::Value.new(
host_function: EmbeddedProtocol::Value::HostFunction.new(
id: @function_registry.register(obj.callback),
signature: obj.signature
)
)
end
when Sass::Value::Mixin
EmbeddedProtocol::Value.new(
compiler_mixin: EmbeddedProtocol::Value::CompilerMixin.new(
id: assert_compiler_value(obj).instance_variable_get(:@id)
)
)
when Sass::Value::Calculation
EmbeddedProtocol::Value.new(
calculation: Calculation.to_proto(obj)
)
when Sass::Value::Boolean
EmbeddedProtocol::Value.new(
singleton: obj.value ? :TRUE : :FALSE
)
when Sass::Value::Null
EmbeddedProtocol::Value.new(
singleton: :NULL
)
else
raise Sass::ScriptError, "Unknown Sass::Value #{obj}"
end
end
def from_proto(proto)
oneof = proto.value
obj = proto.public_send(oneof)
case oneof
when :string
Sass::Value::String.new(
obj.text,
quoted: obj.quoted
)
when :number
Number.from_proto(obj)
when :color
Sass::Value::Color.send(
:for_space,
obj.space,
obj.has_channel1? ? obj.channel1 : nil,
obj.has_channel2? ? obj.channel2 : nil,
obj.has_channel3? ? obj.channel3 : nil,
obj.has_alpha? ? obj.alpha : nil
)
when :argument_list
compiler_value(
Sass::Value::ArgumentList.new(
obj.contents.map do |element|
from_proto(element)
end,
obj.keywords.to_enum.with_object({}) do |(key, value), hash|
hash[key.to_sym] = from_proto(value)
end,
ListSeparator.from_proto(obj.separator)
),
obj.id
)
when :list
Sass::Value::List.new(
obj.contents.map do |element|
from_proto(element)
end,
separator: ListSeparator.from_proto(obj.separator),
bracketed: obj.has_brackets
)
when :map
Sass::Value::Map.new(
obj.entries.to_enum.with_object({}) do |entry, hash|
hash[from_proto(entry.key)] = from_proto(entry.value)
end
)
when :compiler_function
compiler_value(Sass::Value::Function.allocate, obj.id)
when :host_function
raise Sass::ScriptError, 'The compiler may not send Value.host_function to host'
when :compiler_mixin
compiler_value(Sass::Value::Mixin.allocate, obj.id)
when :calculation
Calculation.from_proto(obj)
when :singleton
case obj
when :TRUE
Sass::Value::Boolean::TRUE
when :FALSE
Sass::Value::Boolean::FALSE
when :NULL
Sass::Value::Null::NULL
else
raise Sass::ScriptError, "Unknown Value.singleton #{obj}"
end
else
raise Sass::ScriptError, "Unknown Value.value #{obj}"
end
end
private
def assert_compiler_value(value)
unless value.instance_variable_get(:@compile_context) == @function_registry.compile_context
raise Sass::ScriptError, "Value #{value} does not belong to this compilation"
end
value
end
def compiler_value(value, id)
value.instance_variable_set(:@compile_context, @function_registry.compile_context)
value.instance_variable_set(:@id, id)
value
end
# The {Number} Protofier.
module Number
module_function
def to_proto(obj)
EmbeddedProtocol::Value::Number.new(
value: obj.value.to_f,
numerators: obj.numerator_units,
denominators: obj.denominator_units
)
end
def from_proto(obj)
Sass::Value::Number.new(
obj.value, {
numerator_units: obj.numerators.to_a,
denominator_units: obj.denominators.to_a
}
)
end
end
private_constant :Number
# The {Calculation} Protofier.
module Calculation
module_function
def to_proto(obj)
EmbeddedProtocol::Value::Calculation.new(
name: obj.name,
arguments: obj.arguments.map { |argument| CalculationValue.to_proto(argument) }
)
end
def from_proto(obj)
Sass::Value::Calculation.send(
:new,
obj.name,
obj.arguments.map { |argument| CalculationValue.from_proto(argument) }
)
end
end
private_constant :Calculation
# The {CalculationValue} Protofier.
module CalculationValue
module_function
def to_proto(value)
case value
when Sass::Value::Number
EmbeddedProtocol::Value::Calculation::CalculationValue.new(
number: Number.to_proto(value)
)
when Sass::Value::Calculation
EmbeddedProtocol::Value::Calculation::CalculationValue.new(
calculation: Calculation.to_proto(value)
)
when Sass::Value::String
EmbeddedProtocol::Value::Calculation::CalculationValue.new(
string: value.text
)
when Sass::CalculationValue::CalculationOperation
EmbeddedProtocol::Value::Calculation::CalculationValue.new(
operation: EmbeddedProtocol::Value::Calculation::CalculationOperation.new(
operator: CalculationOperator.to_proto(value.operator),
left: to_proto(value.left),
right: to_proto(value.right)
)
)
else
raise Sass::ScriptError, "Unknown CalculationValue #{value}"
end
end
def from_proto(value)
oneof = value.value
obj = value.public_send(oneof)
case oneof
when :number
Number.from_proto(obj)
when :calculation
Calculation.from_proto(obj)
when :string
Sass::Value::String.new(obj, quoted: false)
when :operation
Sass::CalculationValue::CalculationOperation.new(
CalculationOperator.from_proto(obj.operator),
from_proto(obj.left),
from_proto(obj.right)
)
else
raise Sass::ScriptError, "Unknown CalculationValue #{value}"
end
end
end
private_constant :CalculationValue
# The {CalculationOperator} Protofier.
module CalculationOperator
module_function
def to_proto(operator)
case operator
when '+'
:PLUS
when '-'
:MINUS
when '*'
:TIMES
when '/'
:DIVIDE
else
raise Sass::ScriptError, "Unknown CalculationOperator #{separator}"
end
end
def from_proto(operator)
case operator
when :PLUS
'+'
when :MINUS
'-'
when :TIMES
'*'
when :DIVIDE
'/'
else
raise Sass::ScriptError, "Unknown CalculationOperator #{separator}"
end
end
end
private_constant :CalculationOperator
# The {ListSeparator} Protofier.
module ListSeparator
module_function
def to_proto(separator)
case separator
when ','
:COMMA
when ' '
:SPACE
when '/'
:SLASH
when nil
:UNDECIDED
else
raise Sass::ScriptError, "Unknown ListSeparator #{separator}"
end
end
def from_proto(separator)
case separator
when :COMMA
','
when :SPACE
' '
when :SLASH
'/'
when :UNDECIDED
nil
else
raise Sass::ScriptError, "Unknown ListSeparator #{separator}"
end
end
end
private_constant :ListSeparator
end
private_constant :Protofier
end
end
end
@@ -0,0 +1,46 @@
# frozen_string_literal: true
module Sass
class Compiler
class Session
# @see https://pub.dev/documentation/stack_trace/latest/stack_trace/
module StackTrace
module_function
# @see https://pub.dev/documentation/stack_trace/latest/stack_trace/Trace/toString.html
def pretty_formatted!(formatted, stack_trace)
longest = 0
frames = stack_trace.lines("\n", chomp: true).map do |frame|
location, member = frame.split(/ +/, 2)
uri, line_column = location.split(' ', 2)
uri = Path.pretty_uri(uri)
location = line_column.nil? ? uri : "#{uri} #{line_column}"
longest = location.length if location.length > longest
[frame, location, member]
end
offset = formatted.length
frames.reverse_each do |frame, location, member|
index = formatted.rindex(frame, offset)
next unless index
offset = index
replacement = "#{location.ljust(longest)} #{member}"
next if frame == replacement
formatted[index, frame.length] = replacement
end
formatted
end
end
private_constant :StackTrace
end
end
end
@@ -0,0 +1,36 @@
# frozen_string_literal: true
module Sass
class Compiler
class Session
# The {Struct} class.
#
# It creates {::Struct}-like objects from {::Hash}.
class Struct
def initialize(hash, attrs: nil, methods: nil)
@hash = hash
@attrs = attrs
@methods = methods
end
def method_missing(symbol, ...)
return super unless @hash.key?(symbol)
if @attrs&.include?(symbol)
@hash.send(:[], symbol, ...)
elsif @methods&.include?(symbol)
@hash[symbol].call(...)
else
super
end
end
def respond_to_missing?(symbol, _include_all)
@hash.key?(symbol) && (@attrs&.include?(symbol) || @methods&.include?(symbol))
end
end
private_constant :Struct
end
end
end
@@ -0,0 +1,39 @@
# frozen_string_literal: true
module Sass
class Compiler
# The {Varint} module.
#
# It reads and writes varints.
module Varint
module_function
def length(value)
return 1 if value < 128
(value.bit_length + 6) / 7
end
def read(readable)
value = bits = 0
loop do
byte = readable.readbyte
value |= (byte & 0x7f) << bits
bits += 7
break if byte < 0x80
end
value
end
def write(writeable, value)
until value < 0x80
writeable << ((value & 0x7f) | 0x80)
value >>= 7
end
writeable << value
end
end
private_constant :Varint
end
end
@@ -0,0 +1,20 @@
#!/bin/sh
# This script drives the standalone dart-sass package, which bundles together a
# Dart executable and a snapshot of dart-sass.
follow_links() {
# Use `readlink -f` if it exists, but fall back to manually following symlnks
# for systems (like older Mac OS) where it doesn't.
file="$1"
if readlink -f "$file" 2>&-; then return; fi
while [ -h "$file" ]; do
file="$(readlink "$file")"
done
echo "$file"
}
# Unlike $0, $BASH_SOURCE points to the absolute path of this file.
path=`dirname "$(follow_links "$0")"`
exec "$path/src/dart" "$path/src/sass.snapshot" "$@"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,386 @@
# frozen_string_literal: true
module Sass
# The {ELF} class.
#
# It parses ELF header to extract interpreter.
# @see https://github.com/torvalds/linux/blob/HEAD/include/uapi/linux/elf.h
# @see https://github.com/torvalds/linux/blob/HEAD/kernel/kexec_elf.c
class ELF
# The {PackInfo} class.
class PackInfo
def initialize(format:, sizeof:, struct:)
@format_le = format.freeze
@format_be = format.tr('<', '>').freeze
@sizeof = sizeof.freeze
@struct = struct.freeze
end
attr_reader :sizeof
def pack(io, data, little_endian)
raise ArgumentError if io.write(data.values_at(*@struct).pack(format(little_endian))) != @sizeof
end
def unpack(io, little_endian)
@struct.zip(io.read(@sizeof).unpack(format(little_endian))).to_h
end
private
def format(little_endian)
little_endian ? @format_le : @format_be
end
end
private_constant :PackInfo
# These constants are for the segment types stored in the image headers
PT_NULL = 0
PT_LOAD = 1
PT_DYNAMIC = 2
PT_INTERP = 3
PT_NOTE = 4
PT_SHLIB = 5
PT_PHDR = 6
PT_TLS = 7
PT_LOOS = 0x60000000
PT_HIOS = 0x6fffffff
PT_LOPROC = 0x70000000
PT_HIPROC = 0x7fffffff
PN_XNUM = 0xffff
# These constants define the different elf file types
ET_NONE = 0
ET_REL = 1
ET_EXEC = 2
ET_DYN = 3
ET_CORE = 4
ET_LOPROC = 0xff00
ET_HIPROC = 0xffff
EI_NIDENT = 16
Elf32_Ehdr = PackInfo.new(
format: "a#{EI_NIDENT}S<2L<5S<6",
sizeof: 52,
struct: %i[
e_ident
e_type
e_machine
e_version
e_entry
e_phoff
e_shoff
e_flags
e_ehsize
e_phentsize
e_phnum
e_shentsize
e_shnum
e_shstrndx
]
).freeze
Elf64_Ehdr = PackInfo.new(
format: "a#{EI_NIDENT}S<2L<Q<3L<S<6",
sizeof: 64,
struct: %i[
e_ident
e_type
e_machine
e_version
e_entry
e_phoff
e_shoff
e_flags
e_ehsize
e_phentsize
e_phnum
e_shentsize
e_shnum
e_shstrndx
]
).freeze
# These constants define the permissions on sections in the program header, p_flags.
PF_R = 0x4
PF_W = 0x2
PF_X = 0x1
Elf32_Phdr = PackInfo.new(
format: 'L<8',
sizeof: 32,
struct: %i[
p_type
p_offset
p_vaddr
p_paddr
p_filesz
p_memsz
p_flags
p_align
]
).freeze
Elf64_Phdr = PackInfo.new(
format: 'L<2Q<6',
sizeof: 56,
struct: %i[
p_type
p_flags
p_offset
p_vaddr
p_paddr
p_filesz
p_memsz
p_align
]
).freeze
# sh_type
SHT_NULL = 0
SHT_PROGBITS = 1
SHT_SYMTAB = 2
SHT_STRTAB = 3
SHT_RELA = 4
SHT_HASH = 5
SHT_DYNAMIC = 6
SHT_NOTE = 7
SHT_NOBITS = 8
SHT_REL = 9
SHT_SHLIB = 10
SHT_DYNSYM = 11
SHT_NUM = 12
SHT_LOPROC = 0x70000000
SHT_HIPROC = 0x7fffffff
SHT_LOUSER = 0x80000000
SHT_HIUSER = 0xffffffff
# sh_flags
SHF_WRITE = 0x1
SHF_ALLOC = 0x2
SHF_EXECINSTR = 0x4
SHF_RELA_LIVEPATCH = 0x00100000
SHF_RO_AFTER_INIT = 0x00200000
SHF_MASKPROC = 0xf0000000
# special section indexes
SHN_UNDEF = 0
SHN_LORESERVE = 0xff00
SHN_LOPROC = 0xff00
SHN_HIPROC = 0xff1f
SHN_LIVEPATCH = 0xff20
SHN_ABS = 0xfff1
SHN_COMMON = 0xfff2
SHN_HIRESERVE = 0xffff
Elf32_Shdr = PackInfo.new(
format: 'L<10',
sizeof: 40,
struct: %i[
sh_name
sh_type
sh_flags
sh_addr
sh_offset
sh_size
sh_link
sh_info
sh_addralign
sh_entsize
]
).freeze
Elf64_Shdr = PackInfo.new(
format: 'L<2Q<4L<2Q<2',
sizeof: 64,
struct: %i[
sh_name
sh_type
sh_flags
sh_addr
sh_offset
sh_size
sh_link
sh_info
sh_addralign
sh_entsize
]
).freeze
# e_ident[] indexes
EI_MAG0 = 0
EI_MAG1 = 1
EI_MAG2 = 2
EI_MAG3 = 3
EI_CLASS = 4
EI_DATA = 5
EI_VERSION = 6
EI_OSABI = 7
EI_PAD = 8
# EI_MAG
ELFMAG0 = 0x7f
ELFMAG1 = 0x45
ELFMAG2 = 0x4c
ELFMAG3 = 0x46
ELFMAG = [ELFMAG0, ELFMAG1, ELFMAG2, ELFMAG3].pack('C*')
SELFMAG = 4
# e_ident[EI_CLASS]
ELFCLASSNONE = 0
ELFCLASS32 = 1
ELFCLASS64 = 2
ELFCLASSNUM = 3
# e_ident[EI_DATA]
ELFDATANONE = 0
ELFDATA2LSB = 1
ELFDATA2MSB = 2
def initialize(io, program_headers: true, section_headers: false)
io.rewind
e_ident = io.read(EI_NIDENT).unpack('C*')
raise ArgumentError unless e_ident.slice(EI_MAG0, SELFMAG).pack('C*') == ELFMAG
case e_ident[EI_CLASS]
when ELFCLASS32
elf_ehdr = Elf32_Ehdr
elf_phdr = Elf32_Phdr
elf_shdr = Elf32_Shdr
when ELFCLASS64
elf_ehdr = Elf64_Ehdr
elf_phdr = Elf64_Phdr
elf_shdr = Elf64_Shdr
else
raise EncodingError
end
case e_ident[EI_DATA]
when ELFDATA2LSB
little_endian = true
when ELFDATA2MSB
little_endian = false
else
raise EncodingError
end
io.rewind
ehdr = elf_ehdr.unpack(io, little_endian)
ehdr[:e_ident] = e_ident
phdrs = if program_headers && ehdr[:e_phnum].positive?
io.seek(ehdr[:e_phoff], IO::SEEK_SET)
Array.new(ehdr[:e_phnum]) do
elf_phdr.unpack(io, little_endian)
end
else
[]
end
shdrs = if section_headers && ehdr[:e_shnum].positive?
io.seek(ehdr[:e_shoff], IO::SEEK_SET)
Array.new(ehdr[:e_shnum]) do
elf_shdr.unpack(io, little_endian)
end
else
[]
end
@io = io
@ehdr = ehdr
@phdrs = phdrs
@shdrs = shdrs
end
def dump(io)
e_ident = @ehdr[:e_ident]
raise ArgumentError unless e_ident.slice(EI_MAG0, SELFMAG).pack('C*') == ELFMAG
ehdr = @ehdr.dup
ehdr[:e_ident] = e_ident.pack('C*')
phdrs = @phdrs
shdrs = @shdrs
case e_ident[EI_CLASS]
when ELFCLASS32
elf_ehdr = Elf32_Ehdr
elf_phdr = Elf32_Phdr
elf_shdr = Elf32_Shdr
when ELFCLASS64
elf_ehdr = Elf64_Ehdr
elf_phdr = Elf64_Phdr
elf_shdr = Elf64_Shdr
else
raise EncodingError
end
case e_ident[EI_DATA]
when ELFDATA2LSB
little_endian = true
when ELFDATA2MSB
little_endian = false
else
raise EncodingError
end
io.rewind
elf_ehdr.pack(io, ehdr, little_endian)
io.seek(ehdr[:e_phoff], IO::SEEK_SET) if ehdr[:e_phnum].positive?
phdrs.each do |phdr|
elf_phdr.pack(io, phdr, little_endian)
end
io.seek(ehdr[:e_shoff], IO::SEEK_SET) if ehdr[:e_shnum].positive?
shdrs.each do |shdr|
elf_shdr.pack(io, shdr, little_endian)
end
io.flush
end
def relocatable?
@ehdr[:e_type] == ET_REL
end
def executable?
@ehdr[:e_type] == ET_EXEC
end
def shared_object?
@ehdr[:e_type] == ET_DYN
end
def core?
@ehdr[:e_type] == ET_CORE
end
def interpreter
phdr = @phdrs.find { |p| p[:p_type] == PT_INTERP }
return if phdr.nil?
@io.seek(phdr[:p_offset], IO::SEEK_SET)
@io.read(phdr[:p_filesz]).unpack1('Z*')
end
INTERPRETER = begin
proc_self_exe = '/proc/self/exe'
if File.exist?(proc_self_exe)
File.open(proc_self_exe, 'rb') do |file|
elf = ELF.new(file)
interpreter = elf.interpreter
if interpreter.nil? && elf.shared_object?
File.readlink(proc_self_exe)
else
interpreter
end
end
end
end.freeze
end
private_constant :ELF
end
@@ -0,0 +1,67 @@
# frozen_string_literal: true
require_relative 'compiler'
# The Sass module.
#
# This communicates with Embedded Dart Sass using the Embedded Sass protocol.
#
# @example
# Sass.compile('style.scss')
#
# @example
# Sass.compile_string('h1 { font-size: 40px; }')
module Sass
@compiler = nil
@mutex = Mutex.new
# rubocop:disable-next Layout/LineLength
class << self
# Compiles the Sass file at +path+ to CSS.
# @overload compile(path, load_paths: [], charset: true, source_map: false, source_map_include_sources: false, style: :expanded, functions: {}, importers: [], alert_ascii: false, alert_color: nil, fatal_deprecations: [], future_deprecations: [], logger: nil, quiet_deps: false, silence_deprecations: [], verbose: false)
# @param (see Compiler#compile)
# @return (see Compiler#compile)
# @raise (see Compiler#compile)
# @see Compiler#compile
def compile(...)
compiler.compile(...)
end
# Compiles a stylesheet whose contents is +source+ to CSS.
# @overload compile_string(source, importer: nil, load_paths: [], syntax: :scss, url: nil, charset: true, source_map: false, source_map_include_sources: false, style: :expanded, functions: {}, importers: [], alert_ascii: false, alert_color: nil, fatal_deprecations: [], future_deprecations: [], logger: nil, quiet_deps: false, silence_deprecations: [], verbose: false)
# @param (see Compiler#compile_string)
# @return (see Compiler#compile_string)
# @raise (see Compiler#compile_string)
# @see Compiler#compile_string
def compile_string(...)
compiler.compile_string(...)
end
# @param (see Compiler#info)
# @return (see Compiler#info)
# @raise (see Compiler#info)
# @see Compiler#info
def info
compiler.info
end
private
def compiler
return @compiler if @compiler
@mutex.synchronize do
return @compiler if @compiler
compiler = Compiler.allocate
compiler.instance_variable_set(:@channel, Compiler.const_get(:Channel).new(idle_timeout: 10))
at_exit do
compiler.close
end
@compiler = compiler
end
end
end
end
@@ -0,0 +1,7 @@
# frozen_string_literal: true
module Sass
module Embedded
VERSION = '1.104.1'
end
end
@@ -0,0 +1,10 @@
# frozen_string_literal: true
module Sass
# @see https://github.com/sass/sass/blob/HEAD/spec/embedded-protocol.md
module EmbeddedProtocol
require 'sass/embedded_sass_pb'
end
private_constant :EmbeddedProtocol
end
File diff suppressed because one or more lines are too long
@@ -0,0 +1,73 @@
# frozen_string_literal: true
module Sass
# An exception thrown because a Sass compilation failed.
class CompileError < StandardError
# @return [String, nil]
attr_reader :sass_stack
# @return [Logger::SourceSpan, nil]
attr_reader :span
# @return [Array<String>]
attr_reader :loaded_urls
# @!visibility private
def initialize(message, detailed_message, sass_stack, span, loaded_urls)
super(message)
@detailed_message = detailed_message
@sass_stack = sass_stack
@span = span
@loaded_urls = loaded_urls
end
# @!visibility private
def detailed_message(highlight: nil, **)
return super if @detailed_message.nil?
highlight = Exception.to_tty? if highlight.nil?
detailed_message = @detailed_message.sub(message, super)
detailed_message.gsub!(/\e\[[0-9;]*m/, '') unless highlight
detailed_message
end
# @return [String]
def to_css
content = full_message(highlight: false, order: :top)
<<~CSS.freeze
/* #{content.gsub('*/', "*\u2060/").gsub("\r\n", "\n").split("\n").join("\n * ")} */
body::before {
position: static;
display: block;
padding: 1em;
margin: 0 0 1em;
border-width: 0 0 2px;
border-bottom-style: solid;
font-family: monospace, monospace;
white-space: pre;
content: #{Serializer.serialize_quoted_string(content).gsub(/[^[:ascii:]][\h\t ]?/) do |match|
ordinal = match.ord
replacement = "\\#{ordinal.to_s(16)}"
if match.length > 1
replacement << ' ' if ordinal < 0x100000
replacement << match[1]
end
replacement
end};
}
CSS
end
end
# An exception thrown by Sass Script.
class ScriptError < StandardError
# @!visibility private
def initialize(message, name = nil)
super(name.nil? ? message : "$#{name}: #{message}")
end
end
end
@@ -0,0 +1,56 @@
# frozen_string_literal: true
module Sass
# The {ForkTracker} module.
#
# It tracks objects that need to be closed after `Process.fork`.
module ForkTracker
module_function
if Process.respond_to?(:_fork)
SET = Set.new.compare_by_identity
MUTEX = Mutex.new
private_constant :SET, :MUTEX
def add(object)
MUTEX.synchronize do
SET.add(object)
end
end
def delete(object)
MUTEX.synchronize do
SET.delete(object)
end
end
def each(&)
MUTEX.synchronize do
SET.to_a
end.each(&)
end
# The {CoreExt} module.
#
# It closes objects after `Process.fork`.
module CoreExt
def _fork
pid = super
ForkTracker.each(&:close) if pid.zero?
pid
end
end
private_constant :CoreExt
Process.singleton_class.prepend(CoreExt)
else
def add(object); end
def delete(object); end
end
end
private_constant :ForkTracker
end
@@ -0,0 +1,28 @@
# frozen_string_literal: true
module Sass
# The built-in RubyGems package importer. This loads pkg: URLs from gems.
#
# @example
# require 'bundler/inline'
#
# gemfile do
# source 'https://rubygems.org'
# gem 'bootstrap', require: false
# gem 'sass-embedded'
# end
#
# puts Sass.compile_string('@use "pkg:bootstrap/assets/stylesheets/bootstrap";', importers: [Sass::GemPackageImporter.new]).css
class GemPackageImporter
# @!visibility private
def find_file_url(url, _canonicalize_context)
return unless url.start_with?('pkg:')
library, _, path = url[4..].partition('/')
gem_dir = Gem::Dependency.new(Uri.decode_uri_component(library)).to_spec.gem_dir
"#{Uri.path_to_file_uri(gem_dir)}/#{path}"
rescue Gem::MissingSpecError
nil
end
end
end
@@ -0,0 +1,28 @@
# frozen_string_literal: true
module Sass
# A namespace for built-in Loggers.
#
# @see https://sass-lang.com/documentation/js-api/modules/logger/
module Logger
module_function
# A Logger that silently ignores all warnings and debug messages.
#
# @see https://sass-lang.com/documentation/js-api/variables/logger.silent/
def silent
Silent
end
# A Logger that silently ignores all warnings and debug messages.
module Silent
module_function
def warn(message, options); end
def debug(message, options); end
end
private_constant :Silent
end
end
@@ -0,0 +1,22 @@
# frozen_string_literal: true
module Sass
module Logger
# A specific location within a source file.
#
# This is always associated with a {SourceSpan} which indicates which file it refers to.
#
# @see https://sass-lang.com/documentation/js-api/interfaces/sourcelocation/
class SourceLocation
# @return [Integer]
attr_reader :offset, :line, :column
# @!visibility private
def initialize(source_location)
@offset = source_location.offset
@line = source_location.line
@column = source_location.column
end
end
end
end
@@ -0,0 +1,28 @@
# frozen_string_literal: true
module Sass
module Logger
# A span of text within a source file.
#
# @see https://sass-lang.com/documentation/js-api/interfaces/sourcespan/
class SourceSpan
# @return [SourceLocation]
attr_reader :start, :end
# @return [String]
attr_reader :text
# @return [String, nil]
attr_reader :url, :context
# @!visibility private
def initialize(source_span)
@start = source_span.start.nil? ? nil : Logger::SourceLocation.new(source_span.start)
@end = source_span.end.nil? ? nil : Logger::SourceLocation.new(source_span.end)
@text = source_span.text
@url = source_span.url == '' ? nil : source_span.url
@context = source_span.context == '' ? nil : source_span.context
end
end
end
end
@@ -0,0 +1,17 @@
# frozen_string_literal: true
module Sass
# The built-in Node.js package importer. This loads pkg: URLs from node_modules
# according to the standard Node.js resolution algorithm.
#
# @see https://sass-lang.com/documentation/js-api/classes/nodepackageimporter/
class NodePackageImporter
# @param entry_point_directory [String] The directory where the {NodePackageImporter} should start when resolving
# `pkg:` URLs in sources other than files on disk.
def initialize(entry_point_directory)
raise ArgumentError, 'entry_point_directory must be set' if entry_point_directory.nil?
@entry_point_directory = File.absolute_path(entry_point_directory)
end
end
end
@@ -0,0 +1,30 @@
# frozen_string_literal: true
module Sass
# The {Serializer} module.
module Serializer
module_function
CSS_ESCAPE = [*"\x01".."\x08", *"\x0A".."\x1F", "\x7F"]
.product([*'0'..'9', *'a'..'f', *'A'..'F', "\t", ' ', nil])
.each_with_object({ "\0" => "\uFFFD", '\\' => '\\\\', '"' => '\\"', "'" => "\\'" }) do |(c, x), h|
h["#{c}#{x}".freeze] = "\\#{c.ord.to_s(16)}#{" #{x}" if x}".freeze
end.freeze
private_constant :CSS_ESCAPE
def serialize_quoted_string(string)
if !string.include?('"') || string.include?("'")
%("#{string.gsub(/[\0\\"]|[\x01-\x08\x0A-\x1F\x7F][\h\t ]?/, CSS_ESCAPE)}")
else
%('#{string.gsub(/[\0\\']|[\x01-\x08\x0A-\x1F\x7F][\h\t ]?/, CSS_ESCAPE)}')
end
end
def serialize_unquoted_string(string)
string.tr("\0", "\uFFFD").gsub(/\n */, ' ')
end
end
private_constant :Serializer
end
@@ -0,0 +1,56 @@
# frozen_string_literal: true
require 'uri'
module Sass
# The {Uri} class.
#
# It follows RFC3986 to match the behavior of Uri class from Dart.
#
# @see https://www.rfc-editor.org/info/rfc3986/
module Uri
module_function
def decode_uri_component(str)
str.b.gsub(/%\h\h/, ::URI::TBLDECWWWCOMP_).force_encoding(str.encoding)
end
def encode_uri_component(str)
str.b.gsub(/[^0-9A-Za-z\-._~]/n, ::URI::TBLENCURICOMP_).force_encoding(str.encoding)
end
def encode_uri_path_component(str)
str.b.gsub(%r{[^0-9A-Za-z\-._~!$&'()*+,;=:@/]}n, ::URI::TBLENCURICOMP_).force_encoding(str.encoding)
end
def encode_uri_query_component(str)
str.b.gsub(%r{[^0-9A-Za-z\-._~!$&'()*+,;=:@/?]}n, ::URI::TBLENCURICOMP_).force_encoding(str.encoding)
end
def file_uri_to_path(uri)
path = decode_uri_component(::URI::RFC3986_PARSER.parse(uri).path)
if path.start_with?('/')
windows_path = path[1..]
path = windows_path if File.absolute_path?(windows_path)
end
path
end
def path_to_file_uri(path)
path = "/#{path}" unless path.start_with?('/')
"file://#{encode_uri_path_component(path)}"
end
def pwd
pwd = Dir.pwd
pwd += '/' unless pwd.end_with?('/')
path_to_file_uri(pwd)
end
def relative(to, from)
::URI::RFC3986_PARSER.parse(to).route_from(from).to_s
end
end
private_constant :Uri
end
@@ -0,0 +1,136 @@
# frozen_string_literal: true
module Sass
# The abstract base class of Sass's value types.
#
# @see https://sass-lang.com/documentation/js-api/classes/value/
module Value
# @return [::String, nil]
def separator
nil
end
# @return [::Boolean]
def bracketed?
false
end
# @return [::Boolean]
def eql?(other)
self == other
end
# @param index [Numeric]
# @return [Value]
def [](index)
at(index)
end
# @param index [Numeric]
# @return [Value]
def at(index)
index < 1 && index >= -1 ? self : nil
end
# @return [Array<Value>]
def to_a
[self]
end
# @return [::Boolean]
def to_bool # rubocop:disable Naming/PredicateMethod
true
end
# @return [Map, nil]
def to_map
nil
end
# @return [Value, nil]
def to_nil
self
end
# @return [Boolean]
# @raise [ScriptError]
def assert_boolean(name = nil)
raise Sass::ScriptError.new("#{self} is not a boolean", name)
end
# @return [Calculation]
# @raise [ScriptError]
def assert_calculation(name = nil)
raise Sass::ScriptError.new("#{self} is not a calculation", name)
end
# @return [Color]
# @raise [ScriptError]
def assert_color(name = nil)
raise Sass::ScriptError.new("#{self} is not a color", name)
end
# @return [Function]
# @raise [ScriptError]
def assert_function(name = nil)
raise Sass::ScriptError.new("#{self} is not a function", name)
end
# @return [Map]
# @raise [ScriptError]
def assert_map(name = nil)
raise Sass::ScriptError.new("#{self} is not a map", name)
end
# @return [Mixin]
# @raise [ScriptError]
def assert_mixin(name = nil)
raise Sass::ScriptError.new("#{self} is not a mixin", name)
end
# @return [Number]
# @raise [ScriptError]
def assert_number(name = nil)
raise Sass::ScriptError.new("#{self} is not a number", name)
end
# @return [String]
# @raise [ScriptError]
def assert_string(name = nil)
raise Sass::ScriptError.new("#{self} is not a string", name)
end
# @param sass_index [Number]
# @return [Integer]
def sass_index_to_array_index(sass_index, name = nil)
index = sass_index.assert_number(name).assert_integer(name)
raise Sass::ScriptError.new('List index may not be 0', name) if index.zero?
if index.abs > to_a_length
raise Sass::ScriptError.new("Invalid index #{sass_index} for a list with #{to_a_length} elements", name)
end
index.negative? ? to_a_length + index : index - 1
end
private
def to_a_length
1
end
end
end
require_relative 'calculation_value'
require_relative 'value/list'
require_relative 'value/argument_list'
require_relative 'value/boolean'
require_relative 'value/calculation'
require_relative 'value/color'
require_relative 'value/function'
require_relative 'value/fuzzy_math'
require_relative 'value/map'
require_relative 'value/mixin'
require_relative 'value/null'
require_relative 'value/number'
require_relative 'value/string'
@@ -0,0 +1,29 @@
# frozen_string_literal: true
module Sass
module Value
# Sass's argument list type.
#
# An argument list comes from a rest argument. It's distinct from a normal {List} in that it may contain a keyword
# map as well as the positional arguments.
#
# @see https://sass-lang.com/documentation/js-api/classes/sassargumentlist/
class ArgumentList < List
# @param contents [Array<Value>]
# @param keywords [Hash<Symbol, Value>]
# @param separator [::String]
def initialize(contents = [], keywords = {}, separator = ',')
super(contents, separator:)
@keywords_accessed = false
@keywords = keywords.freeze
end
# @return [Hash<Symbol, Value>]
def keywords
@keywords_accessed = true
@keywords
end
end
end
end
@@ -0,0 +1,52 @@
# frozen_string_literal: true
module Sass
module Value
# Sass's boolean type.
#
# @see https://sass-lang.com/documentation/js-api/classes/sassboolean/
class Boolean
include Value
# @param value [::Boolean]
def initialize(value)
@value = value
end
# @return [::Boolean]
attr_reader :value
# @return [Boolean]
def !
value ? Boolean::FALSE : Boolean::TRUE
end
# @return [::Boolean]
def ==(other)
other.is_a?(Sass::Value::Boolean) && other.value == value
end
# @return [Integer]
def hash
@hash ||= value.hash
end
alias to_bool value
# @return [Boolean]
def assert_boolean(_name = nil)
self
end
# Sass's true value.
TRUE = Boolean.new(true)
# Sass's false value.
FALSE = Boolean.new(false)
def self.new(value)
value ? Boolean::TRUE : Boolean::FALSE
end
end
end
end
@@ -0,0 +1,90 @@
# frozen_string_literal: true
module Sass
module Value
# Sass's calculation type.
#
# @see https://sass-lang.com/documentation/js-api/classes/sasscalculation/
class Calculation
include Value
include CalculationValue
class << self
private :new
# @param argument [CalculationValue]
# @return [Calculation]
def calc(argument)
new('calc', [argument])
end
# @param arguments [Array<CalculationValue>]
# @return [Calculation]
def min(arguments)
new('min', arguments)
end
# @param arguments [Array<CalculationValue>]
# @return [Calculation]
def max(arguments)
new('max', arguments)
end
# @param min [CalculationValue]
# @param value [CalculationValue]
# @param max [CalculationValue]
# @return [Calculation]
def clamp(min, value = nil, max = nil)
if (value.nil? && !valid_clamp_arg?(min)) ||
(max.nil? && [min, value].none? { |x| x && valid_clamp_arg?(x) })
raise Sass::ScriptError, 'Argument must be an unquoted SassString.'
end
new('clamp', [min, value, max].compact)
end
private
def valid_clamp_arg?(value)
value.is_a?(Sass::Value::String) && !value.quoted?
end
end
private
def initialize(name, arguments)
arguments.each do |value|
assert_calculation_value(value)
end
@name = name.freeze
@arguments = arguments.freeze
end
public
# @return [::String]
attr_reader :name
# @return [Array<CalculationValue>]
attr_reader :arguments
# @return [Calculation]
def assert_calculation(_name = nil)
self
end
# @return [::Boolean]
def ==(other)
other.is_a?(Sass::Value::Calculation) &&
other.name == name &&
other.arguments == arguments
end
# @return [Integer]
def hash
@hash ||= [name, *arguments].hash
end
end
end
end
@@ -0,0 +1,631 @@
# frozen_string_literal: true
require_relative 'color/channel'
require_relative 'color/conversions'
require_relative 'color/gamut_map_method'
require_relative 'color/interpolation_method'
require_relative 'color/space'
module Sass
module Value
# Sass's color type.
#
# No matter what representation was originally used to create this color, all of its channels are accessible.
#
# @see https://sass-lang.com/documentation/js-api/classes/sasscolor/
class Color
include Value
# @param red [Numeric]
# @param green [Numeric]
# @param blue [Numeric]
# @param hue [Numeric]
# @param saturation [Numeric]
# @param lightness [Numeric]
# @param whiteness [Numeric]
# @param blackness [Numeric]
# @param a [Numeric]
# @param b [Numeric]
# @param chroma [Numeric]
# @param x [Numeric]
# @param y [Numeric]
# @param z [Numeric]
# @param alpha [Numeric]
# @param space [::String]
# @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'rgb')
# @overload initialize(hue: nil, saturation: nil, lightness: nil, alpha: nil, space: 'hsl')
# @overload initialize(hue: nil, whiteness: nil, blackness: nil, alpha: nil, space: 'hwb')
# @overload initialize(lightness: nil, a: nil, b: nil, alpha: nil, space: 'lab')
# @overload initialize(lightness: nil, a: nil, b: nil, alpha: nil, space: 'oklab')
# @overload initialize(lightness: nil, chroma: nil, hue: nil, alpha: nil, space: 'lch')
# @overload initialize(lightness: nil, chroma: nil, hue: nil, alpha: nil, space: 'oklch')
# @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'a98-rgb')
# @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'display-p3')
# @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'display-p3-linear')
# @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'prophoto-rgb')
# @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'rec2020')
# @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'srgb')
# @overload initialize(red: nil, green: nil, blue: nil, alpha: nil, space: 'srgb-linear')
# @overload initialize(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz')
# @overload initialize(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz-d50')
# @overload initialize(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz-d65')
def initialize(**options)
unless options.key?(:space)
options[:space] = case options
in { red: _, green: _, blue: _ }
'rgb'
in { hue: _, saturation: _, lightness: _ }
'hsl'
in { hue: _, whiteness: _, blackness: _ }
'hwb'
else
raise Sass::ScriptError.new('No color space found', 'space')
end
end
space = Space.from_name(options[:space])
keys = _assert_options(space, options)
_initialize_for_space_internal(space,
options[keys[0]],
options[keys[1]],
options[keys[2]],
options.fetch(:alpha, 1))
end
# @return [::String]
def space
_space.name
end
# @param space [::String]
# @return [Color]
def to_space(space)
_to_space(Space.from_name(space))
end
# @param space [::String]
# @return [::Boolean]
def in_gamut?(space = nil)
return to_space(space)._in_gamut? unless space.nil?
_in_gamut?
end
# @param method [::String]
# @param space [::String]
# @return [Color]
def to_gamut(method:, space: nil)
return to_space(space).to_gamut(method:)._to_space(_space) unless space.nil?
_to_gamut(GamutMapMethod.from_name(method, 'method'))
end
# @return [Array<Numeric, nil>]
def channels_or_nil
[channel0_or_nil, channel1_or_nil, channel2_or_nil].freeze
end
# @return [Array<Numeric>]
def channels
[channel0, channel1, channel2].freeze
end
# @param channel [::String]
# @param space [::String]
# @return [Numeric]
def channel(channel, space: nil)
return to_space(space).channel(channel) unless space.nil?
channels = _space.channels
return channel0 if channel == channels[0].name
return channel1 if channel == channels[1].name
return channel2 if channel == channels[2].name
return alpha if channel == 'alpha'
raise Sass::ScriptError.new("Color #{self} doesn't have a channel named \"#{channel}\".", channel)
end
# @param channel [::String]
# @return [::Boolean]
def channel_missing?(channel)
channels = _space.channels
return channel0_missing? if channel == channels[0].name
return channel1_missing? if channel == channels[1].name
return channel2_missing? if channel == channels[2].name
return alpha_missing? if channel == 'alpha'
raise Sass::ScriptError.new("Color #{self} doesn't have a channel named \"#{channel}\".", channel)
end
# @param channel [::String]
# @param space [::String]
# @return [::Boolean]
def channel_powerless?(channel, space: nil)
return to_space(space).channel_powerless?(channel) unless space.nil?
channels = _space.channels
return channel0_powerless? if channel == channels[0].name
return channel1_powerless? if channel == channels[1].name
return channel2_powerless? if channel == channels[2].name
return false if channel == 'alpha'
raise Sass::ScriptError.new("Color #{self} doesn't have a channel named \"#{channel}\".", channel)
end
# @param other [Color]
# @param method [::String]
# @param weight [Numeric]
# @return [Color]
def interpolate(other, method: nil, weight: nil)
interpolation_method = if !method.nil?
InterpolationMethod.new(_space, HueInterpolationMethod.from_name(method))
elsif !_space.polar?
InterpolationMethod.new(_space)
else
InterpolationMethod.new(_space, :shorter)
end
_interpolate(other, interpolation_method, weight:)
end
# @param red [Numeric]
# @param green [Numeric]
# @param blue [Numeric]
# @param hue [Numeric]
# @param saturation [Numeric]
# @param lightness [Numeric]
# @param whiteness [Numeric]
# @param blackness [Numeric]
# @param a [Numeric]
# @param b [Numeric]
# @param chroma [Numeric]
# @param x [Numeric]
# @param y [Numeric]
# @param z [Numeric]
# @param alpha [Numeric]
# @param space [::String]
# @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'rgb')
# @overload change(hue: nil, saturation: nil, lightness: nil, alpha: nil, space: 'hsl')
# @overload change(hue: nil, whiteness: nil, blackness: nil, alpha: nil, space: 'hwb')
# @overload change(lightness: nil, a: nil, b: nil, alpha: nil, space: 'lab')
# @overload change(lightness: nil, a: nil, b: nil, alpha: nil, space: 'oklab')
# @overload change(lightness: nil, chroma: nil, hue: nil, alpha: nil, space: 'lch')
# @overload change(lightness: nil, chroma: nil, hue: nil, alpha: nil, space: 'oklch')
# @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'a98-rgb')
# @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'display-p3')
# @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'display-p3-linear')
# @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'prophoto-rgb')
# @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'rec2020')
# @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'srgb')
# @overload change(red: nil, green: nil, blue: nil, alpha: nil, space: 'srgb-linear')
# @overload change(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz')
# @overload change(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz-d50')
# @overload change(x: nil, y: nil, z: nil, alpha: nil, space: 'xyz-d65')
# @return [Color]
def change(**options)
space_set_explictly = !options[:space].nil?
space = space_set_explictly ? Space.from_name(options[:space]) : _space
if legacy? && !space_set_explictly
case options
in { whiteness: _ } | { blackness: _ }
space = Space::HWB
in { saturation: _ } | { lightness: _ }
space = Space::HSL
in { hue: _ }
space = if _space == Space::HWB
Space::HWB
else
Space::HSL
end
in { red: _ } | { blue: _ } | { green: _ }
space = Space::RGB
else
end
if space != _space
# deprecated
end
end
keys = _assert_options(space, options)
color = _to_space(space)
changed_color = if space_set_explictly
Color.send(:for_space_internal,
space,
options.fetch(keys[0], color.channel0_or_nil),
options.fetch(keys[1], color.channel1_or_nil),
options.fetch(keys[2], color.channel2_or_nil),
options.fetch(:alpha, color.alpha_or_nil))
else
changed_channel0_or_nil = options[keys[0]]
changed_channel1_or_nil = options[keys[1]]
changed_channel2_or_nil = options[keys[2]]
changed_alpha_or_nil = options[:alpha]
Color.send(:for_space_internal,
space,
changed_channel0_or_nil.nil? ? color.channel0_or_nil : changed_channel0_or_nil,
changed_channel1_or_nil.nil? ? color.channel1_or_nil : changed_channel1_or_nil,
changed_channel2_or_nil.nil? ? color.channel2_or_nil : changed_channel2_or_nil,
changed_alpha_or_nil.nil? ? color.alpha_or_nil : changed_alpha_or_nil)
end
changed_color._to_space(_space)
end
# @return [Numeric]
def alpha
@alpha_or_nil.nil? ? 0 : @alpha_or_nil
end
# @return [::Boolean]
def legacy?
_space.legacy?
end
# @deprecated
# @return [Numeric]
def red
_to_space(Space::RGB).channel('red').round
end
# @deprecated
# @return [Numeric]
def green
_to_space(Space::RGB).channel('green').round
end
# @deprecated
# @return [Numeric]
def blue
_to_space(Space::RGB).channel('blue').round
end
# @deprecated
# @return [Numeric]
def hue
_to_space(Space::HSL).channel('hue')
end
# @deprecated
# @return [Numeric]
def saturation
_to_space(Space::HSL).channel('saturation')
end
# @deprecated
# @return [Numeric]
def lightness
_to_space(Space::HSL).channel('lightness')
end
# @deprecated
# @return [Numeric]
def whiteness
_to_space(Space::HWB).channel('whiteness')
end
# @deprecated
# @return [Numeric]
def blackness
_to_space(Space::HWB).channel('blackness')
end
# @return [::Boolean]
def ==(other)
return false unless other.is_a?(Sass::Value::Color)
if legacy?
return false unless other.legacy?
return false unless FuzzyMath.equals_nilable?(other.alpha_or_nil, alpha_or_nil)
if _space == other._space
FuzzyMath.equals_nilable?(other.channel0_or_nil, channel0_or_nil) &&
FuzzyMath.equals_nilable?(other.channel1_or_nil, channel1_or_nil) &&
FuzzyMath.equals_nilable?(other.channel2_or_nil, channel2_or_nil)
else
_to_space(Space::RGB) == other._to_space(Space::RGB)
end
else
other._space == _space &&
FuzzyMath.equals_nilable?(other.channel0_or_nil, channel0_or_nil) &&
FuzzyMath.equals_nilable?(other.channel1_or_nil, channel1_or_nil) &&
FuzzyMath.equals_nilable?(other.channel2_or_nil, channel2_or_nil) &&
FuzzyMath.equals_nilable?(other.alpha_or_nil, alpha_or_nil)
end
end
# @return [Integer]
def hash
@hash ||= [
_space.name,
FuzzyMath._hash(channel0_or_nil),
FuzzyMath._hash(channel1_or_nil),
FuzzyMath._hash(channel2_or_nil),
FuzzyMath._hash(alpha_or_nil)
].hash
end
# @return [Color]
def assert_color(_name = nil)
self
end
protected
attr_reader :channel0_or_nil, :channel1_or_nil, :channel2_or_nil, :alpha_or_nil
def channel0
@channel0_or_nil.nil? ? 0 : @channel0_or_nil
end
def channel0_missing?
@channel0_or_nil.nil?
end
def channel0_powerless?
case _space
when Space::HSL
FuzzyMath.equals?(channel1, 0)
when Space::HWB
FuzzyMath.greater_than_or_equals?(channel1 + channel2, 100)
else
false
end
end
def channel1
@channel1_or_nil.nil? ? 0 : @channel1_or_nil
end
def channel1_missing?
@channel1_or_nil.nil?
end
def channel1_powerless?
false
end
def channel2
@channel2_or_nil.nil? ? 0 : @channel2_or_nil
end
def channel2_missing?
@channel2_or_nil.nil?
end
def channel2_powerless?
case _space
when Space::LCH, Space::OKLCH
FuzzyMath.equals?(channel1, 0)
else
false
end
end
def alpha_missing?
@alpha_or_nil.nil?
end
def _space
@space
end
def _to_space(space)
return self if _space == space
_space.convert(space, channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
end
def _in_gamut?
return true unless _space.bounded?
_is_channel_in_gamut?(channel0, _space.channels[0]) &&
_is_channel_in_gamut?(channel1, _space.channels[1]) &&
_is_channel_in_gamut?(channel2, _space.channels[2])
end
def _to_gamut(method)
_in_gamut? ? self : method.map(self)
end
private
def _assert_options(space, options)
keys = space.channels.map do |channel|
channel.name.to_sym
end << :alpha << :space
options.each_key do |key|
unless keys.include?(key)
raise Sass::ScriptError.new("`#{key}` is not a valid channel in `#{space.name}`.", key)
end
end
keys
end
def _initialize_for_space_internal(space, channel0, channel1, channel2, alpha = 1)
case space
when Space::HSL
_initialize_for_space(
space,
_normalize_hue(channel0, invert: !channel1.nil? && FuzzyMath.less_than?(channel1, 0)),
_normalize_linear(channel1&.abs),
_normalize_linear(channel2),
_normalize_linear(alpha)
)
when Space::HWB
_initialize_for_space(
space,
_normalize_hue(channel0, invert: false),
_normalize_linear(channel1),
_normalize_linear(channel2),
_normalize_linear(alpha)
)
when Space::LCH, Space::OKLCH
_initialize_for_space(
space,
_normalize_linear(channel0),
_normalize_linear(channel1&.abs),
_normalize_hue(channel2, invert: !channel1.nil? && FuzzyMath.less_than?(channel1, 0)),
_normalize_linear(alpha)
)
else
_initialize_for_space(
space,
_normalize_linear(channel0),
_normalize_linear(channel1),
_normalize_linear(channel2),
_normalize_linear(alpha)
)
end
end
def _initialize_for_space(space, channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
@space = space
@channel0_or_nil = channel0_or_nil
@channel1_or_nil = channel1_or_nil
@channel2_or_nil = channel2_or_nil
@alpha_or_nil = alpha
FuzzyMath.assert_between(@alpha_or_nil, 0, 1, 'alpha') unless @alpha_or_nil.nil?
end
def _normalize_linear(value)
return value if value.nil?
return 0 if value.zero? || value.to_f.nan?
value
end
def _normalize_hue(hue, invert:)
return hue if hue.nil?
return 0 if hue.zero? || !hue.finite?
((hue % 360) + 360 + (invert ? 180 : 0)) % 360
end
def _is_channel_in_gamut?(value, channel)
case channel
when LinearChannel
FuzzyMath.less_than_or_equals?(value, channel.max) && FuzzyMath.greater_than_or_equals?(value, channel.min)
else
true
end
end
def _interpolate(other, method, weight: nil)
weight = 0.5 if weight.nil?
if weight.negative? || weight > 1
raise Sass::ScriptError.new("Expected #{wieght} to be within 0 and 1.", 'weight')
end
return other if FuzzyMath.equals?(weight, 0)
return self if FuzzyMath.equals?(weight, 1)
color1 = _to_space(method.space)
color2 = other._to_space(method.space)
c1_channel0 = color1.channel0_or_nil.nil? ? color2.channel0_or_nil : color1.channel0_or_nil
c1_channel1 = color1.channel1_or_nil.nil? ? color2.channel1_or_nil : color1.channel1_or_nil
c1_channel2 = color1.channel2_or_nil.nil? ? color2.channel2_or_nil : color1.channel2_or_nil
c2_channel0 = color2.channel0_or_nil.nil? ? color1.channel0_or_nil : color2.channel0_or_nil
c2_channel1 = color2.channel1_or_nil.nil? ? color1.channel1_or_nil : color2.channel1_or_nil
c2_channel2 = color2.channel2_or_nil.nil? ? color1.channel2_or_nil : color2.channel2_or_nil
c1_alpha = alpha_or_nil.nil? ? other.alpha : alpha_or_nil
c2_alpha = other.alpha_or_nil.nil? ? alpha : other.alpha_or_nil
c1_multiplier = (alpha_or_nil.nil? ? 1 : alpha_or_nil) * weight
c2_multiplier = (other.alpha_or_nil.nil? ? 1 : other.alpha_or_nil) * (1 - weight)
mixed_alpha = alpha_missing? && other.alpha_missing? ? nil : (c1_alpha * weight) + (c2_alpha * (1 - weight))
mixed0 = if c1_channel0.nil?
nil
else
((c1_channel0 * c1_multiplier) + (c2_channel0 * c2_multiplier)) /
(mixed_alpha.nil? ? 1 : mixed_alpha)
end
mixed1 = if c1_channel1.nil?
nil
else
((c1_channel1 * c1_multiplier) + (c2_channel1 * c2_multiplier)) /
(mixed_alpha.nil? ? 1 : mixed_alpha)
end
mixed2 = if c1_channel2.nil?
nil
else
((c1_channel2 * c1_multiplier) + (c2_channel2 * c2_multiplier)) /
(mixed_alpha.nil? ? 1 : mixed_alpha)
end
case method.space
when Space::HSL, Space::HWB
Color.send(:for_space_internal,
method.space,
c1_channel0.nil? ? nil : _interpolate_hues(c1_channel0, c2_channel0, method.hue, weight),
mixed1,
mixed2,
mixed_alpha)
when Space::LCH, Space::OKLCH
Color.send(:for_space_internal,
method.space,
mixed0,
mixed1,
c1_channel2.nil? ? nil : _interpolate_hues(c1_channel2, c2_channel2, method.hue, weight),
mixed_alpha)
else
Color.send(:_for_space,
method.space, mixed0, mixed1, mixed2, mixed_alpha)
end._to_space(_space)
end
def _interpolate_hues(hue1, hue2, method, weight)
case method
when :shorter
diff = hue2 - hue1
if diff > 180
hue1 += 360
elsif diff < -180
hue2 += 360
end
when :longer
diff = hue2 - hue1
if diff.positive? && diff < 180
hue2 += 360
elsif diff > -180 && diff <= 0
hue1 += 360
end
when :increasing
hue2 += 360 if hue2 < hue1
when :decreasing
hue1 += 360 if hue1 < hue2
end
(hue1 * weight) + (hue2 * (1 - weight))
end
class << self
private
def for_space(space, channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
_for_space(Space.from_name(space), channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
end
def for_space_internal(space, channel0, channel1, channel2, alpha)
o = allocate
o.send(:_initialize_for_space_internal, space, channel0, channel1, channel2, alpha)
o
end
def _for_space(space, channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
o = allocate
o.send(:_initialize_for_space, space, channel0_or_nil, channel1_or_nil, channel2_or_nil, alpha)
o
end
end
end
end
end
@@ -0,0 +1,79 @@
# frozen_string_literal: true
module Sass
module Value
class Color
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/channel.dart
class ColorChannel
# @return [::String]
attr_reader :name
# @return [::Boolean]
def polar_angle?
@polar_angle
end
# @return [::String, nil]
attr_reader :associated_unit
# @param name [::String]
# @param polar_angle [::Boolean]
# @param associated_unit [::String]
def initialize(name, polar_angle:, associated_unit: nil)
@name = name
@polar_angle = polar_angle
@associated_unit = associated_unit
end
# @return [::Boolean]
def analogous?(other)
case [name, other.name]
in ['red' | 'x', 'red' | 'x'] |
['green' | 'y', 'green' | 'y'] |
['blue' | 'z', 'blue' | 'z'] |
['chroma' | 'saturation', 'chroma' | 'saturation'] |
['lightness', 'lightness'] |
['hue', 'hue']
true
else
false
end
end
end
private_constant :ColorChannel
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/channel.dart
class LinearChannel < ColorChannel
# @return [Numeric]
attr_reader :min, :max
# @return [::Boolean]
attr_reader :requires_percent, :lower_clamped, :upper_clamped
# @param name [::String]
# @param min [Numeric]
# @param max [Numeric]
# @param requires_percent [::Boolean]
# @param lower_clamped [::Boolean]
# @param upper_clamped [::Boolean]
# @param conventionally_percent [::Boolean]
def initialize(name, min, max, requires_percent: false, lower_clamped: false, upper_clamped: false,
conventionally_percent: nil)
super(name,
polar_angle: false,
associated_unit: if conventionally_percent.nil? ? (min.zero? && max == 100) : conventionally_percent
'%'
end)
@min = min
@max = max
@requires_percent = requires_percent
@lower_clamped = lower_clamped
@upper_clamped = upper_clamped
end
end
private_constant :LinearChannel
end
end
end
@@ -0,0 +1,473 @@
# frozen_string_literal: true
module Sass
module Value
class Color
# @see https:#www.w3.org/TR/css-color-4/#color-conversion-code.
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/conversions.dart
module Conversions
# The D50 white point.
#
# Definition from https://www.w3.org/TR/css-color-4/#color-conversion-code.
D50 = [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585].freeze
# Matrix values from https://www.w3.org/TR/css-color-4/#color-conversion-code.
# The transformation matrix for converting LMS colors to OKLab.
#
# Note that this can't be directly multiplied with [XYZ_D65_TO_LMS]; see Color
# Level 4 spec for details on how to convert between XYZ and OKLab.
LMS_TO_OKLAB = [
0.21045426830931400, 0.79361777470230540, -0.00407204301161930,
1.97799853243116840, -2.42859224204858000, 0.45059370961741100,
0.02590404246554780, 0.78277171245752960, -0.80867575492307740
].freeze
# The transformation matrix for converting OKLab colors to LMS.
#
# Note that this can't be directly multiplied with [LMS_TO_XYZ_D65]; see Color
# Level 4 spec for details on how to convert between XYZ and OKLab.
OKLAB_TO_LMS = [
1.00000000000000020, 0.39633777737617490, 0.21580375730991360,
0.99999999999999980, -0.10556134581565854, -0.06385417282581334,
0.99999999999999990, -0.08948417752981180, -1.29148554801940940
].freeze
# The following matrices were precomputed using
# https://gist.github.com/nex3/3d7ecfef467b22e02e7a666db1b8a316.
# The transformation matrix for converting linear-light srgb colors to
# linear-light display-p3.
LINEAR_SRGB_TO_LINEAR_DISPLAY_P3 = [
0.82246196871436230, 0.17753803128563775, 0.00000000000000000,
0.03319419885096161, 0.96680580114903840, 0.00000000000000000,
0.01708263072112003, 0.07239744066396346, 0.91051992861491650
].freeze
# The transformation matrix for converting linear-light display-p3 colors to
# linear-light srgb.
LINEAR_DISPLAY_P3_TO_LINEAR_SRGB = [
1.22494017628055980, -0.22494017628055996, 0.00000000000000000,
-0.04205695470968816, 1.04205695470968800, 0.00000000000000000,
-0.01963755459033443, -0.07863604555063188, 1.09827360014096630
].freeze
# The transformation matrix for converting linear-light srgb colors to
# linear-light a98-rgb.
LINEAR_SRGB_TO_LINEAR_A98_RGB = [
0.71512560685562470, 0.28487439314437535, 0.00000000000000000,
0.00000000000000000, 1.00000000000000000, 0.00000000000000000,
0.00000000000000000, 0.04116194845011846, 0.95883805154988160
].freeze
# The transformation matrix for converting linear-light a98-rgb colors to
# linear-light srgb.
LINEAR_A98_RGB_TO_LINEAR_SRGB = [
1.39835574396077830, -0.39835574396077830, 0.00000000000000000,
0.00000000000000000, 1.00000000000000000, 0.00000000000000000,
0.00000000000000000, -0.04292898929447326, 1.04292898929447330
].freeze
# The transformation matrix for converting linear-light srgb colors to
# linear-light rec2020.
LINEAR_SRGB_TO_LINEAR_REC2020 = [
0.62740389593469900, 0.32928303837788370, 0.04331306568741722,
0.06909728935823208, 0.91954039507545870, 0.01136231556630917,
0.01639143887515027, 0.08801330787722575, 0.89559525324762400
].freeze
# The transformation matrix for converting linear-light rec2020 colors to
# linear-light srgb.
LINEAR_REC2020_TO_LINEAR_SRGB = [
1.66049100210843450, -0.58764113878854950, -0.07284986331988487,
-0.12455047452159074, 1.13289989712596030, -0.00834942260436947,
-0.01815076335490530, -0.10057889800800737, 1.11872966136291270
].freeze
# The transformation matrix for converting linear-light srgb colors to xyz.
LINEAR_SRGB_TO_XYZ_D65 = [
0.41239079926595950, 0.35758433938387796, 0.18048078840183430,
0.21263900587151036, 0.71516867876775590, 0.07219231536073371,
0.01933081871559185, 0.11919477979462598, 0.95053215224966060
].freeze
# The transformation matrix for converting xyz colors to linear-light srgb.
XYZ_D65_TO_LINEAR_SRGB = [
3.24096994190452130, -1.53738317757009350, -0.49861076029300330,
-0.96924363628087980, 1.87596750150772060, 0.04155505740717561,
0.05563007969699360, -0.20397695888897657, 1.05697151424287860
].freeze
# The transformation matrix for converting linear-light srgb colors to lms.
LINEAR_SRGB_TO_LMS = [
0.41222146947076300, 0.53633253726173480, 0.05144599326750220,
0.21190349581782520, 0.68069955064523420, 0.10739695353694050,
0.08830245919005641, 0.28171883913612150, 0.62997870167382210
].freeze
# The transformation matrix for converting lms colors to linear-light srgb.
LMS_TO_LINEAR_SRGB = [
4.07674163607595800, -3.30771153925806200, 0.23096990318210417,
-1.26843797328503200, 2.60975734928768900, -0.34131937600265710,
-0.00419607613867551, -0.70341861793593630, 1.70761469407461200
].freeze
# The transformation matrix for converting linear-light srgb colors to
# linear-light prophoto-rgb.
LINEAR_SRGB_TO_LINEAR_PROPHOTO_RGB = [
0.52927697762261160, 0.33015450197849283, 0.14056852039889556,
0.09836585954044917, 0.87347071290696180, 0.02816342755258900,
0.01687534092138684, 0.11765941425612084, 0.86546524482249230
].freeze
# The transformation matrix for converting linear-light prophoto-rgb colors to
# linear-light srgb.
LINEAR_PROPHOTO_RGB_TO_LINEAR_SRGB = [
2.03438084951699600, -0.72763578993413420, -0.30674505958286180,
-0.22882573163305037, 1.23174254119010480, -0.00291680955705449,
-0.00855882878391742, -0.15326670213803720, 1.16182553092195470
].freeze
# The transformation matrix for converting linear-light srgb colors to
# xyz-d50.
LINEAR_SRGB_TO_XYZ_D50 = [
0.43606574687426936, 0.38515150959015960, 0.14307841996513868,
0.22249317711056518, 0.71688701309448240, 0.06061980979495235,
0.01392392146316939, 0.09708132423141015, 0.71409935681588070
].freeze
# The transformation matrix for converting xyz-d50 colors to linear-light
# srgb.
XYZ_D50_TO_LINEAR_SRGB = [
3.13413585290011780, -1.61738599801804200, -0.49066221791109754,
-0.97879547655577770, 1.91625437739598840, 0.03344287339036693,
0.07195539255794733, -0.22897675981518200, 1.40538603511311820
].freeze
# The transformation matrix for converting linear-light display-p3 colors to
# linear-light a98-rgb.
LINEAR_DISPLAY_P3_TO_LINEAR_A98_RGB = [
0.86400513747404840, 0.13599486252595164, 0.00000000000000000,
-0.04205695470968816, 1.04205695470968800, 0.00000000000000000,
-0.02056038078232985, -0.03250613804550798, 1.05306651882783790
].freeze
# The transformation matrix for converting linear-light a98-rgb colors to
# linear-light display-p3.
LINEAR_A98_RGB_TO_LINEAR_DISPLAY_P3 = [
1.15009441814101840, -0.15009441814101834, 0.00000000000000000,
0.04641729862941844, 0.95358270137058150, 0.00000000000000000,
0.02388759479083904, 0.02650477632633013, 0.94960762888283080
].freeze
# The transformation matrix for converting linear-light display-p3 colors to
# linear-light rec2020.
LINEAR_DISPLAY_P3_TO_LINEAR_REC2020 = [
0.75383303436172180, 0.19859736905261630, 0.04756959658566187,
0.04574384896535833, 0.94177721981169350, 0.01247893122294812,
-0.00121034035451832, 0.01760171730108989, 0.98360862305342840
].freeze
# The transformation matrix for converting linear-light rec2020 colors to
# linear-light display-p3.
LINEAR_REC2020_TO_LINEAR_DISPLAY_P3 = [
1.34357825258433200, -0.28217967052613570, -0.06139858205819628,
-0.06529745278911953, 1.07578791584857460, -0.01049046305945495,
0.00282178726170095, -0.01959849452449406, 1.01677670726279310
].freeze
# The transformation matrix for converting linear-light display-p3 colors to
# xyz.
LINEAR_DISPLAY_P3_TO_XYZ_D65 = [
0.48657094864821626, 0.26566769316909294, 0.19821728523436250,
0.22897456406974884, 0.69173852183650620, 0.07928691409374500,
0.00000000000000000, 0.04511338185890257, 1.04394436890097570
].freeze
# The transformation matrix for converting xyz colors to linear-light
# display-p3.
XYZ_D65_TO_LINEAR_DISPLAY_P3 = [
2.49349691194142450, -0.93138361791912360, -0.40271078445071684,
-0.82948896956157490, 1.76266406031834680, 0.02362468584194359,
0.03584583024378433, -0.07617238926804170, 0.95688452400768730
].freeze
# The transformation matrix for converting linear-light display-p3 colors to
# lms.
LINEAR_DISPLAY_P3_TO_LMS = [
0.48137985274995443, 0.46211837101131803, 0.05650177623872756,
0.22883194181124472, 0.65321681938356760, 0.11795123880518774,
0.08394575232299319, 0.22416527097756642, 0.69188897669944040
].freeze
# The transformation matrix for converting lms colors to linear-light
# display-p3.
LMS_TO_LINEAR_DISPLAY_P3 = [
3.12776897136187370, -2.25713576259163860, 0.12936679122976494,
-1.09100901843779790, 2.41333171030692250, -0.32232269186912466,
-0.02601080193857045, -0.50804133170416700, 1.53405213364273730
].freeze
# The transformation matrix for converting linear-light display-p3 colors to
# linear-light prophoto-rgb.
LINEAR_DISPLAY_P3_TO_LINEAR_PROPHOTO_RGB = [
0.63168691934035890, 0.21393038569465722, 0.15438269496498390,
0.08320371426648458, 0.88586513676302430, 0.03093114897049121,
-0.00127273456473881, 0.05075510433665735, 0.95051763022808140
].freeze
# The transformation matrix for converting linear-light prophoto-rgb colors to
# linear-light display-p3.
LINEAR_PROPHOTO_RGB_TO_LINEAR_DISPLAY_P3 = [
1.63257560870691790, -0.37977161848259840, -0.25280399022431950,
-0.15370040233755072, 1.16670254724250140, -0.01300214490495082,
0.01039319529676572, -0.06280731264959440, 1.05241411735282870
].freeze
# The transformation matrix for converting linear-light display-p3 colors to
# xyz-d50.
LINEAR_DISPLAY_P3_TO_XYZ_D50 = [
0.51514644296811600, 0.29200998206385770, 0.15713925139759397,
0.24120032212525520, 0.69222254113138180, 0.06657713674336294,
-0.00105013914714014, 0.04187827018907460, 0.78427647146852570
].freeze
# The transformation matrix for converting xyz-d50 colors to linear-light
# display-p3.
XYZ_D50_TO_LINEAR_DISPLAY_P3 = [
2.40393412185549730, -0.99003044249559310, -0.39761363181465614,
-0.84227001614546880, 1.79895801610670820, 0.01604562477090472,
0.04819381686413303, -0.09738519815446048, 1.27367136933212730
].freeze
# The transformation matrix for converting linear-light a98-rgb colors to
# linear-light rec2020.
LINEAR_A98_RGB_TO_LINEAR_REC2020 = [
0.87733384166365680, 0.07749370651571998, 0.04517245182062317,
0.09662259146620378, 0.89152732024418050, 0.01185008828961569,
0.02292106270284839, 0.04303668501067932, 0.93404225228647230
].freeze
# The transformation matrix for converting linear-light rec2020 colors to
# linear-light a98-rgb.
LINEAR_REC2020_TO_LINEAR_A98_RGB = [
1.15197839471591630, -0.09750305530240860, -0.05447533941350766,
-0.12455047452159074, 1.13289989712596030, -0.00834942260436947,
-0.02253038278105590, -0.04980650742838876, 1.07233689020944460
].freeze
# The transformation matrix for converting linear-light a98-rgb colors to xyz.
LINEAR_A98_RGB_TO_XYZ_D65 = [
0.57666904291013080, 0.18555823790654627, 0.18822864623499472,
0.29734497525053616, 0.62736356625546600, 0.07529145849399789,
0.02703136138641237, 0.07068885253582714, 0.99133753683763890
].freeze
# The transformation matrix for converting xyz colors to linear-light a98-rgb.
XYZ_D65_TO_LINEAR_A98_RGB = [
2.04158790381074600, -0.56500697427885960, -0.34473135077832950,
-0.96924363628087980, 1.87596750150772060, 0.04155505740717561,
0.01344428063203102, -0.11836239223101823, 1.01517499439120540
].freeze
# The transformation matrix for converting linear-light a98-rgb colors to lms.
LINEAR_A98_RGB_TO_LMS = [
0.57643225961839410, 0.36991322261987963, 0.05365451776172635,
0.29631647054222465, 0.59167613325218850, 0.11200739620558686,
0.12347825101427760, 0.21949869837199862, 0.65702305061372380
].freeze
# The transformation matrix for converting lms colors to linear-light a98-rgb.
LMS_TO_LINEAR_A98_RGB = [
2.55403683861155660, -1.62197618068286990, 0.06793934207131327,
-1.26843797328503200, 2.60975734928768900, -0.34131937600265710,
-0.05623473593749381, -0.56704183956690610, 1.62327657550439990
].freeze
# The transformation matrix for converting linear-light a98-rgb colors to
# linear-light prophoto-rgb.
LINEAR_A98_RGB_TO_LINEAR_PROPHOTO_RGB = [
0.74011750180477920, 0.11327951328898105, 0.14660298490623970,
0.13755046469802620, 0.83307708026948400, 0.02937245503248977,
0.02359772990871766, 0.07378347703906656, 0.90261879305221580
].freeze
# The transformation matrix for converting linear-light prophoto-rgb colors to
# linear-light a98-rgb.
LINEAR_PROPHOTO_RGB_TO_LINEAR_A98_RGB = [
1.38965124815152000, -0.16945907691487766, -0.22019217123664242,
-0.22882573163305037, 1.23174254119010480, -0.00291680955705449,
-0.01762544368426068, -0.09625702306122665, 1.11388246674548740
].freeze
# The transformation matrix for converting linear-light a98-rgb colors to
# xyz-d50.
LINEAR_A98_RGB_TO_XYZ_D50 = [
0.60977504188618140, 0.20530000261929401, 0.14922063192409227,
0.31112461220464155, 0.62565323083468560, 0.06322215696067286,
0.01947059555648168, 0.06087908649415867, 0.74475492045981980
].freeze
# The transformation matrix for converting xyz-d50 colors to linear-light
# a98-rgb.
XYZ_D50_TO_LINEAR_A98_RGB = [
1.96246703637688060, -0.61074234048150730, -0.34135809808271540,
-0.97879547655577770, 1.91625437739598840, 0.03344287339036693,
0.02870443944957101, -0.14067486633170680, 1.34891418141379370
].freeze
# The transformation matrix for converting linear-light rec2020 colors to xyz.
LINEAR_REC2020_TO_XYZ_D65 = [
0.63695804830129130, 0.14461690358620838, 0.16888097516417205,
0.26270021201126703, 0.67799807151887100, 0.05930171646986194,
0.00000000000000000, 0.02807269304908750, 1.06098505771079090
].freeze
# The transformation matrix for converting xyz colors to linear-light rec2020.
XYZ_D65_TO_LINEAR_REC2020 = [
1.71665118797126760, -0.35567078377639240, -0.25336628137365980,
-0.66668435183248900, 1.61648123663493900, 0.01576854581391113,
0.01763985744531091, -0.04277061325780865, 0.94210312123547400
].freeze
# The transformation matrix for converting linear-light rec2020 colors to lms.
LINEAR_REC2020_TO_LMS = [
0.61675578486544440, 0.36019840122646335, 0.02304581390809228,
0.26513305939263670, 0.63583937206784910, 0.09902756853951408,
0.10010262952034828, 0.20390652261661452, 0.69599084786303720
].freeze
# The transformation matrix for converting lms colors to linear-light rec2020.
LMS_TO_LINEAR_REC2020 = [
2.13990673043465130, -1.24638949376061800, 0.10648276332596668,
-0.88473583575776740, 2.16323093836120070, -0.27849510260343340,
-0.04857374640044396, -0.45450314971409640, 1.50307689611454040
].freeze
# The transformation matrix for converting linear-light rec2020 colors to
# linear-light prophoto-rgb.
LINEAR_REC2020_TO_LINEAR_PROPHOTO_RGB = [
0.83518733312972350, 0.04886884858605698, 0.11594381828421951,
0.05403324519953363, 0.92891840856920440, 0.01704834623126199,
-0.00234203897072539, 0.03633215316169465, 0.96600988580903070
].freeze
# The transformation matrix for converting linear-light prophoto-rgb colors to
# linear-light rec2020.
LINEAR_PROPHOTO_RGB_TO_LINEAR_REC2020 = [
1.20065932951740800, -0.05756805370122346, -0.14309127581618444,
-0.06994154955888504, 1.08061789759721400, -0.01067634803832895,
0.00554147334294746, -0.04078219298657951, 1.03524071964363200
].freeze
# The transformation matrix for converting linear-light rec2020 colors to
# xyz-d50.
LINEAR_REC2020_TO_XYZ_D50 = [
0.67351546318827600, 0.16569726370390453, 0.12508294953738705,
0.27905900514112060, 0.67531800574910980, 0.04562298910976962,
-0.00193242713400438, 0.02997782679282923, 0.79705920285163550
].freeze
# The transformation matrix for converting xyz-d50 colors to linear-light
# rec2020.
XYZ_D50_TO_LINEAR_REC2020 = [
1.64718490467176600, -0.39368189813164710, -0.23595963848828266,
-0.68266410741738180, 1.64771461274440760, 0.01281708338512084,
0.02966887665275675, -0.06292589642970030, 1.25355782018657710
].freeze
# The transformation matrix for converting xyz colors to lms.
XYZ_D65_TO_LMS = [
0.81902243799670300, 0.36190626005289034, -0.12887378152098788,
0.03298365393238846, 0.92928686158634330, 0.03614466635064235,
0.04817718935962420, 0.26423953175273080, 0.63354782846943080
].freeze
# The transformation matrix for converting lms colors to xyz.
LMS_TO_XYZ_D65 = [
1.22687987584592430, -0.55781499446021710, 0.28139104566596460,
-0.04057574521480084, 1.11228680328031730, -0.07171105806551635,
-0.07637293667466007, -0.42149333240224324, 1.58692401983678180
].freeze
# The transformation matrix for converting xyz colors to linear-light
# prophoto-rgb.
XYZ_D65_TO_LINEAR_PROPHOTO_RGB = [
1.40319046337749790, -0.22301514479051668, -0.10160668507413790,
-0.52623840216330720, 1.48163196292346440, 0.01701879027252688,
-0.01120226528622150, 0.01824640347962099, 0.91124722749150480
].freeze
# The transformation matrix for converting linear-light prophoto-rgb colors to
# xyz.
LINEAR_PROPHOTO_RGB_TO_XYZ_D65 = [
0.75559074229692100, 0.11271984265940525, 0.08214534209534540,
0.26832184357857190, 0.71511525666179120, 0.01656289975963685,
0.00391597276242580, -0.01293344283684181, 1.09807522083429450
].freeze
# The transformation matrix for converting xyz colors to xyz-d50.
XYZ_D65_TO_XYZ_D50 = [
1.04792979254499660, 0.02294687060160952, -0.05019226628920519,
0.02962780877005567, 0.99043442675388000, -0.01707379906341879,
-0.00924304064620452, 0.01505519149029816, 0.75187428142813700
].freeze
# The transformation matrix for converting xyz-d50 colors to xyz.
XYZ_D50_TO_XYZ_D65 = [
0.95547342148807520, -0.02309845494876452, 0.06325924320057065,
-0.02836970933386358, 1.00999539808130410, 0.02104144119191730,
0.01231401486448199, -0.02050764929889898, 1.33036592624212400
].freeze
# The transformation matrix for converting lms colors to linear-light
# prophoto-rgb.
LMS_TO_LINEAR_PROPHOTO_RGB = [
1.73835514811572070, -0.98795094275144580, 0.24959579463572504,
-0.70704940153292660, 1.93437004444013820, -0.22732064290721150,
-0.08407882206239634, -0.35754060521141334, 1.44161942727380970
].freeze
# The transformation matrix for converting linear-light prophoto-rgb colors to
# lms.
LINEAR_PROPHOTO_RGB_TO_LMS = [
0.71544846056555340, 0.35279155007721186, -0.06824001064276530,
0.27441164900156710, 0.66779764984123670, 0.05779070115719616,
0.10978443261622942, 0.18619829115002018, 0.70401727623375040
].freeze
# The transformation matrix for converting lms colors to xyz-d50.
LMS_TO_XYZ_D50 = [
1.28858621817270600, -0.53787174449737450, 0.21358120275423640,
-0.00253387643187372, 1.09231679887191650, -0.08978292244004273,
-0.06937382305734124, -0.29500839894431263, 1.18948682451211420
].freeze
# The transformation matrix for converting xyz-d50 colors to lms.
XYZ_D50_TO_LMS = [
0.77070004204311720, 0.34924840261939616, -0.11202351884164681,
0.00559649248368848, 0.93707234011367690, 0.06972568836252771,
0.04633714262191069, 0.25277531574310524, 0.85145807674679600
].freeze
# The transformation matrix for converting linear-light prophoto-rgb colors to
# xyz-d50.
LINEAR_PROPHOTO_RGB_TO_XYZ_D50 = [
0.79776664490064230, 0.13518129740053308, 0.03134773412839220,
0.28807482881940130, 0.71183523424187300, 0.00008993693872564,
0.00000000000000000, 0.00000000000000000, 0.82510460251046020
].freeze
# The transformation matrix for converting xyz-d50 colors to linear-light
# prophoto-rgb.
XYZ_D50_TO_LINEAR_PROPHOTO_RGB = [
1.34578688164715830, -0.25557208737979464, -0.05110186497554526,
-0.54463070512490190, 1.50824774284514680, 0.02052744743642139,
0.00000000000000000, 0.00000000000000000, 1.21196754563894520
].freeze
end
private_constant :Conversions
end
end
end
@@ -0,0 +1,45 @@
# frozen_string_literal: true
module Sass
module Value
class Color
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/gamut_map_method.dart
module GamutMapMethod
# @return [::String]
attr_reader :name
# @param name [::String]
def initialize(name)
@name = name
end
class << self
# @param name [::String]
# @param argument_name [::String]
# @return [GamutMapMethod]
def from_name(name, argument_name = nil)
case name
when 'clip'
CLIP
when 'local-minde'
LOCAL_MINDE
else
raise Sass::ScriptError.new("Unknown gamut map method \"#{name}\".", argument_name)
end
end
end
# @param color [Color]
# @return [Color]
def map(color)
raise NotImplementedError, "[BUG] gamut map method #{name} doesn't implement map."
end
end
private_constant :GamutMapMethod
end
end
end
require_relative 'gamut_map_method/clip'
require_relative 'gamut_map_method/local_minde'
@@ -0,0 +1,45 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module GamutMapMethod
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/gamut_map_method/local_minde.dart
class Clip
include GamutMapMethod
def initialize
super('clip')
end
def map(color)
space = color.send(:_space)
Color.send(:for_space_internal,
space,
_clamp_channel(color.send(:channel0_or_nil), space.channels[0]),
_clamp_channel(color.send(:channel1_or_nil), space.channels[1]),
_clamp_channel(color.send(:channel2_or_nil), space.channels[2]),
color.send(:alpha_or_nil))
end
private
def _clamp_channel(value, channel)
return nil if value.nil?
case channel
when LinearChannel
FuzzyMath._clamp_like_css(value, channel.min, channel.max)
else
value
end
end
end
private_constant :Clip
CLIP = Clip.new
end
end
end
end
@@ -0,0 +1,94 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module GamutMapMethod
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/gamut_map_method/local_minde.dart
class LocalMinde
include GamutMapMethod
# A constant from the gamut-mapping algorithm.
JND = 0.02
private_constant :JND
# A constant from the gamut-mapping algorithm.
EPSILON = 0.0001
private_constant :EPSILON
def initialize
super('local-minde')
end
def map(color)
original_oklch = color.send(:_to_space, Space::OKLCH)
lightness = original_oklch.send(:channel0_or_nil)
hue = original_oklch.send(:channel2_or_nil)
alpha = original_oklch.send(:alpha_or_nil)
if FuzzyMath.greater_than_or_equals?(lightness.nil? ? 0 : lightness, 1)
if color.legacy?
return Color.send(:_for_space,
Space::RGB, 255, 255, 255, color.send(:alpha_or_nil))
.send(:_to_space, color.send(:_space))
else
return Color.send(:for_space_internal,
color.send(:_space), 1, 1, 1, color.send(:alpha_or_nil))
end
elsif FuzzyMath.less_than_or_equals?(lightness.nil? ? 0 : lightness, 0)
return Color.send(:_for_space,
Space::RGB, 0, 0, 0, color.send(:alpha_or_nil))
.send(:_to_space, color.send(:_space))
end
clipped = color.send(:_to_gamut, CLIP)
return clipped if _delta_eok(clipped, color) < JND
min = 0.0
max = original_oklch.send(:channel1)
min_in_gamut = true
while max - min > EPSILON
chroma = (min + max) / 2
current = Space::OKLCH.convert(color.send(:_space), lightness, chroma, hue, alpha)
if min_in_gamut && current.in_gamut?
min = chroma
next
end
clipped = current.send(:_to_gamut, CLIP)
e = _delta_eok(clipped, current)
if e < JND
return clipped if JND - e < EPSILON
min_in_gamut = false
min = chroma
else
max = chroma
end
end
clipped
end
private
def _delta_eok(color1, color2)
lab1 = color1.send(:_to_space, Space::OKLAB)
lab2 = color2.send(:_to_space, Space::OKLAB)
Math.sqrt(((lab1.send(:channel0) - lab2.send(:channel0))**2) +
((lab1.send(:channel1) - lab2.send(:channel1))**2) +
((lab1.send(:channel2) - lab2.send(:channel2))**2))
end
end
private_constant :LocalMinde
LOCAL_MINDE = LocalMinde.new
end
end
end
end
@@ -0,0 +1,51 @@
# frozen_string_literal: true
module Sass
module Value
class Color
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/interpolation_method.dart
class InterpolationMethod
# @return [Space]
attr_reader :space
# @return [Symbol, nil]
attr_reader :hue
# @param space [Space]
# @param hue [Symbol]
def initialize(space, hue = nil)
@space = space
@hue = if space.polar?
hue.nil? ? :shorter : hue
end
return unless !space.polar? && !hue.nil?
raise Sass::ScriptError,
"Hue interpolation method may not be set for rectangular color space #{space.name}."
end
end
private_constant :InterpolationMethod
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/interpolation_method.dart
module HueInterpolationMethod
class << self
# @param name [::String]
# @param argument_name [::String]
# @return [Symbol]
def from_name(name, argument_name = nil)
case name
when 'decreasing', 'increasing', 'longer', 'shorter'
name.to_sym
else
raise Sass::ScriptError.new("Unknown hue interpolation method \"#{name}\".", argument_name)
end
end
end
end
private_constant :HueInterpolationMethod
end
end
end
@@ -0,0 +1,218 @@
# frozen_string_literal: true
module Sass
module Value
class Color
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space.dart
module Space
# @return [::String]
attr_reader :name
# @return [Array<ColorChannel>]
attr_reader :channels
# @return [::Boolean]
def bounded?
raise NotImplementedError
end
# @return [::Boolean]
def legacy?
false
end
# @return [::Boolean]
def polar?
false
end
# @param name [::String]
# @param channels [Array<ColorChannel>]
def initialize(name, channels)
@name = name
@channels = channels
end
class << self
# @param name [::String]
# @param argument_name [::String]
# @return [Space]
def from_name(name, argument_name = nil)
case name.downcase
when 'rgb'
RGB
when 'hwb'
HWB
when 'hsl'
HSL
when 'srgb'
SRGB
when 'srgb-linear'
SRGB_LINEAR
when 'display-p3'
DISPLAY_P3
when 'display-p3-linear'
DISPLAY_P3_LINEAR
when 'a98-rgb'
A98_RGB
when 'prophoto-rgb'
PROPHOTO_RGB
when 'rec2020'
REC2020
when 'xyz', 'xyz-d65'
XYZ_D65
when 'xyz-d50'
XYZ_D50
when 'lab'
LAB
when 'lch'
LCH
when 'oklab'
OKLAB
when 'oklch'
OKLCH
else
raise Sass::ScriptError.new("Unknown color space \"#{name}\".", argument_name)
end
end
end
# @param dest [Space]
# @param channel0 [Numeric]
# @param channel1 [Numeric]
# @param channel2 [Numeric]
# @param alpha [Numeric]
# @return [Color]
def convert(dest, channel0, channel1, channel2, alpha)
convert_linear(dest, channel0, channel1, channel2, alpha)
end
protected
def convert_linear(dest, red, green, blue, alpha,
missing_lightness: false,
missing_chroma: false,
missing_hue: false,
missing_a: false,
missing_b: false)
if missing_a && missing_b
missing_chroma = true
missing_hue = true
elsif missing_chroma && missing_hue
missing_a = true
missing_b = true
end
if (missing_lightness && missing_chroma && missing_hue) || (red.nil? && green.nil? && blue.nil?)
return Color.send(:_for_space,
dest,
nil,
nil,
nil,
alpha)
end
linear_dest = case dest
when HSL, HWB
SRGB
when LAB, LCH
XYZ_D50
when OKLAB, OKLCH
LMS
else
dest
end
if linear_dest == self
transformed_red = red
transformed_green = green
transformed_blue = blue
else
linear_red = to_linear(red.nil? ? 0 : red)
linear_green = to_linear(green.nil? ? 0 : green)
linear_blue = to_linear(blue.nil? ? 0 : blue)
matrix = transformation_matrix(linear_dest)
# (matrix * [linear_red, linear_green, linear_blue]).map(linear_dest.from_linear)
transformed_red = linear_dest.from_linear((matrix[0] * linear_red) +
(matrix[1] * linear_green) +
(matrix[2] * linear_blue))
transformed_green = linear_dest.from_linear((matrix[3] * linear_red) +
(matrix[4] * linear_green) +
(matrix[5] * linear_blue))
transformed_blue = linear_dest.from_linear((matrix[6] * linear_red) +
(matrix[7] * linear_green) +
(matrix[8] * linear_blue))
end
case dest
when HSL, HWB
SRGB.convert(dest, transformed_red, transformed_green, transformed_blue, alpha,
missing_lightness:,
missing_chroma:,
missing_hue:)
when LAB, LCH
XYZ_D50.convert(dest, transformed_red, transformed_green, transformed_blue, alpha,
missing_lightness:,
missing_chroma:,
missing_hue:,
missing_a:,
missing_b:)
when OKLAB, OKLCH
LMS.convert(dest, transformed_red, transformed_green, transformed_blue, alpha,
missing_lightness:,
missing_chroma:,
missing_hue:,
missing_a:,
missing_b:)
else
Color.send(:_for_space,
dest,
red.nil? ? nil : transformed_red,
green.nil? ? nil : transformed_green,
blue.nil? ? nil : transformed_blue,
alpha)
end
end
# @param channel [Numeric]
# @return [Numeric]
def to_linear(channel)
raise NotImplementedError, "[BUG] Color space #{name} doesn't support linear conversions."
end
# @param channel [Numeric]
# @return [Numeric]
def from_linear(channel)
raise NotImplementedError, "[BUG] Color space #{name} doesn't support linear conversions."
end
# @param dest [Space]
# @return [Array<Numeric>]
def transformation_matrix(dest)
raise NotImplementedError, "[BUG] Color space conversion from #{name} to #{dest.name} not implemented."
end
end
private_constant :Space
end
end
end
require_relative 'space/utils'
require_relative 'space/a98_rgb'
require_relative 'space/display_p3'
require_relative 'space/display_p3_linear'
require_relative 'space/hsl'
require_relative 'space/hwb'
require_relative 'space/lab'
require_relative 'space/lch'
require_relative 'space/lms'
require_relative 'space/oklab'
require_relative 'space/oklch'
require_relative 'space/prophoto_rgb'
require_relative 'space/rec2020'
require_relative 'space/rgb'
require_relative 'space/srgb'
require_relative 'space/srgb_linear'
require_relative 'space/xyz_d50'
require_relative 'space/xyz_d65'
@@ -0,0 +1,57 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/a98_rgb.dart
class A98Rgb
include Space
def bounded?
true
end
def initialize
super('a98-rgb', Utils::RGB_CHANNELS)
end
def to_linear(channel)
(channel <=> 0) * (channel.abs**(563 / 256.0))
end
def from_linear(channel)
(channel <=> 0) * (channel.abs**(256 / 563.0))
end
private
def transformation_matrix(dest)
case dest
when DISPLAY_P3, DISPLAY_P3_LINEAR
Conversions::LINEAR_A98_RGB_TO_LINEAR_DISPLAY_P3
when LMS
Conversions::LINEAR_A98_RGB_TO_LMS
when PROPHOTO_RGB
Conversions::LINEAR_A98_RGB_TO_LINEAR_PROPHOTO_RGB
when REC2020
Conversions::LINEAR_A98_RGB_TO_LINEAR_REC2020
when RGB, SRGB, SRGB_LINEAR
Conversions::LINEAR_A98_RGB_TO_LINEAR_SRGB
when XYZ_D50
Conversions::LINEAR_A98_RGB_TO_XYZ_D50
when XYZ_D65
Conversions::LINEAR_A98_RGB_TO_XYZ_D65
else
super
end
end
end
private_constant :A98Rgb
A98_RGB = A98Rgb.new
end
end
end
end
@@ -0,0 +1,72 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/display_p3.dart
class DisplayP3
include Space
def bounded?
true
end
def initialize
super('display-p3', Utils::RGB_CHANNELS)
end
def convert(dest, red, green, blue, alpha)
if dest == DISPLAY_P3_LINEAR
Color.send(
:for_space_internal,
dest,
red.nil? ? nil : to_linear(red),
green.nil? ? nil : to_linear(green),
blue.nil? ? nil : to_linear(blue),
alpha
)
else
convert_linear(dest, red, green, blue, alpha)
end
end
def to_linear(channel)
Utils.srgb_and_display_p3_to_linear(channel)
end
def from_linear(channel)
Utils.srgb_and_display_p3_from_linear(channel)
end
private
def transformation_matrix(dest)
case dest
when A98_RGB
Conversions::LINEAR_DISPLAY_P3_TO_LINEAR_A98_RGB
when LMS
Conversions::LINEAR_DISPLAY_P3_TO_LMS
when PROPHOTO_RGB
Conversions::LINEAR_DISPLAY_P3_TO_LINEAR_PROPHOTO_RGB
when REC2020
Conversions::LINEAR_DISPLAY_P3_TO_LINEAR_REC2020
when RGB, SRGB, SRGB_LINEAR
Conversions::LINEAR_DISPLAY_P3_TO_LINEAR_SRGB
when XYZ_D50
Conversions::LINEAR_DISPLAY_P3_TO_XYZ_D50
when XYZ_D65
Conversions::LINEAR_DISPLAY_P3_TO_XYZ_D65
else
super
end
end
end
private_constant :DisplayP3
DISPLAY_P3 = DisplayP3.new
end
end
end
end
@@ -0,0 +1,72 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/display_p3_linear.dart
class DisplayP3Linear
include Space
def bounded?
true
end
def initialize
super('display-p3-linear', Utils::RGB_CHANNELS)
end
def convert(dest, red, green, blue, alpha)
if dest == DISPLAY_P3
Color.send(
:for_space_internal,
dest,
red.nil? ? nil : Utils.srgb_and_display_p3_from_linear(red),
green.nil? ? nil : Utils.srgb_and_display_p3_from_linear(green),
blue.nil? ? nil : Utils.srgb_and_display_p3_from_linear(blue),
alpha
)
else
super
end
end
def to_linear(channel)
channel
end
def from_linear(channel)
channel
end
private
def transformation_matrix(dest)
case dest
when A98_RGB
Conversions::LINEAR_DISPLAY_P3_TO_LINEAR_A98_RGB
when LMS
Conversions::LINEAR_DISPLAY_P3_TO_LMS
when PROPHOTO_RGB
Conversions::LINEAR_DISPLAY_P3_TO_LINEAR_PROPHOTO_RGB
when REC2020
Conversions::LINEAR_DISPLAY_P3_TO_LINEAR_REC2020
when RGB, SRGB, SRGB_LINEAR
Conversions::LINEAR_DISPLAY_P3_TO_LINEAR_SRGB
when XYZ_D50
Conversions::LINEAR_DISPLAY_P3_TO_XYZ_D50
when XYZ_D65
Conversions::LINEAR_DISPLAY_P3_TO_XYZ_D65
else
super
end
end
end
private_constant :DisplayP3Linear
DISPLAY_P3_LINEAR = DisplayP3Linear.new
end
end
end
end
@@ -0,0 +1,65 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/hsl.dart
class Hsl
include Space
def bounded?
true
end
def legacy?
true
end
def polar?
true
end
def initialize
super('hsl', [
Utils::HUE_CHANNEL,
LinearChannel.new('saturation', 0, 100, requires_percent: true, lower_clamped: true).freeze,
LinearChannel.new('lightness', 0, 100, requires_percent: true).freeze
].freeze)
end
def convert(dest, hue, saturation, lightness, alpha)
missing_lightness = lightness.nil?
missing_chroma = saturation.nil?
missing_hue = hue.nil?
hue = ((hue.nil? ? 0 : hue) % 360) / 30.0
saturation = (saturation.nil? ? 0 : saturation) / 100.0
lightness = (lightness.nil? ? 0 : lightness) / 100.0
a = saturation * [lightness, 1 - lightness].min
f = lambda do |n|
k = (n + hue) % 12
lightness - (a * [-1, [k - 3, 9 - k, 1].min].max)
end
SRGB.convert(
dest,
f.call(0),
f.call(8),
f.call(4),
alpha,
missing_lightness:,
missing_chroma:,
missing_hue:
)
end
end
private_constant :Hsl
HSL = Hsl.new
end
end
end
end
@@ -0,0 +1,94 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/hwb.dart
class Hwb
include Space
def bounded?
true
end
def legacy?
true
end
def polar?
true
end
def initialize
super('hwb', [
Utils::HUE_CHANNEL,
LinearChannel.new('whiteness', 0, 100, requires_percent: true).freeze,
LinearChannel.new('blackness', 0, 100, requires_percent: true).freeze
].freeze)
end
def convert(dest, hue, whiteness, blackness, alpha)
if whiteness.nil? && blackness.nil?
return Color.send(:_for_space, dest, nil, nil, nil, alpha) if hue.nil?
converted = convert(dest, hue, 0, 0, alpha)
return case dest
when HSL
Color.send(:_for_space,
dest,
converted.channel0,
nil,
nil,
converted.alpha)
when LCH, OKLCH
Color.send(:_for_space,
dest,
nil,
nil,
converted.channel2,
converted.alpha)
else
converted
end
end
missing_hue = hue.nil?
hue = ((hue.nil? ? 0 : hue) % 360) / 30.0
whiteness = (whiteness.nil? ? 0 : whiteness) / 100.0
blackness = (blackness.nil? ? 0 : blackness) / 100.0
sum = whiteness + blackness
if sum > 1
gray = whiteness / sum
SRGB.convert(dest,
gray,
gray,
gray,
alpha,
missing_hue:)
else
f = lambda do |n|
k = (n + hue) % 12
0.5 - ([-1, [k - 3, 9 - k, 1].min].max / 2.0)
end
factor = 1 - sum
SRGB.convert(dest,
(f.call(0) * factor) + whiteness,
(f.call(8) * factor) + whiteness,
(f.call(4) * factor) + whiteness,
alpha,
missing_hue:)
end
end
end
private_constant :Hwb
HWB = Hwb.new
end
end
end
end
@@ -0,0 +1,85 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/lab.dart
class Lab
include Space
def bounded?
false
end
def initialize
super('lab', [
LinearChannel.new('lightness', 0, 100, lower_clamped: true, upper_clamped: true).freeze,
LinearChannel.new('a', -125, 125).freeze,
LinearChannel.new('b', -125, 125).freeze
].freeze)
end
def convert(dest, lightness, a, b, alpha, # rubocop:disable Naming/MethodParameterName
missing_chroma: false, missing_hue: false)
if missing_chroma && missing_hue
a = nil
b = nil
elsif a.nil? && b.nil?
missing_chroma = true
missing_hue = true
end
case dest
when LAB
powerless_ab = lightness.nil? || FuzzyMath.equals?(lightness, 0)
Color.send(
:_for_space,
dest,
lightness,
a.nil? || powerless_ab ? nil : a,
b.nil? || powerless_ab ? nil : b,
alpha
)
when LCH
Utils.lab_to_lch(dest, lightness, a, b, alpha)
else
missing_lightness = lightness.nil?
lightness = 0 if missing_lightness
f1 = (lightness + 16) / 116.0
XYZ_D50.convert(
dest,
_convert_f_to_x_or_z(((a.nil? ? 0 : a) / 500.0) + f1) * Conversions::D50[0],
(if lightness > Utils::LAB_KAPPA * Utils::LAB_EPSILON
(((lightness + 16) / 116.0)**3)
else
lightness / Utils::LAB_KAPPA
end) * Conversions::D50[1],
_convert_f_to_x_or_z(f1 - ((b.nil? ? 0 : b) / 200.0)) * Conversions::D50[2],
alpha,
missing_lightness:,
missing_chroma:,
missing_hue:,
missing_a: a.nil?,
missing_b: b.nil?
)
end
end
private
def _convert_f_to_x_or_z(component)
cubed = component**3
cubed > Utils::LAB_EPSILON ? cubed : ((116 * component) - 16) / Utils::LAB_KAPPA
end
end
private_constant :Lab
LAB = Lab.new
end
end
end
end
@@ -0,0 +1,53 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/lch.dart
class Lch
include Space
def bounded?
false
end
def polar?
true
end
def initialize
super('lch', [
LinearChannel.new('lightness', 0, 100, lower_clamped: true, upper_clamped: true).freeze,
LinearChannel.new('chroma', 0, 150, lower_clamped: true).freeze,
Utils::HUE_CHANNEL
].freeze)
end
def convert(dest, lightness, chroma, hue, alpha)
missing_chroma = chroma.nil?
missing_hue = hue.nil?
chroma = 0 if missing_chroma
hue = 0 if missing_hue
hue_radians = hue * Math::PI / 180
LAB.convert(
dest,
lightness,
chroma * Math.cos(hue_radians),
chroma * Math.sin(hue_radians),
alpha,
missing_chroma:,
missing_hue:
)
end
end
private_constant :Lch
LCH = Lch.new
end
end
end
end
@@ -0,0 +1,146 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/lms.dart
class Lms
include Space
def bounded?
false
end
def initialize
super('lms', [
LinearChannel.new('long', 0, 1).freeze,
LinearChannel.new('medium', 0, 1).freeze,
LinearChannel.new('short', 0, 1).freeze
].freeze)
end
def convert(dest, long, medium, short, alpha,
missing_lightness: false,
missing_chroma: false,
missing_hue: false,
missing_a: false,
missing_b: false)
if missing_a && missing_b
missing_chroma = true
missing_hue = true
elsif missing_chroma && missing_hue
missing_a = true
missing_b = true
end
if (missing_lightness && missing_chroma && missing_hue) || (long.nil? && medium.nil? && short.nil?)
return Color.send(:_for_space,
dest,
nil,
nil,
nil,
alpha)
end
case dest
when OKLAB
long_scaled = Math.cbrt(long.nil? ? 0 : long)
medium_scaled = Math.cbrt(medium.nil? ? 0 : medium)
short_scaled = Math.cbrt(short.nil? ? 0 : short)
Color.send(
:_for_space,
dest,
unless missing_lightness
(Conversions::LMS_TO_OKLAB[0] * long_scaled) +
(Conversions::LMS_TO_OKLAB[1] * medium_scaled) +
(Conversions::LMS_TO_OKLAB[2] * short_scaled)
end,
unless missing_a
(Conversions::LMS_TO_OKLAB[3] * long_scaled) +
(Conversions::LMS_TO_OKLAB[4] * medium_scaled) +
(Conversions::LMS_TO_OKLAB[5] * short_scaled)
end,
unless missing_b
(Conversions::LMS_TO_OKLAB[6] * long_scaled) +
(Conversions::LMS_TO_OKLAB[7] * medium_scaled) +
(Conversions::LMS_TO_OKLAB[8] * short_scaled)
end,
alpha
)
when OKLCH
long_scaled = Math.cbrt(long.nil? ? 0 : long)
medium_scaled = Math.cbrt(medium.nil? ? 0 : medium)
short_scaled = Math.cbrt(short.nil? ? 0 : short)
Utils.lab_to_lch(
dest,
unless missing_lightness
(Conversions::LMS_TO_OKLAB[0] * long_scaled) +
(Conversions::LMS_TO_OKLAB[1] * medium_scaled) +
(Conversions::LMS_TO_OKLAB[2] * short_scaled)
end,
unless missing_a
(Conversions::LMS_TO_OKLAB[3] * long_scaled) +
(Conversions::LMS_TO_OKLAB[4] * medium_scaled) +
(Conversions::LMS_TO_OKLAB[5] * short_scaled)
end,
unless missing_b
(Conversions::LMS_TO_OKLAB[6] * long_scaled) +
(Conversions::LMS_TO_OKLAB[7] * medium_scaled) +
(Conversions::LMS_TO_OKLAB[8] * short_scaled)
end,
alpha
)
else
convert_linear(dest, long, medium, short, alpha,
missing_lightness:,
missing_chroma:,
missing_hue:,
missing_a:,
missing_b:)
end
end
def to_linear(channel)
channel
end
def from_linear(channel)
channel
end
private
def transformation_matrix(dest)
case dest
when A98_RGB
Conversions::LMS_TO_LINEAR_A98_RGB
when DISPLAY_P3, DISPLAY_P3_LINEAR
Conversions::LMS_TO_LINEAR_DISPLAY_P3
when PROPHOTO_RGB
Conversions::LMS_TO_LINEAR_PROPHOTO_RGB
when REC2020
Conversions::LMS_TO_LINEAR_REC2020
when RGB, SRGB, SRGB_LINEAR
Conversions::LMS_TO_LINEAR_SRGB
when XYZ_D50
Conversions::LMS_TO_XYZ_D50
when XYZ_D65
Conversions::LMS_TO_XYZ_D65
else
super
end
end
end
private_constant :Lms
LMS = Lms.new
private_constant :LMS
end
end
end
end
@@ -0,0 +1,71 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/oklab.dart
class Oklab
include Space
def bounded?
false
end
def initialize
super('oklab', [
LinearChannel.new('lightness', 0, 1,
conventionally_percent: true, lower_clamped: true, upper_clamped: true).freeze,
LinearChannel.new('a', -0.4, 0.4).freeze,
LinearChannel.new('b', -0.4, 0.4).freeze
].freeze)
end
def convert(dest, lightness, a, b, alpha, # rubocop:disable Naming/MethodParameterName
missing_chroma: false, missing_hue: false)
return Utils.lab_to_lch(dest, lightness, a, b, alpha, missing_chroma:, missing_hue:) if dest == OKLCH
if a.nil? && b.nil?
missing_chroma = true
missing_hue = true
elsif missing_chroma && missing_hue
a = nil
b = nil
end
missing_lightness = lightness.nil?
missing_a = a.nil?
missing_b = b.nil?
lightness = 0 if missing_lightness
a = 0 if missing_a
b = 0 if missing_b
LMS.convert(
dest,
((Conversions::OKLAB_TO_LMS[0] * lightness) +
(Conversions::OKLAB_TO_LMS[1] * a) +
(Conversions::OKLAB_TO_LMS[2] * b))**3,
((Conversions::OKLAB_TO_LMS[3] * lightness) +
(Conversions::OKLAB_TO_LMS[4] * a) +
(Conversions::OKLAB_TO_LMS[5] * b))**3,
((Conversions::OKLAB_TO_LMS[6] * lightness) +
(Conversions::OKLAB_TO_LMS[7] * a) +
(Conversions::OKLAB_TO_LMS[8] * b))**3,
alpha,
missing_lightness:,
missing_chroma:,
missing_hue:,
missing_a:,
missing_b:
)
end
end
private_constant :Oklab
OKLAB = Oklab.new
end
end
end
end
@@ -0,0 +1,54 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/oklch.dart
class Oklch
include Space
def bounded?
false
end
def polar?
true
end
def initialize
super('oklch', [
LinearChannel.new('lightness', 0, 1,
conventionally_percent: true, lower_clamped: true, upper_clamped: true).freeze,
LinearChannel.new('chroma', 0, 0.4, lower_clamped: true).freeze,
Utils::HUE_CHANNEL
].freeze)
end
def convert(dest, lightness, chroma, hue, alpha)
missing_chroma = chroma.nil?
missing_hue = hue.nil?
chroma = 0 if missing_chroma
hue = 0 if missing_hue
hue_radians = hue * Math::PI / 180
OKLAB.convert(
dest,
lightness,
chroma * Math.cos(hue_radians),
chroma * Math.sin(hue_radians),
alpha,
missing_chroma:,
missing_hue:
)
end
end
private_constant :Oklch
OKLCH = Oklch.new
end
end
end
end
@@ -0,0 +1,59 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/prophoto_rgb.dart
class ProphotoRgb
include Space
def bounded?
true
end
def initialize
super('prophoto-rgb', Utils::RGB_CHANNELS)
end
def to_linear(channel)
abs = channel.abs
abs <= 16 / 512.0 ? channel / 16.0 : (channel <=> 0) * (abs**1.8)
end
def from_linear(channel)
abs = channel.abs
abs >= 1 / 512.0 ? (channel <=> 0) * (abs**(1 / 1.8)) : 16 * channel
end
private
def transformation_matrix(dest)
case dest
when A98_RGB
Conversions::LINEAR_PROPHOTO_RGB_TO_LINEAR_A98_RGB
when DISPLAY_P3, DISPLAY_P3_LINEAR
Conversions::LINEAR_PROPHOTO_RGB_TO_LINEAR_DISPLAY_P3
when LMS
Conversions::LINEAR_PROPHOTO_RGB_TO_LMS
when REC2020
Conversions::LINEAR_PROPHOTO_RGB_TO_LINEAR_REC2020
when RGB, SRGB, SRGB_LINEAR
Conversions::LINEAR_PROPHOTO_RGB_TO_LINEAR_SRGB
when XYZ_D50
Conversions::LINEAR_PROPHOTO_RGB_TO_XYZ_D50
when XYZ_D65
Conversions::LINEAR_PROPHOTO_RGB_TO_XYZ_D65
else
super
end
end
end
private_constant :ProphotoRgb
PROPHOTO_RGB = ProphotoRgb.new
end
end
end
end
@@ -0,0 +1,57 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/rec2020.dart
class Rec2020
include Space
def bounded?
true
end
def initialize
super('rec2020', Utils::RGB_CHANNELS)
end
def to_linear(channel)
(channel <=> 0) * (channel.abs**2.4)
end
def from_linear(channel)
(channel <=> 0) * (channel.abs**(1 / 2.4))
end
private
def transformation_matrix(dest)
case dest
when A98_RGB
Conversions::LINEAR_REC2020_TO_LINEAR_A98_RGB
when DISPLAY_P3, DISPLAY_P3_LINEAR
Conversions::LINEAR_REC2020_TO_LINEAR_DISPLAY_P3
when LMS
Conversions::LINEAR_REC2020_TO_LMS
when PROPHOTO_RGB
Conversions::LINEAR_REC2020_TO_LINEAR_PROPHOTO_RGB
when RGB, SRGB, SRGB_LINEAR
Conversions::LINEAR_REC2020_TO_LINEAR_SRGB
when XYZ_D50
Conversions::LINEAR_REC2020_TO_XYZ_D50
when XYZ_D65
Conversions::LINEAR_REC2020_TO_XYZ_D65
else
super
end
end
end
private_constant :Rec2020
REC2020 = Rec2020.new
end
end
end
end
@@ -0,0 +1,52 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/rgb.dart
class Rgb
include Space
def bounded?
true
end
def legacy?
true
end
def initialize
super('rgb', [
LinearChannel.new('red', 0, 255, lower_clamped: true, upper_clamped: true).freeze,
LinearChannel.new('green', 0, 255, lower_clamped: true, upper_clamped: true).freeze,
LinearChannel.new('blue', 0, 255, lower_clamped: true, upper_clamped: true).freeze
].freeze)
end
def convert(dest, red, green, blue, alpha)
SRGB.convert(
dest,
red.nil? ? nil : red / 255.0,
green.nil? ? nil : green / 255.0,
blue.nil? ? nil : blue / 255.0,
alpha
)
end
def to_linear(channel)
Utils.srgb_and_display_p3_to_linear(channel / 255.0)
end
def from_linear(channel)
Utils.srgb_and_display_p3_from_linear(channel) * 255
end
end
private_constant :Rgb
RGB = Rgb.new
end
end
end
end
@@ -0,0 +1,145 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/srgb.dart
class Srgb
include Space
def bounded?
true
end
def initialize
super('srgb', Utils::RGB_CHANNELS)
end
def convert(dest, red, green, blue, alpha,
missing_lightness: false,
missing_chroma: false,
missing_hue: false)
if (red.nil? && green.nil? && blue.nil?) ||
(missing_lightness && missing_chroma && missing_hue)
return Color.send(:for_space, dest, nil, nil, nil, alpha)
end
case dest
when HSL, HWB
red = 0 if red.nil?
green = 0 if green.nil?
blue = 0 if blue.nil?
min, max = [red, green, blue].minmax
delta = max - min
hue = if max == min
0.0
elsif max == red
(60.0 * (green - blue) / delta) + 360
elsif max == green
(60.0 * (blue - red) / delta) + 120
else # max == blue
(60.0 * (red - green) / delta) + 240
end
if dest == HSL
lightness = (min + max) / 2.0
saturation = if [0, 1].include?(lightness)
0.0
else
100.0 * (max - lightness) / [lightness, 1 - lightness].min
end
if saturation.negative?
hue += 180
saturation = saturation.abs
end
Color.send(
:for_space_internal,
dest,
missing_hue || FuzzyMath.equals?(saturation, 0) ? nil : hue % 360,
missing_chroma ? nil : saturation,
missing_lightness ? nil : lightness * 100,
alpha
)
else
whiteness = min * 100
blackness = 100 - (max * 100)
Color.send(
:for_space_internal,
dest,
missing_hue || FuzzyMath.greater_than_or_equals?(whiteness + blackness, 100) ? nil : hue % 360,
missing_chroma && missing_lightness ? nil : whiteness,
missing_chroma && missing_lightness ? nil : blackness,
alpha
)
end
when RGB
Color.send(
:_for_space,
dest,
red.nil? ? nil : red * 255,
green.nil? ? nil : green * 255,
blue.nil? ? nil : blue * 255,
alpha
)
when SRGB_LINEAR
Color.send(
:_for_space,
dest,
red.nil? ? nil : to_linear(red),
green.nil? ? nil : to_linear(green),
blue.nil? ? nil : to_linear(blue),
alpha
)
else
convert_linear(dest, red, green, blue, alpha,
missing_lightness:,
missing_chroma:,
missing_hue:)
end
end
def to_linear(channel)
Utils.srgb_and_display_p3_to_linear(channel)
end
def from_linear(channel)
Utils.srgb_and_display_p3_from_linear(channel)
end
private
def transformation_matrix(dest)
case dest
when A98_RGB
Conversions::LINEAR_SRGB_TO_LINEAR_A98_RGB
when DISPLAY_P3, DISPLAY_P3_LINEAR
Conversions::LINEAR_SRGB_TO_LINEAR_DISPLAY_P3
when LMS
Conversions::LINEAR_SRGB_TO_LMS
when PROPHOTO_RGB
Conversions::LINEAR_SRGB_TO_LINEAR_PROPHOTO_RGB
when REC2020
Conversions::LINEAR_SRGB_TO_LINEAR_REC2020
when XYZ_D50
Conversions::LINEAR_SRGB_TO_XYZ_D50
when XYZ_D65
Conversions::LINEAR_SRGB_TO_XYZ_D65
else
super
end
end
end
private_constant :Srgb
SRGB = Srgb.new
end
end
end
end
@@ -0,0 +1,72 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/srgb_linear.dart
class SrgbLinear
include Space
def bounded?
true
end
def initialize
super('srgb-linear', Utils::RGB_CHANNELS)
end
def convert(dest, red, green, blue, alpha)
case dest
when HSL, HWB, RGB, SRGB
SRGB.convert(
dest,
red.nil? ? nil : Utils.srgb_and_display_p3_from_linear(red),
green.nil? ? nil : Utils.srgb_and_display_p3_from_linear(green),
blue.nil? ? nil : Utils.srgb_and_display_p3_from_linear(blue),
alpha
)
else
super
end
end
def to_linear(channel)
channel
end
def from_linear(channel)
channel
end
private
def transformation_matrix(dest)
case dest
when A98_RGB
Conversions::LINEAR_SRGB_TO_LINEAR_A98_RGB
when DISPLAY_P3, DISPLAY_P3_LINEAR
Conversions::LINEAR_SRGB_TO_LINEAR_DISPLAY_P3
when PROPHOTO_RGB
Conversions::LINEAR_SRGB_TO_LINEAR_PROPHOTO_RGB
when REC2020
Conversions::LINEAR_SRGB_TO_LINEAR_REC2020
when XYZ_D65
Conversions::LINEAR_SRGB_TO_XYZ_D65
when XYZ_D50
Conversions::LINEAR_SRGB_TO_XYZ_D50
when LMS
Conversions::LINEAR_SRGB_TO_LMS
else
super
end
end
end
private_constant :SrgbLinear
SRGB_LINEAR = SrgbLinear.new
end
end
end
end
@@ -0,0 +1,89 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/utils.dart
module Utils
module_function
# A constant used to convert Lab to/from XYZ.
LAB_KAPPA = Rational(24_389, 27) # 29^3/3^3
# A constant used to convert Lab to/from XYZ.
LAB_EPSILON = Rational(216, 24_389) # 6^3/29^3
# The hue channel shared across all polar color spaces.
HUE_CHANNEL = ColorChannel.new('hue', polar_angle: true, associated_unit: 'deg').freeze
# The color channels shared across all RGB color spaces (except the legacy RGB space).
RGB_CHANNELS = [
LinearChannel.new('red', 0, 1).freeze,
LinearChannel.new('green', 0, 1).freeze,
LinearChannel.new('blue', 0, 1).freeze
].freeze
# The color channels shared across both XYZ color spaces.
XYZ_CHANNELS = [
LinearChannel.new('x', 0, 1).freeze,
LinearChannel.new('y', 0, 1).freeze,
LinearChannel.new('z', 0, 1).freeze
].freeze
# The algorithm for converting a single `srgb` or `display-p3` channel to
# linear-light form.
# @param channel [Numeric]
# @return [Numeric]
def srgb_and_display_p3_to_linear(channel)
abs = channel.abs
abs <= 0.04045 ? channel / 12.92 : (channel <=> 0) * (((abs + 0.055) / 1.055)**2.4)
end
# The algorithm for converting a single `srgb` or `display-p3` channel to
# gamma-corrected form.
# @param channel [Numeric]
# @return [Numeric]
def srgb_and_display_p3_from_linear(channel)
abs = channel.abs
abs <= 0.0031308 ? channel * 12.92 : (channel <=> 0) * ((1.055 * (abs**(1 / 2.4))) - 0.055)
end
# Converts a Lab or OKLab color to LCH or OKLCH, respectively.
#
# The [missing_chroma] and [missing_hue] arguments indicate whether this came
# from a color that was missing its chroma or hue channels, respectively.
# @param dest [Space]
# @param lightness [Numeric]
# @param a [Numeric]
# @param b [Numeric]
# @param alpha [Numeric]
# @return [Color]
def lab_to_lch(dest, lightness, a, b, alpha, # rubocop:disable Naming/MethodParameterName
missing_chroma: false, missing_hue: false)
missing_chroma ||= a.nil? && b.nil?
missing_hue ||= a.nil? && b.nil?
chroma = Math.sqrt(((a.nil? ? 0 : a)**2) + ((b.nil? ? 0 : b)**2))
hue = if missing_hue || FuzzyMath.equals?(chroma, 0)
nil
else
Math.atan2(b.nil? ? 0 : b, a.nil? ? 0 : a) * 180 / Math::PI
end
Color.send(
:for_space_internal,
dest,
lightness,
missing_chroma ? nil : chroma,
hue.nil? || hue >= 0 ? hue : hue + 360,
alpha
)
end
end
private_constant :Utils
end
end
end
end
@@ -0,0 +1,113 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/xyz_d50.dart
class XyzD50
include Space
def bounded?
false
end
def initialize
super('xyz-d50', Utils::XYZ_CHANNELS)
end
def convert(dest, x, y, z, alpha, # rubocop:disable Naming/MethodParameterName
missing_lightness: false,
missing_chroma: false,
missing_hue: false,
missing_a: false,
missing_b: false)
if missing_a && missing_b
missing_chroma = true
missing_hue = true
elsif missing_chroma && missing_hue
missing_a = true
missing_b = true
end
if (missing_lightness && missing_chroma && missing_hue) ||
(x.nil? && y.nil? && z.nil?)
return Color.send(:_for_space, dest, nil, nil, nil, alpha)
end
case dest
when LAB, LCH
f0 = _convert_component_to_lab_f((x.nil? ? 0 : x) / Conversions::D50[0])
f1 = _convert_component_to_lab_f((y.nil? ? 0 : y) / Conversions::D50[1])
f2 = _convert_component_to_lab_f((z.nil? ? 0 : z) / Conversions::D50[2])
lightness = missing_lightness ? nil : (116 * f1) - 16
a = 500 * (f0 - f1)
b = 200 * (f1 - f2)
if dest == LAB
Color.send(:_for_space,
dest,
lightness,
missing_a ? nil : a,
missing_b ? nil : b,
alpha)
else
Utils.lab_to_lch(dest, lightness, a, b, alpha, missing_chroma:, missing_hue:)
end
else
convert_linear(dest, x, y, z, alpha,
missing_lightness:,
missing_chroma:,
missing_hue:,
missing_a:,
missing_b:)
end
end
def to_linear(channel)
channel
end
def from_linear(channel)
channel
end
private
def _convert_component_to_lab_f(component)
if component > Utils::LAB_EPSILON
Math.cbrt(component)
else
((Utils::LAB_KAPPA * component) + 16) / 116.0
end
end
def transformation_matrix(dest)
case dest
when A98_RGB
Conversions::XYZ_D50_TO_LINEAR_A98_RGB
when DISPLAY_P3, DISPLAY_P3_LINEAR
Conversions::XYZ_D50_TO_LINEAR_DISPLAY_P3
when LMS
Conversions::XYZ_D50_TO_LMS
when PROPHOTO_RGB
Conversions::XYZ_D50_TO_LINEAR_PROPHOTO_RGB
when REC2020
Conversions::XYZ_D50_TO_LINEAR_REC2020
when RGB, SRGB, SRGB_LINEAR
Conversions::XYZ_D50_TO_LINEAR_SRGB
when XYZ_D65
Conversions::XYZ_D50_TO_XYZ_D65
else
super
end
end
end
private_constant :XyzD50
XYZ_D50 = XyzD50.new
end
end
end
end
@@ -0,0 +1,57 @@
# frozen_string_literal: true
module Sass
module Value
class Color
module Space
# @see https://github.com/sass/dart-sass/blob/main/lib/src/value/color/space/xyz_d65.dart
class XyzD65
include Space
def bounded?
false
end
def initialize
super('xyz', Utils::XYZ_CHANNELS)
end
def to_linear(channel)
channel
end
def from_linear(channel)
channel
end
private
def transformation_matrix(dest)
case dest
when A98_RGB
Conversions::XYZ_D65_TO_LINEAR_A98_RGB
when DISPLAY_P3, DISPLAY_P3_LINEAR
Conversions::XYZ_D65_TO_LINEAR_DISPLAY_P3
when LMS
Conversions::XYZ_D65_TO_LMS
when PROPHOTO_RGB
Conversions::XYZ_D65_TO_LINEAR_PROPHOTO_RGB
when REC2020
Conversions::XYZ_D65_TO_LINEAR_REC2020
when RGB, SRGB, SRGB_LINEAR
Conversions::XYZ_D65_TO_LINEAR_SRGB
when XYZ_D50
Conversions::XYZ_D65_TO_XYZ_D50
else
super
end
end
end
private_constant :XyzD65
XYZ_D65 = XyzD65.new
end
end
end
end
@@ -0,0 +1,52 @@
# frozen_string_literal: true
module Sass
module Value
# Sass's function type.
#
# @see https://sass-lang.com/documentation/js-api/classes/sassfunction/
class Function
include Value
# @param signature [::String]
# @param callback [Proc]
def initialize(signature, &callback)
@signature = signature.freeze
@callback = callback.freeze
end
# @return [Object, nil]
protected attr_reader :compile_context
# @return [Integer, nil]
protected attr_reader :id
# @return [::String, nil]
attr_reader :signature
# @return [Proc, nil]
attr_reader :callback
# @return [::Boolean]
def ==(other)
return false unless other.is_a?(Sass::Value::Function)
if defined?(@id)
other.compile_context == compile_context && other.id == id
else
other.signature == signature && other.callback == callback
end
end
# @return [Integer]
def hash
@hash ||= defined?(@id) ? [compile_context, id].hash : [signature, callback].hash
end
# @return [Function]
def assert_function(_name = nil)
self
end
end
end
end
@@ -0,0 +1,84 @@
# frozen_string_literal: true
module Sass
module Value
# Sass's {FuzzyMath} module.
module FuzzyMath
PRECISION = 10
EPSILON = 10**(-PRECISION - 1)
INVERSE_EPSILON = 10**(PRECISION + 1)
module_function
def equals?(number1, number2)
return true if number1 == number2
(number1 - number2).abs <= EPSILON &&
(number1 * INVERSE_EPSILON).round ==
(number2 * INVERSE_EPSILON).round
end
def equals_nilable?(number1, number2)
return true if number1 == number2
return false if number1.nil? || number2.nil?
(number1 - number2).abs <= EPSILON &&
(number1 * INVERSE_EPSILON).round ==
(number2 * INVERSE_EPSILON).round
end
def less_than?(number1, number2)
number1 < number2 && !equals?(number1, number2)
end
def less_than_or_equals?(number1, number2)
number1 < number2 || equals?(number1, number2)
end
def greater_than?(number1, number2)
number1 > number2 && !equals?(number1, number2)
end
def greater_than_or_equals?(number1, number2)
number1 > number2 || equals?(number1, number2)
end
def integer?(number)
return false unless number.finite?
equals?(number, number.round)
end
def to_i(number)
integer?(number) ? number.round : nil
end
def between(number, min, max)
return min if equals?(number, min)
return max if equals?(number, max)
return number if number > min && number < max
nil
end
def assert_between(number, min, max, name)
result = between(number, min, max)
return result unless result.nil?
raise Sass::ScriptError.new("#{number} must be between #{min} and #{max}.", name)
end
def _clamp_like_css(number, lower_bound, upper_bound)
number.to_f.nan? ? lower_bound : number.clamp(lower_bound, upper_bound)
end
def _hash(number)
number&.finite? ? (number * INVERSE_EPSILON).round : number
end
end
private_constant :FuzzyMath
end
end
@@ -0,0 +1,79 @@
# frozen_string_literal: true
module Sass
module Value
# Sass's list type.
#
# @see https://sass-lang.com/documentation/js-api/classes/sasslist/
class List
include Value
# @param contents [Array<Value>]
# @param separator [::String]
# @param bracketed [::Boolean]
def initialize(contents = [], separator: ',', bracketed: false)
if separator.nil? && contents.length > 1
raise Sass::ScriptError, 'A list with more than one element must have an explicit separator'
end
@contents = contents.freeze
@separator = separator.freeze
@bracketed = bracketed.freeze
end
# @return [::String, nil]
attr_reader :separator
# @return [::Boolean]
def bracketed?
@bracketed
end
# @return [::Boolean]
def ==(other)
(other.is_a?(Sass::Value::List) &&
other.to_a == to_a &&
other.separator == separator &&
other.bracketed? == bracketed?) ||
(to_a.empty? && other.is_a?(Sass::Value::Map) && other.to_a.empty?)
end
# @param index [Numeric]
# @return [Value]
def at(index)
index = index.floor
index = to_a.length + index if index.negative?
return nil if index.negative? || index >= to_a.length
to_a[index]
end
# @return [Integer]
def hash
@hash ||= contents.hash
end
# @return [Array<Value>]
def to_a
@contents
end
# @return [Map, nil]
def to_map
to_a.empty? ? Sass::Value::Map.new({}) : nil
end
# @return [Map]
# @raise [ScriptError]
def assert_map(name = nil)
to_a.empty? ? Sass::Value::Map.new({}) : super.assert_map(name)
end
private
def to_a_length
to_a.length
end
end
end
end
@@ -0,0 +1,71 @@
# frozen_string_literal: true
module Sass
module Value
# Sass's map type.
#
# @see https://sass-lang.com/documentation/js-api/classes/sassmap/
class Map
include Value
# @param contents [Hash<Value, Value>]
def initialize(contents = {})
@contents = contents.freeze
end
# @return [Hash<Value, Value>]
attr_reader :contents
# @return [::String, nil]
def separator
contents.empty? ? nil : ','
end
# @return [::Boolean]
def ==(other)
(other.is_a?(Sass::Value::Map) && other.contents == contents) ||
(contents.empty? && other.is_a?(Sass::Value::List) && other.to_a.empty?)
end
# @param index [Numeric, Value]
# @return [List<(Value, Value)>, Value]
def at(index)
if index.is_a?(Numeric)
index = index.floor
index = to_a_length + index if index.negative?
return nil if index.negative? || index >= to_a_length
Sass::Value::List.new(contents.to_a[index], separator: ' ')
else
contents[index]
end
end
# @return [Integer]
def hash
@hash ||= contents.hash
end
# @return [Array<List<(Value, Value)>>]
def to_a
contents.map { |key, value| Sass::Value::List.new([key, value], separator: ' ') }
end
# @return [Map]
def to_map
self
end
# @return [Map]
def assert_map(_name = nil)
self
end
private
def to_a_length
contents.length
end
end
end
end
@@ -0,0 +1,37 @@
# frozen_string_literal: true
module Sass
module Value
# Sass's mixin type.
#
# @see https://sass-lang.com/documentation/js-api/classes/sassmixin/
class Mixin
include Value
class << self
private :new
end
# @return [Object]
protected attr_reader :compile_context
# @return [Integer]
protected attr_reader :id
# @return [::Boolean]
def ==(other)
other.is_a?(Sass::Value::Mixin) && other.compile_context == compile_context && other.id == id
end
# @return [Integer]
def hash
@hash ||= [compile_context, id].hash
end
# @return [Mixin]
def assert_mixin(_name = nil)
self
end
end
end
end
@@ -0,0 +1,48 @@
# frozen_string_literal: true
module Sass
module Value
# Sass's null type.
#
# @see https://sass-lang.com/documentation/js-api/variables/sassnull/
class Null
include Value
def initialize
@value = nil
end
# @return [nil]
attr_reader :value
# @return [Boolean]
def !
Boolean::TRUE
end
# @return [::Boolean]
def ==(other)
other.is_a?(Sass::Value::Null)
end
# @return [Integer]
def hash
@hash ||= value.hash
end
# @return [::Boolean]
def to_bool # rubocop:disable Naming/PredicateMethod
false
end
alias to_nil value
# Sass's null value.
NULL = Null.new
def self.new
NULL
end
end
end
end
@@ -0,0 +1,364 @@
# frozen_string_literal: true
require_relative 'number/unit'
module Sass
module Value
# Sass's number type.
#
# @see https://sass-lang.com/documentation/js-api/classes/sassnumber/
class Number
include Value
include CalculationValue
# @param value [Numeric]
# @param unit [::String, Hash]
# @option unit [Array<::String>] :numerator_units
# @option unit [Array<::String>] :denominator_units
def initialize(value, unit = nil)
case unit
when nil
numerator_units = []
denominator_units = []
when ::String
numerator_units = [unit]
denominator_units = []
when ::Hash
numerator_units = unit.fetch(:numerator_units, [])
unless numerator_units.is_a?(::Array)
raise Sass::ScriptError, "invalid numerator_units #{numerator_units.inspect}"
end
denominator_units = unit.fetch(:denominator_units, [])
unless denominator_units.is_a?(::Array)
raise Sass::ScriptError, "invalid denominator_units #{denominator_units.inspect}"
end
else
raise Sass::ScriptError, "invalid unit #{unit.inspect}"
end
unless denominator_units.empty? && numerator_units.empty?
value = value.dup
numerator_units = numerator_units.dup
new_denominator_units = []
denominator_units.each do |denominator_unit|
index = numerator_units.find_index do |numerator_unit|
factor = Unit.conversion_factor(denominator_unit, numerator_unit)
if factor.nil?
false
else
value *= factor
true
end
end
if index.nil?
new_denominator_units.push(denominator_unit)
else
numerator_units.delete_at(index)
end
end
denominator_units = new_denominator_units
end
@value = value.freeze
@numerator_units = numerator_units.each(&:freeze).freeze
@denominator_units = denominator_units.each(&:freeze).freeze
end
# @return [Numeric]
attr_reader :value
# @return [Array<::String>]
attr_reader :numerator_units, :denominator_units
# @return [::Boolean]
def ==(other)
return false unless other.is_a?(Sass::Value::Number)
return false if numerator_units.length != other.numerator_units.length ||
denominator_units.length != other.denominator_units.length
return FuzzyMath.equals?(value, other.value) if unitless?
if Unit.canonicalize_units(numerator_units) != Unit.canonicalize_units(other.numerator_units) &&
Unit.canonicalize_units(denominator_units) != Unit.canonicalize_units(other.denominator_units)
return false
end
FuzzyMath.equals?(
value *
Unit.canonical_multiplier(numerator_units) /
Unit.canonical_multiplier(denominator_units),
other.value *
Unit.canonical_multiplier(other.numerator_units) /
Unit.canonical_multiplier(other.denominator_units)
)
end
# @return [Integer]
def hash
@hash ||= FuzzyMath._hash(canonical_units_value).hash
end
# @return [::Boolean]
def unitless?
numerator_units.empty? && denominator_units.empty?
end
# @return [Number]
# @raise [ScriptError]
def assert_unitless(name = nil)
raise Sass::ScriptError.new("Expected #{self} to have no units", name) unless unitless?
self
end
# @return [::Boolean]
def units?
!unitless?
end
# @param unit [::String]
# @return [::Boolean]
def unit?(unit)
single_unit? && numerator_units.first == unit
end
# @param unit [::String]
# @return [Number]
# @raise [ScriptError]
def assert_unit(unit, name = nil)
raise Sass::ScriptError.new("Expected #{self} to have unit #{unit.inspect}", name) unless unit?(unit)
self
end
# @return [::Boolean]
def integer?
FuzzyMath.integer?(value)
end
# @return [Integer]
# @raise [ScriptError]
def assert_integer(name = nil)
raise Sass::ScriptError.new("#{self} is not an integer", name) unless integer?
to_i
end
# @return [Integer]
def to_i
FuzzyMath.to_i(value)
end
# @param min [Numeric]
# @param max [Numeric]
# @return [Numeric]
# @raise [ScriptError]
def assert_between(min, max, name = nil)
FuzzyMath.assert_between(value, min, max, name)
end
# @param unit [::String]
# @return [::Boolean]
def compatible_with_unit?(unit)
single_unit? && !Unit.conversion_factor(numerator_units.first, unit).nil?
end
# @param new_numerator_units [Array<::String>]
# @param new_denominator_units [Array<::String>]
# @return [Number]
def convert(new_numerator_units, new_denominator_units, name = nil)
Number.new(convert_value(new_numerator_units, new_denominator_units, name), {
numerator_units: new_numerator_units,
denominator_units: new_denominator_units
})
end
# @param new_numerator_units [Array<::String>]
# @param new_denominator_units [Array<::String>]
# @return [Numeric]
def convert_value(new_numerator_units, new_denominator_units, name = nil)
coerce_or_convert_value(new_numerator_units, new_denominator_units,
coerce_unitless: false,
name:)
end
# @param other [Number]
# @return [Number]
def convert_to_match(other, name = nil, other_name = nil)
Number.new(convert_value_to_match(other, name, other_name), {
numerator_units: other.numerator_units,
denominator_units: other.denominator_units
})
end
# @param other [Number]
# @return [Numeric]
def convert_value_to_match(other, name = nil, other_name = nil)
coerce_or_convert_value(other.numerator_units, other.denominator_units,
coerce_unitless: false,
name:,
other:,
other_name:)
end
# @param new_numerator_units [Array<::String>]
# @param new_denominator_units [Array<::String>]
# @return [Number]
def coerce(new_numerator_units, new_denominator_units, name = nil)
Number.new(coerce_value(new_numerator_units, new_denominator_units, name), {
numerator_units: new_numerator_units,
denominator_units: new_denominator_units
})
end
# @param new_numerator_units [Array<::String>]
# @param new_denominator_units [Array<::String>]
# @return [Numeric]
def coerce_value(new_numerator_units, new_denominator_units, name = nil)
coerce_or_convert_value(new_numerator_units, new_denominator_units,
coerce_unitless: true,
name:)
end
# @param unit [::String]
# @return [Numeric]
def coerce_value_to_unit(unit, name = nil)
coerce_value([unit], [], name)
end
# @param other [Number]
# @return [Number]
def coerce_to_match(other, name = nil, other_name = nil)
Number.new(coerce_value_to_match(other, name, other_name), {
numerator_units: other.numerator_units,
denominator_units: other.denominator_units
})
end
# @param other [Number]
# @return [Numeric]
def coerce_value_to_match(other, name = nil, other_name = nil)
coerce_or_convert_value(other.numerator_units, other.denominator_units,
coerce_unitless: true,
name:,
other:,
other_name:)
end
# @return [Number]
def assert_number(_name = nil)
self
end
private
def single_unit?
numerator_units.length == 1 && denominator_units.empty?
end
def canonical_units_value
if unitless?
value
elsif single_unit?
value * Unit.canonical_multiplier_for_unit(numerator_units.first)
else
value * Unit.canonical_multiplier(numerator_units) / Unit.canonical_multiplier(denominator_units)
end
end
def coerce_or_convert_value(new_numerator_units, new_denominator_units,
coerce_unitless:,
name: nil,
other: nil,
other_name: nil)
unless other.nil? ||
(other.numerator_units == new_numerator_units && other.denominator_units == new_denominator_units)
raise Sass::ScriptError,
"Expected #{other} to have units #{unit_string(new_numerator_units, new_denominator_units).inspect}"
end
return value if numerator_units == new_numerator_units && denominator_units == new_denominator_units
other_unitless = new_numerator_units.empty? && new_denominator_units.empty?
return value if coerce_unitless && (unitless? || other_unitless)
compatibility_error = lambda {
unless other.nil?
message = "#{self} and"
message << " $#{other_name}:" unless other_name.nil?
message << " #{other} have incompatible units"
message << " (one has units and the other doesn't)" if unitless? || other_unitless
return Sass::ScriptError.new(message, name)
end
return Sass::ScriptError.new("Expected #{self} to have no units", name) unless other_unitless
if new_numerator_units.length == 1 && new_denominator_units.empty?
type = Unit::TYPES_BY_UNIT[new_numerator_units.first]
return Sass::ScriptError.new(
"Expected #{self} to have a #{type} unit (#{Unit::UNITS_BY_TYPE[type].join(', ')})", name
)
end
unit_length = new_numerator_units.length + new_denominator_units.length
units = unit_string(new_numerator_units, new_denominator_units)
Sass::ScriptError.new("Expected #{self} to have unit#{'s' if unit_length > 1} #{units}", name)
}
result = value
old_numerator_units = numerator_units.dup
new_numerator_units.each do |new_numerator_unit|
index = old_numerator_units.find_index do |old_numerator_unit|
factor = Unit.conversion_factor(new_numerator_unit, old_numerator_unit)
if factor.nil?
false
else
result *= factor
true
end
end
raise compatibility_error.call if index.nil?
old_numerator_units.delete_at(index)
end
old_denominator_units = denominator_units.dup
new_denominator_units.each do |new_denominator_unit|
index = old_denominator_units.find_index do |old_denominator_unit|
factor = Unit.conversion_factor(new_denominator_unit, old_denominator_unit)
if factor.nil?
false
else
result /= factor
true
end
end
raise compatibility_error.call if index.nil?
old_denominator_units.delete_at(index)
end
raise compatibility_error.call unless old_numerator_units.empty? && old_denominator_units.empty?
result
end
def unit_string(numerator_units, denominator_units)
if numerator_units.empty?
return 'no units' if denominator_units.empty?
return denominator_units.length == 1 ? "#{denominator_units.first}^-1" : "(#{denominator_units.join('*')})^-1"
end
return numerator_units.join('*') if denominator_units.empty?
"#{numerator_units.join('*')}/#{denominator_units.join('*')}"
end
end
end
end
@@ -0,0 +1,187 @@
# frozen_string_literal: true
module Sass
module Value
class Number
# The {Unit} module.
module Unit
CONVERSIONS = {
# Length
'in' => {
'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)
},
'cm' => {
'in' => Rational(2.54),
'cm' => Rational(1),
'pc' => Rational(2.54, 6),
'mm' => Rational(1, 10),
'q' => Rational(1, 40),
'pt' => Rational(2.54, 72),
'px' => Rational(2.54, 96)
},
'pc' => {
'in' => Rational(6),
'cm' => Rational(6, 2.54),
'pc' => Rational(1),
'mm' => Rational(6, 25.4),
'q' => Rational(6, 101.6),
'pt' => Rational(1, 12),
'px' => Rational(1, 16)
},
'mm' => {
'in' => Rational(25.4),
'cm' => Rational(10),
'pc' => Rational(25.4, 6),
'mm' => Rational(1),
'q' => Rational(1, 4),
'pt' => Rational(25.4, 72),
'px' => Rational(25.4, 96)
},
'q' => {
'in' => Rational(101.6),
'cm' => Rational(40),
'pc' => Rational(101.6, 6),
'mm' => Rational(4),
'q' => Rational(1),
'pt' => Rational(101.6, 72),
'px' => Rational(101.6, 96)
},
'pt' => {
'in' => Rational(72),
'cm' => Rational(72, 2.54),
'pc' => Rational(12),
'mm' => Rational(72, 25.4),
'q' => Rational(72, 101.6),
'pt' => Rational(1),
'px' => Rational(3, 4)
},
'px' => {
'in' => Rational(96),
'cm' => Rational(96, 2.54),
'pc' => Rational(16),
'mm' => Rational(96, 25.4),
'q' => Rational(96, 101.6),
'pt' => Rational(4, 3),
'px' => Rational(1)
},
# Rotation
'deg' => {
'deg' => Rational(1),
'grad' => Rational(9, 10),
'rad' => Rational(180, Math::PI),
'turn' => Rational(360)
},
'grad' => {
'deg' => Rational(10, 9),
'grad' => Rational(1),
'rad' => Rational(200, Math::PI),
'turn' => Rational(400)
},
'rad' => {
'deg' => Rational(Math::PI, 180),
'grad' => Rational(Math::PI, 200),
'rad' => Rational(1),
'turn' => Rational(Math::PI * 2)
},
'turn' => {
'deg' => Rational(1, 360),
'grad' => Rational(1, 400),
'rad' => Rational(1, Math::PI * 2),
'turn' => Rational(1)
},
# Time
's' => {
's' => Rational(1),
'ms' => Rational(1, 1000)
},
'ms' => {
's' => Rational(1000),
'ms' => Rational(1)
},
# Frequency
'Hz' => {
'Hz' => Rational(1),
'kHz' => Rational(1000)
},
'kHz' => {
'Hz' => Rational(1, 1000),
'kHz' => Rational(1)
},
# Pixel density
'dpi' => {
'dpi' => Rational(1),
'dpcm' => Rational(2.54),
'dppx' => Rational(96)
},
'dpcm' => {
'dpi' => Rational(1, 2.54),
'dpcm' => Rational(1),
'dppx' => Rational(96, 2.54)
},
'dppx' => {
'dpi' => Rational(1, 96),
'dpcm' => Rational(2.54, 96),
'dppx' => Rational(1)
}
}.freeze
UNITS_BY_TYPE = {
time: %w[s ms],
frequency: %w[Hz kHz],
'pixel density': %w[dpi dpcm dppx]
}.freeze
TYPES_BY_UNIT = UNITS_BY_TYPE.each_with_object({}) do |(key, values), hash|
values.each do |value|
hash[value] = key
end
end
module_function
def conversion_factor(unit1, unit2)
return 1 if unit1 == unit2
CONVERSIONS.dig(unit1, unit2)
end
def canonicalize_units(units)
return units if units.empty?
if units.length == 1
type = TYPES_BY_UNIT[units.first]
return type.nil? ? units : [UNITS_BY_TYPE[type].first]
end
units.map do |unit|
type = TYPES_BY_UNIT[unit]
type.nil? ? units : [UNITS_BY_TYPE[type].first]
end.sort
end
def canonical_multiplier(units)
units.reduce(1) do |multiplier, unit|
multiplier * canonical_multiplier_for_unit(unit)
end
end
def canonical_multiplier_for_unit(unit)
inner_map = CONVERSIONS[unit]
inner_map.nil? ? 1 : 1 / inner_map.values.first
end
end
private_constant :Unit
end
end
end
@@ -0,0 +1,61 @@
# frozen_string_literal: true
module Sass
module Value
# Sass's string type.
#
# @see https://sass-lang.com/documentation/js-api/classes/sassstring/
class String
include Value
include CalculationValue
# @param text [::String]
# @param quoted [::Boolean]
def initialize(text = '', quoted: true)
@text = text.freeze
@quoted = quoted
end
# @return [::String]
attr_reader :text
# @return [::Boolean]
def quoted?
@quoted
end
# @return [::Boolean]
def ==(other)
other.is_a?(Sass::Value::String) && other.text == text
end
# @return [Integer]
def hash
@hash ||= text.hash
end
# @return [String]
def assert_string(_name = nil)
self
end
# @param sass_index [Number]
# @return [Integer]
def sass_index_to_string_index(sass_index, name = nil)
index = sass_index.assert_number(name).assert_integer(name)
raise Sass::ScriptError.new('String index may not be 0', name) if index.zero?
if index.abs > text.length
raise Sass::ScriptError.new("Invalid index #{sass_index} for a string with #{text.length} characters", name)
end
index.negative? ? text.length + index : index - 1
end
# @return [::String]
def to_s
@quoted ? Serializer.serialize_quoted_string(@text) : Serializer.serialize_unquoted_string(@text)
end
end
end
end