2015-09-14 18:59:09 -04:00
|
|
|
import {
|
|
|
|
ddescribe,
|
|
|
|
describe,
|
|
|
|
xdescribe,
|
|
|
|
it,
|
|
|
|
iit,
|
|
|
|
xit,
|
|
|
|
expect,
|
|
|
|
beforeEach,
|
|
|
|
afterEach,
|
|
|
|
AsyncTestCompleter,
|
|
|
|
inject,
|
|
|
|
beforeEachBindings
|
|
|
|
} from 'angular2/test_lib';
|
|
|
|
|
|
|
|
import {Promise, PromiseWrapper} from 'angular2/src/core/facade/async';
|
|
|
|
import {Type, isPresent, isBlank, stringify, isString} from 'angular2/src/core/facade/lang';
|
|
|
|
import {MapWrapper, SetWrapper, ListWrapper} from 'angular2/src/core/facade/collection';
|
|
|
|
import {RuntimeMetadataResolver} from 'angular2/src/compiler/runtime_metadata';
|
|
|
|
import {
|
|
|
|
TemplateCompiler,
|
|
|
|
NormalizedComponentWithViewDirectives
|
|
|
|
} from 'angular2/src/compiler/template_compiler';
|
2015-09-18 13:33:23 -04:00
|
|
|
import {CompileDirectiveMetadata} from 'angular2/src/compiler/directive_metadata';
|
2015-09-14 18:59:09 -04:00
|
|
|
import {evalModule} from './eval_module';
|
|
|
|
import {SourceModule, moduleRef} from 'angular2/src/compiler/source_module';
|
|
|
|
import {XHR} from 'angular2/src/core/render/xhr';
|
|
|
|
import {MockXHR} from 'angular2/src/core/render/xhr_mock';
|
|
|
|
|
|
|
|
import {Locals} from 'angular2/src/core/change_detection/change_detection';
|
|
|
|
|
|
|
|
import {
|
|
|
|
CommandVisitor,
|
|
|
|
TextCmd,
|
|
|
|
NgContentCmd,
|
|
|
|
BeginElementCmd,
|
|
|
|
BeginComponentCmd,
|
|
|
|
EmbeddedTemplateCmd,
|
|
|
|
TemplateCmd,
|
|
|
|
visitAllCommands,
|
|
|
|
CompiledTemplate
|
|
|
|
} from 'angular2/src/core/compiler/template_commands';
|
|
|
|
|
2015-09-28 13:30:33 -04:00
|
|
|
import {Component, View, Directive, bind} from 'angular2/core';
|
2015-09-14 18:59:09 -04:00
|
|
|
|
|
|
|
import {TEST_BINDINGS} from './test_bindings';
|
|
|
|
import {TestContext, TestDispatcher, TestPipes} from './change_detector_mocks';
|
|
|
|
import {codeGenValueFn, codeGenExportVariable} from 'angular2/src/compiler/util';
|
2015-09-28 13:30:33 -04:00
|
|
|
import {APP_ID} from 'angular2/src/core/render/dom/dom_tokens';
|
2015-09-14 18:59:09 -04:00
|
|
|
|
|
|
|
// Attention: This path has to point to this test file!
|
|
|
|
const THIS_MODULE = 'angular2/test/compiler/template_compiler_spec';
|
|
|
|
var THIS_MODULE_REF = moduleRef(THIS_MODULE);
|
|
|
|
|
2015-09-28 13:30:33 -04:00
|
|
|
const APP_ID_VALUE = 'app1';
|
|
|
|
|
2015-09-14 18:59:09 -04:00
|
|
|
export function main() {
|
|
|
|
describe('TemplateCompiler', () => {
|
|
|
|
var compiler: TemplateCompiler;
|
|
|
|
var runtimeMetadataResolver: RuntimeMetadataResolver;
|
|
|
|
|
2015-09-28 13:30:33 -04:00
|
|
|
beforeEachBindings(() => [bind(APP_ID).toValue(APP_ID_VALUE), TEST_BINDINGS]);
|
2015-09-14 18:59:09 -04:00
|
|
|
beforeEach(inject([TemplateCompiler, RuntimeMetadataResolver],
|
|
|
|
(_compiler, _runtimeMetadataResolver) => {
|
|
|
|
compiler = _compiler;
|
|
|
|
runtimeMetadataResolver = _runtimeMetadataResolver;
|
|
|
|
}));
|
|
|
|
|
|
|
|
describe('compile templates', () => {
|
|
|
|
|
|
|
|
function runTests(compile) {
|
2015-09-18 13:33:23 -04:00
|
|
|
it('should throw for non components', inject([AsyncTestCompleter], (async) => {
|
|
|
|
PromiseWrapper.catchError(PromiseWrapper.wrap(() => compile([NonComponent])), (error) => {
|
|
|
|
expect(error.message)
|
|
|
|
.toEqual(
|
|
|
|
`Could not compile '${stringify(NonComponent)}' because it is not a component.`);
|
|
|
|
async.done();
|
|
|
|
});
|
|
|
|
}));
|
|
|
|
|
2015-09-14 18:59:09 -04:00
|
|
|
it('should compile host components', inject([AsyncTestCompleter], (async) => {
|
|
|
|
compile([CompWithBindingsAndStyles])
|
|
|
|
.then((humanizedTemplate) => {
|
|
|
|
expect(humanizedTemplate['styles']).toEqual([]);
|
|
|
|
expect(humanizedTemplate['commands'][0]).toEqual('<comp-a>');
|
|
|
|
expect(humanizedTemplate['cd']).toEqual(['elementProperty(title)=someDirValue']);
|
|
|
|
|
|
|
|
async.done();
|
|
|
|
});
|
|
|
|
}));
|
|
|
|
|
|
|
|
it('should compile nested components', inject([AsyncTestCompleter], (async) => {
|
|
|
|
compile([CompWithBindingsAndStyles])
|
|
|
|
.then((humanizedTemplate) => {
|
|
|
|
var nestedTemplate = humanizedTemplate['commands'][1];
|
|
|
|
expect(nestedTemplate['styles']).toEqual(['div {color: red}']);
|
|
|
|
expect(nestedTemplate['commands'][0]).toEqual('<a>');
|
|
|
|
expect(nestedTemplate['cd']).toEqual(['elementProperty(href)=someCtxValue']);
|
|
|
|
|
|
|
|
async.done();
|
|
|
|
});
|
|
|
|
}));
|
|
|
|
|
|
|
|
it('should compile recursive components', inject([AsyncTestCompleter], (async) => {
|
|
|
|
compile([TreeComp])
|
|
|
|
.then((humanizedTemplate) => {
|
|
|
|
expect(humanizedTemplate['commands'][0]).toEqual('<tree>');
|
|
|
|
expect(humanizedTemplate['commands'][1]['commands'][0]).toEqual('<tree>');
|
|
|
|
expect(humanizedTemplate['commands'][1]['commands'][1]['commands'][0])
|
|
|
|
.toEqual('<tree>');
|
|
|
|
|
|
|
|
async.done();
|
|
|
|
});
|
|
|
|
}));
|
2015-09-18 13:33:23 -04:00
|
|
|
|
|
|
|
it('should pass the right change detector to embedded templates',
|
|
|
|
inject([AsyncTestCompleter], (async) => {
|
|
|
|
compile([CompWithEmbeddedTemplate])
|
|
|
|
.then((humanizedTemplate) => {
|
|
|
|
expect(humanizedTemplate['commands'][1]['commands'][0]).toEqual('<template>');
|
|
|
|
expect(humanizedTemplate['commands'][1]['commands'][1]['cd'])
|
|
|
|
.toEqual(['elementProperty(href)=someCtxValue']);
|
|
|
|
|
|
|
|
async.done();
|
|
|
|
});
|
|
|
|
}));
|
2015-09-14 18:59:09 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
describe('compileHostComponentRuntime', () => {
|
|
|
|
function compile(components: Type[]): Promise<any[]> {
|
2015-09-28 13:30:33 -04:00
|
|
|
return compiler.compileHostComponentRuntime(components[0])
|
|
|
|
.then((compiledHostTemplate) => humanizeTemplate(compiledHostTemplate.getTemplate()));
|
2015-09-14 18:59:09 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
runTests(compile);
|
|
|
|
|
2015-09-18 13:33:23 -04:00
|
|
|
it('should cache components for parallel requests',
|
|
|
|
inject([AsyncTestCompleter, XHR], (async, xhr: MockXHR) => {
|
|
|
|
xhr.expect('angular2/test/compiler/compUrl.html', 'a');
|
|
|
|
PromiseWrapper.all([compile([CompWithTemplateUrl]), compile([CompWithTemplateUrl])])
|
2015-09-14 18:59:09 -04:00
|
|
|
.then((humanizedTemplates) => {
|
2015-09-18 13:33:23 -04:00
|
|
|
expect(humanizedTemplates[0]['commands'][1]['commands']).toEqual(['#text(a)']);
|
|
|
|
expect(humanizedTemplates[1]['commands'][1]['commands']).toEqual(['#text(a)']);
|
|
|
|
|
2015-09-14 18:59:09 -04:00
|
|
|
async.done();
|
|
|
|
});
|
|
|
|
xhr.flush();
|
|
|
|
}));
|
|
|
|
|
2015-09-18 13:33:23 -04:00
|
|
|
it('should cache components for sequential requests',
|
|
|
|
inject([AsyncTestCompleter, XHR], (async, xhr: MockXHR) => {
|
|
|
|
xhr.expect('angular2/test/compiler/compUrl.html', 'a');
|
|
|
|
compile([CompWithTemplateUrl])
|
|
|
|
.then((humanizedTemplate0) => {
|
|
|
|
return compile([CompWithTemplateUrl])
|
|
|
|
.then((humanizedTemplate1) => {
|
|
|
|
expect(humanizedTemplate0['commands'][1]['commands'])
|
|
|
|
.toEqual(['#text(a)']);
|
|
|
|
expect(humanizedTemplate1['commands'][1]['commands'])
|
|
|
|
.toEqual(['#text(a)']);
|
|
|
|
async.done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
xhr.flush();
|
|
|
|
}));
|
|
|
|
|
|
|
|
it('should allow to clear the cache',
|
|
|
|
inject([AsyncTestCompleter, XHR], (async, xhr: MockXHR) => {
|
|
|
|
xhr.expect('angular2/test/compiler/compUrl.html', 'a');
|
|
|
|
compile([CompWithTemplateUrl])
|
|
|
|
.then((humanizedTemplate) => {
|
|
|
|
compiler.clearCache();
|
|
|
|
xhr.expect('angular2/test/compiler/compUrl.html', 'b');
|
|
|
|
var result = compile([CompWithTemplateUrl]);
|
|
|
|
xhr.flush();
|
|
|
|
return result;
|
|
|
|
})
|
|
|
|
.then((humanizedTemplate) => {
|
|
|
|
expect(humanizedTemplate['commands'][1]['commands']).toEqual(['#text(b)']);
|
|
|
|
async.done();
|
|
|
|
});
|
|
|
|
xhr.flush();
|
|
|
|
}));
|
2015-09-14 18:59:09 -04:00
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('compileTemplatesCodeGen', () => {
|
|
|
|
function normalizeComponent(component: Type):
|
|
|
|
Promise<NormalizedComponentWithViewDirectives> {
|
|
|
|
var compAndViewDirMetas = [runtimeMetadataResolver.getMetadata(component)].concat(
|
|
|
|
runtimeMetadataResolver.getViewDirectivesMetadata(component));
|
2015-09-17 12:58:18 -04:00
|
|
|
return PromiseWrapper.all(compAndViewDirMetas.map(
|
|
|
|
meta => compiler.normalizeDirectiveMetadata(meta)))
|
2015-09-18 13:33:23 -04:00
|
|
|
.then((normalizedCompAndViewDirMetas: CompileDirectiveMetadata[]) =>
|
2015-09-14 18:59:09 -04:00
|
|
|
new NormalizedComponentWithViewDirectives(
|
|
|
|
normalizedCompAndViewDirMetas[0],
|
|
|
|
normalizedCompAndViewDirMetas.slice(1)));
|
|
|
|
}
|
|
|
|
|
|
|
|
function compile(components: Type[]): Promise<any[]> {
|
|
|
|
return PromiseWrapper.all(components.map(normalizeComponent))
|
|
|
|
.then((normalizedCompWithViewDirMetas: NormalizedComponentWithViewDirectives[]) => {
|
|
|
|
var sourceModule =
|
|
|
|
compiler.compileTemplatesCodeGen(THIS_MODULE, normalizedCompWithViewDirMetas);
|
|
|
|
var sourceWithImports =
|
|
|
|
testableTemplateModule(sourceModule,
|
|
|
|
normalizedCompWithViewDirMetas[0].component)
|
|
|
|
.getSourceWithImports();
|
|
|
|
return evalModule(sourceWithImports.source, sourceWithImports.imports, null);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
runTests(compile);
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
2015-09-17 12:58:18 -04:00
|
|
|
describe('normalizeDirectiveMetadata', () => {
|
2015-09-18 13:33:23 -04:00
|
|
|
it('should return the given DirectiveMetadata for non components',
|
|
|
|
inject([AsyncTestCompleter], (async) => {
|
|
|
|
var meta = runtimeMetadataResolver.getMetadata(NonComponent);
|
|
|
|
compiler.normalizeDirectiveMetadata(meta).then(normMeta => {
|
|
|
|
expect(normMeta).toBe(meta);
|
|
|
|
async.done();
|
|
|
|
});
|
|
|
|
}));
|
|
|
|
|
2015-09-14 18:59:09 -04:00
|
|
|
it('should normalize the template',
|
|
|
|
inject([AsyncTestCompleter, XHR], (async, xhr: MockXHR) => {
|
|
|
|
xhr.expect('angular2/test/compiler/compUrl.html', 'loadedTemplate');
|
2015-09-17 12:58:18 -04:00
|
|
|
compiler.normalizeDirectiveMetadata(
|
|
|
|
runtimeMetadataResolver.getMetadata(CompWithTemplateUrl))
|
2015-09-18 13:33:23 -04:00
|
|
|
.then((meta: CompileDirectiveMetadata) => {
|
2015-09-14 18:59:09 -04:00
|
|
|
expect(meta.template.template).toEqual('loadedTemplate');
|
|
|
|
async.done();
|
|
|
|
});
|
|
|
|
xhr.flush();
|
|
|
|
}));
|
|
|
|
|
|
|
|
it('should copy all the other fields', inject([AsyncTestCompleter], (async) => {
|
|
|
|
var meta = runtimeMetadataResolver.getMetadata(CompWithBindingsAndStyles);
|
2015-09-18 13:33:23 -04:00
|
|
|
compiler.normalizeDirectiveMetadata(meta).then((normMeta: CompileDirectiveMetadata) => {
|
|
|
|
expect(normMeta.type).toEqual(meta.type);
|
|
|
|
expect(normMeta.isComponent).toEqual(meta.isComponent);
|
|
|
|
expect(normMeta.dynamicLoadable).toEqual(meta.dynamicLoadable);
|
|
|
|
expect(normMeta.selector).toEqual(meta.selector);
|
|
|
|
expect(normMeta.exportAs).toEqual(meta.exportAs);
|
|
|
|
expect(normMeta.changeDetection).toEqual(meta.changeDetection);
|
|
|
|
expect(normMeta.properties).toEqual(meta.properties);
|
|
|
|
expect(normMeta.events).toEqual(meta.events);
|
|
|
|
expect(normMeta.hostListeners).toEqual(meta.hostListeners);
|
|
|
|
expect(normMeta.hostProperties).toEqual(meta.hostProperties);
|
|
|
|
expect(normMeta.hostAttributes).toEqual(meta.hostAttributes);
|
|
|
|
expect(normMeta.lifecycleHooks).toEqual(meta.lifecycleHooks);
|
|
|
|
async.done();
|
|
|
|
});
|
2015-09-14 18:59:09 -04:00
|
|
|
}));
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('compileStylesheetCodeGen', () => {
|
|
|
|
it('should compile stylesheets into code', inject([AsyncTestCompleter], (async) => {
|
|
|
|
var cssText = 'div {color: red}';
|
|
|
|
var sourceModule = compiler.compileStylesheetCodeGen('someModuleId', cssText)[0];
|
|
|
|
var sourceWithImports = testableStylesModule(sourceModule).getSourceWithImports();
|
|
|
|
evalModule(sourceWithImports.source, sourceWithImports.imports, null)
|
|
|
|
.then(loadedCssText => {
|
|
|
|
expect(loadedCssText).toEqual([cssText]);
|
|
|
|
async.done();
|
|
|
|
});
|
|
|
|
|
|
|
|
}));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'comp-a',
|
|
|
|
host: {'[title]': 'someProp'},
|
2015-09-18 13:33:23 -04:00
|
|
|
moduleId: THIS_MODULE,
|
|
|
|
exportAs: 'someExportAs'
|
2015-09-14 18:59:09 -04:00
|
|
|
})
|
|
|
|
@View({template: '<a [href]="someProp"></a>', styles: ['div {color: red}']})
|
|
|
|
class CompWithBindingsAndStyles {
|
|
|
|
}
|
|
|
|
|
2015-09-18 13:33:23 -04:00
|
|
|
@Component({selector: 'tree', moduleId: THIS_MODULE})
|
2015-09-14 18:59:09 -04:00
|
|
|
@View({template: '<tree></tree>', directives: [TreeComp]})
|
|
|
|
class TreeComp {
|
|
|
|
}
|
|
|
|
|
2015-09-18 13:33:23 -04:00
|
|
|
@Component({selector: 'comp-url', moduleId: THIS_MODULE})
|
2015-09-14 18:59:09 -04:00
|
|
|
@View({templateUrl: 'compUrl.html'})
|
|
|
|
class CompWithTemplateUrl {
|
|
|
|
}
|
|
|
|
|
2015-09-18 13:33:23 -04:00
|
|
|
@Component({selector: 'comp-tpl', moduleId: THIS_MODULE})
|
|
|
|
@View({template: '<template><a [href]="someProp"></a></template>'})
|
|
|
|
class CompWithEmbeddedTemplate {
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-09-14 18:59:09 -04:00
|
|
|
@Directive({selector: 'plain', moduleId: THIS_MODULE})
|
2015-09-18 13:33:23 -04:00
|
|
|
@View({template: ''})
|
|
|
|
class NonComponent {
|
2015-09-14 18:59:09 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
@Component({selector: 'comp', moduleId: THIS_MODULE})
|
|
|
|
@View({template: ''})
|
|
|
|
class CompWithoutHost {
|
|
|
|
}
|
|
|
|
|
2015-09-18 13:33:23 -04:00
|
|
|
function testableTemplateModule(sourceModule: SourceModule, normComp: CompileDirectiveMetadata):
|
2015-09-14 18:59:09 -04:00
|
|
|
SourceModule {
|
2015-09-28 13:30:33 -04:00
|
|
|
var resultExpression =
|
|
|
|
`${THIS_MODULE_REF}humanizeTemplate(Host${normComp.type.name}Template.getTemplate())`;
|
2015-09-17 12:58:18 -04:00
|
|
|
var testableSource = `${sourceModule.sourceWithModuleRefs}
|
2015-09-14 18:59:09 -04:00
|
|
|
${codeGenExportVariable('run')}${codeGenValueFn(['_'], resultExpression)};`;
|
|
|
|
return new SourceModule(sourceModule.moduleId, testableSource);
|
|
|
|
}
|
|
|
|
|
|
|
|
function testableStylesModule(sourceModule: SourceModule): SourceModule {
|
2015-09-17 12:58:18 -04:00
|
|
|
var testableSource = `${sourceModule.sourceWithModuleRefs}
|
2015-09-14 18:59:09 -04:00
|
|
|
${codeGenExportVariable('run')}${codeGenValueFn(['_'], 'STYLES')};`;
|
|
|
|
return new SourceModule(sourceModule.moduleId, testableSource);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Attention: read by eval!
|
|
|
|
export function humanizeTemplate(template: CompiledTemplate,
|
|
|
|
humanizedTemplates: Map<number, StringMap<string, any>> = null):
|
|
|
|
StringMap<string, any> {
|
|
|
|
if (isBlank(humanizedTemplates)) {
|
2015-09-29 14:11:06 -04:00
|
|
|
humanizedTemplates = new Map<number, StringMap<string, any>>();
|
2015-09-14 18:59:09 -04:00
|
|
|
}
|
|
|
|
var result = humanizedTemplates.get(template.id);
|
|
|
|
if (isPresent(result)) {
|
|
|
|
return result;
|
|
|
|
}
|
2015-09-28 13:30:33 -04:00
|
|
|
var templateData = template.getData(APP_ID_VALUE);
|
2015-09-14 18:59:09 -04:00
|
|
|
var commands = [];
|
2015-09-18 13:33:23 -04:00
|
|
|
result = {
|
2015-09-28 13:30:33 -04:00
|
|
|
'styles': templateData.styles,
|
2015-09-18 13:33:23 -04:00
|
|
|
'commands': commands,
|
2015-09-28 13:30:33 -04:00
|
|
|
'cd': testChangeDetector(templateData.changeDetectorFactory)
|
2015-09-18 13:33:23 -04:00
|
|
|
};
|
2015-09-14 18:59:09 -04:00
|
|
|
humanizedTemplates.set(template.id, result);
|
2015-09-28 13:30:33 -04:00
|
|
|
visitAllCommands(new CommandHumanizer(commands, humanizedTemplates), templateData.commands);
|
2015-09-14 18:59:09 -04:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function testChangeDetector(changeDetectorFactory: Function): string[] {
|
|
|
|
var ctx = new TestContext();
|
|
|
|
ctx.someProp = 'someCtxValue';
|
|
|
|
var dir1 = new TestContext();
|
|
|
|
dir1.someProp = 'someDirValue';
|
|
|
|
|
|
|
|
var dispatcher = new TestDispatcher([dir1], []);
|
|
|
|
var cd = changeDetectorFactory(dispatcher);
|
|
|
|
var locals = new Locals(null, MapWrapper.createFromStringMap({'someVar': null}));
|
|
|
|
cd.hydrate(ctx, locals, dispatcher, new TestPipes());
|
|
|
|
cd.detectChanges();
|
|
|
|
return dispatcher.log;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class CommandHumanizer implements CommandVisitor {
|
|
|
|
constructor(private result: any[],
|
|
|
|
private humanizedTemplates: Map<number, StringMap<string, any>>) {}
|
2015-09-18 13:33:23 -04:00
|
|
|
visitText(cmd: TextCmd, context: any): any {
|
|
|
|
this.result.push(`#text(${cmd.value})`);
|
|
|
|
return null;
|
|
|
|
}
|
2015-09-14 18:59:09 -04:00
|
|
|
visitNgContent(cmd: NgContentCmd, context: any): any { return null; }
|
|
|
|
visitBeginElement(cmd: BeginElementCmd, context: any): any {
|
|
|
|
this.result.push(`<${cmd.name}>`);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
visitEndElement(context: any): any {
|
|
|
|
this.result.push('</>');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
visitBeginComponent(cmd: BeginComponentCmd, context: any): any {
|
|
|
|
this.result.push(`<${cmd.name}>`);
|
|
|
|
this.result.push(humanizeTemplate(cmd.template, this.humanizedTemplates));
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
visitEndComponent(context: any): any { return this.visitEndElement(context); }
|
2015-09-18 13:33:23 -04:00
|
|
|
visitEmbeddedTemplate(cmd: EmbeddedTemplateCmd, context: any): any {
|
|
|
|
this.result.push(`<template>`);
|
|
|
|
this.result.push({'cd': testChangeDetector(cmd.changeDetectorFactory)});
|
|
|
|
this.result.push(`</template>`);
|
|
|
|
return null;
|
|
|
|
}
|
2015-09-14 18:59:09 -04:00
|
|
|
}
|