| /** | 
 |  * @license | 
 |  * Copyright 2022 Google LLC | 
 |  * SPDX-License-Identifier: Apache-2.0 | 
 |  */ | 
 | import {LitElement, html, TemplateResult} from 'lit'; | 
 | import {property} from 'lit/decorators.js'; | 
 | import {styleMap} from 'lit/directives/style-map.js'; | 
 | import {diffClasses} from '../gr-diff/gr-diff-utils'; | 
 |  | 
 | const SURROGATE_PAIR = /[\uD800-\uDBFF][\uDC00-\uDFFF]/; | 
 |  | 
 | const TAB = '\t'; | 
 |  | 
 | /** | 
 |  * Renders one line of code on one side of the diff. It takes care of: | 
 |  * - Tabs, see `tabSize` property. | 
 |  * - Line Breaks, see `lineLimit` property. | 
 |  * - Surrogate Character Pairs. | 
 |  * | 
 |  * Note that other modifications to the code in a gr-diff is done via diff | 
 |  * layers, which manipulate the DOM directly. So `gr-diff-text` is thrown | 
 |  * away and re-rendered every time something changes by its parent | 
 |  * `gr-diff-row`. So don't bother to optimize this component for re-rendering | 
 |  * performance. And be aware that building longer lived local state is not | 
 |  * useful here. | 
 |  */ | 
 | export class GrDiffText extends LitElement { | 
 |   /** | 
 |    * The browser API for handling selection does not (yet) work for selection | 
 |    * across multiple shadow DOM elements. So we are rendering gr-diff components | 
 |    * into the light DOM instead of the shadow DOM by overriding this method, | 
 |    * which was the recommended workaround by the lit team. | 
 |    * See also https://github.com/WICG/webcomponents/issues/79. | 
 |    */ | 
 |   override createRenderRoot() { | 
 |     return this; | 
 |   } | 
 |  | 
 |   @property({type: String}) | 
 |   text = ''; | 
 |  | 
 |   @property({type: Boolean}) | 
 |   isResponsive = false; | 
 |  | 
 |   @property({type: Number}) | 
 |   tabSize = 2; | 
 |  | 
 |   @property({type: Number}) | 
 |   lineLimit = 80; | 
 |  | 
 |   /** Temporary state while rendering. */ | 
 |   private textOffset = 0; | 
 |  | 
 |   /** Temporary state while rendering. */ | 
 |   private columnPos = 0; | 
 |  | 
 |   /** Temporary state while rendering. */ | 
 |   private pieces: (string | TemplateResult)[] = []; | 
 |  | 
 |   /** Split up the string into tabs, surrogate pairs and regular segments. */ | 
 |   override render() { | 
 |     this.textOffset = 0; | 
 |     this.columnPos = 0; | 
 |     this.pieces = []; | 
 |     const splitByTab = this.text.split('\t'); | 
 |     for (let i = 0; i < splitByTab.length; i++) { | 
 |       const splitBySurrogate = splitByTab[i].split(SURROGATE_PAIR); | 
 |       for (let j = 0; j < splitBySurrogate.length; j++) { | 
 |         this.renderSegment(splitBySurrogate[j]); | 
 |         if (j < splitBySurrogate.length - 1) { | 
 |           this.renderSurrogatePair(); | 
 |         } | 
 |       } | 
 |       if (i < splitByTab.length - 1) { | 
 |         this.renderTab(); | 
 |       } | 
 |     } | 
 |     if (this.textOffset !== this.text.length) throw new Error('unfinished'); | 
 |     return this.pieces; | 
 |   } | 
 |  | 
 |   /** Render regular characters, but insert line breaks appropriately. */ | 
 |   private renderSegment(segment: string) { | 
 |     let segmentOffset = 0; | 
 |     while (segmentOffset < segment.length) { | 
 |       const newOffset = Math.min( | 
 |         segment.length, | 
 |         segmentOffset + this.lineLimit - this.columnPos | 
 |       ); | 
 |       this.renderString(segment.substring(segmentOffset, newOffset)); | 
 |       segmentOffset = newOffset; | 
 |       if (segmentOffset < segment.length && this.columnPos === this.lineLimit) { | 
 |         this.renderLineBreak(); | 
 |       } | 
 |     } | 
 |   } | 
 |  | 
 |   /** Render regular characters. */ | 
 |   private renderString(s: string) { | 
 |     if (s.length === 0) return; | 
 |     this.pieces.push(s); | 
 |     this.textOffset += s.length; | 
 |     this.columnPos += s.length; | 
 |     if (this.columnPos > this.lineLimit) throw new Error('over line limit'); | 
 |   } | 
 |  | 
 |   /** Render a tab character. */ | 
 |   private renderTab() { | 
 |     let tabSize = this.tabSize - (this.columnPos % this.tabSize); | 
 |     if (this.columnPos + tabSize > this.lineLimit) { | 
 |       this.renderLineBreak(); | 
 |       tabSize = this.tabSize; | 
 |     } | 
 |     const piece = html`<span | 
 |       class=${diffClasses('tab')} | 
 |       style=${styleMap({'tab-size': `${tabSize}`})} | 
 |       >${TAB}</span | 
 |     >`; | 
 |     this.pieces.push(piece); | 
 |     this.textOffset += 1; | 
 |     this.columnPos += tabSize; | 
 |   } | 
 |  | 
 |   /** Render a surrogate pair: string length is 2, but is just 1 char. */ | 
 |   private renderSurrogatePair() { | 
 |     if (this.columnPos === this.lineLimit) { | 
 |       this.renderLineBreak(); | 
 |     } | 
 |     this.pieces.push(this.text.substring(this.textOffset, this.textOffset + 2)); | 
 |     this.textOffset += 2; | 
 |     this.columnPos += 1; | 
 |   } | 
 |  | 
 |   /** Render a line break, don't advance text offset, reset col position. */ | 
 |   private renderLineBreak() { | 
 |     if (this.isResponsive) { | 
 |       this.pieces.push(html`<wbr class=${diffClasses()}></wbr>`); | 
 |     } else { | 
 |       this.pieces.push(html`<span class=${diffClasses('br')}></span>`); | 
 |     } | 
 |     // this.textOffset += 0; | 
 |     this.columnPos = 0; | 
 |   } | 
 | } | 
 |  | 
 | customElements.define('gr-diff-text', GrDiffText); | 
 |  | 
 | declare global { | 
 |   interface HTMLElementTagNameMap { | 
 |     'gr-diff-text': GrDiffText; | 
 |   } | 
 | } |