From 9e44dd85ada181b11be869841da2c157b095ee07 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Mon, 23 Nov 2015 22:07:57 -0800 Subject: [PATCH] feat(camelCase Angular): legacy template transformer --- .../angular2/src/compiler/legacy_template.ts | 214 +++++++++++++ .../test/compiler/legacy_template_spec.ts | 289 ++++++++++++++++++ 2 files changed, 503 insertions(+) create mode 100644 modules/angular2/src/compiler/legacy_template.ts create mode 100644 modules/angular2/test/compiler/legacy_template_spec.ts diff --git a/modules/angular2/src/compiler/legacy_template.ts b/modules/angular2/src/compiler/legacy_template.ts new file mode 100644 index 0000000000..33be52c603 --- /dev/null +++ b/modules/angular2/src/compiler/legacy_template.ts @@ -0,0 +1,214 @@ +import {Injectable, Provider, provide} from 'angular2/src/core/di'; + +import { + StringWrapper, + RegExpWrapper, + CONST_EXPR, + isBlank, + isPresent +} from 'angular2/src/facade/lang'; + +import {HtmlAstVisitor, HtmlAttrAst, HtmlElementAst, HtmlTextAst, HtmlAst} from './html_ast'; +import {HtmlParser, HtmlParseTreeResult} from './html_parser'; + +import {dashCaseToCamelCase, camelCaseToDashCase} from './util'; + +var LONG_SYNTAX_REGEXP = /^(?:on-(.*)|bindon-(.*)|bind-(.*)|var-(.*))$/ig; +var SHORT_SYNTAX_REGEXP = /^(?:\((.*)\)|\[\((.*)\)\]|\[(.*)\]|#(.*))$/ig; +var VARIABLE_TPL_BINDING_REGEXP = /(\bvar\s+|#)(\S+)/ig; +var TEMPLATE_SELECTOR_REGEXP = /^(\S+)/g; +var SPECIAL_PREFIXES_REGEXP = /^(class|style|attr)\./ig; +var INTERPOLATION_REGEXP = /\{\{.*?\}\}/g; + +const SPECIAL_CASES = CONST_EXPR([ + 'ng-non-bindable', + 'ng-default-control', + 'ng-no-form', +]); + +/** + * Convert templates to the case sensitive syntax + * + * @internal + */ +export class LegacyHtmlAstTransformer implements HtmlAstVisitor { + rewrittenAst: HtmlAst[] = []; + visitingTemplateEl: boolean = false; + + constructor(private dashCaseSelectors?: string[]) {} + + visitElement(ast: HtmlElementAst, context: any): HtmlElementAst { + this.visitingTemplateEl = ast.name.toLowerCase() == 'template'; + let attrs = ast.attrs.map(attr => attr.visit(this, null)); + let children = ast.children.map(child => child.visit(this, null)); + return new HtmlElementAst(ast.name, attrs, children, ast.sourceSpan); + } + + visitAttr(originalAst: HtmlAttrAst, context: any): HtmlAttrAst { + let ast = originalAst; + + if (this.visitingTemplateEl) { + if (isPresent(RegExpWrapper.firstMatch(LONG_SYNTAX_REGEXP, ast.name))) { + // preserve the "-" in the prefix for the long syntax + ast = this._rewriteLongSyntax(ast); + } else { + // rewrite any other attribute + let name = dashCaseToCamelCase(ast.name); + ast = name == ast.name ? ast : new HtmlAttrAst(name, ast.value, ast.sourceSpan); + } + } else { + ast = this._rewriteTemplateAttribute(ast); + ast = this._rewriteLongSyntax(ast); + ast = this._rewriteShortSyntax(ast); + ast = this._rewriteStar(ast); + ast = this._rewriteInterpolation(ast); + ast = this._rewriteSpecialCases(ast); + } + + if (ast !== originalAst) { + this.rewrittenAst.push(ast); + } + + return ast; + } + + visitText(ast: HtmlTextAst, context: any): HtmlTextAst { return ast; } + + private _rewriteLongSyntax(ast: HtmlAttrAst): HtmlAttrAst { + let m = RegExpWrapper.firstMatch(LONG_SYNTAX_REGEXP, ast.name); + let attrName = ast.name; + let attrValue = ast.value; + + if (isPresent(m)) { + if (isPresent(m[1])) { + attrName = `on-${dashCaseToCamelCase(m[1])}`; + } else if (isPresent(m[2])) { + attrName = `bindon-${dashCaseToCamelCase(m[2])}`; + } else if (isPresent(m[3])) { + attrName = `bind-${dashCaseToCamelCase(m[3])}`; + } else if (isPresent(m[4])) { + attrName = `var-${dashCaseToCamelCase(m[4])}`; + attrValue = dashCaseToCamelCase(attrValue); + } + } + + return attrName == ast.name && attrValue == ast.value ? + ast : + new HtmlAttrAst(attrName, attrValue, ast.sourceSpan); + } + + private _rewriteTemplateAttribute(ast: HtmlAttrAst): HtmlAttrAst { + let name = ast.name; + let value = ast.value; + + if (name.toLowerCase() == 'template') { + name = 'template'; + + // rewrite the directive selector + value = StringWrapper.replaceAllMapped(value, TEMPLATE_SELECTOR_REGEXP, + (m) => { return dashCaseToCamelCase(m[1]); }); + + // rewrite the var declarations + value = StringWrapper.replaceAllMapped(value, VARIABLE_TPL_BINDING_REGEXP, m => { + return `${m[1].toLowerCase()}${dashCaseToCamelCase(m[2])}`; + }); + } + + if (name == ast.name && value == ast.value) { + return ast; + } + + return new HtmlAttrAst(name, value, ast.sourceSpan); + } + + private _rewriteShortSyntax(ast: HtmlAttrAst): HtmlAttrAst { + let m = RegExpWrapper.firstMatch(SHORT_SYNTAX_REGEXP, ast.name); + let attrName = ast.name; + let attrValue = ast.value; + + if (isPresent(m)) { + if (isPresent(m[1])) { + attrName = `(${dashCaseToCamelCase(m[1])})`; + } else if (isPresent(m[2])) { + attrName = `[(${dashCaseToCamelCase(m[2])})]`; + } else if (isPresent(m[3])) { + let prop = StringWrapper.replaceAllMapped(m[3], SPECIAL_PREFIXES_REGEXP, + (m) => { return m[1].toLowerCase() + '.'; }); + + if (prop.startsWith('class.') || prop.startsWith('attr.') || prop.startsWith('style.')) { + attrName = `[${prop}]`; + } else { + attrName = `[${dashCaseToCamelCase(prop)}]`; + } + } else if (isPresent(m[4])) { + attrName = `#${dashCaseToCamelCase(m[4])}`; + attrValue = dashCaseToCamelCase(attrValue); + } + } + + return attrName == ast.name && attrValue == ast.value ? + ast : + new HtmlAttrAst(attrName, attrValue, ast.sourceSpan); + } + + private _rewriteStar(ast: HtmlAttrAst): HtmlAttrAst { + let attrName = ast.name; + let attrValue = ast.value; + + if (attrName[0] == '*') { + attrName = dashCaseToCamelCase(attrName); + // rewrite the var declarations + attrValue = StringWrapper.replaceAllMapped(attrValue, VARIABLE_TPL_BINDING_REGEXP, m => { + return `${m[1].toLowerCase()}${dashCaseToCamelCase(m[2])}`; + }); + } + + return attrName == ast.name && attrValue == ast.value ? + ast : + new HtmlAttrAst(attrName, attrValue, ast.sourceSpan); + } + + private _rewriteInterpolation(ast: HtmlAttrAst): HtmlAttrAst { + let hasInterpolation = RegExpWrapper.test(INTERPOLATION_REGEXP, ast.value); + + if (!hasInterpolation) { + return ast; + } + + let name = ast.name; + + if (!(name.startsWith('attr.') || name.startsWith('class.') || name.startsWith('style.'))) { + name = dashCaseToCamelCase(ast.name); + } + + return name == ast.name ? ast : new HtmlAttrAst(name, ast.value, ast.sourceSpan); + } + + private _rewriteSpecialCases(ast: HtmlAttrAst): HtmlAttrAst { + let attrName = ast.name; + + if (SPECIAL_CASES.indexOf(attrName) > -1) { + return new HtmlAttrAst(dashCaseToCamelCase(attrName), ast.value, ast.sourceSpan); + } + + if (isPresent(this.dashCaseSelectors) && this.dashCaseSelectors.indexOf(attrName) > -1) { + return new HtmlAttrAst(dashCaseToCamelCase(attrName), ast.value, ast.sourceSpan); + } + + return ast; + } +} + +@Injectable() +export class LegacyHtmlParser extends HtmlParser { + parse(sourceContent: string, sourceUrl: string): HtmlParseTreeResult { + let transformer = new LegacyHtmlAstTransformer(); + let htmlParseTreeResult = super.parse(sourceContent, sourceUrl); + + let rootNodes = htmlParseTreeResult.rootNodes.map(node => node.visit(transformer, null)); + + return transformer.rewrittenAst.length > 0 ? + new HtmlParseTreeResult(rootNodes, htmlParseTreeResult.errors) : + htmlParseTreeResult; + } +} diff --git a/modules/angular2/test/compiler/legacy_template_spec.ts b/modules/angular2/test/compiler/legacy_template_spec.ts new file mode 100644 index 0000000000..37ef47c32f --- /dev/null +++ b/modules/angular2/test/compiler/legacy_template_spec.ts @@ -0,0 +1,289 @@ +import { + TestComponentBuilder, + AsyncTestCompleter, + ddescribe, + describe, + it, + iit, + xit, + expect, + beforeEach, + afterEach, + beforeEachProviders, + inject +} from 'angular2/testing_internal'; + +import { + HtmlAst, + HtmlAstVisitor, + HtmlElementAst, + HtmlAttrAst, + HtmlTextAst, + htmlVisitAll +} from 'angular2/src/compiler/html_ast'; + +import {LegacyHtmlAstTransformer} from 'angular2/src/compiler/legacy_template'; + +export function main() { + describe('Support for legacy template', () => { + + describe('Template rewriting', () => { + let visitor; + + beforeEach(() => { visitor = new LegacyHtmlAstTransformer(['yes-mapped']); }); + + describe('non template elements', () => { + it('should rewrite event binding', () => { + let fixtures = [ + {'from': 'on-dash-case', 'to': 'on-dashCase'}, + {'from': 'ON-dash-case', 'to': 'on-dashCase'}, + {'from': 'bindon-dash-case', 'to': 'bindon-dashCase'}, + {'from': '(dash-case)', 'to': '(dashCase)'}, + {'from': '[(dash-case)]', 'to': '[(dashCase)]'}, + {'from': 'on-camelCase', 'to': 'on-camelCase'}, + {'from': 'bindon-camelCase', 'to': 'bindon-camelCase'}, + {'from': '(camelCase)', 'to': '(camelCase)'}, + {'from': '[(camelCase)]', 'to': '[(camelCase)]'}, + ]; + + fixtures.forEach((f) => { + let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null); + let attr = visitor.visitAttr(legacyAttr, null); + expect(attr.name).toEqual(f['to']); + expect(attr.value).toEqual('expression'); + }); + }); + + it('should not rewrite style binding', () => { + let fixtures = [ + {'from': '[style.background-color]', 'to': '[style.background-color]'}, + {'from': '[style.margin-top.px]', 'to': '[style.margin-top.px]'}, + {'from': '[style.camelCase]', 'to': '[style.camelCase]'}, + {'from': '[STYLE.camelCase]', 'to': '[style.camelCase]'}, + ]; + + fixtures.forEach((f) => { + let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null); + let attr = visitor.visitAttr(legacyAttr, null); + expect(attr.name).toEqual(f['to']); + expect(attr.value).toEqual('expression'); + }); + }); + + it('should not rewrite attribute bindings', () => { + let fixtures = [ + {'from': '[attr.my-attr]', 'to': '[attr.my-attr]'}, + {'from': '[ATTR.my-attr]', 'to': '[attr.my-attr]'}, + ]; + + fixtures.forEach((f) => { + let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null); + let attr = visitor.visitAttr(legacyAttr, null); + expect(attr.name).toEqual(f['to']); + expect(attr.value).toEqual('expression'); + }); + }); + + it('should not rewrite class bindings', () => { + let fixtures = [ + {'from': '[class.my-class]', 'to': '[class.my-class]'}, + {'from': '[CLASS.my-class]', 'to': '[class.my-class]'}, + ]; + + fixtures.forEach((f) => { + let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null); + let attr = visitor.visitAttr(legacyAttr, null); + expect(attr.name).toEqual(f['to']); + expect(attr.value).toEqual('expression'); + }); + }); + + it('should rewrite variables', () => { + let fixtures = [ + {'from': '#dash-case', 'to': '#dashCase'}, + {'from': 'var-dash-case', 'to': 'var-dashCase'}, + {'from': 'VAR-dash-case', 'to': 'var-dashCase'}, + {'from': 'VAR-camelCase', 'to': 'var-camelCase'}, + ]; + + fixtures.forEach((f) => { + let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null); + let attr = visitor.visitAttr(legacyAttr, null); + expect(attr.name).toEqual(f['to']); + expect(attr.value).toEqual('expression'); + }); + }); + + it('should rewrite variable values', () => { + let fixtures = [ + {'from': 'dash-case', 'to': 'dashCase'}, + {'from': 'lower', 'to': 'lower'}, + {'from': 'camelCase', 'to': 'camelCase'}, + ]; + + fixtures.forEach((f) => { + let legacyAttr = new HtmlAttrAst('#a', f['from'], null); + let attr = visitor.visitAttr(legacyAttr, null); + expect(attr.name).toEqual('#a'); + expect(attr.value).toEqual(f['to']); + + legacyAttr = new HtmlAttrAst('var-a', f['from'], null); + attr = visitor.visitAttr(legacyAttr, null); + expect(attr.name).toEqual('var-a'); + expect(attr.value).toEqual(f['to']); + }); + }); + + + it('should rewrite variables in template bindings', () => { + let fixtures = [ + {'from': 'dir: #a-b', 'to': 'dir: #aB'}, + {'from': 'dir: var a-b', 'to': 'dir: var aB'}, + {'from': 'dir: VAR a-b;', 'to': 'dir: var aB;'}, + {'from': 'dir: VAR a-b; #c-d=e', 'to': 'dir: var aB; #cD=e'}, + {'from': 'dir: VAR aB; #cD=e', 'to': 'dir: var aB; #cD=e'}, + ]; + + fixtures.forEach((f) => { + let legacyAttr = new HtmlAttrAst('template', f['from'], null); + let attr = visitor.visitAttr(legacyAttr, null); + expect(attr.value).toEqual(f['to']); + }); + }); + + it('should lowercase the "template" attribute', () => { + let fixtures = ['Template', 'TEMPLATE', 'template']; + + fixtures.forEach((f) => { + let legacyAttr = new HtmlAttrAst(f, 'expression', null); + let attr = visitor.visitAttr(legacyAttr, null); + expect(attr.name).toEqual('template'); + expect(attr.value).toEqual('expression'); + }); + }); + + it('should rewrite property binding', () => { + let fixtures = [ + {'from': '[my-prop]', 'to': '[myProp]'}, + {'from': 'bind-my-prop', 'to': 'bind-myProp'}, + ]; + + fixtures.forEach((f) => { + let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null); + let attr = visitor.visitAttr(legacyAttr, null); + expect(attr.name).toEqual(f['to']); + expect(attr.value).toEqual('expression'); + }); + }); + + it('should rewrite structural directive selectors template="..."', () => { + let legacyAttr = new HtmlAttrAst('TEMPLATE', 'ng-if condition', null); + let attr = visitor.visitAttr(legacyAttr, null); + expect(attr.name).toEqual('template'); + expect(attr.value).toEqual('ngIf condition'); + + }); + + it('should rewrite *-selectors', () => { + let legacyAttr = new HtmlAttrAst('*ng-for', '#my-item of myItems', null); + let attr = visitor.visitAttr(legacyAttr, null); + expect(attr.name).toEqual('*ngFor'); + expect(attr.value).toEqual('#myItem of myItems'); + }); + + it('should rewrite directive special cases', () => { + let fixtures = [ + {'from': 'ng-non-bindable', 'to': 'ngNonBindable'}, + {'from': 'yes-mapped', 'to': 'yesMapped'}, + {'from': 'no-mapped', 'to': 'no-mapped'}, + ]; + + fixtures.forEach((f) => { + let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null); + let attr = visitor.visitAttr(legacyAttr, null); + expect(attr.name).toEqual(f['to']); + expect(attr.value).toEqual('expression'); + }); + + }); + + it('should not rewrite random attributes', () => { + let fixtures = [ + {'from': 'custom-attr', 'to': 'custom-attr'}, + {'from': 'ng-if', 'to': 'ng-if'}, + ]; + + fixtures.forEach((f) => { + let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null); + let attr = visitor.visitAttr(legacyAttr, null); + expect(attr.name).toEqual(f['to']); + expect(attr.value).toEqual('expression'); + }); + }); + + it('should rewrite interpolation', () => { + let fixtures = [ + {'from': 'dash-case', 'to': 'dashCase'}, + {'from': 'lcase', 'to': 'lcase'}, + {'from': 'camelCase', 'to': 'camelCase'}, + {'from': 'attr.dash-case', 'to': 'attr.dash-case'}, + {'from': 'class.dash-case', 'to': 'class.dash-case'}, + {'from': 'style.dash-case', 'to': 'style.dash-case'}, + ]; + + fixtures.forEach((f) => { + let legacyAttr = new HtmlAttrAst(f['from'], '{{ exp }}', null); + let attr = visitor.visitAttr(legacyAttr, null); + expect(attr.name).toEqual(f['to']); + expect(attr.value).toEqual('{{ exp }}'); + }); + }); + + }); + }); + + describe('template elements', () => { + let visitor; + + beforeEach(() => { + visitor = new LegacyHtmlAstTransformer(); + visitor.visitingTemplateEl = true; + }); + + it('should rewrite angular constructs', () => { + let fixtures = [ + {'from': 'on-dash-case', 'to': 'on-dashCase'}, + {'from': 'ON-dash-case', 'to': 'on-dashCase'}, + {'from': 'bindon-dash-case', 'to': 'bindon-dashCase'}, + {'from': '(dash-case)', 'to': '(dashCase)'}, + {'from': '[(dash-case)]', 'to': '[(dashCase)]'}, + {'from': 'on-camelCase', 'to': 'on-camelCase'}, + {'from': 'bindon-camelCase', 'to': 'bindon-camelCase'}, + {'from': '(camelCase)', 'to': '(camelCase)'}, + {'from': '[(camelCase)]', 'to': '[(camelCase)]'}, + ]; + + fixtures.forEach((f) => { + let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null); + let attr = visitor.visitAttr(legacyAttr, null); + expect(attr.name).toEqual(f['to']); + expect(attr.value).toEqual('expression'); + }); + }); + + it('should rewrite all attributes', () => { + let fixtures = [ + {'from': 'custom-attr', 'to': 'customAttr'}, + {'from': 'ng-if', 'to': 'ngIf'}, + ]; + + fixtures.forEach((f) => { + let legacyAttr = new HtmlAttrAst(f['from'], 'expression', null); + let attr = visitor.visitAttr(legacyAttr, null); + expect(attr.name).toEqual(f['to']); + expect(attr.value).toEqual('expression'); + }); + }); + }); + }); +}