Module:Section sizes explained

require('strict');

local lang_obj = mw.language.getContentLanguage; -- language object for number formatting appropriate to local language

----------------------------< I 1 8 N _ T >------------------------------------------------------------------

An associative table of static text used in this module for use when translating this module to other languages.

The values $1 and $2 are replaced with values as stated in the associated message comment

local i18n_t =

local section_top = i18n_t.top or -- i18n_t.top rarely needed mw.message.new ('vector-toc-beginning') -- lead section doesn't have a heading, get the interface message for 'top' :inLanguage (lang_obj:getCode) -- in the current wiki's content language :plain; -- and make sure we have a string

----------------------------< E R R O R _ M S G _ M A K E >--------------------------------------------------

common funtion to emit both fatal and non-fatal error messages

template_name used in error message help text link and error category link fetched from MediaWiki

error category emitted only for transclusions in the Talk namespace; only when i18n_t['error category'] has avalue; only when we have not already emitted the category link.

local err_cat_added; -- page-scope boolean flag; true when error cat link has been emmitted; nil elselocal function error_msg_make (template_name, msg, args_t, fatal) local err_cat = ; -- empty string for concatenation

if not err_cat_added and i18n_t['error category'] and 1

mw.title.getCurrentTitle.namespace then err_cat = mw.message.newRawMessage (i18n_t['error category'],):plain; -- create error category wikilink err_cat_added = true; -- we will do this only once end

local err_msg = table.concat ; return err_msg;end

----------------------------< R E D L I N K _ T E M P L A T E _ R E M O V E >--------------------------------

Following a preprocessing of the section heading, any templates not known to the local wiki will have been convertedto a template-space redlink to the unknown template-name in the locally named template namespace. These redlinksmust be removed and the section name marked as modified so that the section heading link can be suppressed.

returns section name and boolean true if redlinks were replaced; boolean false else

local function redlink_template_remove (section_name) local redlink_pattern = '%[%[:' .. mw.site.namespaces[10]["name"] .. ':.-%]%]'; -- fetch template names space name in the wiki's local language local count; section_name, count = section_name:gsub (redlink_pattern, '[...]'); -- replace unknown-template redlinks with bracketed ellipses return section_name, 0 < count;end

--

local function anchors_remove (section_name) local patterns =

for _, pattern in ipairs (patterns) do section_name = section_name:gsub (pattern, ); -- remove all anchor spans end return section_name;end

----------------------------< R E F S _ R E M O V E >--------------------------------------------------------

remove wikitext reference markup. done this way because we later preprocess the section name to render any templatesthat are present in the section name (there shouldn't be but that doesn't stop editors from including them).preprocessing a section name with reference markup causes MediaWiki to create a reflist; a side effect that wedon't want.

returns modified section name and boolean true when references have been removed; unmodified section name and false else.

local function refs_remove (section_name) local name; -- modified (or unmodified) section name local markup_removed; -- boolean true when reference markup has been removed local count; name, count = section_name:gsub ('', ); -- remove self-closed tags markup_removed = 0 < count; -- count not zero, set true name, count = name:gsub ('[1] ', ); -- remove remaining ref tags and content (if any)

return name, markup_removed or (0 < count)end

----------------------------< S T R I P M A R K E R S _ R E M O V E >----------------------------------------

remove stripmarkers from preprocessed section names. it may be best to preserve tags before section nameis preprocessed to prevent ' from being interpreted as bold markup. It is not possible to do thathere because all nowiki strip markers are only identifiable by the numbers.

returns modified section name and boolean true when stripmarkers have been removed; unmodified section name and false else.

local function stripmarkers_remove (section_name) local count; section_name, count = section_name:gsub ('\127[^\127]*UNIQ%-%-%a+%-[%x]+%-QINU[^\127]*\127', ); return section_name, (0 < count);end

--[=[-------------------------< R E M O V E _ W I K I _ L I N K >---------------------------------------------- Gets the display text from a wikilink like [[A|B]] or B gives B

The str:gsub returns either A|B from a B or B from B or B from B (no wikilink markup).

In l, l:gsub removes the link and pipe (if they exist); the second :gsub trims white space from the labelif str was wrapped in wikilink markup. Presumably, this is because without wikimarkup in str, there is no matchin the initial gsub, the replacement function l doesn't get called.

]=]

local function remove_wiki_link (str) return (str:gsub("%[%[([^%[%]]*)%]%]", function(l) return l:gsub("^[^|]*|(.*)$", "%1"):gsub("^%s*(.-)%s*$", "%1"); end));end

----------------------------< R E M O V E _ C O N T A I N E R >----------------------------------------------

Inspired from above, removes everything between < & >Used to remove html containers from headings to fix breaking section links, but legitimate text within < & > are removed too

returns text and boolean true if modified; text and boolean false else

local function remove_container (str) local count; str, count = str:gsub("<([^>]*)>", function(l) return l:gsub("^%s*(.-)%s*$", ""); end); return str, 0 < countend

--[=[-------------------------< M A K E _ W I K I L I N K >---------------------------------------------------- Makes a wikilink; when both link and display text is provided, returns a wikilink in the form [[L|D]]; if onlylink is provided, returns a wikilink in the form L; if neither are provided or link is omitted, returns anempty string.

]=]

local function make_wikilink (link, display) if link and ( ~= link) then if display and ( ~= display) then return table.concat ; else return table.concat ; end end return display or ; -- link not set so return the display textend

----------------------------< S E C T I O N _ D A T A _ G E T >----------------------------------------------

Read article content and fill associative arrays in sequence . The associative arrays hold sectionname, its starting location within content, section size, and section level (2 (

), 3 (

), etc). Modifies andreturns (article size) and (longest section).

local function section_data_get (content, total, max, sections_t, max_i) local s, e, name;

----------< M A X _ U P D A T E >---------- local function max_update (max, max_i, sec_size, sec_i) -- local function to update max and its index if max < sec_size then -- new section is longer return sec_size, sec_i; -- update and done end return max, max_i; -- no update end -------------------------------------------

while (1) do -- done this way because some articles reuse section names s, e, name = string.find (content, '\n

+ *(.-) *

+', e); -- get start, end, and section name beginning at end of last find; newline must precede '

' heading markup if s then table.insert (sections_t,); -- save section name and start location of this find else break; end end

local max_i = 1; -- assume lead is longest section for i, section_t in ipairs (sections_t) do if 1 ~= i then -- i

1 is the lead section; already accounted for local escaped_section_name = section_t.name:gsub ('[%(%)%.%%%+%-%*%?%[%^%$%]]', '%%%0'); -- escape lua patterns in section name local pattern = '(

+ *' .. escaped_section_name .. ' *

+.-)

+'; -- make a pattern to get the content of a section local section_content = string.match (content, pattern, section_t.start); -- get the content beginning at the string.find start location if section_content then section_t.size = #section_content; -- get a count of the bytes in the section total = total + section_t.size; max, max_i = max_update (max, max_i, section_t.size, i); -- keep track of largest count else -- probably the last section (no proper header follows this section name) pattern = '(

+ *' .. escaped_section_name .. ' *

+.+)'; -- make a new pattern section_content = string.match (content, pattern, section_t.start); -- try to get content if section_content then section_t.size = #section_content; -- get a count of the bytes in the section total = total + section_t.size; max, max_i = max_update (max, max_i, section_t.size, i); -- keep track of largest count else section_t.size = '—'; -- no content so show that end end local _; _, section_t.level = section_content:find ('^=+'); -- should always be the first n characters of section content end end

return total, max;end

----------------------------< A R T I C L E _ C O N T E N T _ G E T >----------------------------------------

Common function to fetch the

content. Also fetches byte count for lead section because that sectionis different from all others

On success, returns unparsed article content, lead section byte count, and nil message. On error, returns nilcontent, nil lead section byte count, and fatal error message

local function article_content_get (article, template_name) local title_obj = mw.title.new (article) local content = title_obj:getContent; -- get unparsed wikitext from the article if not content then return nil, nil, error_msg_make (template_name, i18n_t['fatal_no_article'],, true); -- emit fatal error message and abandon end

if title_obj.isRedirect then -- redirects don't have sections return nil, nil, error_msg_make (template_name, i18n_t['fatal_redirect'],, true); -- emit fatal error message and abandon end

local section_content = content:match ('(.-)

*'); -- get the lead section if not section_content then return nil, nil, error_msg_make (template_name, i18n_t['fatal_no_sections'],, true); -- emit fatal error message and abandon end

return content, #section_content; -- return the contnet and length of the lead section and nil for error messageend

--size|

}}

local function size (frame) local template_name = frame:getParent:getTitle; -- get template name for use in error messaging and category name

local section_info_t = ; -- table to hold section names and sizes local section_content; -- section content used for counting local totcount = ; local lastlevel; local maxlevels; local levelcounts = ; local upperlevel; local highlight; local highlighttot; local total; -- sum of all byte counts local max; -- largest section so far encountered local totmax; -- largest section so far encountered (section total) local _; -- dummy for using gsub to count bytes local wl_name; -- anchor and display portion for wikilinks in rendered list

local content, sec_0_count, msg = article_content_get (frame.args[1], template_name); -- get the article content with lead section byte count because lead is different from all others if msg then -- is something wrong return msg; -- emit message and abandon end

total = sec_0_count; max = sec_0_count; -- sequence of associative arrays with section name, starting location, size, and level local sections_t = ; -- init with lead info total, max = section_data_get (content, total, max, sections_t); -- fill arrays for all sections except the lead

totmax=0; lastlevel=0; maxlevels=7; for j=1,maxlevels do levelcounts[j]=0; end

local level, size; for i = #sections_t, 1, -1 do level = sections_t[i].level; size = sections_t[i].size;

if level < lastlevel then -- reset all totcount[i] = levelcounts[level] + size; for j=level,maxlevels do levelcounts[j]=0; end end

if level >= lastlevel then totcount[i] = size; end

if level > 0 then upperlevel = level - 1; levelcounts[upperlevel]=levelcounts[upperlevel]+totcount[i]; end

lastlevel = level; if totcount[i] > totmax then totmax = totcount[i]; end end

for i, section_t in ipairs (sections_t) do level = section_t.level; size = section_t.size;

if size

max then highlight='background:red;"|'; else local proportion = size / max -- get value of "how much of the max" the count is local gb = 250 - math.floor(250 * proportion) -- approach #f8f9fa [r=248,g=249,b=250] (default wikitable cell color) for small bytecounts highlight = string.format('background:#F8%02X%02X;"|', gb, gb) -- shade the bg as r: 248, g: gb, and b: gb end highlighttot=; -- start the style declaration if totcount[i]

totmax then highlighttot=highlighttot .. 'background:red;'; else local proportion = totcount[i] / totmax -- get value of "how much of the max" the count is local gb = 250 - math.floor(250 * proportion) -- approach #f8f9fa [r=248,g=249,b=250] (default wikitable cell color) for small bytecounts highlighttot=highlighttot .. string.format('background:#F8%02X%02X;', gb, gb) -- shade the bg as r: 248, g: gb, and b: gb end

if level

2 then highlighttot=highlighttot .. 'font-weight:bold;'; -- if main section, make it bold elseif totcount[i]

size then highlighttot='color:transparent;'; -- hide totals for subsections with no subsubsections, values required for proper sorting end highlighttot=highlighttot .. '"|'; -- close the style declaration level = (2 < level) and ((level - 2) * 1.6) or nil; -- remove offset and mult by 1.6em (same indent as ':' markup which doesn't work in a table)

local markup_removed; -- temp flag to note that the section heading has been modified (references and html-like markup stripped, etc) local modified = false; -- flag to select section heading styling; false: wikilink; true: plain text with error message wl_name, modified = refs_remove (section_t.name); -- remove all references wl_name = remove_wiki_link (wl_name); -- remove all wikilinks wl_name = wl_name:gsub ('', '__sss_nowiki/__'); -- replace tag with special secret symbol wl_name = frame:preprocess (wl_name); -- render to html wl_name, markup_removed = redlink_template_remove (wl_name); -- remove redlink template wikilinks created by preprocessing of unknown templates modified = modified or markup_removed; -- update wl_name = anchors_remove (wl_name); -- remove known anchor markup; these allowed in section heading so do not bump wl_name = wl_name:gsub ('__sss_nowiki/__', ''); -- replace special secret symbol with tag wl_name, markup_removed = stripmarkers_remove (wl_name); -- remove any strip markers resulting from preprocessing modified = modified or markup_removed; -- update wl_name = wl_name:gsub ('', '\'\); -- italic markup has been converted to html; unconvert so remove_container doesn't remove inappropriately wl_name = wl_name:gsub ('', '\'\'\); -- bold markup has been converted to html; unconvert so remove_container doesn't remove inappropriately wl_name, markup_removed = remove_container (wl_name); -- remove html containers from section headings so that we can link to the section modified = modified or markup_removed; -- update

wl_name = wl_name:gsub ('[%[%]]',); -- replace '[' and ']' characters with html entities so that wikilinked section names work wl_name = mw.text.trim (wl_name); -- trim leading/trailing white space if any because white space buggers up url anchor links local heading_text; if modified then heading_text = table.concat ; -- close help link else heading_text = make_wikilink (frame.args[1] .. '#' .. wl_name:gsub ("+", ), wl_name); -- unmodified rendered as is end

table.insert (section_info_t, table.concat); end

local out = ; -- make a sortable wikitable for output table.insert (out, string.format ('

%s', -- output caption and column headings frame.args.style or , -- value for style= attribute mw.message.newRawMessage (i18n_t['table caption'],):plain ));

table.insert (out, table.concat);

table.insert (out, table.concat (section_info_t, '\n

-\n')); -- section rows with leading pipes (except first row already done) table.insert (out, table.concat); table.insert (out, '\n
'); -- close the wikitable local result = table.concat (out, ); return result; -- because gsub returns string and number of replacementsend

--_nosep= – accepts one value 'yes'; renders section size without thousands separator; ignored when |_pct= set; default is commafied |_pct= – accepts one value 'yes'; function returns size specified by name or keyword as a percentage of _total rounded to two decimals; appends '%' symbol; _total as a percentage of _total allowed but why?

returns nil when there is no keyword match and no

match

local function section_size_get (frame) local args_t = require ("Module:Arguments").getArgs (frame); local template_name = frame:getParent:getTitle; -- get template name for use in error messaging and category name local nosep = 'yes'

args_t._nosep; -- boolean true when |_nosep=yes; false else local pct = 'yes'

args_t._pct; -- boolean true when |_pct=yes; false else local content, sec_0_count, msg = article_content_get (args_t[1], template_name); -- get the article content with lead section byte count in because lead is different from all others if msg then -- is something wrong return msg; -- emit message and abandon end

local total = sec_0_count; -- initialize with length of the lead local max = sec_0_count; local max_i = 1; -- updated by section_data_get ----------< R E T V A L >---------- local function retval (size) -- local function to select return value format according to |pct= return pct and string.format ("%2.2f%%", 100 * (size / total)) or -- return the percentage lang_obj:formatNum (size,); -- return the size end ----------------------------------- -- sequence of associative arrays with section name, starting location, size, and level local sections_t = ; -- init with lead info total, max, max_i = section_data_get (content, total, max, sections_t, max_i); -- max_i only used here if '_lead'

args_t[2] then -- has '_lead' keyword; lead section is handled differently from all other sections return retval (sec_0_count); -- return selected output form and done

elseif '_max'

args_t[2] then -- when has '_max' keyword return retval (max); -- return selected output form and done

elseif '_total'

args_t[2] then -- when has '_total' keyword return retval (total); -- return the article's total length and done; pct possible, but why?

elseif args_t[2] then -- if set, it should be a section name local target_section_name = args_t[2]:lower; -- get it and force lower case for comparison local section_level; local section_size = 0; for i, section_t in ipairs (sections_t) do -- loop through looking for a match; TODO: is there a better way to do this? if not section_level then -- not yet found a match if section_t.name:lower

target_section_name then -- force lower case for comparison if '_all'

args_t[3] then -- when has '_all' keyword section_level = section_t.level; -- found it so initialize these; this one is a flag to know that we found the target section section_size = section_t.size; -- init else return retval (section_t.size); -- return selected output form and done end end elseif section_level < section_t.level then -- here when we found the section, '_all' keyword present section_size = section_size + section_t.size; -- this section level greater than target's, so add in the subsection's size else -- here when we found the section, '_all' keyword present return retval (section_size); -- return selected output form and done end end

else -- here when has nothing return nil; -- so return nothing endend

----------------------------< E X P O R T E D F U N C T I O N S >------------------------------------------

return

Notes and References

  1. .-