-- Line wrapping local my_utf8 = require 'my_utf8' local wrap = {} local I = {} wrap.internal = I -- include width for each codepoint function I.widths(s, startpos, font) local next_char = my_utf8.chars(s, startpos) return function() local p, cp, char = next_char() if not p then return nil end return p, cp, char, font:getWidth(char) end end local WHITESPACE = { [0x0009] = true, [0x000A] = true, [0x000B] = true, [0x000C] = true, [0x000D] = true, [0x0020] = true, [0x0085] = true, [0x00A0] = true, [0x1680] = true, [0x2000] = true, [0x2001] = true, [0x2002] = true, [0x2003] = true, [0x2004] = true, [0x2005] = true, [0x2006] = true, [0x2007] = true, [0x2008] = true, [0x2009] = true, [0x200A] = true, [0x2028] = true, [0x2029] = true, [0x202F] = true, [0x205F] = true, [0x3000] = true, } local WORD_START = 1 local WORD_END = 2 -- tag codepoints at WORD_START (after space) and WORD_END (before space) function I.words(s, startpos, font) local next_char = I.widths(s, startpos, font) local was_space = true return function() local p, cp, char, w = next_char() if not p then return nil end local space = WHITESPACE[cp] local state = nil if not space and was_space then state = WORD_START elseif space and not was_space then state = WORD_END end was_space = space return p, cp, char, w, state end end -- tag each WORD_START codepoint with width of word (WORD_START to WORD_END) function I.wordwidth(s, startpos, font) local next_char = I.words(s, startpos, font) return coroutine.wrap(function() local pos, cp, char, w, state while true do pos, cp, char, w, state = next_char() if not pos then return end if state ~= WORD_START then coroutine.yield(pos, cp, char, w, state, nil) else break end end while pos do assert(state == WORD_START) -- buffer to next WORD_START local buf = {{pos, cp, char, w, state}} local word_w = w while true do pos, cp, char, w, state = next_char() if not pos then break end table.insert(buf, {pos, cp, char, w, state}) if state == WORD_END then break end word_w = word_w + w end -- emit buffer table.insert(buf[1], word_w) -- just to the first char of word for _,e in ipairs(buf) do coroutine.yield(unpack(e)) end -- emit spaces to next word if not pos then break end assert(state == WORD_END) while true do pos, cp, char, w, state = next_char() if not pos then break end if state == WORD_START then break end coroutine.yield(pos, cp, char, w, state) end end end) end -- tag each WORD_START codepoint with total width of word+spaces (WORD_START to WORD_START) function I.totalwordwidth(s, startpos, font) local next_char = I.wordwidth(s, startpos, font) return coroutine.wrap(function() local pos, cp, char, w, state, word_w while true do pos, cp, char, w, state, word_w = next_char() if not pos then return end if state ~= WORD_START then coroutine.yield(pos, cp, char, w, state, word_w, nil) else break end end while pos do assert(state == WORD_START) -- buffer to next WORD_START local buf = {{pos, cp, char, w, state, word_w}} local total_w = w while true do pos, cp, char, w, state, word_w = next_char() if state == WORD_START or not pos then break end table.insert(buf, {pos, cp, char, w, state, word_w}) total_w = total_w + w end -- emit buffer table.insert(buf[1], total_w) -- just to the first char of word for _,e in ipairs(buf) do coroutine.yield(unpack(e)) end end end) end function wrap.wrap(s, startpos, min_width, max_width, font) local next_char = I.totalwordwidth(s, startpos, font) local x = 0 local limit = max_width return function() while true do local p, cp, char, w, state, word_w, total_w = next_char() local is_wrap if not p then return nil end if state == WORD_START then limit = max_width if x + total_w > max_width and x >= min_width then -- Wrap at word boundary x = 0 is_wrap = {word_wrap=true} elseif x + word_w >= min_width and x + word_w < max_width and x + total_w > max_width then -- We can't word wrap just yet, and the word also wouldn't fit -- without pushing trailing spaces to the next line. -- Plan to truncate word at min_width so the next line starts with -- non-spaces. limit = min_width end end if x > 0 and x + w > limit then -- overflow x = 0 is_wrap = {} assert(char ~= ' ') limit = max_width end local char_x = x x = x + w return p, char, w, char_x, is_wrap end end end -- like wrap, but can indent the next line after wrapping function wrap.indented_wrap(s, startpos, min_width, max_width, font, wrap_indent, debug) local next_char = I.totalwordwidth(s, startpos, font) local x = 0 -- first line starts at 0 local limit = max_width return function() while true do local p, cp, char, w, state, word_w, total_w = next_char() local is_wrap if not p then return nil end if state == WORD_START then limit = max_width if x + total_w > max_width and x >= min_width then -- Wrap at word boundary if debug then print_to_output(('word wrap instead of printing to %d because next word including trailing whitespace would take us to %d which is greater than %d. Line is longer than %d.'):format(x, x+total_w, max_width, min_width)) end is_wrap = {word_wrap=true, unwrapped_x=x} x = wrap_indent elseif x + word_w >= min_width and x + word_w < max_width and x + total_w > max_width then -- We can't word wrap just yet, and the word also wouldn't fit -- without pushing trailing spaces to the next line. -- Plan to truncate word at min_width so the next line starts with -- non-spaces. if debug then print_to_output(('Line length %d < min width %d and next word including trailing whitespace would take us to %d > max width %d. But just the next word would take us to %d < %d. So word wrap would put whitespace at the start of the next wrapped line. Instead truncate at %d.'):format(x, min_width, x + total_w, max_width, x + word_w, max_width, min_width)) end limit = min_width end end if x > 0 and x + w > limit then -- overflow if debug then print_to_output(('wrap at %d because next character would put us at %d > limit %d'):format(x, x+w, limit)) end is_wrap = {unwrapped_x=x} x = wrap_indent assert(char ~= ' ') limit = max_width end local char_x = x x = x + w return p, char, w, char_x, is_wrap end end end return wrap