local case_policy = require("clever_f.case_policy") local domain = require("clever_f.domain") local text_topology = require("clever_f.text_topology") local M = {} local TargetPlanFactory = {} M.TargetPlanFactory = TargetPlanFactory M.SYMBOLS = "!\"#$%&'()=~|\\-^@`[]{};:+*<>,.?_/" local SYMBOL_CHARACTERS = {} local SYMBOL_SET = {} for index = 1, #M.SYMBOLS do local character = M.SYMBOLS:sub(index, index) SYMBOL_CHARACTERS[index] = character SYMBOL_SET[character] = true end local factory_records = setmetatable({}, { __mode = "k" }) local function fail(message, level) error(message, (level or 1) + 1) end local function require_target(target) if not domain.TargetValue.is(target) then fail("target plan target must be a TargetValue", 2) end return target end local function require_boolean(value, name) if type(value) ~= "boolean" then fail("match policy " .. name .. " must be a Boolean", 2) end return value end local function require_string(value, name) if type(value) ~= "string" then fail("match policy " .. name .. " must be a string", 2) end return value end local function copy_list(values) local result = {} for index = 1, #values do result[index] = values[index] end return result end function M.symbol_characters() return copy_list(SYMBOL_CHARACTERS) end function M.is_symbol(character) return type(character) == "string" and SYMBOL_SET[character] == true end local function require_character_list(characters, source) if type(characters) ~= "table" then fail("editor character splitter must return a list", 3) end local result = {} local item_count = 0 for key, character in pairs(characters) do if type(key) ~= "number" or key ~= math.floor(key) or key < 1 or key > #characters or type(character) ~= "string" or character == "" then fail("editor character splitter must return a list of nonempty strings", 3) end result[key] = character item_count = item_count + 1 end if item_count ~= #characters or table.concat(result) ~= source then fail("editor character splitter must preserve the configured trigger string", 3) end return result end function M.parse_trigger_characters(value, splitter) if type(value) ~= "string" then fail("chars_match_any_signs must be a string", 2) end splitter = splitter or text_topology.split_editor_characters if type(splitter) ~= "function" then fail("editor character splitter must be a function", 2) end return require_character_list(splitter(value), value) end local function trigger_set(value, splitter) local result = {} for _, character in ipairs(M.parse_trigger_characters(value, splitter)) do result[character] = true end return result end local function false_matcher() return false end local function symbol_matcher(candidate_character) return M.is_symbol(candidate_character) end local function normalize_factory_options(options) if options == nil then return {} end if type(options) == "function" then return { lowercase = options } end if type(options) ~= "table" then fail("TargetPlanFactory options must be a table", 3) end if type(options.sample_match) == "function" and options.policy == nil and options.policy_service == nil and options.case_resolver == nil and options.lowercase == nil and options.splitter == nil and options.split_editor_characters == nil then return { policy = options } end return options end local function require_policy_service(service) if service ~= nil and (type(service) ~= "table" or type(service.sample_match) ~= "function") then fail("TargetPlanFactory policy must provide sample_match", 3) end return service end local function require_case_resolver(resolver, options) if resolver == nil then return case_policy.new({ lowercase = options.lowercase, }) end if type(resolver) ~= "table" or type(resolver.resolve) ~= "function" or type(resolver.comparator) ~= "function" then fail("TargetPlanFactory case resolver is invalid", 3) end return resolver end local function require_splitter(splitter) splitter = splitter or text_topology.split_editor_characters if type(splitter) ~= "function" then fail("TargetPlanFactory editor character splitter must be a function", 3) end return splitter end local factory_metatable = { __index = TargetPlanFactory, __newindex = function() fail("TargetPlanFactory values are immutable", 2) end, __tostring = function() return "target-plan-factory" end, __metatable = "clever_f.target_plan.TargetPlanFactory", } function TargetPlanFactory.new(options) if TargetPlanFactory.is(options) then return options end options = normalize_factory_options(options) local factory = setmetatable({}, factory_metatable) factory_records[factory] = { policy = require_policy_service(options.policy or options.policy_service), case_resolver = require_case_resolver(options.case_resolver, options), splitter = require_splitter( options.split_editor_characters or options.splitter ), } return factory end function TargetPlanFactory.is(value) return type(value) == "table" and factory_records[value] ~= nil end function M.new(options) return TargetPlanFactory.new(options) end setmetatable(M, { __call = function(_, options) return TargetPlanFactory.new(options) end, }) local function default_match_policy() return { ignore_case = false, smart_case = false, chars_match_any_signs = "", } end local function sampled_policy(factory, target, match_policy) local service if match_policy == nil then service = factory_records[factory].policy if service == nil then return default_match_policy() end elseif type(match_policy) == "table" and type(match_policy.sample_match) == "function" then service = match_policy end if service ~= nil then match_policy = service:sample_match(target) end if type(match_policy) ~= "table" then fail("target match policy must be a table", 3) end return { ignore_case = require_boolean(match_policy.ignore_case, "ignore_case"), smart_case = require_boolean(match_policy.smart_case, "smart_case"), chars_match_any_signs = require_string( match_policy.chars_match_any_signs, "chars_match_any_signs" ), } end local function new_plan(target, kind, case_mode, matcher) return domain.TargetPlan.new({ target = target, kind = kind, case_mode = case_mode, matcher = matcher, }) end function TargetPlanFactory:build(target, match_policy) target = require_target(target) local record = factory_records[self] local sampled = sampled_policy(self, target, match_policy) local case_mode = record.case_resolver:resolve( target, sampled.ignore_case, sampled.smart_case ) if target.first_code == 0x80 then return new_plan( target, domain.TargetPlanKind.EMPTY, case_mode, false_matcher ) end local triggers = trigger_set(sampled.chars_match_any_signs, record.splitter) if triggers[target.value] then return new_plan( target, domain.TargetPlanKind.SYMBOL, case_mode, symbol_matcher ) end local kind = target.value == "\\" and domain.TargetPlanKind.BACKSLASH or domain.TargetPlanKind.LITERAL return new_plan( target, kind, case_mode, record.case_resolver:comparator(target.value, case_mode) ) end function M.build(target, match_policy, options) return TargetPlanFactory.new(options):build(target, match_policy) end M.create = M.build M.create_plan = M.build M.SYMBOL_SET_STRING = M.SYMBOLS return M