/**
* 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';
import {BaseRoutingModule} from './routing.module';
import {WelcomeComponent} from './welcome.component';
-// Import and 'provide' globally required services.
@NgModule({
declarations: [
BaseComponent,
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
OrgService,
PrintService,
AudioService,
- FormatService,
- TemplateParserService
+ FormatService
]
};
}
sort: string;
fieldClass: string[];
query: string[];
- identQuery: string;
+ identQuery: string;
identQueryType: string; // isbn, issn, etc.
joinOp: string[];
matchOp: string[];
}
idlInfoFromDotpath(dotpath: string): any {
- if (!dotpath) return null;
+ if (!dotpath) { return null; }
let idlParent;
let idlField;
if (idlField) {
if (idlField['class'] && (
- idlField.datatype === 'link' ||
+ idlField.datatype === 'link' ||
idlField.datatype === 'org_unit')) {
idlClass = this.idl.classes[idlField['class']];
}
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.
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) {
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',
// 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
) {}
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.
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
});
*/
}
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()
+++ /dev/null
-/**
- * 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));
- });
- }
-}
-
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.
return {
ngModule: StaffCommonModule,
providers: [ // Export staff-wide services
+ LegacyTemplateService,
AccessKeyService,
StringService,
ToastService
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'
private strings: StringService,
private toast: ToastService,
private printer: PrintService,
- private parser: TemplateParserService
+ private parser: LegacyTemplateService
) {}
ngOnInit() {
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'
- });
}
}
--- /dev/null
+/**
+ * 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));
+ });
+ }
+}
+