LP#1775466 AngJS style minimal template parser continued
authorBill Erickson <berickxx@gmail.com>
Wed, 20 Jun 2018 19:08:50 +0000 (15:08 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 20 Jun 2018 19:08:50 +0000 (15:08 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/share/util/parser.service.ts
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts

index b33fa20..fc7a0dc 100644 (file)
@@ -1,3 +1,14 @@
+/**
+ * AngularJS-style minimal template parser.
+ * Original use case is supporting AngularJS style print templates.
+ *
+ * Supports the following template constructs:
+ *  {{variables}}
+ *  <element ng-if="expression">
+ *  <element ng-repeat="expression">
+ *  <element ng-switch="expression">
+ *    <child-element ng-switch-when="string">
+ */
 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 <html><body> 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);
index 8e5c162..e2e7d30 100644 (file)
@@ -127,10 +127,7 @@ export class SandboxComponent implements OnInit {
         }, 4000);
     }
 
-
-
     testParser() {
-
         const template = `
             <div>
               <div>This item needs to be routed to
@@ -148,8 +145,20 @@ export class SandboxComponent implements OnInit {
                 <li ng-repeat="copy in copies">
                   <div>Barcode: {{copy.barcode}}</div>
                   <div>Title: {{copy.title}}</div>
+                  <div ng-repeat="part in copy.parts">
+                    part = {{part}}
+                  </div>
                 </li>
               </ol>
+              <div ng-switch="dest_location.shortname">
+                <div ng-switch-when="BR2">THIS IS BR2</div>
+                <div ng-switch-when="BR1">
+                  THIS IS BR1
+                  <div ng-repeat="cp in copies">
+                    swith bc = {{cp.barcode}}
+                  </div>
+                </div>
+              </div>
         `;
 
         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']}
             ]
         }