Edge Rewrite
// HTMLRewriter · presentation

This page was redesigned at the edge.

Cloudflare fetched the original article and streamed it through HTMLRewriter to apply an entirely new visual system without rebuilding the source page.

Jump to content

Module:IA Citation/sandbox

From Wikipedia, the free encyclopedia
require('strict')
local p = {}

--[[--------------------------< I N L I N E _ E R R O R >------------------------------------
Formats a visible error message for the editor.
]]
local function inlineError(msg)
    return '<span style="font-size:100%" class="error">Error in [[Module:IA Citation]]: ' .. msg .. '</span>'
end

--[[--------------------------< T R I M _ A R G >--------------------------------------------
Trims whitespace and returns nil if the resulting string is empty.
]]
local function trimArg(arg)
    if arg == "" or arg == nil then return nil end
    return mw.text.trim(arg)
end

--[[--------------------------< G E T _ S A F E _ I A _ I D >--------------------------------
Extracts the Archive.org ID from a raw string or full URL.
]]
local function get_safe_ia_id(raw_id)
    if not raw_id then return nil end
    if mw.ustring.match(raw_id, "^http") then
        local extracted = mw.ustring.match(raw_id, "/details/([^/?#]+)")
        if extracted then return extracted end
        return mw.ustring.match(raw_id, "([^/]+)$")
    end
    return raw_id
end

--[[--------------------------< P A R S E _ C H U N K >--------------------------------------
Parses a single coordinate string (e.g., "xiv (n8) [keyword]") into its components.
Input: chunk (string)
Output: table {display, target, search}
]]
local function parse_chunk(chunk)
    local leaf_pattern = "%s*%(n([0-9]+)%)"
    local search_pattern = "%s*%[(.-)%]"
    
    local leaf_num = mw.ustring.match(chunk, leaf_pattern)
    local inline_search = mw.ustring.match(chunk, search_pattern)
    
    -- Clean the display text by stripping the metadata
    local clean_display = mw.ustring.gsub(chunk, leaf_pattern, "")
    clean_display = mw.ustring.gsub(clean_display, search_pattern, "")
    clean_display = mw.text.trim(clean_display)
    
    -- Determine the URL target (physical leaf or logical pagenum)
    local target
    if leaf_num then
        target = "n" .. leaf_num
    else
        target = mw.ustring.match(clean_display, "([%w%.]+)") or clean_display
    end
    
    return {
        display = clean_display,
        target = target,
        search = inline_search and mw.text.trim(inline_search)
    }
end

--[[--------------------------< R E S O L V E _ S E A R C H >--------------------------------
Determines the final search term based on priority and configuration settings.
Input: inline_search, user_ia_search, config_ia_search, display_text
Output: string or nil
]]
local function resolve_search_term(inline_search, user_ia_search, config_ia_search, display_text)
    -- 1. Inline [search] always wins
    if inline_search then return inline_search end
    
    -- 2. Check User override (|ia-search=)
    if user_ia_search then
        if user_ia_search == 'none' or user_ia_search == '' then return nil end
        if user_ia_search == 'pagenum' then
            return mw.ustring.match(display_text, "^([%w]+)") or display_text
        end
        return user_ia_search
    end
    
    -- 3. Check Data Shard config (config.iasearch)
    if config_ia_search then
        if config_ia_search == 'none' or config_ia_search == '' then return nil end
        if config_ia_search == 'pagenum' then
            return mw.ustring.match(display_text, "^([%w]+)") or display_text
        end
        return config_ia_search
    end
    
    -- 4. Global Fallback: use the page number itself
    return mw.ustring.match(display_text, "^([%w]+)") or display_text
end

--[[--------------------------< M A K E _ I A _ U R L >--------------------------------------
Constructs a valid Internet Archive URL for a specific page and search term.
Input: ia_id, target, search_term, display_mode
Output: string (URL)
]]
local function make_ia_url(ia_id, target, search_term, display_mode)
    local endpoint = (display_mode == "full screen") and "stream" or "details"
    local base_url = string.format("https://archive.org/%s/%s/page/%s/mode/2up", 
                                  endpoint, ia_id, target)
    
    if search_term and search_term ~= "" then
        -- QUERY encoding ensures spaces and special characters don't break the URL
        local q_param = mw.uri.encode(search_term, "QUERY")
        return base_url .. "?q=" .. q_param
    end
    
    return base_url
end

--[[--------------------------< B U I L D _ L I N K S >--------------------------------------
Orchestrates the conversion of a page/pages string into formatted Wikitext links.
Input: input, config, user_ia_search, user_ia_display
Output: string (joined links) or nil
]]
local function build_links(input, config, user_ia_search, user_ia_display)
    if not input or mw.text.trim(input) == '' then return nil end
    if not config.id then return input end 
    
    local safe_id = get_safe_ia_id(config.id)
    -- Remove "p." or "pp." prefixes if present
    local clean_input = mw.ustring.gsub(input, "^p+%.%s*", "") 
    
    local chunks = mw.text.split(clean_input, "%s*,%s*")
    local results = {}

    for _, chunk in ipairs(chunks) do
        -- 1. Extract the raw parts
        local parts = parse_chunk(chunk)
        
        -- 2. Figure out what the search term should be
        local search_term = resolve_search_term(
            parts.search, 
            user_ia_search, 
            config.iasearch, 
            parts.display
        )
        
        -- 3. Create the actual URL
        local url = make_ia_url(
            safe_id, 
            parts.target, 
            search_term, 
            user_ia_display or config.iadisplay
        )
        
        -- 4. Format as a Wikitext link
        table.insert(results, string.format("[%s %s]", url, parts.display))
    end
    
    return table.concat(results, ", ")
end

--[[--------------------------< G E T _ C O N F I G >----------------------------------------
Locates and loads the data shard for a given book key.
Input: book_key (string)
Output: table (config), nil OR nil, string (error message)
]]
local function get_config(book_key)
    local normalized_key = mw.ustring.gsub(mw.ustring.upper(book_key), " ", "_")
    local first_char = mw.ustring.sub(normalized_key, 1, 1)
    
    -- Determine shard path: A-Z shards or OTHER for symbols/numbers
    local sub_path = (mw.ustring.match(first_char, '^[A-Z]$')) 
                     and ('Module:IA Citation/data/' .. first_char) 
                     or 'Module:IA Citation/data/OTHER'
    
    local success, library = pcall(mw.loadData, sub_path)
    if not success then 
        return nil, 'Data shard not found: ' .. sub_path 
    end
    
    local config = library[normalized_key]
    if not config then 
        return nil, 'Key "' .. normalized_key .. '" not found in ' .. sub_path 
    end
    
    -- Validate that the config has at least one way to build a link
    if not config.id and not config.query and not (config.cite_params and config.cite_params.url) then
        return nil, 'Missing IA ID, Query, or explicit URL for "' .. normalized_key .. '".'
    end
    
    return config, nil
end

--[[--------------------------< F I L T E R _ A R G S >--------------------------------------
Separates internal module control arguments from standard template parameters.
Input: raw_args (table)
Output: table (internal_metadata), table (template_params)
]]
local function filter_args(raw_args)
    local metadata = {}
    local pass_through = {}
    
    -- List of parameters the module intercepts and uses internally
    local internal_keys = {
        ['ia-search'] = true,
        ['ia-display'] = true,
        ['chapter-page'] = true
    }

    for k, v in pairs(raw_args) do
        local val = trimArg(v)
        if val then
            local k_lower = mw.ustring.lower(k)
            if internal_keys[k_lower] then
                metadata[k_lower] = val
            else
                pass_through[k] = val
            end
        end
    end
    
    return metadata, pass_through
end

--[[--------------------------< G E T _ F A L L B A C K _ U R L >----------------------------
Constructs a base URL when no specific page coordinates are provided.
Input: config (table), user_ia_search (string), user_ia_display (string)
Output: string (URL), string (access status)
]]
local function get_fallback_url(config, user_ia_search, user_ia_display)
    if config.id then
        local safe_id = get_safe_ia_id(config.id)
        local display_mode = user_ia_display or config.iadisplay
        local endpoint = (display_mode == "full screen") and "stream" or "details"
        local base_url = "https://archive.org/" .. endpoint .. "/" .. safe_id .. "/"
        
        -- Determine if we should append a search query to the landing page
        local fallback_search = user_ia_search
        if not fallback_search and config.iasearch 
           and config.iasearch ~= 'pagenum' 
           and config.iasearch ~= 'none' 
           and config.iasearch ~= '' then
            fallback_search = config.iasearch
        end
        
        if fallback_search and fallback_search ~= "none" and fallback_search ~= "" then
            base_url = base_url .. "?q=" .. mw.uri.encode(fallback_search, "QUERY")
        end
        
        return base_url, (config['url-access'] or "registration")
        
    elseif config.query then
        return "https://archive.org/search?query=" .. mw.uri.encode(config.query, "QUERY"), nil
    end
    
    return nil, nil
end

--[[--------------------------< M A I N   O R C H E S T R A T O R >--------------------------
Decision logic for the module. Handles parameter merging, link building, and fallbacks.
]]
setmetatable(p, {
    __index = function(t, key)
        return function(frame)
            -- 1. Argument Sanitization
            local metadata, citeArgs = filter_args(frame:getParent().args)
            
            -- 2. Configuration Retrieval
            local config, err = get_config(key)
            if err then return inlineError(err) end

            -- 3. Template and Data Merging
            local targetTemplate = config.template or 'cite book'
            if config.cite_params then
                for k, v in pairs(config.cite_params) do 
                    citeArgs[k] = citeArgs[k] or v 
                end
            end

            -- 4. Process Coordinate Links (page/pages)
            if citeArgs['page'] then 
                citeArgs['page'] = build_links(citeArgs['page'], config, metadata['ia-search'], metadata['ia-display']) 
            end
            if citeArgs['pages'] then 
                citeArgs['pages'] = build_links(citeArgs['pages'], config, metadata['ia-search'], metadata['ia-display']) 
            end

            -- 5. Process Chapter Link
            if metadata['chapter-page'] and config.id then
                local parts = parse_chunk(metadata['chapter-page'])
                -- Force a dummy search for the chapter URL to aid IA reader stability
                local dummy_search = resolve_search_term(nil, nil, nil, parts.display)
                
                citeArgs['chapter-url'] = make_ia_url(
                    get_safe_ia_id(config.id), 
                    parts.target, 
                    dummy_search, 
                    metadata['ia-display'] or config.iadisplay
                )
            end

            -- 6. Fallback Logic (Build base URL if no specific page/chapter coordinates exist)
            if not citeArgs['url'] and not citeArgs['chapter-url'] and not citeArgs['title-link'] then
                if not citeArgs['page'] and not citeArgs['pages'] then
                    local url, access = get_fallback_url(config, metadata['ia-search'], metadata['ia-display'])
                    citeArgs['url'] = url
                    citeArgs['url-access'] = access
                end
            end

            -- 7. Final Output
            return frame:expandTemplate{ title = targetTemplate, args = citeArgs }
        end
    end
})

return p