모듈:GameJSONParser: 두 판 사이의 차이

리버티게임, 모두가 만들어가는 자유로운 게임
잔글 (분류:게임 메타데이터가 없는 게임 => 분류:메타데이터가 없는 게임)
편집 요약 없음
 
(사용자 4명의 중간 판 93개는 보이지 않습니다)
1번째 줄: 1번째 줄:
-- 본 소스코드는 "크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 4.0 국제 라이센스" (CC BY-NC-SA 4.0)하에 배포됩니다.
-- 본 소스코드는 "크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 4.0 국제 라이센스" (CC BY-NC-SA 4.0)하에 배포됩니다.
-- BANIP 2023년 8월 2일 (수) 10:23 (KST)
-- BANIP 2023년 8월 2일 (수) 10:23 (KST)


local table = require('table') -- 배열 입출력을 위한 테이블 내부 라이브러리
local table = require('table') -- 배열 입출력을 위한 테이블 내부 라이브러리
 
local m = require('모듈:Metadata')


-- 테이블 관련 유틸리티 함수들
-- 테이블 관련 유틸리티 함수들
local t = {
local t = {
     -- 자바스크립트의 find 함수와 동일
     -- 자바스크립트의 find 함수와 동일
     -- 주어진 함수를 만족하는 첫 번째 요소를 반환
     -- 주어진 함수를 만족하는 첫 번째 요소를 반환  
     find = function(tb, func)
     find = function(tb, func)
         for _, value in ipairs(tb) do
         for _, value in pairs(tb) do
             if func(value) then
             if func(value) then
                 return value
                 return value
22번째 줄: 21번째 줄:
     map = function(tb, func)
     map = function(tb, func)
         local newTable = {}
         local newTable = {}
         for i, value in ipairs(tb) do
         for i, value in pairs(tb) do
             newTable[i] = func(value)
             newTable[i] = func(value)
         end
         end
31번째 줄: 30번째 줄:
     filter = function(tb, func)
     filter = function(tb, func)
         local newTable = {}
         local newTable = {}
         for _, value in ipairs(tb) do
         for _, value in pairs(tb) do
             if func(value) then
             if func(value) then
                 table.insert(newTable, value)
                 table.insert(newTable, value)
42번째 줄: 41번째 줄:
     walk = function(tbl, keys)
     walk = function(tbl, keys)
         local value = tbl
         local value = tbl
         for i, key in ipairs(keys) do
         for i, key in pairs(keys) do
             value = value[key]
             value = value[key]
             if value == nil or type(value) == 'number' or type(value) == 'string' then
             if value == nil or type(value) == 'number' or type(value) == 'string' then
54번째 줄: 53번째 줄:


-- 카테고리 관련 유틸리티 함수들
-- 카테고리 관련 유틸리티 함수들
local c = {
local c = {}
-- #dpl 파서함수로 생성한 HTML에서 페이지 이름을 추출합니다.
-- #dpl 파서함수로 생성한 HTML에서 페이지 이름을 추출합니다.
    getPagenamesFromDpl = function(html)
function c.getPagenamesFromDpl(html)
        local pageNames = {} -- 반환될 페이지 이름을 담을 배열을 초기화합니다.
    local pageNames = {} -- 반환될 페이지 이름을 담을 배열을 초기화합니다.
       
   
        -- 페이지 이름을 추출합니다.
    -- 페이지 이름을 추출합니다.
        for li in html:gmatch('<li>(.-)</li>') do -- <li>(.*)</li> 패턴
    for li in html:gmatch('<li>(.-)</li>') do -- <li>(.*)</li> 패턴
            li = li:gsub('%[%[', ''):gsub('%]%]', ''):gsub('<li>', ''):gsub('</li>', '') -- 필요없는 문자열 제거
        li = li:gsub('%[%[', ''):gsub('%]%]', ''):gsub('<li>', ''):gsub('</li>', '') -- 필요없는 문자열 제거
            local pageName = li:match('(.-)|') or li -- 페이지 이름 추출
        local pageName = li:match('(.-)|') or li -- 페이지 이름 추출
            table.insert(pageNames, pageName)
        table.insert(pageNames, pageName)
    end
   
    return pageNames
end
 
-- #dpl 파서함수로 생성한 스트링인지 확인합니다
function c.isDplString(str)
    return str:sub(1, 8) == "<ul><li>"
end
 
-- 페이지 이름을 추출합니다.
function c.getPagenames(pagenamesString)
    local pagenames = {}
    if c.isDplString(pagenamesString) then
        -- dpl쿼리로 생성했으면 pagenames로 변환
        pagenames = c.getPagenamesFromDpl(pagenamesString)
        -- 스트링을 트림하고 난 후의 문자열이 {로 시작하지 않고(JSON의 케이스)
        -- 문자열중 ,가 포함되어 있으면 ,를 기준으로 분리, 분리된 문자열은 trim으로 좌우 공백 삭제
    elseif string.find(pagenamesString,",") and not (mw.text.trim(pagenamesString):sub(1, 1) == "{") then
        pagenames = mw.text.split(pagenamesString,",")
        for i, pagename in ipairs(pagenames) do
            pagenames[i] = mw.text.trim(pagename)
         end
         end
          
    else
         return pageNames
         -- 그 외엔 그대로 pagenames에 추가
    end,
         pagenames = {pagenamesString}
-- #dpl 파서함수로 생성한 스트링인지 확인합니다
    end
isDplString = function(str)
    return pagenames
return str:sub(1, 8) == "<ul><li>"
end
end
 
}
-- 기타 유틸리티 함수들
local util = {}
-- falsy한 값인지 확인합니다.
function util.isFalsy(value)
    -- value가 string일 시 trim
    if type(value) == "string" then
        value = mw.text.trim(value)
    end
    return value == nil or value == false or value == "" or value == "0" or value == 0
end


local p = {
local p = {
126번째 줄: 156번째 줄:
             end)
             end)
             if ageDef == nil then return nil end
             if ageDef == nil then return nil end
             if ageData == nil then return ageDef[1] end
             if ageData == nil then return t.walk(scheme, {"properties", "rating", "oneOf", 1}) end
           
            local summary = t.walk(gameMeta, {"rating", "libertygame", "summary", 1})
            local date = t.walk(gameMeta, {"rating", "libertygame", "date", 1})
            -- ageData 딥카피
            ageData = mw.clone(ageData)
            ageData.summary = summary
            ageData.date = date
 
             return ageData
             return ageData
         end
         end
155번째 줄: 193번째 줄:
         end
         end
     },
     },
AuthorName = {
author = {
validate = function(gameMeta, scheme)
validate = function(gameMeta, scheme)
local authorName = t.walk(gameMeta, {"AuthorName"})
local authorName = t.walk(gameMeta, {"author"})
-- authorName이 스트링이면 배열로 반환, [[MTR/game.json]] 케이스의 예외처리
-- authorName이 스트링이면 배열로 반환, [[MTR/game.json]] 케이스의 예외처리
-- nil일 시 익명
-- nil일 시 익명
170번째 줄: 208번째 줄:
end)
end)
end
end
}
},
    name = {
validate = function(gameMeta, scheme)
            local gameName = t.walk(gameMeta, {"name"})
            return {const = gameName}
end
    },
    created = {
        validate = function(gameMeta, scheme)
            local const = gameMeta.created
            -- "2023-08-07"형식으로 된 const가 2016년 이전이면 isClassic true
            local isClassic = false
            if type(const) == "string" then
                local year = tonumber(string.sub(const, 1, 4))
                if year and year < 2016 then
                    isClassic = true
                end
            end
 
            return {const = const, isClassic = isClassic}
        end
    },
    image = {
        validate = function(gameMeta, scheme)
            return {const = gameMeta.image}
        end
    },
    featured = {
        validate = function(gameMeta, scheme)
            local featured = t.walk(gameMeta, {"featured"})
            if featured == nil then
                return nil
            end
            return {
                const = "featured",
                date = t.walk(featured, {"date"}) or "미정",
                description = t.walk(featured, {"description"}) or "아직 추천평이 작성되지 않았으며 현재 추천평 토의 중입니다.",
                author = t.walk(featured, {"author"}) or "미상"
            }
        end
    },
    variant = {
    validate = function(gameMeta, scheme)
    return {const = gameMeta.variant}
    end
    }
}
}
-- 게임 메타데이터를 분석하고 각 속성에 대해 적절한 포맷을 적용하는 함수
-- 게임 메타데이터를 분석하고 각 속성에 대해 적절한 포맷을 적용하는 함수
241번째 줄: 324번째 줄:
                         for propKey, propValue in pairs(props) do
                         for propKey, propValue in pairs(props) do
                             -- propValue string일 시 찾아바꾸기
                             -- propValue string일 시 찾아바꾸기
                             if type(propValue) == 'string' then
                             if type(propValue) == 'string' and type(thisItemString) == 'string' then
                                 thisItemString = thisItemString:gsub("${" .. propKey .. "}", propValue)
                                 thisItemString = thisItemString:gsub("${" .. propKey .. "}", propValue)
                             end
                             end
                         end
                         end
                         table.insert(resultItem, thisItemString)
                         table.insert(resultItem, thisItemString)
                     end  
                     end  
251번째 줄: 335번째 줄:
                  
                  
                 -- 결과 항목을 'formatters'에 지정된 구분자로 연결하여 최종 결과에 추가합니다.
                 -- 결과 항목을 'formatters'에 지정된 구분자로 연결하여 최종 결과에 추가합니다.
                 results[resultKey] = table.concat(resultItem, separator)
                 -- 첫번째 결과가 함수인 경우 그대로 입력 20230810
                if type(resultItem[1]) == 'function' then
                    -- 2번째 키가 없는 경우 첫번째 키만 반환
                    if resultItem[2] == nil then
                        results[resultKey] = resultItem[1]
                    else
                        results[resultKey] = resultItem
                    end
                else
                    results[resultKey] = table.concat(resultItem, separator)
                end
 
             end
             end
         end
         end
261번째 줄: 356번째 줄:
     pagename = pagename or mw.title.getCurrentTitle().prefixedText
     pagename = pagename or mw.title.getCurrentTitle().prefixedText
     local gameMetaPagename = pagename
     local gameMetaPagename = pagename
    -- JSON 타입을 사용할것이라 추정되는 경우 ({로 시작하는 경우) JSON을 파싱합니다.
    if mw.text.trim(gameMetaPagename):sub(1, 1) == "{" then
        return true, mw.text.jsonDecode(gameMetaPagename)
    end


    -- /game.json으로 안끝나면 붙여주기
-- 자동으로 /game.json 문서 찾기
    if not gameMetaPagename:find("/game.json$") then
gameMetaPagename = m.resolve(gameMetaPagename)
        gameMetaPagename = gameMetaPagename .. "/game.json"
if gameMetaPagename then
gameMetaPagename = gameMetaPagename.fullText
    else
        return false,"<div class='gamecard gamecard-error'><div class='content'>[[" .. pagename.. "]]의 게임 메타데이터를 찾을 수 없습니다.</div></div>", gameMetaPagename
     end
     end
    -- -- /game.json으로 안끝나면 붙여주기
    -- if not gameMetaPagename:find("/game.json$") then
    --    gameMetaPagename = gameMetaPagename .. "/game.json"
    -- end


     -- 위키낚시/game.json => :위키낚시/game.json
     -- 위키낚시/game.json => :위키낚시/game.json
279번째 줄: 387번째 줄:
     end)
     end)
     if not templateCallOk then
     if not templateCallOk then
         return false,"[[" .. gameMetaPagename.. "|" .. pagename.. "]]의 게임 메타데이터가 없거나 올바르지 않습니다."
         return false,"<div class='gamecard gamecard-error'><div class='content'>[[" .. gameMetaPagename.. "|" .. pagename.. "]]의 게임 메타데이터가 올바르지 않습니다.</div></div>", gameMetaPagename
     end
     end
      
      
     return true, gameJson
     return true, gameJson, gameMetaPagename


end
end
301번째 줄: 409번째 줄:
end
end
      
      
function p.getGameCategories(frame)
-- 틀 호출하기 위해서 필요한 현재 페이지 프레임 변수 획득
local currentFrame = mw.getCurrentFrame()
    -- 스키마 획득
    local scheme = p._getSchemaTable(currentFrame)
    -- 첫번째 변수혹은 현재 페이지명으로 게임 메타데이터 페이지 이름 획득
    local gameMetaOk, gameMeta = p._getGameJsonTable(frame.args[1])
    -- schemePagename 페이지가 존재하는지 확인 없으면 오류반환
    if scheme == nil then
        return "자동 분류에 필요한 [[" .. p.SCHEME_PAGENAME .. "]] 페이지가 존재하지 않습니다. 관리자에게 알려주세요. "
    end
    -- game.json 페이지 있는지 확인 및 없으면 오류 반환
    if not gameMetaOk then
        return "[[분류:메타데이터가 없는 게임]]"
    end
    local formatters = {
        {
            key = "platform",
            formatter = "[[분류:${title} 게임]]",
        }, {
          key = "abandon",
          formatter = "[[분류:버려진 게임]]",
          enable = function(prop)
              return prop.const
          end
        }, {
          key = "construction",
          formatter = "[[분류:공사중인 게임]]",
          enable = function(prop)
              return prop.const
          end
        }, {
            key = "repair",
            formatter = "[[분류:수리중인 게임]]",
            enable = function(prop)
                return prop.const
            end
        }, {
          key = "progress",
          formatter = "[[분류:${title}]]",
        }, {
          key = "rating",
          formatter = "[[분류:${title} 게임]]",
        }, {
          key = "genre",
          formatter = "[[분류:${title}]]",
          emptyFormatter = "[[분류:장르가 분류되지 않은 게임]]",
        }
    }
    -- 게임 메타데이터 파싱
    local parsed = p._getParsedGameJson(scheme, gameMeta, formatters)
   
    -- 파싱 결과 join후 반환
    return table.concat(parsed)
end
local stringToNumber = function(input)
local stringToNumber = function(input)
     -- input값이 string이 아닐 시 0 반환
     -- input값이 string이 아닐 시 0 반환
382번째 줄: 425번째 줄:
     return math.random(start_val, end_val)
     return math.random(start_val, end_val)
end
end


function p.getGamecard(frame)
function p.getGamecard(frame)
389번째 줄: 433번째 줄:
         return '<div class="gamecard-404 mw-message-box-warning mw-message-box" style="font-weight:bold;">검색 결과가 없습니다.</div>'
         return '<div class="gamecard-404 mw-message-box-warning mw-message-box" style="font-weight:bold;">검색 결과가 없습니다.</div>'
     end
     end
     local iconFormatter = [[<div class="icon-wrapper">]] ..
     local iconFormatterBuilder = function(option)
        [[<span class="material-symbols-outlined icon">${icon}</span>]] ..
        option = option or {}
        [[<span class="description">${description}</span>]] ..
        local categorySuffix = option.categorySuffix or ""
    [[</div>]]
        return "<div class='icon-wrapper'>" ..
            "<span class='material-symbols-outlined icon'>[[:분류:${title}" .. categorySuffix .. "|${icon}]]</span>" ..
            "<span class='description'>${description}</span>" ..
        "</div>"
    end
     local getGenreColor = function(value)
     local getGenreColor = function(value)
         return rand(0,360,value) ..",".. rand(20,100,value) .."%"  
         return rand(0,360,value) ..",".. rand(20,100,value) .."%"  
400번째 줄: 448번째 줄:
             key = "genre",
             key = "genre",
             formatter = function(props)
             formatter = function(props)
                 return getGenreColor(props)
                 return getGenreColor(props.const)
             end,
             end,
             enable = function(props,index)
             enable = function(props,index)
416번째 줄: 464번째 줄:
                 local value = props.const or ""
                 local value = props.const or ""
                 return "<span class='genre' style='background: hsl(".. getGenreColor(value) ..",50%)'>" ..
                 return "<span class='genre' style='background: hsl(".. getGenreColor(value) ..",50%)'>" ..
                     "[[:분류:" .. title .. "|" .. title .. "]]" ..
                     "[[:분류:" .. title .. "|" .. value .. "]]" ..
                 "</span>"
                 "</span>"
             end,
             end,
422번째 줄: 470번째 줄:
         platform={
         platform={
             key = "platform",
             key = "platform",
             formatter = iconFormatter,
             formatter = iconFormatterBuilder({categorySuffix=" 게임"}),
         },  
         },  
         progress={
         progress={
             key = "progress",
             key = "progress",
             formatter = iconFormatter,
             formatter = iconFormatterBuilder(),
         },  
         },  
         editpolicy={
         editpolicy={
           key = "editpolicy",
           key = "editpolicy",
           formatter = iconFormatter, -- 파서 구현 필요
           formatter = iconFormatterBuilder({categorySuffix=" 게임"}),
        },
        getGameLink={
key = "name",
            formatter = function(props)
                return function(pagename, variant)
                    -- variant가 nil이 아니면 variant 반환
                    if variant ~= nil then
                        return variant
                    end
                    return "[[" .. pagename .. "|" .. props.const .. "]]"
                end
            end,
        },
        title={
key = "name",
            formatter = function(props)
    return  props.const
            end,
        },
        rating={
            key = "rating",
            formatter = iconFormatterBuilder({categorySuffix=" 게임"}),
         },
         },
author={
author={
key = "AuthorName",
key = "author",
formatter = function(props)
formatter = function(props)
                 -- 아이피가 포함되어 있으면 특:기여/(아이피)로 반환
                 -- 아이피가 포함되어 있으면 특:기여/(아이피)로 반환
457번째 줄: 527번째 줄:
end,
end,
             separator = "<span class='separator'>•</span>"
             separator = "<span class='separator'>•</span>"
}
},
        image={
            key = "image",
            formatter = function(props)
                return "<div class='image-wrapper'>[[파일:" .. props.const .. "|link=]]</div>"
            end,
            enable = function(prop) return prop.const end
        },
     }
     }


465번째 줄: 542번째 줄:


     local paramPagename = frame.args[1]
     local paramPagename = frame.args[1]
local pagenames = {}
    local classFlag = frame.args["속성"] or ""
   
    -- classFlag를 ' '로 분리
    local classFlagArray = mw.text.split(classFlag," ")


     -- classFlag에 이미지표시가 있으면 imageFlag true
if c.isDplString(paramPagename) then
    local imageFlag = false
        -- dpl쿼리로 생성했으면 pagenames로 변환
    for i, flag in ipairs(classFlagArray) do
pagenames = c.getPagenamesFromDpl(paramPagename)
        if flag == "이미지표시" then
     elseif string.find(paramPagename,",") then
             imageFlag = true
        -- 문자열중 ,가 포함되어 있으면 ,를 기준으로 분리, 분리된 문자열은 trim으로 좌우 공백 삭제
        pagenames = mw.text.split(paramPagename,",")
        for i, pagename in ipairs(pagenames) do
             pagenames[i] = mw.text.trim(pagename)
         end
         end
else
    end
        -- 그 외엔 그대로 pagenames에 추가
 
pagenames = {paramPagename}
    local pagenames = c.getPagenames(paramPagename)
end


local gameCards = {}
local gameCards = {}
486번째 줄: 561번째 줄:
for i, pagename in ipairs(pagenames) do
for i, pagename in ipairs(pagenames) do
-- 첫번째 변수혹은 현재 페이지명으로 게임 메타데이터 페이지 이름 획득
xpcall(function()
local gameMetaOk, gameMeta = p._getGameJsonTable(pagename)
-- 첫번째 변수혹은 현재 페이지명으로 게임 메타데이터 페이지 이름 획득
local gameMetaOk, gameMeta = p._getGameJsonTable(pagename)
        -- pagename이 {로 시작하는 경우(JSON인 경우) 해당 JSON의 네임 필드를 사용
        if string.sub(pagename,1,1) == "{" then
            pagename = gameMeta.name
        end
-- 게임 메타데이터가 비어있으면 빈값 반환
if not gameMetaOk then
table.insert(gameCards,gameMeta)
else
-- 게임 메타데이터 파싱
local parsed = p._getParsedGameJson(scheme, gameMeta, formatters)
            -- flagImage일때 parsed.image, 아니면 빈 문자열
            local imageElement = imageFlag and parsed.image or ""
            -- 메타데이터 마법사에서 해당 게임의 페이지 위치를 반영하기 위해 gameMeta.target 임시사용 20230810
local gameCard = "" ..
"<div class='gamecard " .. classFlag .. "' style='border-color:hsl(".. parsed.genreColor ..",80%);'>"..
    "<div class='theme' style='background-color:hsl(".. parsed.genreColor ..",92%);'></div>"..
    imageElement ..
    "<div class='content'>"..
        "<div class='badges'>" ..
                parsed.platform .. 
                parsed.editpolicy .. 
                parsed.progress .. 
                parsed.rating .. 
        "</div>" ..
        "<div class='genres'>" ..
            parsed.genres ..
        "</div>" ..
        "<div class='title'>" .. parsed.getGameLink(gameMeta.target or pagename,gameMeta.variant) .. "</div>" ..
        "<div class='summary'>" .. (gameMeta.summary or "").. "</div>" ..
        "<div class='description'>" .. (gameMeta.description or "") .. "</div>" ..
        "<div class='detail'>" ..
            "<div class='detail-left'>" ..
                "<div class='author'>" .. parsed.author .."</div>" ..
                "<div class='created'>" .. (gameMeta.created or "") .."</div>" ..
            "</div>" ..
            "<div class='metapage'>" ..
                "[["..pagename .. "/game.json|<span class='material-symbols-outlined icon'>data_object</span>]]" ..
            "</div>" ..
        "</div>" ..
    "</div>" ..
"</div>"
table.insert(gameCards,gameCard)
end
end, function(err)
table.insert(gameCards, '<div class="gamecard-error error mw-message-box-warning mw-message-box">[[' .. pagename .. ']]의 게임 카드 생성 오류: ' .. err .. '</div>')
end)
end
 
-- <div class="gamecards">로 감싸서 묶어서 출력
    return "<div class='gamecards'>" .. table.concat(gameCards) .. "</div>"
end
 
--print(p.getGameInfo({args = {"위키낚시"}}))
function p.getGameInfo(frame)
-- 틀 호출하기 위해서 필요한 현재 페이지 프레임 변수 획득
local currentFrame = mw.getCurrentFrame()
 
    -- 스키마 획득
    local scheme = p._getSchemaTable(currentFrame)
    local title = frame.args[1]


-- 게임 메타데이터가 비어있으면 빈값 반환
    -- frame.args['머릿글감춤']이 유의미한 값일때만 보이기
if not gameMetaOk then
    local showMBox = util.isFalsy(frame.args['머릿글감춤'])  
table.insert(gameCards,gameMeta .. "<br />")
    -- title이 비어있거나 null이면 자동 획득
else
    if title == nil or title == "" then
-- 게임 메타데이터 파싱
        title = mw.title.getCurrentTitle().prefixedText
local parsed = p._getParsedGameJson(scheme, gameMeta, formatters)
    end
local gameLink = "[[" .. pagename .. "|" .. pagename .. "]]"
   
local gameCard = [[
    -- 자동분류 미사용 여부
    <div class='gamecard' style='border-color:hsl(]].. parsed.genreColor ..[[,80%);'>
    local noCategory = frame.args['분류없음']
<div class='theme' style='background-color:hsl(]].. parsed.genreColor ..[[,92%);'></div>
    noCategory = noCategory and noCategory ~= ''
<div class='content'>
 
<div class='badges'>]] ..
    -- 첫번째 변수혹은 현재 페이지명으로 게임 메타데이터 페이지 이름 획득
parsed.platform .. 
    local gameMetaOk, gameMeta, gameMetaPagename = p._getGameJsonTable(title)
parsed.editpolicy .. 
 
parsed.progress .. 
    -- schemePagename 페이지가 존재하는지 확인 없으면 오류반환
"</div>" ..
    if scheme == nil then
            "<div class='genres'>" ..
        return "자동 분류에 필요한 [[" .. p.SCHEME_PAGENAME .. "]] 페이지가 존재하지 않습니다. 관리자에게 알려주세요. "
                parsed.genres ..
    end
            "</div>" ..
    -- game.json 페이지 있는지 확인 및 없으면 오류 반환
[[<div class='title'>]] .. gameLink ..[[</div>
    if not gameMetaOk then
<div class='summary'>]] .. (gameMeta.Summary or "")..[[</div>
        return currentFrame:expandTemplate({title = '메타데이터 없음'})
<div class='description'>]] .. (gameMeta.Description or "") ..[[</div>
    end
<div class='detail'>
   
<div class='author'>]].. parsed.author ..[[</div>
    local templatePropFactory = function(key, templateName, dateParamName)
<div class='created'>]].. (gameMeta.Created or "") ..[[</div>
        return {
<div class='metapage'>
            key = key,
]].. "[["..pagename .. "/game.json|<span class='material-symbols-outlined icon'>data_object</span>]]" ..[[
            formatter = function(prop)
</div>
                local templateParam = {}
</div>
                templateParam["기획"] = 1
</div>
                if noCategory then
</div>
                templateParam["분류없음"] = 1
]]
                end
table.insert(gameCards,gameCard)
                if prop.date ~= nil then
end
                    templateParam[dateParamName] = prop.date
                end
                return currentFrame:expandTemplate({title = templateName,args = templateParam})
            end,
            enable = function(prop)
                return prop.const
            end
        }
    end


    -- #================================= 자동 분류 =================================# --
    local parsedCategory = {}
    if not noCategory then
    local categoryProps = {
        {
            key = "platform",
            formatter = "[[분류:${title} 게임]]",
        },{
          key = "progress",
          formatter = "[[분류:${title}]]",
        }, {
          key = "rating",
          formatter = "[[분류:${title} 게임]]",
        }, {
            key = "genre",
            formatter = "[[분류:${title}]]",
            emptyFormatter = "[[분류:장르가 분류되지 않은 게임]]",
        }, {
            key = "created",
            formatter = "[[분류:백괴클래식]]",
            enable = function(prop)
                return prop.isClassic
            end
        }, {
            key = "editpolicy",
            formatter = "[[분류:${title} 게임]]",
        }, {
        key = "variant",
        formatter = "[[분류:링크가 재정의 된 게임]]"
        }
    }
    parsedCategory = p._getParsedGameJson(scheme, gameMeta, categoryProps)
end
end


-- <div class="gamecards">로 감싸서 묶어서 출력
    -- #================================= 머릿글 틀 =================================# --
    return "<div class='gamecards'>" .. table.concat(gameCards) .. "</div>"
    local templateProps = {
        {
            key = "featured",
            formatter = function(prop)
                local templateParam = {nil}
                templateParam["추천평"] = prop.description
                if noCategory then
                templateParam["분류없음"] = 1
                end
                return currentFrame:expandTemplate({title = "특집",args = templateParam})
            end,
            enable = function(prop)
                return prop and prop.const
            end
        },
        templatePropFactory("abandon","버려진 게임","날짜"),
        templatePropFactory("construction","게임 공사중","기간"),
        templatePropFactory("repair","게임 수리중","기간"),
        {
            key = "editpolicy",
            formatter = function(prop)
                return currentFrame:expandTemplate({title = "편집가능", args = {['분류없음'] = noCategory and '1' or nil}})
            end,
            enable = function(prop)
                return prop.const == "open"
            end
        },
        {
            key = "editpolicy",
            formatter = function(prop)
                return currentFrame:expandTemplate({title = "편집금지", args = {['분류없음'] = noCategory and '1' or nil}})
            end,
            enable = function(prop)
                return prop.const == "closed"
            end
        },
        {
            key = "editpolicy",
            formatter = function(prop)
                return currentFrame:expandTemplate({title = "부분 편집가능", args = { nil, "[[토론:" .. title .. "]]", ['분류없음'] = noCategory and '1' or nil } })
            end,
            enable = function(prop)
                return prop.const == "limited"
            end
        },
        {
            key = "rating",
            formatter = function(prop)
                local constmap = { rtest= "평가용", rall= "전체", r12= "12", r15= "15", r18= "18"}
                local templateParam = {}
                -- 등급 지정
                templateParam[1] = constmap["r"..prop.const]
                if prop.date ~= nil then
                    -- 등급 지정일 지정
                    templateParam[2] = prop.date
                end
                if prop.summary ~= nil then
                    -- 등급 이유 지정
                    local reasonText = prop.summary
                    if type(prop.summary) == "table" then
                        reasonText = table.concat(reasonText, ", ")
                    end
                    templateParam["이유"] = reasonText
                end
                if noCategory then
                templateParam["분류없음"] = 1
                end
                return currentFrame:expandTemplate({title = "게임 등급",args = templateParam})
            end,
        },
    }
   
    -- 머릿글 틀 파싱
    local parsedTemplateString = ""
    if showMBox then
        local parsedTemplate = p._getParsedGameJson(scheme, gameMeta, templateProps)
 
        -- string 아닌것 필터링
        parsedTemplate = t.filter(parsedTemplate, function(prop)
            return type(prop) == "string" and prop ~= ""
        end)
 
        parsedTemplateString = currentFrame:expandTemplate({title = "뱃지그룹",args = { table.concat(parsedTemplate) }})
    end
 
 
    -- #================================= SEO(검색엔진 최적화) =================================# --
    local parsedOther = p._getParsedGameJson(scheme, gameMeta, {author = {
        key = "author",
        formatter = function(props)
            return props.const
        end,
        separator = ", "
    }})
   
 
    -- SEO 정보 획득
    local getSeo = function(gameMeta,parsedOther)
        local seoParam = {""}
 
        -- descrition
        if gameMeta.description ~= nil or gameMeta.summary ~= nil then
            if gameMeta.description ~= nil then
                seoParam.description = gameMeta.description
            else
                seoParam.description = gameMeta.summary
            end
        end
 
        -- author
        if gameMeta.author ~= nil then
            seoParam.author = parsedOther.author
        end
 
        -- created
        if gameMeta.created ~= nil then
            seoParam.created = gameMeta.created
        end
 
        return currentFrame:callParserFunction("#seo",seoParam)
    end
 
    -- #================================= 기타 =================================# --
    -- 기본 분류
    local otherString = noCategory and "" or "[[분류:리버티게임]]"
    -- 정보 수정탭 추가
    if gameMetaPagename ~= nil then
        otherString = otherString .. currentFrame:expandTemplate({title = "탭 추가",args = {
            currentFrame:callParserFunction("fullurl",{gameMetaPagename,action='edit'}),
            "정보 수정",
            "더보기",
            "타이틀=게임 메타데이터를 수정합니다."
        }})
    end
 
    -- 파싱 결과 join후 반환
    return "" ..
        (noCategory and '' or getSeo(gameMeta,parsedOther)) .. -- 메타태그
        parsedTemplateString .. -- 머릿글 틀
        table.concat(parsedCategory) .. -- 자동 분류
        otherString -- 기타
end
end


-- 추천평 카드 출력
function p.getFeaturedCard(frame)
-- 틀 호출하기 위해서 필요한 현재 페이지 프레임 변수 획득
local currentFrame = mw.getCurrentFrame()
    -- 스키마 획득
    local scheme = p._getSchemaTable(currentFrame)
    local title = frame.args[1] or mw.title.getCurrentTitle().prefixedText
    -- 첫번째 변수혹은 현재 페이지명으로 게임 메타데이터 페이지 이름 획득
    local gameMetaOk, gameMeta, gameMetaPagename = p._getGameJsonTable(title)
   
    -- 게임 메타데이터가 없으면 오류 바노한
    if not gameMetaOk then
        return '추천평 카드를 만들기 위한 ' .. gameMetaPagename .. '페이지가 올바르지 않습니다.'
    end
    local parsed = p._getParsedGameJson(scheme, gameMeta, {{
        key = "featured",
        formatter = function()
            return "" ..
            "<div class='featuredcard'>" ..
                "<div class='title'><div class='logo-wrappeer'>[[파일:Symbol_star_FA.svg]]</div> 특집 게임 선정 추천평 </div>" ..
            "<div class='content-wrapper'>" ..
                "<div class='content'>" ..
                    "<div class='gametitle'>"..title.."</div>" ..
                    "<div class='description'>${description}</div>" ..
                    "<div class='author'>-- [[사용자:${author}|${author}]]</div>" ..
                "</div>" ..
            "</div>" ..
                "<div class='verbose'>" ..
                    "${date}에 특집게임으로 선정됨" ..
                "</div>" ..
            "</div>"
        end,
        enable = true
    }})[1] or (title .. '는 특집게임이 아닙니다.')
    return parsed
end
return p
return p

2024년 11월 11일 (월) 17:50 기준 최신판


모듈 설명문서[보기] [편집] [역사] [새로 고침]

getGameInfo

이 부분의 본문은 틀:게임 정보입니다.

getGamecard

이 부분의 본문은 틀:게임카드입니다.

getFeaturedCard

이 부분의 본문은 틀:추천평카드입니다.

도보시오


-- 본 소스코드는 "크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 4.0 국제 라이센스" (CC BY-NC-SA 4.0)하에 배포됩니다.
-- BANIP 2023년 8월 2일 (수) 10:23 (KST)

local table = require('table') -- 배열 입출력을 위한 테이블 내부 라이브러리
local m = require('모듈:Metadata')

-- 테이블 관련 유틸리티 함수들
local t = {
    -- 자바스크립트의 find 함수와 동일
    -- 주어진 함수를 만족하는 첫 번째 요소를 반환 
    find = function(tb, func)
        for _, value in pairs(tb) do
            if func(value) then
                return value
            end
        end
        return nil
    end,
    -- 자바스크립트의 map 함수와 동일
    -- 주어진 함수를 이용하여 테이블의 모든 요소를 변환
    map = function(tb, func)
        local newTable = {}
        for i, value in pairs(tb) do
            newTable[i] = func(value)
        end
        return newTable
    end,
    -- 자바스크립트의 filter 함수와 동일
    -- 주어진 함수를 만족하는 요소만으로 새 테이블 생성
    filter = function(tb, func)
        local newTable = {}
        for _, value in pairs(tb) do
            if func(value) then
                table.insert(newTable, value)
            end
        end
        return newTable
    end,
    -- gameMeta.rating.libertygame.age와 같이 다단계 키로 이루어진 테이블 값을 가져오는 함수
    -- 키의 경로 중간에 nil이 있는 경우 nil 반환
    walk = function(tbl, keys)
        local value = tbl
        for i, key in pairs(keys) do
            value = value[key]
            if value == nil or type(value) == 'number' or type(value) == 'string' then
                return value
            end
        end
        return value
    end,
}
 

-- 카테고리 관련 유틸리티 함수들
local c = {}
-- #dpl 파서함수로 생성한 HTML에서 페이지 이름을 추출합니다.
function c.getPagenamesFromDpl(html)
    local pageNames = {} -- 반환될 페이지 이름을 담을 배열을 초기화합니다.
    
    -- 페이지 이름을 추출합니다.
    for li in html:gmatch('<li>(.-)</li>') do -- <li>(.*)</li> 패턴
        li = li:gsub('%[%[', ''):gsub('%]%]', ''):gsub('<li>', ''):gsub('</li>', '') -- 필요없는 문자열 제거
        local pageName = li:match('(.-)|') or li -- 페이지 이름 추출
        table.insert(pageNames, pageName)
    end
    
    return pageNames
end

-- #dpl 파서함수로 생성한 스트링인지 확인합니다
function c.isDplString(str)
    return str:sub(1, 8) == "<ul><li>"
end

-- 페이지 이름을 추출합니다.
function c.getPagenames(pagenamesString)
    local pagenames = {}
    if c.isDplString(pagenamesString) then
        -- dpl쿼리로 생성했으면 pagenames로 변환
        pagenames = c.getPagenamesFromDpl(pagenamesString)
        -- 스트링을 트림하고 난 후의 문자열이 {로 시작하지 않고(JSON의 케이스)
        -- 문자열중 ,가 포함되어 있으면 ,를 기준으로 분리, 분리된 문자열은 trim으로 좌우 공백 삭제
    elseif string.find(pagenamesString,",") and not (mw.text.trim(pagenamesString):sub(1, 1) == "{") then
        pagenames = mw.text.split(pagenamesString,",")
        for i, pagename in ipairs(pagenames) do
            pagenames[i] = mw.text.trim(pagename)
        end
    else
        -- 그 외엔 그대로 pagenames에 추가
        pagenames = {pagenamesString}
    end
    return pagenames
end

-- 기타 유틸리티 함수들
local util = {}
-- falsy한 값인지 확인합니다.
function util.isFalsy(value)
    -- value가 string일 시 trim
    if type(value) == "string" then
        value = mw.text.trim(value)
    end
    return value == nil or value == false or value == "" or value == "0" or value == 0
end

local p = {
    SCHEME_PAGENAME = '리버티게임:게임 메타데이터/스키마.json',
    validateBuilder = {
        oneof = function(key, defaultPropKey, schemePath)
            local defaultPropKey = defaultPropKey or 1
            local schemePath = schemePath or {"properties", key, "oneOf"}
            return function(gameMeta, scheme)
                local propValue = t.walk(gameMeta, {key})
                local propDef = t.walk(scheme, schemePath)
                if propDef == nil then return nil end
                local schemeValue = t.find(propDef, function(p)
                    return p.const == propValue
                end)
                if schemeValue == nil then return propDef[defaultPropKey] end
                return schemeValue
            end 
        end
    }
} 

-- 속성 케이스 검사할 값, 발견시 반환할 값 정의
p.propertyCases = {
    platform = {
        validate = p.validateBuilder.oneof("platform", 1, {"$defs", "platform", "oneOf"}), 
    },
    abandon = {
        validate = function(gameMeta, scheme)
            return {const = t.walk(gameMeta, {"abandon"}) or false}
        end
    },
    construction = {
        validate = function(gameMeta, scheme)
            local construction = t.walk(gameMeta, {"construction"})
            if not construction then return {const = false} end
            if construction == true then return {const = true} end
            return {const = true, date = construction}
        end
    },
    progress = {
        validate = p.validateBuilder.oneof("progress"), 
    },    
    editpolicy = {
        validate = p.validateBuilder.oneof("editpolicy",3), 
    },
    rating = {
        validate = function(gameMeta, scheme)
            local age = t.walk(gameMeta, {"rating", "libertygame", "age"})
            local ageDef = t.walk(scheme, {"properties", "rating", "oneOf", 2, "properties", "libertygame", "properties", "age", "oneOf"})
            local ageData = t.find(ageDef, function(a)
                return a.const == age
            end)
            if ageDef == nil then return nil end
            if ageData == nil then return t.walk(scheme, {"properties", "rating", "oneOf", 1}) end
            
            local summary = t.walk(gameMeta, {"rating", "libertygame", "summary", 1})
            local date = t.walk(gameMeta, {"rating", "libertygame", "date", 1})
            -- ageData 딥카피
            ageData = mw.clone(ageData)
            ageData.summary = summary
            ageData.date = date

            return ageData
        end
    },
    repair = {
        validate = function(gameMeta, scheme)
            local repair = t.walk(gameMeta, {"repair"})
            if not repair then return {const = false} end
            if repair == true then return {const = true} end
            return {const = true, date = repair}
        end
    },
    genre = {
        validate = function(gameMeta, scheme)
            local genres = t.walk(gameMeta, {"genre"})
            if genres == nil then return false end
            if type(genres) == "string" then genres = {genres} end
            local genreDef = t.walk(scheme, {"$defs", "genre", "oneOf"})
            local genreData = t.filter(t.map(genres, function(genre)
                return t.find(genreDef, function(genreData)
                    return genreData.const == genre
                end)
            end), function(genreData)
                return genreData ~= nil
            end)

            return genreData
        end
    },
	author = {
		validate = function(gameMeta, scheme)
			local authorName = t.walk(gameMeta, {"author"})
			-- authorName이 스트링이면 배열로 반환, [[MTR/game.json]] 케이스의 예외처리
			-- nil일 시 익명
			if authorName == nil then authorName = {"익명"} end
			if type(authorName) == "string" then
				authorName = {authorName}
			end

			-- authorName키를 const로 변환 후 반환
			return t.map(authorName, function(name)
				return {const = name}
			end)
		end
	},
    name = {
		validate = function(gameMeta, scheme)
            local gameName = t.walk(gameMeta, {"name"})
            return {const = gameName}
		end
    },
    created = {
        validate = function(gameMeta, scheme)
            local const = gameMeta.created
            -- "2023-08-07"형식으로 된 const가 2016년 이전이면 isClassic true 
            local isClassic = false
            if type(const) == "string" then
                local year = tonumber(string.sub(const, 1, 4))
                if year and year < 2016 then
                    isClassic = true
                end
            end

            return {const = const, isClassic = isClassic}
        end 
    },
    image = {
        validate = function(gameMeta, scheme)
            return {const = gameMeta.image}
        end
    },
    featured = {
        validate = function(gameMeta, scheme)
            local featured = t.walk(gameMeta, {"featured"})
            if featured == nil then 
                return nil
            end
            return {
                const = "featured",
                date = t.walk(featured, {"date"}) or "미정",
                description = t.walk(featured, {"description"}) or "아직 추천평이 작성되지 않았으며 현재 추천평 토의 중입니다.",
                author = t.walk(featured, {"author"}) or "미상"
            }
        end
    },
    variant = {
    	validate = function(gameMeta, scheme)
    		return {const = gameMeta.variant}
    	end
    }
}
-- 게임 메타데이터를 분석하고 각 속성에 대해 적절한 포맷을 적용하는 함수
function p._getParsedGameJson(scheme, gameMeta, formatters)
    local results = {}
    -- 'formatters' 매개변수를 순회합니다. 각 항목은 {formatter, separator, key, enable}의 형태를 가집니다.
    for resultKey, formatterItem in pairs(formatters) do
		-- - formatter: 각 속성을 어떻게 변환할지 결정하는 함수나 문자열입니다. 
		--   함수인 경우, 현재 처리 중인 속성 그룹을 인자로 받아 특정 형식의 문자열을 반환해야 합니다.
        local formatter = formatterItem.formatter

		-- - separator: 결과 항목들을 연결할 때 사용하는 구분자입니다. 기본값은 ""입니다.
        local separator = formatterItem.separator or ""

		-- - key: 처리하려는 속성의 키입니다.
        local propertyKey = formatterItem.key



        -- 속성키가 게임스키마에 없는 키면 무시하고 다음으로 넘어갑니다.
        if p.propertyCases[propertyKey] == nil then 
            mw.log("파싱하려는 " .. propertyKey .. "키는 propertyCase에 없습니다.")
        else 
            -- 현재 속성의 유효성을 확인합니다.
            local validatedGroup = p.propertyCases[propertyKey].validate(gameMeta,scheme)
            -- 속성이 유효한 경우에만 처리를 계속합니다.
            if validatedGroup ~= false and validatedGroup ~= nil then

                -- 속성 그룹이 테이블이 아닌 경우 기본 테이블로 변환합니다.
                if type(validatedGroup) ~= 'table' then
                    validatedGroup = {}
                end

                -- 속성 그룹이 1차원 테이블인 경우 2차원 테이블로 변환합니다.
                if type(validatedGroup[1]) ~= 'table' then
                    validatedGroup = {validatedGroup}
                end

                local resultItem = {}
                
                -- 각 속성 그룹에 대해 처리를 반복합니다.
                for propIndex, props in ipairs(validatedGroup) do
                    -- - enable: 항목이 포함될지 여부를 결정하는 불리언 값 또는 함수입니다.
		            --   함수인 경우, 현재 처리 중인 속성 그룹과 그 인덱스를 인자로 받아 불리언 값을 반환해야 합니다. 기본값은 'true'입니다.
            		local isEnable = formatterItem.enable or true
                    
                    -- 속성이 비어있을 경우 해당값을 반환합니다.
                    local emptyFormatter = formatterItem.emptyFormatter or ""
                
                    if type(isEnable) == 'function' then
                        isEnable = isEnable(props, propIndex)
                    end

                    if isEnable then

                        -- 속성이 활성화된 경우에만 결과를 포맷팅하고 결과 항목에 추가합니다.
                        -- props.const가 비어있을 시 emptyFormatter로 대체
                        local thisItemString
                        if (props.const == nil) then
                            thisItemString = emptyFormatter
                        else
                            thisItemString = formatter
                        end

                        if type(thisItemString) == 'function' then
                            -- formatter가 함수면 prop 파라미터로 넣어서 실행
                            thisItemString = thisItemString(props)
                        end
                        -- props foreach
                        for propKey, propValue in pairs(props) do
                            -- propValue string일 시 찾아바꾸기
                            if type(propValue) == 'string' and type(thisItemString) == 'string' then
                                thisItemString = thisItemString:gsub("${" .. propKey .. "}", propValue)
                            end
                        end

                        table.insert(resultItem, thisItemString)
                    end 

                end
                
                -- 결과 항목을 'formatters'에 지정된 구분자로 연결하여 최종 결과에 추가합니다.
                -- 첫번째 결과가 함수인 경우 그대로 입력 20230810
                if type(resultItem[1]) == 'function' then
                    -- 2번째 키가 없는 경우 첫번째 키만 반환
                    if resultItem[2] == nil then
                        results[resultKey] = resultItem[1]
                    else
                        results[resultKey] = resultItem
                    end
                else
                    results[resultKey] = table.concat(resultItem, separator)
                end

            end
        end
    end
    return results
end

function p._getGameJsonTable(pagename)
    pagename = pagename or mw.title.getCurrentTitle().prefixedText
    local gameMetaPagename = pagename
    -- JSON 타입을 사용할것이라 추정되는 경우 ({로 시작하는 경우) JSON을 파싱합니다.
    if mw.text.trim(gameMetaPagename):sub(1, 1) == "{" then
        return true, mw.text.jsonDecode(gameMetaPagename)
    end

	-- 자동으로 /game.json 문서 찾기
	gameMetaPagename = m.resolve(gameMetaPagename)
	
	if gameMetaPagename then
		gameMetaPagename = gameMetaPagename.fullText
    else
        return false,"<div class='gamecard gamecard-error'><div class='content'>[[" .. pagename.. "]]의 게임 메타데이터를 찾을 수 없습니다.</div></div>", gameMetaPagename
    end

    -- -- /game.json으로 안끝나면 붙여주기
    -- if not gameMetaPagename:find("/game.json$") then
    --     gameMetaPagename = gameMetaPagename .. "/game.json"
    -- end

    -- 위키낚시/game.json => :위키낚시/game.json
    -- 사:BANIP/위키낚시/game.json => 사:BANIP/위키낚시/game.json
	-- 기존에 : 문자가 포함하는것을 모두 케이스에 포함시켰는데, 비밀4: 악몽/game.json같은 예외사항때문에 사용자 네임스페이스로 한정
    if not ( gameMetaPagename:find("사:") or gameMetaPagename:find("사용자:") ) then
        gameMetaPagename = ":" .. gameMetaPagename
    end

    -- 게임 메타데이터 획득 후 테이블로 변환
    local templateCallOk, gameJson = pcall(function()
        return mw.loadJsonData(gameMetaPagename) 
    end)
    if not templateCallOk then
        return false,"<div class='gamecard gamecard-error'><div class='content'>[[" .. gameMetaPagename.. "|" .. pagename.. "]]의 게임 메타데이터가 올바르지 않습니다.</div></div>", gameMetaPagename
    end
    
    return true, gameJson, gameMetaPagename

end

function p._getSchemaTable(frame)
    frame = frame or mw.getCurrentFrame()

    -- schemePagename 페이지가 존재하는지 확인 없으면 null 반환
    if not mw.title.new(p.SCHEME_PAGENAME).exists then
        return nil
    end

    -- 메타데이터 스키마 획득 후 테이블로 변환
    local schemeRaw = frame:expandTemplate{title = p.SCHEME_PAGENAME}
    local scheme = mw.text.jsonDecode(schemeRaw)

    return scheme
end
    
local stringToNumber = function(input)
    -- input값이 string이 아닐 시 0 반환
    if type(input) ~= "string" then
        return 0
    end
    local sum = 1
    for i = 1, #input do
        sum = (sum * string.byte(input, i)) % 100001
    end
    return sum
end

local rand = function(start_val, end_val,seed)
    math.randomseed(stringToNumber(seed))
    return math.random(start_val, end_val)
end


function p.getGamecard(frame)
	local currentFrame = mw.getCurrentFrame()
	-- args1이 nil은 아닌데 비어있을 때 반환
    if frame.args[1] == nil or frame.args[1] == "" then
        return '<div class="gamecard-404 mw-message-box-warning mw-message-box" style="font-weight:bold;">검색 결과가 없습니다.</div>'
    end
    local iconFormatterBuilder = function(option)
        option = option or {}
        local categorySuffix = option.categorySuffix or ""
        return "<div class='icon-wrapper'>" ..
            "<span class='material-symbols-outlined icon'>[[:분류:${title}" .. categorySuffix .. "|${icon}]]</span>" ..
            "<span class='description'>${description}</span>" ..
        "</div>"
    end
    local getGenreColor = function(value)
        return rand(0,360,value) ..",".. rand(20,100,value) .."%" 
    end
    local formatters = {
        genreColor={
            key = "genre",
            formatter = function(props)
                return getGenreColor(props.const)
            end,
            enable = function(props,index)
                return index == 1
            end,
            emptyFormatter = function()
                return "0,0%"
            end
        }, 
        genres = {
            key = "genre",
            formatter = function(props)
                props = props or {}
                local title = props.title or ""
                local value = props.const or ""
                return "<span class='genre' style='background: hsl(".. getGenreColor(value) ..",50%)'>" ..
                    "[[:분류:" .. title .. "|" .. value .. "]]" ..
                "</span>"
            end,
        },
        platform={
            key = "platform",
            formatter = iconFormatterBuilder({categorySuffix=" 게임"}),
        }, 
        progress={
            key = "progress",
            formatter = iconFormatterBuilder(),
        }, 
        editpolicy={
           key = "editpolicy",
           formatter = iconFormatterBuilder({categorySuffix=" 게임"}),
        },
        getGameLink={
			key = "name",
            formatter = function(props)
                return function(pagename, variant)
                    -- variant가 nil이 아니면 variant 반환
                    if variant ~= nil then
                        return variant
                    end
                    return "[[" .. pagename .. "|" .. props.const .. "]]"
                end
            end,
        },
        title={
			key = "name",
            formatter = function(props)
			    return  props.const
            end,
        },
        rating={
            key = "rating",
            formatter = iconFormatterBuilder({categorySuffix=" 게임"}), 
        },
		author={
			key = "author",
			formatter = function(props)
                -- 아이피가 포함되어 있으면 특:기여/(아이피)로 반환
                if string.find(props.const,"%d+%.%d+%.%d+%.%d+") then
                    return "[[특:기여/" .. props.const .. "|" .. props.const .. "]]"
                end
                
				--props.const에 "|" 문자열이 있을 시 [[]]로 감싸서 반환
                -- 예시: "사용자:{{USERNAME}}|멍청이 -> "[[사용자:{{USERNAME}}|멍청이]]"
				if string.find(props.const,"|") then
					return "[[" .. props.const .. "]]"
				end

                -- |문자열이 있고 : 문자열이 있을 때 ":"를 기준으로 전체문자열을 링크로, 제일 뒷부분을 타이틀로 반환
                -- 예시: "사용자:테스트" -> "[[사용자:테스트|테스트]]"
                if string.find(props.const,":") then
                    local split = mw.text.split(props.const,":")
                    return "[[" .. props.const .. "|" .. split[#split] .. "]]"
                end

				-- 아닐 시 "[[사용자:${const}|${const}]]"로 반환
				return "[[사용자:" .. props.const .. "|" .. props.const .. "]]"
			end,
            separator = "<span class='separator'>•</span>"
		},
        image={
            key = "image",
            formatter = function(props)
                return "<div class='image-wrapper'>[[파일:" .. props.const .. "|link=]]</div>"
            end, 
            enable = function(prop) return prop.const end
        },
    }


    -- 스키마 획득
    local scheme = p._getSchemaTable(currentFrame)

    local paramPagename = frame.args[1]
    local classFlag = frame.args["속성"] or ""
    
    -- classFlag를 ' '로 분리
    local classFlagArray = mw.text.split(classFlag," ")

    -- classFlag에 이미지표시가 있으면 imageFlag true
    local imageFlag = false
    for i, flag in ipairs(classFlagArray) do
        if flag == "이미지표시" then
            imageFlag = true
        end
    end

    local pagenames = c.getPagenames(paramPagename)

	local gameCards = {}

	
	for i, pagename in ipairs(pagenames) do
		xpcall(function()
			-- 첫번째 변수혹은 현재 페이지명으로 게임 메타데이터 페이지 이름 획득
			local gameMetaOk, gameMeta = p._getGameJsonTable(pagename)
	
	        -- pagename이 {로 시작하는 경우(JSON인 경우) 해당 JSON의 네임 필드를 사용
	        if string.sub(pagename,1,1) == "{" then
	            pagename = gameMeta.name
	        end
	
			-- 게임 메타데이터가 비어있으면 빈값 반환
			if not gameMetaOk then
				table.insert(gameCards,gameMeta)
			else 
				-- 게임 메타데이터 파싱
				local parsed = p._getParsedGameJson(scheme, gameMeta, formatters)
	
	            -- flagImage일때 parsed.image, 아니면 빈 문자열
	            local imageElement = imageFlag and parsed.image or "" 
	            -- 메타데이터 마법사에서 해당 게임의 페이지 위치를 반영하기 위해 gameMeta.target 임시사용 20230810
				local gameCard = "" ..
	"<div class='gamecard " .. classFlag .. "' style='border-color:hsl(".. parsed.genreColor ..",80%);'>"..
	    "<div class='theme' style='background-color:hsl(".. parsed.genreColor ..",92%);'></div>"..
	    imageElement ..
	    "<div class='content'>"..
	        "<div class='badges'>" ..
	                parsed.platform ..  
	                parsed.editpolicy ..  
	                parsed.progress ..  
	                parsed.rating ..  
	        "</div>" ..
	        "<div class='genres'>" ..
	            parsed.genres ..
	        "</div>" ..
	        "<div class='title'>" .. parsed.getGameLink(gameMeta.target or pagename,gameMeta.variant) .. "</div>" ..
	        "<div class='summary'>" .. (gameMeta.summary or "").. "</div>" ..
	        "<div class='description'>" .. (gameMeta.description or "") .. "</div>" ..
	        "<div class='detail'>" ..
	            "<div class='detail-left'>" ..
	                "<div class='author'>" .. parsed.author .."</div>" ..
	                "<div class='created'>" .. (gameMeta.created or "") .."</div>" ..
	            "</div>" ..
	            "<div class='metapage'>" ..
	                "[["..pagename .. "/game.json|<span class='material-symbols-outlined icon'>data_object</span>]]" ..
	            "</div>" ..
	        "</div>" ..
	    "</div>" ..
	"</div>"
				table.insert(gameCards,gameCard)
			end
		end, function(err)
			table.insert(gameCards, '<div class="gamecard-error error mw-message-box-warning mw-message-box">[[' .. pagename .. ']]의 게임 카드 생성 오류: ' .. err .. '</div>')
		end)
	end

	-- <div class="gamecards">로 감싸서 묶어서 출력
    return "<div class='gamecards'>" .. table.concat(gameCards) .. "</div>"
end

--print(p.getGameInfo({args = {"위키낚시"}}))
function p.getGameInfo(frame)
	-- 틀 호출하기 위해서 필요한 현재 페이지 프레임 변수 획득
	local currentFrame = mw.getCurrentFrame()

    -- 스키마 획득
    local scheme = p._getSchemaTable(currentFrame)
    local title = frame.args[1]

    -- frame.args['머릿글감춤']이 유의미한 값일때만 보이기
    local showMBox = util.isFalsy(frame.args['머릿글감춤']) 
    -- title이 비어있거나 null이면 자동 획득
    if title == nil or title == "" then
        title = mw.title.getCurrentTitle().prefixedText
    end
    
    -- 자동분류 미사용 여부
    local noCategory = frame.args['분류없음']
    noCategory = noCategory and noCategory ~= ''

    -- 첫번째 변수혹은 현재 페이지명으로 게임 메타데이터 페이지 이름 획득
    local gameMetaOk, gameMeta, gameMetaPagename = p._getGameJsonTable(title)

    -- schemePagename 페이지가 존재하는지 확인 없으면 오류반환
    if scheme == nil then
        return "자동 분류에 필요한 [[" .. p.SCHEME_PAGENAME .. "]] 페이지가 존재하지 않습니다. 관리자에게 알려주세요. "
    end
    -- game.json 페이지 있는지 확인 및 없으면 오류 반환
    if not gameMetaOk then
        return currentFrame:expandTemplate({title = '메타데이터 없음'})
    end
    
    local templatePropFactory = function(key, templateName, dateParamName)
        return {
            key = key,
            formatter = function(prop)
                local templateParam = {}
                templateParam["기획"] = 1
                if noCategory then
                	templateParam["분류없음"] = 1
                end
                if prop.date ~= nil then
                    templateParam[dateParamName] = prop.date
                end
                return currentFrame:expandTemplate({title = templateName,args = templateParam})
            end,
            enable = function(prop)
                return prop.const
            end
        }
    end

    -- #================================= 자동 분류 =================================# -- 
    local parsedCategory = {}
    if not noCategory then
	    local categoryProps = {
	        {
	            key = "platform",
	            formatter = "[[분류:${title} 게임]]",
	        },{
	           key = "progress",
	           formatter = "[[분류:${title}]]",
	        }, {
	           key = "rating",
	           formatter = "[[분류:${title} 게임]]",
	        }, {
	            key = "genre",
	            formatter = "[[분류:${title}]]",
	            emptyFormatter = "[[분류:장르가 분류되지 않은 게임]]",
	         }, {
	            key = "created",
	            formatter = "[[분류:백괴클래식]]",
	            enable = function(prop)
	                return prop.isClassic
	            end
	         }, {
	            key = "editpolicy",
	            formatter = "[[분류:${title} 게임]]",
	        }, {
	        	key = "variant",
	        	formatter = "[[분류:링크가 재정의 된 게임]]"
	        }
	    }
	    parsedCategory = p._getParsedGameJson(scheme, gameMeta, categoryProps)
	end

    -- #================================= 머릿글 틀 =================================# -- 
    local templateProps = {
        {
            key = "featured",
            formatter = function(prop)
                local templateParam = {nil}
                templateParam["추천평"] = prop.description
                if noCategory then
                	templateParam["분류없음"] = 1
                end
                return currentFrame:expandTemplate({title = "특집",args = templateParam})
            end,
            enable = function(prop)
                return prop and prop.const
            end
        },
        templatePropFactory("abandon","버려진 게임","날짜"),
        templatePropFactory("construction","게임 공사중","기간"),
        templatePropFactory("repair","게임 수리중","기간"),
        {
            key = "editpolicy",
            formatter = function(prop)
                return currentFrame:expandTemplate({title = "편집가능", args = {['분류없음'] = noCategory and '1' or nil}})
            end,
            enable = function(prop)
                return prop.const == "open"
            end
        },
        {
            key = "editpolicy",
            formatter = function(prop)
                return currentFrame:expandTemplate({title = "편집금지", args = {['분류없음'] = noCategory and '1' or nil}})
            end,
            enable = function(prop)
                return prop.const == "closed"
            end
        },
        {
            key = "editpolicy",
            formatter = function(prop)
                return currentFrame:expandTemplate({title = "부분 편집가능", args = { nil, "[[토론:" .. title .. "]]", ['분류없음'] = noCategory and '1' or nil } })
            end,
            enable = function(prop) 
                return prop.const == "limited"
            end
        },
        { 
            key = "rating",
            formatter = function(prop)
                local constmap = { rtest= "평가용", rall= "전체", r12= "12", r15= "15", r18= "18"}
                local templateParam = {}
                -- 등급 지정
                templateParam[1] = constmap["r"..prop.const]
                if prop.date ~= nil then
                    -- 등급 지정일 지정
                    templateParam[2] = prop.date
                end
                if prop.summary ~= nil then
                    -- 등급 이유 지정
                    local reasonText = prop.summary
                    if type(prop.summary) == "table" then
                        reasonText = table.concat(reasonText, ", ")
                    end
                    templateParam["이유"] = reasonText
                end
                if noCategory then
                	templateParam["분류없음"] = 1
                end
                return currentFrame:expandTemplate({title = "게임 등급",args = templateParam})
            end,
        },
    }
    
    -- 머릿글 틀 파싱
    local parsedTemplateString = ""
    if showMBox then
        local parsedTemplate = p._getParsedGameJson(scheme, gameMeta, templateProps)

        -- string 아닌것 필터링
        parsedTemplate = t.filter(parsedTemplate, function(prop)
            return type(prop) == "string" and prop ~= ""
        end)

        parsedTemplateString = currentFrame:expandTemplate({title = "뱃지그룹",args = { table.concat(parsedTemplate) }})
    end


    -- #================================= SEO(검색엔진 최적화) =================================# -- 
    local parsedOther = p._getParsedGameJson(scheme, gameMeta, {author = {
        key = "author",
        formatter = function(props)
            return props.const
        end,
        separator = ", "
    }})
    

    -- SEO 정보 획득
    local getSeo = function(gameMeta,parsedOther)
        local seoParam = {""}

        -- descrition
        if gameMeta.description ~= nil or gameMeta.summary ~= nil then
            if gameMeta.description ~= nil then
                seoParam.description = gameMeta.description
            else
                seoParam.description = gameMeta.summary
            end
        end

        -- author
        if gameMeta.author ~= nil then
            seoParam.author = parsedOther.author
        end

        -- created
        if gameMeta.created ~= nil then
            seoParam.created = gameMeta.created
        end

        return currentFrame:callParserFunction("#seo",seoParam)
    end

    -- #================================= 기타 =================================# --
    -- 기본 분류
    local otherString = noCategory and "" or "[[분류:리버티게임]]"
    -- 정보 수정탭 추가
    if gameMetaPagename ~= nil then
        otherString = otherString .. currentFrame:expandTemplate({title = "탭 추가",args = { 
            currentFrame:callParserFunction("fullurl",{gameMetaPagename,action='edit'}),
            "정보 수정",
            "더보기",
            "타이틀=게임 메타데이터를 수정합니다."
        }})
    end

    -- 파싱 결과 join후 반환
    return "" ..
        (noCategory and '' or getSeo(gameMeta,parsedOther)) .. -- 메타태그
        parsedTemplateString .. -- 머릿글 틀
        table.concat(parsedCategory) .. -- 자동 분류
        otherString -- 기타
end

-- 추천평 카드 출력
function p.getFeaturedCard(frame)
	-- 틀 호출하기 위해서 필요한 현재 페이지 프레임 변수 획득
	local currentFrame = mw.getCurrentFrame()

    -- 스키마 획득
    local scheme = p._getSchemaTable(currentFrame)
    local title = frame.args[1] or mw.title.getCurrentTitle().prefixedText


    -- 첫번째 변수혹은 현재 페이지명으로 게임 메타데이터 페이지 이름 획득
    local gameMetaOk, gameMeta, gameMetaPagename = p._getGameJsonTable(title)
    

    -- 게임 메타데이터가 없으면 오류 바노한
    if not gameMetaOk then
        return '추천평 카드를 만들기 위한 ' .. gameMetaPagename .. '페이지가 올바르지 않습니다.'
    end

    local parsed = p._getParsedGameJson(scheme, gameMeta, {{
        key = "featured",
        formatter = function()
            return "" ..
            "<div class='featuredcard'>" ..
                "<div class='title'><div class='logo-wrappeer'>[[파일:Symbol_star_FA.svg]]</div> 특집 게임 선정 추천평 </div>" ..
	            "<div class='content-wrapper'>" ..
	                "<div class='content'>" ..
	                    "<div class='gametitle'>"..title.."</div>" ..
	                    "<div class='description'>${description}</div>" ..
	                    "<div class='author'>-- [[사용자:${author}|${author}]]</div>" ..
	                "</div>" ..
	            "</div>" ..
                "<div class='verbose'>" ..
                    "${date}에 특집게임으로 선정됨" ..
                "</div>" ..
            "</div>"
        end,
        enable = true
    }})[1] or (title .. '는 특집게임이 아닙니다.')
    return parsed
end
return p