OSDN Git Service

BugTrack/2514 PHP8: uasort with non-static comparison func
[pukiwiki/pukiwiki.git] / skin / main.js
1 // PukiWiki - Yet another WikiWikiWeb clone.
2 // main.js
3 // Copyright 2017-2020 PukiWiki Development Team
4 // License: GPL v2 or (at your option) any later version
5 //
6 // PukiWiki JavaScript client script
7 /* eslint-env browser */
8 // eslint-disable-next-line no-unused-expressions
9 window.addEventListener && window.addEventListener('DOMContentLoaded', function () {
10   'use strict'
11   /**
12    * @param {NodeList} nodeList
13    * @param {function(Node, number): void} func
14    */
15   function forEach (nodeList, func) {
16     if (nodeList.forEach) {
17       nodeList.forEach(func)
18     } else {
19       for (var i = 0, n = nodeList.length; i < n; i++) {
20         func(nodeList[i], i)
21       }
22     }
23   }
24   // Name for comment
25   function setYourName () {
26     var NAME_KEY_ID = 'pukiwiki_comment_plugin_name'
27     var actionPathname = null
28     function getPathname (formAction) {
29       if (actionPathname) return actionPathname
30       try {
31         var u = new URL(formAction, document.location)
32         var u2 = new URL('./', u)
33         actionPathname = u2.pathname
34         return u2.pathname
35       } catch (e) {
36         // Note: Internet Explorer doesn't support URL class
37         var m = formAction.match(/^https?:\/\/([^/]+)(\/([^?&]+\/)?)/)
38         if (m) {
39           actionPathname = m[2] // pathname
40         } else {
41           actionPathname = '/'
42         }
43         return actionPathname
44       }
45     }
46     function getNameKey (form) {
47       var pathname = getPathname(form.action)
48       var key = 'path.' + pathname + '.' + NAME_KEY_ID
49       return key
50     }
51     function getForm (element) {
52       if (element.form && element.form.tagName === 'FORM') {
53         return element.form
54       }
55       var e = element.parentElement
56       for (var i = 0; i < 5; i++) {
57         if (e.tagName === 'FORM') {
58           return e
59         }
60         e = e.parentElement
61       }
62       return null
63     }
64     function handleCommentPlugin (form) {
65       var namePrevious = ''
66       var nameKey = getNameKey(form)
67       if (typeof localStorage !== 'undefined') {
68         namePrevious = localStorage[nameKey]
69       }
70       var onFocusForm = function () {
71         if (form.name && !form.name.value && namePrevious) {
72           form.name.value = namePrevious
73         }
74       }
75       var addOnForcusForm = function (eNullable) {
76         if (!eNullable) return
77         if (eNullable.addEventListener) {
78           eNullable.addEventListener('focus', onFocusForm)
79         }
80       }
81       if (namePrevious) {
82         var textList = form.querySelectorAll('input[type=text],textarea')
83         textList.forEach(function (v) {
84           addOnForcusForm(v)
85         })
86       }
87       form.addEventListener('submit', function () {
88         if (typeof localStorage !== 'undefined') {
89           localStorage[nameKey] = form.name.value
90         }
91       }, false)
92     }
93     function setNameForComment () {
94       if (!document.querySelectorAll) return
95       var elements = document.querySelectorAll(
96         'input[type=hidden][name=plugin][value=comment],' +
97         'input[type=hidden][name=plugin][value=pcomment],' +
98         'input[type=hidden][name=plugin][value=article],' +
99         'input[type=hidden][name=plugin][value=bugtrack]')
100       for (var i = 0; i < elements.length; i++) {
101         var form = getForm(elements[i])
102         if (form) {
103           handleCommentPlugin(form)
104         }
105       }
106     }
107     setNameForComment()
108   }
109   // AutoTicketLink
110   function autoTicketLink () {
111     var headReText = '([\\s\\b:\\[\\(,;]|^)'
112     var tailReText = '\\b'
113     var ignoreTags = ['A', 'INPUT', 'TEXTAREA', 'BUTTON',
114       'SCRIPT', 'FRAME', 'IFRAME']
115     var ticketSiteList = []
116     var jiraProjects = null
117     var jiraDefaultInfo = null
118     function regexEscape (key) {
119       return key.replace(/[-.]/g, function (m) {
120         return '\\' + m
121       })
122     }
123     function setupSites (siteList) {
124       for (var i = 0, length = siteList.length; i < length; i++) {
125         var site = siteList[i]
126         var reText = ''
127         switch (site.type) {
128           case 'jira':
129             reText = '(' + regexEscape(site.key) +
130               '):([A-Z][A-Z0-9]{1,20}(?:_[A-Z0-9]{1,10}){0,2}-\\d{1,10})'
131             break
132           case 'redmine':
133             reText = '(' + regexEscape(site.key) + '):(\\d{1,10})'
134             break
135           case 'git':
136             reText = '(' + regexEscape(site.key) + '):([0-9a-f]{7,40})'
137             break
138           default:
139             continue
140         }
141         site.reText = reText
142         site.re = new RegExp(headReText + reText + tailReText)
143       }
144     }
145     function getJiraSite () {
146       var reText = '()([A-Z][A-Z0-9]{1,20}(?:_[A-Z0-9]{1,10}){0,2}-\\d{1,10})'
147       var site = {
148         title: 'Builtin JIRA',
149         type: '_jira_',
150         key: '_jira_',
151         reText: reText,
152         re: new RegExp(headReText + reText + tailReText)
153       }
154       return site
155     }
156     function getSiteListFromBody () {
157       var defRoot = document.querySelector('#pukiwiki-site-properties .ticketlink-def')
158       if (defRoot && defRoot.value) {
159         var list = JSON.parse(defRoot.value)
160         setupSites(list)
161         return list
162       }
163       return []
164     }
165     function getJiraProjectsFromBody () {
166       var defRoot = document.querySelector('#pukiwiki-site-properties .ticketlink-jira-def')
167       if (defRoot && defRoot.value) {
168         try {
169           return JSON.parse(defRoot.value) // List
170         } catch (e) {
171           return null
172         }
173       }
174       return null
175     }
176     function getJiraDefaultInfoFromBody () {
177       var defRoot = document.querySelector('#pukiwiki-site-properties .ticketlink-jira-default-def')
178       if (defRoot && defRoot.value) {
179         try {
180           return JSON.parse(defRoot.value) // object
181         } catch (e) {
182           return null
183         }
184       }
185       return null
186     }
187     function getSiteList () {
188       return ticketSiteList
189     }
190     function getJiraProjectList () {
191       return jiraProjects
192     }
193     function getDefaultJira () {
194       return jiraDefaultInfo
195     }
196     function ticketToLink (keyText) {
197       var siteList = getSiteList()
198       for (var i = 0; i < siteList.length; i++) {
199         var site = siteList[i]
200         var m = keyText.match(site.re)
201         if (m) {
202           var ticketKey = m[3]
203           var title = ticketKey
204           var ticketUrl
205           if (site.type === '_jira_') {
206             // JIRA issue
207             var projects = getJiraProjectList()
208             var hyphen = keyText.indexOf('-')
209             if (hyphen > 0) {
210               var projectKey = keyText.substr(0, hyphen)
211               if (projects) {
212                 for (var j = 0; j < projects.length; j++) {
213                   var p = projects[j]
214                   if (p.key === projectKey) {
215                     if (p.title) {
216                       title = p.title.replace(/\$1/g, ticketKey)
217                     }
218                     ticketUrl = p.base_url + ticketKey
219                     break
220                   }
221                 }
222               }
223               if (!ticketUrl) {
224                 var defaultJira = getDefaultJira()
225                 if (defaultJira) {
226                   if (defaultJira.title) {
227                     title = defaultJira.title.replace(/\$1/g, ticketKey)
228                   }
229                   ticketUrl = defaultJira.base_url + ticketKey
230                 }
231               }
232             }
233             if (!ticketUrl) {
234               return null
235             }
236           } else {
237             // Explicit TicketLink
238             if (site.title) {
239               title = site.title.replace(/\$1/g, ticketKey)
240             }
241             ticketUrl = site.base_url + ticketKey
242           }
243           return {
244             url: ticketUrl,
245             title: title
246           }
247         }
248       }
249       return null
250     }
251     function getRegex (list) {
252       var reText = ''
253       for (var i = 0, length = list.length; i < length; i++) {
254         if (reText.length > 0) {
255           reText += '|'
256         }
257         reText += list[i].reText
258       }
259       return new RegExp(headReText + '(' + reText + ')' + tailReText)
260     }
261     function makeTicketLink (element) {
262       var siteList = getSiteList()
263       if (!siteList || siteList.length === 0) {
264         return
265       }
266       var re = getRegex(siteList)
267       var f
268       var m
269       var text = element.nodeValue
270       while (m = text.match(re)) { // eslint-disable-line no-cond-assign
271         // m[1]: head, m[2]: keyText
272         if (!f) {
273           f = document.createDocumentFragment()
274         }
275         if (m.index > 0 || m[1].length > 0) {
276           f.appendChild(document.createTextNode(text.substr(0, m.index) + m[1]))
277         }
278         var linkKey = m[2]
279         var linkInfo = ticketToLink(linkKey)
280         if (linkInfo) {
281           var a = document.createElement('a')
282           a.textContent = linkKey
283           a.href = linkInfo.url
284           a.title = linkInfo.title
285           f.appendChild(a)
286         } else {
287           f.appendChild(document.createTextNode(m[2]))
288         }
289         text = text.substr(m.index + m[0].length)
290       }
291       if (f) {
292         if (text.length > 0) {
293           f.appendChild(document.createTextNode(text))
294         }
295         element.parentNode.replaceChild(f, element)
296       }
297     }
298     function walkElement (element) {
299       var e = element.firstChild
300       while (e) {
301         if (e.nodeType === 3 && e.nodeValue &&
302             e.nodeValue.length > 5 && /\S/.test(e.nodeValue)) {
303           var next = e.nextSibling
304           makeTicketLink(e)
305           e = next
306         } else {
307           if (e.nodeType === 1 && ignoreTags.indexOf(e.tagName) === -1) {
308             walkElement(e)
309           }
310           e = e.nextSibling
311         }
312       }
313     }
314     if (!Array.prototype.indexOf || !document.createDocumentFragment) {
315       return
316     }
317     ticketSiteList = getSiteListFromBody()
318     jiraProjects = getJiraProjectsFromBody()
319     jiraDefaultInfo = getJiraDefaultInfoFromBody()
320     if (jiraDefaultInfo || (jiraProjects && jiraProjects.length > 0)) {
321       ticketSiteList.push(getJiraSite())
322     }
323     var target = document.getElementById('body')
324     walkElement(target)
325   }
326   function confirmEditFormLeaving () {
327     function trim (s) {
328       if (typeof s !== 'string') {
329         return s
330       }
331       return s.replace(/^\s+|\s+$/g, '')
332     }
333     if (!document.querySelector) return
334     var canceled = false
335     var pluginNameE = document.querySelector('#pukiwiki-site-properties .plugin-name')
336     if (!pluginNameE) return
337     var originalText = null
338     if (pluginNameE.value !== 'edit') return
339     var editForm = document.querySelector('.edit_form form._plugin_edit_edit_form')
340     if (!editForm) return
341     var cancelMsgE = editForm.querySelector('#_msg_edit_cancel_confirm')
342     var unloadBeforeMsgE = editForm.querySelector('#_msg_edit_unloadbefore_message')
343     var textArea = editForm.querySelector('textarea[name="msg"]')
344     if (!textArea) return
345     originalText = textArea.value
346     var isPreview = false
347     var inEditE = document.querySelector('#pukiwiki-site-properties .page-in-edit')
348     if (inEditE && inEditE.value) {
349       isPreview = (inEditE.value === 'true')
350     }
351     var cancelForm = document.querySelector('.edit_form form._plugin_edit_cancel')
352     var submited = false
353     editForm.addEventListener('submit', function () {
354       canceled = false
355       submited = true
356     })
357     cancelForm.addEventListener('submit', function (e) {
358       submited = false
359       canceled = false
360       if (trim(textArea.value) === trim(originalText)) {
361         canceled = true
362         return false
363       }
364       var message = 'The text you have entered will be discarded. Is it OK?'
365       if (cancelMsgE && cancelMsgE.value) {
366         message = cancelMsgE.value
367       }
368       if (window.confirm(message)) { // eslint-disable-line no-alert
369         // Execute "Cancel"
370         canceled = true
371         return true
372       }
373       e.preventDefault()
374       return false
375     })
376     window.addEventListener('beforeunload', function (e) {
377       if (canceled) return
378       if (submited) return
379       if (!isPreview) {
380         if (trim(textArea.value) === trim(originalText)) return
381       }
382       var message = 'Data you have entered will not be saved.'
383       if (unloadBeforeMsgE && unloadBeforeMsgE.value) {
384         message = unloadBeforeMsgE.value
385       }
386       e.returnValue = message
387     }, false)
388   }
389   function showPagePassage () {
390     /**
391      * @param {Date} now
392      * @param {string} dateText
393      */
394     function getSimplePassage (dateText, now) {
395       if (!dateText) {
396         return ''
397       }
398       var units = [{ u: 'm', max: 60 }, { u: 'h', max: 24 }, { u: 'd', max: 1 }]
399       var d = new Date()
400       d.setTime(Date.parse(dateText))
401       var t = (now.getTime() - d.getTime()) / (1000 * 60) // minutes
402       var unit = units[0].u; var card = units[0].max
403       for (var i = 0; i < units.length; i++) {
404         unit = units[i].u; card = units[i].max
405         if (t < card) break
406         t = t / card
407       }
408       return '' + Math.floor(t) + unit
409     }
410     /**
411      * @param {Date} now
412      * @param {string} dateText
413      */
414     function getPassage (dateText, now) {
415       return '(' + getSimplePassage(dateText, now) + ')'
416     }
417     var now = new Date()
418     var elements = document.getElementsByClassName('page_passage')
419     forEach(elements, function (e) {
420       var dt = e.getAttribute('data-mtime')
421       if (dt) {
422         var d = new Date(dt)
423         e.textContent = ' ' + getPassage(d, now)
424       }
425     })
426     var links = document.getElementsByClassName('link_page_passage')
427     forEach(links, function (e) {
428       var dt = e.getAttribute('data-mtime')
429       if (dt) {
430         var d = new Date(dt)
431         if (e.title) {
432           e.title = e.title + ' ' + getPassage(d, now)
433         } else {
434           e.title = e.textContent + ' ' + getPassage(d, now)
435         }
436       }
437     })
438     var simplePassages = document.getElementsByClassName('simple_passage')
439     forEach(simplePassages, function (e) {
440       var dt = e.getAttribute('data-mtime')
441       if (dt) {
442         var d = new Date(dt)
443         e.textContent = getSimplePassage(d, now)
444       }
445     })
446     // new plugin
447     var newItems = document.getElementsByClassName('__plugin_new')
448     forEach(newItems, function (e) {
449       var dt = e.getAttribute('data-mtime')
450       if (dt) {
451         var d = new Date(dt)
452         var diff = now.getTime() - d.getTime()
453         var daySpan = diff / 1000 / 60 / 60 / 24
454         if (daySpan < 1) {
455           e.textContent = ' New!'
456           e.title = getPassage(d, now)
457           if (e.classList && e.classList.add) {
458             e.classList.add('new1')
459           }
460         } else if (daySpan < 5) {
461           e.textContent = ' New'
462           e.title = getPassage(d, now)
463           if (e.classList && e.classList.add) {
464             e.classList.add('new5')
465           }
466         }
467       }
468     })
469   }
470   function convertExternalLinkToCushionPageLink () {
471     function domainQuote (domain) {
472       return domain.replace(/\./g, '\\.')
473     }
474     function domainsToRegex (domains) {
475       var regexList = []
476       domains.forEach(function (domain) {
477         if (domain.substr(0, 2) === '*.') {
478           // Wildcard domain
479           var apex = domain.substr(2)
480           var r = new RegExp('((^.*\\.)|^)' + domainQuote(apex) + '$', 'i')
481           regexList.push(r)
482         } else {
483           // Normal domain
484           regexList.push(new RegExp('^' + domainQuote(domain) + '$', 'i'))
485         }
486       })
487       return regexList
488     }
489     function domainMatch (domain, regexList) {
490       for (var i = 0, n = regexList.length; i < n; i++) {
491         if (regexList[i].test(domain)) {
492           return true
493         }
494       }
495       return false
496     }
497     function removeCushionPageLinks () {
498       var links = document.querySelectorAll('a.external-link')
499       forEach(links, function (link) {
500         var originalUrl = link.getAttribute('data-original-url')
501         if (originalUrl) {
502           link.setAttribute('href', originalUrl)
503         }
504       })
505     }
506     if (!document.querySelector || !JSON) return
507     if (!Array || !Array.prototype || !Array.prototype.indexOf) return
508     var extLinkDef = document.querySelector('#pukiwiki-site-properties .external-link-cushion')
509     if (!extLinkDef || !extLinkDef.value) return
510     var extLinkInfo = JSON.parse(extLinkDef.value)
511     if (!extLinkInfo) return
512     var refInternalDomains = extLinkInfo.internal_domains
513     var silentExternalDomains = extLinkInfo.silent_external_domains
514     if (!Array.isArray(refInternalDomains)) {
515       refInternalDomains = []
516     }
517     var internalDomains = refInternalDomains.slice()
518     var location = document.location
519     if (location.protocol === 'file:') {
520       removeCushionPageLinks()
521       return
522     }
523     if (location.protocol !== 'http:' && location.protocol !== 'https:') return
524     if (internalDomains.indexOf(location.hostname) < 0) {
525       internalDomains.push(location.hostname)
526     }
527     if (!Array.isArray(silentExternalDomains)) {
528       silentExternalDomains = []
529     }
530     var propsE = document.querySelector('#pukiwiki-site-properties .site-props')
531     if (!propsE || !propsE.value) return
532     var siteProps = JSON.parse(propsE.value)
533     var sitePathname = siteProps && siteProps.base_uri_pathname
534     if (!sitePathname) return
535     var internalDomainsR = domainsToRegex(internalDomains)
536     var silentExternalDomainsR = domainsToRegex(silentExternalDomains)
537     var links = document.querySelectorAll('a:not(.external-link):not(.internal-link)')
538     var classListEnabled = null
539     forEach(links, function (link) {
540       if (classListEnabled === null) {
541         classListEnabled = link.classList && link.classList.add && true
542       }
543       if (!classListEnabled) return
544       var href = link.getAttribute('href')
545       if (!href) return // anchor without href attribute (a name)
546       var m = href.match(/^https?:\/\/([0-9a-zA-Z.-]+)(:\d+)?/)
547       if (m) {
548         var host = m[1]
549         if (domainMatch(host, internalDomainsR)) {
550           link.classList.add('internal-link')
551         } else {
552           if (domainMatch(host, silentExternalDomainsR) ||
553             link.textContent.replace(/\s+/g, '') === '') {
554             // Don't show extenal link icons on these domains
555             link.classList.add('external-link-silent')
556           }
557           link.classList.add('external-link')
558           link.setAttribute('title', href)
559           link.setAttribute('data-original-url', href)
560           link.setAttribute('href', sitePathname + '?cmd=external_link&url=' + encodeURIComponent(href))
561         }
562       } else {
563         link.classList.add('internal-link')
564       }
565     })
566   }
567   function makeTopicpathTitle () {
568     if (!document.createDocumentFragment || !window.JSON) return
569     var sitePropE = document.querySelector('#pukiwiki-site-properties')
570     if (!sitePropE) return
571     var pageNameE = sitePropE.querySelector('.page-name')
572     if (!pageNameE || !pageNameE.value) return
573     var pageName = pageNameE.value
574     var topicpathE = sitePropE.querySelector('.topicpath-links')
575     if (!topicpathE || !topicpathE.value) return
576     var topicpathLinks = JSON.parse(topicpathE.value)
577     if (!topicpathLinks) return
578     var titleH1 = document.querySelector('h1.title')
579     if (!titleH1) return
580     var aList = titleH1.querySelectorAll('a')
581     if (!aList || aList.length > 1) return
582     var a = titleH1.querySelector('a')
583     if (!a) return
584     if (a.textContent !== pageName) return
585     var fragment = document.createDocumentFragment()
586     for (var i = 0, n = topicpathLinks.length; i < n; i++) {
587       var path = topicpathLinks[i]
588       if (path.uri) {
589         var a1 = document.createElement('a')
590         a1.setAttribute('href', path.uri)
591         a1.setAttribute('title', path.page)
592         a1.textContent = path.leaf
593         fragment.appendChild(a1)
594       } else {
595         var s1 = document.createElement('span')
596         s1.textContent = path.leaf
597         fragment.appendChild(s1)
598       }
599       var span = document.createElement('span')
600       span.className = 'topicpath-slash'
601       span.textContent = '/'
602       fragment.appendChild(span)
603     }
604     var a2 = document.createElement('a')
605     a2.setAttribute('href', a.getAttribute('href'))
606     a2.setAttribute('title', 'Backlinks')
607     a2.textContent = a.textContent.replace(/^.+\//, '')
608     fragment.appendChild(a2)
609     a.parentNode.replaceChild(fragment, a)
610   }
611   setYourName()
612   autoTicketLink()
613   confirmEditFormLeaving()
614   showPagePassage()
615   convertExternalLinkToCushionPageLink()
616   makeTopicpathTitle()
617 })