/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {assertDefined} from '../util/assert'; import {ACTIVE_INDEX, CONTAINER_HEADER_OFFSET, LContainer, NATIVE} from './interfaces/container'; import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, TIcu} from './interfaces/i18n'; import {TNode} from './interfaces/node'; import {LQueries} from './interfaces/query'; import {RComment, RElement} from './interfaces/renderer'; import {StylingContext} from './interfaces/styling'; import {BINDING_INDEX, CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, TVIEW, T_HOST} from './interfaces/view'; import {getTNode, unwrapRNode} from './util/view_utils'; function attachDebugObject(obj: any, debug: any) { Object.defineProperty(obj, 'debug', {value: debug, enumerable: false}); } /* * This file contains conditionally attached classes which provide human readable (debug) level * information for `LView`, `LContainer` and other internal data structures. These data structures * are stored internally as array which makes it very difficult during debugging to reason about the * current state of the system. * * Patching the array with extra property does change the array's hidden class' but it does not * change the cost of access, therefore this patching should not have significant if any impact in * `ngDevMode` mode. (see: https://jsperf.com/array-vs-monkey-patch-array) * * So instead of seeing: * ``` * Array(30) [Object, 659, null, …] * ``` * * You get to see: * ``` * LViewDebug { * views: [...], * flags: {attached: true, ...} * nodes: [ * {html: '
', ..., nodes: [ * {html: '', ..., nodes: null} * ]} * ] * } * ``` */ export function attachLViewDebug(lView: LView) { attachDebugObject(lView, new LViewDebug(lView)); } export function attachLContainerDebug(lContainer: LContainer) { attachDebugObject(lContainer, new LContainerDebug(lContainer)); } export function toDebug(obj: LView): LViewDebug; export function toDebug(obj: LView | null): LViewDebug|null; export function toDebug(obj: LView | LContainer | null): LViewDebug|LContainerDebug|null; export function toDebug(obj: any): any { if (obj) { const debug = (obj as any).debug; assertDefined(debug, 'Object does not have a debug representation.'); return debug; } else { return obj; } } /** * Use this method to unwrap a native element in `LView` and convert it into HTML for easier * reading. * * @param value possibly wrapped native DOM node. * @param includeChildren If `true` then the serialized HTML form will include child elements (same * as `outerHTML`). If `false` then the serialized HTML form will only contain the element itself * (will not serialize child elements). */ function toHtml(value: any, includeChildren: boolean = false): string|null { const node: HTMLElement|null = unwrapRNode(value) as any; if (node) { const isTextNode = node.nodeType === Node.TEXT_NODE; const outerHTML = (isTextNode ? node.textContent : node.outerHTML) || ''; if (includeChildren || isTextNode) { return outerHTML; } else { const innerHTML = node.innerHTML; return outerHTML.split(innerHTML)[0] || null; } } else { return null; } } export class LViewDebug { constructor(private readonly _raw_lView: LView) {} /** * Flags associated with the `LView` unpacked into a more readable state. */ get flags() { const flags = this._raw_lView[FLAGS]; return { __raw__flags__: flags, initPhaseState: flags & LViewFlags.InitPhaseStateMask, creationMode: !!(flags & LViewFlags.CreationMode), firstViewPass: !!(flags & LViewFlags.FirstLViewPass), checkAlways: !!(flags & LViewFlags.CheckAlways), dirty: !!(flags & LViewFlags.Dirty), attached: !!(flags & LViewFlags.Attached), destroyed: !!(flags & LViewFlags.Destroyed), isRoot: !!(flags & LViewFlags.IsRoot), indexWithinInitPhase: flags >> LViewFlags.IndexWithinInitPhaseShift, }; } get parent(): LViewDebug|LContainerDebug|null { return toDebug(this._raw_lView[PARENT]); } get host(): string|null { return toHtml(this._raw_lView[HOST], true); } get context(): {}|null { return this._raw_lView[CONTEXT]; } /** * The tree of nodes associated with the current `LView`. The nodes have been normalized into a * tree structure with relevant details pulled out for readability. */ get nodes(): DebugNode[]|null { const lView = this._raw_lView; const tNode = lView[TVIEW].firstChild; return toDebugNodes(tNode, lView); } /** * Additional information which is hidden behind a property. The extra level of indirection is * done so that the debug view would not be cluttered with properties which are only rarely * relevant to the developer. */ get __other__() { return { tView: this._raw_lView[TVIEW], cleanup: this._raw_lView[CLEANUP], injector: this._raw_lView[INJECTOR], rendererFactory: this._raw_lView[RENDERER_FACTORY], renderer: this._raw_lView[RENDERER], sanitizer: this._raw_lView[SANITIZER], childHead: toDebug(this._raw_lView[CHILD_HEAD]), next: toDebug(this._raw_lView[NEXT]), childTail: toDebug(this._raw_lView[CHILD_TAIL]), declarationView: toDebug(this._raw_lView[DECLARATION_VIEW]), contentQueries: this._raw_lView[CONTENT_QUERIES], queries: this._raw_lView[QUERIES], tHost: this._raw_lView[T_HOST], bindingIndex: this._raw_lView[BINDING_INDEX], }; } /** * Normalized view of child views (and containers) attached at this location. */ get childViews(): Array { const childViews: Array = []; let child = this.__other__.childHead; while (child) { childViews.push(child); child = child.__other__.next; } return childViews; } } export interface DebugNode { html: string|null; native: Node; nodes: DebugNode[]|null; component: LViewDebug|null; } /** * Turns a flat list of nodes into a tree by walking the associated `TNode` tree. * * @param tNode * @param lView */ export function toDebugNodes(tNode: TNode | null, lView: LView): DebugNode[]|null { if (tNode) { const debugNodes: DebugNode[] = []; let tNodeCursor: TNode|null = tNode; while (tNodeCursor) { const rawValue = lView[tNode.index]; const native = unwrapRNode(rawValue); const componentLViewDebug = toDebug(readLViewValue(rawValue)); debugNodes.push({ html: toHtml(native), native: native as any, nodes: toDebugNodes(tNode.child, lView), component: componentLViewDebug }); tNodeCursor = tNodeCursor.next; } return debugNodes; } else { return null; } } export class LContainerDebug { constructor(private readonly _raw_lContainer: LContainer) {} get activeIndex(): number { return this._raw_lContainer[ACTIVE_INDEX]; } get views(): LViewDebug[] { return this._raw_lContainer.slice(CONTAINER_HEADER_OFFSET) .map(toDebug as(l: LView) => LViewDebug); } get parent(): LViewDebug|LContainerDebug|null { return toDebug(this._raw_lContainer[PARENT]); } get queries(): LQueries|null { return this._raw_lContainer[QUERIES]; } get host(): RElement|RComment|StylingContext|LView { return this._raw_lContainer[HOST]; } get native(): RComment { return this._raw_lContainer[NATIVE]; } get __other__() { return { next: toDebug(this._raw_lContainer[NEXT]), }; } } /** * Return an `LView` value if found. * * @param value `LView` if any */ export function readLViewValue(value: any): LView|null { while (Array.isArray(value)) { // This check is not quite right, as it does not take into account `StylingContext` // This is why it is in debug, not in util.ts if (value.length >= HEADER_OFFSET - 1) return value as LView; value = value[HOST]; } return null; } export class I18NDebugItem { [key: string]: any; get tNode() { return getTNode(this.nodeIndex, this._lView); } constructor( public __raw_opCode: any, private _lView: LView, public nodeIndex: number, public type: string) {} } /** * Turns a list of "Create" & "Update" OpCodes into a human-readable list of operations for * debugging purposes. * @param mutateOpCodes mutation opCodes to read * @param updateOpCodes update opCodes to read * @param icus list of ICU expressions * @param lView The view the opCodes are acting on */ export function attachI18nOpCodesDebug( mutateOpCodes: I18nMutateOpCodes, updateOpCodes: I18nUpdateOpCodes, icus: TIcu[] | null, lView: LView) { attachDebugObject(mutateOpCodes, new I18nMutateOpCodesDebug(mutateOpCodes, lView)); attachDebugObject(updateOpCodes, new I18nUpdateOpCodesDebug(updateOpCodes, icus, lView)); if (icus) { icus.forEach(icu => { icu.create.forEach( icuCase => { attachDebugObject(icuCase, new I18nMutateOpCodesDebug(icuCase, lView)); }); icu.update.forEach(icuCase => { attachDebugObject(icuCase, new I18nUpdateOpCodesDebug(icuCase, icus, lView)); }); }); } } export class I18nMutateOpCodesDebug implements I18nOpCodesDebug { constructor(private readonly __raw_opCodes: I18nMutateOpCodes, private readonly __lView: LView) {} /** * A list of operation information about how the OpCodes will act on the view. */ get operations() { const {__lView, __raw_opCodes} = this; const results: any[] = []; for (let i = 0; i < __raw_opCodes.length; i++) { const opCode = __raw_opCodes[i]; let result: any; if (typeof opCode === 'string') { result = { __raw_opCode: opCode, type: 'Create Text Node', nodeIndex: __raw_opCodes[++i], text: opCode, }; } if (typeof opCode === 'number') { switch (opCode & I18nMutateOpCode.MASK_OPCODE) { case I18nMutateOpCode.AppendChild: const destinationNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_PARENT; result = new I18NDebugItem(opCode, __lView, destinationNodeIndex, 'AppendChild'); break; case I18nMutateOpCode.Select: const nodeIndex = opCode >>> I18nMutateOpCode.SHIFT_REF; result = new I18NDebugItem(opCode, __lView, nodeIndex, 'Select'); break; case I18nMutateOpCode.ElementEnd: let elementIndex = opCode >>> I18nMutateOpCode.SHIFT_REF; result = new I18NDebugItem(opCode, __lView, elementIndex, 'ElementEnd'); break; case I18nMutateOpCode.Attr: elementIndex = opCode >>> I18nMutateOpCode.SHIFT_REF; result = new I18NDebugItem(opCode, __lView, elementIndex, 'Attr'); result['attrName'] = __raw_opCodes[++i]; result['attrValue'] = __raw_opCodes[++i]; break; } } if (!result) { switch (opCode) { case COMMENT_MARKER: result = { __raw_opCode: opCode, type: 'COMMENT_MARKER', commentValue: __raw_opCodes[++i], nodeIndex: __raw_opCodes[++i], }; break; case ELEMENT_MARKER: result = { __raw_opCode: opCode, type: 'ELEMENT_MARKER', }; break; } } if (!result) { result = { __raw_opCode: opCode, type: 'Unknown Op Code', code: opCode, }; } results.push(result); } return results; } } export class I18nUpdateOpCodesDebug implements I18nOpCodesDebug { constructor( private readonly __raw_opCodes: I18nUpdateOpCodes, private readonly icus: TIcu[]|null, private readonly __lView: LView) {} /** * A list of operation information about how the OpCodes will act on the view. */ get operations() { const {__lView, __raw_opCodes, icus} = this; const results: any[] = []; for (let i = 0; i < __raw_opCodes.length; i++) { // bit code to check if we should apply the next update const checkBit = __raw_opCodes[i] as number; // Number of opCodes to skip until next set of update codes const skipCodes = __raw_opCodes[++i] as number; let value = ''; for (let j = i + 1; j <= (i + skipCodes); j++) { const opCode = __raw_opCodes[j]; if (typeof opCode === 'string') { value += opCode; } else if (typeof opCode == 'number') { if (opCode < 0) { // It's a binding index whose value is negative // We cannot know the value of the binding so we only show the index value += `�${-opCode - 1}�`; } else { const nodeIndex = opCode >>> I18nUpdateOpCode.SHIFT_REF; let tIcuIndex: number; let tIcu: TIcu; switch (opCode & I18nUpdateOpCode.MASK_OPCODE) { case I18nUpdateOpCode.Attr: const attrName = __raw_opCodes[++j] as string; const sanitizeFn = __raw_opCodes[++j]; results.push({ __raw_opCode: opCode, checkBit, type: 'Attr', attrValue: value, attrName, sanitizeFn, }); break; case I18nUpdateOpCode.Text: results.push({ __raw_opCode: opCode, checkBit, type: 'Text', nodeIndex, text: value, }); break; case I18nUpdateOpCode.IcuSwitch: tIcuIndex = __raw_opCodes[++j] as number; tIcu = icus ![tIcuIndex]; let result = new I18NDebugItem(opCode, __lView, nodeIndex, 'IcuSwitch'); result['tIcuIndex'] = tIcuIndex; result['checkBit'] = checkBit; result['mainBinding'] = value; result['tIcu'] = tIcu; results.push(result); break; case I18nUpdateOpCode.IcuUpdate: tIcuIndex = __raw_opCodes[++j] as number; tIcu = icus ![tIcuIndex]; result = new I18NDebugItem(opCode, __lView, nodeIndex, 'IcuUpdate'); result['tIcuIndex'] = tIcuIndex; result['checkBit'] = checkBit; result['tIcu'] = tIcu; results.push(result); break; } } } } i += skipCodes; } return results; } } export interface I18nOpCodesDebug { operations: any[]; }