Editing Module:TextFit

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.
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.


Everything is measured at bold weight. Retail Demo's bold advances run about 4%
Widths are in em. Sources, all extracted with fonttools:
wider than its regular ones, so measuring regular text against the bold table
   Retail Demo regular [[:File:Retaildemo-regular.woff2]]  unitsPerEm 1000
can only make the estimate slightly conservative -- text comes out a hair small
   Retail Demo bold    [[:File:Retaildemo-bold.woff2]]     unitsPerEm 1000
rather than overflowing -- and it removes the need to track font weight through
  Open Sans            [[:File:Open_Sans.woff2]]           unitsPerEm 2048
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.
]==]
]==]


Line 24: Line 14:
local ustring = mw.ustring
local ustring = mw.ustring


-- Retail Demo, bold weight. Invisible characters are spelled with
-- Retail Demo, normal weight
-- ustring.char so they cannot be mistaken for a plain space when edited.
local RD_REGULAR = {
local WIDTHS = {
[" "]=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,
[ustring.char(0xA0)]=0.240, -- no-break space
[" "]=0.240, -- U+00A0 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 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, [ustring.char(0xAD)]=0.322, -- soft hyphen
["«"]=0.496, ["¬"]=0.572, ["­"]=0.322, ["®"]=0.832, ["¯"]=0.500, ["°"]=0.428, ["±"]=0.572, ["²"]=0.348,
["®"]=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 = WIDTHS[ch]
                    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(word, ".") do
for _, part in ipairs(word) do
total = total + charWidth(ch)
for ch in ustring.gmatch(part.text, ".") do
total = total + charWidth(ch, part.bold)
end
end
end
return total
return total
end
end


-- Strip wiki markup that carries no visible text of its own.
-- Reduce expanded wikitext/HTML to the text a reader actually sees, keeping
local function stripMarkup(src)
-- 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.
return (src:gsub("'''''", ""):gsub("'''", ""):gsub("''", ""))
src = src:gsub("'''''", ""):gsub("'''", ""):gsub("''", "")
end


-- Reduce expanded wikitext/HTML to the text a reader actually sees: a list of
local lines, line, word, part = {}, {}, {}, nil
-- lines, each a list of words.
local stack = {}
-- 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 &lt; cannot look like one.
src = mw.text.decode(src, true)


local lines = {}
local function boldNow()
for line in (src .. "\n"):gmatch("([^\n]*)\n") do
if baseBold then return true end
local words = {}
for k = #stack, 1, -1 do
for word in line:gmatch("%S+") do
if stack[k] then return true end
words[#words + 1] = word
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
lines[#lines + 1] = words
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. Takes lines of word WIDTHS, already
-- expressed as a fraction of that width. Returns the number of rendered lines.
-- measured by fit(). 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 _, widths in ipairs(lines) do
for _, words in ipairs(lines) do
local cur = 0
local cur = 0
total = total + 1
total = total + 1
for _, ww in ipairs(widths) do
for _, word in ipairs(words) do
ww = ww * r
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)


-- Measure every word once; the shrink loop below only scales the results.
local widest = 0
local lines, widest = {}, 0
for _, words in ipairs(lines) do
for _, words in ipairs(p.parse(text)) do
for _, word in ipairs(words) do
local widths = {}
local ww = wordWidth(word)
for i, word in ipairs(words) do
if ww > widest then widest = ww end
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 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
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: