Jump to content

Module:Gallery

From HopperWiki

This module builds MediaWiki <gallery> tags from either hand-built item lists or Cargo query results. It is the single place gallery HTML gets generated on HopperWiki — templates should never write <gallery> markup by hand.

Architecture

The module is layered so that a new gallery "flavor" (a new Cargo table, a new JSON dataset, a new domain like species or resources) can be added without touching the rendering layer.

Layer 1 · Render Layer 2 · Normalize Layer 3 · Pipeline Layer 4 · Flavor

p.build_gallery / p.build_modern_gallery. Takes a plain array of {image, caption, link, alt} items. Knows nothing about Cargo, JSON, or any data source.

p.items_from_cargo_rows. Converts raw Cargo rows into the item shape Layer 1 expects — grouping, captioning, link resolution. This is the seam: everything upstream is data-source-specific, everything downstream is plain gallery HTML.

p.cargo_gallery_pipeline. Reads #invoke args, runs the Cargo query, calls Layer 2, then Layer 1. Fully generic — works for any Cargo table given the right args.

Thin wrapper functions (e.g. p.get_species_gallery) that pre-fill Layer 3's args for one domain and delegate. This is the extension point — see #Adding a new flavor.

Design intent: keep new domains as Layer 4 wrappers around the existing pipeline, not new copies of the pipeline. Only reach for a new Layer 2 normalizer (parallel to items_from_cargo_rows) if a data source genuinely isn't Cargo-shaped — e.g. a JSON-backed gallery. Don't build a config-driven meta-system to unify Cargo and JSON sources pre-emptively; two concrete flavors is not yet a pattern that needs its own abstraction.

Functions

p.build_gallery(items, options)

Lowest-level builder. Emits a raw <gallery> tag.

Parameter Notes
items Array of tables: image (required — filename, with or without File: prefix), caption (optional, may contain wikilinks), link (optional click target), alt (optional alt text)
options.mode packed, packed-hover, nolines, traditional, slideshow
options.heights / widths e.g. "200px"
options.perrow Images per row
options.caption Overall gallery caption (not per-image)
options.showfilename "yes" or "no"

p.build_modern_gallery(items, options)

Same signature as build_gallery, wrapped in a modern-gallery styled <div>. Adds options.show_captions ("hover" default, or "always") which picks packed-hover vs nolines mode automatically when options.mode isn't set explicitly. Used directly by Module:Geography's species_gallery_for_geography, which builds its own items from a JSON dataset rather than Cargo.

p.items_from_cargo_rows(cargo_results, config)

Normalizes raw Cargo rows into the item shape build_gallery expects. This is the function to reuse if you're writing a new Cargo-backed flavor by hand rather than going through cargo_gallery_pipeline.

Parameter Notes
config.focal_field Required. Field whose value becomes the caption text and the grouping key.
config.image_field Required. Field containing the image filename. File: prefix is added automatically if missing (case-insensitive check).
config.placeholder_image Fallback image used when a row's image field is empty.
config.name_array Optional ordered array of focal_field values — controls both which rows are included and their display order. Omit to include everything in query order.
config.link_config.image_link_field Cargo field to read for the image's click target. Default "image_link".
config.link_config.caption_link_field Cargo field to read for the caption's link target. Default "caption_link".
config.link_config.enable_caption_links Boolean, default true. Set false to render captions as plain text with no wikilink.
config.link_config.fallback_image_link / fallback_caption_link Used when the row has no value for the configured link field. If omitted, both default to the row's focal_field value — i.e. images and captions link to the item's own page unless told otherwise.
config.link_config.no_link_field Optional field name. When a row's value for this field is truthy (anything except empty/"0"/"false"/"no"), that row's caption renders as plain text, overriding enable_caption_links for that row only — other rows in the same gallery are unaffected. Use this to mix "will eventually get a page" items (still linked/red-linked) with "will never get its own page" items (e.g. an external organization or a person who's intentionally data-only) inside a single gallery.

Returns items, nil on success, or nil, error_message if there's nothing to render (empty results, or no rows matched a given name_array). Callers must check for nil before passing to build_gallery.

The generic #invoke entry point. Runs a Cargo query from template args, then Layers 2 and 1.

Required args

Parameter Notes
cargo_table Cargo table to query.
cargo_fields Comma-separated fields to fetch.

Field configuration

Parameter Default Notes
cargo_focal_field — Shorthand that sets both search_field and display_field to the same field. Kept for backward compatibility with older templates.
search_field cargo_focal_field Field used in the WHERE clause.
search_field_type cargo_focal_field_type, else "string" Passed to Module:Cargo query utilities' build_where_clause — use "list_of_string" for HOLDS-type fields.
display_field cargo_focal_field Field shown as the caption/grouping key. Can differ from search_field.
image_field "Image" Field holding the image filename.
placeholder_image "No image available.svg" Used when a row's image field is empty.

Filtering

Parameter Notes
names Comma-separated values to include. Also controls display order (see name_array above). Filters on search_field.
not_values Comma-separated values to exclude.
cargo_where Escape hatch — raw WHERE clause, bypasses names/not_values entirely when set.
limit Hard cap passed to the Cargo query.

Linking (optional)

Parameter Notes
image_link_field Cargo field for the image's click target. If omitted, images link to their own display_field page.
caption_link_field Cargo field for the caption's link target. If omitted, captions link to their own display_field page.
enable_caption_links "false" to render plain-text captions with no link. Anything else (including omitted) means true.
fallback_image_link / fallback_caption_link Override the "link to own page" default without pointing at a Cargo field.
no_link_field Cargo field to check per-row for an "opt out of linking" flag (truthy value means plain text, overriding enable_caption_links for that row only). Use this to mix linkable and never-linkable items in one gallery — e.g. a boolean Never_gets_page column on the Organization or Person table.

Display

Parameter Default Notes
gallery_mode "packed" Passed straight through to build_gallery's options.mode.
heights "200px" Passed straight through to build_gallery's options.heights.
widths (unset) Passed straight through to build_gallery's options.widths. Omitted entirely unless given.
perrow (unset) Passed straight through to build_gallery's options.perrow.

p.get_species_gallery(frame)

Species-flavored wrapper. Pre-fills cargo_gallery_pipeline's args for the common case — querying Image filtered by Species — then delegates. See #Adding a new flavor for the pattern this demonstrates.

Notable implementation detail: it edits frame.args directly rather than calling getArgs(frame) first. getArgs returns a copy; edits to that copy would not be visible to cargo_gallery_pipeline's own getArgs(frame) call. Any new wrapper that pre-fills args before delegating to cargo_gallery_pipeline must follow the same approach.

Examples

Generic pipeline, minimal args

{{#invoke:Gallery
|cargo_gallery_pipeline
|cargo_table=Species
|cargo_fields=Species, Image
|cargo_focal_field=Species
}}

From templates/Species gallery.wiki. Images and captions both link to each species' own page (no image_link_field/caption_link_field given, so the default-to-own-page fallback applies).

{{#invoke:Gallery|get_species_gallery
|cargo_table=Image
|cargo_fields=Species, Image,
|search_field=Species
|search_field_type=list_of_string
|display_field=Species
|image_field=Image
|image_link_field=Image
|caption_link_field=Species
|enable_caption_links=false
|gallery_mode=packed-hover
}}

From templates/Images for species.wiki. Note enable_caption_links=false — captions render as plain species names, no link — while the image itself still links via image_link_field=Image (clicking the photo opens the file page).

Pass-through wrapper with a single templated arg

{{#invoke:Gallery|cargo_gallery_pipeline|limit={{{limit|}}}}}

From templates/Get cargo gallery.wiki — the rest of the args are expected to be supplied by whatever calls this template.

Adding a new flavor

To add a gallery for a new Cargo-backed domain (a new table, or the same table queried differently):

  1. Write a wrapper function p.get_<domain>_gallery(frame) in this module, modeled on p.get_species_gallery.
  2. Edit frame.args directly (not a getArgs(frame) copy) to pre-fill cargo_table, cargo_fields, search_field, display_field, image_field, and any link/display defaults specific to the domain.
  3. Delegate to p.cargo_gallery_pipeline(frame) — do not duplicate the query/normalize/render logic.
  4. Add a template under templates/ that calls {{#invoke:Gallery|get_<domain>_gallery|...}}, exposing only the args callers actually need to vary.

For a JSON-backed (non-Cargo) domain, follow Module:Geography's species_gallery_for_geography as the reference: build the items array by hand (matching the {image, caption, link, alt} shape) and call p.build_modern_gallery directly, skipping Layers 2–3 entirely since they're Cargo-specific. Only extract a shared JSON-side normalizer (parallel to items_from_cargo_rows) once a second JSON-backed gallery actually needs one — don't build it speculatively.

Known gotchas

Situation What happens Fix
Image filename already has a lowercase file: prefix build_gallery's prefix check is case-sensitive (^File:) — a lowercase prefix isn't recognized as already-prefixed and gets double-prefixed Don't pre-prefix filenames when building items by hand; let build_gallery or items_from_cargo_rows (which does a case-insensitive check) add it
Breaks the gallery line's pipe-delimited syntax — but avoid feeding free-text fields into caption/link without checking
Calling items_from_cargo_rows or cargo_gallery_pipeline without checking for a nil return Passing nil items into build_gallery errors Always check the err second return before rendering; cargo_gallery_pipeline already does this and renders a gallery-error span instead

Module family

Module Role
Module:Arguments Frame argument processing (getArgs)
Module:String utilities parse_csv_to_table for names/not_values
Module:Cargo query utilities build_where_clause — schema-aware WHERE clause construction
Module:Geography Calls p.build_modern_gallery directly for its JSON-backed species_gallery_for_geography flavor

-- ================================================================================
-- Module Dependencies
-- ================================================================================
local cargo = mw.ext.cargo
local getArgs = require('Module:Arguments').getArgs -- for processing arguments
local str = require("Module:String utilities")
local cq = require("Module:Cargo query utilities")





-- ================================================================================
-- Main Table
-- ================================================================================
local p = {}






-- ================================================================
-- Build MediaWiki Gallery
--
-- Creates a <gallery> tag with fine-grained control over images,
-- captions, and links.
--
-- Args:
--   items: Array of tables with:
--     - image: Required. Filename (with or without "File:" prefix)
--     - caption: Optional. Caption text (can include wikilinks)
--     - link: Optional. Where to go when clicking image
--     - alt: Optional. Alt text for accessibility
--   options: Optional table with gallery-wide settings:
--     - mode: "packed", "packed-hover", "nolines", "traditional", "slideshow"
--     - heights: e.g., "150px"
--     - widths: e.g., "200px"
--     - perrow: Number of images per row
--     - caption: Overall gallery caption
--     - showfilename: "yes" or "no"
--
-- Returns:
--   Wikitext string with <gallery> tag
-- ================================================================
function p.build_gallery(items, options)
    options = options or {}
    
    -- Start gallery tag with options
    local gallery_attrs = {}
    if options.mode then table.insert(gallery_attrs, 'mode="' .. options.mode .. '"') end
    if options.heights then table.insert(gallery_attrs, 'heights="' .. options.heights .. '"') end
    if options.widths then table.insert(gallery_attrs, 'widths="' .. options.widths .. '"') end
    if options.perrow then table.insert(gallery_attrs, 'perrow="' .. options.perrow .. '"') end
    if options.caption then table.insert(gallery_attrs, 'caption="' .. options.caption .. '"') end
    if options.showfilename then table.insert(gallery_attrs, 'showfilename="' .. options.showfilename .. '"') end
    
    local gallery = "<gallery"
    if #gallery_attrs > 0 then
        gallery = gallery .. " " .. table.concat(gallery_attrs, " ")
    end
    gallery = gallery .. ">\n"
    
    -- Add each image
    for _, item in ipairs(items) do
        -- Build image line (str.file_title adds exactly one "File:", whatever the input carried)
        local line = str.file_title(item.image)
        if line ~= "" then
        
            -- Add link parameter if specified
            if item.link then
                line = line .. "|link=" .. item.link
            end
        
            -- Add alt parameter if specified
            if item.alt then
                line = line .. "|alt=" .. item.alt
            end
        
            -- Add caption if specified
            if item.caption then
                line = line .. "|" .. item.caption
            end
        
            gallery = gallery .. line .. "\n"
        end
    end
    
    gallery = gallery .. "</gallery>"
    
    return gallery
end




-- ================================================================
-- Build Modern Gallery (enhanced styling wrapper)
--
-- Wraps the generic gallery builder with modern CSS class.
-- Uses same API as build_gallery() but applies modern theme.
--
-- Args: Same as build_gallery()
-- Returns: Gallery HTML wrapped in modern-gallery div
-- ================================================================
function p.build_modern_gallery(items, options)
    options = options or {}
    
    -- NEW: Allow caption display control
    local show_captions = options.show_captions or "hover"  -- "hover" or "always"
    
    -- Set mode based on caption preference
    if not options.mode then
        if show_captions == "always" then
            options.mode = "nolines"  -- Captions always visible
        else
            options.mode = "packed-hover"  -- Captions on hover (default)
        end
    end
    
    if not options.heights then 
        options.heights = "200px" 
    end
    
    local gallery = p.build_gallery(items, options)
    local class = "modern-gallery"
    if options.variant then
        class = class .. " modern-gallery--" .. options.variant
    end
    return '<div class="' .. class .. '" style="width: 100%;">\n' .. gallery .. '\n</div>'
end




-- ================================================================
-- Hand-curated modern gallery (template entry point)
--
-- Lets a page list items by hand instead of pulling them from Cargo.
-- Each item is a numbered set of parameters:
--   imageN: Required. File name (with or without "File:" prefix).
--           Plain positional params work too: {{Modern gallery|A.pdf|B.pdf}}
--   labelN: Optional. Caption text (defaults to the file name)
--   urlN:   Optional. Target for both the image and the caption link.
--           External URLs (http/https) become [url label]; anything
--           else is treated as a wiki page. When omitted, the file
--           itself is the target (so an uploaded PDF opens directly).
--   linkN:  Optional. Override the image target only.
--   sourceN: Optional. Publisher shown as a small label above the title
--           (documents variant only).
-- Gallery-wide: widths, heights, perrow,
--   show_captions ("always" default, or "hover"),
--   variant ("documents" is automatic for all-PDF galleries; "none" opts out),
--   meta ("no" hides the automatic "PDF · 12 pages · 3.4 MB" line).
--
-- Returns: Preprocessed modern gallery
-- ================================================================
function p.manual_gallery(frame)
    local args = getArgs(frame)

    local function is_external(target)
        return target:match("^https?://") ~= nil
    end

    -- "PDF · 12 pages · 3.4 MB" from the file's own metadata (file info is an
    -- expensive lookup, so only done for the documents variant)
    local function file_meta(image)
        local title = mw.title.new(image, "File")
        local file = title and title.file
        if not (file and file.exists) then return nil end
        local parts = {}
        local ext = image:match("%.(%w+)$")
        if ext then table.insert(parts, ext:upper()) end
        if file.pages and #file.pages > 0 then
            table.insert(parts, #file.pages == 1 and "1 page" or (#file.pages .. " pages"))
        end
        if file.size then
            if file.size >= 1048576 then
                table.insert(parts, string.format("%.1f MB", file.size / 1048576))
            else
                table.insert(parts, math.max(1, math.floor(file.size / 1024 + 0.5)) .. " KB")
            end
        end
        return table.concat(parts, " · ")
    end

    -- Collect entries first; the variant decides how captions are built
    local entries = {}
    local i = 1
    while args["image" .. i] or args[i] do
        local image = str.remove_file_prefix(mw.text.trim(args["image" .. i] or args[i]))
        local url = args["url" .. i] or frame:callParserFunction("filepath", image)
        table.insert(entries, {
            image = image,
            label = args["label" .. i] or image:gsub("%.%w+$", ""),
            url = url,
            link = args["link" .. i] or url,
            source = args["source" .. i],
        })
        i = i + 1
    end

    if #entries == 0 then
        return '<span class="gallery-error">No images given (use image1, label1, url1, ...)</span>'
    end

    -- All-PDF galleries default to the uncropped document look
    local variant = args.variant
    if not variant then
        variant = "documents"
        for _, entry in ipairs(entries) do
            if not entry.image:lower():match("%.pdf$") then
                variant = nil
                break
            end
        end
    end
    if variant == "none" then variant = nil end
    local documents = (variant == "documents")

    local items = {}
    for _, entry in ipairs(entries) do
        local caption
        if is_external(entry.url) then
            caption = string.format("[%s %s]", entry.url, entry.label)
        else
            -- Leading colon so File:/Category: targets link instead of embedding
            caption = string.format("[[:%s|%s]]", entry.url:gsub("^:", ""), entry.label)
        end

        if documents then
            caption = '<span class="modern-gallery__title">' .. caption .. '</span>'
            if entry.source then
                caption = '<span class="modern-gallery__eyebrow">' .. entry.source .. '</span>' .. caption
            end
            local meta = (args.meta ~= "no") and file_meta(entry.image)
            if meta and meta ~= "" then
                caption = caption .. '<span class="modern-gallery__meta">' .. meta .. '</span>'
            end
        end

        table.insert(items, {
            image = entry.image,
            link = entry.link,
            alt = entry.label,
            caption = caption,
        })
    end

    -- Documents render larger in the responsive grid; request sharper thumbs
    local gallery_output = p.build_modern_gallery(items, {
        variant = variant,
        widths = args.widths or (documents and "260px" or "175px"),
        heights = args.heights or (documents and "320px" or nil),
        perrow = args.perrow,
        show_captions = args.show_captions or "always",
    })

    return frame:preprocess(gallery_output)
end






-- ========================================
-- Species-specific gallery wrapper function
-- ========================================

--[[
This is slight overkill but this function is a wrapper around the cargo_gallery_pipeline that is more generic so it can handle
some upstream logic that is specific to the species theme. 
--]]
function p.get_species_gallery(frame)
    --[[ Work directly with frame.args instead of getArgs(). This is important because this is a
    parent function that needs to pass the frame to it's child and when you use getArgs() you're pulling
    out an additional copy of the arguments and editing them but those are no longer that is no longer part of the frame.
    --]]
    
    local args = frame.args
    
    -- Get the species name from page title or override
    local species_name
    if args.species and args.species ~= "" then 
        species_name = args.species
    else
        species_name = str.title_to_sci(mw.title.getCurrentTitle().text)
    end
    
    -- Set the required parameters directly on frame.args
    args.names = species_name
    args.cargo_table = args.cargo_table or "Image"
    args.search_field = args.search_field or "Species"
    args.search_field_type = args.search_field_type or "list_of_string"
    args.display_field = args.display_field or "Species"
    args.image_field = args.image_field or "Image"
    args.gallery_mode = args.gallery_mode or "packed"
    
    -- Build the field list
    local fields = {args.search_field, args.image_field}
    if args.display_field ~= args.search_field then
        table.insert(fields, args.display_field)
    end
    args.cargo_fields = table.concat(fields, ", ")
    
    -- Debug
    if args.debug == "true" then
        return string.format("Species: '%s', cargo_fields: '%s'", 
                           tostring(species_name), 
                           tostring(args.cargo_fields))
    end
    
    -- Call the existing function - it should now see our modified frame.args
    return p.cargo_gallery_pipeline(frame)
end





-- ================================================================
-- Normalize Cargo rows into gallery items
--
-- Takes raw Cargo query results and turns them into the {image, caption,
-- link, alt} item shape that build_gallery()/build_modern_gallery() expect.
-- This is the "querying meets rendering" seam: everything upstream of this
-- function is Cargo-specific, everything downstream is plain gallery HTML.
--
-- Args:
--   cargo_results: Array of Cargo rows
--   config: table with:
--     - focal_field: Field whose value becomes the caption/grouping key (required)
--     - image_field: Field containing the image filename (required)
--     - placeholder_image: Fallback image when image_field is empty (optional)
--     - name_array: Optional ordered array of focal_field values to include/order by
--     - link_config: Optional table controlling link behaviour:
--         - image_link_field: Field for image click target (default "image_link")
--         - caption_link_field: Field for caption link target (default "caption_link")
--         - enable_caption_links: boolean, default true
--         - fallback_image_link: default image link when field is absent
--         - fallback_caption_link: default caption link when field is absent
--         - no_link_field: Optional field name. When a row's value for this
--             field is truthy (anything except empty/"0"/"false"/"no"), that
--             row's caption is rendered as plain text, overriding
--             enable_caption_links for that row only. Use this for entities
--             that should never get a wiki page (e.g. an Organization or
--             Person row flagged "never gets its own page") while other rows
--             in the same gallery still get linked/red-linked normally.
--
-- Returns:
--   Array of gallery items, or nil + error message if there's nothing to show
-- ================================================================
function p.items_from_cargo_rows(cargo_results, config)
    if not cargo_results or type(cargo_results) ~= "table" or #cargo_results == 0 then
        return nil, "No images found for this query -- should be coming soon!"
    end

    config = config or {}
    local focal_field = config.focal_field
    local image_field = config.image_field
    local placeholder_image = config.placeholder_image
    local name_array = config.name_array
    local link_config = config.link_config or {}

    local image_link_field = link_config.image_link_field or "image_link"
    local caption_link_field = link_config.caption_link_field or "caption_link"
    local enable_caption_links = (link_config.enable_caption_links ~= false)
    local fallback_image_link = link_config.fallback_image_link
    local fallback_caption_link = link_config.fallback_caption_link
    local no_link_field = link_config.no_link_field

    local function link_is_suppressed(row)
        if not no_link_field then return false end
        local raw = row[no_link_field]
        if raw == nil then return false end
        local normalized = mw.text.trim(tostring(raw)):lower()
        return normalized ~= "" and normalized ~= "0" and normalized ~= "false" and normalized ~= "no"
    end

    local file_name_map = {}
    local ordered_names = {}

    for _, row in ipairs(cargo_results) do
        local name = row[focal_field]
        if name then
            local image = row[image_field] or placeholder_image
            image = str.file_title(image)

            local image_link_target
            if row[image_link_field] ~= nil then
                image_link_target = row[image_link_field]
            elseif row.link ~= nil then
                image_link_target = row.link
            else
                image_link_target = fallback_image_link or name
            end

            local caption_link_target
            if link_is_suppressed(row) then
                caption_link_target = ""
            elseif row[caption_link_field] ~= nil then
                caption_link_target = row[caption_link_field]
            else
                caption_link_target = fallback_caption_link or name
            end

            if not file_name_map[name] then
                file_name_map[name] = {}
                if not name_array then
                    table.insert(ordered_names, name)
                end
            end

            table.insert(file_name_map[name], {
                image = image,
                image_link = image_link_target,
                caption_link = caption_link_target,
            })
        end
    end

    if next(file_name_map) == nil then
        return nil, "No images found for the provided names!"
    end

    local names_to_use = (type(name_array) == "table" and #name_array > 0)
        and name_array or ordered_names

    local items = {}
    for _, name in ipairs(names_to_use) do
        for _, entry in ipairs(file_name_map[name] or {}) do
            local caption_text
            if enable_caption_links and entry.caption_link ~= "" then
                caption_text = "<center>[[" .. entry.caption_link .. "|" .. name .. "]]</center>"
            else
                caption_text = "<center>" .. name .. "</center>"
            end

            table.insert(items, {
                image = entry.image,
                link = entry.image_link,
                caption = caption_text,
            })
        end
    end

    return items
end




-- ========================================
-- The core gallery building pipeline
-- ========================================

function p.cargo_gallery_pipeline(frame)
    local args = getArgs(frame)
    
    -- ========================================
    -- Parse arguments (your existing logic)
    -- ========================================
    local gallery_mode = args.gallery_mode or "packed"
    local name_array = str.parse_csv_to_table(args.names)
    local not_values = str.parse_csv_to_table(args.not_values)
    local cargo_table = args.cargo_table
    local fields = args.cargo_fields
    -- Field configuration - all configurable from template
    local search_field = args.search_field or args.cargo_focal_field -- Field to search in for WHERE clause
    local display_field = args.display_field or args.cargo_focal_field -- Field to display in gallery
    local image_field = args.image_field or "Image" -- Field containing the image filename
    local search_field_type = args.search_field_type or args.cargo_focal_field_type or "string"
    local placeholder_image = args.placeholder_image or "No image available.svg"
    
    -- For backward compatibility, if cargo_focal_field is explicitly set, use it for both search and display
    if args.cargo_focal_field then
        search_field = args.cargo_focal_field
        display_field = args.cargo_focal_field
    end
    
    -- ========================================
    -- NEW: Parse enhanced linking arguments (optional)
    -- ========================================
    local image_link_field = args.image_link_field
    local caption_link_field = args.caption_link_field
    local enable_caption_links = (args.enable_caption_links ~= "false")
    local fallback_image_link = args.fallback_image_link
    local fallback_caption_link = args.fallback_caption_link
    local no_link_field = args.no_link_field

    -- ========================================
    -- NEW: Parse limit argument (optional)
    -- ========================================
    local limit = args.limit and tonumber(args.limit) or nil
    
    -- ========================================
    -- Build WHERE clause
    -- ========================================
    local cargo_where = args.cargo_where
    if not cargo_where or cargo_where == "" then
        cargo_where = cq.build_where_clause(search_field_type, search_field, name_array, not_values)
    end
    
    -- ========================================
    -- Dynamically add link fields to query if specified
    -- ========================================
    local query_fields = fields
    if image_link_field and image_link_field ~= "" then
        query_fields = query_fields .. ", " .. image_link_field
    end
    if caption_link_field and caption_link_field ~= "" then
        query_fields = query_fields .. ", " .. caption_link_field
    end
    if no_link_field and no_link_field ~= "" then
        query_fields = query_fields .. ", " .. no_link_field
    end

    local cargo_args = { where = cargo_where }

    -- Pass limit to Cargo query if specified
    if limit then
        cargo_args.limit = limit
    end
    
    -- ========================================
    -- Run Cargo query
    -- ========================================
    if not fields then
        return "ERROR: fields is nil. args.cargo_fields = " .. tostring(args.cargo_fields)
    end
    
    local cargo_results = cargo.query(cargo_table, query_fields, cargo_args)
    
    -- ========================================
    -- Post-process results
    -- ========================================
    if cargo_results and #cargo_results > 0 then
        for _, row in ipairs(cargo_results) do
            if image_link_field and row[image_link_field] then
                row.image_link = mw.text.trim(row[image_link_field])
            end
            
            if caption_link_field and row[caption_link_field] then
                row.caption_link = mw.text.trim(row[caption_link_field])
            end

            if no_link_field and row[no_link_field] then
                row.no_link = row[no_link_field]
            end
        end
    end

    -- ========================================
    -- Setup link configuration
    -- ========================================
    local link_config = {
        image_link_field = "image_link",
        caption_link_field = "caption_link",
        enable_caption_links = enable_caption_links,
        fallback_image_link = fallback_image_link,
        fallback_caption_link = fallback_caption_link,
        no_link_field = no_link_field and no_link_field ~= "" and "no_link" or nil
    }
    
    -- ========================================
    -- Generate gallery
    -- ========================================
    local items, err = p.items_from_cargo_rows(cargo_results, {
        focal_field = display_field,
        image_field = image_field,
        placeholder_image = placeholder_image,
        name_array = (#name_array > 0) and name_array or nil,
        link_config = link_config,
    })

    local gallery_wikitext
    if not items then
        gallery_wikitext = '<span class="gallery-error">' .. err .. '</span>'
    else
        gallery_wikitext = p.build_gallery(items, {
            mode = gallery_mode,
            heights = args.heights or "200px",
            widths = args.widths,
            perrow = args.perrow,
        })
    end

    return frame:preprocess(gallery_wikitext)
end









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