1# vim:fileencoding=utf-8 2# License: GPL v3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net> 3from __python__ import hash_literals 4 5from ajax import encode_query 6from encodings import hexlify 7from book_list.theme import get_font_family 8 9 10is_ios = v'!!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform)' 11if !is_ios and v'!!navigator.platform' and window? and window.navigator.platform is 'MacIntel' and window.navigator.maxTouchPoints > 1: 12 # iPad Safari in desktop mode https://stackoverflow.com/questions/57765958/how-to-detect-ipad-and-ipad-os-version-in-ios-13-and-up 13 is_ios = True 14 15 16def default_context_menu_should_be_allowed(evt): 17 if evt.target and evt.target.tagName and evt.target.tagName.toLowerCase() in ('input', 'textarea'): 18 return True 19 return False 20 21 22def debounce(func, wait, immediate=False): 23 # Returns a function, that, as long as it continues to be invoked, will not 24 # be triggered. The function will be called after it stops being called for 25 # wait milliseconds. If `immediate` is True, trigger the function on the 26 # leading edge, instead of the trailing. 27 timeout = None 28 return def debounce_inner(): # noqa: unused-local 29 nonlocal timeout 30 context, args = this, arguments 31 def later(): 32 nonlocal timeout 33 timeout = None 34 if not immediate: 35 func.apply(context, args) 36 call_now = immediate and not timeout 37 window.clearTimeout(timeout) 38 timeout = window.setTimeout(later, wait) 39 if call_now: 40 func.apply(context, args) 41 42if Object.assign: 43 copy_hash = def (obj): 44 return Object.assign({}, obj) 45else: 46 copy_hash = def (obj): 47 return {k:obj[k] for k in Object.keys(obj)} 48 49 50def parse_url_params(url=None, allow_multiple=False): 51 cache = parse_url_params.cache 52 url = url or window.location.href 53 if cache[url]: 54 return copy_hash(parse_url_params.cache[url]) 55 qs = url.indexOf('#') 56 ans = {} 57 if qs < 0: 58 cache[url] = ans 59 return copy_hash(ans) 60 q = url.slice(qs + 1, (url.length + 1)) 61 if not q: 62 cache[url] = ans 63 return copy_hash(ans) 64 pairs = q.replace(/\+/g, " ").split("&") 65 for pair in pairs: 66 key, val = pair.partition('=')[::2] 67 key, val = decodeURIComponent(key), decodeURIComponent(val) 68 if allow_multiple: 69 if ans[key] is undefined: 70 ans[key] = v'[]' 71 ans[key].append(val) 72 else: 73 ans[key] = val 74 cache[url] = ans 75 return copy_hash(ans) 76parse_url_params.cache = {} 77 78 79def encode_query_with_path(query, path): 80 path = path or window.location.pathname 81 return path + encode_query(query, '#') 82 83 84def full_screen_supported(elem): 85 elem = elem or document.documentElement 86 if elem.requestFullScreen or elem.webkitRequestFullScreen or elem.mozRequestFullScreen: 87 return True 88 return False 89 90 91def request_full_screen(elem): 92 elem = elem or document.documentElement 93 options = {'navigationUI': 'hide'} 94 if elem.requestFullScreen: 95 elem.requestFullScreen(options) 96 elif elem.webkitRequestFullScreen: 97 elem.webkitRequestFullScreen() 98 elif elem.mozRequestFullScreen: 99 elem.mozRequestFullScreen() 100 101 102def full_screen_element(): 103 return document.fullscreenElement or document.webkitFullscreenElement or document.mozFullScreenElement or document.msFullscreenElement 104 105 106_roman = list(zip( 107[1000,900,500,400,100,90,50,40,10,9,5,4,1], 108["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"] 109)) 110 111def roman(num): 112 if num <= 0 or num >= 4000 or int(num) is not num: 113 return num + '' 114 result = [] 115 for d, r in _roman: 116 while num >= d: 117 result.append(r) 118 num -= d 119 return result.join('') 120 121def fmt_sidx(val, fmt='{:.2f}', use_roman=True): 122 if val is undefined or val is None or val is '': 123 return '1' 124 if int(val) is float(val): 125 if use_roman: 126 return roman(val) 127 return int(val) + '' 128 return fmt.format(float(val)) 129 130def rating_to_stars(value, allow_half_stars=False, star='★', half='⯨'): 131 r = max(0, min(int(value or 0), 10)) 132 if allow_half_stars: 133 ans = star.repeat(r // 2) 134 if r % 2: 135 ans += half 136 else: 137 ans = star.repeat(int(r/2.0)) 138 return ans 139 140def human_readable(size, sep=' '): 141 divisor, suffix = 1, "B" 142 for i, candidate in enumerate(('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB')): 143 if size < (1 << ((i + 1) * 10)): 144 divisor, suffix = (1 << (i * 10)), candidate 145 break 146 size = (float(size)/divisor) + '' 147 pos = size.find(".") 148 if pos > -1: 149 size = size[:pos + 2] 150 if size.endswith('.0'): 151 size = size[:-2] 152 return size + sep + suffix 153 154def document_height(): 155 html = document.documentElement 156 return max(document.body.scrollHeight, document.body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight) 157 158def document_width(): 159 html = document.documentElement 160 return max(document.body.scrollWidth, document.body.offsetWidth, html.clientWidth, html.scrollWidth, html.offsetWidth) 161 162_data_ns = None 163 164def data_ns(name): 165 nonlocal _data_ns 166 if _data_ns is None: 167 rand = Uint8Array(12) 168 window.crypto.getRandomValues(rand) 169 _data_ns = 'data-' + hexlify(rand) + '-' 170 return _data_ns + name 171 172def get_elem_data(elem, name, defval): 173 ans = elem.getAttribute(data_ns(name)) 174 if ans is None: 175 return defval ? None 176 return JSON.parse(ans) 177 178def set_elem_data(elem, name, val): 179 elem.setAttribute(data_ns(name), JSON.stringify(val)) 180 181def username_key(username): 182 return ('u' if username else 'n') + username 183 184def html_escape(text): 185 repl = { '&': "&", '"': """, '<': "<", '>': ">" } 186 return String.prototype.replace.call(text, /[&"<>]/g, def (c): return repl[c];) 187 188def uniq(vals): 189 # Remove all duplicates from vals, while preserving order 190 ans = v'[]' 191 seen = {} 192 for x in vals: 193 if not seen[x]: 194 seen[x] = True 195 ans.push(x) 196 return ans 197 198def conditional_timeout(elem_id, timeout, func): 199 def ct_impl(): 200 elem = document.getElementById(elem_id) 201 if elem: 202 func.call(elem) 203 window.setTimeout(ct_impl, timeout) 204 205 206def simple_markup(html): 207 html = (html or '').replace(/\uffff/g, '').replace( 208 /<\s*(\/?[a-zA-Z1-6]+)[^>]*>/g, def (match, tag): 209 tag = tag.toLowerCase() 210 is_closing = '/' if tag[0] is '/' else '' 211 if is_closing: 212 tag = tag[1:] 213 if simple_markup.allowed_tags.indexOf(tag) < 0: 214 tag = 'span' 215 return f'\uffff{is_closing}{tag}\uffff' 216 ) 217 div = document.createElement('b') 218 div.textContent = html 219 html = div.innerHTML 220 return html.replace(/\uffff(\/?[a-z1-6]+)\uffff/g, '<$1>') 221simple_markup.allowed_tags = v"'a|b|i|br|hr|h1|h2|h3|h4|h5|h6|div|em|strong|span'.split('|')" 222 223 224def safe_set_inner_html(elem, html): 225 elem.innerHTML = simple_markup(html) 226 return elem 227 228 229def sandboxed_html(html, style, sandbox): 230 ans = document.createElement('iframe') 231 ans.setAttribute('sandbox', sandbox or '') 232 ans.setAttribute('seamless', '') 233 ans.style.width = '100%' 234 html = html or '' 235 css = 'html, body { margin: 0; padding: 0; font-family: __FONT__ } p:first-child { margin-top: 0; padding-top: 0; -webkit-margin-before: 0 }'.replace('__FONT__', get_font_family()) 236 css += style or '' 237 final_html = f'<!DOCTYPE html><html><head><style>{css}</style></head><body>{html}</body></html>' 238 # Microsoft Edge does not support srcdoc not does it work using a data URI. 239 ans.srcdoc = final_html 240 return ans 241