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,21 @@
MIT License
Copyright (c) 2025 なつき
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,48 @@
# Embedded Sass Host for Ruby
[![build](https://github.com/sass-contrib/sass-embedded-host-ruby/actions/workflows/build.yml/badge.svg)](https://github.com/sass-contrib/sass-embedded-host-ruby/actions/workflows/build.yml)
[![gem](https://badge.fury.io/rb/sass-embedded.svg)](https://rubygems.org/gems/sass-embedded)
This is a Ruby library that implements the host side of the [Embedded Sass protocol](https://github.com/sass/sass/blob/HEAD/spec/embedded-protocol.md).
It exposes a Ruby API for Sass that's backed by a native [Dart Sass](https://sass-lang.com/dart-sass) executable on [supported hardware architectures and platforms](https://dart.dev/get-dart#system-requirements) or a Node.js Dart Sass executable everywhere else.
## Install
``` sh
gem install sass-embedded
```
## Usage
The Ruby API provides two entrypoints for compiling Sass to CSS.
- `Sass.compile` takes a path to a Sass file and return the result of compiling that file to CSS.
``` ruby
require 'sass-embedded'
result = Sass.compile('style.scss')
puts result.css
compressed = Sass.compile('style.scss', style: :compressed)
puts compressed.css
```
- `Sass.compile_string` takes a string that represents the contents of a Sass file and return the result of compiling that file to CSS.
``` ruby
require 'sass-embedded'
result = Sass.compile_string('h1 { font-size: 40px; }')
puts result.css
compressed = Sass.compile_string('h1 { font-size: 40px; }', style: :compressed)
puts compressed.css
```
See [rubydoc.info/gems/sass-embedded/Sass](https://rubydoc.info/gems/sass-embedded/Sass) for full API documentation.
---
Disclaimer: this is not an official Google product.
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'sass/cli'
module Sass
# The `sass` command line interface
module CLI
Kernel.exec(*COMMAND, *ARGV)
end
private_constant :CLI
end
@@ -0,0 +1,4 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require_relative 'sass/embedded'
@@ -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