Editing
Module
:
TextFit
Give feedback
Jump to navigation
Jump to search
Warning:
You are not logged in. Once you make an edit, a temporary account will be created for you.
Learn more
.
Log in
or
create an account
to continue receiving notifications after this account expires, and to access other features.
Anti-spam check. Do
not
fill this in!
--[==[ Estimates the largest font size at which a string will wrap into a box of a given width and height, using real per-glyph advance widths rather than a character count. Everything is measured at bold weight. Retail Demo's bold advances run about 4% wider than its regular ones, so measuring regular text against the bold table can only make the estimate slightly conservative -- text comes out a hair small rather than overflowing -- and it removes the need to track font weight through the markup at all. Both callers render their text bold in any case. Widths are in em: each font's raw advances divided by its own unitsPerEm, so the tables are directly comparable despite the differing internal grids. Extracted with fonttools from: Retail Demo bold [[:File:Retaildemo-bold.woff2]] unitsPerEm 1000 Open Sans [[:File:Open_Sans.woff2]] unitsPerEm 2048 Note the level-2 long brackets on this comment: a plain --[[ ]] comment would be closed by the first ]] below, which the wiki links above contain. ]==] local p = {} local ustring = mw.ustring -- Retail Demo, bold weight. Invisible characters are spelled with -- ustring.char so they cannot be mistaken for a plain space when edited. local WIDTHS = { [" "]=0.240, [","]=0.281, ["."]=0.281, ["0"]=0.662, ["1"]=0.312, ["2"]=0.560, ["3"]=0.566, ["4"]=0.612, ["5"]=0.562, ["6"]=0.601, ["7"]=0.520, ["8"]=0.583, ["9"]=0.598, A=0.677, B=0.590, C=0.621, D=0.693, E=0.552, F=0.530, G=0.686, H=0.702, I=0.276, J=0.301, K=0.637, L=0.491, M=0.904, N=0.695, O=0.741, P=0.585, Q=0.740, R=0.628, S=0.555, T=0.555, U=0.694, V=0.668, W=0.913, X=0.667, Y=0.635, Z=0.666, a=0.573, b=0.608, c=0.473, d=0.604, e=0.570, f=0.382, g=0.558, h=0.608, i=0.269, j=0.266, k=0.584, l=0.268, m=0.907, n=0.602, o=0.611, p=0.604, q=0.605, r=0.392, s=0.496, t=0.404, u=0.602, v=0.549, w=0.776, x=0.571, y=0.550, z=0.561, [ustring.char(0xA0)]=0.240, -- no-break space } -- No bold Open Sans is loaded, so browsers synthesise it. Estimated from -- Retail Demo's measured bold/regular ratio of 1.037. local SYNTH_BOLD = 1.04 -- Open Sans, for the characters Retail Demo does not contain local FALLBACK_WIDTHS = { ["!"]=0.264, ["\""]=0.398, ["#"]=0.646, ["$"]=0.572, ["%"]=0.827, ["&"]=0.729, ["'"]=0.219, ["("]=0.295, [")"]=0.295, ["*"]=0.551, ["+"]=0.572, ["-"]=0.322, ["/"]=0.367, [":"]=0.263, [";"]=0.263, ["<"]=0.572, ["="]=0.572, [">"]=0.572, ["?"]=0.432, ["@"]=0.896, ["["]=0.327, ["\\"]=0.367, ["]"]=0.327, ["^"]=0.572, ["_"]=0.438, ["`"]=0.277, ["{"]=0.375, ["|"]=0.549, ["}"]=0.375, ["~"]=0.572, ["¡"]=0.264, ["¢"]=0.572, ["£"]=0.572, ["¤"]=0.572, ["¥"]=0.572, ["¦"]=0.549, ["§"]=0.514, ["¨"]=0.580, ["©"]=0.832, ["ª"]=0.353, ["«"]=0.496, ["¬"]=0.572, [ustring.char(0xAD)]=0.322, -- soft hyphen ["®"]=0.832, ["¯"]=0.500, ["°"]=0.428, ["±"]=0.572, ["²"]=0.348, ["³"]=0.348, ["´"]=0.277, ["µ"]=0.618, ["¶"]=0.655, ["·"]=0.263, ["¸"]=0.222, ["¹"]=0.348, ["º"]=0.374, ["»"]=0.496, ["¼"]=0.740, ["½"]=0.768, ["¾"]=0.778, ["¿"]=0.432, ["À"]=0.632, ["Á"]=0.632, ["Â"]=0.632, ["Ã"]=0.632, ["Ä"]=0.632, ["Å"]=0.632, ["Æ"]=0.868, ["Ç"]=0.630, ["È"]=0.556, ["É"]=0.556, ["Ê"]=0.556, ["Ë"]=0.556, ["Ì"]=0.279, ["Í"]=0.279, ["Î"]=0.279, ["Ï"]=0.279, ["Ð"]=0.726, ["Ñ"]=0.753, ["Ò"]=0.778, ["Ó"]=0.778, ["Ô"]=0.778, ["Õ"]=0.778, ["Ö"]=0.778, ["×"]=0.572, ["Ø"]=0.778, ["Ù"]=0.729, ["Ú"]=0.729, ["Û"]=0.729, ["Ü"]=0.729, ["Ý"]=0.559, ["Þ"]=0.602, ["ß"]=0.623, ["à"]=0.556, ["á"]=0.556, ["â"]=0.556, ["ã"]=0.556, ["ä"]=0.556, ["å"]=0.556, ["æ"]=0.862, ["ç"]=0.479, ["è"]=0.562, ["é"]=0.562, ["ê"]=0.562, ["ë"]=0.562, ["ì"]=0.252, ["í"]=0.252, ["î"]=0.252, ["ï"]=0.252, ["ð"]=0.600, ["ñ"]=0.613, ["ò"]=0.602, ["ó"]=0.602, ["ô"]=0.602, ["õ"]=0.602, ["ö"]=0.602, ["÷"]=0.572, ["ø"]=0.602, ["ù"]=0.613, ["ú"]=0.613, ["û"]=0.613, ["ü"]=0.613, ["ý"]=0.501, ["þ"]=0.612, ["ÿ"]=0.501, ["Ā"]=0.632, ["ā"]=0.556, ["Ă"]=0.632, ["ă"]=0.556, ["Ą"]=0.632, ["ą"]=0.556, ["Ć"]=0.630, ["ć"]=0.479, ["Ĉ"]=0.630, ["ĉ"]=0.479, ["Ċ"]=0.630, ["ċ"]=0.479, ["Č"]=0.630, ["č"]=0.479, ["Ď"]=0.726, ["ď"]=0.612, ["Đ"]=0.726, ["đ"]=0.613, ["Ē"]=0.556, ["ē"]=0.562, ["Ĕ"]=0.556, ["ĕ"]=0.562, ["Ė"]=0.556, ["ė"]=0.562, ["Ę"]=0.556, ["ę"]=0.562, ["Ě"]=0.556, ["ě"]=0.562, ["Ĝ"]=0.727, ["ĝ"]=0.543, ["Ğ"]=0.727, ["ğ"]=0.543, ["Ġ"]=0.727, ["ġ"]=0.543, ["Ģ"]=0.727, ["ģ"]=0.543, ["Ĥ"]=0.737, ["ĥ"]=0.613, ["Ħ"]=0.737, ["ħ"]=0.613, ["Ĩ"]=0.279, ["ĩ"]=0.252, ["Ī"]=0.279, ["ī"]=0.252, ["Ĭ"]=0.279, ["ĭ"]=0.252, ["Į"]=0.279, ["į"]=0.252, ["İ"]=0.279, ["ı"]=0.252, ["IJ"]=0.548, ["ij"]=0.505, ["Ĵ"]=0.269, ["ĵ"]=0.252, ["Ķ"]=0.612, ["ķ"]=0.525, ["ĸ"]=0.525, ["Ĺ"]=0.522, ["ĺ"]=0.252, ["Ļ"]=0.522, ["ļ"]=0.252, ["Ľ"]=0.522, ["ľ"]=0.252, ["Ŀ"]=0.522, ["ŀ"]=0.261, ["Ł"]=0.522, ["ł"]=0.252, ["Ń"]=0.753, ["ń"]=0.613, ["Ņ"]=0.753, ["ņ"]=0.613, ["Ň"]=0.753, ["ň"]=0.613, ["ʼn"]=0.676, ["Ŋ"]=0.753, ["ŋ"]=0.613, ["Ō"]=0.778, ["ō"]=0.602, ["Ŏ"]=0.778, ["ŏ"]=0.602, ["Ő"]=0.778, ["ő"]=0.602, ["Œ"]=0.925, ["œ"]=0.948, ["Ŕ"]=0.617, ["ŕ"]=0.409, ["Ŗ"]=0.617, ["ŗ"]=0.409, ["Ř"]=0.617, ["ř"]=0.409, ["Ś"]=0.548, ["ś"]=0.477, ["Ŝ"]=0.548, ["ŝ"]=0.477, ["Ş"]=0.548, ["ş"]=0.477, ["Š"]=0.548, ["š"]=0.477, ["Ţ"]=0.551, ["ţ"]=0.356, ["Ť"]=0.551, ["ť"]=0.356, ["Ŧ"]=0.551, ["ŧ"]=0.356, ["Ũ"]=0.729, ["ũ"]=0.613, ["Ū"]=0.729, ["ū"]=0.613, ["Ŭ"]=0.729, ["ŭ"]=0.613, ["Ů"]=0.729, ["ů"]=0.613, ["Ű"]=0.729, ["ű"]=0.613, ["Ų"]=0.729, ["ų"]=0.613, ["Ŵ"]=0.923, ["ŵ"]=0.775, ["Ŷ"]=0.559, ["ŷ"]=0.501, ["Ÿ"]=0.559, ["Ź"]=0.572, ["ź"]=0.469, ["Ż"]=0.572, ["ż"]=0.469, ["Ž"]=0.572, ["ž"]=0.469, ["ſ"]=0.323, ["Ѐ"]=0.556, ["Ё"]=0.556, ["Ђ"]=0.733, ["Ѓ"]=0.520, ["Є"]=0.639, ["Ѕ"]=0.548, ["І"]=0.279, ["Ї"]=0.279, ["Ј"]=0.269, ["Љ"]=0.935, ["Њ"]=0.958, ["Ћ"]=0.733, ["Ќ"]=0.610, ["Ѝ"]=0.760, ["Ў"]=0.618, ["Џ"]=0.728, ["А"]=0.632, ["Б"]=0.612, ["В"]=0.646, ["Г"]=0.520, ["Д"]=0.684, ["Е"]=0.556, ["Ж"]=0.841, ["З"]=0.583, ["И"]=0.760, ["Й"]=0.760, ["К"]=0.610, ["Л"]=0.703, ["М"]=0.899, ["Н"]=0.737, ["О"]=0.778, ["П"]=0.728, ["Р"]=0.602, ["С"]=0.630, ["Т"]=0.551, ["У"]=0.618, ["Ф"]=0.797, ["Х"]=0.578, ["Ц"]=0.737, ["Ч"]=0.693, ["Ш"]=1.032, ["Щ"]=1.037, ["Ъ"]=0.686, ["Ы"]=0.848, ["Ь"]=0.634, ["Э"]=0.630, ["Ю"]=1.048, ["Я"]=0.632, ["а"]=0.556, ["б"]=0.595, ["в"]=0.574, ["г"]=0.431, ["д"]=0.575, ["е"]=0.562, ["ж"]=0.735, ["з"]=0.483, ["и"]=0.636, ["й"]=0.636, ["к"]=0.517, ["л"]=0.572, ["м"]=0.733, ["н"]=0.633, ["о"]=0.602, ["п"]=0.620, ["р"]=0.612, ["с"]=0.479, ["т"]=0.471, ["у"]=0.501, ["ф"]=0.715, ["х"]=0.523, ["ц"]=0.626, ["ч"]=0.607, ["ш"]=0.890, ["щ"]=0.897, ["ъ"]=0.687, ["ы"]=0.768, ["ь"]=0.590, ["э"]=0.494, ["ю"]=0.829, ["я"]=0.554, ["ѐ"]=0.562, ["ё"]=0.562, ["ђ"]=0.613, ["ѓ"]=0.431, ["є"]=0.493, ["ѕ"]=0.477, ["і"]=0.252, ["ї"]=0.252, ["ј"]=0.252, ["љ"]=0.839, ["њ"]=0.889, ["ћ"]=0.613, ["ќ"]=0.517, ["ѝ"]=0.636, ["ў"]=0.501, ["џ"]=0.622, ["–"]=0.500, ["—"]=1.000, ["―"]=1.000, ["‘"]=0.169, ["’"]=0.169, ["‚"]=0.245, ["‛"]=0.169, ["“"]=0.349, ["”"]=0.349, ["…"]=0.778, ["€"]=0.572, } -- Characters in neither face (CJK, rare symbols). local CJK_WIDTH = 2.0 local FALLBACK = 0.55 local function charWidth(ch) local w = WIDTHS[ch] if w then return w end w = FALLBACK_WIDTHS[ch] if w then return w * SYNTH_BOLD end local cp = ustring.codepoint(ch) or 0 if (cp >= 0x1100 and cp <= 0x115F) or (cp >= 0x2E80 and cp <= 0xA4CF) or (cp >= 0xAC00 and cp <= 0xD7A3) or (cp >= 0xF900 and cp <= 0xFAFF) or (cp >= 0xFF00 and cp <= 0xFF60) or (cp >= 0xFFE0 and cp <= 0xFFE6) then return CJK_WIDTH end return FALLBACK end local function wordWidth(word) local total = 0 for ch in ustring.gmatch(word, ".") do total = total + charWidth(ch) end return total end -- Strip wiki markup that carries no visible text of its own. local function stripMarkup(src) src = mw.text.killMarkers(src or "") src = src:gsub("%[%[[Ff]ile:[^%[%]]*%]%]", "") src = src:gsub("%[%[[^%[%]|]*|([^%[%]]*)%]%]", "%1") src = src:gsub("%[%[([^%[%]]*)%]%]", "%1") -- Quote markup survives frame:preprocess; strip it so it is not measured. return (src:gsub("'''''", ""):gsub("'''", ""):gsub("''", "")) end -- Reduce expanded wikitext/HTML to the text a reader actually sees: a list of -- lines, each a list of words. -- Tags are deleted rather than replaced, so a word broken across a tag boundary -- stays one word. Splitting on ASCII whitespace only leaves NBSP joining its -- neighbours, which is how the browser wraps. function p.parse(src) src = stripMarkup(src) src = src:gsub("<%s*[bB][rR]%s*/?%s*>", "\n") src = src:gsub("<[^<>]*>", "") -- Decode after the tags are gone, so an escaped < cannot look like one. src = mw.text.decode(src, true) local lines = {} for line in (src .. "\n"):gmatch("([^\n]*)\n") do local words = {} for word in line:gmatch("%S+") do words[#words + 1] = word end lines[#lines + 1] = words end return lines end -- Greedy wrap with the usable width normalised to 1, so `r` is the font size -- expressed as a fraction of that width. Takes lines of word WIDTHS, already -- measured by fit(). Returns the number of rendered lines. local function lineCount(lines, r) local space = charWidth(" ") * r local total = 0 for _, widths in ipairs(lines) do local cur = 0 total = total + 1 for _, ww in ipairs(widths) do ww = ww * r if cur == 0 then cur = ww elseif cur + space + ww <= 1 then cur = cur + space + ww else total = total + 1 cur = ww end end end return total end -- Largest font size at which `text` fits its box, returned as a PERCENTAGE of -- the box's usable text width. Emit it as a cqw value against a query -- container whose content box is exactly that width; no pixel geometry is -- needed here or in the caller. -- -- opts: -- aspect max height of the text block, as a multiple of its width -- lineHeight line-height multiplier used when rendering -- max, min ceiling and floor, also as a percentage of the width -- step search granularity function p.fit(text, opts) local aspect = opts.aspect local lineH = opts.lineHeight or 1.2 local maxPct = opts.max or 100 local minPct = opts.min or 1 local step = opts.step or 0.1 -- Measure every word once; the shrink loop below only scales the results. local lines, widest = {}, 0 for _, words in ipairs(p.parse(text)) do local widths = {} for i, word in ipairs(words) do widths[i] = wordWidth(word) if widths[i] > widest then widest = widths[i] end end lines[#lines + 1] = widths end if widest == 0 then return maxPct end -- The longest single word must not overflow horizontally local pct = math.min(100 / widest, maxPct) -- Then shrink until the wrapped block fits vertically while pct > minPct and lineCount(lines, pct / 100) * lineH * (pct / 100) > aspect do pct = pct - step end return math.max(pct, minPct) end return p
Summary:
Please note that all contributions to The Deadlock Wiki are considered to be released under the Creative Commons Attribution-NonCommercial-ShareAlike (see
Deadlock:Copyrights
for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource.
Do not submit copyrighted work without permission!
Cancel
Editing help
(opens in new window)
Preview page with this template
Page included on this page:
Module:TextFit/doc
(
view source
)
Navigation menu
Personal tools
Create account
Log in
associated-pages
Module
Discussion
English
Views
Read
Edit source
History
Tools
Search
Discord
Discord
Navigation
Main page
About us
Random page
Special pages
Recent changes
Holliday/Sounds
5h ago
·
Gammaton32
Holliday/Sounds
5h ago
·
Gammaton32
Holliday/Voice lines
5h ago
·
Gammaton32
Mirage
5h ago
·
Gammaton32
Show more...
Interface Admin
Common CSS
Vector CSS
Minerva CSS
Citizen CSS
Brown theme CSS
Gadgets
Gadgets definition
Editing
How to edit
Style guide
All guides
New files
Unresolved feedback
Tools
What links here
Related changes
Page information