@rdkmaster/jigsaw-labs
Version:
Jigsaw, the next generation component set for RDK
220 lines (187 loc) • 8.27 kB
text/typescript
import {Directive, HostBinding, Input, NgModule, OnDestroy, OnInit} from "@angular/core";
import {DomSanitizer} from "@angular/platform-browser";
import {CommonUtils} from "../../core/utils/common-utils";
export type HtmlCallback = (...args) => any;
type CallbackValues = { [callbackName: string]: HtmlCallback };
({
selector: '[trustedHtml]',
})
export class JigsawTrustedHtml implements OnInit, OnDestroy {
private static _callbacks = new Map<any, CallbackValues>();
private static _contexts: { context: any, counter: number }[] = [];
private static _getContext(magicNumber: number): any {
return JigsawTrustedHtml._contexts[magicNumber];
}
private static _registerContext(context: any): number {
if (CommonUtils.isUndefined(context)) {
return -1;
}
const index = JigsawTrustedHtml._contexts.findIndex(i => i && i.context === context);
let info = JigsawTrustedHtml._contexts[index];
if (CommonUtils.isUndefined(info)) {
info = {context: context, counter: 1};
JigsawTrustedHtml._contexts.push(info);
} else {
info.counter++;
}
return index;
}
private static _jigsawInternalCallbackWrapper(callbackName: string, contextMagicNumber: number, ...args) {
const contextInfo = JigsawTrustedHtml._getContext(contextMagicNumber);
if (CommonUtils.isUndefined(contextInfo)) {
console.error('no context found by magic number, callbackName = ' + callbackName);
return;
}
const callbacks = JigsawTrustedHtml._callbacks.get(contextInfo.context);
if (CommonUtils.isUndefined(callbacks)) {
console.error('no callback cache info found by magic number, callbackName = ' + callbackName);
return;
}
const callback = callbacks[callbackName];
if (!(callback instanceof Function)) {
console.error(`no callback function named "${callbackName}" found`);
console.log(`Hint: add a member method named ${callbackName} to the context object.`);
return;
}
CommonUtils.safeInvokeCallback(contextInfo.context, callback, args);
}
private static _declareCallback(context: any, name: string, callback: HtmlCallback) {
if (CommonUtils.isUndefined(context)) {
console.error(`invalid context for callback "{$name}"`);
return;
}
if (CommonUtils.isUndefined(callback)) {
console.error(`invalid callback "${name}", it is undefined.`);
return;
}
if (!(callback instanceof Function)) {
console.error(`invalid callback "${name}", it is not a function.`);
return;
}
let callbacks = JigsawTrustedHtml._callbacks.get(context);
if (CommonUtils.isUndefined(callbacks)) {
callbacks = {};
JigsawTrustedHtml._callbacks.set(context, callbacks);
}
callbacks[name] = callback;
}
private static _clearCallbacks(context: any) {
const idx = JigsawTrustedHtml._contexts.findIndex(i => i && i.context === context);
if (idx == -1) {
return;
}
const info = JigsawTrustedHtml._contexts[idx];
info.counter--;
if (info.counter == 0) {
JigsawTrustedHtml._callbacks.delete(context);
info.context = null;
info.counter = -1;
JigsawTrustedHtml._contexts[idx] = null;
}
}
//====================================================
constructor(private _sanitizer: DomSanitizer) {
if (!window.hasOwnProperty('_jigsawInternalCallbackWrapper') || !(window['_jigsawInternalCallbackWrapper'] instanceof Function)) {
window['_jigsawInternalCallbackWrapper'] = JigsawTrustedHtml._jigsawInternalCallbackWrapper;
}
}
private _trustedHtmlContext: any;
private _initialized: boolean = false;
()
public get trustedHtmlContext(): any {
return this._trustedHtmlContext;
}
public set trustedHtmlContext(value: any) {
if (CommonUtils.isUndefined(value)) {
return;
}
this._trustedHtmlContext = value;
this._updateHtml();
}
private _safeHtml: any;
private _trustedHtml: string;
()
public get trustedHtml(): string {
return this._trustedHtml;
}
public set trustedHtml(value: string) {
this._trustedHtml = CommonUtils.isDefined(value) ? value.trim() : '';
this._updateHtml();
}
private _modifiedHtml: string;
private _updateHtml(): void {
if (!this._trustedHtml || !this._initialized) {
return;
}
const modifiedHtml = !this._trustedHtmlContext ? this._trustedHtml : this._trustedHtml
.replace(/(on|\()(\w+)\)?\s*=(['"])\s*([_$a-z][_$a-z0-9.]*)\s*\((.*?)\)/ig,
(found, prefix, event, quot, funcAccessor, args) => this._replacer(`on${event}=${quot}`, funcAccessor, args))
.replace(/(javascript\s*:)\s*([_$a-z][_$a-z0-9]*)\s*\((.*?)\)/ig,
(found, jsPrefix, funcAccessor, args) => this._replacer(jsPrefix, funcAccessor, args));
if (modifiedHtml != this._modifiedHtml || !this._safeHtml) {
this._modifiedHtml = modifiedHtml;
this._safeHtml = this._sanitizer.bypassSecurityTrustHtml(modifiedHtml);
}
}
private _replacer(prefix, funcAccessor, args) {
const [realContext, callback] = this._getCallback(this._trustedHtmlContext, funcAccessor);
const magicNumber = this._registerContext(realContext);
JigsawTrustedHtml._declareCallback(realContext, funcAccessor, callback);
const modified = `${prefix}_jigsawInternalCallbackWrapper("${funcAccessor}",${magicNumber}`;
args = CommonUtils.isDefined(args) ? args.trim() : '';
return modified + (!!args ? ',' + args + ')' : ')');
}
private _getCallback(context: any, accessor: string | string[]): [any, HtmlCallback] {
if (CommonUtils.isUndefined(context) || CommonUtils.isUndefined(accessor)) {
return [null, null];
}
const accessors = accessor instanceof Array ? accessor : accessor.split(/\./);
if (accessors[0] == 'this') {
// 例如 this.aa.bb() 这里的this指context本身,要跳过
// 这里可能会误伤这样的情况 this.aa.this.bb(),因为实际情况下是不可能有这样的属性链的,因此无视这个情况
accessors.shift();
}
const tmp: any = context[accessors[0]];
if (accessors.length == 1) {
return [context, tmp];
}
accessors.shift();
return this._getCallback(tmp, accessors);
}
private _registeredContexts: any[] = [];
private _registerContext(context: any): number {
if (CommonUtils.isUndefined(context)) {
return -1;
}
if (!this._registeredContexts.find(ctx => ctx === context)) {
JigsawTrustedHtml._registerContext(context);
this._registeredContexts.push(context);
}
return this._getMagicNumber(context);
}
private _getMagicNumber(context: any): number {
return JigsawTrustedHtml._contexts.findIndex(i => i && i.context === context);
}
('innerHtml')
public get innerHtml(): any {
return this._safeHtml ? this._safeHtml : '';
}
ngOnInit() {
this._initialized = true;
this._updateHtml();
}
ngOnDestroy() {
this._registeredContexts.forEach(ctx => JigsawTrustedHtml._clearCallbacks(ctx));
this._registeredContexts = null;
this._trustedHtmlContext = null;
this._trustedHtml = null;
this._safeHtml = null;
}
}
({
imports: [],
declarations: [JigsawTrustedHtml],
exports: [JigsawTrustedHtml]
})
export class JigsawTrustedHtmlModule {
}