Convert gr-formatted-text to TypeScript

Change-Id: I65dffda16b0f04ae5432e8437cb800ac6ca0a8c8
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index 7170079..e642373 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -160,3 +160,40 @@
   LEFT = 'left',
   RIGHT = 'right',
 }
+
+/**
+ * The type in ConfigParameterInfo entity.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#config-parameter-info
+ */
+export enum ConfigParameterInfoType {
+  STRING = 'STRING',
+  INT = 'INT',
+  LONG = 'LONG',
+  BOOLEAN = 'BOOLEAN',
+  LIST = 'LIST',
+  ARRAY = 'ARRAY',
+}
+
+/**
+ * All supported submit types.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#submit-type-info
+ */
+export enum SubmitType {
+  MERGE_IF_NECESSARY = 'MERGE_IF_NECESSARY',
+  FAST_FORWARD_ONLY = 'FAST_FORWARD_ONLY',
+  REBASE_IF_NECESSARY = 'REBASE_IF_NECESSARY',
+  REBASE_ALWAYS = 'REBASE_ALWAYS',
+  MERGE_ALWAYS = 'MERGE_ALWAYS ',
+  CHERRY_PICK = 'CHERRY_PICK',
+  INHERIT = 'INHERIT',
+}
+
+/*
+ * Enum for possible configured value in InheritedBooleanInfo.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#inherited-boolean-info
+ */
+export enum InheritedBooleanInfoConfiguredValue {
+  TRUE = 'TRUE',
+  FALSE = 'FALSE',
+  INHERITED = 'INHERITED',
+}
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
index 08c7897..b55c9a9 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.ts
@@ -30,6 +30,7 @@
   CommitId,
   Hashtag,
   UrlEncodedCommentId,
+  CommentLinks,
 } from '../../../types/common';
 
 // Navigation parameters object format:
@@ -117,7 +118,7 @@
 
 const uninitializedMapCommentLinks: MapCommentLinksCallback = () => {
   uninitialized();
-  return [];
+  return {};
 };
 
 // TODO(TS): PatchSetNum type express an API type, it is not good to add
@@ -345,15 +346,7 @@
   params: GenerateWebLinksParameters
 ) => WebLink[] | WebLink;
 
-export interface Pattern {
-  enabled: boolean | null;
-  match: string;
-  html?: string;
-  link?: string;
-}
-
-// TODO(TS): type is not clear until more code converted to a typescript.
-export type MapCommentLinksCallback = (patterns: Pattern[]) => Pattern[];
+export type MapCommentLinksCallback = (patterns: CommentLinks) => CommentLinks;
 
 export interface WebLink {
   label: string;
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
index 862c48c..f8d414e 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -14,44 +14,55 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../gr-linked-text/gr-linked-text.js';
-import '../../../styles/shared-styles.js';
-import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-formatted-text_html.js';
+import '../gr-linked-text/gr-linked-text';
+import '../../../styles/shared-styles';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
+import {PolymerElement} from '@polymer/polymer/polymer-element';
+import {customElement, property} from '@polymer/decorators/lib/decorators';
+import {htmlTemplate} from './gr-formatted-text_html';
+import {CommentLinks} from '../../../types/common';
 
-// eslint-disable-next-line no-unused-vars
-const QUOTE_MARKER_PATTERN = /\n\s?>\s/g;
 const CODE_MARKER_PATTERN = /^(`{1,3})([^`]+?)\1$/;
 
-/** @extends PolymerElement */
-class GrFormattedText extends GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement)) {
-  static get template() { return htmlTemplate; }
+interface Block {
+  type: string;
+  text?: string;
+  blocks?: Block[];
+  items?: string[];
+}
 
-  static get is() { return 'gr-formatted-text'; }
+declare global {
+  interface HTMLElementTagNameMap {
+    'gr-formatted-text': GrFormattedText;
+  }
+}
 
-  static get properties() {
-    return {
-      content: {
-        type: String,
-        observer: '_contentChanged',
-      },
-      config: Object,
-      noTrailingMargin: {
-        type: Boolean,
-        value: false,
-      },
-    };
+export interface GrFormattedText {
+  $: {
+    container: HTMLElement;
+  };
+}
+
+@customElement('gr-formatted-text')
+export class GrFormattedText extends GestureEventListeners(
+  LegacyElementMixin(PolymerElement)
+) {
+  static get template() {
+    return htmlTemplate;
   }
 
+  @property({type: String, observer: '_contentChanged'})
+  content?: string;
+
+  @property({type: Object})
+  config?: CommentLinks;
+
+  @property({type: Boolean})
+  noTrailingMargin = false;
+
   static get observers() {
-    return [
-      '_contentOrConfigChanged(content, config)',
-    ];
+    return ['_contentOrConfigChanged(content, config)'];
   }
 
   /** @override */
@@ -62,19 +73,19 @@
     }
   }
 
-  _contentChanged(content) {
+  _contentChanged(content: string) {
     // In the case where the config may not be set (perhaps due to the
     // request for it still being in flight), set the content anyway to
     // prevent waiting on the config to display the text.
-    if (this.config) { return; }
+    if (this.config) return;
     this._contentOrConfigChanged(content);
   }
 
   /**
    * Given a source string, update the DOM inside #container.
    */
-  _contentOrConfigChanged(content) {
-    const container = dom(this.$.container);
+  _contentOrConfigChanged(content?: string) {
+    const container = this.$.container;
 
     // Remove existing content.
     while (container.firstChild) {
@@ -106,12 +117,9 @@
    * list of blocks contained in the quote.
    *
    * NOTE: Strings appearing in all block objects are NOT escaped.
-   *
-   * @param {string} content
-   * @return {!Array<!Object>}
    */
-  _computeBlocks(content) {
-    if (!content) { return []; }
+  _computeBlocks(content?: string): Block[] {
+    if (!content) return [];
 
     const result = [];
     const lines = content.replace(/[\s\n\r\t]+$/g, '').split('\n');
@@ -123,7 +131,7 @@
 
       if (this._isCodeMarkLine(lines[i])) {
         // handle multi-line code
-        let nextI = i+1;
+        let nextI = i + 1;
         while (!this._isCodeMarkLine(lines[nextI]) && nextI < lines.length) {
           nextI++;
         }
@@ -131,7 +139,7 @@
         if (this._isCodeMarkLine(lines[nextI])) {
           result.push({
             type: 'code',
-            text: lines.slice(i+1, nextI).join('\n'),
+            text: lines.slice(i + 1, nextI).join('\n'),
           });
           i = nextI;
           continue;
@@ -143,7 +151,7 @@
 
       if (this._isSingleLineCode(lines[i])) {
         // no guard check as _isSingleLineCode tested on the pattern
-        const codeContent = lines[i].match(CODE_MARKER_PATTERN)[2];
+        const codeContent = lines[i].match(CODE_MARKER_PATTERN)![2];
         result.push({type: 'code', text: codeContent});
       } else if (this._isList(lines[i])) {
         let nextI = i + 1;
@@ -157,8 +165,9 @@
         while (this._isQuote(lines[nextI])) {
           nextI++;
         }
-        const blockLines = lines.slice(i, nextI)
-            .map(l => l.replace(/^[ ]?>[ ]?/, ''));
+        const blockLines = lines
+          .slice(i, nextI)
+          .map(l => l.replace(/^[ ]?>[ ]?/, ''));
         result.push({
           type: 'quote',
           blocks: this._computeBlocks(blockLines.join('\n')),
@@ -167,8 +176,10 @@
       } else if (this._isPreFormat(lines[i])) {
         let nextI = i + 1;
         // include pre or all regular lines but stop at next new line
-        while (this._isPreFormat(lines[nextI])
-         || (this._isRegularLine(lines[nextI]) && lines[nextI].length)) {
+        while (
+          this._isPreFormat(lines[nextI]) ||
+          (this._isRegularLine(lines[nextI]) && lines[nextI].length)
+        ) {
           nextI++;
         }
         result.push({
@@ -202,58 +213,56 @@
    *
    * TODO(taoalpha): maybe we should also support nested list
    *
-   * @param {!Array<string>} lines The block containing the list.
+   * @param lines The block containing the list.
    */
-  _makeList(lines) {
-    const block = {type: 'list', items: []};
-    let line;
-
+  _makeList(lines: string[]) {
+    const items = [];
     for (let i = 0; i < lines.length; i++) {
-      line = lines[i];
+      let line = lines[i];
       line = line.substring(1).trim();
-      block.items.push(line);
+      items.push(line);
     }
-    return block;
+    return {type: 'list', items};
   }
 
-  _isRegularLine(line) {
+  _isRegularLine(line: string) {
     // line can not be recognized by existing patterns
     if (line === undefined) return false;
-    return !this._isQuote(line) && !this._isCodeMarkLine(line)
-    && !this._isSingleLineCode(line) && !this._isList(line) &&
-    !this._isPreFormat(line);
+    return (
+      !this._isQuote(line) &&
+      !this._isCodeMarkLine(line) &&
+      !this._isSingleLineCode(line) &&
+      !this._isList(line) &&
+      !this._isPreFormat(line)
+    );
   }
 
-  _isQuote(line) {
+  _isQuote(line: string) {
     return line && (line.startsWith('> ') || line.startsWith(' > '));
   }
 
-  _isCodeMarkLine(line) {
+  _isCodeMarkLine(line: string) {
     return line && line.trim() === '```';
   }
 
-  _isSingleLineCode(line) {
+  _isSingleLineCode(line: string) {
     return line && CODE_MARKER_PATTERN.test(line);
   }
 
-  _isPreFormat(line) {
+  _isPreFormat(line: string) {
     return line && /^[ \t]/.test(line);
   }
 
-  _isList(line) {
+  _isList(line: string) {
     return line && /^[-*] /.test(line);
   }
 
-  /**
-   * @param {string} content
-   * @param {boolean=} opt_isPre
-   */
-  _makeLinkedText(content, opt_isPre) {
+  _makeLinkedText(content = '', isPre?: boolean) {
     const text = document.createElement('gr-linked-text');
     text.config = this.config;
     text.content = content;
     text.pre = true;
-    if (opt_isPre) {
+    if (isPre) {
       text.classList.add('pre');
     }
     return text;
@@ -261,11 +270,8 @@
 
   /**
    * Map an array of block objects to an array of DOM nodes.
-   *
-   * @param  {!Array<!Object>} blocks
-   * @return {!Array<!HTMLElement>}
    */
-  _computeNodes(blocks) {
+  _computeNodes(blocks: Block[]): HTMLElement[] {
     return blocks.map(block => {
       if (block.type === 'paragraph') {
         const p = document.createElement('p');
@@ -275,7 +281,7 @@
 
       if (block.type === 'quote') {
         const bq = document.createElement('blockquote');
-        for (const node of this._computeNodes(block.blocks)) {
+        for (const node of this._computeNodes(block.blocks || [])) {
           if (node) bq.appendChild(node);
         }
         return bq;
@@ -283,7 +289,7 @@
 
       if (block.type === 'code') {
         const code = document.createElement('code');
-        code.textContent = block.text;
+        code.textContent = block.text || '';
         return code;
       }
 
@@ -293,7 +299,8 @@
 
       if (block.type === 'list') {
         const ul = document.createElement('ul');
-        for (const item of block.items) {
+        const items = block.items || [];
+        for (const item of items) {
           const li = document.createElement('li');
           li.appendChild(this._makeLinkedText(item));
           ul.appendChild(li);
@@ -302,9 +309,7 @@
       }
 
       console.warn('Unrecognized type.');
-      return;
+      return document.createElement('span');
     });
   }
 }
-
-customElements.define(GrFormattedText.is, GrFormattedText);
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.ts b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.ts
index 1bc19cd..ec6b6ab 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.ts
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/link-text-parser.ts
@@ -16,7 +16,7 @@
  */
 
 import {getBaseUrl} from '../../../utils/url-util';
-import {Pattern} from '../../core/gr-navigation/gr-navigation';
+import {CommentLinkInfo} from '../../../types/common';
 
 /**
  * Pattern describing URLs with supported protocols.
@@ -32,7 +32,7 @@
   html: HTMLAnchorElement | DocumentFragment;
 }
 
-export type LinkTextParserConfig = Pattern[];
+export type LinkTextParserConfig = {[name: string]: CommentLinkInfo};
 
 export class GrLinkTextParser {
   private readonly baseUrl = getBaseUrl();
@@ -325,7 +325,7 @@
           // We suppose, that prefixText just a single word
           // before link and add this word as is, without processing
           // any patterns in it.
-          this.parseLinks(prefixText, []);
+          this.parseLinks(prefixText, {});
           text = text.substring(prefixText.length);
           href = href.substring(prefixText.length);
         }
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 0a6885a..21e3600 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -25,6 +25,9 @@
   RequirementStatus,
   ReviewerState,
   RevisionKind,
+  SubmitType,
+  InheritedBooleanInfoConfiguredValue,
+  ConfigParameterInfoType,
 } from '../constants/constants';
 
 export type BrandType<T, BrandName extends string> = T &
@@ -79,7 +82,6 @@
 export type ChangeInfoId = BrandType<string, '_changeInfoId'>;
 export type Hashtag = BrandType<string, '_hashtag'>;
 export type StarLabel = BrandType<string, '_startLabel'>;
-export type SubmitType = BrandType<string, '_submitType'>;
 export type CommitId = BrandType<string, '_commitId'>;
 
 // The UUID of the group
@@ -1110,3 +1112,96 @@
   _width: number;
   _height: number;
 }
+
+/**
+ * A boolean value that can also be inherited.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#inherited-boolean-info
+ */
+export interface InheritedBooleanInfo {
+  value: string;
+  configured_value: InheritedBooleanInfoConfiguredValue;
+  inherited_value?: string;
+}
+
+/**
+ * The MaxObjectSizeLimitInfo entity contains information about themax object
+ * size limit of a project.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#max-object-size-limit-info
+ */
+export interface MaxObjectSizeLimitInfo {
+  value?: number;
+  configured_value?: string;
+  summary?: string;
+}
+
+/**
+ * Information about the default submittype of a project, taking into account
+ * project inheritance.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#submit-type-info
+ */
+export interface SubmitTypeInfo {
+  value: Exclude<SubmitType, SubmitType.INHERIT>;
+  configured_value: SubmitType;
+  inherited_value: Exclude<SubmitType, SubmitType.INHERIT>;
+}
+
+/**
+ * The CommentLinkInfo entity describes acommentlink.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#commentlink-info
+ */
+export interface CommentLinkInfo {
+  match: string;
+  link?: string;
+  enabled?: boolean;
+  html?: string;
+}
+
+/**
+ * The ConfigParameterInfo entity describes a project configurationparameter.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#config-parameter-info
+ */
+export interface ConfigParameterInfo {
+  display_name?: string;
+  description?: string;
+  warning?: string;
+  type: ConfigParameterInfoType;
+  value?: string;
+  values?: string[];
+  editable?: boolean;
+  permitted_values?: string[];
+  inheritable?: boolean;
+  configured_value?: string;
+  inherited_value?: string;
+}
+
+export interface CommentLinks {
+  [name: string]: CommentLinkInfo;
+}
+
+/**
+ * The ConfigInfo entity contains information about the effective
+ * projectconfiguration.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#config-info
+ */
+export interface ConfigInfo {
+  description?: string;
+  use_contributor_agreements?: InheritedBooleanInfo;
+  use_content_merge?: InheritedBooleanInfo;
+  use_signed_off_by?: InheritedBooleanInfo;
+  create_new_change_for_all_not_in_target?: InheritedBooleanInfo;
+  require_change_id?: InheritedBooleanInfo;
+  enable_signed_push?: InheritedBooleanInfo;
+  require_signed_push?: InheritedBooleanInfo;
+  reject_implicit_merges?: InheritedBooleanInfo;
+  private_by_default: InheritedBooleanInfo;
+  work_in_progress_by_default: InheritedBooleanInfo;
+  max_object_size_limit: MaxObjectSizeLimitInfo;
+  default_submit_type: SubmitTypeInfo;
+  submit_type: SubmitType;
+  match_author_to_committer_date?: InheritedBooleanInfo;
+  state?: ProjectState;
+  commentlinks: CommentLinks;
+  plugin_config?: ConfigParameterInfo;
+  actions?: {[viewName: string]: ActionInfo};
+  reject_empty_commit?: InheritedBooleanInfo;
+}