وحدة:SST

يمكن إنشاء صفحة توثيق الوحدة في وحدة:SST/شرح

require('strict')
local p = {}

--[[--------------------------< H O S T _ M A P >--------------------------------------------]]
local host_map = {
    ['ia']              = { engine = 'IA',         shard_key = 'ia' },
    ['internetarchive'] = { engine = 'IA',         shard_key = 'ia' },
    ['hathi']           = { engine = 'Hathi',      shard_key = 'hathi' },
    ['hathitrust']      = { engine = 'Hathi',      shard_key = 'hathi' },
    ['guten']           = { engine = 'Gutenberg',  shard_key = 'guten' },
    ['gutenberg']       = { engine = 'Gutenberg',  shard_key = 'guten' },
    ['wikisrc']         = { engine = 'Wikisource', shard_key = 'wikisrc' },
    ['wikisource']      = { engine = 'Wikisource', shard_key = 'wikisrc' },
    ['gbook']           = { engine = 'GBook',      shard_key = 'gbook' },
    ['googlebooks']     = { engine = 'GBook',      shard_key = 'gbook' },
    ['web']             = { engine = 'Web',        shard_key = 'web' },
    ['physical']        = { engine = 'Physical',   shard_key = 'physical' } 
}

--[[--------------------------< P A R A M E T E R   M A P >----------------------------------]]
-- SSTS Architecture Note:
-- The parameter_map is designed specifically for sub-divisions of a single work
-- (like individual chapters or dictionary entries). 
-- To maintain the Single-Source Template (SSTS) philosophy, core identity 
-- parameters like 'title', 'year', or 'isbn' are managed at the variant 
-- level (creating a new library entry) rather than through this map.
local parameter_map_guardrail = {
    ['chapter'] = true,
    ['entry']   = true,
    ['article'] = true,
    ['section'] = true,
    ['part']    = true
}

local function apply_parameter_map(user_args, book_data, library)
    -- Follow the pointer if an alias exists
    local map_source = book_data
    if book_data.parameter_map_alias and library and library[book_data.parameter_map_alias] then
        map_source = library[book_data.parameter_map_alias]
    end

    if not map_source.parameter_map then return end
    
    for param, map_table in pairs(map_source.parameter_map) do
        if parameter_map_guardrail[param] then
            local user_input = user_args[param]
            if user_input then
                -- Trim whitespace so "|chapter= 8 " matches key ['8']
                local clean_input = mw.text.trim(tostring(user_input))
                if map_table[clean_input] then
                    user_args[param] = map_table[clean_input]
                end
            end
        end
    end
end

--[[--------------------------< H E L P E R S >----------------------------------------------]]
local function error_msg(msg, tracking_category)
    local err_text = string.format('<strong class="error">SSTS Error: %s</strong>', msg)
    local title = mw.title.getCurrentTitle()
    
    if title and (title.namespace == 0 or title.namespace == 118) then
        if tracking_category then
            return err_text .. '[[Category:' .. tracking_category .. ']]'
        else
            return err_text .. '[[Category:SSTS errors]]'
        end
    end
    
    return err_text
end

--[[--------------------------< C O R E _ E X E C U T I O N >--------------------------------]]
-- Shared function to build the citation regardless of how it was routed
local function build_citation(frame, book_data, target_host, library)
    if not target_host then return error_msg("No host defined for this record.") end
    
    -- 1. Route to the correct Host/Engine
    local host_info = host_map[string.lower(mw.text.trim(target_host))]
    if not host_info then return error_msg("Unsupported host '" .. target_host .. "'") end

    -- 2. Validate Host Data within the record
    local host_data = book_data.hosts and book_data.hosts[host_info.shard_key]
    if not host_data then 
        return error_msg("Host '" .. host_info.shard_key .. "' is not defined for this book.") 
    end

    -- 3. Load the Hosts module
    local success_engine, hosts_module = pcall(require, 'Module:SST/hosts')
    if not success_engine then 
        return error_msg("Could not load Module:SST/hosts.") 
    end
    
    local engine = hosts_module[host_info.engine]
    if not engine then 
        return error_msg("Engine '" .. host_info.engine .. "' not found in hosts module.") 
    end

    -- 4. Process Arguments & Apply ignore_args Filter
    local ignore_list = { 
        ['ignore_args'] = true, 
        ['id'] = true, 
        ['key'] = true, 
        ['default'] = true 
    }
    
    if frame.args['ignore_args'] then
        for key in string.gmatch(frame.args['ignore_args'], '([^,]+)') do
            ignore_list[mw.text.trim(key)] = true
        end
    end

    local combined_args = {}
    
    -- Grab parent args (user input), skipping ignored keys
    for k, v in pairs(frame:getParent().args) do
        if type(k) == 'string' and not ignore_list[k] then
            combined_args[k] = v
        elseif type(k) == 'number' and not ignore_list[tostring(k)] then
            combined_args[k] = v
        end
    end

    -- Grab current frame args (template overrides like our generated 'chapter')
    for k, v in pairs(frame.args) do
        if type(k) == 'string' and not ignore_list[k] then
            combined_args[k] = v
        end
    end

    -- ==========================================================
    -- 4.5 APPLY PARAMETER MAP (Translate shorthand into full titles)
    -- ==========================================================
    apply_parameter_map(combined_args, book_data, library)

    local citeArgs = {}
    for k, v in pairs(book_data.cite_params or {}) do 
        citeArgs[k] = v 
    end

    -- ==========================================================
    -- 4.7 SILENT LOGIC TRAP (Inspect raw shard data for faults)
    -- ==========================================================
    local has_logic_fault = false
    local t_type = citeArgs['_template'] or 'cite book'
    
    if t_type == 'cite encyclopedia' then
        -- Encyclopedia shards must use 'encyclopedia', not 'title' or specific entries.
        -- They also cannot contain book or journal container names.
        if citeArgs['title'] or citeArgs['title-link'] or citeArgs['article'] or citeArgs['entry'] then
            has_logic_fault = true
        elseif citeArgs['journal'] or citeArgs['magazine'] or citeArgs['work'] then
            has_logic_fault = true
        end

    elseif t_type == 'cite journal' then
        -- Journal shards must have a journal container.
        -- They cannot be locked to specific chapters, nor contain book/encyclopedia containers.
        if not citeArgs['journal'] and not citeArgs['work'] and not citeArgs['magazine'] then
            has_logic_fault = true
        end
        if citeArgs['chapter'] or citeArgs['encyclopedia'] then
            has_logic_fault = true
        end

    elseif t_type == 'cite book' then
        -- Book shards cannot contain encyclopedia or journal container names.
        if citeArgs['encyclopedia'] or citeArgs['journal'] or citeArgs['magazine'] or citeArgs['work'] then
            has_logic_fault = true
        end
    end

    -- 5. Pass execution to the Hosts module for parsing and link-building
    if type(hosts_module.process) == "function" then
        citeArgs = hosts_module.process(engine, host_data, citeArgs, combined_args)
    else
        return error_msg("System Error: hosts_module.process is missing.")
    end

    -- 6. Render standard Cite Book/Encyclopedia template
    local template_name = citeArgs['_template'] or 'cite book'
    citeArgs['_template'] = nil 
    
    if citeArgs['ref'] and mw.ustring.match(citeArgs['ref'], '^{{') then
        local pre_ref = frame:preprocess(citeArgs['ref'])
        citeArgs['ref'] = mw.ustring.gsub(pre_ref, " ", "_")
    end
    
    local citation = frame:expandTemplate{ title = template_name, args = citeArgs }

    -- 7. Catch CS1/CS2 errors and inject silent tracking categories
    local current_page = mw.title.getCurrentTitle()
    if current_page and (current_page.namespace == 0 or current_page.namespace == 118) then
        -- Catch native CS1 red errors
        local has_cs1_error = string.find(citation, "cs1%-visible%-error") or 
                              string.find(citation, "cs1%-hidden%-error") or 
                              string.find(citation, "Category:CS1 errors")
                              
        if has_cs1_error then
            citation = citation .. "[[Category:SSTS errors]]"
        end
        
        -- Catch parameter logical faults
        if has_logic_fault then
            citation = citation .. "[[Category:SSTS parameter logic faults]]"
        end
    end

    return citation
end

--[[--------------------------< T H E   B R I D G E >----------------------------------------]]
-- LEGACY ROUTER: Supports existing live templates. DO NOT DELETE until Phase 4.
function p.main(frame)
    local raw_host = frame.args[1]
    local book_key = frame.args[2]
    
    if not raw_host then return error_msg("No host specified in #invoke.") end
    if not book_key then return error_msg("No book key specified in #invoke.") end
    
    local shard_letter = mw.ustring.upper(mw.ustring.sub(mw.text.trim(book_key), 1, 1))
    local success_shard, library = pcall(mw.loadData, 'Module:SST/shards/' .. shard_letter)
    
    if not success_shard then return error_msg("Could not load shard module '" .. shard_letter .. "'.") end
    
    local book_data = library[mw.text.trim(book_key)]
    if not book_data then return error_msg("Book key '" .. book_key .. "' not found.", "SSTS missing records") end
    
    -- Smart Host Fallback
    local final_host = book_data.host
    
    if not final_host then
        -- Fallback 1: Try to grab the first available host from the hosts table
        if book_data.hosts and type(book_data.hosts) == 'table' then
            for k, _ in pairs(book_data.hosts) do
                final_host = k
                break
            end
        end
        
        -- Fallback 2: If still nothing, treat it as a physical book
        if not final_host then
            final_host = 'physical'
        end
    end
    
    local output = build_citation(frame, book_data, final_host, library)
    
    -- Only categorize in Main (0) and Template (10) namespaces to ignore sandboxes
    local ns = mw.title.getCurrentTitle().namespace
    if ns == 0 or ns == 10 then
        output = output .. "[[Category:SSTS errors|*LEGACY]]"
    end
    
    return output
end


--[[--------------------------< A R C H I T E C T U R E >----------------------------]]

-- ROUTER: Single A-Z Books
function p.single(frame)
    local base_id = frame.args.id
    if not base_id then return error_msg("No id specified in #invoke.") end
    
    local shard_letter = mw.ustring.upper(mw.ustring.sub(mw.text.trim(base_id), 1, 1))
    local success_shard, library = pcall(mw.loadData, 'Module:SST/shards/' .. shard_letter)
    
    if not success_shard then return error_msg("Could not load shard module '" .. shard_letter .. "'.") end
    
    local book_data = library[mw.text.trim(base_id)]
    if not book_data then return error_msg("Record '" .. base_id .. "' not found.", "SSTS missing records") end
    
    -- Smart Host Fallback
    local final_host = book_data.host
    if not final_host then
        if book_data.hosts and type(book_data.hosts) == 'table' then
            for k, _ in pairs(book_data.hosts) do
                final_host = k
                break
            end
        end
        if not final_host then final_host = 'physical' end
    end
    
    return build_citation(frame, book_data, final_host, library)
end

-- ROUTER: Multi-Volume Sets
function p.set(frame)
    local base_id = frame.args.id
    if not base_id then return error_msg("No id specified for the set in #invoke.") end
    
    -- Determine target key (e.g. "1", "2", "ALL")
    local raw_key = frame.args.key
    local target_key
    if not raw_key or mw.text.trim(raw_key) == "" then
        target_key = frame.args.default or "ALL"
    else
        target_key = mw.text.trim(raw_key)
    end
    
    local prefix = base_id .. "_"
    local lookup_id
    
    -- Smart detection: Check if the editor accidentally passed the full book key instead of the short variant
    if mw.ustring.sub(target_key, 1, mw.ustring.len(prefix)) == prefix then
        lookup_id = target_key
        -- Strip the prefix from target_key so error messages still look clean
        target_key = mw.ustring.sub(target_key, mw.ustring.len(prefix) + 1)
    else
        lookup_id = prefix .. target_key
    end
    
    -- Route to dedicated /sets/ module
    local success_shard, library = pcall(mw.loadData, 'Module:SST/shards/sets/' .. base_id)
    if not success_shard then return error_msg("Could not load set module for '" .. base_id .. "'.") end
    
    local book_data = library[lookup_id]
    if not book_data then return error_msg("Part '" .. target_key .. "' not found in set '" .. base_id .. "'.", "SSTS missing records") end
    
    -- Smart Host Fallback
    local final_host = book_data.host
    if not final_host then
        if book_data.hosts and type(book_data.hosts) == 'table' then
            for k, _ in pairs(book_data.hosts) do
                final_host = k
                break
            end
        end
        if not final_host then final_host = 'physical' end
    end
    
    return build_citation(frame, book_data, final_host, library)
end

return p