-- Validates height formatting in infoboxes and adds error tracking categories when issues are foundlocalp={}-- Default rangeslocalDEFAULT_FT_MIN=4.10localDEFAULT_FT_MAX=6.11localDEFAULT_CM_MIN=147localDEFAULT_CM_MAX=212-- Helper function to parse and validate ftin_range parameterlocalfunctionparseFtInRange(rangeStr)ifnotrangeStrorrangeStr==''thenreturnDEFAULT_FT_MIN,DEFAULT_FT_MAXend-- Pattern: number (with optional decimal) - number (with optional decimal)localminStr,maxStr=rangeStr:match('^(%d+%.%d+)%-(%d+%.%d+)$')ifnotminStrornotmaxStrthenreturnDEFAULT_FT_MIN,DEFAULT_FT_MAXendlocalminVal=tonumber(minStr)localmaxVal=tonumber(maxStr)ifnotminValornotmaxValthenreturnDEFAULT_FT_MIN,DEFAULT_FT_MAXend-- Validate that decimal part is exactly 2 digits and represents inches (00-11)localminInches=minStr:match('%.(%d+)$')localmaxInches=maxStr:match('%.(%d+)$')ifnotminInchesornotmaxInchesthenreturnDEFAULT_FT_MIN,DEFAULT_FT_MAXend-- Check exactly 2 digitsif#minInches~=2or#maxInches~=2thenreturnDEFAULT_FT_MIN,DEFAULT_FT_MAXend-- Check range 00-11localminInchesNum=tonumber(minInches)localmaxInchesNum=tonumber(maxInches)ifnotminInchesNumornotmaxInchesNumorminInchesNum<0orminInchesNum>11ormaxInchesNum<0ormaxInchesNum>11thenreturnDEFAULT_FT_MIN,DEFAULT_FT_MAXendreturnminVal,maxValend-- Helper function to parse and validate cm_range parameterlocalfunctionparseCmRange(rangeStr)ifnotrangeStrorrangeStr==''thenreturnDEFAULT_CM_MIN,DEFAULT_CM_MAXend-- Pattern: number - number (no decimals allowed)localminStr,maxStr=rangeStr:match('^(%d+)%-(%d+)$')ifnotminStrornotmaxStrthenreturnDEFAULT_CM_MIN,DEFAULT_CM_MAXendlocalminVal=tonumber(minStr)localmaxVal=tonumber(maxStr)ifnotminValornotmaxValthenreturnDEFAULT_CM_MIN,DEFAULT_CM_MAXendreturnminVal,maxValend-- Helper function to remove fraction HTML markuplocalfunctionremoveFractionMarkup(str)ifnotstrthenreturnstrend-- Remove templatestyles strip markerstr=str:gsub("\127'\"`UNIQ%-%-templatestyles.-QINU`\"'\127",'')-- Remove span tags with specific classes while preserving inner content appropriately-- For sr-only spans, we want to convert the + to actual +str=str:gsub('<span class="sr%-only">%+</span>','+')-- Remove the frac wrapper span (keep contents)str=str:gsub('<span class="frac">(.-)</span>','%1')-- Remove num spans (keep contents)str=str:gsub('<span class="num">(.-)</span>','%1')-- Remove den spans (keep contents)str=str:gsub('<span class="den">(.-)</span>','%1')returnstrend-- Helper function to remove references/inline tags and everything afterlocalfunctionremoveReferences(str)ifnotstrthenreturnstrend-- Pattern for MediaWiki strip markers (references)localrefPattern="\127'\"`UNIQ.-QINU`\"'\127"-- Find the first reference and remove it and everything afterlocalrefStart=str:find(refPattern)ifrefStartthenstr=str:sub(1,refStart-1)end-- Find the first inline tag and remove it and everything afterlocalcatStart=str:find('%[%[Category:')ifcatStartthenstr=str:sub(1,catStart-1)endreturnstrend-- Helper function to extract regular and converted heightslocalfunctionextractHeights(str)ifnotstrthenreturnnil,nil,falseendlocalregular,converted=str:match('^(.-)%s%((.-)%)$')ifnotregularornotconvertedthenreturnnil,nil,falseendlocalreconstructed=str:gsub('^'..regular:gsub('([%(%)%.%%%+%-%*%?%[%]%^%$])','%%%1'),'')localexpectedRemainder=' ('..converted..')'localactualRemainder=reconstructedlocalremainderCheck=actualRemainder:gsub(converted:gsub('([%(%)%.%%%+%-%*%?%[%]%^%$])','%%%1'),'')ifremainderCheck~=' ()'thenreturnnil,nil,falseendreturnregular,converted,trueend-- Validate imperial height formatlocalfunctionvalidateImperial(height,ftMin,ftMax)ifnotheightthenreturnfalseend-- Extract feet and inches parts from the min/max valueslocalminFeet=math.floor(ftMin)localminInches=math.floor((ftMin-minFeet)*100+0.5)localmaxFeet=math.floor(ftMax)localmaxInches=math.floor((ftMax-maxFeet)*100+0.5)-- Pattern 1: X ft Y in (simple)localfeet,inches=height:match('^(%d+) ft (%d+) in$')iffeetandinchesthenfeet=tonumber(feet)inches=tonumber(inches)-- Check if within rangeiffeet<minFeetorfeet>maxFeetthenreturnfalseendiffeet==minFeetandinches<minInchesthenreturnfalseendiffeet==maxFeetandinches>maxInchesthenreturnfalseendifinches<0orinches>11thenreturnfalseendreturntrueend-- Pattern 2a: X ft Y+N⁄D in (whole inches plus fraction, e.g., "6 ft 2+1/2 in")localfeet2,inches2,num2,den2=height:match('^(%d+) ft (%d+)%+(%d+)⁄(%d+) in$')iffeet2andinches2andnum2andden2thenfeet2=tonumber(feet2)inches2=tonumber(inches2)num2=tonumber(num2)den2=tonumber(den2)-- Reject "0+" before fraction (should use pattern 2b instead)ifinches2==0thenreturnfalseend-- Check if within rangeiffeet2<minFeetorfeet2>maxFeetthenreturnfalseendiffeet2==minFeetandinches2<minInchesthenreturnfalseendiffeet2==maxFeetandinches2>maxInchesthenreturnfalseendifinches2<0orinches2>11thenreturnfalseendifnum2<1orden2<=1ornum2>=den2thenreturnfalseendreturntrueend-- Pattern 2b: X ft N⁄D in (fraction only, e.g., "6 ft 1/2 in")localfeet3,num3,den3=height:match('^(%d+) ft (%d+)⁄(%d+) in$')iffeet3andnum3andden3thenfeet3=tonumber(feet3)num3=tonumber(num3)den3=tonumber(den3)-- Check if within range (inches = 0 for range checking purposes)iffeet3<minFeetorfeet3>maxFeetthenreturnfalseendiffeet3==minFeetand0<minInchesthenreturnfalseendiffeet3==maxFeetand0>maxInchesthenreturnfalseendifnum3<1orden3<=1ornum3>=den3thenreturnfalseendreturntrueendreturnfalseend-- Validate metric height format (meters)localfunctionvalidateMeters(height,cmMin,cmMax)ifnotheightthenreturnfalseend-- Pattern: X.XX mlocalmeters=height:match('^(%d+%.%d+) m$')ifmetersthen-- Remove decimal point and check valuelocalmetersNoDot=meters:gsub('%.','')localnumericValue=tonumber(metersNoDot)ifnumericValueandnumericValue>=cmMinandnumericValue<=cmMaxthenreturntrueendendreturnfalseend-- Validate metric height format (centimeters)localfunctionvalidateCentimeters(height,cmMin,cmMax)ifnotheightthenreturnfalseend-- Pattern: XXX cmlocalcm=height:match('^(%d+) cm$')ifcmthenlocalnumericValue=tonumber(cm)ifnumericValueandnumericValue>=cmMinandnumericValue<=cmMaxthenreturntrueendendreturnfalseend-- Validate metric height based on metric parameterlocalfunctionvalidateMetric(height,metricParam,cmMin,cmMax)ifnotheightthenreturnfalseendmetricParam=metricParamor'both'ifmetricParam=='m'thenreturnvalidateMeters(height,cmMin,cmMax)elseifmetricParam=='cm'thenreturnvalidateCentimeters(height,cmMin,cmMax)else-- 'both' or defaultreturnvalidateMeters(height,cmMin,cmMax)orvalidateCentimeters(height,cmMin,cmMax)endend-- Main functionfunctionp.main(frame)localarguments=require('Module:Arguments')localpersonHeight=require('Module:Person height')localargs=arguments.getArgs(frame,{trim=true})localparentArgs=frame:getParent().args-- Get parameterslocalheight=args[1]orparentArgs[1]or''localmetricParam=args['metric']orparentArgs['metric']or'both'localcatParam=args['cat']orparentArgs['cat']or''localftinRangeParam=args['ftin_range']orparentArgs['ftin_range']or''localcmRangeParam=args['cm_range']orparentArgs['cm_range']or''-- Parse range parameterslocalftMin,ftMax=parseFtInRange(ftinRangeParam)localcmMin,cmMax=parseCmRange(cmRangeParam)-- If no height provided, return emptyifheight==''thenreturn''end-- Set enforce based on metric parameter (nil if 'both')localenforceValue=nilifmetricParam=='m'thenenforceValue='m'elseifmetricParam=='cm'thenenforceValue='cm'endlocalheightArgs={[1]=height,['enforce']=enforceValue,['ri']='cmin'}-- Use pcall to catch any errors from Module:Person heightlocalsuccess,formattedHeight=pcall(function()returnpersonHeight.main(frame:newChild{args=heightArgs})end)-- If Module:Person height produced an error, treat as invalid structureifnotsuccessthenifcatParam~=''thenreturn'[[Category:'..catParam..']]'endreturn''end-- Remove fraction markuplocalcleanedHeight=removeFractionMarkup(formattedHeight)-- Remove references and everything aftercleanedHeight=removeReferences(cleanedHeight)-- Trim any trailing whitespacecleanedHeight=cleanedHeight:gsub('%s+$','')-- Extract regular and converted heightslocalregularHeight,convertedHeight,structureValid=extractHeights(cleanedHeight)-- Check if structure is validifnotstructureValidthen-- Add error categorymw.addWarning('<span style="color:#d33">Height format error: The height format does not match the expected pattern.</span>')ifcatParam~=''thenreturn'[[Category:'..catParam..']]'endreturn''end-- Validate the heightslocalregularValid=falselocalconvertedValid=false-- Check if regular height is imperial or metricifvalidateImperial(regularHeight,ftMin,ftMax)thenregularValid=true-- If regular is imperial, converted should be metricconvertedValid=validateMetric(convertedHeight,metricParam,cmMin,cmMax)elseifvalidateMetric(regularHeight,metricParam,cmMin,cmMax)thenregularValid=true-- If regular is metric, converted should be imperialconvertedValid=validateImperial(convertedHeight,ftMin,ftMax)end-- If validation failed, add error categoryifnotregularValidornotconvertedValidthenifcatParam~=''then-- Create dynamic error message based on current rangeslocalftMinFeet=math.floor(ftMin)localftMinInches=math.floor((ftMin-ftMinFeet)*100+0.5)localftMaxFeet=math.floor(ftMax)localftMaxInches=math.floor((ftMax-ftMaxFeet)*100+0.5)localerrorMsg=string.format('<span style="color:#d33">Height format error: The height values or format are not within acceptable ranges. Height must be between %d\'%d" and %d\'%d" (%d–%d cm).</span>',ftMinFeet,ftMinInches,ftMaxFeet,ftMaxInches,cmMin,cmMax)mw.addWarning(errorMsg)return'[[Category:'..catParam..']]'endend-- All checks passed, return emptyreturn''endreturnp