/** * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ // THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.domline // %APPJET%: import("etherpad.admin.plugins"); /** * Copyright 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS-IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // requires: top // requires: plugins // requires: undefined const Security = require('./security'); const hooks = require('./pluginfw/hooks'); const _ = require('./underscore'); const lineAttributeMarker = require('./linestylefilter').lineAttributeMarker; const noop = function () {}; const domline = {}; domline.addToLineClass = function (lineClass, cls) { // an "empty span" at any point can be used to add classes to // the line, using line:className. otherwise, we ignore // the span. cls.replace(/\S+/g, (c) => { if (c.indexOf('line:') == 0) { // add class to line lineClass = (lineClass ? `${lineClass} ` : '') + c.substring(5); } }); return lineClass; }; // if "document" is falsy we don't create a DOM node, just // an object with innerHTML and className domline.createDomLine = function (nonEmpty, doesWrap, optBrowser, optDocument) { const result = { node: null, appendSpan: noop, prepareForAdd: noop, notifyAdded: noop, clearSpans: noop, finishUpdate: noop, lineMarker: 0, }; const document = optDocument; if (document) { result.node = document.createElement('div'); } else { result.node = { innerHTML: '', className: '', }; } let html = []; let preHtml = ''; let postHtml = ''; let curHTML = null; function processSpaces(s) { return domline.processSpaces(s, doesWrap); } const perTextNodeProcess = (doesWrap ? _.identity : processSpaces); const perHtmlLineProcess = (doesWrap ? processSpaces : _.identity); let lineClass = 'ace-line'; result.appendSpan = function (txt, cls) { let processedMarker = false; // Handle lineAttributeMarker, if present if (cls.indexOf(lineAttributeMarker) >= 0) { let listType = /(?:^| )list:(\S+)/.exec(cls); const start = /(?:^| )start:(\S+)/.exec(cls); _.map(hooks.callAll('aceDomLinePreProcessLineAttributes', { domline, cls, }), (modifier) => { preHtml += modifier.preHtml; postHtml += modifier.postHtml; processedMarker |= modifier.processedMarker; }); if (listType) { listType = listType[1]; if (listType) { if (listType.indexOf('number') < 0) { preHtml += `${postHtml}`; } else { if (start) { // is it a start of a list with more than one item in? if (start[1] == 1) { // if its the first one at this level? lineClass = `${lineClass} ` + `list-start-${listType}`; // Add start class to DIV node } preHtml += `
  1. `; } else { preHtml += `
    1. `; // Handles pasted contents into existing lists } postHtml += '
    '; } } processedMarker = true; } _.map(hooks.callAll('aceDomLineProcessLineAttributes', { domline, cls, }), (modifier) => { preHtml += modifier.preHtml; postHtml += modifier.postHtml; processedMarker |= modifier.processedMarker; }); if (processedMarker) { result.lineMarker += txt.length; return; // don't append any text } } let href = null; let simpleTags = null; if (cls.indexOf('url') >= 0) { cls = cls.replace(/(^| )url:(\S+)/g, (x0, space, url) => { href = url; return `${space}url`; }); } if (cls.indexOf('tag') >= 0) { cls = cls.replace(/(^| )tag:(\S+)/g, (x0, space, tag) => { if (!simpleTags) simpleTags = []; simpleTags.push(tag.toLowerCase()); return space + tag; }); } let extraOpenTags = ''; let extraCloseTags = ''; _.map(hooks.callAll('aceCreateDomLine', { domline, cls, }), (modifier) => { cls = modifier.cls; extraOpenTags += modifier.extraOpenTags; extraCloseTags = modifier.extraCloseTags + extraCloseTags; }); if ((!txt) && cls) { lineClass = domline.addToLineClass(lineClass, cls); } else if (txt) { if (href) { urn_schemes = new RegExp('^(about|geo|mailto|tel):'); if (!~href.indexOf('://') && !urn_schemes.test(href)) // if the url doesn't include a protocol prefix, assume http { href = `http://${href}`; } // Using rel="noreferrer" stops leaking the URL/location of the pad when clicking links in the document. // Not all browsers understand this attribute, but it's part of the HTML5 standard. // https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer // Additionally, we do rel="noopener" to ensure a higher level of referrer security. // https://html.spec.whatwg.org/multipage/links.html#link-type-noopener // https://mathiasbynens.github.io/rel-noopener/ // https://github.com/ether/etherpad-lite/pull/3636 extraOpenTags = `${extraOpenTags}`; extraCloseTags = `${extraCloseTags}`; } if (simpleTags) { simpleTags.sort(); extraOpenTags = `${extraOpenTags}<${simpleTags.join('><')}>`; simpleTags.reverse(); extraCloseTags = `${extraCloseTags}`; } html.push('', extraOpenTags, perTextNodeProcess(Security.escapeHTML(txt)), extraCloseTags, ''); } }; result.clearSpans = function () { html = []; lineClass = 'ace-line'; result.lineMarker = 0; }; function writeHTML() { let newHTML = perHtmlLineProcess(html.join('')); if (!newHTML) { if ((!document) || (!optBrowser)) { newHTML += ' '; } else if (!optBrowser.msie) { newHTML += '
    '; } } if (nonEmpty) { newHTML = (preHtml || '') + newHTML + (postHtml || ''); } html = preHtml = postHtml = ''; // free memory if (newHTML !== curHTML) { curHTML = newHTML; result.node.innerHTML = curHTML; } if (lineClass !== null) result.node.className = lineClass; hooks.callAll('acePostWriteDomLineHTML', { node: result.node, }); } result.prepareForAdd = writeHTML; result.finishUpdate = writeHTML; result.getInnerHTML = function () { return curHTML || ''; }; return result; }; domline.processSpaces = function (s, doesWrap) { if (s.indexOf('<') < 0 && !doesWrap) { // short-cut return s.replace(/ /g, ' '); } const parts = []; s.replace(/<[^>]*>?| |[^ <]+/g, (m) => { parts.push(m); }); if (doesWrap) { let endOfLine = true; let beforeSpace = false; // last space in a run is normal, others are nbsp, // end of line is nbsp for (var i = parts.length - 1; i >= 0; i--) { var p = parts[i]; if (p == ' ') { if (endOfLine || beforeSpace) parts[i] = ' '; endOfLine = false; beforeSpace = true; } else if (p.charAt(0) != '<') { endOfLine = false; beforeSpace = false; } } // beginning of line is nbsp for (var i = 0; i < parts.length; i++) { var p = parts[i]; if (p == ' ') { parts[i] = ' '; break; } else if (p.charAt(0) != '<') { break; } } } else { for (var i = 0; i < parts.length; i++) { var p = parts[i]; if (p == ' ') { parts[i] = ' '; } } } return parts.join(''); }; exports.domline = domline;