chromium/chrome/browser/resources/extensions/code_section.ts

// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'chrome://resources/cr_elements/cr_hidden_style.css.js';
import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
import './strings.m.js';
import './shared_vars.css.js';

import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {getTemplate} from './code_section.html.js';


function visibleLineCount(totalCount: number, oppositeCount: number): number {
  // We limit the number of lines shown for DOM performance.
  const MAX_VISIBLE_LINES = 1000;
  const max =
      Math.max(MAX_VISIBLE_LINES / 2, MAX_VISIBLE_LINES - oppositeCount);
  return Math.min(max, totalCount);
}

const ExtensionsCodeSectionElementBase = I18nMixin(PolymerElement);

export class ExtensionsCodeSectionElement extends
    ExtensionsCodeSectionElementBase {
  static get is() {
    return 'extensions-code-section';
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      code: {
        type: Object,
        value: null,
      },

      isActive: Boolean,

      /** Highlighted code. */
      highlighted_: String,

      /** Code before the highlighted section. */
      before_: String,

      /** Code after the highlighted section. */
      after_: String,

      showNoCode_: {
        type: Boolean,
        computed: 'computeShowNoCode_(isActive, highlighted_)',
      },

      /** Description for the highlighted section. */
      highlightDescription_: String,

      lineNumbers_: String,
      truncatedBefore_: Number,
      truncatedAfter_: Number,

      /**
       * The string to display if no |code| is set (e.g. because we couldn't
       * load the relevant source file).
       */
      couldNotDisplayCode: String,
    };
  }

  code: chrome.developerPrivate.RequestFileSourceResponse|null;
  isActive: boolean;
  couldNotDisplayCode: string;
  private highlighted_: string;
  private before_: string;
  private after_: string;
  private showNoCode_: boolean;
  private highlightDescription_: string;
  private lineNumbers_: string;
  private truncatedBefore_: number;
  private truncatedAfter_: number;

  static get observers() {
    return ['onCodeChanged_(code.*)'];
  }

  private onCodeChanged_() {
    if (!this.code ||
        (!this.code.beforeHighlight && !this.code.highlight &&
         !this.code.afterHighlight)) {
      this.highlighted_ = '';
      this.highlightDescription_ = '';
      this.before_ = '';
      this.after_ = '';
      this.lineNumbers_ = '';
      return;
    }

    const before = this.code.beforeHighlight;
    const highlight = this.code.highlight;
    const after = this.code.afterHighlight;

    const linesBefore = before ? before.split('\n') : [];
    const linesAfter = after ? after.split('\n') : [];
    const visibleLineCountBefore =
        visibleLineCount(linesBefore.length, linesAfter.length);
    const visibleLineCountAfter =
        visibleLineCount(linesAfter.length, linesBefore.length);

    const visibleBefore =
        linesBefore.slice(linesBefore.length - visibleLineCountBefore)
            .join('\n');
    let visibleAfter = linesAfter.slice(0, visibleLineCountAfter).join('\n');
    // If the last character is a \n, force it to be rendered.
    if (visibleAfter.charAt(visibleAfter.length - 1) === '\n') {
      visibleAfter += ' ';
    }

    this.highlighted_ = highlight;
    this.highlightDescription_ = this.getAccessibilityHighlightDescription_(
        linesBefore.length, highlight.split('\n').length);
    this.before_ = visibleBefore;
    this.after_ = visibleAfter;
    this.truncatedBefore_ = linesBefore.length - visibleLineCountBefore;
    this.truncatedAfter_ = linesAfter.length - visibleLineCountAfter;

    const visibleCode = visibleBefore + highlight + visibleAfter;

    this.setLineNumbers_(
        this.truncatedBefore_ + 1,
        this.truncatedBefore_ + visibleCode.split('\n').length);
    this.scrollToHighlight_(visibleLineCountBefore);
  }

  private getLinesNotShownLabel_(
      lineCount: number, stringSingular: string,
      stringPluralTemplate: string): string {
    return lineCount === 1 ?
        stringSingular :
        loadTimeData.substituteString(stringPluralTemplate, lineCount);
  }

  private setLineNumbers_(start: number, end: number) {
    let lineNumbers = '';
    for (let i = start; i <= end; ++i) {
      lineNumbers += i + '\n';
    }

    this.lineNumbers_ = lineNumbers;
  }

  private scrollToHighlight_(linesBeforeHighlight: number) {
    const CSS_LINE_HEIGHT = 20;

    // Count how many pixels is above the highlighted code.
    const highlightTop = linesBeforeHighlight * CSS_LINE_HEIGHT;

    // Find the position to show the highlight roughly in the middle.
    const targetTop = highlightTop - this.clientHeight * 0.5;

    this.$['scroll-container'].scrollTo({top: targetTop});
  }

  private getAccessibilityHighlightDescription_(
      lineStart: number, numLines: number): string {
    if (numLines > 1) {
      return this.i18n(
          'accessibilityErrorMultiLine', lineStart.toString(),
          (lineStart + numLines - 1).toString());
    } else {
      return this.i18n('accessibilityErrorLine', lineStart.toString());
    }
  }

  private computeShowNoCode_(): boolean {
    return this.isActive && !this.highlighted_;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'extensions-code-section': ExtensionsCodeSectionElement;
  }
}


customElements.define(
    ExtensionsCodeSectionElement.is, ExtensionsCodeSectionElement);