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:Mountains Prism

From Wikipedia, the free encyclopedia

--[[ v1.4
This module builds a wiki table that lists information pertinent to mountains. This module accomodates up to 500 lines
of mountains where each line consists of the elevation, wiki-linked name and up to three custom fields. This
module uses the wiki-linked name to find its corresponding Wikidata item (eid) and then retrieves the following
information from Wikidata: elevation, prominence, isolation, range, first ascent, country, coordinates

Version  Date         Changes
=======  ====         =========
1.0      Mar  8 2025  Initial release
1.1      Mar 23 2025  Various fixes for metric and imperial units
1.2      Apr  8 2025  Don't display entity id not found for non-existent wiki pages, fixed custom1 and coordinates bugs
1.3      Apr 18 2025  Support qid after page name for items that exist on Wikidata but not on Wikipedia
1.4      Jul  5 2025  Column order based on options order; support three independent custom fields
]]
local p = {}
--require('Module:Wd')
local Titled_coords = require('Module:Titled_coords')
local newBuffer = require('Module:OutputBuffer')

-- Wikidata properties
local WD_PROPERTIES = {
    elevation  = "P2044",
    prominence = "P2660",
    mtn_range  = "P4552",
    coords     = "P625",
    sig_event  = "P793",
    pt_in_time = "P585",
    isolation  = "P2659",
    country    = "P17"
}

-- Table column titles for easier translation
local COL_TITLES = {
    rank    = "Rank",
    name    = "Mountain / Peak",
    elev    = "Elevation",
    prom    = "Prominence",
    isoltn  = "Isolation",
    range   = "Subrange",
    fa      = "FA",
    coords  = "Coordinates",
    country = "Country",
    custom  = { "Notes", "Notes", "Notes"}
}

-- ** Runtime Option names **
local OPT_NAMES = {
    NAME   = "name",
    RANK   = "rank",
    ELEV   = "elev",
    PROM   = "prom",
    ISOLTN = "isolation",
    RANGE  = "range",
    FA     = "fa",
    COORDS = "coords",
    TCOORDS= "tcoords",
    CUSTOM1= "custom1",
    CUSTOM2= "custom2",
    CUSTOM3= "custom3",
    COUNTRY= "country",
    ORDER  = "order",
    DEBUG  = "debug"
}
local OPTIONS_SEP = ","
local RANK_ORDER = { ELEV = "E", PROM = "P"}

local UNITS = {
    METRE = "m", FOOT  = "ft",
    KILOMETRE = "km", MILE = "mi"
}

-- Runtime options about what information to display
local Options  = { debug = false, test = false, colOrder, showName = false, showFA = false, showProm = false,
                   showCoord = false, showTCoord = false, rankOrder = RANK_ORDER.ELEV,
                   showRank = false, showRange = false, showIsolation = false, 
                   showCountry = false, showCustom = { false, false, false} }

-- Information about a mountain from Module arguments and Wikidata.
local Mountain = { rank, wiki_link, page, name, eid, qid, elevation, elevation_wd = 0, elev_wd_unit,
                   prominence, prom_unit, range = "", isolation = "", fa = "",
                   coords, custom = {}, country }

-- "Constants" (Wikipedia not using LUA 5.4)
local QID_FIRST_ASCENT = "Q1194369"
local FA_UNKNOWN = "Unk"
local FA_ERROR   = "????"
local FS = "^"    -- field separator
local NBSP = " "
local COL_TITLE_SEP = ":"     -- do not use "="
local OPTIONS_SEP = ","
local NEWLINE = "\n"
local BR_NEWLINE ="<br/>"..NEWLINE
local TD = '|'
local TD_NL = NEWLINE .. TD
local WD_NOT_FOUND = -1        -- Not found on Wikidata
local WD_NF_TEXT = "Not Found"

local Errors, Dbgout

local function debug(msg)
    mw.log(msg)
    Dbgout = Dbgout .. msg .. BR_NEWLINE
end

local function debugv(var, value)
    local s = var .. "=" .. value
    mw.log(s)
    Dbgout = Dbgout .. s .. BR_NEWLINE
end

local function debugTable(var, table)
    if Options.debug then
        local s = var .. ":"
        for i=1,#table do
            s = s .. "[" .. i .. "]=" .. table[i] .. " "
        end
        mw.log(s)
    end
end

local function addError(msg)
    Errors = Errors .. msg .. BR_NEWLINE
    debug(msg)
end

local function colorize(text, color)
    return '<span style="color:' .. color .. '">' .. text .. "</span>"
end

local function errorStyle(text)
    return colorize(text, "red")
end

local function infoStyle(text)
    return colorize(text, "green")
end

-- *** Split a string based on a separator ***
local function split(istring, sep)
    -- if sep is null, use default
    if sep == nil then sep = FS end

    local t = {}
    for str in string.gmatch(istring, '([^'..sep..']+)') do
        table.insert(t, str)
    end
    return t
end

-- *** Strip newline character at end ***
local function stripNewline(value)
    local n = string.find(value, NEWLINE)
    if n then
        return string.sub(value, 1, n-1)
    end
    return value
end

-- *** Extract raw elevation/prominence/isolation value and unit from WD value ***
local function extractRaw(wdValue)
    local i, n
    local units = { " metre", UNITS.METRE, " foot", UNITS.FOOT, " kilometre", UNITS.KILOMETRE, " mile", UNITS.MILE }
    if wdValue == nil then return WD_NOT_FOUND end

    for i=1,#units,2 do
        n = string.find(wdValue, units[i])
        if n then
            -- remove thousands separator while we are it
            return string.sub(wdValue, 1, n-1):gsub(',', ''), units[i+1]
        end
    end

    return -1
end

-- *** Call {{convert}} ***
local function convert(frame, value, unit)
    debug("Converting "..value..unit)
    local result = frame:expandTemplate{title='convert', args= { value, unit, disp='out', abbr='values', comma='off'}}
    debugv("result", result)
    return result
end

-- *** Call {{elevation_cells} to format the values ***
local function getElevationCells(frame, elev, unit, flip)
    if frame.expandTemplate then
        if not flip then
            return frame:expandTemplate{title='elevation_cells', args= { elev, unit}}
        else
            debug("Converting with order=flip")
            return frame:expandTemplate{title='elevation_cells', args= { elev, unit, order='flip'}}
        end
    end

    return "{{elevation_cells|" .. elev .. "|" .. unit .. "}}"
end

-- *** Call {{convert}} to get table formatted cells ***
local function getConvertCells(frame, value, from_unit, to_unit, flip)
    local cells
    if not flip then
        cells = frame:expandTemplate{title='convert', args= { value, from_unit, to_unit, disp='table', sortable='on'}}
    else
        cells = frame:expandTemplate{title='convert', args= { value, from_unit, to_unit, disp='table', sortable='on', order='flip' }}
    end
    return cells
end

-- *** Get table formatted cells of the isolation ***
local function getIsolationCells(frame, isolation)
    local isoltn_cells

    if isolation and isolation ~= WD_NOT_FOUND and isolation ~= "" then
        isoltn_cells = getConvertCells(frame, isolation, UNITS.KILOMETRE, UNITS.MILE)
    else
        isoltn_cells = "&nbsp;||&nbsp;"
    end
    debugv("isoltn_cells", isoltn_cells)
    return isoltn_cells
end

-- *** Get table formatted cells of the topo prominence ***
local function getProminenceCells(frame, unit, prominence, prom_unit)
    local prom_cells, to_unit

    if prominence ~= nil and prominence ~= WD_NOT_FOUND and prominence ~= "" then
        local flip = prom_unit ~= unit
        debug("unit="..unit..";prom_unit="..prom_unit..";flip="..tostring(flip))
        if not flip then
            if unit == UNITS.FOOT then to_unit = UNITS.METRE else to_unit = UNITS.FOOT end
        else
            to_unit = unit
        end
        prom_cells = getConvertCells(frame, prominence, prom_unit, to_unit, flip)
    else
        prom_cells = "&nbsp;||&nbsp;"
    end
    debugv("prom_cells", prom_cells)
    return prom_cells
end

local function stripBrackets(value)
    local stripped = value;

    -- strip leading brackets if found
    local n = string.find(stripped, "%[%[")
    if n then
        stripped = string.sub(stripped, 3)
    end

    -- strip trailing brackets if found
    n = string.find(stripped, "%]%]")
    if n then
        stripped = string.sub(stripped,1,n-1)
    end

    if stripped then return stripped
    else return value end
end

-- *** Parse a wiki link ***
local function parseWikiLink(wikiLink, mtn)
    local wiki_link = wikiLink

    -- Extract a qid (optional) --
    local n = string.find(wiki_link, "@qid:")
    if n then
        mtn.qid = string.sub(wiki_link, n+5)
        wiki_link = string.sub(wiki_link, 1, n-1)
    else
        mtn.qid = nil
    end

    mtn.wiki_link = wiki_link
    local parts = split(wiki_link, "|")
    local page = parts[1]
    n = string.find(page, "%[%[")
    if n then
        page = string.sub(page,3)
    end
    n = string.find(page, "%]%]")
    if n then
        page = string.sub(page,1,n-1)
    end

    mtn.page = page

    if #parts == 2 then
        mtn.name = stripBrackets(parts[2])
    else
        mtn.name = stripBrackets(parts[1])
    end

    local msg = "wikilink="..wikiLink..";page="..page..";name="..mtn.name
    if mtn.qid then msg = msg .. ";qid="..mtn.qid end
    debug(msg)

    return page
end

--[[
    Retrieve a entity's property value from Wikidata. Unfortunately
    the interface only supports one property at a time.
]]
local function getWD(frame, eid, name, cmdFlag, multiple)
    if not frame.preprocess then
        return "{{Wikidata|property|" .. name .. "|eid=" .. eid .. "}}"
    end

    local args
    if not multiple then args = "property|" else args = "properties|" end
    if cmdFlag then args = args .. cmdFlag .. "|" end
    args = args .. name .. "|eid=" .. eid 
    local invoke = "{{#invoke:Wd|" .. args .. "}}"
    local value = frame:preprocess(invoke)
    debug(invoke .. " => " .. value)
    return value
end

-- ** Get a wiki linked property value **
local function getWDLinked(frame, eid, name)
    return getWD(frame, eid, name, "linked", false)
end

-- ** Get the raw value of a property value **
local function getWDRaw(frame, eid, name)
    return getWD(frame, eid, name, "raw", false)
end

-- ** Get an entity's property from Wikidata **
local function getWDProperty(frame, eid, name, linked)
    debug("eid=" .. eid .. " name=" .. name)

--[[    local stmts = mw.wikibase.getBestStatements(eid, name)
    if stmts ~= nil then
        mw.logObject(stmts)
        for i=1,#stmts do
            mw.log("stmts["..i.."]="..stmts[i]);
        end
    end ]]

    -- call Module:Wd using template syntax
    if frame.preprocess then
        local args = "property|"
        if linked then args = args .. "linked|" end
        args = args .. name .. "|eid=" .. eid 
        local invoke = "{{#invoke:Wd|" .. args .. "}}"
        local value = frame:preprocess(invoke)
        debug(invoke .. " => " .. value)
        return value
    end
    return "{{Wikidata|property|" .. name .. "|eid=" .. eid .. "}}"
end
--    if frame.expandTemplate then
--        local args = { ['1'] = name, ['page'] = page }
--        return wd._property({eid, args})
--        local args = { ['1'] = 'property', ['2'] = name, ['page'] = page }
--        return frame:expandTemplate{title='Wikidata', args= args}

local function stripCoords(wdCoords)
    --debugv("wdCoords", wdCoords)
    if Options.test then  -- this only shows up in console test mode
        local stripped = string.gsub(wdCoords, "\"`UNIQ%-%-templatestyles%-%d+%-QINU`\"", "")
        --debugv("stripped", stripped)
        return stripped
    end
    return wdCoords
end

-- ** Get country from Wikidata **
local function getCountryWD(frame, eid)
    local value = getWD(frame, eid, WD_PROPERTIES.country, nil, true)
    return value
end

-- *** Get location coordinates from Wikidata ***
local function getCoords(frame, eid)
    local wdCoords = getWDLinked(frame, eid, WD_PROPERTIES.coords)
    return stripCoords(wdCoords)
end

-- *** Call Module:Titled_coords to get titled coordinates ***
local function getTitledCoords(frame, eid, title)
    local raw_coords = getWDRaw(frame, eid, WD_PROPERTIES.coords)
    local fmt_coords = Titled_coords.build(raw_coords, title, "")
    debug(fmt_coords .. " => " .. fmt_coords)
    return stripCoords(fmt_coords)
end

local function getElevationWD(frame, eid)
    local value = getWD(frame, eid, WD_PROPERTIES.elevation, nil)
    return extractRaw(value)
end

-- *** Get FA year from Wikidata ***
local function getFirstAscentWD(frame, eid)
    if frame.preprocess == nil then return FA_ERROR end

    local names = WD_PROPERTIES.sig_event .. "|" .. QID_FIRST_ASCENT .. "|" .. WD_PROPERTIES.pt_in_time
    local value = frame:preprocess("{{#invoke:Wd|property|qualifier|" .. names .. "|eid=" .. eid .. "}}")
    debugv("FA value", value)
    if value == "" then
        debugv("No FA on Wikidata for eid", eid)
        return FA_UNKNOWN
    end

    -- FA deliberately set to unknown value
    if string.find(value, "(unknown)") then
        debugv("FA unknown for eid", eid)
        return FA_UNKNOWN
    end

    -- Find date such as: 10 July 1913
    local i1, i2 = string.find(value, "%(%d+%s%a*%s%d%d%d%d%)")
    if i1 then
        --mw.log("i1="..i1)
        local date = string.sub(value, i1+1, i2-1)
        debugv("FA date", date)
        local len  = string.len(date)
        local year = string.sub(date, len-4, len)
        return year
    end

    -- Find date with just the year; e.g. (2025)
    i1, i2 = string.find(value, "%(%d+%)")
    if i1 then
        local year = string.sub(value, i1+1, i2-1)
        return year
    end
    addError("Unknown FA date format for eid " .. eid .. ": " .. value)
    return FA_ERROR
end

-- *** get topgraphic isolation from Wikidata ***
local function getIsolationWD(frame, eid)
    local isoltn = getWD(frame, eid, WD_PROPERTIES.isolation, nil)
    return extractRaw(isoltn)
end

-- *** get prominence from Wikidata ***
local function getProminenceWD(frame, eid)
    local prom = getWD(frame, eid, WD_PROPERTIES.prominence, nil)
    return extractRaw(prom)
end

-- get mountain range from Wikidata. We want it wiki-linked for
-- the first occurrence of it in the output.
local function getRangeWD(frame, eid)
    return getWDLinked(frame, eid, WD_PROPERTIES.mtn_range)
end

-- Check elevations from input and Wikidata. If not equal, add an info
-- message to custom field 1. Note that this check is only done if
-- there is a custom field in the output.
local function checkElevations(frame, mtn, unit)
    if not mtn.eid then return end    -- v1.3 don't check if no WD item

    local elevation_wd = mtn.elevation_wd

    -- If output unit not the unit from WD, convert WD value to output unit
    if unit ~= mtn.elev_wd_unit and elevation_wd ~= WD_NOT_FOUND then
        elevation_wd = convert(frame, elevation_wd, mtn.elev_wd_unit)
    end

    if elevation_wd ~= 0 and mtn.elevation ~= elevation_wd then
        if string.len(mtn.custom[1]) ~= 0 then
            mtn.custom[1] = mtn.custom[1] .. "<br/>"
        end
        local elev_wd_v = elevation_wd
        if elev_wd_v == WD_NOT_FOUND then elev_wd_v = WD_NF_TEXT end
        local msg = 'Local/WD elevations: "' .. mtn.elevation .. '"/"' .. elev_wd_v .. '"'
        mtn.custom[1] = mtn.custom[1] .. infoStyle(msg)
    end
end

-- *** generate table header ***
local function genHeader(options, unit)
    local unit_1, unit_2, hct

    if unit == nil or unit == "" then
        unit = UNITS.METRE
    end

    if unit == UNITS.METRE then unit_1 = UNITS.METRE; unit_2 = UNITS.FOOT
    else unit_1 = UNITS.FOOT; unit_2 = UNITS.METRE
    end

    local s = "{| class=\"wikitable sortable\"\n!"

    -- Generate first header line based on options order (v1.4)
    for i=1,#options.colOrder do
        local col = options.colOrder[i]
       
        if col == OPT_NAMES.RANK then
            hct = " align=\"left\" rowspan=2|" ..COL_TITLES.rank .. "||"
        elseif col == OPT_NAMES.NAME then
            hct = "rowspan=2|" .. COL_TITLES.name
        elseif col == OPT_NAMES.ELEV then
            hct = "||colspan=2|" .. COL_TITLES.elev
        elseif col == OPT_NAMES.PROM then
            hct = "||colspan=2|" .. COL_TITLES.prom
        elseif col == OPT_NAMES.RANGE then
            hct = "||rowspan=2|" .. COL_TITLES.range
        elseif col == OPT_NAMES.FA then
            hct = "||rowspan=2|" .. COL_TITLES.fa
        elseif col == OPT_NAMES.TCOORDS or col == OPT_NAMES.COORDS then
            hct = "||rowspan=2|" .. COL_TITLES.coords
        elseif col == OPT_NAMES.CUSTOM1 then
            hct = "||rowspan=2|" .. COL_TITLES.custom[1]
        elseif col == OPT_NAMES.CUSTOM2 then
            hct = "||rowspan=2|" .. COL_TITLES.custom[2]
        elseif col == OPT_NAMES.CUSTOM3 then
            hct = "||rowspan=2|" .. COL_TITLES.custom[3]
        elseif col == OPT_NAMES.ISOLTN then
            hct = "||colspan=2|" .. COL_TITLES.isoltn
        elseif col == OPT_NAMES.COUNTRY then
            hct = "||rowspan=2|" .. COL_TITLES.country
        end
        s = s .. hct
    end

    -- Add second line headers for elevation, prominence, isolation
    s = s .. "\n|-\n"
    s = s .. '!' .. unit_1 .. '||' .. unit_2  -- elevation cells
    if options.showProm then s = s .. '||' .. unit_1 .. '||' .. unit_2 end
    if options.showIsolation then
        local iso_u1, iso_u2
        if unit == UNITS.METRE then iso_u1 = UNITS.KILOMETRE; iso_u2 = UNITS.MILE
        else iso_u1 = UNITS.MILE; iso_u2 = UNITS.KILOMETRE
        end
        s = s .. '||' .. iso_u1 .. '||' .. iso_u2
    end
    s = s .. NEWLINE
    if options.debug then debugv("header", s) end
    return s
end

local function finish()
    return "|}"        -- table end
end

local function handleCustomOption(n, options, columnTitle)
    options.showCustom[n] = true
    if columnTitle then COL_TITLES.custom[n] = columnTitle end
end

--[[ Process run options
     rank       - show ranking
     name       - override default name column title
     elev       - show elevation
     prom       - show prominence
     isolation  - show isolation
     range      - show mountain range or subrange from WD
     fa         - show first ascent (year only) from WD
     coords     - show coordinates from WD
     tcoords    - show titled coordinates from WD
     country    - show country
     custom1    - show custom field1 (e.g. Notes)
     custom2    - show custom field2
     custom3    - show custom field3
     debug      - generate debug information
]]
local function processOptions(runOptions)
    local o = Options
    if runOptions == nil then return o end
    runOptions = stripNewline(runOptions)
    debugv("runOptions", runOptions)

    o.colOrder = {}
    local parts = split(runOptions, OPTIONS_SEP)
    for i=1,#parts do
        option = parts[i]
        debugv("option", option)

        -- Strip out column title if given for an option
        local colTitle
        local n = string.find(option, COL_TITLE_SEP)
        if n then
            colTitle = string.sub(option, n+1)
            debugv("colTitle", colTitle)
            option = string.sub(option, 1, n-1)
         end

        local add_option = true
        if option     == OPT_NAMES.RANK    then o.showRank   = true
        elseif option == OPT_NAMES.NAME then
            if colTitle then COL_TITLES.name = colTitle end
            o.showName = true
        elseif option == OPT_NAMES.PROM    then o.showProm   = true
        elseif option == OPT_NAMES.ELEV    then
            if colTitle then COL_TITLES.elev = colTitle end
        elseif option == OPT_NAMES.FA      then o.showFA     = true
        elseif option == OPT_NAMES.PROM    then o.showProm   = true
        elseif option == OPT_NAMES.ISOLTN  then o.showIsolation = true
        elseif option == OPT_NAMES.COORDS  then
            o.showCoord  = true
            if colTitle then COL_TITLES.coords = colTitle end
        elseif option == OPT_NAMES.TCOORDS then
            o.showTCoord = true
            if colTitle then COL_TITLES.coords = colTitle end
        elseif option == OPT_NAMES.RANGE   then
            o.showRange  = true
            if colTitle then COL_TITLES.range = colTitle end
        elseif option == OPT_NAMES.CUSTOM1 then
            handleCustomOption(1, o, colTitle)
        elseif option == OPT_NAMES.CUSTOM2 then
            handleCustomOption(2, o, colTitle)
        elseif option == OPT_NAMES.CUSTOM3 then
            handleCustomOption(3, o, colTitle)
        elseif option == OPT_NAMES.COUNTRY then o.showCountry  = true
        elseif option == OPT_NAMES.ORDER then
            if colTitle and colTitle == "prom" then
                o.rankOrder = RANK_ORDER.PROM
            end
        elseif option == OPT_NAMES.DEBUG then
            o.debug = true; add_option = false
        else
            addError("Unknown option: " .. option)
            add_option = false
        end
        if add_option then
            table.insert(o.colOrder, option)
        end
    end   -- for
    --debugTable("colOrder", o.colOrder)
    return o
end

-- *** Get the Wikidata entity id ***
local function getEntityId(mtn)
    local page = mtn.page
    local eid = mw.wikibase.getEntityIdForTitle(page)
    if eid then return eid end

    debug("Cannot find entity id for page " .. page)
    if mw.title.new(page).exists then
        mtn.name = mtn.name .. BR_NEWLINE .. errorStyle("Cannot find entity id")
    else
        debug("Page not found: " .. page)
    end

    return mtn.qid
end

local function setMtnCustom(mtn, n, customText)
    local custom = stripNewline(customText)
    mtn.custom[n] = custom
    debugv("custom["..n.."]", custom)
end

local function initMountain(mtn, wikiLink, elev)
    local page = parseWikiLink(wikiLink, mtn)

    mtn.eid  = nil
    mtn.elevation = string.gsub(elev, " ", "")  -- strip spaces
    mtn.elevation_wd = 0
    mtn.elev_wd_unit = ""
    mtn.prominence = nil
    mtn.isolation = nil
    mtn.range = ""
    mtn.fa = ""
    mtn.coords = ""
    mtn.custom = { "", "", "" }
    return page
end

-- *** Process a mountain line ***
local function processLine(frame, options, line)
    local parts, n, wl_name, elevatn, page
    local has_custom = {false, false, false}

    -- line contains elevation, wiki link and optionally up to three custom values
    parts = split(line, FS)
    if #parts >= 3 then has_custom[1] = true end
    if #parts >= 4 then has_custom[2] = true end
    if #parts == 5 then has_custom[3] = true end
    if #parts < 2 then
        local m = "<br/>Bad format on argument (<nowiki>" .. line .. "</nowiki>) -- skipped"
        Errors = Errors .. m
        mw.log(m)
        return nil
    end

    elevatn = parts[1]       -- either in metres or feet
    wl_name = parts[2]       -- wiki-linked name and optional qid
    if not wl_name then
        addError("wiki-linked name is empty")
        return nil
    end

    local mtn = Mountain
    page = initMountain(mtn, wl_name, elevatn)

    --- Set mountain's custom fields from input line
    for i=1,#has_custom do
        if options.showCustom[i] and has_custom[i] then
            setMtnCustom(mtn, i, parts[i+2])
        end
    end

    -- Get the Wikidata entity id
    local eid = getEntityId(mtn)
    if not eid then return mtn end

    debug("page="..page .. ",eid=" .. eid)
    mtn.eid = eid

    mtn.elevation_wd, mtn.elev_wd_unit = getElevationWD(frame, eid)
    if mtn.elevation_wd ~= WD_NOT_FOUND then
        debug("WD elevation=" .. mtn.elevation_wd .. " unit=" .. mtn.elev_wd_unit)
    else
        debug("No elevation on Wikidata for eid=" .. eid)
    end

    if options.showProm then
        mtn.prominence, mtn.prom_unit = getProminenceWD(frame, eid)
        if mtn.prominence ~= WD_NOT_FOUND then
            debug("WD prominence=" .. mtn.prominence .. " unit=" .. mtn.prom_unit)
        else
            debug("No prominence on Wikidata for eid=" .. eid)
        end
    end

    if options.showIsolation then
        mtn.isolation = getIsolationWD(frame, eid)
        debugv("mtn.isolation", mtn.isolation)
    end

    if options.showRange then
        mtn.range = getRangeWD(frame, eid)
    end

    if options.showFA then
        mtn.fa    = getFirstAscentWD(frame, eid)
    end

    if options.showCoord then
        mtn.coords = getCoords(frame, eid)
    elseif options.showTCoord then
        mtn.coords = getTitledCoords(frame, eid, mtn.name)
    end

    if options.showCountry then
        mtn.country = getCountryWD(frame, eid)
    end

    return mtn
end

-- *** Process a mountain range. Only display the linked range once. ***
local function processRange(ranges, mtn)
    local found = false;
    local range = mtn.range

    -- Do we already have it?
    for k,v in pairs(ranges) do
        if v == range then
            found = true; break
         end
    end

    if not found then
        debug("Adding range " .. range)
        table.insert(ranges, range)
    else
        local i1, i2 = string.find(range, "|")
        if i1 then
            local ei = string.len(range) - 2  -- strip ending brackets
            name = string.sub(range, i1+1, ei)
        else
            local len = string.len(range)
            name = string.sub(range, 3, len-2)
        end
        mtn.range = name
    end
end

-- *** Main entry point ***
function p.list(frame)
    local debug_on = false
    local rank_number = 0
    local last_elev, last_prom = ""
    local same_rank = 0

    Errors = ""; Dbgout = ""
    --debugv("args[1]", frame.args[1])
    --debugv("args[2]", frame.args[2])
    --debugv("args[3]", frame.args[3])
    local unit  = frame.args[1]
    local options = processOptions(frame.args[2])
    if options.showCoord and options.showTCoord then
        return "<p>" .. errorStyle("Can only specify one of coords or tcoords") .. "</p>"
    end
    if not options.showName then
        return "<p>" .. errorStyle("\"name\" option is required") .. "</p>"
    end

    -- Create the output buffer and add the table header
    local getBuffer, print, printf = newBuffer()
    print(genHeader(options, unit))
    local ranges = {}
    local rankOrderElev = options.rankOrder == RANK_ORDER.ELEV

    -- Main processing loop
    for i=3,502,1 do
        local prom_cells, isoltn_cells
        local line = frame.args[i]
        if line == nil then break end

        debugv("line", line)
        local mtn = processLine(frame, options, line)
        if mtn then   -- only do if no error
            if options.showRank then
                if rankOrderElev then
                    if last_elev ~= mtn.elevation then
                        rank_number = rank_number + 1 + same_rank
                        mtn.rank = rank_number
                        last_elev = mtn.elevation
                        same_rank = 0
                    else
                        same_rank = same_rank + 1
                    end
                 else  -- ranked by prominence
                     if last_prom ~= mtn.prominence then
                         rank_number = rank_number + 1 + same_rank
                         mtn.rank = rank_number
                         last_prom = mtn.prominence
                         same_rank = 0
                    else
                        same_rank = same_rank + 1
                    end
                 end
            end

            if options.showRange and mtn.eid then
                processRange(ranges, mtn)
            end

            local elev_cells = getElevationCells(frame, mtn.elevation, unit)
 
            debug("elev=" .. mtn.elevation ..";elev_wd="..mtn.elevation_wd)
            if options.showCustom[1] then
                checkElevations(frame, mtn, unit)
                if mtn.custom[1] == "" then mtn.custom[1] = NBSP end
            end

            print("|-\n|")
            for i=1,#options.colOrder do
                local col = options.colOrder[i]
                debugv("col", col)
       
                if col == OPT_NAMES.RANK then
                    print("align=center|" .. mtn.rank .. "||")
                elseif col == OPT_NAMES.NAME then
                    print(mtn.wiki_link)
                elseif col == OPT_NAMES.ELEV then
                    printf("\n|%s\n", elev_cells)
                elseif col == OPT_NAMES.PROM then
                    local prom_cells = getProminenceCells(frame, unit, mtn.prominence, mtn.prom_unit)
                    printf(TD .. "%s\n", prom_cells)
                elseif col == OPT_NAMES.RANGE then
                   printf(TD .. "%s\n", mtn.range)
                elseif col == OPT_NAMES.FA then
                    printf(TD .. "%s\n", mtn.fa)
                elseif col == OPT_NAMES.TCOORDS or col == OPT_NAMES.COORDS then
                    printf(TD .. "%s\n", mtn.coords)
                elseif col == OPT_NAMES.CUSTOM1 then
                    printf(TD .. "%s\n", mtn.custom[1])
                elseif col == OPT_NAMES.CUSTOM2 then
                    printf(TD .. "%s\n", mtn.custom[2])
                elseif col == OPT_NAMES.CUSTOM3 then
                    printf(TD .. "%s\n", mtn.custom[3])
                elseif col == OPT_NAMES.ISOLTN then
                    isoltn_cells = getIsolationCells(frame, mtn.isolation)
                    printf(TD .. "%s\n", isoltn_cells)
                elseif col == OPT_NAMES.COUNTRY then
                    printf(TD .. "%s\n", mtn.country)
                end
            end -- for
         end -- if mtn
    end -- for

    print(finish())
    if debug_on then
        printf("</br><nowiki>%s</nowiki>\n")
    end

    if string.len(Errors) > 0 then
        printf("%s\n", errorStyle(Errors))
    end
    if options.debug and string.len(Dbgout) > 0 and not options.test then
        printf("<br/><span style=\"color:green\">Debug output<br/>\n%s</span>\n",  Dbgout)
    end

    return getBuffer('')
end

--[[
Test via Preview Window Debug console
  print(p.test())
]]

function p._test(doDebug)
    local frame = mw.getCurrentFrame()
--    if frame then mw.logObject(frame) end
--    local test_data = { {["e"]="3954",["p"]="[[Mount Robson]]",
--                                ["n"]="Highest point in the Canadian Rockies<ref name=robson/>"},
--                        {["e"]="3747",["p"]="[[Mount Columbia (Canada)|Mount Columbia]]",
--                                ["n"]="Highest point in [[Alberta]]<ref name=columbia/>"}
--                    }
    local sep = ","
    frame.args = {}
    frame.args[1]  = 'm'
    frame.args[2]  = OPT_NAMES.RANK..sep..
                     OPT_NAMES.RANGE..COL_TITLE_SEP.."Mtn Range"..sep..OPT_NAMES.FA
                     ..sep..OPT_NAMES.PROM
                     --..sep..OPT_NAMES.ISOLTN
                     ..sep..OPT_NAMES.TCOORDS
                     ..sep..OPT_NAMES.CUSTOM2..COL_TITLE_SEP.."Additional info"
                     -- ..sep..OPT_NAMES.COUNTRY
                     -- ..sep..OPT_NAMES.ORDER..COL_TITLE_SEP.."prom"
    if doDebug then frame.args[2] = frame.args[2] ..sep .. OPT_NAMES.DEBUG end

    frame.args[3]  = "3954"..FS.."[[Mount Robson]]"..FS.."Highest point in the Canadian Rockies<ref name=robson/>"
    frame.args[4]  = "3448"..FS.."[[Mount Saint Elias]]"..FS.."Canada/US border"
    frame.args[5]  = "3310"..FS.."[[Mount Vaux]]"..FS.."Ottertail Range"
    frame.args[6]  = "3204"..FS.."[[Ghost Mountain (Chaba Icefield)|Ghost Mountain]]"..FS.."<ref name=ghost/>"
    frame.args[7]  = "2433"..FS.."[[Saddle Mountain (Alberta)|Saddle Mountain]]"..FS
--[===[
    frame.args[3]  = "3954"..FS.."[[Mount Robson]]"..FS.."Highest point in the Canadian Rockies<ref name=robson/>"
    frame.args[4]  = "3747"..FS.."[[Mount Columbia (Canada)|Mount Columbia]]"..FS.."Highest point in [[Alberta]]<ref name=columbia/>"
    frame.args[5]  = "3731"..FS.."[[North Twin Peak]]"..FS.."Highest peak of The Twins Massif"
    frame.args[6]  = "3648"..FS.."[[Mount Clemenceau]]"..FS.."Named for [[Georges Clemenceau]], premier of France during WWI"
    frame.args[7]  = "3619"..FS.."[[Mount Alberta]]"..FS.."Most difficult +11,000 climbing objective<ref name=alberta/>"
    frame.args[8]  = "3618"..FS.."[[Mount Assiniboine]]"..FS.."Highest point in the Southern Rockies<ref name=assiniboine/>"
    frame.args[9]  = "3612"..FS.."[[Mount Forbes]]"..FS.."Highest point within the confines of [[Banff National Park|Banff Nat'l Park]]<ref name=forbes/>"
    frame.args[10] = "3567"..FS.."[[Mount Goodsir]]"..FS.."Two major summits: South Tower and North Tower (lowest)"
    frame.args[11] = "3556"..FS.."[[South Twin Peak]]"..FS.."Lowest peak of The Twins Massif"
    frame.args[12] = "3543"..FS.."[[Mount Temple (Alberta)|Mount Temple]]"..FS.."Highest point near [[Lake Louise, Alberta|Lake Louise]]<ref name=temple/>"
    frame.args[13] = "3425"..FS.."[[Resplendent Mountain]]"..FS.."tbd"
    frame.args[14] = "3204"..FS.."[[Ghost Mountain (Park Ranges)|Ghost Mountain]]"..FS.."<ref name=ghost/>"
--]===]
--    frame.args[7] = ""
--    frame.args[8] = "&nbsp;"
--[[ Commented out
    local fi = 3
    for i=1,#test_data do
        frame.args[fi] = test_data[i]["e"] .. FS .. test_data[i]["l"] .. FS .. test_data[i]["n"]
        fi = fi + 1
    end ]]

    Options.test = true
    return p.list(frame)
end

function p.testd()   -- test with debug
    return p._test(true)
end

function p.test()
    return p._test(false)
end

function p.test2()
    local s = '<a href="/wiki/Rainbow_Range_(Rocky_Mountains)" title="Rainbow Range (Rocky Mountains)">Rainbow Range</a>'
    --local i1,i2 = string.find(s,"%>(.*)%<%/a%>")
    local i1,i2 = string.find(s,"%b><")
    if i1 then 
        mw.log("i1=" .. i1 .. " i2=" .. i2)
        mw.log(string.sub(s,i1+1,i2-1))
    else
        mw.log("not found")
    end
end

function p.test3()
    mw.log(stripBrackets("[[abc]]"))
    mw.log(stripBrackets("def"))
    mw.log(stripBrackets("[[Mount Victoria (Bow Range)|Mount Victoria]]"))
end

function p.testF()   -- test Feet unit
    local frame = mw.getCurrentFrame()
    frame.args = {}
    local sep = OPTIONS_SEP
    frame.args[1]  = UNITS.FOOT
    frame.args[2]  = OPT_NAMES.DEBUG
                     ..sep.. OPT_NAMES.PROM
                     ..sep.. OPT_NAMES.FA
                     --sep.. OPT_NAMES_TCOORDS
                     ..sep.. OPT_NAMES.CUSTOM2

    frame.args[3]  = "12726"..FS.."[[Mount Crillon]]"..FS.."C note"
    frame.args[4]  = "10728"..FS.."[[Mount La Perouse]]"..FS.."La P note"
--    frame.args[4]  = " 8200"..FS.."[[Mount Abbe]]"..FS.."Abbe note"
--    frame.args[5]  = " 6780"..FS.."[[Mount Cooper (Alaska)|Mount Cooper]]"

    Options.test = true
    return p.list(frame)
end

function p.testQ()    -- Test @qid
    local frame = mw.getCurrentFrame()
    frame.args = {}
    frame.args[1]  = UNITS.METRE
    frame.args[2]  = "debug,fa,tcoords,custom2:Easiest route"

    frame.args[3]  = "2736"..FS.."[[Angle Peak (British Columbia)|Angle Peak]]@qid:Q22418313"..FS..""
    frame.args[4]  = "2656"..FS.."[[Byward Peak]]@qid:Q22369121"..FS.."test"
    Options.test = true
    return p.list(frame)
end

function p.testR()
    local frame = mw.getCurrentFrame()
    frame.args = {}
    frame.args[1]  = UNITS.METRE
    frame.args[2]  = "debug,fa,tcoords,custom2:Easiest route"

    frame.args[3]  = "3543"..FS.."[[Mount Temple (Alberta)|Mount Temple]]"..FS.."Moderate scramble on SW face"
    frame.args[4]  = "3492"..FS.."[[Mount Hungabee|Hungabee Mountain]]"..FS.."[[International Climbing and Mountaineering Federation|UIAA]] III 5.4 on West ridge"
    frame.args[5]  = "3464"..FS.."[[Mount Victoria (Bow Range)|Mount Victoria]]"..FS.."UIAA II on SE ridge, South Summitend"
    frame.args[6]  = "2860"..FS.."[[Aquila Mountain (British Columbia)|Aquila Mountain]]"..FS.."NA"
    Options.test = true
    return p.list(frame)
end

function p.testExists()
    local title = "Mount Logan"
    title = "Aquila Mountain (British Columbia)"
    local page = mw.title.new(title)
    local status;
    if page.exists then status = "exists" else status = "not found" end
    mw.log("page \"" .. title .. "\" " .. status)
end

function p.testCustom()
    local frame = mw.getCurrentFrame()
    frame.args = {}
    frame.args[1]  = UNITS.METRE
    frame.args[2]  = "rank,name,elev,custom1:Province,custom2:Easiest route,custom3:Scramble,tcoords"

    frame.args[3]  = "3543"..FS.."[[Mount Temple (Alberta)|Mount Temple]]"..FS.."Alberta"..FS.."Moderate scramble on SW face"..FS.."Yes"
    frame.args[4]  = "2860"..FS.."[[Aquila Mountain (British Columbia)|Aquila Mountain]]"..FS.."British Columbia"..FS.."NA"..FS.."No"
    Options.test = true
    return p.list(frame)
end

function p.testNoName()
    local frame = mw.getCurrentFrame()
    frame.args = {}
    frame.args[1]  = UNITS.METRE
    frame.args[2]  = "rank,elev,tcoords"
    Options.test = true
    return p.list(frame)
end

return p