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