From 26f40280c2c5d06837fa548bd852e73a82e8ef82 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Wed, 20 Jun 2018 15:08:50 -0400 Subject: [PATCH] LP#1775466 AngJS style minimal template parser continued Signed-off-by: Bill Erickson --- .../src/eg2/src/app/share/util/parser.service.ts | 127 ++++++++++++++++++--- .../eg2/src/app/staff/sandbox/sandbox.component.ts | 19 ++- 2 files changed, 123 insertions(+), 23 deletions(-) diff --git a/Open-ILS/src/eg2/src/app/share/util/parser.service.ts b/Open-ILS/src/eg2/src/app/share/util/parser.service.ts index b33fa20fd2..fc7a0dccae 100644 --- a/Open-ILS/src/eg2/src/app/share/util/parser.service.ts +++ b/Open-ILS/src/eg2/src/app/share/util/parser.service.ts @@ -1,3 +1,14 @@ +/** + * AngularJS-style minimal template parser. + * Original use case is supporting AngularJS style print templates. + * + * Supports the following template constructs: + * {{variables}} + * + * + * + * + */ import {Injectable, EventEmitter} from '@angular/core'; import {FormatService} from '@eg/share/util/format.service'; @@ -17,9 +28,11 @@ class ParserInstance { // process the HTML template declarations, apply variable // replacements, and return the restulting HTML string. parse(html: string, context: any): string { - this.context = context; - const parser = new DOMParser(); + // Shallow copy the context since we modify the keys internally + this.context = Object.assign({}, context); + + const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); // Parsing as html wraps the content in an wrapper @@ -30,17 +43,20 @@ class ParserInstance { return this.domNode.innerHTML; } - // Process each node in the source document. + // Process each node in the in-progress document. traverse(node: Node) { if (!node) return; switch (node.nodeType) { case Node.ELEMENT_NODE: - this.processElement(node as Element); + const complete = this.processElementNode(node as Element); + if (complete) { + return; + } break; case Node.TEXT_NODE: - this.replaceText(node as Text); + this.processTextNode(node as Text); break; } @@ -49,31 +65,106 @@ class ParserInstance { } // Process expressions found on each element. - processElement(node: Element) { + // Returns true if the node was processed within and needs no + // further processing, false otherwise. + processElementNode(node: Element): boolean { + + const switchExp = node.getAttribute('ng-switch'); + if (switchExp) { + node.removeAttribute('ng-switch'); + if (!this.processSwitchExpression(node, switchExp)) { + // We removed all child nodes in the switch + return true; + } + } const ifExp = node.getAttribute('ng-if'); - if (ifExp && !this.testIfExpression(ifExp)) { - // A failing IF expression means the node does not render. - node.remove(); + if (ifExp) { + node.removeAttribute('ng-if'); + if (!this.testIfExpression(ifExp)) { + // A failing IF expression means the node does not render. + node.remove(); + return true; + } } const loopExp = node.getAttribute('ng-repeat'); if (loopExp) { + node.removeAttribute('ng-repeat'); this.processLoopExpression(node, loopExp); + return true; } + + return false; + } + + // Returns true if any of the switch expressions resulted + // in true, thus allowing a child node to remain and require + // future processing. + processSwitchExpression(node: Element, expr: string): boolean { + + // ng-switch only works on string values + const targetVal = this.getContextStringValue(expr); + let matchNode: Node; + + // Find the switch-matching child node + Array.from(node.childNodes).forEach(child => { + if (!matchNode && child.nodeType === Node.ELEMENT_NODE) { + const elm: Element = child as Element; + const val = elm.getAttribute('ng-switch-when'); + if (val === null || val === undefined) { return; } + if (val + '' === targetVal) { + matchNode = child; + } + } + }); + + // Remove all child nodes + while (node.firstChild) { + node.removeChild(node.firstChild); + } + + // Add the matching node back if found + if (matchNode) { + node.appendChild(matchNode); + return true; + } + + return false; // no matching switch values } processLoopExpression(node: Element, expr: string) { const parts = expr.split('in'); - const key = parts[0].trim(); + const listItemKey = parts[0].trim(); const listPath = parts[1].trim(); - - this.generateEvalEnv(); const list = this.getContextValueAt(listPath); if (!Array.isArray(list)) { throw new Error(`Template value ${listPath} is not an Array`); } + + // Loop over the repeat array and for each iteration, add the + // loop variable as a new value in the (temporary) context so + // it can be referenced in the eval context. + const origContext = this.context; + let prevNode: Element = node; + + list.forEach(listItem => { + const listCtx = {}; + listCtx[listItemKey] = listItem; + this.context = Object.assign(this.context, listCtx); + const newNode = node.cloneNode(true) as Element; + // appendChild used internally when needed. + prevNode.parentNode.insertBefore(newNode, prevNode.nextSibling); + this.traverse(newNode); + prevNode = newNode; + }); + + // recover the original context sans any loop data. + this.context = origContext; + + // get rid of the source/template node + node.remove(); } // To evaluate free-form expressions, create an environment @@ -81,19 +172,19 @@ class ParserInstance { // For example: ng-if="foo.bar" -- the 'foo' key from the // context must be defined in advance for evaluation to succeed. generateEvalEnv() { - if (this.evalEnv) return; - this.evalEnv = ''; + let env = ''; Object.keys(this.context).forEach(key => { - this.evalEnv += `var ${key} = this.context['${key}'];\n`; + env += `var ${key} = this.context['${key}'];\n`; }); + return env; } // Returns true of the IF expression evaluates to true, // false otherwise. testIfExpression(expr: string): boolean { - this.generateEvalEnv(); - const evalStr = `${this.evalEnv}; Boolean(${expr})`; + const env = this.generateEvalEnv(); + const evalStr = `${env}; Boolean(${expr})`; //console.debug('ng-if eval string: ', evalStr); try { @@ -106,7 +197,7 @@ class ParserInstance { // Replace variable {{...}} instances in a given Text node // with the matching data from the context. - replaceText(node: Text) { + processTextNode(node: Text) { if (!node || !node.data) { return; } const matches = node.data.match(/{{.*?}}/g); diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts index 8e5c1623db..e2e7d30b73 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts @@ -127,10 +127,7 @@ export class SandboxComponent implements OnInit { }, 4000); } - - testParser() { - const template = `
This item needs to be routed to @@ -148,8 +145,20 @@ export class SandboxComponent implements OnInit {
  • Barcode: {{copy.barcode}}
    Title: {{copy.title}}
    +
    + part = {{part}} +
  • +
    +
    THIS IS BR2
    +
    + THIS IS BR1 +
    + swith bc = {{cp.barcode}} +
    +
    +
    `; const context = { @@ -165,8 +174,8 @@ export class SandboxComponent implements OnInit { boolFalse: false, today: new Date(), copies: [ - {barcode: 'abc123', title: 'welcome to the jungle'}, - {barcode: 'def456', title: 'hello mudda, hello fadda'} + {barcode: 'abc123', title: 'welcome to the jungle', parts: ['a', 'b', 'c']}, + {barcode: 'def456', title: 'hello mudda, hello fadda', parts: ['x','y']} ] } -- 2.11.0