'use strict'; // 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 = () => {}; const domline = {}; domline.addToLineClass = (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 = (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'); // JAWS and NVDA screen reader compatibility. Only needed if in a real browser. result.node.setAttribute('aria-live', 'assertive'); } else { result.node = { innerHTML: '', className: '', }; } let html = []; let preHtml = ''; let postHtml = ''; let curHTML = null; const processSpaces = (s) => domline.processSpaces(s, doesWrap); const perTextNodeProcess = (doesWrap ? _.identity : processSpaces); const perHtmlLineProcess = (doesWrap ? processSpaces : _.identity); let lineClass = 'ace-line'; result.appendSpan = (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 (Number.parseInt(start[1]) === 1) { // if its the first one at this level? // Add start class to DIV node lineClass = `${lineClass} ` + `list-start-${listType}`; } preHtml += `
  1. `; } else { // Handles pasted contents into existing lists preHtml += `
    1. `; } 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) { const urn_schemes = new RegExp('^(about|geo|mailto|tel):'); // if the url doesn't include a protocol prefix, assume http if (!~href.indexOf('://') && !urn_schemes.test(href)) { 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 const escapedHref = Security.escapeHTMLAttribute(href); 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 = () => { html = []; lineClass = 'ace-line'; result.lineMarker = 0; }; const writeHTML = () => { let newHTML = perHtmlLineProcess(html.join('')); if (!newHTML) { if ((!document) || (!optBrowser)) { newHTML += ' '; } else { 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; return result; }; domline.processSpaces = (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 (let i = parts.length - 1; i >= 0; i--) { const 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 (let i = 0; i < parts.length; i++) { const p = parts[i]; if (p === ' ') { parts[i] = ' '; break; } else if (p.charAt(0) !== '<') { break; } } } else { for (let i = 0; i < parts.length; i++) { const p = parts[i]; if (p === ' ') { parts[i] = ' '; } } } return parts.join(''); }; exports.domline = domline;