From: Bill Erickson Date: Wed, 20 Jun 2018 17:12:05 +0000 (-0400) Subject: LP#1775466 AngJS style minimal template parser WIP X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=1980b0f45974cbf96904451f149c6ea47654c8da;p=working%2FEvergreen.git LP#1775466 AngJS style minimal template parser WIP So we can continue supporting AngularJS-style print templates -- without having to use AngularJS -- which will buy some time for determining how we want to handle print templates in the future and to ease existing template migration. Signed-off-by: Bill Erickson --- 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 new file mode 100644 index 0000000000..b33fa20fd2 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/util/parser.service.ts @@ -0,0 +1,169 @@ +import {Injectable, EventEmitter} from '@angular/core'; +import {FormatService} from '@eg/share/util/format.service'; + +class ParserInstance { + + private context: any; + private domNode: HTMLElement; + private format: FormatService; + private evalEnv: string; + + // 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 { + this.context = context; + const parser = new DOMParser(); + + const doc = parser.parseFromString(html, "text/html"); + + // Parsing as html wraps the content in an wrapper + this.domNode = doc.getElementsByTagName('body')[0]; + + this.traverse(this.domNode); + + return this.domNode.innerHTML; + } + + // Process each node in the source document. + traverse(node: Node) { + if (!node) return; + + switch (node.nodeType) { + case Node.ELEMENT_NODE: + this.processElement(node as Element); + break; + + case Node.TEXT_NODE: + this.replaceText(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. + processElement(node: Element) { + + const ifExp = node.getAttribute('ng-if'); + if (ifExp && !this.testIfExpression(ifExp)) { + // A failing IF expression means the node does not render. + node.remove(); + } + + const loopExp = node.getAttribute('ng-repeat'); + if (loopExp) { + this.processLoopExpression(node, loopExp); + } + } + + processLoopExpression(node: Element, expr: string) { + const parts = expr.split('in'); + const key = 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`); + } + } + + // 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() { + if (this.evalEnv) return; + this.evalEnv = ''; + Object.keys(this.context).forEach(key => { + this.evalEnv += `var ${key} = this.context['${key}'];\n`; + }); + } + + // Returns true of the IF expression evaluates to true, + // false otherwise. + testIfExpression(expr: string): boolean { + + this.generateEvalEnv(); + const evalStr = `${this.evalEnv}; 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. + replaceText(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 { + const value = this.getContextValueAt(dotpath); + // TODO IDL stuff when possible for formatting (e.g. dates) + return this.format.transform({value: value}); + } +} + + +@Injectable() +export class TemplateParserService { + + constructor(private format: FormatService) {} + + // make async for now, may be needed later + public apply(html: string, context: any): Promise { + const parser = new ParserInstance(this.format); + + return new Promise((resolve, reject) => { + resolve(parser.parse(html, context)); + }); + } +} + diff --git a/Open-ILS/src/eg2/src/app/staff/common.module.ts b/Open-ILS/src/eg2/src/app/staff/common.module.ts index 2dfbb3cd8f..6527a6616d 100644 --- a/Open-ILS/src/eg2/src/app/staff/common.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/common.module.ts @@ -10,6 +10,7 @@ import {ToastService} from '@eg/share/toast/toast.service'; import {ToastComponent} from '@eg/share/toast/toast.component'; import {StringComponent} from '@eg/share/string/string.component'; import {StringService} from '@eg/share/string/string.service'; +import {TemplateParserService} from '@eg/share/util/parser.service'; import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; import {DateSelectComponent} from '@eg/share/date-select/date-select.component'; @@ -53,7 +54,8 @@ export class StaffCommonModule { providers: [ // Export staff-wide services AccessKeyService, StringService, - ToastService + ToastService, + TemplateParserService ] }; } diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html index dbea15aad7..2f8cea43c8 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html @@ -74,6 +74,10 @@

+ + +

+ HELLO {{userContext.hello}} 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 eb0fdb035f..8e5c1623db 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 @@ -13,6 +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'; @Component({ templateUrl: 'sandbox.component.html' @@ -53,7 +54,8 @@ export class SandboxComponent implements OnInit { private pcrud: PcrudService, private strings: StringService, private toast: ToastService, - private printer: PrintService + private printer: PrintService, + private parser: TemplateParserService ) {} ngOnInit() { @@ -70,7 +72,7 @@ export class SandboxComponent implements OnInit { if (sort.length) { orderBy.cbt = sort[0].name + ' ' + sort[0].dir; } - + return this.pcrud.retrieveAll('cbt', { offset: pager.offset, limit: pager.limit, @@ -124,6 +126,54 @@ export class SandboxComponent implements OnInit { .then(txt => this.toast.success(txt)); }, 4000); } + + + + testParser() { + + const template = ` +
+
This item needs to be routed to + {{dest_location.shortname}} AKA {{dest_location.name}} +
+
i should disappear
+
i should also disappear
+
i should stick around
+
+
{{dest_address.street1}}
+
{{dest_address.street2}}
+
+
Slip Date: {{today}}
+
    +
  1. +
    Barcode: {{copy.barcode}}
    +
    Title: {{copy.title}}
    +
  2. +
+ `; + + const context = { + dest_location: { + shortname: 'BR1', + name: 'Branch Uno', + }, + dest_address: { + street1: '123 Pineapple Rd', + street2: 'APT #3', + }, + boolTrue: true, + boolFalse: false, + today: new Date(), + copies: [ + {barcode: 'abc123', title: 'welcome to the jungle'}, + {barcode: 'def456', title: 'hello mudda, hello fadda'} + ] + } + + this.parser.apply(template, context).then(html => { + console.log('parsed: ', html); + }); + } }