text: /**
   * @license Copyright 2019 Google Inc. All Rights Reserved.
   * 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.
   */
  'use strict';
  
  /* globals self, Util */
  
  /** @typedef {import('./details-renderer')} DetailsRenderer */
  
  /** @enum {number} */
  const LineVisibility = {
    /** Show regardless of whether the snippet is collapsed or expanded */
    ALWAYS: 0,
    WHEN_COLLAPSED: 1,
    WHEN_EXPANDED: 2,
  };
  
  /** @enum {number} */
  const LineContentType = {
    /** A line of content */
    CONTENT_NORMAL: 0,
    /** A line of content that's emphasized by setting the CSS background color */
    CONTENT_HIGHLIGHTED: 1,
    /** Use when some lines are hidden, shows the "..." placeholder */
    PLACEHOLDER: 2,
    /** A message about a line of content or the snippet in general */
    MESSAGE: 3,
  };
  
  /** @typedef {{
      content: string;
      lineNumber: string | number;
      contentType: LineContentType;
      truncated?: boolean;
      visibility?: LineVisibility;
  }} LineDetails */
  
  const classNamesByContentType = {
    [LineContentType.CONTENT_NORMAL]: ['lh-snippet__line--content'],
    [LineContentType.CONTENT_HIGHLIGHTED]: [
      'lh-snippet__line--content',
      'lh-snippet__line--content-highlighted',
    ],
    [LineContentType.PLACEHOLDER]: ['lh-snippet__line--placeholder'],
    [LineContentType.MESSAGE]: ['lh-snippet__line--message'],
  };
  
  /**
   * @param {LH.Audit.Details.SnippetValue['lines']} lines
   * @param {number} lineNumber
   * @return {{line?: LH.Audit.Details.SnippetValue['lines'][0], previousLine?: LH.Audit.Details.SnippetValue['lines'][0]}}
   */
  function getLineAndPreviousLine(lines, lineNumber) {
    return {
      line: lines.find(l => l.lineNumber === lineNumber),
      previousLine: lines.find(l => l.lineNumber === lineNumber - 1),
    };
  }
  
  /**
   * @param {LH.Audit.Details.SnippetValue["lineMessages"]} messages
   * @param {number} lineNumber
   */
  function getMessagesForLineNumber(messages, lineNumber) {
    return messages.filter(h => h.lineNumber === lineNumber);
  }
  
  /**
   * @param {LH.Audit.Details.SnippetValue} details
   * @return {LH.Audit.Details.SnippetValue['lines']}
   */
  function getLinesWhenCollapsed(details) {
    const SURROUNDING_LINES_TO_SHOW_WHEN_COLLAPSED = 2;
    return Util.filterRelevantLines(
      details.lines,
      details.lineMessages,
      SURROUNDING_LINES_TO_SHOW_WHEN_COLLAPSED
    );
  }
  
  /**
   * Render snippet of text with line numbers and annotations.
   * By default we only show a few lines around each annotation and the user
   * can click "Expand snippet" to show more.
   * Content lines with annotations are highlighted.
   */
  class SnippetRenderer {
    /**
     * @param {DOM} dom
     * @param {DocumentFragment} tmpl
     * @param {LH.Audit.Details.SnippetValue} details
     * @param {DetailsRenderer} detailsRenderer
     * @param {function} toggleExpandedFn
     * @return {DocumentFragment}
     */
    static renderHeader(dom, tmpl, details, detailsRenderer, toggleExpandedFn) {
      const linesWhenCollapsed = getLinesWhenCollapsed(details);
      const canExpand = linesWhenCollapsed.length < details.lines.length;
  
      const header = dom.cloneTemplate('#tmpl-lh-snippet__header', tmpl);
      dom.find('.lh-snippet__title', header).textContent = details.title;
  
      const {
        snippetCollapseButtonLabel,
        snippetExpandButtonLabel,
      } = Util.UIStrings;
      dom.find(
        '.lh-snippet__btn-label-collapse',
        header
      ).textContent = snippetCollapseButtonLabel;
      dom.find(
        '.lh-snippet__btn-label-expand',
        header
      ).textContent = snippetExpandButtonLabel;
  
      const toggleExpandButton = dom.find('.lh-snippet__toggle-expand', header);
      // If we're already showing all the available lines of the snippet, we don't need an
      // expand/collapse button and can remove it from the DOM.
      // If we leave the button in though, wire up the click listener to toggle visibility!
      if (!canExpand) {
        toggleExpandButton.remove();
      } else {
        toggleExpandButton.addEventListener('click', () => toggleExpandedFn());
      }
  
      // We only show the source node of the snippet in DevTools because then the user can
      // access the full element detail. Just being able to see the outer HTML isn't very useful.
      if (details.node && dom.isDevTools()) {
        const nodeContainer = dom.find('.lh-snippet__node', header);
        nodeContainer.appendChild(detailsRenderer.renderNode(details.node));
      }
  
      return header;
    }
  
    /**
     * Renders a line (text content, message, or placeholder) as a DOM element.
     * @param {DOM} dom
     * @param {DocumentFragment} tmpl
     * @param {LineDetails} lineDetails
     * @return {Element}
     */
    static renderSnippetLine(
        dom,
        tmpl,
        {content, lineNumber, truncated, contentType, visibility}
    ) {
      const clonedTemplate = dom.cloneTemplate('#tmpl-lh-snippet__line', tmpl);
      const contentLine = dom.find('.lh-snippet__line', clonedTemplate);
      const {classList} = contentLine;
  
      classNamesByContentType[contentType].forEach(typeClass =>
        classList.add(typeClass)
      );
  
      if (visibility === LineVisibility.WHEN_COLLAPSED) {
        classList.add('lh-snippet__show-if-collapsed');
      } else if (visibility === LineVisibility.WHEN_EXPANDED) {
        classList.add('lh-snippet__show-if-expanded');
      }
  
      const lineContent = content + (truncated ? '…' : '');
      const lineContentEl = dom.find('.lh-snippet__line code', contentLine);
      if (contentType === LineContentType.MESSAGE) {
        lineContentEl.appendChild(dom.convertMarkdownLinkSnippets(lineContent));
      } else {
        lineContentEl.textContent = lineContent;
      }
  
      dom.find(
        '.lh-snippet__line-number',
        contentLine
      ).textContent = lineNumber.toString();
  
      return contentLine;
    }
  
    /**
     * @param {DOM} dom
     * @param {DocumentFragment} tmpl
     * @param {{message: string}} message
     * @return {Element}
     */
    static renderMessage(dom, tmpl, message) {
      return SnippetRenderer.renderSnippetLine(dom, tmpl, {
        lineNumber: ' ',
        content: message.message,
        contentType: LineContentType.MESSAGE,
      });
    }
  
    /**
     * @param {DOM} dom
     * @param {DocumentFragment} tmpl
     * @param {LineVisibility} visibility
     * @return {Element}
     */
    static renderOmittedLinesPlaceholder(dom, tmpl, visibility) {
      return SnippetRenderer.renderSnippetLine(dom, tmpl, {
        lineNumber: '…',
        content: '',
        visibility,
        contentType: LineContentType.PLACEHOLDER,
      });
    }
  
    /**
     * @param {DOM} dom
     * @param {DocumentFragment} tmpl
     * @param {LH.Audit.Details.SnippetValue} details
     * @return {DocumentFragment}
     */
    static renderSnippetContent(dom, tmpl, details) {
      const template = dom.cloneTemplate('#tmpl-lh-snippet__content', tmpl);
      const snippetEl = dom.find('.lh-snippet__snippet-inner', template);
  
      // First render messages that don't belong to specific lines
      details.generalMessages.forEach(m =>
        snippetEl.append(SnippetRenderer.renderMessage(dom, tmpl, m))
      );
      // Then render the lines and their messages, as well as placeholders where lines are omitted
      snippetEl.append(SnippetRenderer.renderSnippetLines(dom, tmpl, details));
  
      return template;
    }
  
    /**
     * @param {DOM} dom
     * @param {DocumentFragment} tmpl
     * @param {LH.Audit.Details.SnippetValue} details
     * @return {DocumentFragment}
     */
    static renderSnippetLines(dom, tmpl, details) {
      const {lineMessages, generalMessages, lineCount, lines} = details;
      const linesWhenCollapsed = getLinesWhenCollapsed(details);
      const hasOnlyGeneralMessages =
        generalMessages.length > 0 && lineMessages.length === 0;
  
      const lineContainer = dom.createFragment();
  
      // When a line is not shown in the collapsed state we try to see if we also need an
      // omitted lines placeholder for the expanded state, rather than rendering two separate
      // placeholders.
      let hasPendingOmittedLinesPlaceholderForCollapsedState = false;
  
      for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) {
        const {line, previousLine} = getLineAndPreviousLine(lines, lineNumber);
        const {
          line: lineWhenCollapsed,
          previousLine: previousLineWhenCollapsed,
        } = getLineAndPreviousLine(linesWhenCollapsed, lineNumber);
  
        const showLineWhenCollapsed = !!lineWhenCollapsed;
        const showPreviousLineWhenCollapsed = !!previousLineWhenCollapsed;
  
        // If we went from showing lines in the collapsed state to not showing them
        // we need to render a placeholder
        if (showPreviousLineWhenCollapsed && !showLineWhenCollapsed) {
          hasPendingOmittedLinesPlaceholderForCollapsedState = true;
        }
        // If we are back to lines being visible in the collapsed and the placeholder
        // hasn't been rendered yet then render it now
        if (
          showLineWhenCollapsed &&
          hasPendingOmittedLinesPlaceholderForCollapsedState
        ) {
          lineContainer.append(
            SnippetRenderer.renderOmittedLinesPlaceholder(
              dom,
              tmpl,
              LineVisibility.WHEN_COLLAPSED
            )
          );
          hasPendingOmittedLinesPlaceholderForCollapsedState = false;
        }
  
        // Render omitted lines placeholder if we have not already rendered one for this gap
        const isFirstOmittedLineWhenExpanded = !line && !!previousLine;
        const isFirstLineOverallAndIsOmittedWhenExpanded =
          !line && lineNumber === 1;
        if (
          isFirstOmittedLineWhenExpanded ||
          isFirstLineOverallAndIsOmittedWhenExpanded
        ) {
          // In the collapsed state we don't show omitted lines placeholders around
          // the edges of the snippet
          const hasRenderedAllLinesVisibleWhenCollapsed = !linesWhenCollapsed.some(
            l => l.lineNumber > lineNumber
          );
          const onlyShowWhenExpanded =
            hasRenderedAllLinesVisibleWhenCollapsed || lineNumber === 1;
          lineContainer.append(
            SnippetRenderer.renderOmittedLinesPlaceholder(
              dom,
              tmpl,
              onlyShowWhenExpanded
                ? LineVisibility.WHEN_EXPANDED
                : LineVisibility.ALWAYS
            )
          );
          hasPendingOmittedLinesPlaceholderForCollapsedState = false;
        }
  
        if (!line) {
          // Can't render the line if we don't know its content (instead we've rendered a placeholder)
          continue;
        }
  
        // Now render the line and any messages
        const messages = getMessagesForLineNumber(lineMessages, lineNumber);
        const highlightLine = messages.length > 0 || hasOnlyGeneralMessages;
        const contentLineDetails = Object.assign({}, line, {
          contentType: highlightLine
            ? LineContentType.CONTENT_HIGHLIGHTED
            : LineContentType.CONTENT_NORMAL,
          visibility: lineWhenCollapsed
            ? LineVisibility.ALWAYS
            : LineVisibility.WHEN_EXPANDED,
        });
        lineContainer.append(
          SnippetRenderer.renderSnippetLine(dom, tmpl, contentLineDetails)
        );
  
        messages.forEach(message => {
          lineContainer.append(SnippetRenderer.renderMessage(dom, tmpl, message));
        });
      }
  
      return lineContainer;
    }
  
    /**
     * @param {DOM} dom
     * @param {ParentNode} templateContext
     * @param {LH.Audit.Details.SnippetValue} details
     * @param {DetailsRenderer} detailsRenderer
     * @return {Element}
     */
    static render(dom, templateContext, details, detailsRenderer) {
      const tmpl = dom.cloneTemplate('#tmpl-lh-snippet', templateContext);
      const snippetEl = dom.find('.lh-snippet', tmpl);
  
      const header = SnippetRenderer.renderHeader(
        dom,
        tmpl,
        details,
        detailsRenderer,
        () => snippetEl.classList.toggle('lh-snippet--expanded')
      );
      const content = SnippetRenderer.renderSnippetContent(dom, tmpl, details);
      snippetEl.append(header, content);
  
      return snippetEl;
    }
  }
  
  // Allow Node require()'ing.
  if (typeof module !== 'undefined' && module.exports) {
    module.exports = SnippetRenderer;
  } else {
    self.SnippetRenderer = SnippetRenderer;
  }
  