Editing Module:TextFitGive feedback
Jump to navigation
Jump to search
The edit can be undone.
Please check the comparison below to verify that this is what you want to do, and then publish the changes below to finish undoing the edit.
| Latest revision | Your text | ||
| Line 5: | Line 5: | ||
character count. | character count. | ||
Widths are in em. Sources, all extracted with fonttools: | |||
Retail Demo regular [[:File:Retaildemo-regular.woff2]] unitsPerEm 1000 | |||
Retail Demo bold [[:File:Retaildemo-bold.woff2]] unitsPerEm 1000 | |||
Open Sans [[:File:Open_Sans.woff2]] unitsPerEm 2048 | |||
Widths are in em | |||
Retail Demo | |||
]==] | ]==] | ||
| Line 24: | Line 14: | ||
local ustring = mw.ustring | local ustring = mw.ustring | ||
-- Retail Demo, | -- Retail Demo, normal weight | ||
-- | local RD_REGULAR = { | ||
local | [" "]=0.240, [","]=0.221, ["."]=0.221, ["0"]=0.645, ["1"]=0.268, ["2"]=0.549, ["3"]=0.557, ["4"]=0.601, | ||
["5"]=0.538, ["6"]=0.583, ["7"]=0.487, ["8"]=0.555, ["9"]=0.580, A=0.624, B=0.585, C=0.641, | |||
D=0.696, E=0.554, F=0.525, G=0.695, H=0.703, I=0.237, J=0.268, K=0.626, | |||
L=0.494, M=0.927, N=0.700, O=0.730, P=0.558, Q=0.734, R=0.614, S=0.527, | |||
T=0.550, U=0.679, V=0.624, W=0.939, X=0.665, Y=0.603, Z=0.647, a=0.553, | |||
b=0.587, c=0.530, d=0.588, e=0.551, f=0.394, g=0.544, h=0.600, i=0.238, | |||
j=0.235, k=0.552, l=0.238, m=0.923, n=0.600, o=0.603, p=0.583, q=0.587, | |||
r=0.369, s=0.477, t=0.413, u=0.600, v=0.509, w=0.810, x=0.540, y=0.513, | |||
z=0.554, | |||
[" "]=0.240, -- U+00A0 no-break space | |||
} | |||
-- Retail Demo, bold weight | |||
local RD_BOLD = { | |||
[" "]=0.240, [","]=0.281, ["."]=0.281, ["0"]=0.662, ["1"]=0.312, ["2"]=0.560, ["3"]=0.566, ["4"]=0.612, | [" "]=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, | ["5"]=0.562, ["6"]=0.601, ["7"]=0.520, ["8"]=0.583, ["9"]=0.598, A=0.677, B=0.590, C=0.621, | ||
| Line 36: | Line 39: | ||
r=0.392, s=0.496, t=0.404, u=0.602, v=0.549, w=0.776, x=0.571, y=0.550, | 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, | z=0.561, | ||
[ | [" "]=0.240, -- U+00A0 no-break space | ||
} | } | ||
-- Open Sans, for the characters Retail Demo does not contain | -- Open Sans, for the characters Retail Demo does not contain | ||
| Line 50: | Line 49: | ||
["_"]=0.438, ["`"]=0.277, ["{"]=0.375, ["|"]=0.549, ["}"]=0.375, ["~"]=0.572, ["¡"]=0.264, ["¢"]=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.572, ["¤"]=0.572, ["¥"]=0.572, ["¦"]=0.549, ["§"]=0.514, ["¨"]=0.580, ["©"]=0.832, ["ª"]=0.353, | ||
["«"]=0.496, ["¬"]=0.572, [ | ["«"]=0.496, ["¬"]=0.572, [""]=0.322, ["®"]=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.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.496, ["¼"]=0.740, ["½"]=0.768, ["¾"]=0.778, ["¿"]=0.432, ["À"]=0.632, ["Á"]=0.632, ["Â"]=0.632, | ||
| Line 92: | Line 90: | ||
["‘"]=0.169, ["’"]=0.169, ["‚"]=0.245, ["‛"]=0.169, ["“"]=0.349, ["”"]=0.349, ["…"]=0.778, ["€"]=0.572, | ["‘"]=0.169, ["’"]=0.169, ["‚"]=0.245, ["‛"]=0.169, ["“"]=0.349, ["”"]=0.349, ["…"]=0.778, ["€"]=0.572, | ||
} | } | ||
-- No bold Open Sans is loaded, so browsers synthesise it | |||
-- Estimated from Retail Demo's measured bold/regular of 1.037. | |||
local SYNTH_BOLD = 1.04 | |||
-- Characters in neither face (CJK, rare symbols). | -- Characters in neither face (CJK, rare symbols). | ||
| Line 97: | Line 99: | ||
local FALLBACK = 0.55 | local FALLBACK = 0.55 | ||
local function charWidth(ch) | local VOID_TAGS = { br = true, img = true, hr = true, input = true, | ||
local w = | meta = true, link = true, wbr = true } | ||
local BOLD_TAGS = { b = true, strong = true } | |||
local function charWidth(ch, bold) | |||
local w = (bold and RD_BOLD or RD_REGULAR)[ch] | |||
if w then return w end | if w then return w end | ||
w = FALLBACK_WIDTHS[ch] | w = FALLBACK_WIDTHS[ch] | ||
if w then return w * SYNTH_BOLD end | if w then | ||
return bold and w * SYNTH_BOLD or w | |||
end | |||
local cp = ustring.codepoint(ch) or 0 | local cp = ustring.codepoint(ch) or 0 | ||
| Line 115: | Line 123: | ||
local function wordWidth(word) | local function wordWidth(word) | ||
local total = 0 | local total = 0 | ||
for ch in ustring.gmatch( | for _, part in ipairs(word) do | ||
for ch in ustring.gmatch(part.text, ".") do | |||
total = total + charWidth(ch, part.bold) | |||
end | |||
end | end | ||
return total | return total | ||
end | end | ||
-- | -- Reduce expanded wikitext/HTML to the text a reader actually sees, keeping | ||
-- track of which runs are bold. Returns a list of lines; each line is a list | |||
-- of words; each word is a list of {text=, bold=} parts. | |||
-- Words break only on real whitespace: NBSP and tag boundaries do not split | |||
-- them, matching how the browser wraps. | |||
function p.parse(src, baseBold) | |||
src = mw.text.killMarkers(src or "") | src = mw.text.killMarkers(src or "") | ||
src = src:gsub("%[%[[Ff]ile:[^%[%]]*%]%]", "") | src = src:gsub("%[%[[Ff]ile:[^%[%]]*%]%]", "") | ||
| Line 128: | Line 142: | ||
src = src:gsub("%[%[([^%[%]]*)%]%]", "%1") | src = src:gsub("%[%[([^%[%]]*)%]%]", "%1") | ||
-- Quote markup survives frame:preprocess; strip it so it is not measured. | -- Quote markup survives frame:preprocess; strip it so it is not measured. | ||
src = src:gsub("'''''", ""):gsub("'''", ""):gsub("''", "") | |||
local lines, line, word, part = {}, {}, {}, nil | |||
local stack = {} | |||
local | local function boldNow() | ||
if baseBold then return true end | |||
for k = #stack, 1, -1 do | |||
if stack[k] then return true end | |||
end | |||
return false | |||
end | |||
local function endWord() | |||
if part then table.insert(word, part); part = nil end | |||
if #word > 0 then table.insert(line, word); word = {} end | |||
end | |||
local function endLine() | |||
endWord() | |||
table.insert(lines, line); line = {} | |||
end | |||
local function push(ch, bold) | |||
if part == nil or part.bold ~= bold then | |||
if part then table.insert(word, part) end | |||
part = { text = ch, bold = bold } | |||
else | |||
part.text = part.text .. ch | |||
end | end | ||
end | end | ||
local i, n = 1, #src | |||
while i <= n do | |||
if src:sub(i, i) == "<" then | |||
local j = src:find(">", i, true) | |||
if not j then break end | |||
local tag = src:sub(i + 1, j - 1) | |||
local closing, name = tag:match("^%s*(/?)%s*(%a+)") | |||
if name then | |||
name = name:lower() | |||
if name == "br" then | |||
endLine() | |||
elseif VOID_TAGS[name] then -- no effect on weight | |||
elseif closing == "/" then | |||
table.remove(stack) | |||
elseif not tag:match("/%s*$") then | |||
table.insert(stack, BOLD_TAGS[name] == true | |||
or tag:lower():match("font%-weight%s*:%s*bold") ~= nil | |||
or tag:lower():match("font%-weight%s*:%s*[6-9]00") ~= nil) | |||
end | |||
end | |||
i = j + 1 | |||
else | |||
local k = src:find("<", i, true) or (n + 1) | |||
local chunk = mw.text.decode(src:sub(i, k - 1), true) | |||
local bold = boldNow() | |||
for ch in ustring.gmatch(chunk, ".") do | |||
if ch == "\n" then | |||
endLine() | |||
elseif ch == " " or ch == "\t" or ch == "\r" then | |||
endWord() | |||
else | |||
push(ch, bold) -- NBSP falls here: joins, never breaks | |||
end | |||
end | |||
i = k | |||
end | |||
end | |||
endLine() | |||
return lines | return lines | ||
end | end | ||
-- Greedy wrap with the usable width normalised to 1, so `r` is the font size | -- Greedy wrap with the usable width normalised to 1, so `r` is the font size | ||
-- expressed as a fraction of that width | -- expressed as a fraction of that width. Returns the number of rendered lines. | ||
local function lineCount(lines, r, bold) | |||
local function lineCount(lines, r) | local space = charWidth(" ", bold == true) * r | ||
local space = charWidth(" ") * r | |||
local total = 0 | local total = 0 | ||
for _, | for _, words in ipairs(lines) do | ||
local cur = 0 | local cur = 0 | ||
total = total + 1 | total = total + 1 | ||
for _, | for _, word in ipairs(words) do | ||
ww = | local ww = wordWidth(word) * r | ||
if cur == 0 then | if cur == 0 then | ||
cur = ww | cur = ww | ||
| Line 194: | Line 251: | ||
local minPct = opts.min or 1 | local minPct = opts.min or 1 | ||
local step = opts.step or 0.1 | local step = opts.step or 0.1 | ||
local bold = opts.bold == true | |||
local lines = p.parse(text, bold) | |||
local widest = 0 | |||
local | for _, words in ipairs(lines) do | ||
for _, words in ipairs( | for _, word in ipairs(words) do | ||
local ww = wordWidth(word) | |||
for | if ww > widest then widest = ww end | ||
if | |||
end | end | ||
end | end | ||
if widest == 0 then return maxPct end | if widest == 0 then return maxPct end | ||
| Line 211: | Line 268: | ||
-- Then shrink until the wrapped block fits vertically | -- Then shrink until the wrapped block fits vertically | ||
while pct > minPct and lineCount(lines, pct / 100) * lineH * (pct / 100) > aspect do | while pct > minPct and lineCount(lines, pct / 100, bold) * lineH * (pct / 100) > aspect do | ||
pct = pct - step | pct = pct - step | ||
end | end | ||