PageSourceSearch

https://wikichree.com/sys/site/skin/main.js

js wikichree.com collected 2026-09-25 21:24:23 UTC 20,482 bytes, 617 lines download raw bytes

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
9window.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})

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.