1 // PukiWiki - Yet another WikiWikiWeb clone.
3 // Copyright 2017-2020 PukiWiki Development Team
4 // License: GPL v2 or (at your option) any later version
6 // PukiWiki JavaScript client script
7 /* eslint-env browser */
8 // eslint-disable-next-line no-unused-expressions
9 window.addEventListener && window.addEventListener('DOMContentLoaded', function () {
12 * @param {NodeList} nodeList
13 * @param {function(Node, number): void} func
15 function forEach (nodeList, func) {
16 if (nodeList.forEach) {
17 nodeList.forEach(func)
19 for (var i = 0, n = nodeList.length; i < n; i++) {
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
31 var u = new URL(formAction, document.location)
32 var u2 = new URL('./', u)
33 actionPathname = u2.pathname
36 // Note: Internet Explorer doesn't support URL class
37 var m = formAction.match(/^https?:\/\/([^/]+)(\/([^?&]+\/)?)/)
39 actionPathname = m[2] // pathname
46 function getNameKey (form) {
47 var pathname = getPathname(form.action)
48 var key = 'path.' + pathname + '.' + NAME_KEY_ID
51 function getForm (element) {
52 if (element.form && element.form.tagName === 'FORM') {
55 var e = element.parentElement
56 for (var i = 0; i < 5; i++) {
57 if (e.tagName === 'FORM') {
64 function handleCommentPlugin (form) {
66 var nameKey = getNameKey(form)
67 if (typeof localStorage !== 'undefined') {
68 namePrevious = localStorage[nameKey]
70 var onFocusForm = function () {
71 if (form.name && !form.name.value && namePrevious) {
72 form.name.value = namePrevious
75 var addOnForcusForm = function (eNullable) {
76 if (!eNullable) return
77 if (eNullable.addEventListener) {
78 eNullable.addEventListener('focus', onFocusForm)
82 var textList = form.querySelectorAll('input[type=text],textarea')
83 textList.forEach(function (v) {
87 form.addEventListener('submit', function () {
88 if (typeof localStorage !== 'undefined') {
89 localStorage[nameKey] = form.name.value
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])
103 handleCommentPlugin(form)
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) {
123 function setupSites (siteList) {
124 for (var i = 0, length = siteList.length; i < length; i++) {
125 var site = siteList[i]
129 reText = '(' + regexEscape(site.key) +
130 '):([A-Z][A-Z0-9]{1,20}(?:_[A-Z0-9]{1,10}){0,2}-\\d{1,10})'
133 reText = '(' + regexEscape(site.key) + '):(\\d{1,10})'
136 reText = '(' + regexEscape(site.key) + '):([0-9a-f]{7,40})'
142 site.re = new RegExp(headReText + reText + tailReText)
145 function getJiraSite () {
146 var reText = '()([A-Z][A-Z0-9]{1,20}(?:_[A-Z0-9]{1,10}){0,2}-\\d{1,10})'
148 title: 'Builtin JIRA',
152 re: new RegExp(headReText + reText + tailReText)
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)
165 function getJiraProjectsFromBody () {
166 var defRoot = document.querySelector('#pukiwiki-site-properties .ticketlink-jira-def')
167 if (defRoot && defRoot.value) {
169 return JSON.parse(defRoot.value) // List
176 function getJiraDefaultInfoFromBody () {
177 var defRoot = document.querySelector('#pukiwiki-site-properties .ticketlink-jira-default-def')
178 if (defRoot && defRoot.value) {
180 return JSON.parse(defRoot.value) // object
187 function getSiteList () {
188 return ticketSiteList
190 function getJiraProjectList () {
193 function getDefaultJira () {
194 return jiraDefaultInfo
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)
203 var title = ticketKey
205 if (site.type === '_jira_') {
207 var projects = getJiraProjectList()
208 var hyphen = keyText.indexOf('-')
210 var projectKey = keyText.substr(0, hyphen)
212 for (var j = 0; j < projects.length; j++) {
214 if (p.key === projectKey) {
216 title = p.title.replace(/\$1/g, ticketKey)
218 ticketUrl = p.base_url + ticketKey
224 var defaultJira = getDefaultJira()
226 if (defaultJira.title) {
227 title = defaultJira.title.replace(/\$1/g, ticketKey)
229 ticketUrl = defaultJira.base_url + ticketKey
237 // Explicit TicketLink
239 title = site.title.replace(/\$1/g, ticketKey)
241 ticketUrl = site.base_url + ticketKey
251 function getRegex (list) {
253 for (var i = 0, length = list.length; i < length; i++) {
254 if (reText.length > 0) {
257 reText += list[i].reText
259 return new RegExp(headReText + '(' + reText + ')' + tailReText)
261 function makeTicketLink (element) {
262 var siteList = getSiteList()
263 if (!siteList || siteList.length === 0) {
266 var re = getRegex(siteList)
269 var text = element.nodeValue
270 while (m = text.match(re)) { // eslint-disable-line no-cond-assign
271 // m[1]: head, m[2]: keyText
273 f = document.createDocumentFragment()
275 if (m.index > 0 || m[1].length > 0) {
276 f.appendChild(document.createTextNode(text.substr(0, m.index) + m[1]))
279 var linkInfo = ticketToLink(linkKey)
281 var a = document.createElement('a')
282 a.textContent = linkKey
283 a.href = linkInfo.url
284 a.title = linkInfo.title
287 f.appendChild(document.createTextNode(m[2]))
289 text = text.substr(m.index + m[0].length)
292 if (text.length > 0) {
293 f.appendChild(document.createTextNode(text))
295 element.parentNode.replaceChild(f, element)
298 function walkElement (element) {
299 var e = element.firstChild
301 if (e.nodeType === 3 && e.nodeValue &&
302 e.nodeValue.length > 5 && /\S/.test(e.nodeValue)) {
303 var next = e.nextSibling
307 if (e.nodeType === 1 && ignoreTags.indexOf(e.tagName) === -1) {
314 if (!Array.prototype.indexOf || !document.createDocumentFragment) {
317 ticketSiteList = getSiteListFromBody()
318 jiraProjects = getJiraProjectsFromBody()
319 jiraDefaultInfo = getJiraDefaultInfoFromBody()
320 if (jiraDefaultInfo || (jiraProjects && jiraProjects.length > 0)) {
321 ticketSiteList.push(getJiraSite())
323 var target = document.getElementById('body')
326 function confirmEditFormLeaving () {
328 if (typeof s !== 'string') {
331 return s.replace(/^\s+|\s+$/g, '')
333 if (!document.querySelector) return
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')
351 var cancelForm = document.querySelector('.edit_form form._plugin_edit_cancel')
353 editForm.addEventListener('submit', function () {
357 cancelForm.addEventListener('submit', function (e) {
360 if (trim(textArea.value) === trim(originalText)) {
364 var message = 'The text you have entered will be discarded. Is it OK?'
365 if (cancelMsgE && cancelMsgE.value) {
366 message = cancelMsgE.value
368 if (window.confirm(message)) { // eslint-disable-line no-alert
376 window.addEventListener('beforeunload', function (e) {
380 if (trim(textArea.value) === trim(originalText)) return
382 var message = 'Data you have entered will not be saved.'
383 if (unloadBeforeMsgE && unloadBeforeMsgE.value) {
384 message = unloadBeforeMsgE.value
386 e.returnValue = message
389 function showPagePassage () {
392 * @param {string} dateText
394 function getSimplePassage (dateText, now) {
398 var units = [{ u: 'm', max: 60 }, { u: 'h', max: 24 }, { u: 'd', max: 1 }]
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
408 return '' + Math.floor(t) + unit
412 * @param {string} dateText
414 function getPassage (dateText, now) {
415 return '(' + getSimplePassage(dateText, now) + ')'
418 var elements = document.getElementsByClassName('page_passage')
419 forEach(elements, function (e) {
420 var dt = e.getAttribute('data-mtime')
423 e.textContent = ' ' + getPassage(d, now)
426 var links = document.getElementsByClassName('link_page_passage')
427 forEach(links, function (e) {
428 var dt = e.getAttribute('data-mtime')
432 e.title = e.title + ' ' + getPassage(d, now)
434 e.title = e.textContent + ' ' + getPassage(d, now)
438 var simplePassages = document.getElementsByClassName('simple_passage')
439 forEach(simplePassages, function (e) {
440 var dt = e.getAttribute('data-mtime')
443 e.textContent = getSimplePassage(d, now)
447 var newItems = document.getElementsByClassName('__plugin_new')
448 forEach(newItems, function (e) {
449 var dt = e.getAttribute('data-mtime')
452 var diff = now.getTime() - d.getTime()
453 var daySpan = diff / 1000 / 60 / 60 / 24
455 e.textContent = ' New!'
456 e.title = getPassage(d, now)
457 if (e.classList && e.classList.add) {
458 e.classList.add('new1')
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')
470 function convertExternalLinkToCushionPageLink () {
471 function domainQuote (domain) {
472 return domain.replace(/\./g, '\\.')
474 function domainsToRegex (domains) {
476 domains.forEach(function (domain) {
477 if (domain.substr(0, 2) === '*.') {
479 var apex = domain.substr(2)
480 var r = new RegExp('((^.*\\.)|^)' + domainQuote(apex) + '$', 'i')
484 regexList.push(new RegExp('^' + domainQuote(domain) + '$', 'i'))
489 function domainMatch (domain, regexList) {
490 for (var i = 0, n = regexList.length; i < n; i++) {
491 if (regexList[i].test(domain)) {
497 function removeCushionPageLinks () {
498 var links = document.querySelectorAll('a.external-link')
499 forEach(links, function (link) {
500 var originalUrl = link.getAttribute('data-original-url')
502 link.setAttribute('href', originalUrl)
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 = []
517 var internalDomains = refInternalDomains.slice()
518 var location = document.location
519 if (location.protocol === 'file:') {
520 removeCushionPageLinks()
523 if (location.protocol !== 'http:' && location.protocol !== 'https:') return
524 if (internalDomains.indexOf(location.hostname) < 0) {
525 internalDomains.push(location.hostname)
527 if (!Array.isArray(silentExternalDomains)) {
528 silentExternalDomains = []
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
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+)?/)
549 if (domainMatch(host, internalDomainsR)) {
550 link.classList.add('internal-link')
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')
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))
563 link.classList.add('internal-link')
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')
580 var aList = titleH1.querySelectorAll('a')
581 if (!aList || aList.length > 1) return
582 var a = titleH1.querySelector('a')
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]
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)
595 var s1 = document.createElement('span')
596 s1.textContent = path.leaf
597 fragment.appendChild(s1)
599 var span = document.createElement('span')
600 span.className = 'topicpath-slash'
601 span.textContent = '/'
602 fragment.appendChild(span)
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)
613 confirmEditFormLeaving()
615 convertExternalLinkToCushionPageLink()