Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:FeaturedExtract

From Landrace.Wiki - The Landrace Cannabis Wiki

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

-- Module:FeaturedExtract
-- Pulls content from articles for use in homepage cards.
--
-- p.lead     — lead-section text. Strips the leading infobox, all <ref>
--              blocks, {{cn}} markers, clips at the first == heading,
--              then truncates to a clean length. Emits explicit <p> tags
--              so paragraph breaks survive when wrapped in a block
--              element on the calling page. Set nomore=1 to suppress
--              the trailing "read more" link.
--
-- p.heroDiv  — float-image div for the featured-article panel. Image
--              source priority: explicit Has image value (arg 1), then
--              first [[File:...]] in page wikitext, then |image=
--              parameter from the leading infobox call. Returns empty
--              string when none yields an image so no broken markup
--              renders. Detects image orientation (landscape, portrait,
--              square-ish) and emits a modifier class so CSS can set a
--              different display width per orientation. Picks a thumb
--              size hint appropriate to the orientation so retina
--              sharpness holds. Caller may override the hint via arg 3.

local p = {}

-- Strip a balanced {{...}} template block at the start of the text.
local function stripLeadingTemplate(text)
    while true do
        local s = text:match("^%s*()")
        if not s or text:sub(s, s+1) ~= "{{" then return text end
        local depth, i = 0, s
        local closed = false
        while i <= #text do
            local c2 = text:sub(i, i+1)
            if c2 == "{{" then
                depth = depth + 1
                i = i + 2
            elseif c2 == "}}" then
                depth = depth - 1
                i = i + 2
                if depth == 0 then
                    text = text:sub(i)
                    closed = true
                    break
                end
            else
                i = i + 1
            end
        end
        if not closed then return text end
    end
end

-- Take everything up to the first level-2+ heading.
local function takeLead(text)
    local heading = text:find("\n==")
    if heading then return text:sub(1, heading - 1) end
    return text
end

-- Truncate at the last sentence boundary (. ! ?) before max chars.
-- Falls back to the last word boundary if no sentence ends in range.
local function truncate(text, maxChars)
    if #text <= maxChars then return text, false end
    local clip = text:sub(1, maxChars)
    local lastSentence = clip:find("[%.%?%!]%s+%u[^%.%?%!]*$")
    if lastSentence then
        local upTo = clip:sub(1, lastSentence)
        upTo = upTo:gsub("%s+$", "")
        return upTo, true
    end
    local lastSpace = clip:find("%s+[^%s]*$")
    if lastSpace then
        return clip:sub(1, lastSpace - 1), true
    end
    return clip, true
end

-- Find the first [[File:...]] or [[Image:...]] filename in wikitext.
local function firstFileIn(content)
    if not content or content == "" then return "" end
    local img = content:match("%[%[%s*[Ff]ile%s*:%s*([^%[%]|]+)")
             or content:match("%[%[%s*[Ii]mage%s*:%s*([^%[%]|]+)")
             or ""
    return mw.text.trim(img)
end

-- Read |image= parameter from the leading infobox call. Handles bare
-- filename, [[File:Foo.jpg]] wrappers, and parameter values with sized
-- options. Limited to the first 3000 chars to scope to the infobox
-- region only.
local function imageFromInfobox(content)
    if not content or content == "" then return "" end
    local head = content:sub(1, 3000)
    local raw = head:match("|%s*image%s*=%s*([^|}\n]+)")
    if not raw then return "" end
    raw = mw.text.trim(raw)
    if raw == "" then return "" end
    local fromLink = raw:match("^%[%[%s*[Ff]ile%s*:%s*([^%[%]|]+)")
    if fromLink then return mw.text.trim(fromLink) end
    return raw
end

-- Classify image as landscape, portrait, or square based on dimensions.
-- Returns modifier class fragment and a thumbnail size hint matched
-- to the display width of that orientation tier (~2x for retina).
-- Falls back to square-ish defaults when dimensions can't be read.
local function orientationFor(filename)
    local fileTitle = mw.title.new("File:" .. filename)
    if fileTitle and fileTitle.file and fileTitle.file.exists then
        local w = fileTitle.file.width or 0
        local h = fileTitle.file.height or 0
        if w > 0 and h > 0 then
            local ratio = w / h
            if ratio >= 1.2 then
                return "home-featured__float--landscape", "800px"
            elseif ratio <= 0.85 then
                return "home-featured__float--portrait", "560px"
            end
        end
    end
    return "home-featured__float--square", "640px"
end

function p.lead(frame)
    local args = frame.args
    local pagename = args[1] or args.article
    if not pagename or pagename == "" then return "" end

    local maxChars = tonumber(args.chars or args[2]) or 600
    local nomore = args.nomore and args.nomore ~= "" and args.nomore ~= "0"

    local title = mw.title.new(pagename)
    if not title or not title.exists then return "" end

    local content = title:getContent()
    if not content then return "" end

    content = stripLeadingTemplate(content)
    content = takeLead(content)

    content = content:gsub("<ref[^/>]*/>", "")
    content = content:gsub("<ref[^>]*>.-</ref>", "")
    content = content:gsub("{{[Cc]n}}", "")
    content = content:gsub("{{[Cc]itation needed}}", "")

    content = content:gsub("^%s+", ""):gsub("%s+$", "")

    local truncated
    content, truncated = truncate(content, maxChars)

    local readMore = nomore and '' or (' <span class="home-featured__more">[[' .. pagename .. '|read more →]]</span>')
    local tail = (truncated and ' …' or '') .. readMore

    local paragraphs = {}
    for para in (content .. "\n\n"):gmatch("(.-)\n\n+") do
        para = para:gsub("^%s+", ""):gsub("%s+$", "")
        if para ~= "" then
            table.insert(paragraphs, para)
        end
    end

    if #paragraphs == 0 then return "" end

    paragraphs[#paragraphs] = paragraphs[#paragraphs] .. tail

    local out = {}
    for _, para in ipairs(paragraphs) do
        table.insert(out, "<p>" .. para .. "</p>")
    end
    return table.concat(out, "\n")
end

function p.heroDiv(frame)
    local explicit     = mw.text.trim(frame.args[1] or "")
    local pageName     = mw.text.trim(frame.args[2] or "")
    local sizeOverride = mw.text.trim(frame.args[3] or "")

    local img = explicit
    if img == "" and pageName ~= "" then
        local title = mw.title.new(pageName)
        if title and title.exists then
            local content = title:getContent() or ""
            img = firstFileIn(content)
            if img == "" then
                img = imageFromInfobox(content)
            end
        end
    end

    if img == "" then return "" end

    local orientationClass, thumbHint = orientationFor(img)
    local size = sizeOverride ~= "" and sizeOverride or thumbHint

    return string.format(
        '<div class="home-featured__float %s">[[File:%s|%s|link=%s|alt=%s]]</div>',
        orientationClass, img, size, pageName, pageName
    )
end

return p