Module:TextFit: Difference between revisionsGive feedback
Lift the parser's inner closures to top-level functions taking an explicit state table, and note in the header why the comment uses level-2 long brackets (with help from vergir-bot LLM) |
Measure everything at bold weight: drop the regular width table and the weight-tracking parser, measure each word once (with help from vergir-bot LLM) |
||
| Line 4: | Line 4: | ||
given width and height, using real per-glyph advance widths rather than a | given width and height, using real per-glyph advance widths rather than a | ||
character count. | 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 | 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. | the tables are directly comparable despite the differing internal grids. | ||
Extracted with fonttools from: | Extracted with fonttools from: | ||
Retail Demo | Retail Demo bold [[:File:Retaildemo-bold.woff2]] unitsPerEm 1000 | ||
Open Sans [[:File:Open_Sans.woff2]] unitsPerEm 2048 | |||
Open Sans | |||
Note the level-2 long brackets on this comment: a plain --[[ ]] comment would | Note the level-2 long brackets on this comment: a plain --[[ ]] comment would | ||
| Line 19: | Line 24: | ||
local ustring = mw.ustring | local ustring = mw.ustring | ||
-- Retail Demo, | -- Retail Demo, bold weight. Invisible characters are spelled with | ||
-- ustring.char so they cannot be mistaken for a plain space when edited. | |||
local WIDTHS = { | |||
local | |||
[" "]=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 44: | Line 36: | ||
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, | ||
[ | [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 | -- Open Sans, for the characters Retail Demo does not contain | ||
| Line 54: | Line 50: | ||
["_"]=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, [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.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 95: | Line 92: | ||
["‘"]=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, | ||
} | } | ||
-- Characters in neither face (CJK, rare symbols). | -- Characters in neither face (CJK, rare symbols). | ||
| Line 104: | Line 97: | ||
local FALLBACK = 0.55 | local FALLBACK = 0.55 | ||
local function charWidth(ch) | |||
local w = WIDTHS[ch] | |||
local function charWidth(ch | |||
local w = | |||
if w then return w end | if w then return w end | ||
w = FALLBACK_WIDTHS[ch] | w = FALLBACK_WIDTHS[ch] | ||
if w then | if w then return w * SYNTH_BOLD end | ||
local cp = ustring.codepoint(ch) or 0 | local cp = ustring.codepoint(ch) or 0 | ||
| Line 128: | Line 115: | ||
local function wordWidth(word) | local function wordWidth(word) | ||
local total = 0 | local total = 0 | ||
for ch in ustring.gmatch(word, ".") do | |||
total = total + charWidth(ch) | |||
end | end | ||
return total | return total | ||
end | end | ||
| Line 187: | Line 131: | ||
end | 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) | |||
-- Reduce expanded wikitext/HTML to the text a reader actually sees | |||
-- | |||
-- | |||
-- | |||
-- | |||
function p.parse(src | |||
src = stripMarkup(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 | 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 | end | ||
lines[#lines + 1] = words | |||
end | end | ||
return lines | |||
return | |||
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. Returns the number of rendered lines. | -- expressed as a fraction of that width. Takes lines of word WIDTHS, already | ||
local function lineCount(lines, r | -- measured by fit(). Returns the number of rendered lines. | ||
local space = charWidth(" " | local function lineCount(lines, r) | ||
local space = charWidth(" ") * r | |||
local total = 0 | local total = 0 | ||
for _, | for _, widths in ipairs(lines) do | ||
local cur = 0 | local cur = 0 | ||
total = total + 1 | total = total + 1 | ||
for _, | for _, ww in ipairs(widths) do | ||
ww = ww * r | |||
if cur == 0 then | if cur == 0 then | ||
cur = ww | cur = ww | ||
| Line 280: | Line 188: | ||
-- max, min ceiling and floor, also as a percentage of the width | -- max, min ceiling and floor, also as a percentage of the width | ||
-- step search granularity | -- step search granularity | ||
function p.fit(text, opts) | function p.fit(text, opts) | ||
local aspect = opts.aspect | local aspect = opts.aspect | ||
| Line 288: | Line 194: | ||
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 widest = 0 | -- Measure every word once; the shrink loop below only scales the results. | ||
for _, words in ipairs( | local lines, widest = {}, 0 | ||
for | for _, words in ipairs(p.parse(text)) do | ||
local widths = {} | |||
if | for i, word in ipairs(words) do | ||
widths[i] = wordWidth(word) | |||
if widths[i] > widest then widest = widths[i] end | |||
end | end | ||
lines[#lines + 1] = widths | |||
end | end | ||
if widest == 0 then return maxPct end | if widest == 0 then return maxPct end | ||
| Line 305: | Line 211: | ||
-- Then shrink until the wrapped block fits vertically | -- Then shrink until the wrapped block fits vertically | ||
while pct > minPct and lineCount(lines, pct / 100 | while pct > minPct and lineCount(lines, pct / 100) * lineH * (pct / 100) > aspect do | ||
pct = pct - step | pct = pct - step | ||
end | end | ||