Module:TextFit: Difference between revisionsGive feedback
Jump to navigation
Jump to search
add bold option to parse/fitText |
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) |
||
| Line 5: | Line 5: | ||
character count. | character count. | ||
Widths are in em. | 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 regular [[:File:Retaildemo-regular.woff2]] unitsPerEm 1000 | Retail Demo regular [[:File:Retaildemo-regular.woff2]] unitsPerEm 1000 | ||
Retail Demo bold [[:File:Retaildemo-bold.woff2]] unitsPerEm 1000 | Retail Demo bold [[:File:Retaildemo-bold.woff2]] unitsPerEm 1000 | ||
Open Sans [[:File:Open_Sans.woff2]] unitsPerEm 2048 | 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 131: | Line 136: | ||
end | end | ||
-- | -- Parser state. `lines` is the finished output; `line`, `word` and `part` are | ||
-- | -- the containers currently being filled. A part is a run of one weight inside | ||
-- | -- a word, so a word split across tags stays a single word. | ||
-- | local function newState() | ||
-- | return { lines = {}, line = {}, word = {}, part = nil } | ||
function | end | ||
local function endWord(st) | |||
if st.part then | |||
table.insert(st.word, st.part) | |||
st.part = nil | |||
end | |||
if #st.word > 0 then | |||
table.insert(st.line, st.word) | |||
st.word = {} | |||
end | |||
end | |||
local function endLine(st) | |||
endWord(st) | |||
table.insert(st.lines, st.line) | |||
st.line = {} | |||
end | |||
local function pushChar(st, ch, bold) | |||
if st.part == nil or st.part.bold ~= bold then | |||
if st.part then table.insert(st.word, st.part) end | |||
st.part = { text = ch, bold = bold } | |||
else | |||
st.part.text = st.part.text .. ch | |||
end | |||
end | |||
-- True if any open tag on the stack set bold weight. | |||
local function stackIsBold(stack) | |||
for k = #stack, 1, -1 do | |||
if stack[k] then return true end | |||
end | |||
return false | |||
end | |||
-- Strip wiki markup that carries no visible text of its own. | |||
local function stripMarkup(src) | |||
src = mw.text.killMarkers(src or "") | src = mw.text.killMarkers(src or "") | ||
src = src:gsub("%[%[[Ff]ile:[^%[%]]*%]%]", "") | src = src:gsub("%[%[[Ff]ile:[^%[%]]*%]%]", "") | ||
| Line 142: | Line 184: | ||
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("''", "")) | |||
end | |||
-- Apply one HTML tag: push or pop the weight stack, or end the line on <br>. | |||
local function applyTag(st, stack, tag) | |||
local closing, name = tag:match("^%s*(/?)%s*(%a+)") | |||
if not name then return end | |||
name = name:lower() | |||
if name == "br" then | |||
endLine(st) | |||
elseif VOID_TAGS[name] then -- no effect on weight | |||
elseif closing == "/" then | |||
table.remove(stack) | |||
elseif not tag:match("/%s*$") then | |||
local lower = tag:lower() | |||
table.insert(stack, BOLD_TAGS[name] == true | |||
or lower:match("font%-weight%s*:%s*bold") ~= nil | |||
or lower:match("font%-weight%s*:%s*[6-9]00") ~= nil) | |||
table.insert( | |||
end | 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. | |||
-- `baseBold` measures every run as bold, for callers whose text is entirely | |||
-- bold by CSS rather than by markup. | |||
function p.parse(src, baseBold) | |||
src = stripMarkup(src) | |||
local st = newState() | |||
local stack = {} | |||
local i, n = 1, #src | local i, n = 1, #src | ||
while i <= n do | while i <= n do | ||
if src:sub(i, i) == "<" then | if src:sub(i, i) == "<" then | ||
local j = src:find(">", i, true) | local j = src:find(">", i, true) | ||
if not j then break end | if not j then break end | ||
applyTag(st, stack, src:sub(i + 1, j - 1)) | |||
i = j + 1 | i = j + 1 | ||
else | else | ||
local k = src:find("<", i, true) or (n + 1) | local k = src:find("<", i, true) or (n + 1) | ||
local chunk = mw.text.decode(src:sub(i, k - 1), true) | local chunk = mw.text.decode(src:sub(i, k - 1), true) | ||
local bold = | local bold = baseBold == true or stackIsBold(stack) | ||
for ch in ustring.gmatch(chunk, ".") do | for ch in ustring.gmatch(chunk, ".") do | ||
if ch == "\n" then | if ch == "\n" then | ||
endLine() | endLine(st) | ||
elseif ch == " " or ch == "\t" or ch == "\r" then | elseif ch == " " or ch == "\t" or ch == "\r" then | ||
endWord() | endWord(st) | ||
else | else | ||
pushChar(st, ch, bold) -- NBSP falls here: joins, never breaks | |||
end | end | ||
end | end | ||
| Line 208: | Line 242: | ||
end | end | ||
end | end | ||
endLine() | |||
return lines | endLine(st) | ||
return st.lines | |||
end | end | ||
| Line 245: | Line 280: | ||
-- 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 | ||
-- bold measure every run at bold weight, for callers whose text is | |||
-- entirely bold rather than marked up | |||
function p.fit(text, opts) | function p.fit(text, opts) | ||
local aspect = opts.aspect | local aspect = opts.aspect | ||