Jump to content

Module:String utilities

From HopperWiki

Documentation for this module may be created at Module:String utilities/doc

--[[
Module:String utilities
Part of the Module:Utilities family.

Covers pure string and table-of-strings operations — no Cargo, no wiki output.
All functions are re-exported through Module:Utilities for backward compatibility.

Functions:
  sc                          Safe concatenation (nil-safe)
  format_delimited_string     Normalise spacing around a delimiter
  parse_csv_to_table          CSV string → trimmed array
  parse_string_to_table       Delimited string → formatted string (legacy; may be overly complex)
  list_to_csv_string          Array → comma-separated string
  count_comma_elements        Count items in a CSV string
  count_double_tilde_elements Count items in a ~~-delimited string
  in_table                    Check if a string exists in a table (array or dict mode)
  element_sandwicher          Wrap each array element in start/end characters
  italicizer                  Apply <i> tags inside or around array elements
  species_italicizer          italicizer wrapper — italicise full string when no parens
  parentheses_italicizer      italicizer wrapper — italicise only inside parens
  italicize_parenthetical     Single-string wrapper around parentheses_italicizer, #invoke-able
  word_italicizer             Italicise specific words within a string
  extract_parenthetical       Extract text inside the first set of parentheses
  strip_parenthetical         Remove parenthetical suffix from a string
  title_to_sci                Extract scientific name from a wiki page title
  remove_file_prefix          Strip leading "File:" from a filename
  add_file_prefix_gentle      Add "File:" prefix safely (handles existing prefixes and links)
  quote_sql_value             Single-quote and escape a value for SQL use
  escape_string_for_holds     Escape a string for use in a Cargo HOLDS clause
  escape_string_for_sql       Escape a string for use in a standard Cargo SQL clause
--]]


local str = {}


-- ============================================================
-- Safe concatenation
-- ============================================================

-- Returns "" for nil values instead of raising an error.
function str.sc(...)
    local parts = { ... }
    for i = 1, #parts do
        if parts[i] == nil then
            parts[i] = ""
        else
            parts[i] = tostring(parts[i])
        end
    end
    return table.concat(parts)
end


-- ============================================================
-- Delimited string helpers
-- ============================================================

--[[
Ensures there is exactly one space after each occurrence of the delimiter.

  str.format_delimited_string("apple,banana,carrot")        → "apple, banana, carrot"
  str.format_delimited_string("apple;banana", ";")          → "apple; banana"
--]]
function str.format_delimited_string(input_string, delimiter)
    delimiter = delimiter or ","
    return input_string:gsub(delimiter .. "%s*", delimiter .. " ")
end


--[[
Splits a comma-delimited string into a trimmed array.
Returns {} for nil input.

  str.parse_csv_to_table(" value1 , value2 ,value3 ") → {"value1", "value2", "value3"}
--]]
function str.parse_csv_to_table(csv_string)
    if csv_string == nil then return {} end
    if type(csv_string) ~= "string" then
        error("Invalid argument: csv_string must be a string")
    end
    local t = {}
    for field in csv_string:gmatch("[^,]+") do
        table.insert(t, mw.text.trim(field))
    end
    return t
end


--[[
Parses a comma-delimited string into a single formatted string.
Supports "page" (wiki links) and "ext_link" (external links) formats.
Carried forward as-is; may be a candidate for future simplification.
--]]
function str.parse_string_to_table(text, format, link_text)
    if text then
        local text_table = {}
        for item in text:gmatch("[^,]+") do
            if format == "page" then
                item = "[[" .. item .. "]]"
            elseif format == "ext_link" then
                item = "[" .. item .. " " .. link_text .. "]"
            else
                item = item:gsub(",", ", ")
            end
            table.insert(text_table, item)
        end
        local full_text = table.concat(text_table, ", ")
        return full_text
    else
        return full_text  -- note: intentional carry-forward of original behaviour
    end
end


-- Joins an array into a comma-separated string. Returns nil for empty input.
function str.list_to_csv_string(list)
    if type(list) ~= "table" or #list == 0 then return nil end
    return table.concat(list, ", ")
end


-- Counts elements in a CSV string. Returns 0 for nil or empty input.
function str.count_comma_elements(input_string)
    if not input_string or input_string == "" then return 0 end
    local count = 0
    for _ in string.gmatch(input_string, "[^,]+") do
        count = count + 1
    end
    return count == 0 and 1 or count
end


-- Counts non-blank elements in a ~~-delimited string.
function str.count_double_tilde_elements(input_string)
    if not input_string or input_string == "" then return 0 end
    local parts = mw.text.split(input_string, "~~", true)
    local count = 0
    for i = 1, #parts do
        if mw.text.trim(parts[i]) ~= "" then count = count + 1 end
    end
    return count
end


-- ============================================================
-- Table searching
-- ============================================================

--[[
Checks whether a string exists in a table.

  mode    "array" (default) — search values
          "dict"            — search keys
  partial  true             — allow substring matches
--]]
function str.in_table(tbl, s, mode, partial)
    mode    = mode    or "array"
    partial = partial or false

    if mode == "dict" then
        if partial then
            for key, _ in pairs(tbl) do
                if string.find(key, s) then return true end
            end
            return false
        else
            return tbl[s] ~= nil
        end
    else
        for _, value in ipairs(tbl) do
            if partial then
                if string.find(value, s) then return true end
            else
                if value == s then return true end
            end
        end
        return false
    end
end


-- ============================================================
-- Array formatting
-- ============================================================

-- Wraps each element of an array with start_char and end_char.
function str.element_sandwicher(array, start_char, end_char)
    local result = {}
    for i = 1, #array do
        if array[i] then
            result[i] = start_char .. array[i] .. end_char
        else
            result[i] = start_char .. "No value" .. end_char
        end
    end
    return result
end


--[[
Applies <i>...</i> formatting to elements of an array.

  italicize_full_if_no_parens = true   → italicise the whole string when no parens found
                              = false  → italicise only text inside parentheses
--]]
function str.italicizer(array, italicize_full_if_no_parens)
    local result = {}
    for _, value in ipairs(array) do
        local start_index, end_index = value:find("%b()")
        local italicized_value

        if start_index then
            local inside = value:sub(start_index + 1, end_index - 1)
            italicized_value =
                value:sub(1, start_index - 1) ..
                "(<i>" .. inside .. "</i>)" ..
                value:sub(end_index + 1)
        elseif italicize_full_if_no_parens then
            italicized_value = "<i>" .. value .. "</i>"
        else
            italicized_value = value
        end

        table.insert(result, italicized_value)
    end
    return result
end

-- Italicise the full string when no parentheses are present (species page titles).
function str.species_italicizer(array)
    return str.italicizer(array, true)
end

-- Italicise only text inside parentheses.
function str.parentheses_italicizer(array)
    return str.italicizer(array, false)
end

--[[
Single-string wrapper around parentheses_italicizer, for use directly from
wikitext via #invoke (e.g. italicising the species binomial in a page title
like "Desert locust (Schistocerca gregaria)" wherever it appears as plain
text, such as a breadcrumb pill).
--]]
function str.italicize_parenthetical(frame)
    local text = frame.args and frame.args[1] or frame
    if not text or text == "" then return text end
    return str.parentheses_italicizer({ text })[1]
end


--[[
Italicises specific words within a string using wiki ''markup''.

  str.word_italicizer("Homo sapiens is cool", {"Homo sapiens"})
  → "''Homo sapiens'' is cool"
--]]
function str.word_italicizer(text, words_to_italicize)
    for _, word in ipairs(words_to_italicize) do
        text = text:gsub(
            "(%f[%a]" .. word:gsub("(%W)", "%%%1") .. "%f[%A])",
            "''%1''"
        )
    end
    return text
end


-- ============================================================
-- Parenthetical extraction / stripping
-- ============================================================

-- Returns the text inside the first set of parentheses, or the full string.
local function extract_parenthetical(input_string)
    return input_string:match("%((.-)%)") or input_string
end
str.extract_parenthetical = extract_parenthetical


-- Removes the parenthetical suffix and trims whitespace.
-- "Oaxaca (state in Mexico)" → "Oaxaca"
function str.strip_parenthetical(text)
    return (text:gsub("%b()", ""):gsub("^%s*(.-)%s*$", "%1"))
end


-- Extracts the scientific name from a wiki page title of the form "Common Name (Genus species)".
function str.title_to_sci(s)
    local start_idx, end_idx = string.find(s, "%b()")
    if start_idx and end_idx then
        return string.sub(s, start_idx + 1, end_idx - 1)
    else
        return s
    end
end


-- ============================================================
-- File name helpers
-- ============================================================

-- Strips a leading "File:" prefix if present.
function str.remove_file_prefix(file_name)
    local prefix = "File:"
    if string.sub(file_name, 1, #prefix) == prefix then
        return string.sub(file_name, #prefix + 1)
    end
    return file_name
end


--[[
Adds a "File:" prefix, handling existing prefixes and full File: links gracefully.
Returns "" for nil or blank input.
--]]
function str.add_file_prefix_gentle(file_name)
    if not file_name or file_name == "" then return "" end
    file_name = mw.text.trim(file_name)

    local inner_file = mw.ustring.match(file_name, "^%[%[File:([^|%]]+)")
    if inner_file then
        return "File:" .. mw.text.trim(inner_file)
    end

    if not mw.ustring.match(file_name, "^[Ff]ile:") then
        file_name = "File:" .. file_name
    end

    return file_name
end


-- ============================================================
-- SQL / Cargo escaping
-- ============================================================

--[[
Wraps a value in single quotes and escapes internal single quotes for SQL.
  str.quote_sql_value("O'Brien") → "'O''Brien'"
--]]
function str.quote_sql_value(value)
    value = mw.text.trim(value)
    return "'" .. (value:gsub("'", "''")) .. "'"
end


-- Escapes a string for use in a Cargo HOLDS clause (single-quote escaping only).
function str.escape_string_for_holds(input_string)
    if not input_string then return "" end
    local sanitized = mw.text.trim(input_string)
    sanitized = sanitized:gsub("'", "''")
    return sanitized
end


-- Escapes a string for use in a standard Cargo SQL equality clause.
function str.escape_string_for_sql(input_string)
    if not input_string then return "" end
    local sanitized = mw.text.trim(input_string)
    sanitized = sanitized:gsub("[#%%]", "")
    sanitized = sanitized:gsub("'", "''")
    return sanitized
end


return str
Cookies help us deliver our services. By using our services, you agree to our use of cookies.