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 = { '&': "&amp;", '"': "&quot;", '<': "&lt;", '>': "&gt;" }
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