| This is the module sandbox page for Module:IA Citation (diff). |
| This module is rated as alpha. It is ready for limited use and third-party feedback. It may be used on a small number of pages, but should be monitored closely. Suggestions for new features or adjustments to input and output are welcome. |
| This module depends on the following other modules: |
This module is the core engine for specific-source templates that cite books hosted at the Internet Archive.
This module acts as a dynamic wrapper and dispatcher. It intercepts specific parameters to build Internet Archive URLs, and passes the remaining formatted data to {{cite book}} or {{cite journal}}.
Usage
editSingle edition
edit<includeonly>{{#invoke:IA Citation|BOOK_NAME}}</includeonly><noinclude>
{{Documentation}}
</noinclude>
The BOOK_NAME has an entry in the appropriate Module:IA Citation/data/x file. Example: {{Template:Bosworth-The New Islamic Dynasties}}
Multiple editions
editBooks with multiple editions are routed via |edition=
<includeonly>{{#switch:{{#invoke:Normalize volume and edition|edition|{{{edition|}}}}}
| 1 = {{#invoke:IA Citation|BOOK_NAME_1990}}
| 2 | #default = {{#invoke:IA Citation|BOOK_NAME_2007}}
}}</includeonly><noinclude>
{{Documentation}}
</noinclude>
Each edition (BOOK_NAME_1990 and BOOK_NAME_2007) has an entry in the Module:IA Citation/data/x file. Example: {{Daftary-The Ismailis}}.
Note: {{Normalize volume and edition}} will match on "Second", "2nd", "ii", etc..
Multiple volumes
editBooks with multiple volumes are routed via |volume=.
<includeonly>{{#switch:{{#invoke:Normalize volume and edition|volume|{{{volume|}}}}}
| 1 = {{#invoke:IA Citation|BOOK_NAME_V1}}
| 2 = {{#invoke:IA Citation|BOOK_NAME_V2}}
| 3 = {{#invoke:IA Citation|BOOK_NAME_V3}}
| {{#invoke:IA Citation|BOOK_NAME_V1}}
}}</includeonly><noinclude>
{{Documentation}}
</noinclude>
Each volume has an entry in the Module:IA Citation/data/x file. Example: {{Catalogue of Byzantine Seals at Dumbarton Oaks and in the Fogg Museum of Art}}
Note: {{Normalize volume and edition}} will match on "1", "i", "Vol. 1", etc..
Features
edit- Rich page linking features:
- Link URL(s) to a single page, a range of pages, or groupings of pages eg. 42-44, 64-65
- Link a URL to Roman Numeral, photos, or any page with or without a printed page number.
- Link a URL to a chapter or section with the custom
|chapter-page=parameter in combination with|chapter=
- Include search key terms that are highlighted, and shows other pages with the same search terms.
- Each page link can have its own search terms e.g search on "Atlantic" in 42-44 and "Pacific" in 64-65
- Optional "Dummy search" feature (advantage explained)
- Optional full-screen mode
- A single URL links to a group of books e.g. the multi-volume set of Encyclopaedia Britannica published in 1911.
- A single URL links to multiple works by an author, or any custom combination, using the powerful search capabilities of IA's logical operators e.g. AND, OR, (), etc..
- Link a URL for Google Books, Haithi Trust and elsewhere (but no support for page/chapter linking and most other features)
Architecture & Data Sharding
editTo avoid loading a single large data table into memory, configuration data is split into alphabetical subpages (shards).
- When invoked, the module extracts the first letter of the Book Key (e.g., "A" from "AIA4").
- It uses `mw.loadData()` to retrieve the corresponding subpage (e.g., Module:IA Citation/data/A).
- If the key does not start with a standard letter, it falls back to Module:IA Citation/data/OTHER.
Parameter Parsing & The "Trap and Drop"
editThe module intercepts template-specific parameters (such as |ia-search= and |ia-display=) to prevent them from causing "unknown parameter" errors in the underlying CS1 template.
Custom Syntaxes
editThe module parses the page= and pages= fields using `mw.ustring` to extract custom routing commands:
- Leaf Mapping
(n#): Extracted using the regex pattern%s*%(n([0-9]+)%)to route URLs to specific physical leaves when printed page numbers (like Roman numerals) do not match the digital structure. - Inline Search
[term]: Extracted using%s*%[(.-)%]to pass a specific search term to the IA viewer.
These markers are stripped from the display text before being rendered in the final Wikipedia citation.
Dummy search
editA core feature of this module is its handling of the Internet Archive's restricted viewing modes.
If an editor provides a page number, the module automatically appends that number as a "dummy" search string (e.g., ?q=42). This is a necessary architectural workaround: supplying a search string forces the IA viewer to open and highlight specific pages that might be blocked due to restrictions (similar to Google Books snippet view with search strings).
Hierarchy of Overrides
editTo accommodate various IA viewing restrictions, display and search behaviors are resolved in the following priority order (Highest to Lowest):
- Inline Brackets:
page=42 [search term] - User Parameter:
|ia-search=or|ia-display=in the Wikipedia template call. - Global Shard Default: Defined in the `Module:IA Citation/data/X` module.
- System Default: The alphanumeric page number (for searches) or the standard 2-up viewer (for display).
Testcases
editTestcases: Template:Catalogue of Byzantine Seals at Dumbarton Oaks and in the Fogg Museum of Art/testcases
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