1# -*- coding: utf-8 -*- 2# Copyright: Ankitects Pty Ltd and contributors 3# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 5import re 6import os 7import random 8import time 9import math 10from html.entities import name2codepoint 11import subprocess 12import tempfile 13import shutil 14import string 15import sys 16import locale 17from hashlib import sha1 18import platform 19import traceback 20from contextlib import contextmanager 21from anki.lang import _, ngettext 22 23# some add-ons expect json to be in the utils module 24import json # pylint: disable=unused-import 25 26# Time handling 27############################################################################## 28 29def intTime(scale=1): 30 "The time in integer seconds. Pass scale=1000 to get milliseconds." 31 return int(time.time()*scale) 32 33timeTable = { 34 "years": lambda n: ngettext("%s year", "%s years", n), 35 "months": lambda n: ngettext("%s month", "%s months", n), 36 "days": lambda n: ngettext("%s day", "%s days", n), 37 "hours": lambda n: ngettext("%s hour", "%s hours", n), 38 "minutes": lambda n: ngettext("%s minute", "%s minutes", n), 39 "seconds": lambda n: ngettext("%s second", "%s seconds", n), 40 } 41 42inTimeTable = { 43 "years": lambda n: ngettext("in %s year", "in %s years", n), 44 "months": lambda n: ngettext("in %s month", "in %s months", n), 45 "days": lambda n: ngettext("in %s day", "in %s days", n), 46 "hours": lambda n: ngettext("in %s hour", "in %s hours", n), 47 "minutes": lambda n: ngettext("in %s minute", "in %s minutes", n), 48 "seconds": lambda n: ngettext("in %s second", "in %s seconds", n), 49 } 50 51def shortTimeFmt(type): 52 return { 53#T: year is an abbreviation for year. %s is a number of years 54 "years": _("%sy"), 55#T: m is an abbreviation for month. %s is a number of months 56 "months": _("%smo"), 57#T: d is an abbreviation for day. %s is a number of days 58 "days": _("%sd"), 59#T: h is an abbreviation for hour. %s is a number of hours 60 "hours": _("%sh"), 61#T: m is an abbreviation for minute. %s is a number of minutes 62 "minutes": _("%sm"), 63#T: s is an abbreviation for second. %s is a number of seconds 64 "seconds": _("%ss"), 65 }[type] 66 67def fmtTimeSpan(time, pad=0, point=0, short=False, inTime=False, unit=99): 68 "Return a string representing a time span (eg '2 days')." 69 (type, point) = optimalPeriod(time, point, unit) 70 time = convertSecondsTo(time, type) 71 if not point: 72 time = int(round(time)) 73 if short: 74 fmt = shortTimeFmt(type) 75 else: 76 if inTime: 77 fmt = inTimeTable[type](_pluralCount(time, point)) 78 else: 79 fmt = timeTable[type](_pluralCount(time, point)) 80 timestr = "%%%(a)d.%(b)df" % {'a': pad, 'b': point} 81 return locale.format_string(fmt % timestr, time) 82 83def optimalPeriod(time, point, unit): 84 if abs(time) < 60 or unit < 1: 85 type = "seconds" 86 point -= 1 87 elif abs(time) < 3600 or unit < 2: 88 type = "minutes" 89 elif abs(time) < 60 * 60 * 24 or unit < 3: 90 type = "hours" 91 elif abs(time) < 60 * 60 * 24 * 30 or unit < 4: 92 type = "days" 93 elif abs(time) < 60 * 60 * 24 * 365 or unit < 5: 94 type = "months" 95 point += 1 96 else: 97 type = "years" 98 point += 1 99 return (type, max(point, 0)) 100 101def convertSecondsTo(seconds, type): 102 if type == "seconds": 103 return seconds 104 elif type == "minutes": 105 return seconds / 60 106 elif type == "hours": 107 return seconds / 3600 108 elif type == "days": 109 return seconds / 86400 110 elif type == "months": 111 return seconds / 2592000 112 elif type == "years": 113 return seconds / 31536000 114 assert False 115 116def _pluralCount(time, point): 117 if point: 118 return 2 119 return math.floor(time) 120 121# Locale 122############################################################################## 123 124def fmtPercentage(float_value, point=1): 125 "Return float with percentage sign" 126 fmt = '%' + "0.%(b)df" % {'b': point} 127 return locale.format_string(fmt, float_value) + "%" 128 129def fmtFloat(float_value, point=1): 130 "Return a string with decimal separator according to current locale" 131 fmt = '%' + "0.%(b)df" % {'b': point} 132 return locale.format_string(fmt, float_value) 133 134# HTML 135############################################################################## 136reComment = re.compile("(?s)<!--.*?-->") 137reStyle = re.compile("(?si)<style.*?>.*?</style>") 138reScript = re.compile("(?si)<script.*?>.*?</script>") 139reTag = re.compile("(?s)<.*?>") 140reEnts = re.compile(r"&#?\w+;") 141reMedia = re.compile("(?i)<img[^>]+src=[\"']?([^\"'>]+)[\"']?[^>]*>") 142 143def stripHTML(s): 144 s = reComment.sub("", s) 145 s = reStyle.sub("", s) 146 s = reScript.sub("", s) 147 s = reTag.sub("", s) 148 s = entsToTxt(s) 149 return s 150 151def stripHTMLMedia(s): 152 "Strip HTML but keep media filenames" 153 s = reMedia.sub(" \\1 ", s) 154 return stripHTML(s) 155 156def minimizeHTML(s): 157 "Correct Qt's verbose bold/underline/etc." 158 s = re.sub('<span style="font-weight:600;">(.*?)</span>', '<b>\\1</b>', 159 s) 160 s = re.sub('<span style="font-style:italic;">(.*?)</span>', '<i>\\1</i>', 161 s) 162 s = re.sub('<span style="text-decoration: underline;">(.*?)</span>', 163 '<u>\\1</u>', s) 164 return s 165 166def htmlToTextLine(s): 167 s = s.replace("<br>", " ") 168 s = s.replace("<br />", " ") 169 s = s.replace("<div>", " ") 170 s = s.replace("\n", " ") 171 s = re.sub(r"\[sound:[^]]+\]", "", s) 172 s = re.sub(r"\[\[type:[^]]+\]\]", "", s) 173 s = stripHTMLMedia(s) 174 s = s.strip() 175 return s 176 177def entsToTxt(html): 178 # entitydefs defines nbsp as \xa0 instead of a standard space, so we 179 # replace it first 180 html = html.replace(" ", " ") 181 def fixup(m): 182 text = m.group(0) 183 if text[:2] == "&#": 184 # character reference 185 try: 186 if text[:3] == "&#x": 187 return chr(int(text[3:-1], 16)) 188 else: 189 return chr(int(text[2:-1])) 190 except ValueError: 191 pass 192 else: 193 # named entity 194 try: 195 text = chr(name2codepoint[text[1:-1]]) 196 except KeyError: 197 pass 198 return text # leave as is 199 return reEnts.sub(fixup, html) 200 201def bodyClass(col, card): 202 bodyclass = "card card%d" % (card.ord+1) 203 if col.conf.get("nightMode"): 204 bodyclass += " nightMode" 205 return bodyclass 206 207# IDs 208############################################################################## 209 210def hexifyID(id): 211 return "%x" % int(id) 212 213def dehexifyID(id): 214 return int(id, 16) 215 216def ids2str(ids): 217 """Given a list of integers, return a string '(int1,int2,...)'.""" 218 return "(%s)" % ",".join(str(i) for i in ids) 219 220def timestampID(db, table): 221 "Return a non-conflicting timestamp for table." 222 # be careful not to create multiple objects without flushing them, or they 223 # may share an ID. 224 t = intTime(1000) 225 while db.scalar("select id from %s where id = ?" % table, t): 226 t += 1 227 return t 228 229def maxID(db): 230 "Return the first safe ID to use." 231 now = intTime(1000) 232 for tbl in "cards", "notes": 233 now = max(now, db.scalar("select max(id) from %s" % tbl) or 0) 234 return now + 1 235 236# used in ankiweb 237def base62(num, extra=""): 238 s = string; table = s.ascii_letters + s.digits + extra 239 buf = "" 240 while num: 241 num, i = divmod(num, len(table)) 242 buf = table[i] + buf 243 return buf 244 245_base91_extra_chars = "!#$%&()*+,-./:;<=>?@[]^_`{|}~" 246def base91(num): 247 # all printable characters minus quotes, backslash and separators 248 return base62(num, _base91_extra_chars) 249 250def guid64(): 251 "Return a base91-encoded 64bit random number." 252 return base91(random.randint(0, 2**64-1)) 253 254# increment a guid by one, for note type conflicts 255def incGuid(guid): 256 return _incGuid(guid[::-1])[::-1] 257 258def _incGuid(guid): 259 s = string; table = s.ascii_letters + s.digits + _base91_extra_chars 260 idx = table.index(guid[0]) 261 if idx + 1 == len(table): 262 # overflow 263 guid = table[0] + _incGuid(guid[1:]) 264 else: 265 guid = table[idx+1] + guid[1:] 266 return guid 267 268# Fields 269############################################################################## 270 271def joinFields(list): 272 return "\x1f".join(list) 273 274def splitFields(string): 275 return string.split("\x1f") 276 277# Checksums 278############################################################################## 279 280def checksum(data): 281 if isinstance(data, str): 282 data = data.encode("utf-8") 283 return sha1(data).hexdigest() 284 285def fieldChecksum(data): 286 # 32 bit unsigned number from first 8 digits of sha1 hash 287 return int(checksum(stripHTMLMedia(data).encode("utf-8"))[:8], 16) 288 289# Temp files 290############################################################################## 291 292_tmpdir = None 293 294def tmpdir(): 295 "A reusable temp folder which we clean out on each program invocation." 296 global _tmpdir 297 if not _tmpdir: 298 def cleanup(): 299 shutil.rmtree(_tmpdir) 300 import atexit 301 atexit.register(cleanup) 302 _tmpdir = os.path.join(tempfile.gettempdir(), "anki_temp") 303 if not os.path.exists(_tmpdir): 304 os.mkdir(_tmpdir) 305 return _tmpdir 306 307def tmpfile(prefix="", suffix=""): 308 (fd, name) = tempfile.mkstemp(dir=tmpdir(), prefix=prefix, suffix=suffix) 309 os.close(fd) 310 return name 311 312def namedtmp(name, rm=True): 313 "Return tmpdir+name. Deletes any existing file." 314 path = os.path.join(tmpdir(), name) 315 if rm: 316 try: 317 os.unlink(path) 318 except (OSError, IOError): 319 pass 320 return path 321 322# Cmd invocation 323############################################################################## 324 325@contextmanager 326def noBundledLibs(): 327 oldlpath = os.environ.pop("LD_LIBRARY_PATH", None) 328 yield 329 if oldlpath is not None: 330 os.environ["LD_LIBRARY_PATH"] = oldlpath 331 332def call(argv, wait=True, **kwargs): 333 "Execute a command. If WAIT, return exit code." 334 # ensure we don't open a separate window for forking process on windows 335 if isWin: 336 si = subprocess.STARTUPINFO() 337 try: 338 si.dwFlags |= subprocess.STARTF_USESHOWWINDOW 339 except: 340 # pylint: disable=no-member 341 si.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW 342 else: 343 si = None 344 # run 345 try: 346 with noBundledLibs(): 347 o = subprocess.Popen(argv, startupinfo=si, **kwargs) 348 except OSError: 349 # command not found 350 return -1 351 # wait for command to finish 352 if wait: 353 while 1: 354 try: 355 ret = o.wait() 356 except OSError: 357 # interrupted system call 358 continue 359 break 360 else: 361 ret = 0 362 return ret 363 364# OS helpers 365############################################################################## 366 367isMac = sys.platform.startswith("darwin") 368isWin = sys.platform.startswith("win32") 369isLin = not isMac and not isWin 370devMode = os.getenv("ANKIDEV", "") 371 372invalidFilenameChars = ":*?\"<>|" 373 374def invalidFilename(str, dirsep=True): 375 for c in invalidFilenameChars: 376 if c in str: 377 return c 378 if (dirsep or isWin) and "/" in str: 379 return "/" 380 elif (dirsep or not isWin) and "\\" in str: 381 return "\\" 382 elif str.strip().startswith("."): 383 return "." 384 385def platDesc(): 386 # we may get an interrupted system call, so try this in a loop 387 n = 0 388 theos = "unknown" 389 while n < 100: 390 n += 1 391 try: 392 system = platform.system() 393 if isMac: 394 theos = "mac:%s" % (platform.mac_ver()[0]) 395 elif isWin: 396 theos = "win:%s" % (platform.win32_ver()[0]) 397 elif system == "Linux": 398 import distro 399 r = distro.linux_distribution(full_distribution_name=False) 400 theos = "lin:%s:%s" % (r[0], r[1]) 401 else: 402 theos = system 403 break 404 except: 405 continue 406 return theos 407 408# Debugging 409############################################################################## 410 411class TimedLog: 412 def __init__(self): 413 self._last = time.time() 414 def log(self, s): 415 path, num, fn, y = traceback.extract_stack(limit=2)[0] 416 sys.stderr.write("%5dms: %s(): %s\n" % ((time.time() - self._last)*1000, fn, s)) 417 self._last = time.time() 418 419# Version 420############################################################################## 421 422def versionWithBuild(): 423 from anki import version 424 try: 425 from anki.buildhash import build 426 except: 427 build = "dev" 428 return "%s (%s)" % (version, build) 429