|
|
1번째 줄: |
1번째 줄: |
| local p = {} | | local p = {} |
| local u = mw.ustring
| |
|
| |
|
| -- ==== CACHES ==== | | -- CSV 데이터를 받아 wikitable 형식으로 변환하는 함수 |
| local cell_cache = setmetatable({}, { __mode = "kv" }) -- 셀 단위 전체 결과 캐시
| | function p.csv(frame) |
| local justify_cache = setmetatable({}, { __mode = "kv" }) -- 기존 유지 | | local csv = frame.args[1] or '' |
| | | local lines = {} |
| -- ==== TOKEN MAPS (단일 gsub용) ====
| | local result = '{| class="wikitable timetable" style="border: 2px solid var(--text);"\n' |
| 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 | | -- CSV 데이터를 줄 단위로 분리 |
| local function transpose(m, fill)
| | for line in csv:gmatch("[^\r\n]+") do |
| fill = fill or ""
| | table.insert(lines, line) |
| 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 | | end |
| out[c] = newrow
| |
| end
| |
| return out
| |
| end
| |
|
| |
| -- === 열 스타일 1회 프리컴파일 ===
| |
| local col_styles -- upvalue
| |
|
| |
| local function ensure_col_styles(ncols, headerColCount)
| |
| if col_styles then return end
| |
| col_styles = {}
| |
| for j = 1, ncols do
| |
| if j == headerColCount - 2 then
| |
| col_styles[j] = [[style="vertical-align: middle !important; border-right: none;"]]
| |
| elseif j == headerColCount - 1 then
| |
| col_styles[j] = [[id="stickyparent" style="padding-left: 0rem; padding-right: 0rem; border-right: none; vertical-align: middle !important; min-width: 8.5ch;"]]
| |
| elseif j == headerColCount then
| |
| col_styles[j] = [[style="text-align: center; border-left: none; border-right: 1px solid var(--text); margin-left: -1rem;"]]
| |
| elseif j == headerColCount + 1 then
| |
| col_styles[j] = [[style="min-width: 37.5px; border-left: 1px solid var(--text);"]]
| |
| elseif j < headerColCount - 2 then
| |
| col_styles[j] = [[style="border-right: 1px solid;"]]
| |
| else
| |
| col_styles[j] = [[style="min-width: 37.5px; border-left: 1px solid var(--text);"]]
| |
| end
| |
| end
| |
| end
| |
|
| |
| -- per-char wrapper for text contained in single quotes, with memoization
| |
| local justify_cache = setmetatable({}, { __mode = "kv" })
| |
|
| |
| -- per-char wrapper for text in single quotes (조건부 실행)
| |
| 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
| |
|
| |
| -- 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
| |
|
| |
| -- Single-pass token normalizer for most ASCII tokens (multi-char handled first)
| |
| -- ==== 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 경로: 토큰 후보/따옴표가 하나도 없으면 그대로
| |
| -- 1) 아주 빠른 no-op 경로(확장):
| |
| -- - ASCII 토큰 없음
| |
| -- - 따옴표 없음
| |
| -- - 대문자 3연속 토큰(ARR/DEP/LEX/… 등) 없음
| |
| -- - 배지류 토큰 패턴(BC{…}, BS{…}, C{…}, S{…}, N{…}) 없음
| |
| 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, SPA, ...
| |
| 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
| | for i = 1, #lines do |
| cell_cache[raw] = v
| | local row = lines[i] |
| return v
| | local values = {} |
| end
| | -- 데이터를 쉼표로 분리하여 values 테이블에 저장 |
| | for value in row:gmatch("[^,]+") do |
| | value = value:gsub("x", "…") |
| | value = value:gsub("/", "<br/>") |
| | value = value:gsub("v", "レ") |
| | value = value:gsub("i", "‖") |
| | value = value:gsub("!R", 'colspan="2" |') |
| | value = value:gsub("!D", 'rowspan="2" |') |
| | value = value:gsub("!L", '--!--') |
| | value = value:gsub("!U", '--!--') |
|
| |
|
| | if value == "--!--" then |
| | table.insert(values, "--!--") |
| | elseif string.find(value, "|") then |
| | table.insert(values, value) |
| | else |
| | table.insert(values, "|" .. value) |
| | end |
| | end |
|
| |
|
| -- 2) 단순 토큰
| | for j, value in ipairs(values) do |
| v = str_gsub(v, "c", 'OPENCENTERSPAN')
| | if value == "--!--" then |
| | | else |
| -- 3) 정규화 (멀티→단일→사후)
| | if j == 1 then |
| v = normalize_tokens(v)
| | values[j] = 'style="text-align: justify; text-align-last: justify; border-right: none;"' .. value |
| | | elseif j == 2 then |
| -- 4) 고정 기호 치환
| | values[j] = 'style="text-align: justify; text-align-last: justify; border-left: none; border-right: 1px solid var(--text);"' .. value |
| v = str_gsub(v, "DEP", '<span style="font-size: 0.92rem;">發</span>')
| | else |
| v = str_gsub(v, "ARR", '<span style="font-size: 0.92rem;">着</span>')
| | values[j] = [[style="text-align: center; font-family: Monaco, 'Lucida Console', 'Andale Mono', 'Courier New', Courier, monospace;"]] .. value |
| v = str_gsub(v, "DIV", '<span class="forcedcenter" style="font-size: 1.1rem; vertical-align: middle; line-height: 0.9rem;">╌</span>')
| | end |
| v = str_gsub(v, "GRR", '<span class="forcedcenter"></span>')
| | end |
| v = str_gsub(v, "GRN", '<span class="forcedcenter"></span>')
| | end |
| 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">')
| |
| | |
| -- parse once, transpose once
| |
| local arr = csv_to_array(csv)
| |
| | |
| local out = {}
| |
| out[#out+1] = '<div style="display: flex; flex-direction: row; padding-bottom: 1rem;"><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] = string.format(
| |
| '</div><div class="table-wrap" style="border:2.5px solid var(--text); padding:3px !important; overflow-x:auto;">\n{| class="timetable" data-sticky-cols="%d" style="text-align:right; table-layout:fixed; width:max-content; white-space:nowrap;"\n',
| |
| headerColCount
| |
| )
| |
| | |
| local total_rows = #arr
| |
| for i = 1, total_rows do
| |
| local row = arr[i]
| |
| local values = {}
| |
| | |
| for c = 1, #row do
| |
| local v = transform_cell(row[c])
| |
| if not str_find(v, "|", 1, true) then
| |
| v = "|" .. v
| |
| end
| |
| if v then values[#values+1] = v end
| |
| end
| |
| | |
| -- prepend style per column index (same logic/order as yours)
| |
| -- prepend style per column index (precompiled)
| |
| ensure_col_styles(#values, headerColCount)
| |
| for j = 1, #values do
| |
| values[j] = col_styles[j] .. values[j]
| |
| end
| |
| | |
| -- (기존 유지) "!!" 제거 루프
| |
| for j = #values, 1, -1 do
| |
| if string.find(values[j], "!!", 1, true) then
| |
| table.remove(values, j) | |
| end
| |
| end
| |
|
| |
|
| -- row separators (unchanged semantics)
| | if i == 4 then |
| if i < headerRowCount + 1 then
| | result = result .. '|- style="border-bottom: double var(--text);"\n| ' .. table.concat(values, " || ") .. "\n" |
| out[#out+1] = '|- style="border-top: 1px solid var(--text);"\n| '
| | else |
| out[#out+1] = tbl_concat(values, " \n| ")
| | result = result .. "|-\n| " .. table.concat(values, " || ") .. "\n" |
| out[#out+1] = "\n"
| | end |
| elseif i == headerRowCount + 1 then
| |
| out[#out+1] = '|- style="border-top: double var(--text);"\n| '
| |
| out[#out+1] = tbl_concat(values, " \n| ")
| |
| out[#out+1] = "\n"
| |
| elseif i == total_rows - footerRowCount + 1 then
| |
| out[#out+1] = '|- style="border-top: double var(--text);"\n| '
| |
| out[#out+1] = tbl_concat(values, " \n| ")
| |
| out[#out+1] = "\n"
| |
| elseif i == total_rows and footerRowCount > 1 then
| |
| out[#out+1] = '|- class="forcetopalign" style="border-top: 1px solid var(--text);"\n| '
| |
| out[#out+1] = tbl_concat(values, " \n| ")
| |
| out[#out+1] = "\n"
| |
| elseif i > total_rows - footerRowCount + 1 then
| |
| out[#out+1] = '|- style="border-top: 1px solid var(--text);"\n| '
| |
| out[#out+1] = tbl_concat(values, " \n| ")
| |
| out[#out+1] = "\n"
| |
| else
| |
| out[#out+1] = "|-\n| "
| |
| out[#out+1] = tbl_concat(values, " \n| ")
| |
| out[#out+1] = "\n"
| |
| end | | end |
| end
| |
|
| |
|
| out[#out+1] = "|}\n</div></div>"
| | result = result .. "|}" |
| return tbl_concat(out)
| | return result |
| end | | end |
|
| |
|
| return p | | return p |