Módulo:Jctint/core

local p = {} -- Package to be exported

-- Local version of string formatting function
local format = mw.ustring.format
-- Local version of string trimming function
local trim = mw.text.trim
-- Store this function in a local variable to avoid expensive table lookups.
local insert = table.insert

-- mw.html object for the generated row
local row
-- Default row span for all columns (`jspan` = "junction span")
local jspan
-- Any error messages produced that will be added to the output
local errorMsg = {}

-- A specification for self-closing HTML tag.
local selfClosing = {selfClosing = true}

---
-- Converts the distance specified in unit from `unit` specified in `unitdef`
-- to the other supported unit.
local function convert(unit, unitdef)
    if unit == nil or unitdef == nil then return {} end

    -- Import module to convert length.
    local util = require("Module:Road data/util")
    local lengths = util.convertLengths({[unitdef] = unit})
    if lengths.error then -- An error occurred during conversion.
        -- Add the transcluding page to an error tracking category.
        local page = mw.title.getCurrentTitle() -- Get transcluding page's title
        local pagename = page.prefixedText -- Extract page's full title as string
        -- Create category string
        local category = format("[[Category:Jctint template using non-numeric parameter values|# %s]]", pagename)
        insert(errorMsg, category) -- Add error category to error message table.
    end
    return lengths
end

--- Creates cells for the location columns.
local function locations(args)
    -- Unitary, e.g., state line
    local unitary = args.unitary -- Value to span all of the location columns
    if unitary then
        -- Text alignment of the cell contents, default to "left".
        local align = args.unitary_align or 'left'
        row:tag('td') -- Create unitary cell
            :attr('colspan', 3) -- spanning three possible columns
            :css('text-align', align)
            :wikitext(unitary) -- Store the contents of unitary in the cell.
        return
    end

    -- Create cells for regular location columns.

    -- Region, for disambiguation and potentially for display
    local region = args.region
    if region or args.region_special then
        -- Row span for region; must be specified to display a region cell.
        local regionSpan = args.regionspan
        if regionSpan then
            row:tag('td') -- Create a region cell
                :attr('rowspan', regionSpan)
                -- Store region text in the cell.
                -- `region_special` argument overrides wikilinked `region` argument.
                :wikitext(args.region_special or format("[[%s]]", region))
        end
    end

    -- Primary topic requires no specialization to supplied locations.
    local primaryTopic = args.primary_topic ~= 'no'

    -- Note below main text in the next column
    local sub1note = args.sub1_note -- check existence later
    -- Row span for the last location column, default to `jspan`
    local sub2span = args.sub2span or jspan

    -- Independent city
    local indepCityText -- Value to span both subdivision columns.
    if args.indep_city_special then
        indepCityText = args.indep_city_special -- Overrides `indep_city` argument.
    elseif args.indep_city then
        local indepCity = args.indep_city
        local cityLink -- Wikilink for independent city
        if primaryTopic then
            cityLink = format('[[%s]]', indepCity)
        elseif region then
            -- Specialize independent city to the region.
            cityLink = format('[[%s, %s|%s]]', indepCity, region, indepCity)
        end
        if cityLink then
            indepCityText = "[[Independent city|City]] of " .. cityLink
        end
    end
    if indepCityText then -- Display independent city.
        -- Text alignment of the cell contents, default to "left".
        local align = args.indep_city_align or 'left'
        local indepCityCell = row:tag('td') -- Create independent city cell
            :attr('colspan', 2) -- spanning two columns
            :attr('rowspan', sub2span) -- with the calculated row span.
            :css('text-align', align)
            :wikitext(indepCityText) -- Store the independent city in the cell.
        if sub1note then -- A note is provided.
            indepCityCell:tag('br', selfClosing) -- Add a line break to the cell.
            -- Add the note to the cell, within an HTML <small> tag.
            indepCityCell:tag('small'):wikitext(sub1note)
        end
        return
    end

    -- Create two cells for the first- and second-level subdivisions.

    -- First-level subdivision, e.g., county
    -- Name of the type of subdivision, e.g., "County" and "Parish"
    local sub1name = args.sub1name -- check existence later
    local sub1Text -- Value for first-level subdivision column.
    if args.sub1_special then
        sub1Text = args.sub1_special -- Overrides `sub1` argument.
    elseif args.sub1 then
        local sub1 = args.sub1
        if primaryTopic then
            -- Add type (if specified) to wikilink for first-level subdivision.
            local sub1Link = sub1name and format("%s %s", sub1, sub1name) or sub1
            sub1Text = format('[[%s|%s]]', sub1Link, sub1)
        elseif region and sub1name then
            -- Add type to first-level subdivision.
            local sub1Typed = trim(format('%s %s', sub1, sub1name))
            -- Specialize first-level subdivision, with type added, to the region.
            sub1Text = format('[[%s, %s|%s]]', sub1Typed, region, sub1)
        end
    end
    if sub1Text then -- Display first-level subdivision.
        -- Row span for first-level subdivision, default to `jspan`.
        local sub1span = args.sub1span or jspan
        local sub1Cell = row:tag('td') -- Create first-level subdivision cell
            :attr('rowspan', sub1span) -- with the calculated row span.
            :wikitext(sub1Text) -- Store the first-level subdivision in the cell.
        if sub1note then -- A note is provided.
            sub1Cell:tag('br', selfClosing) -- Add a line break to the cell.
            -- Add the note to the cell, within an HTML <small> tag.
            sub1Cell:tag('small'):wikitext(sub1note)
        end
    end

    -- Second-level subdivision, e.g., city and town
    local sub2Text -- Value for second-level subdivision column.
    if args.sub2_special then
        sub2Text = args.sub2_special -- Overrides `sub2` argument.
    elseif args.sub2 then
        local sub2 = args.sub2
        if sub2 == "none" or sub2 == "&nbsp;" then
            sub2Text = "&#8203;" -- Zero-width space
        elseif primaryTopic then
            sub2Text = format("[[%s]]", sub2)
        else
            local sub2Link = {sub2}
            local sub2Name = sub2
            -- Type of area, e.g., city and village, as a form of disambiguation
            local area = args.area
            if area then
                insert(sub2Link, format(' (%s)', area)) -- Add area to wikilink.
                local areas = { -- table of different area types
                    borough = "Borough",
                    city = "City",
                    community = "Community",
                    CDP = "Community",
                    hamlet = "Hamlet",
                    town = "Town",
                    village = "Village",
                    ["unorganized territory"] = "Unorganized Territory"
                }
                -- Add area name to displayed wikitext.
                sub2Name = format("%s of %s", areas[area], sub2Name)
            end
            insert(sub2Link, ", ")
            -- Some second-level subdivisions are not unique in a given region.
            -- `sub1dab` is the first-level subdivision to be used for disambiguation.
            local sub1dab = args.sub1dab
            if sub1dab and sub1name then
                insert(sub2Link, trim(format('%s %s', sub1dab, sub1name)) .. ", ")
            end
            if region then
                insert(sub2Link, region) -- Add region to wikilink
            end

            sub2Text = format("[[%s|%s]]", table.concat(sub2Link), sub2Name)
        end
    end
    if sub2Text then -- Display second-level subdivision.
        row:tag('td') -- Create second-level subdivision cell
            :attr('rowspan', sub2span) -- with the calculated row span.
            :wikitext(sub2Text) -- Store the second-level subdivision in the cell.
    end
end

--- Creates cells for the distance columns.
local function units(args)
    -- Alternate units, e.g., California's postmiles.
    local alt_unit = args.altunit
    if alt_unit then -- Alternate units override standard units.
        -- Row span (`auspan` = "alt[ernate] unit span")
        local auspan = args.auspan or jspan
        -- Create the alternate unit cell as a header cell for the row,
        -- since it is usually unique within the table.
        row:tag('th'):attr('scope', 'row')
            :css('text-align', 'right')
            :attr('rowspan', auspan)
            :wikitext(alt_unit) -- Store the contents of alt_unit in the cell.
    else
        -- Convert numeric distances to a secondary unit, and display both units.
        -- Distance in the primary unit, or 'none'
        local unit = args.unit
        -- If `unit` is "none", no cells are displayed.
        if unit == "none" then return end
        local unitdef = args.unitdef or "km" -- The primary unit ('mi' or 'km')
        -- Convert and format the distance.
        local lengths = convert(unit, unitdef)
        -- Row span (`uspan` = "unit span")
        local uspan = args.uspan or jspan
        -- Create the primary unit cell as a header cell for the row,
        -- since it is usually unique within the table.
        local primary = row:tag('th'):attr('scope', 'row')
            :css('text-align', 'right')
            :attr('rowspan', uspan)
            -- Store the primary distance and any conversion error message in the cell.
            :wikitext(lengths[lengths.orig], lengths.error)
        local secondary = row:tag('td') -- Create the secondary unit cell.
            :css('text-align', 'right')
            :css('background-color', '#eaecf0')
            :attr('rowspan', uspan)
            :wikitext(lengths[lengths.comp]) -- Store the secondary distance in the cell.

        local unit_ref = args.unit_ref
        if unit_ref then -- A reference is provided for the distance.
            primary:wikitext(unit_ref) -- Add reference to the primary distance cell.
        end

        local unit2 = args.unit2
        if unit2 then -- A second distance is provided.
            local line = args.line -- A horizontal rule may be requested between the distances.
            if line then
                -- Add a horizontal rule to both cells.
                primary:tag('hr', selfClosing)
                secondary:tag('hr', selfClosing)
            else
                -- Add an en-dash and a line break to both cells.
                primary:wikitext('–'):tag('br', selfClosing)
                secondary:wikitext('–'):tag('br', selfClosing)
            end
            -- Convert and format the second distance.
            local lengths2 = convert(unit2, unitdef)
            -- Add the second distance and any conversion error message to the primary distance cell.
            primary:wikitext(lengths2[lengths2.orig], lengths2.error)
            -- Add the converted second distance to the secondary distance cell.
            secondary:wikitext(lengths2[lengths2.comp])
            end
        
        local unit2_ref = args.unit2_ref
        if unit2_ref then -- A reference is provided for the distance.
            primary:wikitext(unit2_ref) -- Add reference to the primary distance cell.
        end
    end
end

-- Color specified by any supplied type
local color
-- Tooltip specified by any supplied type
local title

--- Apply any type-derived coloring and tooltip to the given cell.
local function applyTypeStyle(cell)
    cell:attr('title', title):css('background-color', color)
end

--- Creates a cell for places, such as bridges and rest areas.
local function place(args)
    local place = args.place -- Contents of the place cell
    -- Do nothing if `place` is "none"
    if place == "none" then return end
    local colspan = 2 -- Initial column span
    local exit = args[1] -- Whether this table has exit number columns
    local named = args[2] -- Whether this table has named junction column
    -- Adjust column span
    if exit == "old" then colspan = colspan + 2
    elseif exit == "exit" then colspan = colspan + 1
    end
    if named == "name" then colspan = colspan + 1 end
    -- Row span (`pspan` = "place span")
    local pspan = args.pspan or jspan
    local placeCell = row:tag('td') -- Create place cell
        :css('text-align', 'center')
        :attr('colspan', colspan)
        :attr('rowspan', pspan)
        :wikitext(place) -- Store the place in the cell
    applyTypeStyle(placeCell)
end

--- Creates cells for exit number and named junction columns.
local function exits(args)
    local exit = args[1] -- 'exit', 'old', or nil
    local named = args[2] -- 'name' or nil

    if exit == 'old' then -- Add old exit number cell
        -- Row span (`ospan` = "old span")
        local ospan = args.ospan or jspan
        row:tag('td') -- Create old exit number cell
            :css('text-align', 'center')
            :css('background-color', '#d3d3d3')
            :attr('title', 'Former exit number')
            :attr('rowspan', ospan)
            :wikitext(args.old) -- Store the old exit number in the cell
    end

    if exit then -- "exit" or "old" is defined; add current exit number cell
        -- Row span (`espan` = "exit span")
        local espan = args.espan or jspan
        local exitCell = row:tag('td') -- Create exit number cell
            :css('text-align', 'center')
            :attr('rowspan', espan)
            :wikitext(args.exit) -- Store the exit number in the cell
        applyTypeStyle(exitCell)
    end

    if named then -- Junction list has a junction name column
        local namespan = args.namespan or jspan -- Row span
        local nameCell = row:tag('td') -- Create junction name cell
            :attr('rowspan', namespan)
            :wikitext(args.name) -- Store the junction name in the cell
        applyTypeStyle(nameCell)
    end
end

--- Creates cell for the destinations column.
local function destinations(args)
    local road = args.road -- Contents of the destinations cell
    -- Do nothing if `road` is "none"
    if road == "none" then return end
    -- Column span (`rcspan` = "road column span"), default to 1
    local rcspan = args.rcspan or 1
    -- Row span (`rspan` = "road span")
    local rspan = args.rspan or jspan
    local destCell = row:tag('td') -- Create destination cell
        :attr('colspan', rcspan)
        :attr('rowspan', rspan)
        :wikitext(road) -- Store the destination in the cell
    applyTypeStyle(destCell)
end

--- Creates cell for the notes column.
local function notes(args)
    local notes = args.notes -- Contents of the notes cell
    -- Do nothing if `notes` is "none"
    if notes == "none" then return end
    -- Row span (`nspan` = "notes span")
    local nspan = args.nspan or jspan
    local notesCell = row:tag('td') -- Create notes cell
        :attr('rowspan', nspan)
        :wikitext(notes) -- Store the notes in the cell
    applyTypeStyle(notesCell)
end

---
-- Returns a row in the junction list.
-- Accessible from other Lua modules
function p._jctint(args)
    jspan = args.jspan or 1 -- Global row span for all columns; defaults to 1
    -- {{{type}}} argument to determine color and tooltips
    local argType = args.type
    if argType then -- {{{type}}} was passed
        -- Type-based data for colors and tooltips
        if argType == 'mplex' then
            local page = mw.title.getCurrentTitle() -- Get transcluding page's title
            local pagename = page.prefixedText -- Extract page's full title as string
            insert(errorMsg,
                format("[[Category:Jctint template with invalid type|$ %s]]", pagename))
        end
        local types = mw.loadData("Module:Road data/RJL types")
        local typeData = types[string.lower(argType)] -- Retrieve the type data
        if typeData then
            color = typeData.color -- Store the color globally
            title = typeData.jctint -- Store the tooltip globally
        else
            -- Add error category to error message table.
            local page = mw.title.getCurrentTitle() -- Get transcluding page's title
            local pagename = page.prefixedText -- Extract page's full title as string
            insert(errorMsg,
                format("[[Category:Jctint template with invalid type|%s]]", pagename))
        end
    end

    local root = mw.html.create() -- Create the root mw.html object to return
    -- Create the table row and store it globally
    row = root:tag('tr'):css('text-align', 'left')

    locations(args) -- Handle location arguments
    units(args) -- Handle distance arguments
    if args.place then -- {{{place}}} spans all columns to the right of the distances
        place(args) -- Create cell for place
    else
        exits(args) -- Handle exit/named junction arguments
        destinations(args) -- Handle destinations
        notes(args) -- Handle notes
    end

    -- Return the HTML code in the mw.html object as a string, plus any error messages
    return tostring(root) .. table.concat(errorMsg)
end

--- Entry function for {{jctint/core}}
function p.jctint(frame)
    -- Import module function to work with passed arguments
    local getArgs = require('Module:Arguments').getArgs
    -- Gather passed arguments into easy-to-use table
    local args = getArgs(frame)
    return p._jctint(args)
end

return p -- Return package