diff --git a/app-shell/package.json b/app-shell/package.json index 73d40bd..0215160 100644 --- a/app-shell/package.json +++ b/app-shell/package.json @@ -10,7 +10,9 @@ "format": "clang-format -i -style=file --glob=src/**/*.ts", "pree2e": "webdriver-manager update", "e2e": "protractor", - "build_publish": "rm -rf dist && tsc -p src/tsconfig.publish.es5.json && tsc -p src/tsconfig.publish.es6.json && cp src/package.json dist/package.json" + "clean": "rm -rf dist", + "build": "ng build && tsc -p src/tsconfig.publish.es5.json && tsc -p src/tsconfig.publish.es6.json && cp src/package.json dist/app/package.json && browserify dist/app/shell-parser/index.js -s shellParserFactory > dist/app/shell-parser.js && rm -rf dist/app/shell-parser && rm -rf dist/app/vendor", + "build_publish": "npm run clean && npm run build" }, "private": true, "dependencies": { @@ -21,7 +23,6 @@ "@angular/platform-browser-dynamic": "2.0.0-rc.0", "@angular/router": "2.0.0-rc.0", "es6-shim": "^0.35.0", - "parse5": "^2.1.5", "reflect-metadata": "0.1.3", "rxjs": "5.0.0-beta.6", "systemjs": "0.19.26", @@ -29,6 +30,7 @@ }, "devDependencies": { "angular-cli": "0.0.*", + "browserify": "^13.0.1", "clang-format": "^1.0.35", "codelyzer": "0.0.14", "ember-cli-inject-live-reload": "^1.4.0", @@ -37,6 +39,7 @@ "karma": "^0.13.15", "karma-chrome-launcher": "^0.2.3", "karma-jasmine": "^0.3.8", + "parse5": "2.1.5", "protractor": "^3.3.0", "ts-node": "^0.5.5", "tslint": "^3.6.0", diff --git a/app-shell/src/app/shell-parser/config.ts b/app-shell/src/app/shell-parser/config.ts index 3bd7635..c924774 100644 --- a/app-shell/src/app/shell-parser/config.ts +++ b/app-shell/src/app/shell-parser/config.ts @@ -2,7 +2,7 @@ export type RouteDefinition = string; const SHELL_PARSER_CACHE_NAME = 'mobile-toolkit:app-shell'; const APP_SHELL_URL = './app_shell.html'; -const NO_RENDER_CSS_SELECTOR = '.shell-no-render'; +const NO_RENDER_CSS_SELECTOR = '[shellNoRender]'; const ROUTE_DEFINITIONS: RouteDefinition[] = []; // TODO(mgechev): use if we decide to include @angular/core diff --git a/app-shell/src/app/shell-parser/shell-parser.spec.ts b/app-shell/src/app/shell-parser/shell-parser.spec.ts index 5c46e68..1e3e9f6 100644 --- a/app-shell/src/app/shell-parser/shell-parser.spec.ts +++ b/app-shell/src/app/shell-parser/shell-parser.spec.ts @@ -27,10 +27,10 @@ const prerenderedTemplate = `

Hey I'm appshell!

- +

Hello world

-
+
@@ -58,7 +58,7 @@ const strippedContent = `

Hey I'm appshell!

-
+
@@ -73,7 +73,7 @@ const strippedWithComposedSelector = `

Hey I'm appshell!

- +

Hello world

@@ -144,7 +144,7 @@ describe('ShellParserImpl', () => { it('should strip with nested selector', (done: any) => { const mockScope = new MockWorkerScope(); const parser = createMockedWorker(mockScope, { - NO_RENDER_CSS_SELECTOR: 'content.shell-no-render' + NO_RENDER_CSS_SELECTOR: 'content[shellNoRender]' }); const response = new MockResponse(prerenderedTemplate); parser.parseDoc(response) @@ -158,7 +158,7 @@ describe('ShellParserImpl', () => { it('should strip with nested selector', (done: any) => { const mockScope = new MockWorkerScope(); const parser = createMockedWorker(mockScope, { - NO_RENDER_CSS_SELECTOR: '.shell-no-render.bar' + NO_RENDER_CSS_SELECTOR: '[shellNoRender].bar' }); const response = new MockResponse(prerenderedTemplate); parser.parseDoc(response) @@ -172,7 +172,7 @@ describe('ShellParserImpl', () => { it('should return content type "text/html" with status 200', (done: any) => { const mockScope = new MockWorkerScope(); const parser = createMockedWorker(mockScope, { - NO_RENDER_CSS_SELECTOR: '.shell-no-render.bar' + NO_RENDER_CSS_SELECTOR: '[shellNoRender].bar' }); const response = new MockResponse(prerenderedTemplate); parser.parseDoc(response) diff --git a/app-shell/src/app/shell-parser/template-parser/index.ts b/app-shell/src/app/shell-parser/template-parser/index.ts index 5cd6ced..087fc85 100644 --- a/app-shell/src/app/shell-parser/template-parser/index.ts +++ b/app-shell/src/app/shell-parser/template-parser/index.ts @@ -1,3 +1,3 @@ export * from './template-parser'; -export * from './parse5-template-parser'; +export * from './parse5/parse5-template-parser'; diff --git a/app-shell/src/app/shell-parser/template-parser/parse5/parse5-template-parser.spec.ts b/app-shell/src/app/shell-parser/template-parser/parse5/parse5-template-parser.spec.ts new file mode 100644 index 0000000..18bb95e --- /dev/null +++ b/app-shell/src/app/shell-parser/template-parser/parse5/parse5-template-parser.spec.ts @@ -0,0 +1,65 @@ +import { + beforeEach, + it, + describe, + expect, + inject +} from '@angular/core/testing'; +import { Parse5TemplateParser } from './parse5-template-parser'; + +const caseSensitiveTemplate = +` + + + + +
+
+
+
+ Content +
+ + +`; + +const normalize = (template: string) => + template + .replace(/^\s+/gm, '') + .replace(/\s+$/gm, '') + .replace(/\n/gm, ''); + +describe('Parse5TemplateParser', () => { + + let parser = new Parse5TemplateParser(); + + describe('parse', () => { + + it('should handle capital letters', () => { + const ast = parser.parse(caseSensitiveTemplate); + const div = ast.childNodes[1].childNodes[2].childNodes[1]; + expect(div.nodeName).toBe('Div'); + expect(div.childNodes[1].attrs[0].name).toBe('Title'); + }); + + it('should perform case sensitive parsing', () => { + const ast = parser.parse(caseSensitiveTemplate); + const section = ast.childNodes[1].childNodes[2].childNodes[3]; + expect(section.nodeName).toBe('sEctiON'); + expect(section.attrs[0].name).toBe('aTTrIbUtE'); + }); + + }); + + describe('serialize', () => { + + it('should serialize the template keeping case sensitivity', () => { + const ast = parser.parse(caseSensitiveTemplate); + expect(normalize(parser.serialize(ast))) + .toBe(normalize(caseSensitiveTemplate)); + }); + + }); + +}); + diff --git a/app-shell/src/app/shell-parser/template-parser/parse5-template-parser.ts b/app-shell/src/app/shell-parser/template-parser/parse5/parse5-template-parser.ts similarity index 54% rename from app-shell/src/app/shell-parser/template-parser/parse5-template-parser.ts rename to app-shell/src/app/shell-parser/template-parser/parse5/parse5-template-parser.ts index b767735..da96d0b 100644 --- a/app-shell/src/app/shell-parser/template-parser/parse5-template-parser.ts +++ b/app-shell/src/app/shell-parser/template-parser/parse5/parse5-template-parser.ts @@ -1,8 +1,10 @@ -import {ASTNode} from '../ast'; -import {TemplateParser} from './template-parser'; +import {ASTNode} from '../../ast'; +import {TemplateParser} from '../template-parser'; -var Parser = require('../../../vendor/parse5/lib/parser'); -var Serializer = require('../../../vendor/parse5/lib/serializer'); +import './tokenizer-patch'; + +var Parser = require('../../../../vendor/parse5/lib/parser'); +var Serializer = require('../../../../vendor/parse5/lib/serializer'); export class Parse5TemplateParser extends TemplateParser { parse(template: string): ASTNode { diff --git a/app-shell/src/app/shell-parser/template-parser/parse5/tokenizer-patch.spec.ts b/app-shell/src/app/shell-parser/template-parser/parse5/tokenizer-patch.spec.ts new file mode 100644 index 0000000..e4eb32c --- /dev/null +++ b/app-shell/src/app/shell-parser/template-parser/parse5/tokenizer-patch.spec.ts @@ -0,0 +1,49 @@ +import './tokenizer-patch'; + +import { + beforeEach, + it, + describe, + expect, + inject +} from '@angular/core/testing'; + +var Tokenizer = require('../../../../vendor/parse5/lib/tokenizer'); + +describe('tokenizer\'s patch', () => { + + let lexer: any; + beforeEach(() => { + lexer = new Tokenizer(); + }); + + it('should keep case sensntivity of elements', () => { + lexer.write('
'); + const openDiv = lexer.getNextToken(); + expect(openDiv.type).toBe('START_TAG_TOKEN'); + expect(openDiv.tagName).toBe('DiV'); + expect(openDiv.selfClosing).toBe(false); + + const closeDiv = lexer.getNextToken(); + expect(closeDiv.type).toBe('END_TAG_TOKEN'); + expect(closeDiv.tagName).toBe('DiV'); + }); + + it('should preserve case sensitivity of complex elements', () => { + lexer.write(''); + const open = lexer.getNextToken(); + expect(open.tagName).toBe('mY-ApP'); + const close = lexer.getNextToken(); + expect(close.tagName).toBe('mY-ApP'); + }); + + it('should keep case sensitivity of attrs', () => { + lexer.write('
'); + const div = lexer.getNextToken(); + expect(div.tagName).toBe('dIV'); + expect(div.attrs[0].name).toBe('StYlE'); + expect(div.attrs[0].value).toBe('color: red;'); + }); + +}); + diff --git a/app-shell/src/app/shell-parser/template-parser/parse5/tokenizer-patch.ts b/app-shell/src/app/shell-parser/template-parser/parse5/tokenizer-patch.ts new file mode 100644 index 0000000..73c9135 --- /dev/null +++ b/app-shell/src/app/shell-parser/template-parser/parse5/tokenizer-patch.ts @@ -0,0 +1,42 @@ +var Tokenizer = require('../../../../vendor/parse5/lib/tokenizer'); + +// Monkey patching the lexer in order to establish +// case sensitive parsing of the input templates. +// This way we'll be able to use case sensitive attribute +// and element selectors for stripping the content that +// is not required for the App Shell. +// +// Since we're patching module's internals we cannot +// use parse5 as dependency of the App Shell since we +// won't have access to the tokenizer in order to patch +// it runtime. Because of that we distribute the entire +// Runtime Parser as a single bundle which includes parse5. +Tokenizer.prototype.getNextToken = function () { + function replaceLastWithUppercase(token: any, prop: string, cp: number) { + if (token) { + let char = String.fromCharCode(cp); + let val = token[prop]; + let last = val[val.length - 1]; + if (last && last !== char) { + token[prop] = val.substring(0, val.length - 1) + last.toUpperCase(); + } + } + } + while (!this.tokenQueue.length && this.active) { + this._hibernationSnapshot(); + const cp = this._consume(); + if (!this._ensureHibernation()) { + this[this.state](cp); + } + switch (this.state) { + case 'TAG_NAME_STATE': + replaceLastWithUppercase(this.currentToken, 'tagName', cp); + break; + case 'ATTRIBUTE_NAME_STATE': + replaceLastWithUppercase(this.currentAttr, 'name', cp); + break; + } + } + return this.tokenQueue.shift(); +}; + diff --git a/app-shell/src/tsconfig.publish.es5.json b/app-shell/src/tsconfig.publish.es5.json index c64ba3c..da8eee2 100644 --- a/app-shell/src/tsconfig.publish.es5.json +++ b/app-shell/src/tsconfig.publish.es5.json @@ -8,7 +8,7 @@ "moduleResolution": "node", "noEmitOnError": true, "noImplicitAny": true, - "outDir": "../dist/", + "outDir": "../dist/app", "rootDir": "./app", "sourceMap": true, "target": "es5", diff --git a/app-shell/src/tsconfig.publish.es6.json b/app-shell/src/tsconfig.publish.es6.json index 7b7b13b..b2980b5 100644 --- a/app-shell/src/tsconfig.publish.es6.json +++ b/app-shell/src/tsconfig.publish.es6.json @@ -8,7 +8,7 @@ "moduleResolution": "node", "noEmitOnError": true, "noImplicitAny": true, - "outDir": "../dist/esm", + "outDir": "../dist/app/esm", "rootDir": "./app", "sourceMap": true, "target": "es6",