모듈:GameJSONParser: 두 판 사이의 차이
둘러보기로 이동
검색으로 이동
>BANIP 잔글 (decodeJSON => jsonDecode) |
>Hsl0 편집 요약 없음 |
||
(사용자 3명의 중간 판 182개는 보이지 않습니다) | |||
1번째 줄: | 1번째 줄: | ||
-- 본 소스코드는 "크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 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 | |||
find = function( | |||
for _, value in | |||
if func(value) then | if func(value) then | ||
return value | return value | ||
48번째 줄: | 17번째 줄: | ||
return nil | return nil | ||
end, | end, | ||
-- 자바스크립트의 map 함수와 동일 | |||
map = function( | -- 주어진 함수를 이용하여 테이블의 모든 요소를 변환 | ||
map = function(tb, func) | |||
local newTable = {} | local newTable = {} | ||
for i, value in | for i, value in pairs(tb) do | ||
newTable[i] = func(value) | newTable[i] = func(value) | ||
end | end | ||
return newTable | return newTable | ||
end, | end, | ||
-- 자바스크립트의 filter 함수와 동일 | |||
filter = function( | -- 주어진 함수를 만족하는 요소만으로 새 테이블 생성 | ||
filter = function(tb, func) | |||
local newTable = {} | local newTable = {} | ||
for _, value in | for _, value in pairs(tb) do | ||
if func(value) then | if func(value) then | ||
table.insert(newTable, value) | table.insert(newTable, value) | ||
65번째 줄: | 36번째 줄: | ||
end | end | ||
return newTable | 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 | 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 | |||
} | |||
} | } | ||
-- 게임 메타데이터를 분석하고 각 속성에 대해 적절한 포맷을 적용하는 함수 | |||
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 | end | ||
table. | |||
-- 결과 항목을 '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 | 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 | end | ||
function p._getSchemaTable(frame) | |||
-- | frame = frame or mw.getCurrentFrame() | ||
-- | |||
-- schemePagename 페이지가 존재하는지 확인 없으면 null 반환 | |||
local scheme = mw.text.jsonDecode( | if not mw.title.new(p.SCHEME_PAGENAME).exists then | ||
local | 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 | 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", | key = "platform", | ||
formatter = "[[ | formatter = iconFormatterBuilder({categorySuffix=" 게임"}), | ||
}, | |||
local | progress={ | ||
key = "progress", | |||
end) | 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 | |||
-- 첫번째 변수혹은 현재 페이지명으로 게임 메타데이터 페이지 이름 획득 | |||
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 | |||
-- <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 "[[분류:메타데이터가 없는 게임]]" | |||
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 | ||
}, | } | ||
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} 게임]]", | |||
}, | |||
} | |||
parsedCategory = p._getParsedGameJson(scheme, gameMeta, categoryProps) | |||
end | |||
-- #================================= 머릿글 틀 =================================# -- | |||
local templateProps = { | |||
{ | { | ||
key = " | key = "featured", | ||
formatter = "{ | formatter = function(prop) | ||
local templateParam = {nil} | |||
return | 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 | end | ||
}, | }, | ||
templatePropFactory("abandon","버려진 게임","날짜"), | |||
templatePropFactory("construction","게임 공사중","기간"), | |||
templatePropFactory("repair","게임 수리중","기간"), | |||
{ | { | ||
key = " | key = "editpolicy", | ||
formatter | formatter = function(prop) | ||
return currentFrame:expandTemplate({title = "편집가능", args = {['분류없음'] = noCategory and '1' or nil}}) | |||
end, | |||
enable = function(prop) | |||
return | return prop.const == "open" | ||
end | end | ||
}, | }, | ||
{ | { | ||
key = " | key = "editpolicy", | ||
formatter = | formatter = function(prop) | ||
return currentFrame:expandTemplate({title = "편집금지", args = {['분류없음'] = noCategory and '1' or nil}}) | |||
end, | |||
enable = function(prop) | |||
return prop.const == "closed" | |||
end | end | ||
}, | }, | ||
{ | { | ||
key = " | key = "editpolicy", | ||
formatter = "[[ | formatter = function(prop) | ||
return currentFrame:expandTemplate({title = "부분 편집가능", args = { nil, "[[토론:" .. title .. "]]", ['분류없음'] = noCategory and '1' or nil } }) | |||
end, | |||
enable = function(prop) | |||
return prop.const == "limited" | |||
end | end | ||
}, | }, | ||
{ | { | ||
key = " | key = "rating", | ||
formatter = "{ | formatter = function(prop) | ||
local constmap = { rtest= "평가용", rall= "전체", r12= "12", r15= "15", r18= "18"} | |||
if | local templateParam = {} | ||
if | -- 등급 지정 | ||
return { | templateParam[1] = constmap["r"..prop.const] | ||
end | 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 | ||
} | 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 |
2023년 10월 14일 (토) 23:50 기준 최신판
getGameInfo
- 이 부분의 본문은 틀:게임 정보입니다.
getGamecard
- 이 부분의 본문은 틀:게임카드입니다.
getFeaturedCard
- 이 부분의 본문은 틀:추천평카드입니다.
도보시오
- 리버티게임:게임 메타데이터/스키마.json : 메타데이터 스키마
- 모듈:GameJSONParser/연구소
위 설명은 모듈:GameJSONParser/설명문서의 내용을 가져와 보여주고 있습니다. (편집 | 역사) 이 모듈에 대한 수정 연습과 시험은 연습장 (만들기 | 미러)과 시험장 (만들기)에서 할 수 있습니다. 분류는 /설명문서에 넣어주세요. 이 모듈에 딸린 문서. |
-- 본 소스코드는 "크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 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
}
}
-- 게임 메타데이터를 분석하고 각 속성에 대해 적절한 포맷을 적용하는 함수
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
-- 첫번째 변수혹은 현재 페이지명으로 게임 메타데이터 페이지 이름 획득
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
-- <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 "[[분류:메타데이터가 없는 게임]]"
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} 게임]]",
},
}
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