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.