모듈:Box-header

local getArgs = require('Module:Arguments').getArgslocal 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:1local 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 templatelocal 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] thenreturn args[parameter]endlocal aliases = parameterAliases[parameter]if not aliases thenreturn nilendif type(aliases) ~= 'table' thenreturn args[aliases]endfor _, alias in ipairs(aliases) doif args[alias] thenreturn args[alias]endendreturn nilendlocal function setCleanArgs(argsTable)local cleanArgs = {}for key, val in pairs(argsTable) doif type(val) == 'string' thenval = val:match('^%s*(.-)%s*$')if val ~= '' thencleanArgs[key] = valendelsecleanArgs[key] = valendendreturn cleanArgsend-- 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) domerged[key] = valendfor key, val in pairs(second) domerged[key] = valendreturn mergedendlocal function toOpenTagString(selfClosedHtmlObject)local closedTagString = tostring(selfClosedHtmlObject)local openTagString = mw.ustring.gsub(closedTagString, ' />$', '>')return openTagStringendlocal function normaliseHexTriplet(hexString)if not hexString then return nil endlocal hexComponent = mw.ustring.match(hexString, '^#(%x%x%x)$') or mw.ustring.match(hexString, '^#(%x%x%x%x%x%x)$')if hexComponent and #hexComponent == 6 thenreturn mw.ustring.upper(hexString)endif hexComponent and #hexComponent == 3 thenlocal 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)endreturn nilend---------- 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)endlocal function hexToDecimal(hexNumber)return tonumber(hexNumber, 16)endlocal function RGBtoHexTriplet(R, G, B)return '#' .. decimalToPaddedHex(R) .. decimalToPaddedHex(G) .. decimalToPaddedHex(B)endlocal 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)endlocal function HSVtoRGB(H, S, V) -- per [[HSL and HSV#Converting_to_RGB]]local C = V * Slocal H_prime = H / 60local X = C * ( 1 - math.abs(math.fmod(H_prime, 2) - 1) )local R1, G1, B1if H_prime <= 1 thenR1 = CG1 = XB1 = 0elseif H_prime <= 2 thenR1 = XG1 = CB1 = 0elseif H_prime <= 3 thenR1 = 0G1 = CB1 = Xelseif H_prime <= 4 thenR1 = 0G1 = XB1 = Celseif H_prime <= 5 thenR1 = XG1 = 0B1 = Celseif H_prime <= 6 thenR1 = CG1 = 0B1 = Xendlocal m = V - Clocal R = R1 + mlocal G = G1 + mlocal B = B1 + mlocal 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_255endlocal function RGBtoHue(R_255, G_255, B_255) -- per [[HSL and HSV#Hue and chroma]]local R = R_255/255local G = G_255/255local B = B_255/255local M = math.max(R, G, B)local m = math.min(R, G, B)local C = M - mlocal H_primeif C == 0 thenreturn nullelseif M == R thenH_prime = math.fmod(((G - B)/C + 6), 6) -- adding six before taking mod ensures positive valueelseif M == G thenH_prime = (B - R)/C + 2elseif M == B thenH_prime = (R - G)/C + 4endlocal H = 60 * H_primereturn Hendlocal function nameToHexTriplet(name)if not name then return nil endlocal 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 thenreturn bgColour, textColourelseif textColour == '#FFFFFF' then-- make the background darker and slightly increase the saturationreturn calculateColours(H, math.min(1, S+0.005), math.max(0, V-0.03), minContrast)else-- make the background lighter and slightly decrease the saturationreturn calculateColours(H, math.max(0, S-0.005), math.min(1, V+0.03), minContrast)endendlocal function makeColours(hue, modeName)local mode = modes[modeName]local isGrey = not(hue)if isGrey then hue = 0 endlocal borderSat = isGrey and modes.grey.sat or 0.15local border = RGBtoHexTriplet(HSVtoRGB(hue, borderSat, 0.75))local titleSat = isGrey and modes.grey.sat or mode.satlocal titleBackground, titleForeground = calculateColours(hue, titleSat, mode.val, min_contrast_ratio_large_text)local contentSat = isGrey and modes.grey.sat or modes.content.satlocal contentBackground, contentForeground = calculateColours(hue, contentSat, modes.content.val, min_contrast_ratio_normal_text)return border, titleForeground, titleBackground, contentForeground, contentBackgroundendlocal function findHue(colour)local colourAsNumber = tonumber(colour)if colourAsNumber and ( -1 < colourAsNumber ) and ( colourAsNumber < 360) thenreturn colourAsNumberendlocal colourAsHexTriplet = normaliseHexTriplet(colour) or nameToHexTriplet(colour)if colourAsHexTriplet thenreturn RGBtoHue(hexTripletToRGB(colourAsHexTriplet))endreturn nullendlocal function normaliseMode(mode)if not mode or not modes[mw.ustring.lower(mode)] or mw.ustring.lower(mode) == 'grey' thenreturn 'normal'endreturn 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)endlocal 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)endlocal function boxHeaderEditLink(args)local page = getParam(args, 'editpage')if not page or page == '{{{2}}}'thenreturn ''endlocal style = {color = getParam(args, 'titleforeground') or '#000'}local tag = mw.html.create('span'):css(style):wikitext('edit')local linktext = tostring(tag)local linktarget = tostring(mw.uri.fullUrl(page, {action='edit', section=getParam(args, 'section')}))return '[' .. linktarget  .. ' ' .. linktext .. ']&nbsp;'endlocal 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>&nbsp;[[" .. linktarget  .. '|' .. linktext .. ']]&nbsp;'endlocal 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') thenlocal rules = mw.text.split(getParam(args, 'extra'), ';', true)for _, rule in pairs(rules) dolocal parts = mw.text.split(rule, ':', true)local prop = parts[1]local val = parts[2]if prop and val thentag:css(prop, val)endendendreturn tostring(tag)endlocal 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)endlocal 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 thenreturn '[[Category:Box-header with insufficient title contrast]][[Category:Box-header with insufficient body contrast]]'elseif insufficientTitleContrast thenreturn '[[Category:Box-header with insufficient title contrast]]'elseif insufficientBodyContrast thenreturn '[[Category:Box-header with insufficient body contrast]]'elsereturn ''endend---------- Main functions / entry points ------------ Entry point for templates (manually-specified colours)function p.boxHeader(frame)local args = getArgs(frame)local page = args.editpageif not args.editpage or args.editpage == '' thenpage = mw.title.getCurrentTitle().prefixedTextendlocal output = p._boxHeader(args, page)if mw.ustring.find(output, '{') thenreturn frame:preprocess(output)endreturn outputend-- Entry point for modules (manually-specified colours)function p._boxHeader(_args, page)local args = setCleanArgs(_args)if page and not args.editpage thenargs.editpage = pageendif not args.title thenargs.title = '{{{title}}}'endlocal output = {}table.insert(output, boxHeaderOuter(args))if not getParam(args, 'EDITLINK') thentable.insert(output, boxHeaderTopLinks(args))if not getParam(args, 'noedit') thentable.insert(output, boxHeaderEditLink(args))endif getParam(args, 'viewpage') thentable.insert(output, boxHeaderViewLink(args))endif getParam(args, 'top') thentable.insert(output, getParam(args, 'top') .. '&nbsp;')endtable.insert(output, '</div>')endtable.insert(output, boxHeaderTitle(args))table.insert(output, '</div>')table.insert(output, boxBody(args))if not getParam(args, 'TOC') thentable.insert(output, '__NOTOC__')endif not getParam(args, 'EDIT') thentable.insert(output, '__NOEDITSECTION__')endtable.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 = nilif not colourParam or colourParam == '' then-- convert the root page name into a number and use thatlocal root = mw.title.getCurrentTitle().rootPageTitle.prefixedTextlocal 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)endlocal output = p._autoColour(args, generatedColour)if mw.ustring.find(output, '{') thenreturn frame:preprocess(output)endreturn outputend-- 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)endreturn p