text: /**
   * @license
   * Copyright (C) 2016 The Android Open Source Project
   *
   * 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.
   */
  (function() {
    'use strict';
  
    const LANGUAGE_MAP = {
      'application/dart': 'dart',
      'application/json': 'json',
      'application/x-powershell': 'powershell',
      'application/typescript': 'typescript',
      'application/xml': 'xml',
      'application/xquery': 'xquery',
      'application/x-erb': 'erb',
      'text/css': 'css',
      'text/html': 'html',
      'text/javascript': 'js',
      'text/jsx': 'jsx',
      'text/x-c': 'cpp',
      'text/x-c++src': 'cpp',
      'text/x-clojure': 'clojure',
      'text/x-cmake': 'cmake',
      'text/x-coffeescript': 'coffeescript',
      'text/x-common-lisp': 'lisp',
      'text/x-crystal': 'crystal',
      'text/x-csharp': 'csharp',
      'text/x-csrc': 'cpp',
      'text/x-d': 'd',
      'text/x-diff': 'diff',
      'text/x-django': 'django',
      'text/x-dockerfile': 'dockerfile',
      'text/x-ebnf': 'ebnf',
      'text/x-elm': 'elm',
      'text/x-erlang': 'erlang',
      'text/x-fortran': 'fortran',
      'text/x-go': 'go',
      'text/x-groovy': 'groovy',
      'text/x-haml': 'haml',
      'text/x-haskell': 'haskell',
      'text/x-haxe': 'haxe',
      'text/x-ini': 'ini',
      'text/x-java': 'java',
      'text/x-julia': 'julia',
      'text/x-kotlin': 'kotlin',
      'text/x-latex': 'latex',
      'text/x-less': 'less',
      'text/x-lua': 'lua',
      'text/x-mathematica': 'mathematica',
      'text/x-nginx-conf': 'nginx',
      'text/x-nsis': 'nsis',
      'text/x-objectivec': 'objectivec',
      'text/x-ocaml': 'ocaml',
      'text/x-perl': 'perl',
      'text/x-pgsql': 'pgsql', // postgresql
      'text/x-php': 'php',
      'text/x-properties': 'properties',
      'text/x-protobuf': 'protobuf',
      'text/x-puppet': 'puppet',
      'text/x-python': 'python',
      'text/x-q': 'q',
      'text/x-ruby': 'ruby',
      'text/x-rustsrc': 'rust',
      'text/x-scala': 'scala',
      'text/x-scss': 'scss',
      'text/x-scheme': 'scheme',
      'text/x-shell': 'shell',
      'text/x-spreadsheet': 'excel',
      'text/x-sh': 'bash',
      'text/x-sql': 'sql',
      'text/x-swift': 'swift',
      'text/x-systemverilog': 'sv',
      'text/x-tcl': 'tcl',
      'text/x-twig': 'twig',
      'text/x-vb': 'vb',
      'text/x-verilog': 'v',
      'text/x-yaml': 'yaml',
      'text/vbscript': 'vbscript',
    };
    const ASYNC_DELAY = 10;
  
    const CLASS_WHITELIST = {
      'gr-diff gr-syntax gr-syntax-attr': true,
      'gr-diff gr-syntax gr-syntax-attribute': true,
      'gr-diff gr-syntax gr-syntax-built_in': true,
      'gr-diff gr-syntax gr-syntax-comment': true,
      'gr-diff gr-syntax gr-syntax-keyword': true,
      'gr-diff gr-syntax gr-syntax-link': true,
      'gr-diff gr-syntax gr-syntax-literal': true,
      'gr-diff gr-syntax gr-syntax-meta': true,
      'gr-diff gr-syntax gr-syntax-meta-keyword': true,
      'gr-diff gr-syntax gr-syntax-name': true,
      'gr-diff gr-syntax gr-syntax-number': true,
      'gr-diff gr-syntax gr-syntax-regexp': true,
      'gr-diff gr-syntax gr-syntax-selector-attr': true,
      'gr-diff gr-syntax gr-syntax-selector-class': true,
      'gr-diff gr-syntax gr-syntax-selector-id': true,
      'gr-diff gr-syntax gr-syntax-selector-pseudo': true,
      'gr-diff gr-syntax gr-syntax-selector-tag': true,
      'gr-diff gr-syntax gr-syntax-string': true,
      'gr-diff gr-syntax gr-syntax-tag': true,
      'gr-diff gr-syntax gr-syntax-template-tag': true,
      'gr-diff gr-syntax gr-syntax-template-variable': true,
      'gr-diff gr-syntax gr-syntax-title': true,
      'gr-diff gr-syntax gr-syntax-type': true,
      'gr-diff gr-syntax gr-syntax-variable': true,
    };
  
    const CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</;
    const CPP_WCHAR_PATTERN = /L\'(\\)?.\'/g;
    const JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g;
    const GO_BACKSLASH_LITERAL = '\'\\\\\'';
    const GLOBAL_LT_PATTERN = /</g;
  
    Polymer({
      is: 'gr-syntax-layer',
  
      properties: {
        diff: {
          type: Object,
          observer: '_diffChanged',
        },
        enabled: {
          type: Boolean,
          value: true,
        },
        _baseRanges: {
          type: Array,
          value() { return []; },
        },
        _revisionRanges: {
          type: Array,
          value() { return []; },
        },
        _baseLanguage: String,
        _revisionLanguage: String,
        _listeners: {
          type: Array,
          value() { return []; },
        },
        /** @type {?number} */
        _processHandle: Number,
        _hljs: Object,
      },
  
      addListener(fn) {
        this.push('_listeners', fn);
      },
  
      /**
       * Annotation layer method to add syntax annotations to the given element
       * for the given line.
       * @param {!HTMLElement} el
       * @param {!HTMLElement} lineNumberEl
       * @param {!Object} line (GrDiffLine)
       */
      annotate(el, lineNumberEl, line) {
        if (!this.enabled) { return; }
  
        // Determine the side.
        let side;
        if (line.type === GrDiffLine.Type.REMOVE || (
            line.type === GrDiffLine.Type.BOTH &&
            el.getAttribute('data-side') !== 'right')) {
          side = 'left';
        } else if (line.type === GrDiffLine.Type.ADD || (
            el.getAttribute('data-side') !== 'left')) {
          side = 'right';
        }
  
        // Find the relevant syntax ranges, if any.
        let ranges = [];
        if (side === 'left' && this._baseRanges.length >= line.beforeNumber) {
          ranges = this._baseRanges[line.beforeNumber - 1] || [];
        } else if (side === 'right' &&
            this._revisionRanges.length >= line.afterNumber) {
          ranges = this._revisionRanges[line.afterNumber - 1] || [];
        }
  
        // Apply the ranges to the element.
        for (const range of ranges) {
          GrAnnotation.annotateElement(
              el, range.start, range.length, range.className);
        }
      },
  
      _getLanguage(diffFileMetaInfo) {
        // The Gerrit API provides only content-type, but for other users of
        // gr-diff it may be more convenient to specify the language directly.
        return diffFileMetaInfo.language ||
            LANGUAGE_MAP[diffFileMetaInfo.content_type];
      },
  
      /**
       * Start processing symtax for the loaded diff and notify layer listeners
       * as syntax info comes online.
       * @return {Promise}
       */
      process() {
        // Discard existing ranges.
        this._baseRanges = [];
        this._revisionRanges = [];
  
        if (!this.enabled || !this.diff.content.length) {
          return Promise.resolve();
        }
  
        this.cancel();
  
        if (this.diff.meta_a) {
          this._baseLanguage = this._getLanguage(this.diff.meta_a);
        }
        if (this.diff.meta_b) {
          this._revisionLanguage = this._getLanguage(this.diff.meta_b);
        }
        if (!this._baseLanguage && !this._revisionLanguage) {
          return Promise.resolve();
        }
  
        const state = {
          sectionIndex: 0,
          lineIndex: 0,
          baseContext: undefined,
          revisionContext: undefined,
          lineNums: {left: 1, right: 1},
          lastNotify: {left: 1, right: 1},
        };
  
        return this._loadHLJS().then(() => {
          return new Promise(resolve => {
            const nextStep = () => {
              this._processHandle = null;
              this._processNextLine(state);
  
              // Move to the next line in the section.
              state.lineIndex++;
  
              // If the section has been exhausted, move to the next one.
              if (this._isSectionDone(state)) {
                state.lineIndex = 0;
                state.sectionIndex++;
              }
  
              // If all sections have been exhausted, finish.
              if (state.sectionIndex >= this.diff.content.length) {
                resolve();
                this._notify(state);
                return;
              }
  
              if (state.lineIndex % 100 === 0) {
                this._notify(state);
                this._processHandle = this.async(nextStep, ASYNC_DELAY);
              } else {
                nextStep.call(this);
              }
            };
  
            this._processHandle = this.async(nextStep, 1);
          });
        });
      },
  
      /**
       * Cancel any asynchronous syntax processing jobs.
       */
      cancel() {
        if (this._processHandle) {
          this.cancelAsync(this._processHandle);
          this._processHandle = null;
        }
      },
  
      _diffChanged() {
        this.cancel();
        this._baseRanges = [];
        this._revisionRanges = [];
      },
  
      /**
       * Take a string of HTML with the (potentially nested) syntax markers
       * Highlight.js emits and emit a list of text ranges and classes for the
       * markers.
       * @param {string} str The string of HTML.
       * @return {!Array<!Object>} The list of ranges.
       */
      _rangesFromString(str) {
        const div = document.createElement('div');
        div.innerHTML = str;
        return this._rangesFromElement(div, 0);
      },
  
      _rangesFromElement(elem, offset) {
        let result = [];
        for (const node of elem.childNodes) {
          const nodeLength = GrAnnotation.getLength(node);
          // Note: HLJS may emit a span with class undefined when it thinks there
          // may be a syntax error.
          if (node.tagName === 'SPAN' && node.className !== 'undefined') {
            if (CLASS_WHITELIST.hasOwnProperty(node.className)) {
              result.push({
                start: offset,
                length: nodeLength,
                className: node.className,
              });
            }
            if (node.children.length) {
              result = result.concat(this._rangesFromElement(node, offset));
            }
          }
          offset += nodeLength;
        }
        return result;
      },
  
      /**
       * For a given state, process the syntax for the next line (or pair of
       * lines).
       * @param {!Object} state The processing state for the layer.
       */
      _processNextLine(state) {
        let baseLine;
        let revisionLine;
  
        const section = this.diff.content[state.sectionIndex];
        if (section.ab) {
          baseLine = section.ab[state.lineIndex];
          revisionLine = section.ab[state.lineIndex];
          state.lineNums.left++;
          state.lineNums.right++;
        } else {
          if (section.a && section.a.length > state.lineIndex) {
            baseLine = section.a[state.lineIndex];
            state.lineNums.left++;
          }
          if (section.b && section.b.length > state.lineIndex) {
            revisionLine = section.b[state.lineIndex];
            state.lineNums.right++;
          }
        }
  
        // To store the result of the syntax highlighter.
        let result;
  
        if (this._baseLanguage && baseLine !== undefined &&
            this._hljs.getLanguage(this._baseLanguage)) {
          baseLine = this._workaround(this._baseLanguage, baseLine);
          result = this._hljs.highlight(this._baseLanguage, baseLine, true,
              state.baseContext);
          this.push('_baseRanges', this._rangesFromString(result.value));
          state.baseContext = result.top;
        }
  
        if (this._revisionLanguage && revisionLine !== undefined &&
            this._hljs.getLanguage(this._revisionLanguage)) {
          revisionLine = this._workaround(this._revisionLanguage, revisionLine);
          result = this._hljs.highlight(this._revisionLanguage, revisionLine,
              true, state.revisionContext);
          this.push('_revisionRanges', this._rangesFromString(result.value));
          state.revisionContext = result.top;
        }
      },
  
      /**
       * Ad hoc fixes for HLJS parsing bugs. Rewrite lines of code in constrained
       * cases before sending them into HLJS so that they parse correctly.
       *
       * Important notes:
       * * These tests should be as constrained as possible to avoid interfering
       *   with code it shouldn't AND to avoid executing regexes as much as
       *   possible.
       * * These tests should document the issue clearly enough that the test can
       *   be condidently removed when the issue is solved in HLJS.
       * * These tests should rewrite the line of code to have the same number of
       *   characters. This method rewrites the string that gets parsed, but NOT
       *   the string that gets displayed and highlighted. Thus, the positions
       *   must be consistent.
       *
       * @param {!string} language The name of the HLJS language plugin in use.
       * @param {!string} line The line of code to potentially rewrite.
       * @return {string} A potentially-rewritten line of code.
       */
      _workaround(language, line) {
        if (language === 'cpp') {
          /**
           * Prevent confusing < and << operators for the start of a meta string
           * by converting them to a different operator.
           * {@see Issue 4864}
           * {@see https://github.com/isagalaev/highlight.js/issues/1341}
           */
          if (CPP_DIRECTIVE_WITH_LT_PATTERN.test(line)) {
            line = line.replace(GLOBAL_LT_PATTERN, '|');
          }
  
          /**
           * Rewrite CPP wchar_t characters literals to wchar_t string literals
           * because HLJS only understands the string form.
           * {@see Issue 5242}
           * {#see https://github.com/isagalaev/highlight.js/issues/1412}
           */
          if (CPP_WCHAR_PATTERN.test(line)) {
            line = line.replace(CPP_WCHAR_PATTERN, 'L"$1."');
          }
  
          return line;
        }
  
        /**
         * Prevent confusing the closing paren of a parameterized Java annotation
         * being applied to a formal argument as the closing paren of the argument
         * list. Rewrite the parens as spaces.
         * {@see Issue 4776}
         * {@see https://github.com/isagalaev/highlight.js/issues/1324}
         */
        if (language === 'java' && JAVA_PARAM_ANNOT_PATTERN.test(line)) {
          return line.replace(JAVA_PARAM_ANNOT_PATTERN, '$1 $2 ');
        }
  
        /**
         * HLJS misunderstands backslash character literals in Go.
         * {@see Issue 5007}
         * {#see https://github.com/isagalaev/highlight.js/issues/1411}
         */
        if (language === 'go' && line.includes(GO_BACKSLASH_LITERAL)) {
          return line.replace(GO_BACKSLASH_LITERAL, '"\\\\"');
        }
  
        return line;
      },
  
      /**
       * Tells whether the state has exhausted its current section.
       * @param {!Object} state
       * @return {boolean}
       */
      _isSectionDone(state) {
        const section = this.diff.content[state.sectionIndex];
        if (section.ab) {
          return state.lineIndex >= section.ab.length;
        } else {
          return (!section.a || state.lineIndex >= section.a.length) &&
              (!section.b || state.lineIndex >= section.b.length);
        }
      },
  
      /**
       * For a given state, notify layer listeners of any processed line ranges
       * that have not yet been notified.
       * @param {!Object} state
       */
      _notify(state) {
        if (state.lineNums.left - state.lastNotify.left) {
          this._notifyRange(
              state.lastNotify.left,
              state.lineNums.left,
              'left');
          state.lastNotify.left = state.lineNums.left;
        }
        if (state.lineNums.right - state.lastNotify.right) {
          this._notifyRange(
              state.lastNotify.right,
              state.lineNums.right,
              'right');
          state.lastNotify.right = state.lineNums.right;
        }
      },
  
      _notifyRange(start, end, side) {
        for (const fn of this._listeners) {
          fn(start, end, side);
        }
      },
  
      _loadHLJS() {
        return this.$.libLoader.getHLJS().then(hljs => {
          this._hljs = hljs;
        });
      },
    });
  })();
  