This commit is contained in:
@@ -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
|
||||
|
||||
[](https://github.com/sass-contrib/sass-embedded-host-ruby/actions/workflows/build.yml)
|
||||
[](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.
|
||||
@@ -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'
|
||||
+49
@@ -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
|
||||
+88
@@ -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
|
||||
+150
@@ -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
|
||||
+77
@@ -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
|
||||
+376
@@ -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
|
||||
+46
@@ -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
BIN
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
+45
@@ -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'
|
||||
+45
@@ -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
|
||||
+94
@@ -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
|
||||
+51
@@ -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
|
||||
+72
@@ -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
|
||||
+72
@@ -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
|
||||
+59
@@ -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
|
||||
+72
@@ -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
|
||||
+113
@@ -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
|
||||
Reference in New Issue
Block a user