Для документации этого модуля может быть создана страница Модуль:Sources/doc

---@alias args table
---@alias frame { args: args, extensionTag: function, newChild: ( fun( args: args ): frame ) }

local p = {}

local monthGen = { 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря' }

local i18nDefaultLanguage = 'ru'
p.i18nDefaultLanguage = i18nDefaultLanguage

local i18nEtAl = {
    ru  = ' и др.',
    en  = ' et al.',
}

local i18nEditors = {
    ru  = 'под ред. ',
    en  = 'ed. by ',
}

local i18nTranslators = {
    ru  = 'пер. ',
    en  = 'transl. ',
}

local i18nVolume = {
    ru  = 'Т.',
    en  = 'Vol.',
}

local i18nIssue = {
    ru  = 'вып.',
    en  = 'Iss.',
}

local i18nPages = {
    ru  = 'С.',
    en  = 'P.',
}

local i18nNumberOfPages = {
    ru  = 'с.',
    en  = 'p.',
}

--- Получение и очистка аргументов
local function getFilledArgs( args )
    local data = {}
    for key, value in pairs( args ) do
        if type(value) == 'string' and mw.text.trim( value ) ~= '' then
            data[ key ] = mw.text.trim( value )
        end
    end
    return data
end

--- Форматирование имени автора (разбор на Фамилию и Инициалы)
local function tokenizeName( fullName )
    local space = '%s+'
    local name = "(%a[%a%-']*)%.?"
    local surname = "(%a[%a%-']*)"
    local surnamePrefixes = { 'ван', 'van', 'де', 'de' }

    local nm, nm2, srn, pref

    fullName = ' ' .. fullName .. ' '
    fullName = mw.ustring.gsub( fullName, ' оглы ', ' ' )
    fullName = mw.text.trim( fullName )

    -- Surname, Name
    local pattern = '^' .. surname .. ',' .. space .. name .. '$'
    srn, nm = mw.ustring.match( fullName, pattern )
    if srn then return { srn, mw.ustring.sub( nm, 1, 1 ) .. '.' } end

    -- Name Name Surname
    pattern = '^' .. name .. space .. name .. space .. surname .. '$'
    nm, nm2, srn = mw.ustring.match( fullName, pattern )
    if srn then return { srn, mw.ustring.sub( nm, 1, 1 ) .. '. ' .. mw.ustring.sub( nm2, 1, 1 ) .. '.' } end

    return { fullName }
end

local function personNameToAuthorName( fullName )
    if not fullName then return nil end
    local tokenized = tokenizeName( fullName )
    if #tokenized == 1 then return tokenized[ 1 ] end
    return tokenized[ 1 ] .. ' ' .. tokenized[ 2 ]
end

local function personNameToResponsibleName( fullName )
    if not fullName then return nil end
    local tokenized = tokenizeName( fullName )
    if #tokenized == 1 then return tokenized[ 1 ] end
    return tokenized[ 2 ] .. ' ' .. tokenized[ 1 ]
end

--- Базовые обертки и добавления свойств
local function wrapInUrl( url, text )
    if not url or url == '' then return text end
    return '[' .. url .. ' ' .. text .. ']'
end

local function appendProperty( data, conjunction, propertyName, urlPropertyName )
    if not data[ propertyName ] or data[ propertyName ] == '' then return '' end
    local text = data[ propertyName ]
    
    if urlPropertyName and data[ urlPropertyName ] and data[ urlPropertyName ] ~= '' then
        text = wrapInUrl( data[ urlPropertyName ], text )
    end
    
    return conjunction .. text
end

local function appendTitle( data )
    local conjunction = ''
    local result = ''

    if data.part then
        result = result .. appendProperty( data, '', 'part', 'parturl' )
        conjunction = ' // '
    end

    local title = data.title or 'Без названия'
    if data.url then
        title = wrapInUrl( data.url, title )
    end

    return result .. conjunction .. title
end

local function appendLanguage( lang )
    if not lang or lang == i18nDefaultLanguage then
        return ''
    end
    return ' <span style="color:#555; font-size:0.9em;">(на ' .. lang .. ' яз.)</span>'
end

local function appendSubtitle( data )
    return appendProperty( data, ': ', 'subtitle', nil )
end

local function appendOriginalTitle( data )
    return appendProperty( data, ' = ', 'originaltitle', nil )
end

local function appendEditor( lang, data )
    if not data.editor and not data.translator then return '' end

    local result = ' / '
    if data.editor then
        local prefix = i18nEditors[ lang ] or i18nEditors[ i18nDefaultLanguage ]
        result = result .. prefix .. personNameToResponsibleName( data.editor )
        if data.translator then result = result .. ', ' end
    end
    if data.translator then
        local prefix = i18nTranslators[ lang ] or i18nTranslators[ i18nDefaultLanguage ]
        result = result .. prefix .. personNameToResponsibleName( data.translator )
    end
    return result
end

local function appendEdition( data )
    return appendProperty( data, ' — ', 'edition', nil )
end

local function appendPublicationData( data )
    if not data.place and not data.publisher and not data.year then return '' end

    local result = ' — '
    if data.place then
        result = result .. data.place
        if data.publisher or data.year then result = result .. ': ' end
    end
    if data.publisher then
        result = result .. data.publisher
        if data.year then result = result .. ', ' end
    end
    if data.year then
        result = result .. data.year
    end
    result = result .. '.'

    return result
end

local function appendVolumeAndIssue( lang, data )
    if not data.volume and not data.issue then return '' end

    local result = ' — '
    local letter_vol = i18nVolume[ lang ] or i18nVolume[ i18nDefaultLanguage ]
    local letter_iss = i18nIssue[ lang ] or i18nIssue[ i18nDefaultLanguage ]
    
    if data.volume then
        result = result .. letter_vol .. ' ' .. data.volume
        if data.issue then result = result .. ', ' .. letter_iss .. ' ' .. data.issue end
    else
        result = result .. letter_iss .. ' ' .. data.issue
    end
    result = result .. '.'

    return result
end

local function appendPages( lang, data )
    if not data.pages then return '' end
    local letter = i18nPages[ lang ] or i18nPages[ i18nDefaultLanguage ]
    local strPages = mw.ustring.gsub( data.pages, '[-—]', '—' )
    return ' — ' .. letter .. ' ' .. strPages .. '.'
end

local function appendNumberOfPages( lang, data )
    if not data.numberOfPages then return '' end
    local letter = i18nNumberOfPages[ lang ] or i18nNumberOfPages[ i18nDefaultLanguage ]
    return ' — ' .. data.numberOfPages .. ' ' .. letter .. '.'
end

local function appendIdentifier( data, key, prefix, urlPrefix )
    if not data[key] then return '' end
    local value = data[key]
    if urlPrefix then
        value = wrapInUrl( urlPrefix .. value, value )
    end
    return ' — ' .. prefix .. value
end

local function appendAccessDate( data )
    if not data.accessdate then return '' end
    return " <small>Проверено " .. data.accessdate .. ".</small>"
end

local function preprocessPlace( data )
    if not data.place then return end
    -- Сокращения для крупных городов
    if data.place == 'Москва' then 
        data.place = '<span title="Москва" style="border-bottom: 1px dotted; cursor: help;">М.</span>' 
    elseif data.place == 'Санкт-Петербург' then 
        data.place = '<span title="Санкт-Петербург" style="border-bottom: 1px dotted; cursor: help;">СПб.</span>' 
    end
end

--- Главная функция сборки строки источника
local function renderSource( data )
    local lang = data.lang or i18nDefaultLanguage
    preprocessPlace( data )

    if not data.title and not data.url then return '' end

    local result = ''
    if data.author then
        result = '<i class="wef_low_priority_links">' .. personNameToAuthorName( data.author ) .. '</i> '
    end
    
    result = result .. appendTitle( data )
    result = result .. appendLanguage( lang )
    result = result .. appendSubtitle( data )
    result = result .. appendOriginalTitle( data )

    result = result .. '<span class="wef_low_priority_links">'
    result = result .. appendEditor( lang, data )
    result = result .. appendEdition( data )
    result = result .. appendPublicationData( data )
    result = result .. appendVolumeAndIssue( lang, data )
    result = result .. appendPages( lang, data )
    result = result .. appendNumberOfPages( lang, data )

    result = result .. appendIdentifier( data, 'isbn', 'ISBN ' )
    result = result .. appendIdentifier( data, 'issn', 'ISSN ', 'https://www.worldcat.org/issn/' )
    result = result .. appendIdentifier( data, 'doi', 'doi:', 'https://dx.doi.org/' )
    result = result .. appendIdentifier( data, 'pmid', 'PMID:', 'https://www.ncbi.nlm.nih.gov/pubmed/?term=' )
    result = result .. appendIdentifier( data, 'arxiv', 'arXiv:', 'https://arxiv.org/abs/' )
    
    result = result .. appendAccessDate( data )
    result = result .. '</span>'

    return result
end

--- Точка входа для обычного вывода
function p.renderSource( frame )
    p.currentFrame = frame
    local data = getFilledArgs( frame.args or {} )
    
    local rendered = renderSource( data )
    if not rendered or rendered == '' then return '' end

    if data.ref then
        local anchorValue = 'CITEREF' .. data.ref .. ( data.year or '' )
        rendered = '<span class="citation" id="' .. mw.uri.anchorEncode( anchorValue ) .. '">' .. rendered .. '</span>'
    end

    return rendered
end

--- Точка входа для генерации тега <ref> (сноски)
function p.renderReference( frame )
    local rendered = p.renderSource( frame )
    if not rendered or rendered == '' then return '' end

    -- Используем стандартный хеш MediaWiki для уникального имени сноски
    local hash = mw.hash.hashValue( 'fnv164', rendered )
    return frame:extensionTag( 'ref', rendered, { name = '_' .. hash } )
end

return p