문서 편집 권한이 없습니다. 다음 이유를 확인해주세요: 요청한 명령은 다음 권한을 가진 사용자에게 제한됩니다: 사용자. 문서의 원본을 보거나 복사할 수 있습니다. local p = {} local u = mw.ustring -- ==== CACHES ==== local cell_cache = setmetatable({}, { __mode = "kv" }) -- 셀 단위 전체 결과 캐시 local justify_cache = setmetatable({}, { __mode = "kv" }) -- 기존 유지 -- ==== TOKEN MAPS (단일 gsub용) ==== local single_map = { ["<"] = " colspan=", [">"] = "⇨", ["h"] = "HEADERSTYLE", ["~"] = "HEIGHTSET", ["r"] = "RIGHTLEFT", ["#"] = "NOBORDERTOP", ["&"] = "YESBORDERTOP", ["g"] = "GREATER", ["^"] = " rowspan=", [";"] = " |", ["x"] = "ELLIPSIS", ["."] = "ENDSPAN", ["i"] = "OPENCENTERSPAN|", ["q"] = "OPENCENTERSPAN〃", ["-"] = "OPENCENTERSPAN_", ["v"] = "OPENCENTERSPAN↓", ["k"] = '<span style="font-weight: 700;">', ["b"] = '<span style="font-size: 1.25em; font-weight: 700;">', ["/"] = "<br/>" } -- hot locals local str_gsub, str_find, str_match, tbl_concat = string.gsub, string.find, string.match, table.concat -- CSV -> array (simple; same behavior as your original) local function csv_to_array(csv) local rows = {} for line in tostring(csv):gmatch("[^\r\n]+") do local row = {} for cell in line:gmatch("([^,]+)") do cell = cell:gsub("^%s+", ""):gsub("%s+$", "") row[#row+1] = cell end rows[#rows+1] = row end return rows end -- transpose without re-stringifying (보존) local function transpose(m, fill) fill = fill or "" local out, rows, maxc = {}, #m, 0 for r = 1, rows do if #m[r] > maxc then maxc = #m[r] end end for c = 1, maxc do local newrow = {} for r = 1, rows do newrow[r] = m[r][c] or fill end out[c] = newrow end return out end -- === 열 스타일 프리컴파일 (고정/스크롤 분리) === local fixed_col_styles -- 왼쪽 고정표 스타일 캐시 local scroll_col_styles -- 오른쪽 스크롤표 스타일 캐시 local function ensure_fixed_col_styles(ncols, headerColCount) if fixed_col_styles then return end fixed_col_styles = {} for j = 1, ncols do local absj = j -- 고정표는 1..headerColCount if headerColCount >= 3 and absj == headerColCount - 2 then fixed_col_styles[j] = [[style="vertical-align: middle !important; border-right: none; white-space: nowrap;"]] elseif headerColCount >= 2 and absj == headerColCount - 1 then fixed_col_styles[j] = [[style="padding-left: 0rem; padding-right: 0rem; border-right: none; vertical-align: middle !important; min-width: 8.5ch; white-space: nowrap;"]] elseif absj == headerColCount then fixed_col_styles[j] = [[style="text-align: center; border-left: none; border-right: 1px solid var(--text); margin-left: -1rem; white-space: nowrap;"]] elseif absj < (headerColCount - 2) then fixed_col_styles[j] = [[style="border-right: 1px solid; white-space: nowrap;"]] else fixed_col_styles[j] = [[style="width: 37.5px; border-left: 1px solid var(--text); white-space: nowrap;"]] end end end local function ensure_scroll_col_styles(ncols, headerColCount) if scroll_col_styles then return end scroll_col_styles = {} for j = 1, ncols do local absj = headerColCount + j -- 스크롤표의 절대 열 인덱스 if absj == headerColCount - 2 then scroll_col_styles[j] = [[style="vertical-align: middle !important; border-right: none; white-space: nowrap;"]] elseif absj == headerColCount - 1 then scroll_col_styles[j] = [[style="padding-left: 0rem; padding-right: 0rem; border-right: none; vertical-align: middle !important; min-width: 8.5ch; white-space: nowrap;"]] elseif absj == headerColCount then scroll_col_styles[j] = [[style="text-align: center; border-left: none; border-right: 1px solid var(--text); margin-left: -1rem; white-space: nowrap;"]] elseif absj == headerColCount + 1 then scroll_col_styles[j] = [[style="min-width: 37.5px; border-left: 1px solid var(--text); white-space: nowrap;"]] elseif absj < headerColCount - 2 then scroll_col_styles[j] = [[style="border-right: 1px solid; white-space: nowrap;"]] else scroll_col_styles[j] = [[style="width: 37.5px; border-left: 1px solid var(--text); white-space: nowrap;"]] end end end -- Memoizers (보존) local C_cache = setmetatable({}, { __mode = "kv" }) local S_cache = setmetatable({}, { __mode = "kv" }) local N_cache = setmetatable({}, { __mode = "kv" }) local BC_cache = setmetatable({}, { __mode = "kv" }) local BS_cache = setmetatable({}, { __mode = "kv" }) local function transform_BC(inner) local cached = BC_cache[inner]; if cached then return cached end inner = inner:gsub("^%s*(.-)%s*$", "%1") local out if tonumber(inner) ~= nil then out = '<span class="badge badge--circle badge--filled badge--num forcedcenter">' .. inner .. '</span>' else out = '<span class="badge badge--circle badge--filled forcedcenter"><span class="stretch-text" data-max-width="1.38rem"><span>' .. inner .. '</span></span></span>' end BC_cache[inner] = out return out end local function transform_N(inner) local cached = N_cache[inner]; if cached then return cached end inner = inner:gsub("^%s*(.-)%s*$", "%1") local out = '<span class="seriftt">' .. inner .. '</span>' N_cache[inner] = out return out end local function transform_BS(inner) local cached = BS_cache[inner]; if cached then return cached end inner = inner:gsub("^%s*(.-)%s*$", "%1") local out if tonumber(inner) ~= nil then out = '<span class="badge badge--filled badge--tight forcetopalign forcedcenter badge--num">' .. inner .. '</span>' else out = '<span class="badge badge--filled badge--tight forcetopalign forcedcenter"><span class="stretch-text" data-max-width="1.4rem"><span>' .. inner .. '</span></span></span>' end BS_cache[inner] = out return out end local function transform_C(inner) local cached = C_cache[inner]; if cached then return cached end inner = inner:gsub("^%s*(.-)%s*$", "%1") local as_num = tonumber(inner) local result if as_num and as_num >= 0 and as_num <= 34 then if as_num == 0 then result = "⓪" elseif as_num <= 20 then result = '<span class="forcedcenter"></span>' .. u.char(0x2460 + (as_num - 1)) else result = '<span class="forcedcenter"></span>' .. u.char(0x3251 + (as_num - 21)) end elseif as_num then result = '<span class="badge badge--circle forcedcenter badge--num">' .. inner .. '</span>' else result = '<span class="badge badge--circle forcedcenter"><span class="stretch-text" data-max-width="1.38rem"><span>' .. inner .. '</span></span></span>' end C_cache[inner] = result return result end local function transform_S(inner) local cached = S_cache[inner]; if cached then return cached end inner = inner:gsub("^%s*(.-)%s*$", "%1") local result if tonumber(inner) ~= nil then result = '<span class="badge badge--tight forcetopalign forcedcenter badge--num">' .. inner .. '</span>' else result = '<span class="badge badge--tight forcetopalign forcedcenter"><span class="stretch-text" data-max-width="1.4rem"><span>' .. inner .. '</span></span></span>' end S_cache[inner] = result return result end -- per-char wrapper for text contained in single quotes, with memoization local justify_cache = setmetatable({}, { __mode = "kv" }) local function justifys(s) if not str_find(s, "'", 1, true) then return s end -- 따옴표 없으면 패스 return u.gsub(s, "'(.-)'", function(inner) local cached = justify_cache[inner] if cached then return cached end local pieces = {} for ch in u.gmatch(inner, ".") do pieces[#pieces+1] = "<span>" .. ch .. "</span>" end local out = '<span class="justify-chars">' .. tbl_concat(pieces) .. "</span>" justify_cache[inner] = out return out end) end -- ==== TOKEN NORMALIZER (멀티→단일→사후 치환 최소화) ==== local function normalize_tokens(s) -- 멀티문자 먼저 (4회만) s = s:gsub("%-%-", "DOUBLEFINISHLINE") :gsub(">>", "➡") :gsub("<>", "OPENCENTERSPAN◆") :gsub("%.%.", "FULLSTOP") :gsub("rr", "OPENTCUSPAN") -- 단일문자 치환 1회 s = s:gsub("([<>h~r#&g%^;x%.iq%-/vkb])", function(ch) return single_map[ch] or ch end) -- 사후 치환 s = s:gsub("ENDSPAN","</span>") :gsub("ELLIPSIS","OPENCENTERSPAN⋯") :gsub("OPENSPAN", '<span></span>') :gsub("HEADERSTYLE", [[colspan="3" style="padding-left: 1.08rem; padding-right: 1.08rem; text-align: center; border-right: none; vertical-align: middle;"]]) :gsub("HEIGHTSET", ' height=') :gsub("RIGHTLEFT", '<span class="wm-rl">') :gsub("NOBORDERTOP", '<span class="nobordertop"></span>') :gsub("YESBORDERTOP", '<span class="yesbordertop"></span>') :gsub("GREATER", '<span style="font-size: 1.2em;">') :gsub("OPENCENTERSPAN", '<span class="forcedcenter"></span>') :gsub("DOUBLEFINISHLINE", '<span class="forcedcenter">=</span>') :gsub("FULLSTOP", '%.') :gsub("OPENTCUSPAN", '<span style="text-combine-upright: all;">') return s end -- Per-cell full transform (캐시/단축경로/조건부 justifys) local function transform_cell(raw) -- 0) 캐시 local hit = cell_cache[raw] if hit then return hit end -- "!!"은 원래대로 유지(뒤에서 제거 루프가 처리) — #3은 적용 안 함 if str_find(raw, "!!", 1, true) then cell_cache[raw] = "!!" return "!!" end local v = raw -- 1) 아주 빠른 no-op 경로(확장): 토큰 없음/따옴표 없음/대문자 토큰 없음/배지 토큰 없음 local has_ascii_tokens = str_find(v, "[<>hr#&g%^;x%.iq%-/vkbCNS{}]", 1) local has_quote = str_find(v, "'", 1, true) local has_caps_token = str_find(v, "%%u%%u%%u") -- ARR, DEP, LEX, ... local has_braced_token = str_find(v, "[BCNS][S C]?%%b{}") -- BC{..}, BS{..}, C{..}, S{..}, N{..} if not (has_ascii_tokens or has_quote or has_caps_token or has_braced_token) then cell_cache[raw] = v return v end -- 2) 단순 토큰 v = str_gsub(v, "c", 'OPENCENTERSPAN') -- 3) 정규화 (멀티→단일→사후) v = normalize_tokens(v) -- 4) 고정 기호 치환 v = str_gsub(v, "DEP", '<span style="font-size: 0.92rem;">發</span>') v = str_gsub(v, "ARR", '<span style="font-size: 0.92rem;">着</span>') v = str_gsub(v, "DIV", '<span class="forcedcenter" style="font-size: 1.1rem; vertical-align: middle; line-height: 0.9rem;">╌</span>') v = str_gsub(v, "GRR", '<span class="forcedcenter"></span>') v = str_gsub(v, "GRN", '<span class="forcedcenter"></span>') v = str_gsub(v, "LEX", '<span class="forcetopalign forcedcenter icon-lg"></span>') v = str_gsub(v, "LSL", '<span class="forcetopalign forcedcenter icon-lg">🌃</span>') v = str_gsub(v, "XSL", '<span class="forcetopalign forcedcenter icon-lg"></span>') v = str_gsub(v, "TRD", '<span class="forcedcenter">⬎</span>') v = str_gsub(v, "TRU", '<span class="forcedcenter">⬏</span>') v = str_gsub(v, "TLD", '<span class="forcedcenter">⬐</span>') v = str_gsub(v, "TLU", '<span class="forcedcenter">⬑</span>') v = str_gsub(v, "TDL", '<span class="forcedcenter">↲</span>') v = str_gsub(v, "TDR", '<span class="forcedcenter">↳</span>') v = str_gsub(v, "TUL", '<span class="forcedcenter">↰</span>') v = str_gsub(v, "TUR", '<span class="forcedcenter">↱</span>') v = str_gsub(v, "MLD", '<span class="forcedcenter">┏</span>') v = str_gsub(v, "MRD", '<span class="forcedcenter">┓</span>') v = str_gsub(v, "MLU", '<span class="forcedcenter">┗</span>') v = str_gsub(v, "MRU", '<span class="forcedcenter">┛</span>') v = str_gsub(v, "TNO", '<span class="forcedcenter bracket-open">[</span>') v = str_gsub(v, "TNC", '<span class="forcedcenter bracket-close">]</span>') v = str_gsub(v, "SUO", '<span class="forcedcenter bracket-open">{</span>') v = str_gsub(v, "SUC", '<span class="forcedcenter bracket-close">}</span>') v = str_gsub(v, "PAO", '<span class="forcedcenter bracket-open">(</span>') v = str_gsub(v, "PAC", '<span class="forcedcenter bracket-close">)</span>') v = str_gsub(v, "SPA", '<span class="forcedcenter"> </span>') -- 5) 배지류 v = u.gsub(v, 'BC{(.-)}', transform_BC) v = u.gsub(v, 'BS{(.-)}', transform_BS) v = u.gsub(v, 'C{(.-)}', transform_C) v = u.gsub(v, 'S{(.-)}', transform_S) v = u.gsub(v, 'N{(.-)}', transform_N) -- 6) 따옴표 있을 때만 per-char justify v = justifys(v) cell_cache[raw] = v return v end function p.csv(frame) local headerRowCount = tonumber(frame.args["header"]) or 4 local headerColCount = tonumber(frame.args["left"]) or 3 local footerRowCount = tonumber(frame.args["footer"]) or 1 local title = frame.args["title"] or '제목 (title 변수 입력)' local csv = frame.args[1] or '' -- title transforms (unchanged) title = title:gsub(">", '—') title = title:gsub("%.", '</div>') title = title:gsub("u", 'UNBOLD') title = title:gsub("k", 'KUDARI') title = title:gsub("n", 'NOBORI') title = title:gsub("l", '<div style="border: var(--text) 1.5px solid; display: inline-block; padding-top: 2px; padding-bottom: 3px; font-size: 1.14rem; font-weight: normal; vertical-align: top; line-height: 1.35rem;">') title = title:gsub("UNBOLD", '<div style="font-weight: 400; display: inline; font-size: 1.17rem; vertical-align: 1.5px; vertical-align: -webkit-baseline-middle;">') title = title:gsub("KUDARI", '<div class="arrow arrow--down outline">') title = title:gsub("NOBORI", '<div class="arrow arrow--up solid">') local arr = csv_to_array(csv) -- 출력 버퍼 local out = {} out[#out+1] = '<div style="display: flex; flex-direction: row; padding-bottom: 1rem;">' out[#out+1] = '<div class="wm-rl timetabletitle" style="font-size: 1.26rem; padding-right: 4px; font-weight: 700; line-height: 1.54rem;">' out[#out+1] = title out[#out+1] = '</div>' -- 컨테이너: 고정표 + 스크롤표 out[#out+1] = '<div style="display: flex; flex-direction: row; gap: 0;">' local total_rows = #arr -- 만약 headerColCount < 1 이면 기존 단일 스크롤 표로 폴백 if headerColCount < 1 then out[#out+1] = string.format('<div style="border: 2.5px solid var(--text); padding: 3px !important; overflow-x: auto;">\n{| class="timetable" style="text-align: right; border-spacing: 30px;">\n') else -- 왼쪽 고정 표 시작 (경계 겹침 방지 위해 오른쪽 테두리 제거) out[#out+1] = '<div class="timetable-fixed" style="border: 2.5px solid var(--text); border-right: 0; padding: 3px !important; overflow: hidden;">\n' out[#out+1] = '{| class="timetable timetable--fixed" style="text-align: right; border-spacing: 30px; table-layout: fixed;">\n' end -- 스타일 프리컴파일 (테이블 분리 반영) if headerColCount >= 1 then ensure_fixed_col_styles(headerColCount, headerColCount) end -- 행 루프 (왼쪽 고정 표) if headerColCount >= 1 then for i = 1, total_rows do local row = arr[i] local left_values = {} -- 왼쪽 고정 열 채우기 for c = 1, math.min(headerColCount, #row) do local v = transform_cell(row[c]) if not str_find(v, "|", 1, true) then v = "|" .. v end left_values[#left_values+1] = v end -- 열 스타일 부여 for j = 1, #left_values do left_values[j] = fixed_col_styles[j] .. left_values[j] end -- "!!" 제거 for j = #left_values, 1, -1 do if string.find(left_values[j], "!!", 1, true) then table.remove(left_values, j) end end -- 구분선 규칙 동일 적용 if i < headerRowCount + 1 then out[#out+1] = '|- style="border-top: 1px solid var(--text);"\n| ' elseif i == headerRowCount + 1 then out[#out+1] = '|- style="border-top: double var(--text);"\n| ' elseif i == total_rows - footerRowCount + 1 then out[#out+1] = '|- style="border-top: double var(--text);"\n| ' elseif i == total_rows and footerRowCount > 1 then out[#out+1] = '|- class="forcetopalign" style="border-top: 1px solid var(--text);"\n| ' elseif i > total_rows - footerRowCount + 1 then out[#out+1] = '|- style="border-top: 1px solid var(--text);"\n| ' else out[#out+1] = '|-\n| ' end out[#out+1] = tbl_concat(left_values, ' \n| ') out[#out+1] = '\n' end out[#out+1] = '|}\n' out[#out+1] = '</div>' end -- 오른쪽 스크롤 표 시작 (왼쪽 경계 제거해 이음매 자연스럽게) out[#out+1] = '<div class="timetable-scroll" style="border: 2.5px solid var(--text); border-left: 0; padding: 3px !important; overflow-x: auto;">\n' out[#out+1] = '{| class="timetable timetable--scroll" style="text-align: right; border-spacing: 30px; table-layout: fixed; white-space: nowrap;">\n' -- 스타일 프리컴파일 (스크롤 영역) local max_cols = 0 for i = 1, total_rows do if #arr[i] > max_cols then max_cols = #arr[i] end end local right_cols = math.max(0, max_cols - headerColCount) ensure_scroll_col_styles(right_cols, headerColCount) -- 행 루프 (오른쪽 스크롤 표) for i = 1, total_rows do local row = arr[i] local right_values = {} for c = headerColCount + 1, #row do local v = transform_cell(row[c]) if not str_find(v, "|", 1, true) then v = "|" .. v end right_values[#right_values+1] = v end for j = 1, #right_values do right_values[j] = scroll_col_styles[j] .. right_values[j] end for j = #right_values, 1, -1 do if string.find(right_values[j], "!!", 1, true) then table.remove(right_values, j) end end if i < headerRowCount + 1 then out[#out+1] = '|- style="border-top: 1px solid var(--text);"\n| ' elseif i == headerRowCount + 1 then out[#out+1] = '|- style="border-top: double var(--text);"\n| ' elseif i == total_rows - footerRowCount + 1 then out[#out+1] = '|- style="border-top: double var(--text);"\n| ' elseif i == total_rows and footerRowCount > 1 then out[#out+1] = '|- class="forcetopalign" style="border-top: 1px solid var(--text);"\n| ' elseif i > total_rows - footerRowCount + 1 then out[#out+1] = '|- style="border-top: 1px solid var(--text);"\n| ' else out[#out+1] = '|-\n| ' end out[#out+1] = tbl_concat(right_values, ' \n| ') out[#out+1] = '\n' end out[#out+1] = '|}\n' out[#out+1] = '</div>' -- 컨테이너 닫기 out[#out+1] = '</div>' -- split container out[#out+1] = '</div>' -- 전체 컨테이너 return tbl_concat(out) end return p 이 문서에서 사용한 틀: 모듈:Timetable/설명문서 (원본 보기) 모듈:Timetable 문서로 돌아갑니다.