Skip to content
This repository was archived by the owner on Oct 12, 2021. It is now read-only.

Commit fb9673a

Browse files
authored
feat(parser): perform case sensitive parsing (#68)
1 parent 5832c35 commit fb9673a

File tree

10 files changed

+178
-17
lines changed

10 files changed

+178
-17
lines changed

app-shell/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
"format": "clang-format -i -style=file --glob=src/**/*.ts",
1111
"pree2e": "webdriver-manager update",
1212
"e2e": "protractor",
13-
"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"
13+
"clean": "rm -rf dist",
14+
"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",
15+
"build_publish": "npm run clean && npm run build"
1416
},
1517
"private": true,
1618
"dependencies": {
@@ -21,14 +23,14 @@
2123
"@angular/platform-browser-dynamic": "2.0.0-rc.0",
2224
"@angular/router": "2.0.0-rc.0",
2325
"es6-shim": "^0.35.0",
24-
"parse5": "^2.1.5",
2526
"reflect-metadata": "0.1.3",
2627
"rxjs": "5.0.0-beta.6",
2728
"systemjs": "0.19.26",
2829
"zone.js": "^0.6.12"
2930
},
3031
"devDependencies": {
3132
"angular-cli": "0.0.*",
33+
"browserify": "^13.0.1",
3234
"clang-format": "^1.0.35",
3335
"codelyzer": "0.0.14",
3436
"ember-cli-inject-live-reload": "^1.4.0",
@@ -37,6 +39,7 @@
3739
"karma": "^0.13.15",
3840
"karma-chrome-launcher": "^0.2.3",
3941
"karma-jasmine": "^0.3.8",
42+
"parse5": "2.1.5",
4043
"protractor": "^3.3.0",
4144
"ts-node": "^0.5.5",
4245
"tslint": "^3.6.0",

app-shell/src/app/shell-parser/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export type RouteDefinition = string;
22

33
const SHELL_PARSER_CACHE_NAME = 'mobile-toolkit:app-shell';
44
const APP_SHELL_URL = './app_shell.html';
5-
const NO_RENDER_CSS_SELECTOR = '.shell-no-render';
5+
const NO_RENDER_CSS_SELECTOR = '[shellNoRender]';
66
const ROUTE_DEFINITIONS: RouteDefinition[] = [];
77

88
// TODO(mgechev): use if we decide to include @angular/core

app-shell/src/app/shell-parser/shell-parser.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ const prerenderedTemplate = `
2727
<header>
2828
<h1>Hey I'm appshell!</h1>
2929
</header>
30-
<content class="shell-no-render">
30+
<content shellNoRender>
3131
<p>Hello world</p>
3232
</content>
33-
<div class="shell-no-render bar baz">
33+
<div shellNoRender class="bar baz">
3434
</div>
3535
</body>
3636
</html>
@@ -58,7 +58,7 @@ const strippedContent = `
5858
<header>
5959
<h1>Hey I'm appshell!</h1>
6060
</header>
61-
<div class="shell-no-render bar baz">
61+
<div shellNoRender="" class="bar baz">
6262
</div>
6363
</body>
6464
</html>
@@ -73,7 +73,7 @@ const strippedWithComposedSelector = `
7373
<header>
7474
<h1>Hey I'm appshell!</h1>
7575
</header>
76-
<content class="shell-no-render">
76+
<content shellNoRender="">
7777
<p>Hello world</p>
7878
</content>
7979
</body>
@@ -144,7 +144,7 @@ describe('ShellParserImpl', () => {
144144
it('should strip with nested selector', (done: any) => {
145145
const mockScope = new MockWorkerScope();
146146
const parser = createMockedWorker(mockScope, {
147-
NO_RENDER_CSS_SELECTOR: 'content.shell-no-render'
147+
NO_RENDER_CSS_SELECTOR: 'content[shellNoRender]'
148148
});
149149
const response = new MockResponse(prerenderedTemplate);
150150
parser.parseDoc(response)
@@ -158,7 +158,7 @@ describe('ShellParserImpl', () => {
158158
it('should strip with nested selector', (done: any) => {
159159
const mockScope = new MockWorkerScope();
160160
const parser = createMockedWorker(mockScope, {
161-
NO_RENDER_CSS_SELECTOR: '.shell-no-render.bar'
161+
NO_RENDER_CSS_SELECTOR: '[shellNoRender].bar'
162162
});
163163
const response = new MockResponse(prerenderedTemplate);
164164
parser.parseDoc(response)
@@ -172,7 +172,7 @@ describe('ShellParserImpl', () => {
172172
it('should return content type "text/html" with status 200', (done: any) => {
173173
const mockScope = new MockWorkerScope();
174174
const parser = createMockedWorker(mockScope, {
175-
NO_RENDER_CSS_SELECTOR: '.shell-no-render.bar'
175+
NO_RENDER_CSS_SELECTOR: '[shellNoRender].bar'
176176
});
177177
const response = new MockResponse(prerenderedTemplate);
178178
parser.parseDoc(response)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export * from './template-parser';
2-
export * from './parse5-template-parser';
2+
export * from './parse5/parse5-template-parser';
33

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
beforeEach,
3+
it,
4+
describe,
5+
expect,
6+
inject
7+
} from '@angular/core/testing';
8+
import { Parse5TemplateParser } from './parse5-template-parser';
9+
10+
const caseSensitiveTemplate =
11+
`
12+
<!DOCTYPE html>
13+
<html>
14+
<head></head>
15+
<body>
16+
<Div>
17+
<Section Title="Bar"></Section>
18+
</Div>
19+
<sEctiON aTTrIbUtE="">
20+
Content
21+
</sEctiON>
22+
</body>
23+
</html>
24+
`;
25+
26+
const normalize = (template: string) =>
27+
template
28+
.replace(/^\s+/gm, '')
29+
.replace(/\s+$/gm, '')
30+
.replace(/\n/gm, '');
31+
32+
describe('Parse5TemplateParser', () => {
33+
34+
let parser = new Parse5TemplateParser();
35+
36+
describe('parse', () => {
37+
38+
it('should handle capital letters', () => {
39+
const ast = parser.parse(caseSensitiveTemplate);
40+
const div = ast.childNodes[1].childNodes[2].childNodes[1];
41+
expect(div.nodeName).toBe('Div');
42+
expect(div.childNodes[1].attrs[0].name).toBe('Title');
43+
});
44+
45+
it('should perform case sensitive parsing', () => {
46+
const ast = parser.parse(caseSensitiveTemplate);
47+
const section = ast.childNodes[1].childNodes[2].childNodes[3];
48+
expect(section.nodeName).toBe('sEctiON');
49+
expect(section.attrs[0].name).toBe('aTTrIbUtE');
50+
});
51+
52+
});
53+
54+
describe('serialize', () => {
55+
56+
it('should serialize the template keeping case sensitivity', () => {
57+
const ast = parser.parse(caseSensitiveTemplate);
58+
expect(normalize(parser.serialize(ast)))
59+
.toBe(normalize(caseSensitiveTemplate));
60+
});
61+
62+
});
63+
64+
});
65+

app-shell/src/app/shell-parser/template-parser/parse5-template-parser.ts renamed to app-shell/src/app/shell-parser/template-parser/parse5/parse5-template-parser.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import {ASTNode} from '../ast';
2-
import {TemplateParser} from './template-parser';
1+
import {ASTNode} from '../../ast';
2+
import {TemplateParser} from '../template-parser';
33

4-
var Parser = require('../../../vendor/parse5/lib/parser');
5-
var Serializer = require('../../../vendor/parse5/lib/serializer');
4+
import './tokenizer-patch';
5+
6+
var Parser = require('../../../../vendor/parse5/lib/parser');
7+
var Serializer = require('../../../../vendor/parse5/lib/serializer');
68

79
export class Parse5TemplateParser extends TemplateParser {
810
parse(template: string): ASTNode {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import './tokenizer-patch';
2+
3+
import {
4+
beforeEach,
5+
it,
6+
describe,
7+
expect,
8+
inject
9+
} from '@angular/core/testing';
10+
11+
var Tokenizer = require('../../../../vendor/parse5/lib/tokenizer');
12+
13+
describe('tokenizer\'s patch', () => {
14+
15+
let lexer: any;
16+
beforeEach(() => {
17+
lexer = new Tokenizer();
18+
});
19+
20+
it('should keep case sensntivity of elements', () => {
21+
lexer.write('<DiV></DiV>');
22+
const openDiv = lexer.getNextToken();
23+
expect(openDiv.type).toBe('START_TAG_TOKEN');
24+
expect(openDiv.tagName).toBe('DiV');
25+
expect(openDiv.selfClosing).toBe(false);
26+
27+
const closeDiv = lexer.getNextToken();
28+
expect(closeDiv.type).toBe('END_TAG_TOKEN');
29+
expect(closeDiv.tagName).toBe('DiV');
30+
});
31+
32+
it('should preserve case sensitivity of complex elements', () => {
33+
lexer.write('<mY-ApP></mY-ApP>');
34+
const open = lexer.getNextToken();
35+
expect(open.tagName).toBe('mY-ApP');
36+
const close = lexer.getNextToken();
37+
expect(close.tagName).toBe('mY-ApP');
38+
});
39+
40+
it('should keep case sensitivity of attrs', () => {
41+
lexer.write('<dIV StYlE="color: red;"></dIV>');
42+
const div = lexer.getNextToken();
43+
expect(div.tagName).toBe('dIV');
44+
expect(div.attrs[0].name).toBe('StYlE');
45+
expect(div.attrs[0].value).toBe('color: red;');
46+
});
47+
48+
});
49+
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
var Tokenizer = require('../../../../vendor/parse5/lib/tokenizer');
2+
3+
// Monkey patching the lexer in order to establish
4+
// case sensitive parsing of the input templates.
5+
// This way we'll be able to use case sensitive attribute
6+
// and element selectors for stripping the content that
7+
// is not required for the App Shell.
8+
//
9+
// Since we're patching module's internals we cannot
10+
// use parse5 as dependency of the App Shell since we
11+
// won't have access to the tokenizer in order to patch
12+
// it runtime. Because of that we distribute the entire
13+
// Runtime Parser as a single bundle which includes parse5.
14+
Tokenizer.prototype.getNextToken = function () {
15+
function replaceLastWithUppercase(token: any, prop: string, cp: number) {
16+
if (token) {
17+
let char = String.fromCharCode(cp);
18+
let val = token[prop];
19+
let last = val[val.length - 1];
20+
if (last && last !== char) {
21+
token[prop] = val.substring(0, val.length - 1) + last.toUpperCase();
22+
}
23+
}
24+
}
25+
while (!this.tokenQueue.length && this.active) {
26+
this._hibernationSnapshot();
27+
const cp = this._consume();
28+
if (!this._ensureHibernation()) {
29+
this[this.state](cp);
30+
}
31+
switch (this.state) {
32+
case 'TAG_NAME_STATE':
33+
replaceLastWithUppercase(this.currentToken, 'tagName', cp);
34+
break;
35+
case 'ATTRIBUTE_NAME_STATE':
36+
replaceLastWithUppercase(this.currentAttr, 'name', cp);
37+
break;
38+
}
39+
}
40+
return this.tokenQueue.shift();
41+
};
42+

app-shell/src/tsconfig.publish.es5.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"moduleResolution": "node",
99
"noEmitOnError": true,
1010
"noImplicitAny": true,
11-
"outDir": "../dist/",
11+
"outDir": "../dist/app",
1212
"rootDir": "./app",
1313
"sourceMap": true,
1414
"target": "es5",

app-shell/src/tsconfig.publish.es6.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"moduleResolution": "node",
99
"noEmitOnError": true,
1010
"noImplicitAny": true,
11-
"outDir": "../dist/esm",
11+
"outDir": "../dist/app/esm",
1212
"rootDir": "./app",
1313
"sourceMap": true,
1414
"target": "es6",

0 commit comments

Comments
 (0)