Module:Taxon
Appearance
Documentation for this module may be created at Module:Taxon/doc
-- ================================================================================
-- Module dependencies
-- ================================================================================
local getArgs = require('Module:Arguments').getArgs
local cargo = mw.ext.cargo -- for cargo queries if needed
local cf = require("Module:Custom_functions")
-- ========================================
-- Get taxonomic schema===================================
local ts = require("Module:Taxonomic schema")
local rank_lookup = ts.rank_lookup
local all_ranks = ts.all_ranks
local rank_dictionary = ts.rank_dictionary
local utils = require("Module:Utilities")
local tf = require("Module:Table_functions")
local ibf = require("Module:Infobox_functions")
local html = mw.html.create()
-- ================================================================================
-- Main table
-- ================================================================================
local p = {}
-- =========================================================
-- Generate taxon lists
-- =========================================================
--[[
This is a parent function that uses various helper functions to create a nested, bulleted, html list
for display on a taxon (or other) page.
--]]
function p.taxon_list_pipeline(frame)
-- ========================================
-- Arguments and settings
-- ========================================
local args = getArgs(frame)
args.parent_title = mw.title.getCurrentTitle().text
-- get focal taxon from the tile of parent page calling the module or user-provided argument
local focal_taxon = args.taxon or args.parent_title
-- Set Cargo table
local cargo_table_name = "Species"
-- ========================================
-- Get focal Cargo row
-- ========================================
local cargo_focal_row = utils.get_cargo_row(cargo_table_name, focal_taxon, all_ranks)
-- ========================================
-- Find the focal rank
-- ========================================
local focal_rank = utils.get_focal_rank(cargo_focal_row, focal_taxon, all_ranks)
if not focal_rank then
return "An taxon list can't be produced because this taxon can't be found in the WhiskerWiki database!\n\n"
end
local focal_rank_lowercase = rank_lookup[focal_rank].lowercase
local focal_rank_sentence_case = rank_lookup[focal_rank].sentence_case
-- ========================================
-- Get correct rank list for display
-- ========================================
-- Create a 'genealogy' for the focal taxon which is a concise list of the Linnean values moving up the hierarchy
-- from the focal taxon for searching the correct taxon rank list
local genealogy = utils.get_genealogy(cargo_focal_row, focal_rank, focal_taxon, all_ranks)
-- Look to see if there is a custom rank list for the focal taxa and if not use the default
local rank_list = utils.check_for_custom_ranks(genealogy, rank_dictionary)
-- Make a string version for use as the fields of cargo.query
local rank_list_string = utils.list_to_csv_string(rank_list)
-- ========================================
-- Define downstream child ranks
-- ========================================
-- get list of the names of the child ranks with respect to page rank
local child_ranks = ""
if focal_rank ~= "Species" then
child_ranks = utils.get_downstream_items(rank_list, focal_rank)
else
return "Warning: 'Module:Higher taxon content' shouldn't be used on species pages."
end
-- ========================================
-- Get Cargo query for the focal rank
-- ========================================
-- Get all the records that correspond to the focal (page) rank
local fields = rank_list_string
local cargo_args = {
where = string.format("%s = '%s'", focal_rank, focal_taxon)
}
local cargo_results = cargo.query(cargo_table_name, fields, cargo_args)
-- ========================================
-- Build a nested hierarchy of the Cargo data
-- ========================================
local nested_taxon_list = utils.build_nested_hierarchy(cargo_results, child_ranks)
-- ========================================
-- Format the nested list
-- ========================================
-- Define and apply formatting before building the html code
--[[I have decided it's best to apply formatting at this stage, i.e. apply the formatting to the nested object and not to the cargo results.
In other pipelines in the wiki, where I don't use this nested structure step, I do apply formatting results directly to the cargo structure.
However, since the generation of these taxonomic lists requires this extra nested structure step before producing the HTML list, it's better for several reasons to
apply the formatting at this stage. In this way the formatting is applied right before producing the forward-facing HTML list. For one thing, that way,
the information of the hierarchy could be used to apply formatting rules. Otherwise I run the risk of having messy code where formatting needs to be applied twice
--]]
-- Function that is used by traverse_and_format to apply formatting rules based on rank
--[[
This helper function, used by traverse_and_format, has the primary responsibility of applying formatting rules.
It contains the specific logic for modifying a table entry based on its field or other hierarchy-based rules (e.g., italicizing species names or adding hyperlinks).
By isolating this logic, we keep the traversal function focused on traversal, improving readability and maintainability.
This function, in turn doesn't apply formatting directly it applies helper functions that have specific raw formatting tasks.
--]]
-- This helper function now takes a node (with .rank and .name) instead of separate "field" and "values".
-- If the node's rank is "Species," italicize the node.name in place.
local function apply_rank_formatting(node)
-- Replace underscores with spaces in the rank field
node.rank = node.rank:gsub("_", " ")
if node.name ~= "No information" and node.rank ~= "Root" then
if node.rank == "Species" or node.rank == "Genus" then
-- For species, italicize and create a page link
node.name = "[[" .. node.name .. "|" .. "''" .. node.name .. "''" .. "]]"
elseif node.rank == "Subgenus" then
local taxon_only = string.match(node.name, "^(.-) %(.-%)$") or node.name
-- For subgenus, italicize taxon name and remove " (subgenus)" text
node.name = "[[" .. node.name .. "|" .. "''" .. taxon_only .. "''" .. "]]"
elseif node.rank == "Species group" then
-- Italicize the binomial in from of species groups
local binomial_only = string.gsub(node.name, " group$", "")
node.name = "[[" .. node.name .. "|" .. "''" .. binomial_only .. "''" .. " group]]"
elseif node.rank == "Clade II" then
-- Get the text before the word "group"
local binomial_only = node.name:match("^(.-) group")
-- Get the text after the word "group"
local string_after_group = node.name:match("group (.+)") or ""
-- Combine with the correct italicizing
node.name = "[[" .. node.name .. "|" .. "''" .. binomial_only .. "''" .. " group " .. string_after_group .. "]]"
elseif node.name == "Incertae sedis" then
-- For "Incertae sedis", italicize and don't create a page link
node.name = "''" .. node.name .. "''"
else
-- For everything else, just create a normal link
node.name = "[[" .. node.name .. "]]"
end
end
-- Finally remove "Species" in the rank field after processing as it's not needed for display
if node.rank == "Species" then
node.rank = node.rank:gsub("Species", "")
end
end
-- Format the nested taxon list
--[[Note that this function edits the nested table in place--]]
utils.traverse_and_format(nested_taxon_list, apply_rank_formatting)
-- ========================================
-- Output section
-- ========================================
-- Build the html formatted list
local taxon_list = utils.build_bulleted_taxon_list(nested_taxon_list)
-- Get values for correct grammatical syntax
local focal_taxon_name_display = rank_lookup[child_ranks[1]].plural
-- Produce the display for the forward-facing page
local output_header = tostring(mw.html.create('h2'):wikitext("Available taxon pages"))
local output_text = tostring(mw.html.create('p'):wikitext("The following " .. focal_taxon_name_display .. " are available on WhiskerWiki for this " .. focal_rank_lowercase .. ":\n"))
if taxon_list then
output = output_header .. output_text .. taxon_list .. "\n"
end
return output
end
-- =========================================================
-- Taxon infobox
-- =========================================================
function p.infobox(frame)
-- ========================================
-- Main settings and arguments
-- ========================================
local args = getArgs(frame)
local focal_taxon = mw.title.getCurrentTitle().text
local placeholder_image = "No-Image-Placeholder.svg"
-- get taxon rank of parent page calling the module
local focal_rank = ts.rank_finder("Species", focal_taxon)
-- Control loop to avoid Lua errors if taxon doesn't exist in Cargo
if not focal_rank then
return "An infobox can't be produced because this taxon can't be found in the WhiskerWiki database!\n\n"
end
local focal_rank_lowercase = rank_lookup[focal_rank].lowercase
local focal_rank_sentence_case = rank_lookup[focal_rank].sentence_case
-- ========================================
-- Get images from Cargo
-- ========================================
local range_map = ""
local profile_image = ""
local tables = "Taxon_image"
local fields = "Species, Range_map, Profile_image"
local cargo_args = {
where = string.format("%s = '%s'", "Species", focal_taxon)
}
local cargo_results = cargo.query( tables, fields, cargo_args )
-- local function to check if a file exists. This is a patch to deal with the fact that we
-- added a lot of file names to a Cargo table that have not been uploaded yet.
local function file_exists(file_name)
local focal_taxon = mw.title.new("File:" .. file_name)
return focal_taxon and focal_taxon.exists
end
if #cargo_results > 0 then
local range_map_name = cargo_results[1].Range_map
if not file_exists(range_map_name) then
range_map_name = placeholder_image
end
range_map = range_map_name
end
-- ========================================
-- Get main infobox information from Cargo
-- ========================================
-- Get infobox information from Cargo
local tables = "Species"
local fields = "Order_taxon, Suborder, Infraorder, Superfamily, Family, Subfamily, Tribe, Genus, Subgenus, Clade_I, Species_group, Clade_II, Species, Common_name, Full_name, Species_author, Species_year"
local cargo_args = {
where = string.format("%s = '%s'", focal_rank, focal_taxon)
}
local cargo_results = cargo.query( tables, fields, cargo_args )
-- Pull the results from the Cargo array and assign to variables
if cargo_results[1] then
local fields_array = {}
for field in fields:gmatch("[^,]+") do -- Split fields by comma
table.insert(fields_array, field:match("^%s*(.-)%s*$")) -- Trim spaces
end
for _, field in ipairs(fields_array) do
args[field] = cargo_results[1][field]
end
end
-- color control for the infobox (do this before objects get set to nil below)
local html_theme_class = "-base"
if focal_rank ~= "Species" then
-- Create a 'genealogy' for the focal taxon which is a concise list of the Linnean values moving up the hierarchy
-- from the focal taxon for searching the correct taxon rank list
-- Look to see if there is a custom rank list for the focal taxa and if not use the default
local rank_list = utils.check_for_custom_ranks({focal_taxon}, rank_dictionary)
local genealogy = utils.get_genealogy(cargo_results, focal_rank, focal_taxon, rank_list)
-- remove the focal taxon from the genealogy as this doesn't need to display in the infobox
table.remove(genealogy, 1)
-- nullify all fields in args except those in the genealogy table
if genealogy then
for key in pairs(args) do
local found = false
for _, allowed_value in pairs(genealogy) do
if args[key] == allowed_value then
found = true
break
end
end
if not found then
args[key] = nil
end
end
end
end
-- ========================================
-- Begin creating the infobox html
-- ========================================
local display_title = focal_rank_sentence_case .. " " .. focal_taxon .. "<br/>" -- this is the default focal_taxon
local subtitle = ""
-- overwrite the default title for certain groups of ranks
if focal_rank == "Species_group" then
local binomial_only = string.gsub(focal_taxon, " group$", "")
display_title = "<i>" .. binomial_only .. "</i>" .. " group"
end
if focal_rank == "Clade_I" or focal_rank == "Clade_II" then
display_title = focal_taxon
if focal_rank == "Clade_I" or focal_rank == "Clade_II" then
subtitle = "(informal rank)"
end
end
if focal_rank == "Genus" then
display_title = focal_rank_sentence_case .. " <i>" .. focal_taxon .. "</i>"
end
if focal_rank == "Subgenus" then
local taxon_only = string.match(focal_taxon, "^(.-) %(.-%)$") or focal_taxon
display_title = focal_rank_sentence_case .. " <i>" .. taxon_only .. "</i>"
end
if focal_rank == "Species" then
display_title = args.Common_name
subtitle = "(<i>" .. focal_taxon .. "</i>)"
end
-- after formatting species reformat the subset that are undescribed species
-- Check if the species follows the undescribed pattern
if focal_taxon:match("^%w+ sp%.") then
-- Extract genus and the remaining part (after "sp.")
local genus, descriptor = focal_taxon:match("^(%w+) sp%. (.+)$")
if genus and descriptor then
-- Format the display title and subtitle
display_title = "<i>" .. genus .. "</i> sp. " .. descriptor
subtitle = "(undescribed)"
else
-- Handle cases where the format is invalid
return "Warning: Formatting is not correct for an undescribed species. Expected format: 'Genus sp. Descriptor'."
end
end
root = ibf.create_infobox(root, display_title, subtitle, html_theme_class)
if range_map and range_map ~= "" then
root = ibf.add_header(root, "Range", html_theme_class)
root = ibf.add_image(root, range_map, html_theme_class)
end
root = ibf.add_header(root, "Taxonomic classification", html_theme_class)
root = ibf.add_row(root, 'Order:', args.Order_taxon, html_theme_class, "page")
root = ibf.add_row(root, 'Suborder:', args.Suborder, html_theme_class, "page")
root = ibf.add_row(root, 'Infraorder:', args.Infraorder, html_theme_class, "page")
root = ibf.add_row(root, 'Superfamily:', args.Superfamily, html_theme_class, "page")
root = ibf.add_row(root, 'Family:', args.Family, html_theme_class, "page")
root = ibf.add_row(root, 'Subfamily:', args.Subfamily, html_theme_class, "page")
root = ibf.add_row(root, 'Tribe:', args.Tribe, html_theme_class, "page")
root = ibf.add_row(root, 'Genus:', args.Genus, html_theme_class, "italic page")
root = ibf.add_row(root, 'Subgenus:', args.Subgenus, html_theme_class, "italic page")
root = ibf.add_row(root, 'Clade I:', args.Clade_i, html_theme_class, "page")
root = ibf.add_row(root, 'Species group:', args.Species_group, html_theme_class, "page")
root = ibf.add_row(root, 'Clade II:', args.Clade_ii, html_theme_class, "page")
if focal_rank == "Species" then
root = ibf.add_header(root, "Binomial details", html_theme_class, 2)
root = ibf.add_spanning_row(root, "<i>" .. args.Species .. "</i> " .. args.Species_author, html_theme_class, "text")
end
return root
end
-- =========================================================
-- General species data based on literary references
-- =========================================================
function p.get_species_refs(frame)
local title = mw.title.getCurrentTitle().text
local args = getArgs(frame)
-- override title with user defined species entry
if args.species then
title = args.species
end
-- set default for collapse cutoff in case user does not through the template
local collapse_cutoff = 100
if args.collapse_cutoff then
collapse_cutoff = tonumber(args.collapse_cutoff)
end
-- get information from cargo
local tables = "Species_reference"
local fields = "Part_of_range, Reference, Total_length, Tail_length, Hindfoot_length, Ear_length, Mass"
local cargo_args = {
where = string.format("%s = '%s'", "Species", title)
}
local cargo_results = cargo.query(tables, fields, cargo_args)
-- Remove exact duplicate rows from cargo_results
local unique_cargo_results = {}
local seen = {}
for _, row in ipairs(cargo_results) do
-- Build a unique string key for the entire row (order of fields matters)
local row_key = (row.Part_of_range or "")
.. "|" .. (row.Reference or "")
.. "|" .. (row.Total_length or "")
.. "|" .. (row.Tail_length or "")
.. "|" .. (row.Hindfoot_length or "")
.. "|" .. (row.Ear_length or "")
.. "|" .. (row.Mass or "")
if not seen[row_key] then
table.insert(unique_cargo_results, row)
seen[row_key] = true
end
end
-- Replace original cargo_results with the deduplicated version
cargo_results = unique_cargo_results
-- format cargo information in place
local field_formats = {
Species = "page",
Part_of_range = "string",
Reference = "string",
}
local cargo_results_formatted = tf.format_cargo_results(cargo_results, field_formats)
-- control row cutoff for collapsing display
local collapse_cutoff = collapse_cutoff
-- return args.collapse_cutoff
local collapse = false
if #cargo_results_formatted > collapse_cutoff then
collapse = true
end
-- Final formatting of output
local header = ""
local description = "Length measurements are in millimeters (mm) and weight measurements are in grams (g), unless stated otherwise. If available, the sample size (n=) is provided. If a range is not provided and n= is not given, then the listed measurement represents an average."
local display_fields = fields
local output = ""
if #cargo_results_formatted > 0 then
local output_table = tf.generate_wiki_table(cargo_results_formatted, header, display_fields, collapse)
output = header .. "\n" .. description .. "\n" .. output_table
else
output = "No results found."
end
return output
end
-- =========================================================
-- Sorex character data querying
-- =========================================================
function p.get_sorex_chars(frame)
local title = mw.title.getCurrentTitle().text
local args = getArgs(frame)
-- override title with user defined species entry
if args.species then title = args.species end
-- get information from cargo
local tables = "Sorex_character"
local fields = "Summer_dorsum_color, Winter_dorsum_color, Condylobasal_length," ..
"Shape_medial_edge_incisors, Tines_upper_incisors, Tine_size, Tine_position," ..
"Number_unicuspids, Unicuspid_notes, Postmandibular_canal, Skull_shape," ..
"Dental_formula, Dental_characters, Other_characters, Tail_bicolored, Paired_toe_pads"
local cargo_args = {where = string.format("%s = '%s'", "Species", title)}
if title == "all" then cargo_args = {} end -- select all records if user enters "all" for species
local cargo_results = cargo.query(tables, fields, cargo_args)
-- format cargo information in place
local field_formats = {
Species = "page",
Part_of_range = "string",
Reference = "string",
}
local cargo_results_formatted = tf.format_cargo_results(cargo_results, field_formats)
-- Final formatting of output
local html_theme_class = "-base"
if #cargo_results_formatted > 0 then
-- Begin creating the html table infobox template
local display_title = "<i>" .. title .. "</i>" .. " skull characters"
root = ibf.create_infobox_small(root, display_title, html_theme_class)
root = ibf.add_spanning_row(root, "<i>units in mm</i>", html_theme_class, "text")
root = ibf.add_header(root, "Skull", html_theme_class)
root = ibf.add_row(root, 'Condylobasal length:', cargo_results[1].Condylobasal_length, html_theme_class, "text")
root = ibf.add_row(root, 'Postmandibular canal:', cargo_results[1].Postmandibular_canal, html_theme_class, "text")
root = ibf.add_row(root, 'Shape:', cargo_results[1].Skull_shape, html_theme_class, "text")
root = ibf.add_header(root, "Dental", html_theme_class)
root = ibf.add_row(root, 'Upper unicuspids:', cargo_results[1].Number_unicuspids, html_theme_class, "text")
root = ibf.add_row(root, 'Unicuspid notes:', cargo_results[1].Unicuspid_notes, html_theme_class, "text")
root = ibf.add_row(root, 'Tines present:', cargo_results[1].Tines_upper_incisors, html_theme_class, "text")
root = ibf.add_row(root, 'Tine size:', cargo_results[1].Tine_size, html_theme_class, "text")
root = ibf.add_row(root, 'Tine position:', cargo_results[1].Tine_position, html_theme_class, "text")
root = ibf.add_row(root, 'Shape upper incisors:', cargo_results[1].Shape_medial_edge_incisors, html_theme_class, "text")
root = ibf.add_row(root, 'Dental characters:', cargo_results[1].Dental_characters, html_theme_class, "text")
return root
else
return "No results found."
end
end
-- =========================================================
-- Taxon name processor
-- =========================================================
function p.taxon_name_processor(frame)
-- process arguments
local args = getArgs(frame)
local taxon_name = args[1]
local focal_field = ts.rank_finder("Species", taxon_name)
-- get information from cargo
local tables = "Species"
local fields = "Species, Common_name"
local where_clause = {
where = string.format("%s = '%s'", "Species", taxon_name)
}
local cargo_results = cargo.query(tables, fields, where_clause)
-- Check if there are any results
if #cargo_results == 0 then
return "No common name found for " .. taxon_name
end
local common_name = cargo_results[1].Common_name
local output = common_name .. " (<i>[[" .. taxon_name .. "]]</i>)"
return output
end
-- =========================================================
-- Get taxon wiki category tag
-- =========================================================
function p.get_taxon_category_tag(frame)
local args = getArgs(frame)
local title = mw.title.getCurrentTitle().text
-- get taxon rank of parent page calling the module
local focal_rank = ts.rank_finder("Species", title)
if focal_rank then
local focal_rank_lowercase = rank_lookup[focal_rank].lowercase
local focal_rank_sentence_case = rank_lookup[focal_rank].sentence_case
local category_tag = "[[Category:" .. focal_rank_sentence_case .. "]]"
return category_tag
else return "A taxon category tag can't be returned because this taxon can't be found in the WhiskerWiki database!\n\n"
end
end
-- =========================================================
-- Get the approriate italics formatting for a title based on rank
-- =========================================================
function p.italic_title_for_taxon(frame)
local args = getArgs(frame)
local title = mw.title.getCurrentTitle().text
-- get taxon rank of parent page calling the module
local focal_rank = ts.rank_finder("Species", title)
-- Control loop to avoid Lua errors if taxon doesn't exist in Cargo
if not focal_rank then
return "An italic title template can't be produced because this taxon can't be found in the WhiskerWiki database!\n\n"
end
local focal_rank = rank_lookup[focal_rank].lowercase
local italic_title_template = ""
if focal_rank == "species" or focal_rank == "genus" then
-- check if the binomial contans "sp."
local contains_sp = title:find("sp%.") ~= nil
if contains_sp then
local substring = title:match("^%S+") -- this should extract the genus only and not "sp."
italic_title_template = "{{Italic title|string=" .. substring .. "|all=yes}}"
else
-- otherwise italicize everything
italic_title_template = "{{Italic title}}"
end
end
if focal_rank == "subgenus" then
local substring = title:gsub("%b()", ""):gsub("^%s*(.-)%s*$", "%1") or title
italic_title_template = "{{Italic title|string=" .. substring .. "|all=yes}}"
end
if focal_rank == "species group" then
local substring = title:match("^%S+%s+%S+")
italic_title_template = "{{Italic title|string=" .. substring .. "|all=yes}}"
end
-- Logic for Sorex-specific clades
if focal_rank == "clade I" then
local substring = title:match("^%S+") -- this should extract the genus only and not "sp."
italic_title_template = "{{Italic title|string=" .. substring .. "|all=yes}}"
elseif focal_rank == "clade II" then
local substring = title:match("^%S+%s+%S+")
italic_title_template = "{{Italic title|string=" .. substring .. "|all=yes}}"
end
-- Use frame:preprocess to ensure the template is interpreted as wikitext
return frame:preprocess(italic_title_template)
end
return p