LP#1775466 more legacy parser; printing; cleanup collab/berick/lp1775466-ang6-base-app-template-parser
authorBill Erickson <berickxx@gmail.com>
Thu, 21 Jun 2018 17:06:29 +0000 (13:06 -0400)
committerBill Erickson <berickxx@gmail.com>
Thu, 21 Jun 2018 17:06:29 +0000 (13:06 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/app.module.ts
Open-ILS/src/eg2/src/app/common.module.ts
Open-ILS/src/eg2/src/app/share/catalog/search-context.ts
Open-ILS/src/eg2/src/app/share/grid/grid.ts
Open-ILS/src/eg2/src/app/share/print/print.component.ts
Open-ILS/src/eg2/src/app/share/print/print.service.ts
Open-ILS/src/eg2/src/app/share/util/parser.service.ts [deleted file]
Open-ILS/src/eg2/src/app/staff/common.module.ts
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts
Open-ILS/src/eg2/src/app/staff/share/legacy-template.service.ts [new file with mode: 0644]

index 1f22bab..20de8ab 100644 (file)
@@ -1,7 +1,7 @@
 /**
  * BaseModule is the shared starting point for all apps.  It provides
- * the root route and core services, and a simple welcome page for users
- * that end up here accidentally.
+ * the root route and a simple welcome page for users that end up here
+ * accidentally.
  */
 import {BrowserModule} from '@angular/platform-browser';
 import {NgModule} from '@angular/core';
@@ -13,7 +13,6 @@ import {BaseComponent} from './app.component';
 import {BaseRoutingModule} from './routing.module';
 import {WelcomeComponent} from './welcome.component';
 
-// Import and 'provide' globally required services.
 @NgModule({
   declarations: [
     BaseComponent,
index dd3c201..e04e003 100644 (file)
@@ -17,7 +17,6 @@ import {PcrudService} from '@eg/core/pcrud.service';
 import {OrgService} from '@eg/core/org.service';
 import {AudioService} from '@eg/share/util/audio.service';
 import {FormatService} from '@eg/share/util/format.service';
-import {TemplateParserService} from '@eg/share/util/parser.service';
 import {PrintService} from '@eg/share/print/print.service';
 
 // Globally available components
@@ -73,8 +72,7 @@ export class EgCommonModule {
                 OrgService,
                 PrintService,
                 AudioService,
-                FormatService,
-                TemplateParserService
+                FormatService
             ]
         };
     }
index 7b918ca..e4e64b2 100644 (file)
@@ -39,7 +39,7 @@ export class CatalogSearchContext {
     sort: string;
     fieldClass: string[];
     query: string[];
-    identQuery: string; 
+    identQuery: string;
     identQueryType: string; // isbn, issn, etc.
     joinOp: string[];
     matchOp: string[];
index 876c2e9..3a967a7 100644 (file)
@@ -80,7 +80,7 @@ export class GridColumnSet {
     }
 
     idlInfoFromDotpath(dotpath: string): any {
-        if (!dotpath) return null;
+        if (!dotpath) { return null; }
 
         let idlParent;
         let idlField;
@@ -95,7 +95,7 @@ export class GridColumnSet {
 
             if (idlField) {
                 if (idlField['class'] && (
-                    idlField.datatype === 'link' || 
+                    idlField.datatype === 'link' ||
                     idlField.datatype === 'org_unit')) {
                     idlClass = this.idl.classes[idlField['class']];
                 }
@@ -463,7 +463,7 @@ export class GridContext {
         for (let i = 0; i < steps.length; i++) {
             const step = steps[i];
 
-            if (typeof obj != 'object') {
+            if (typeof obj !== 'object') {
                 // We have run out of data to step through before
                 // reaching the end of the path.  Conclude fleshing via
                 // callback if provided then exit.
@@ -481,7 +481,7 @@ export class GridContext {
             obj = this.getObjectFieldValue(obj, step);
         }
 
-        // We found a nested IDL object which may or may not have 
+        // We found a nested IDL object which may or may not have
         // been configured as a top-level column.  Flesh the column
         // metadata with our newly found IDL info.
         if (idlField) {
index ad41e5e..81b8020 100644 (file)
@@ -1,6 +1,5 @@
 import {Component, OnInit, TemplateRef, ElementRef, Renderer2} from '@angular/core';
 import {PrintService, PrintRequest} from './print.service';
-import {TemplateParserService} from '@eg/share/util/parser.service';
 
 @Component({
     selector: 'eg-print',
@@ -12,21 +11,15 @@ export class PrintComponent implements OnInit {
     // Template that requires local processing
     template: TemplateRef<any>;
 
-    // TemplateParserService-compatible template string.  
-    templateString: string;
-
     // Context data used for processing the template.
     context: any;
 
+    // Insertion point for externally-compiled templates
     htmlContainer: Element;
 
-    // Final HTML print string
-    htmlResult: string;
-  
     constructor(
         private renderer: Renderer2,
         private elm: ElementRef,
-        private parser: TemplateParserService,
         private printer: PrintService
     ) {}
 
@@ -34,54 +27,41 @@ export class PrintComponent implements OnInit {
         this.printer.onPrintRequest$.subscribe(
             printReq => this.handlePrintRequest(printReq));
 
-        this.htmlContainer = 
+        this.htmlContainer =
             this.renderer.selectRootElement('#eg-print-container');
     }
 
     handlePrintRequest(printReq: PrintRequest) {
-        this.applyTemplate(printReq).then(ok => {
-            // Give templates a chance to render before printing
-            setTimeout(() => this.dispatchPrint(printReq));
-        });
-    }
+        this.applyTemplate(printReq);
 
-    applyTemplate(printReq: PrintRequest): Promise<void> {
-        return new Promise((resolve, reject) => {
+        // Give templates a chance to render before printing
+        setTimeout(() => this.dispatchPrint(printReq));
+    }
 
-            if (printReq.template) {
-                // Inline template.  Let Angular do the work.
-                this.template = printReq.template;
-                this.context = {$implicit: printReq.contextData};
-                resolve();
-            }
+    applyTemplate(printReq: PrintRequest) {
 
-            let promise;
-            if (printReq.templateString) {
+        if (printReq.template) {
+            // Inline template.  Let Angular do the interpolationwork.
+            this.template = printReq.template;
+            this.context = {$implicit: printReq.contextData};
+            return;
+        }
 
-                promise = this.parser.apply(
-                    printReq.templateString, printReq.contextData);
+        if (printReq.text && true /* !this.hatch.isActive */) {
+            // Insert HTML into the browser DOM for in-browser printing only.
 
-            } else if (printReq.htmlString) {
-                promise = Promise.resolve(printReq.htmlString);
+            if (printReq.contentType === 'text/plain') {
+                // Wrap text/plain content in pre's to prevent
+                // unintended html formatting.
+                printReq.text = `<pre>${printReq.text}</pre>`;
             }
 
-            promise.then(html => {
-                this.htmlResult = html;
-
-                if (true /* !this.hatch.isActive */) {
-
-                    // Only insert the HTML into the browser DOM when
-                    // printing locally
-                    this.htmlContainer.innerHTML = this.htmlResult;
-                }
-
-                resolve();
-            });
-        });
+            this.htmlContainer.innerHTML = printReq.text;
+        }
     }
 
     dispatchPrint(printReq: PrintRequest) {
-        if (0 /*this.hatch.isActive*/) {
+        if (0 /* this.hatch.isActive */) {
             this.printViaHatch(printReq);
         } else {
             // Here the needed HTML is already in the page.
@@ -91,16 +71,19 @@ export class PrintComponent implements OnInit {
 
     printViaHatch(printReq: PrintRequest) {
 
-        if (!this.htmlResult) {
-            // Sometimes the results come from an externally-parsed HTML 
+        if (!printReq.text) {
+            // Sometimes the results come from an externally-parsed HTML
             // template, other times they come from an in-page template.
-            this.htmlResult = this.elm.nativeElement.innerHTML;
+            printReq.text = this.elm.nativeElement.innerHTML;
         }
 
+        // Send a full HTML document to Hatch
+        const html = `<html><body>${printReq.text}</body></html>`;
+
         /*
         this.hatch.print({
             printContext: printReq.printContext,
-            content: this.htmlResult
+            content: html
         });
         */
     }
index 2825af0..e296720 100644 (file)
@@ -2,11 +2,10 @@ import {Injectable, EventEmitter, TemplateRef} from '@angular/core';
 
 export interface PrintRequest {
     template?: TemplateRef<any>;
-    templateString?: string;
-    templateName?: string; // TODO
-    htmlString?: string,
     contextData?: any;
+    text?: string;
     printContext: string;
+    contentType?: string; // defaults to text/html
 }
 
 @Injectable()
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
deleted file mode 100644 (file)
index 165866d..0000000
+++ /dev/null
@@ -1,291 +0,0 @@
-/**
- * AngularJS-style minimal template parser.
- * Original use case is supporting AngularJS style print templates.
- * Template context data is applied only once at parsing time, there 
- * is no Angular-style data binding.  
- *
- * 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';
-
-// Internal class for modeling a single template parsing instance.
-class ParserInstance {
-
-    private context: any;
-    private format: FormatService;
-
-    // FormatService injected by ParserService
-    constructor(format: FormatService) {
-        this.format = format;
-    }
-
-    // Given an HTML string and an interpolation context/scope,
-    // process the HTML template declarations, apply variable 
-    // replacements, and return the restulting HTML string.
-    parse(html: string, context: any): string {
-
-        // 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
-        const domNode = doc.getElementsByTagName('body')[0];
-
-        this.traverse(domNode);
-
-        return domNode.innerHTML;
-    }
-
-    // Process each node in the in-progress document.
-    traverse(node: Node) {
-        if (!node) return;
-
-        switch (node.nodeType) {
-            case Node.ELEMENT_NODE:
-                const complete = this.processElementNode(node as Element);
-                if (complete) {
-                    return;
-                }
-                break;
-
-            case Node.TEXT_NODE:
-                this.processTextNode(node as Text);
-                break;
-        }
-
-        // Array.from() avoids TS compiler warnings
-        Array.from(node.childNodes).forEach(child => this.traverse(child));
-    }
-
-    // Process expressions found on each 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) {
-            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 listItemKey = parts[0].trim();
-        const listPath = parts[1].trim();
-        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
-    // where references to elements in the context are available.
-    // For example:  ng-if="foo.bar" -- the 'foo' key from the 
-    // context must be defined in advance for evaluation to succeed.
-    generateEvalEnv() {
-        let env = '';
-        Object.keys(this.context).forEach(key => {
-            env += `var ${key} = this.context['${key}'];\n`;
-        });
-        return env;
-    }
-
-    // Returns true of the IF expression evaluates to true, 
-    // false otherwise.
-    testIfExpression(expr: string): boolean {
-
-        const env = this.generateEvalEnv();
-        const evalStr = `${env}; Boolean(${expr})`;
-        //console.debug('ng-if eval string: ', evalStr);
-
-        try {
-            return eval(evalStr);
-        } catch (err) {
-            //console.debug('IF expression failed with: ', err);
-            return false;
-        }
-    }
-
-    // Replace variable {{...}} instances in a given Text node
-    // with the matching data from the context.
-    processTextNode(node: Text) {
-        if (!node || !node.data) { return; }
-
-        const matches = node.data.match(/{{.*?}}/g);
-        if (!matches) { return };
-
-        matches.forEach(match => {
-            let dotpath = match.replace(/[{}]/g, '');
-            node.replaceData(
-                node.data.indexOf(match), 
-                match.length,
-                this.getContextStringValue(dotpath)
-            );
-        });
-    }
-
-    // Returns the context item at the given path
-    getContextValueAt(dotpath: string): any {
-        let idx;
-        let obj = this.context;
-        const parts = dotpath.split('.');
-
-        for (idx = 0; idx < parts.length; idx++) {
-            obj = obj[parts[idx]];
-            if (obj === null || typeof obj !== 'object') {
-                break;
-            }
-        }
-
-        if (idx < parts.length - 1) {
-            // Loop exited before inspecting the whole path.
-            return null;
-        }
-
-        return obj;
-    }
-
-    // Find the value within the context at the given path.
-    getContextStringValue(dotpath: string): string {
-
-        // Variable replacements may contain filters.
-        const pieces = dotpath.split('|').map(p => p.trim());
-        const path = pieces[0]
-        const filter = pieces[1];
-        const data = {
-            datatype: null, // potentially applied below
-            value: this.getContextValueAt(path)
-        };
-
-        // TODO: teach the format service about processing due dates.
-        if (filter) {
-            const fParts = filter.split(':').map(p => p.trim());
-
-            switch (fParts[0]) {
-                case 'date':
-                    data.datatype = 'timestamp';
-                    break;
-                case 'currency':
-                    data.datatype = 'money';
-                    break;
-                case 'limitTo':
-                    const size = fParts[1];
-                    const offset = fParts[2] || 0;
-                    if (size) {
-                        data.value = 
-                            data.value.substring(offset, offset + size);
-                    }
-                    break;
-            }
-        }
-
-        return this.format.transform(data);
-    }
-}
-
-
-@Injectable()
-export class TemplateParserService {
-
-    constructor(private format: FormatService) {}
-
-    // make async for now, may be needed later
-    public apply(html: string, context: any): Promise<string> {
-        const parser = new ParserInstance(this.format);
-
-        return new Promise((resolve, reject) => {
-            resolve(parser.parse(html, context));
-        });
-    }
-}
-
index 2dfbb3c..98de443 100644 (file)
@@ -12,6 +12,7 @@ import {StringComponent} from '@eg/share/string/string.component';
 import {StringService} from '@eg/share/string/string.service';
 import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
 import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
+import {LegacyTemplateService} from '@eg/staff/share/legacy-template.service';
 
 /**
  * Imports the EG common modules and adds modules common to all staff UI's.
@@ -51,6 +52,7 @@ export class StaffCommonModule {
         return {
             ngModule: StaffCommonModule,
             providers: [ // Export staff-wide services
+                LegacyTemplateService,
                 AccessKeyService,
                 StringService,
                 ToastService
index 521db03..63bb2fa 100644 (file)
@@ -13,7 +13,7 @@ import {OrgService} from '@eg/core/org.service';
 import {Pager} from '@eg/share/util/pager';
 import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
 import {PrintService} from '@eg/share/print/print.service';
-import {TemplateParserService} from '@eg/share/util/parser.service';
+import {LegacyTemplateService} from '@eg/staff/share//legacy-template.service';
 
 @Component({
   templateUrl: 'sandbox.component.html'
@@ -55,7 +55,7 @@ export class SandboxComponent implements OnInit {
         private strings: StringService,
         private toast: ToastService,
         private printer: PrintService,
-        private parser: TemplateParserService
+        private parser: LegacyTemplateService
     ) {}
 
     ngOnInit() {
@@ -177,24 +177,16 @@ export class SandboxComponent implements OnInit {
             cost: 23.3,
             copies: [
                 {barcode: 'abc123', title: 'welcome to the jungle', parts: ['a', 'b', 'c']},
-                {barcode: 'def456', title: 'hello mudda, hello fadda', parts: ['x','y']}
+                {barcode: 'def456', title: 'hello mudda, hello fadda', parts: ['x', 'y']}
             ]
-        }
+        };
 
-        /*
         this.parser.apply(template, context).then(html => {
             this.printer.print({
-                htmlString: html,
+                text: html,
                 printContext: 'default'
             });
         });
-        */
-
-        this.printer.print({
-            templateString: template,
-            contextData: context,
-            printContext: 'default'
-        });
     }
 }
 
diff --git a/Open-ILS/src/eg2/src/app/staff/share/legacy-template.service.ts b/Open-ILS/src/eg2/src/app/staff/share/legacy-template.service.ts
new file mode 100644 (file)
index 0000000..fbd6265
--- /dev/null
@@ -0,0 +1,295 @@
+/**
+ * USING EVAL() OPENS A LARGE SECURITY HOLE.  
+ * DEPRECATE ME.
+ * AngularJS-style minimal template parser.
+ * Original use case is supporting AngularJS style print templates.
+ * Template context data is applied only once at parsing time, there
+ * is no data binding.
+ *
+ * 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';
+
+// Internal class for modeling a single template parsing instance.
+class ParserInstance {
+
+    private context: any;
+    private format: FormatService;
+
+    // FormatService injected by ParserService
+    constructor(format: FormatService) {
+        this.format = format;
+    }
+
+    // Given an HTML string and an interpolation context/scope,
+    // process the HTML template declarations, apply variable
+    // replacements, and return the restulting HTML string.
+    parse(html: string, context: any): string {
+
+        // 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
+        const domNode = doc.getElementsByTagName('body')[0];
+
+        this.traverse(domNode);
+
+        return domNode.innerHTML;
+    }
+
+    // Process each node in the in-progress document.
+    traverse(node: Node) {
+        if (!node) { return; }
+
+        switch (node.nodeType) {
+            case Node.ELEMENT_NODE:
+                const complete = this.processElementNode(node as Element);
+                if (complete) {
+                    return;
+                }
+                break;
+
+            case Node.TEXT_NODE:
+                this.processTextNode(node as Text);
+                break;
+        }
+
+        // Array.from() avoids TS compiler warnings
+        Array.from(node.childNodes).forEach(child => this.traverse(child));
+    }
+
+    // Process expressions found on each 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) {
+            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 listItemKey = parts[0].trim();
+        const listPath = parts[1].trim();
+        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
+    // where references to elements in the context are available.
+    // For example:  ng-if="foo.bar" -- the 'foo' key from the
+    // context must be defined in advance for evaluation to succeed.
+    generateEvalEnv() {
+        let env = '';
+        Object.keys(this.context).forEach(key => {
+            env += `var ${key} = this.context['${key}'];\n`;
+        });
+        return env;
+    }
+
+    // Returns true of the IF expression evaluates to true,
+    // false otherwise.
+    testIfExpression(expr: string): boolean {
+
+        const env = this.generateEvalEnv();
+        const evalStr = `${env}; Boolean(${expr})`;
+        // console.debug('ng-if eval string: ', evalStr);
+
+        try {
+            return eval(evalStr);
+        } catch (err) {
+            // console.debug('IF expression failed with: ', err);
+            return false;
+        }
+    }
+
+    // Replace variable {{...}} instances in a given Text node
+    // with the matching data from the context.
+    processTextNode(node: Text) {
+        if (!node || !node.data) { return; }
+
+        const matches = node.data.match(/{{.*?}}/g);
+        if (!matches) { return; }
+
+        matches.forEach(match => {
+            const dotpath = match.replace(/[{}]/g, '');
+            node.replaceData(
+                node.data.indexOf(match),
+                match.length,
+                this.getContextStringValue(dotpath)
+            );
+        });
+    }
+
+    // Returns the context item at the given path
+    getContextValueAt(dotpath: string): any {
+        let idx;
+        let obj = this.context;
+        const parts = dotpath.split('.');
+
+        for (idx = 0; idx < parts.length; idx++) {
+            obj = obj[parts[idx]];
+            if (obj === null || typeof obj !== 'object') {
+                break;
+            }
+        }
+
+        if (idx < parts.length - 1) {
+            // Loop exited before inspecting the whole path.
+            return null;
+        }
+
+        return obj;
+    }
+
+    // Find the value within the context at the given path.
+    getContextStringValue(dotpath: string): string {
+
+        // Variable replacements may contain filters.
+        const pieces = dotpath.split('|').map(p => p.trim());
+        const path = pieces[0];
+        const filter = pieces[1];
+        const data = {
+            datatype: null, // potentially applied below
+            value: this.getContextValueAt(path)
+        };
+
+        // TODO: teach the format service about processing due dates.
+        if (filter) {
+            const fParts = filter.split(':').map(p => p.trim());
+            const fName = fParts[0];
+            const fArgs = fParts.slice(1);
+
+            switch (fName) {
+                case 'date':
+                    data.datatype = 'timestamp';
+                    break;
+                case 'currency':
+                    data.datatype = 'money';
+                    break;
+                case 'limitTo':
+                    const size = fArgs[0];
+                    if (size) {
+                        const offset = fArgs[1] || 0;
+                        data.value =
+                            data.value.substring(offset, offset + size);
+                    }
+                    break;
+            }
+        }
+
+        return this.format.transform(data);
+    }
+}
+
+
+@Injectable()
+export class LegacyTemplateService {
+
+    constructor(private format: FormatService) {}
+
+    // make async for now, may be needed later
+    public apply(html: string, context: any): Promise<string> {
+        const parser = new ParserInstance(this.format);
+
+        return new Promise((resolve, reject) => {
+            resolve(parser.parse(html, context));
+        });
+    }
+}
+