201 lines
6.0 KiB
Ruby
201 lines
6.0 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Shared value validation and frozen editor-owned configuration objects.
|
|
module Vamp
|
|
MAX_FRAME = 4 * 1024 * 1024
|
|
|
|
# Validates a boundary value and returns a UTF-8 copy.
|
|
# @param value [Object]
|
|
# @param label [String]
|
|
# @return [String]
|
|
def self.string!(value, label)
|
|
raise TypeError, "#{label} must be a String" unless value.is_a?(String)
|
|
|
|
copy = value.dup.force_encoding(Encoding::UTF_8)
|
|
raise ArgumentError, "#{label} must be valid UTF-8" unless copy.valid_encoding?
|
|
|
|
copy
|
|
end
|
|
|
|
# Recursively validates JSON values and normalizes hash keys to strings.
|
|
# @param value [Object]
|
|
# @return [Hash{String => Object}, Array<Object>, String, Integer, Float, Boolean, nil]
|
|
def self.json_value!(value)
|
|
case value
|
|
when Hash
|
|
json_hash(value)
|
|
when Array
|
|
value.map { |item| json_value!(item) }
|
|
else
|
|
json_scalar(value)
|
|
end
|
|
end
|
|
|
|
# Freezes the supplied value and every nested hash/array member in place.
|
|
# @param value [Object]
|
|
# @return [Object] the same value
|
|
def self.deep_freeze(value)
|
|
case value
|
|
when Hash
|
|
value.each do |key, item|
|
|
deep_freeze(key)
|
|
deep_freeze(item)
|
|
end
|
|
when Array
|
|
value.each { |item| deep_freeze(item) }
|
|
end
|
|
value.freeze
|
|
end
|
|
|
|
# @!override Vamp::Action#name
|
|
# @return [String] the catalog's dotted action name
|
|
Action = Struct.new(:name)
|
|
# @!override Vamp::Mode#name
|
|
# @return [String] the lowercase binding mode
|
|
Mode = Struct.new(:name)
|
|
|
|
# Action methods and nested modules come exclusively from the Rust catalog.
|
|
# @type [Module<Vamp::Core>]
|
|
Core = Module.new
|
|
|
|
# @param entries [Array<Hash{String => String, nil}>] action name and optional Ex alias descriptors
|
|
# @return [Array<Module>] frozen action namespaces
|
|
def self.install_actions(entries)
|
|
raise TypeError, "core action catalog must be an Array" unless entries.is_a?(Array)
|
|
|
|
namespaces = [Core]
|
|
aliases = {}
|
|
entries.each do |entry|
|
|
raise TypeError, "core action descriptor must be a Hash" unless entry.is_a?(Hash)
|
|
|
|
install_action(entry.fetch("name"), namespaces)
|
|
ex = entry.fetch("ex")
|
|
aliases[string!(ex, "core Ex alias").freeze] = true unless ex.nil?
|
|
end
|
|
@builtin_commands = aliases.freeze
|
|
namespaces.each(&:freeze)
|
|
end
|
|
|
|
# @param name [String] binding command name
|
|
# @return [Boolean] whether the catalog owns this textual Ex alias
|
|
def self.builtin_command?(name)
|
|
@builtin_commands.key?(name)
|
|
end
|
|
|
|
# Frozen binding mode values, distinct from a buffer's editing state.
|
|
module Modes
|
|
# @!method self.normal
|
|
# @return [Vamp::Mode]
|
|
# @!method self.insert
|
|
# @return [Vamp::Mode]
|
|
# @!method self.command
|
|
# @return [Vamp::Mode]
|
|
# @!method self.search
|
|
# @return [Vamp::Mode]
|
|
# @!method self.visual
|
|
# @return [Vamp::Mode]
|
|
# @!method self.selection
|
|
# @return [Vamp::Mode]
|
|
# @!method self.tree
|
|
# @return [Vamp::Mode]
|
|
%w[normal insert command search visual selection tree].each do |name|
|
|
mode = Mode.new(name.freeze).freeze
|
|
define_singleton_method(name) { mode }
|
|
end
|
|
end
|
|
|
|
Action.private_class_method :new
|
|
Mode.private_class_method :new
|
|
|
|
# @param value [Hash{String, Symbol => Object}]
|
|
# @return [Hash{String => Object}]
|
|
def self.json_hash(value)
|
|
value.to_h do |key, item|
|
|
raise TypeError, "option keys must be Strings or Symbols" unless key.is_a?(String) || key.is_a?(Symbol)
|
|
|
|
[string!(key.to_s, "option key"), json_value!(item)]
|
|
end
|
|
end
|
|
|
|
# @param value [Object]
|
|
# @return [String, Integer, Float, Boolean, nil]
|
|
def self.json_scalar(value)
|
|
case value
|
|
when String
|
|
string!(value, "option value")
|
|
when Integer, TrueClass, FalseClass, NilClass, Float
|
|
json_literal(value)
|
|
else
|
|
raise TypeError, "options must contain only JSON values"
|
|
end
|
|
end
|
|
|
|
# @param value [Integer, Float, Boolean, nil]
|
|
# @return [Integer, Float, Boolean, nil]
|
|
def self.json_literal(value)
|
|
raise ArgumentError, "option numbers must be finite" if value.is_a?(Float) && !value.finite?
|
|
|
|
value
|
|
end
|
|
|
|
# @param name [String]
|
|
# @param namespaces [Array<Module>]
|
|
# @return [Symbol] the installed action method name
|
|
def self.install_action(name, namespaces)
|
|
name = action_name(name)
|
|
parts = name.split(".").drop(1)
|
|
namespace = parts[0...-1].reduce(Core) { |parent, part| action_namespace(parent, part, namespaces, name) }
|
|
leaf = parts.last
|
|
available_action!(namespace, leaf, name)
|
|
action = Action.send(:new, name.freeze).freeze
|
|
namespace.define_singleton_method(leaf) { action }
|
|
end
|
|
|
|
# @param name [Object]
|
|
# @return [String]
|
|
def self.action_name(name)
|
|
name = string!(name, "core action name")
|
|
raise ArgumentError, "invalid core action name: #{name}" unless name.match?(/\Acore(?:\.[a-z_][a-z0-9_]*)+\z/)
|
|
|
|
name
|
|
end
|
|
|
|
# @param parent [Module]
|
|
# @param part [String]
|
|
# @param namespaces [Array<Module>]
|
|
# @param name [String]
|
|
# @return [Module]
|
|
def self.action_namespace(parent, part, namespaces, name)
|
|
return new_action_namespace(parent, part, namespaces) unless parent.singleton_methods(false).include?(part.to_sym)
|
|
|
|
child = parent.public_send(part)
|
|
raise ArgumentError, "conflicting core action namespace: #{name}" unless child.is_a?(Module)
|
|
|
|
child
|
|
end
|
|
|
|
# @param parent [Module]
|
|
# @param part [String]
|
|
# @param namespaces [Array<Module>]
|
|
# @return [Module]
|
|
def self.new_action_namespace(parent, part, namespaces)
|
|
child = Module.new
|
|
parent.define_singleton_method(part) { child }
|
|
namespaces << child
|
|
child
|
|
end
|
|
|
|
# @param namespace [Module]
|
|
# @param leaf [String]
|
|
# @param name [String]
|
|
# @return [nil]
|
|
def self.available_action!(namespace, leaf, name)
|
|
return unless namespace.singleton_methods(false).include?(leaf.to_sym)
|
|
|
|
raise ArgumentError, "conflicting core action name: #{name}"
|
|
end
|
|
|
|
private_class_method :json_hash, :json_scalar, :json_literal, :install_action, :action_name,
|
|
:action_namespace, :new_action_namespace, :available_action!
|
|
end
|