diff --git a/init.lua b/init.lua index 511faa3..dec0709 100644 --- a/init.lua +++ b/init.lua @@ -54,6 +54,7 @@ function loveframes.load() require(dir .. ".libraries.skins") require(dir .. ".libraries.templates") require(dir .. ".libraries.debug") + require(dir .. ".libraries.utf8") -- replace all "." with "/" in the directory setting dir = dir:gsub("\\", "/"):gsub("(%a)%.(%a)", "%1/%2") diff --git a/libraries/utf8.lua b/libraries/utf8.lua new file mode 100644 index 0000000..52a373e --- /dev/null +++ b/libraries/utf8.lua @@ -0,0 +1,398 @@ +-- $Id: utf8.lua 179 2009-04-03 18:10:03Z pasta $ +-- +-- Provides UTF-8 aware string functions implemented in pure lua: +-- * string.utf8len(s) +-- * string.utf8sub(s, i, j) +-- * string.utf8reverse(s) +-- * string.utf8char(unicode) +-- * string.utf8unicode(s, i, j) +-- * string.utf8gensub(s, sub_len) +-- +-- If utf8data.lua (containing the lower<->upper case mappings) is loaded, these +-- additional functions are available: +-- * string.utf8upper(s) +-- * string.utf8lower(s) +-- +-- All functions behave as their non UTF-8 aware counterparts with the exception +-- that UTF-8 characters are used instead of bytes for all units. + +--[[ +Copyright (c) 2006-2007, Kyle Smith +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the author nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--]] + +-- ABNF from RFC 3629 +-- +-- UTF8-octets = *( UTF8-char ) +-- UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4 +-- UTF8-1 = %x00-7F +-- UTF8-2 = %xC2-DF UTF8-tail +-- UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) / +-- %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail ) +-- UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) / +-- %xF4 %x80-8F 2( UTF8-tail ) +-- UTF8-tail = %x80-BF +-- + +local len = string.len +local sub = string.sub +local char = string.char + +-- returns the number of bytes used by the UTF-8 character at byte i in s +-- also doubles as a UTF-8 character validator +local function utf8charbytes (s, i) + -- argument defaults + i = i or 1 + + -- argument checking + if type(s) ~= "string" then + error("bad argument #1 to 'utf8charbytes' (string expected, got ".. type(s).. ")") + end + if type(i) ~= "number" then + error("bad argument #2 to 'utf8charbytes' (number expected, got ".. type(i).. ")") + end + + local c = s:byte(i) + + -- determine bytes needed for character, based on RFC 3629 + -- validate byte 1 + if c > 0 and c <= 127 then + -- UTF8-1 + return 1 + + elseif c >= 194 and c <= 223 then + -- UTF8-2 + local c2 = s:byte(i + 1) + + if not c2 then + error("UTF-8 string terminated early") + end + + -- validate byte 2 + if c2 < 128 or c2 > 191 then + error("Invalid UTF-8 character") + end + + return 2 + + elseif c >= 224 and c <= 239 then + -- UTF8-3 + local c2 = s:byte(i + 1) + local c3 = s:byte(i + 2) + + if not c2 or not c3 then + error("UTF-8 string terminated early") + end + + -- validate byte 2 + if c == 224 and (c2 < 160 or c2 > 191) then + error("Invalid UTF-8 character") + elseif c == 237 and (c2 < 128 or c2 > 159) then + error("Invalid UTF-8 character") + elseif c2 < 128 or c2 > 191 then + error("Invalid UTF-8 character") + end + + -- validate byte 3 + if c3 < 128 or c3 > 191 then + error("Invalid UTF-8 character") + end + + return 3 + + elseif c >= 240 and c <= 244 then + -- UTF8-4 + local c2 = s:byte(i + 1) + local c3 = s:byte(i + 2) + local c4 = s:byte(i + 3) + + if not c2 or not c3 or not c4 then + error("UTF-8 string terminated early") + end + + -- validate byte 2 + if c == 240 and (c2 < 144 or c2 > 191) then + error("Invalid UTF-8 character") + elseif c == 244 and (c2 < 128 or c2 > 143) then + error("Invalid UTF-8 character") + elseif c2 < 128 or c2 > 191 then + error("Invalid UTF-8 character") + end + + -- validate byte 3 + if c3 < 128 or c3 > 191 then + error("Invalid UTF-8 character") + end + + -- validate byte 4 + if c4 < 128 or c4 > 191 then + error("Invalid UTF-8 character") + end + + return 4 + + else + error("Invalid UTF-8 character") + end +end + +-- returns the number of characters in a UTF-8 string +local function utf8len (s) + -- argument checking + if type(s) ~= "string" then + for k,v in pairs(s) do print('"',tostring(k),'"',tostring(v),'"') end + error("bad argument #1 to 'utf8len' (string expected, got ".. type(s).. ")") + end + + local pos = 1 + local bytes = len(s) + local len = 0 + + while pos <= bytes do + len = len + 1 + pos = pos + utf8charbytes(s, pos) + end + + return len +end + +-- functions identically to string.sub except that i and j are UTF-8 characters +-- instead of bytes +local function utf8sub (s, i, j) + -- argument defaults + j = j or -1 + + local pos = 1 + local bytes = len(s) + local len = 0 + + -- only set l if i or j is negative + local l = (i >= 0 and j >= 0) or utf8len(s) + local startChar = (i >= 0) and i or l + i + 1 + local endChar = (j >= 0) and j or l + j + 1 + + -- can't have start before end! + if startChar > endChar then + return "" + end + + -- byte offsets to pass to string.sub + local startByte,endByte = 1,bytes + + while pos <= bytes do + len = len + 1 + + if len == startChar then + startByte = pos + end + + pos = pos + utf8charbytes(s, pos) + + if len == endChar then + endByte = pos - 1 + break + end + end + + if startChar > len then startByte = bytes+1 end + if endChar < 1 then endByte = 0 end + + return sub(s, startByte, endByte) +end + + +-- replace UTF-8 characters based on a mapping table +local function utf8replace (s, mapping) + -- argument checking + if type(s) ~= "string" then + error("bad argument #1 to 'utf8replace' (string expected, got ".. type(s).. ")") + end + if type(mapping) ~= "table" then + error("bad argument #2 to 'utf8replace' (table expected, got ".. type(mapping).. ")") + end + + local pos = 1 + local bytes = len(s) + local charbytes + local newstr = "" + + while pos <= bytes do + charbytes = utf8charbytes(s, pos) + local c = sub(s, pos, pos + charbytes - 1) + + newstr = newstr .. (mapping[c] or c) + + pos = pos + charbytes + end + + return newstr +end + + +-- identical to string.upper except it knows about unicode simple case conversions +local function utf8upper (s) + return utf8replace(s, utf8_lc_uc) +end + +-- identical to string.lower except it knows about unicode simple case conversions +local function utf8lower (s) + return utf8replace(s, utf8_uc_lc) +end + +-- identical to string.reverse except that it supports UTF-8 +local function utf8reverse (s) + -- argument checking + if type(s) ~= "string" then + error("bad argument #1 to 'utf8reverse' (string expected, got ".. type(s).. ")") + end + + local bytes = len(s) + local pos = bytes + local charbytes + local newstr = "" + + while pos > 0 do + c = s:byte(pos) + while c >= 128 and c <= 191 do + pos = pos - 1 + c = s:byte(pos) + end + + charbytes = utf8charbytes(s, pos) + + newstr = newstr .. sub(s, pos, pos + charbytes - 1) + + pos = pos - 1 + end + + return newstr +end + +-- http://en.wikipedia.org/wiki/Utf8 +-- http://developer.coronalabs.com/code/utf-8-conversion-utility +local function utf8char(unicode) + if unicode <= 0x7F then return char(unicode) end + + if (unicode <= 0x7FF) then + local Byte0 = 0xC0 + math.floor(unicode / 0x40); + local Byte1 = 0x80 + (unicode % 0x40); + return char(Byte0, Byte1); + end; + + if (unicode <= 0xFFFF) then + local Byte0 = 0xE0 + math.floor(unicode / 0x1000); + local Byte1 = 0x80 + (math.floor(unicode / 0x40) % 0x40); + local Byte2 = 0x80 + (unicode % 0x40); + return char(Byte0, Byte1, Byte2); + end; + + if (unicode <= 0x10FFFF) then + local code = unicode + local Byte3= 0x80 + (code % 0x40); + code = math.floor(code / 0x40) + local Byte2= 0x80 + (code % 0x40); + code = math.floor(code / 0x40) + local Byte1= 0x80 + (code % 0x40); + code = math.floor(code / 0x40) + local Byte0= 0xF0 + code; + + return char(Byte0, Byte1, Byte2, Byte3); + end; + + error 'Unicode cannot be greater than U+10FFFF!' +end + +local shift_6 = 2^6 +local shift_12 = 2^12 +local shift_18 = 2^18 + +local utf8unicode +utf8unicode = function(str, i, j, byte_pos) + i = i or 1 + j = j or i + + if i > j then return end + + local char,bytes + + if byte_pos then + bytes = utf8charbytes(str,byte_pos) + char = sub(str,byte_pos,byte_pos-1+bytes) + else + char,byte_pos = utf8sub(str,i,i) + bytes = #char + end + + local unicode + + if bytes == 1 then unicode = string.byte(char) end + if bytes == 2 then + local byte0,byte1 = string.byte(char,1,2) + local code0,code1 = byte0-0xC0,byte1-0x80 + unicode = code0*shift_6 + code1 + end + if bytes == 3 then + local byte0,byte1,byte2 = string.byte(char,1,3) + local code0,code1,code2 = byte0-0xE0,byte1-0x80,byte2-0x80 + unicode = code0*shift_12 + code1*shift_6 + code2 + end + if bytes == 4 then + local byte0,byte1,byte2,byte3 = string.byte(char,1,4) + local code0,code1,code2,code3 = byte0-0xF0,byte1-0x80,byte2-0x80,byte3-0x80 + unicode = code0*shift_18 + code1*shift_12 + code2*shift_6 + code3 + end + + return unicode,utf8unicode(str, i+1, j, byte_pos+bytes) +end + +-- Returns an iterator which returns the next substring and its byte interval +local function utf8gensub(str, sub_len) + sub_len = sub_len or 1 + local byte_pos = 1 + local len = #str + return function() + local char_count = 0 + local start = byte_pos + repeat + if byte_pos > len then return end + char_count = char_count + 1 + local bytes = utf8charbytes(str,byte_pos) + byte_pos = byte_pos+bytes + + until char_count == sub_len + + local last = byte_pos-1 + local sub = sub(str,start,last) + return sub, start, last + end +end + +string.len = utf8len +string.sub = utf8sub +string.reverse = utf8reverse +string.char = utf8char +string.unicode = utf8unicode +string.gensub = utf8gensub diff --git a/objects/text.lua b/objects/text.lua index 6a4e074..54e0e0e 100644 --- a/objects/text.lua +++ b/objects/text.lua @@ -316,10 +316,10 @@ function newobject:SetText(t) local key = k if width > maxw then table.remove(self.formattedtext, k) - for n=1, #data do + for n=1, string.len(data) do local item = data:sub(n, n) local itemw = v.font:getWidth(item) - if n ~= #data then + if n ~= string.len(data) then if (curw + itemw) > maxw then table.insert(inserts, { key = key, @@ -389,7 +389,7 @@ function newobject:SetText(t) local text = v.text local color = v.color if detectlinks then - if #text > 7 and (text:sub(1, 7) == "http://" or text:sub(1, 8) == "https://") then + if string.len(text) > 7 and (text:sub(1, 7) == "http://" or text:sub(1, 8) == "https://") then v.link = true end end diff --git a/objects/textinput.lua b/objects/textinput.lua index d94ea9b..532c503 100644 --- a/objects/textinput.lua +++ b/objects/textinput.lua @@ -337,6 +337,7 @@ function newobject:mousepressed(x, y, button) local hover = self.hover local internals = self.internals + local alt = love.keyboard.isDown("lalt", "ralt") local vbar = self.vbar local hbar = self.hbar local scrollamount = self.mousewheelscrollamount @@ -377,26 +378,36 @@ function newobject:mousepressed(x, y, button) baseparent:MakeTop() end elseif button == "wu" then - if vbar and not hbar then - local vbar = self:GetVerticalScrollBody().internals[1].internals[1] - vbar:Scroll(-scrollamount) - elseif vbar and hbar then - local vbar = self:GetVerticalScrollBody().internals[1].internals[1] - vbar:Scroll(-scrollamount) - elseif not vbar and hbar then - local hbar = self:GetHorizontalScrollBody().internals[1].internals[1] - hbar:Scroll(-scrollamount) + if not alt then + if focus then + self.line = math.max(self.line - scrollamount, 1) + elseif vbar then + local vbar = self:GetVerticalScrollBody().internals[1].internals[1] + vbar:Scroll(-scrollamount) + end + else + if focus then + self:MoveIndicator(-scrollamount) + elseif hbar then + local hbar = self:GetHorizontalScrollBody().internals[1].internals[1] + hbar:Scroll(-scrollamount) + end end elseif button == "wd" then - if vbar and not hbar then - local vbar = self:GetVerticalScrollBody().internals[1].internals[1] - vbar:Scroll(scrollamount) - elseif vbar and hbar then - local vbar = self:GetVerticalScrollBody().internals[1].internals[1] - vbar:Scroll(scrollamount) - elseif not vbar and hbar then - local hbar = self:GetHorizontalScrollBody().internals[1].internals[1] - hbar:Scroll(scrollamount) + if not alt then + if focus then + self.line = math.min(self.line + scrollamount, #self.lines) + elseif vbar then + local vbar = self:GetVerticalScrollBody().internals[1].internals[1] + vbar:Scroll(scrollamount) + end + else + if focus then + self:MoveIndicator(scrollamount) + elseif hbar then + local hbar = self:GetHorizontalScrollBody().internals[1].internals[1] + hbar:Scroll(scrollamount) + end end end else @@ -591,7 +602,7 @@ function newobject:RunKey(key, istext) if indicatornum == 0 then if line > 1 then self.line = line - 1 - local numchars = #lines[self.line] + local numchars = string.len(lines[self.line]) self:MoveIndicator(numchars) end else @@ -609,14 +620,14 @@ function newobject:RunKey(key, istext) if not multiline then self:MoveIndicator(1) local indicatorx = self.indicatorx - if indicatorx >= (x + swidth) and indicatornum ~= #text then + if indicatorx >= (x + swidth) and indicatornum ~= string.len(text) then local width = font:getWidth(text:sub(indicatornum, indicatornum)) self.offsetx = offsetx + width - elseif indicatornum == #text and offsetx ~= ((font:getWidth(text)) - swidth + 10) and font:getWidth(text) + textoffsetx > swidth then + elseif indicatornum == string.len(text) and offsetx ~= ((font:getWidth(text)) - swidth + 10) and font:getWidth(text) + textoffsetx > swidth then self.offsetx = ((font:getWidth(text)) - swidth + 10) end else - if indicatornum == #text then + if indicatornum == string.len(text) then if line < numlines then self.line = line + 1 self:MoveIndicator(0, true) @@ -627,7 +638,7 @@ function newobject:RunKey(key, istext) end if alltextselected then self.line = #lines - self.indicatornum = lines[#lines]:len() + self.indicatornum = string.len(lines[#lines]) self.alltextselected = false end return @@ -635,8 +646,8 @@ function newobject:RunKey(key, istext) if multiline then if line > 1 then self.line = line - 1 - if indicatornum > #lines[self.line] then - self.indicatornum = #lines[self.line] + if indicatornum > string.len(lines[self.line]) then + self.indicatornum = string.len(lines[self.line]) end end end @@ -645,8 +656,8 @@ function newobject:RunKey(key, istext) if multiline then if line < #lines then self.line = line + 1 - if indicatornum > #lines[self.line] then - self.indicatornum = #lines[self.line] + if indicatornum > string.len(lines[self.line]) then + self.indicatornum = string.len(lines[self.line]) end end end @@ -676,12 +687,12 @@ function newobject:RunKey(key, istext) local oldtext = lines[line] table.remove(lines, line) self.line = line - 1 - if #oldtext > 0 then - newindicatornum = #lines[self.line] + if string.len(oldtext) > 0 then + newindicatornum = string.len(lines[self.line]) lines[self.line] = lines[self.line] .. oldtext self:MoveIndicator(newindicatornum) else - self:MoveIndicator(#lines[self.line]) + self:MoveIndicator(string.len(lines[self.line])) end end end @@ -689,9 +700,9 @@ function newobject:RunKey(key, istext) local cwidth = 0 if masked then local maskchar = self.maskchar - cwidth = font:getWidth(text:sub(#text):gsub(".", maskchar)) + cwidth = font:getWidth(text:sub(string.len(text)):gsub(".", maskchar)) else - cwidth = font:getWidth(text:sub(#text)) + cwidth = font:getWidth(text:sub(string.len(text))) end if self.offsetx > 0 then self.offsetx = self.offsetx - cwidth @@ -709,13 +720,13 @@ function newobject:RunKey(key, istext) self.alltextselected = false indicatornum = self.indicatornum else - if text ~= "" and indicatornum < #text then + if text ~= "" and indicatornum < string.len(text) then text = self:RemoveFromText(indicatornum + 1) lines[line] = text - elseif indicatornum == #text and line < #lines then + elseif indicatornum == string.len(text) and line < #lines then local oldtext = lines[line + 1] - if #oldtext > 0 then - newindicatornum = #lines[self.line] + if string.len(oldtext) > 0 then + newindicatornum = string.len(lines[self.line]) lines[self.line] = lines[self.line] .. oldtext end table.remove(lines, line + 1) @@ -739,8 +750,8 @@ function newobject:RunKey(key, istext) if indicatornum == 0 then newtext = self.lines[line] self.lines[line] = "" - elseif indicatornum > 0 and indicatornum < #self.lines[line] then - newtext = self.lines[line]:sub(indicatornum + 1, #self.lines[line]) + elseif indicatornum > 0 and indicatornum < string.len(self.lines[line]) then + newtext = self.lines[line]:sub(indicatornum + 1, string.len(self.lines[line])) self.lines[line] = self.lines[line]:sub(1, indicatornum) end if line ~= #lines then @@ -762,14 +773,14 @@ function newobject:RunKey(key, istext) end ckey = key self.lines[self.line] = self:AddIntoText(self.tabreplacement, self.indicatornum) - self:MoveIndicator(#self.tabreplacement) + self:MoveIndicator(string.len(self.tabreplacement)) end else if not editable then return end -- do not continue if the text limit has been reached or exceeded - if #text >= self.limit and self.limit ~= 0 and not alltextselected then + if string.len(text) >= self.limit and self.limit ~= 0 and not alltextselected then return end -- check for unusable characters @@ -804,11 +815,11 @@ function newobject:RunKey(key, istext) lines = self.lines line = self.line end - if indicatornum ~= 0 and indicatornum ~= #text then + if indicatornum ~= 0 and indicatornum ~= string.len(text) then text = self:AddIntoText(key, indicatornum) lines[line] = text self:MoveIndicator(1) - elseif indicatornum == #text then + elseif indicatornum == string.len(text) then text = text .. key lines[line] = text self:MoveIndicator(1) @@ -867,8 +878,8 @@ function newobject:MoveIndicator(num, exact) self.indicatornum = num end - if self.indicatornum > #text then - self.indicatornum = #text + if self.indicatornum > string.len(text) then + self.indicatornum = string.len(text) elseif self.indicatornum < 0 then self.indicatornum = 0 end @@ -1074,7 +1085,7 @@ function newobject:GetTextCollisions(x, y) end local line = self.line local curline = lines[line] - for i=1, #curline do + for i=1, string.len(curline) do local char = text:sub(i, i) local width = 0 if masked then @@ -1094,7 +1105,7 @@ function newobject:GetTextCollisions(x, y) self:MoveIndicator(i - 1, true) break else - self.indicatornum = #curline + self.indicatornum = string.len(curline) end if x < tx then @@ -1102,16 +1113,16 @@ function newobject:GetTextCollisions(x, y) end if x > (tx + width) then - self:MoveIndicator(#curline, true) + self:MoveIndicator(string.len(curline), true) end end - if #curline == 0 then + if string.len(curline) == 0 then self.indicatornum = 0 end end else - for i=1, #text do + for i=1, string.len(text) do local char = text:sub(i, i) local width = 0 if masked then @@ -1133,7 +1144,7 @@ function newobject:GetTextCollisions(x, y) self:MoveIndicator(0, true) end if x > (tx + width) then - self:MoveIndicator(#text, true) + self:MoveIndicator(string.len(text), true) end end end @@ -1363,13 +1374,13 @@ function newobject:SetText(text) self.lines = {""} end self.line = #self.lines - self.indicatornum = #self.lines[#self.lines] + self.indicatornum = string.len(self.lines[#self.lines]) else text = text:gsub(string.char(92) .. string.char(110), "") text = text:gsub(string.char(10), "") self.lines = {text} self.line = 1 - self.indicatornum = #text + self.indicatornum = string.len(text) end return self