Modul:Box-header: Perbedaan antara revisi
Loncat ke navigasi
Loncat ke pencarian
(←Membuat halaman berisi 'local getArgs = require('Module:Arguments').getArgs local p = {} ---------- Config data ---------- local namedColours = mw.loadData( 'Module:Box-header/colours' ) local modes = { lightest = { sat=0.10, val=1.00 }, light = { sat=0.15, val=0.95 }, normal = { sat=0.40, val=0.85 }, dark = { sat=0.90, val=0.70 }, darkest = { sat=1.00, val=0.45 }, content = { sat=0.04, val=1.00 }, grey = { sat=0.00 } } local min_contrast_ratio_normal_text = 7 --...') |
k (1 revisi diimpor) |
(Tidak ada perbedaan)
|
Revisi terkini sejak 6 Oktober 2023 20.52
Alpha | Modul ini dinilai sebagai modul alpha. Modul ini sudah siap untuk input dari pihak ketiga, dan bisa digunakan dalam beberapa halaman untuk melihat apakah ada masalah yang timbul, namun harus terus diawasi. Saran untuk fitur baru atau perubahan dalam mekanisme input dan output dipersilahkan.Box-header |
Modul ini menggunakan Lua: |
local getArgs = require('Module:Arguments').getArgs local p = {} ---------- Config data ---------- local namedColours = mw.loadData( 'Module:Box-header/colours' ) local modes = { lightest = { sat=0.10, val=1.00 }, light = { sat=0.15, val=0.95 }, normal = { sat=0.40, val=0.85 }, dark = { sat=0.90, val=0.70 }, darkest = { sat=1.00, val=0.45 }, content = { sat=0.04, val=1.00 }, grey = { sat=0.00 } } local min_contrast_ratio_normal_text = 7 -- i.e 7:1 local min_contrast_ratio_large_text = 4.5 -- i.e. 4.5:1 -- Template parameter aliases -- Specify each as either a single value, or a table of values -- Aliases are checked left-to-right, i.e. `['one'] = { 'two', 'three' }` is equivalent to using `{{{one| {{{two| {{{three|}}} }}} }}}` in a template local parameterAliases = { ['1'] = 1, ['2'] = 2, ['colour'] = 'color' } ---------- Dependecies ---------- local colourContrastModule = require('Module:Color contrast') local hex = require( 'luabit.hex' ) ---------- Utility functions ---------- local function getParam(args, parameter) if args[parameter] then return args[parameter] end local aliases = parameterAliases[parameter] if not aliases then return nil end if type(aliases) ~= 'table' then return args[aliases] end for _, alias in ipairs(aliases) do if args[alias] then return args[alias] end end return nil end local function setCleanArgs(argsTable) local cleanArgs = {} for key, val in pairs(argsTable) do if type(val) == 'string' then val = val:match('^%s*(.-)%s*$') if val ~= '' then cleanArgs[key] = val end else cleanArgs[key] = val end end return cleanArgs end -- Merge two tables into a new table. If the are any duplicate keys, the values from the second overwrite the values from the first. local function mergeTables(first, second) local merged = {} for key, val in pairs(first) do merged[key] = val end for key, val in pairs(second) do merged[key] = val end return merged end local function toOpenTagString(selfClosedHtmlObject) local closedTagString = tostring(selfClosedHtmlObject) local openTagString = mw.ustring.gsub(closedTagString, ' />$', '>') return openTagString end local function normaliseHexTriplet(hexString) if not hexString then return nil end local hexComponent = mw.ustring.match(hexString, '^#(%x%x%x)$') or mw.ustring.match(hexString, '^#(%x%x%x%x%x%x)$') if hexComponent and #hexComponent == 6 then return mw.ustring.upper(hexString) end if hexComponent and #hexComponent == 3 then local r = mw.ustring.rep(mw.ustring.sub(hexComponent, 1, 1), 2) local g = mw.ustring.rep(mw.ustring.sub(hexComponent, 2, 2), 2) local b = mw.ustring.rep(mw.ustring.sub(hexComponent, 3, 3), 2) return '#' .. mw.ustring.upper(r .. g .. b) end return nil end ---------- Conversions ---------- local function decimalToPaddedHex(number) local prefixedHex = hex.to_hex(tonumber(number)) -- prefixed with '0x' local padding = #prefixedHex == 3 and '0' or '' return mw.ustring.gsub(prefixedHex, '0x', padding) end local function hexToDecimal(hexNumber) return tonumber(hexNumber, 16) end local function RGBtoHexTriplet(R, G, B) return '#' .. decimalToPaddedHex(R) .. decimalToPaddedHex(G) .. decimalToPaddedHex(B) end local function hexTripletToRGB(hexTriplet) local R_hex, G_hex, B_hex = string.match(hexTriplet, '(%x%x)(%x%x)(%x%x)') return hexToDecimal(R_hex), hexToDecimal(G_hex), hexToDecimal(B_hex) end local function HSVtoRGB(H, S, V) -- per [[HSL and HSV#Converting_to_RGB]] local C = V * S local H_prime = H / 60 local X = C * ( 1 - math.abs(math.fmod(H_prime, 2) - 1) ) local R1, G1, B1 if H_prime <= 1 then R1 = C G1 = X B1 = 0 elseif H_prime <= 2 then R1 = X G1 = C B1 = 0 elseif H_prime <= 3 then R1 = 0 G1 = C B1 = X elseif H_prime <= 4 then R1 = 0 G1 = X B1 = C elseif H_prime <= 5 then R1 = X G1 = 0 B1 = C elseif H_prime <= 6 then R1 = C G1 = 0 B1 = X end local m = V - C local R = R1 + m local G = G1 + m local B = B1 + m local R_255 = math.floor(R*255) local G_255 = math.floor(G*255) local B_255 = math.floor(B*255) return R_255, G_255, B_255 end local function RGBtoHue(R_255, G_255, B_255) -- per [[HSL and HSV#Hue and chroma]] local R = R_255/255 local G = G_255/255 local B = B_255/255 local M = math.max(R, G, B) local m = math.min(R, G, B) local C = M - m local H_prime if C == 0 then return null elseif M == R then H_prime = math.fmod(((G - B)/C + 6), 6) -- adding six before taking mod ensures positive value elseif M == G then H_prime = (B - R)/C + 2 elseif M == B then H_prime = (R - G)/C + 4 end local H = 60 * H_prime return H end local function nameToHexTriplet(name) if not name then return nil end local codename = mw.ustring.gsub(mw.ustring.lower(name), ' ', '') return namedColours[codename] end ---------- Choose colours ---------- local function calculateColours(H, S, V, minContrast) local bgColour = RGBtoHexTriplet(HSVtoRGB(H, S, V)) local textColour = colourContrastModule._greatercontrast({bgColour}) local contrast = colourContrastModule._ratio({ bgColour, textColour }) if contrast >= minContrast then return bgColour, textColour elseif textColour == '#FFFFFF' then -- make the background darker and slightly increase the saturation return calculateColours(H, math.min(1, S+0.005), math.max(0, V-0.03), minContrast) else -- make the background lighter and slightly decrease the saturation return calculateColours(H, math.max(0, S-0.005), math.min(1, V+0.03), minContrast) end end local function makeColours(hue, modeName) local mode = modes[modeName] local isGrey = not(hue) if isGrey then hue = 0 end local borderSat = isGrey and modes.grey.sat or 0.15 local border = RGBtoHexTriplet(HSVtoRGB(hue, borderSat, 0.75)) local titleSat = isGrey and modes.grey.sat or mode.sat local titleBackground, titleForeground = calculateColours(hue, titleSat, mode.val, min_contrast_ratio_large_text) local contentSat = isGrey and modes.grey.sat or modes.content.sat local contentBackground, contentForeground = calculateColours(hue, contentSat, modes.content.val, min_contrast_ratio_normal_text) return border, titleForeground, titleBackground, contentForeground, contentBackground end local function findHue(colour) local colourAsNumber = tonumber(colour) if colourAsNumber and ( -1 < colourAsNumber ) and ( colourAsNumber < 360) then return colourAsNumber end local colourAsHexTriplet = normaliseHexTriplet(colour) or nameToHexTriplet(colour) if colourAsHexTriplet then return RGBtoHue(hexTripletToRGB(colourAsHexTriplet)) end return null end local function normaliseMode(mode) if not mode or not modes[mw.ustring.lower(mode)] or mw.ustring.lower(mode) == 'grey' then return 'normal' end return mw.ustring.lower(mode) end ---------- Build output ---------- local function boxHeaderOuter(args) local baseStyle = { clear = 'both', ['box-sizing'] = 'border-box', border = ( getParam(args, 'border-type') or 'solid' ) .. ' ' .. ( getParam(args, 'titleborder') or getParam(args, 'border') or '#ababab' ), background = getParam(args, 'titlebackground') or '#bcbcbc', color = getParam(args, 'titleforeground') or '#000', padding = getParam(args, 'padding') or '.1em', ['text-align'] = getParam(args, 'title-align') or 'center', ['font-family'] = getParam(args, 'font-family') or 'sans-serif', ['font-size'] = getParam(args, 'titlefont-size') or '100%', ['margin-bottom'] = '0px', } local tag = mw.html.create('div', {selfClosing = true}) :addClass('box-header-title-container') :addClass('flex-columns-noflex') :css(baseStyle) :css('border-width', ( getParam(args, 'border-top') or getParam(args, 'border-width') or '1' ) .. 'px ' .. ( getParam(args, 'border-width') or '1' ) .. 'px 0') :css('padding-top', getParam(args, 'padding-top') or '.1em') :css('padding-left', getParam(args, 'padding-left') or '.1em') :css('padding-right', getParam(args, 'padding-right') or '.1em') :css('padding-bottom', getParam(args, 'padding-bottom') or '.1em') :css('moz-border-radius', getParam(args, 'title-border-radius') or '0') :css('webkit-border-radius', getParam(args, 'title-border-radius') or '0') :css('border-radius', getParam(args, 'title-border-radius') or '0') return toOpenTagString(tag) end local function boxHeaderTopLinks(args) local style = { float = 'right', ['margin-bottom'] = '.1em', ['font-size'] = getParam(args, 'font-size') or '80%', color = getParam(args, 'titleforeground') or '#000' } local tag = mw.html.create('div', {selfClosing = true}) :addClass('plainlinks noprint' ) :css(style) return toOpenTagString(tag) end local function boxHeaderEditLink(args) local page = getParam(args, 'editpage') if not page or page == '{{{2}}}' then return '' end local style = { color = getParam(args, 'titleforeground') or '#000' } local tag = mw.html.create('span') :css(style) :wikitext('s') local linktext = tostring(tag) local linktarget = tostring(mw.uri.fullUrl(page, {action='edit', section=getParam(args, 'section')})) return '[' .. linktarget .. ' ' .. linktext .. '] ' end local function boxHeaderViewLink(args) local style = { color = getParam(args, 'titleforeground') or '#000' } local tag = mw.html.create('span') :css(style) :wikitext('view') local linktext = tostring(tag) local linktarget = ':' .. getParam(args, 'viewpage') return "<b>·</b> [[" .. linktarget .. '|' .. linktext .. ']] ' end local function boxHeaderTitle(args) local baseStyle = { ['font-family'] = getParam(args, 'title-font-family') or 'sans-serif', ['font-size'] = getParam(args, 'title-font-size') or '100%', ['font-weight'] = getParam(args, 'title-font-weight') or 'bold', border = 'none', margin = '0', padding = '0', color = getParam(args, 'titleforeground') or '#000'; } local tagName = getParam(args, 'SPAN') and 'span' or 'h2' local tag = mw.html.create(tagName) :css(baseStyle) :css('padding-bottom', '.1em') :wikitext(getParam(args, 'title')) if getParam(args, 'extra') then local rules = mw.text.split(getParam(args, 'extra'), ';', true) for _, rule in pairs(rules) do local parts = mw.text.split(rule, ':', true) local prop = parts[1] local val = parts[2] if prop and val then tag:css(prop, val) end end end return tostring(tag) end local function boxBody(args) local baseStyle = { ['box-sizing'] = 'border-box', border = ( getParam(args, 'border-width') or '1' ) .. 'px solid ' .. ( getParam(args, 'border') or '#ababab'), ['vertical-align'] = 'top'; background = getParam(args, 'background') or '#fefeef', opacity = getParam(args, 'background-opacity') or '1', color = getParam(args, 'foreground') or '#000', ['text-align'] = getParam(args, 'text-align') or 'left', margin = '0 0 10px', padding = getParam(args, 'padding') or '1em', } local tag = mw.html.create('div', {selfClosing = true}) :css(baseStyle) :css('border-top-width', ( getParam(args, 'border-top') or '1' ) .. 'px') :css('padding-top', getParam(args, 'padding-top') or '.3em') :css('border-radius', getParam(args, 'border-radius') or '0') return toOpenTagString(tag) end local function contrastCategories(args) local cats = '' local titleText = nameToHexTriplet(getParam(args, 'titleforeground')) or normaliseHexTriplet(getParam(args, 'titleforeground')) or '#000000' local titleBackground = nameToHexTriplet(getParam(args, 'titlebackground')) or normaliseHexTriplet(getParam(args, 'titlebackground')) or '#bcbcbc' local titleContrast = colourContrastModule._ratio({titleBackground, titleText}) local insufficientTitleContrast = type(titleContrast) == 'number' and ( titleContrast < min_contrast_ratio_large_text ) local bodyText = nameToHexTriplet(getParam(args, 'foreground')) or normaliseHexTriplet(getParam(args, 'foreground')) or '#000000' local bodyBackground = nameToHexTriplet(getParam(args, 'background')) or normaliseHexTriplet(getParam(args, 'background')) or '#fefeef' local bodyContrast = colourContrastModule._ratio({bodyBackground, bodyText}) local insufficientBodyContrast = type(bodyContrast) == 'number' and ( bodyContrast < min_contrast_ratio_normal_text ) if insufficientTitleContrast and insufficientBodyContrast then return '[[Kategori:Box-header dengan kontras judul yang tidak memadai]][[Kategori:Box-header dengan kontras isi yang tidak memadai]]' elseif insufficientTitleContrast then return '[[Kategori:Box-header dengan kontras judul yang tidak memadai]]' elseif insufficientBodyContrast then return '[[Kategori:Box-header dengan kontras isi yang tidak memadai]]' else return '' end end ---------- Main functions / entry points ---------- -- Entry point for templates (manually-specified colours) function p.boxHeader(frame) local args = getArgs(frame) local page = args.editpage if not args.editpage or args.editpage == '' then page = mw.title.getCurrentTitle().prefixedText end local output = p._boxHeader(args, page) if mw.ustring.find(output, '{') then return frame:preprocess(output) end return output end -- Entry point for modules (manually-specified colours) function p._boxHeader(_args, page) local args = setCleanArgs(_args) if page and not args.editpage then args.editpage = page end if not args.title then args.title = '{{{title}}}' end local output = {} table.insert(output, boxHeaderOuter(args)) if not getParam(args, 'EDITLINK') then table.insert(output, boxHeaderTopLinks(args)) if not getParam(args, 'noedit') then table.insert(output, boxHeaderEditLink(args)) end if getParam(args, 'viewpage') then table.insert(output, boxHeaderViewLink(args)) end if getParam(args, 'top') then table.insert(output, getParam(args, 'top') .. ' ') end table.insert(output, '</div>') end table.insert(output, boxHeaderTitle(args)) table.insert(output, '</div>') table.insert(output, boxBody(args)) if not getParam(args, 'TOC') then table.insert(output, '__NOTOC__') end if not getParam(args, 'EDIT') then table.insert(output, '__NOEDITSECTION__') end table.insert(output, contrastCategories(args)) return table.concat(output) end -- Entry point for templates (automatically calculated colours) function p.autoColour(frame) local args = getArgs(frame) local colourParam = getParam(args, 'colour') local generatedColour = nil if not colourParam or colourParam == '' then -- convert the root page name into a number and use that local root = mw.title.getCurrentTitle().rootPageTitle.prefixedText local rootStart = mw.ustring.sub(root, 1, 12) local digitsFromRootStart = mw.ustring.gsub(rootStart, ".", function(s) return math.fmod(string.byte(s, 2) or string.byte(s, 1), 10) end) local numberFromRoot = tonumber(digitsFromRootStart, 10) generatedColour = math.fmod(numberFromRoot, 360) end local output = p._autoColour(args, generatedColour) if mw.ustring.find(output, '{') then return frame:preprocess(output) end return output end -- Entry point for modules (automatically calculated colours) function p._autoColour(_args, generatedColour) local args = setCleanArgs(_args) local hue = generatedColour or findHue(getParam(args, 'colour')) local mode = normaliseMode(getParam(args, 'mode')) local border, titleForeground, titleBackground, contentForeground, contentBackground = makeColours(hue, mode) local boxTemplateArgs = mergeTables(args, { title = getParam(args, '1') or '{{{1}}}', editpage = getParam(args, '2') or '', noedit = getParam(args, '2') and '' or 'yes', border = border, titleforeground = titleForeground, titlebackground = titleBackground, foreground = contentForeground, background = contentBackground }) return p._boxHeader(boxTemplateArgs) end return p