text: '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');
    } 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 += `<ul class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
              postHtml = `</li></ul>${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 +=
                  `<ol start=${start[1]} class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
              } else {
                // Handles pasted contents into existing lists
                preHtml += `<ol class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
              }
              postHtml += '</li></ol>';
            }
          }
          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}<a href="${escapedHref}" rel="noreferrer noopener">`;
          extraCloseTags = `</a>${extraCloseTags}`;
        }
        if (simpleTags) {
          simpleTags.sort();
          extraOpenTags = `${extraOpenTags}<${simpleTags.join('><')}>`;
          simpleTags.reverse();
          extraCloseTags = `</${simpleTags.join('></')}>${extraCloseTags}`;
        }
        html.push(
            '<span class="', Security.escapeHTMLAttribute(cls || ''),
            '">',
            extraOpenTags,
            perTextNodeProcess(Security.escapeHTML(txt)),
            extraCloseTags,
            '</span>');
      }
    };
    result.clearSpans = () => {
      html = [];
      lineClass = 'ace-line';
      result.lineMarker = 0;
    };
  
    const writeHTML = () => {
      let newHTML = perHtmlLineProcess(html.join(''));
      if (!newHTML) {
        if ((!document) || (!optBrowser)) {
          newHTML += '&nbsp;';
        } else {
          newHTML += '<br/>';
        }
      }
      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, '&nbsp;');
    }
    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] = '&nbsp;';
          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] = '&nbsp;';
          break;
        } else if (p.charAt(0) !== '<') {
          break;
        }
      }
    } else {
      for (let i = 0; i < parts.length; i++) {
        const p = parts[i];
        if (p === ' ') {
          parts[i] = '&nbsp;';
        }
      }
    }
    return parts.join('');
  };
  
  exports.domline = domline;
  