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

Commit 9cc156d

Browse files
authored
feat(AppShell): add runtime parser (#54)
* chore(app-shell): update to typings 1.0.4 * feat(AppShell): add runtime app shell parser
1 parent f8e0220 commit 9cc156d

34 files changed

+1166
-11
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ before_install:
88

99
install: ./install.sh
1010
script: ./test.sh
11+

app-shell/angular-cli-build.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ module.exports = function(defaults) {
1111
'es6-shim/es6-shim.js',
1212
'reflect-metadata/*.js',
1313
'rxjs/**/*.js',
14-
'@angular/**/*.js'
14+
'@angular/**/*.js',
15+
'parse5/**/*.js'
1516
]
1617
});
1718
};

app-shell/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@angular/platform-browser-dynamic": "2.0.0-rc.0",
2222
"@angular/router": "2.0.0-rc.0",
2323
"es6-shim": "^0.35.0",
24+
"parse5": "https://github.com/mgechev/parse5",
2425
"reflect-metadata": "0.1.3",
2526
"rxjs": "5.0.0-beta.6",
2627
"systemjs": "0.19.26",
@@ -40,6 +41,6 @@
4041
"ts-node": "^0.5.5",
4142
"tslint": "^3.6.0",
4243
"typescript": "^1.8.10",
43-
"typings": "^0.8.1"
44+
"typings": "^1.0.4"
4445
}
4546
}

app-shell/src/app/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ShellNoRender } from './shell-no-render.directive';
55
export * from './is-prerender.service';
66
export * from './shell-no-render.directive';
77
export * from './shell-render.directive';
8+
export * from './shell-parser';
89

910
export const APP_SHELL_DIRECTIVES: Type[] = [
1011
ShellRender,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export interface ASTAttribute {
2+
name: string;
3+
value: string;
4+
}
5+
6+
export interface ASTNode {
7+
attrs: ASTAttribute[];
8+
childNodes?: ASTNode[];
9+
parentNode?: ASTNode;
10+
nodeName: string;
11+
}
12+
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './ast-node';
2+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export type RouteDefinition = string;
2+
3+
const SHELL_PARSER_CACHE_NAME = 'mobile-toolkit:app-shell';
4+
const APP_SHELL_URL = './app_shell.html';
5+
const NO_RENDER_CSS_SELECTOR = '.shell-no-render';
6+
const ROUTE_DEFINITIONS: RouteDefinition[] = [];
7+
8+
// TODO(mgechev): use if we decide to include @angular/core
9+
// export const SHELL_PARSER_CONFIG = new OpaqueToken('ShellRuntimeParserConfig');
10+
11+
export interface ShellParserConfig {
12+
APP_SHELL_URL?: string;
13+
SHELL_PARSER_CACHE_NAME?: string;
14+
NO_RENDER_CSS_SELECTOR?: string;
15+
ROUTE_DEFINITIONS?: RouteDefinition[];
16+
}
17+
18+
export const SHELL_PARSER_DEFAULT_CONFIG: ShellParserConfig = {
19+
SHELL_PARSER_CACHE_NAME,
20+
APP_SHELL_URL,
21+
NO_RENDER_CSS_SELECTOR,
22+
ROUTE_DEFINITIONS
23+
};
24+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export abstract class WorkerScope {
2+
abstract fetch(url: string | Request): Promise<Response>;
3+
abstract newRequest(input: string | Request, init?: RequestInit): Request;
4+
abstract newResponse(body?: BodyInit, init?: ResponseInit): Response;
5+
caches: CacheStorage;
6+
}
7+
8+
export class BrowserWorkerScope {
9+
fetch(url: string | Request): Promise<Response> {
10+
return fetch(url);
11+
}
12+
13+
get caches() {
14+
return caches;
15+
}
16+
17+
newRequest(input: string | Request, init?: RequestInit): Request {
18+
return new Request(input, init);
19+
}
20+
21+
newResponse(body?: BodyInit, init?: ResponseInit) {
22+
return new Response(body, init);
23+
}
24+
}
25+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {ShellParser} from './shell-parser';
2+
import {shellParserFactory} from './shell-parser-factory';
3+
import {RouteDefinition, ShellParserConfig} from './config';
4+
5+
export {
6+
shellParserFactory,
7+
ShellParser,
8+
RouteDefinition,
9+
ShellParserConfig
10+
};
11+
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import {
2+
beforeEach,
3+
it,
4+
describe,
5+
expect,
6+
inject
7+
} from '@angular/core/testing';
8+
import { ASTNode } from '../ast';
9+
import { CssSelector } from './css-selector';
10+
import { CssNodeMatcher } from './css-node-matcher';
11+
12+
describe('CssNodeMatcher', () => {
13+
14+
var node: ASTNode;
15+
beforeEach(() => {
16+
node = {
17+
attrs: [
18+
{ name: 'foo', value: 'bar' },
19+
{ name: 'class', value: 'dialog modal--drop' },
20+
{ name: 'id', value: 'dialog-id' }
21+
],
22+
nodeName: 'div',
23+
parentNode: null
24+
};
25+
});
26+
27+
describe('successful match', () => {
28+
29+
it('should match any node with empty selector', () => {
30+
const emptySelector = CssSelector.parse('');
31+
const selector = new CssNodeMatcher(emptySelector);
32+
expect(selector.match(node)).toBe(true);
33+
});
34+
35+
it('should match basic element selector', () => {
36+
const elementSelector = CssSelector.parse('div');
37+
const selector = new CssNodeMatcher(elementSelector);
38+
expect(selector.match(node)).toBe(true);
39+
});
40+
41+
it('should match attribute selector', () => {
42+
const attrSelector = CssSelector.parse('[foo=bar]');
43+
const selector = new CssNodeMatcher(attrSelector);
44+
expect(selector.match(node)).toBe(true);
45+
});
46+
47+
it('should match attribute selector when no value is provided', () => {
48+
const attrSelector = CssSelector.parse('[foo]');
49+
const selector = new CssNodeMatcher(attrSelector);
50+
expect(selector.match(node)).toBe(true);
51+
});
52+
53+
it('should match class selector', () => {
54+
const classSelector = CssSelector.parse('.dialog');
55+
const selector = new CssNodeMatcher(classSelector);
56+
expect(selector.match(node)).toBe(true);
57+
const complexClassSelector = CssSelector.parse('.dialog.modal--drop');
58+
const complexSelector = new CssNodeMatcher(complexClassSelector);
59+
expect(complexSelector.match(node)).toBe(true);
60+
});
61+
62+
it('should match element by id', () => {
63+
const idSelector = CssSelector.parse('#dialog-id');
64+
const selector = new CssNodeMatcher(idSelector);
65+
expect(selector.match(node)).toBe(true);
66+
});
67+
68+
});
69+
70+
describe('unsuccessful match', () => {
71+
72+
it('should fail when different element is used', () => {
73+
const elementSelector = CssSelector.parse('span');
74+
const selector = new CssNodeMatcher(elementSelector);
75+
expect(selector.match(node)).toBe(false);
76+
});
77+
78+
it('should fail when different attribute selector is provided', () => {
79+
const attrSelector = CssSelector.parse('[foo=qux]');
80+
const selector = new CssNodeMatcher(attrSelector);
81+
expect(selector.match(node)).toBe(false);
82+
});
83+
84+
it('should fail when non-matching class selector is used', () => {
85+
const classSelector = CssSelector.parse('.modal');
86+
const selector = new CssNodeMatcher(classSelector);
87+
expect(selector.match(node)).toBe(false);
88+
const complexClassSelector = CssSelector.parse('.dialog.modal-drop');
89+
const complexSelector = new CssNodeMatcher(complexClassSelector);
90+
expect(complexSelector.match(node)).toBe(false);
91+
});
92+
93+
it('should fail when superset of attributes is used in selector', () => {
94+
const cssSelector = CssSelector.parse('[foo=bar][baz=bar]');
95+
const selector = new CssNodeMatcher(cssSelector);
96+
expect(selector.match(node)).toBe(false);
97+
});
98+
99+
it('should fail when superset of attributes is used in selector', () => {
100+
const cssSelector = CssSelector.parse('[no-render]');
101+
const selector = new CssNodeMatcher(cssSelector);
102+
expect(selector.match(node)).toBe(false);
103+
})
104+
105+
it('should fail match by id when element has no id', () => {
106+
const cssSelector = CssSelector.parse('#foo');
107+
const selector = new CssNodeMatcher(cssSelector);
108+
const node1: ASTNode = {
109+
nodeName: 'div',
110+
parentNode: null,
111+
attrs: []
112+
};
113+
const node2: ASTNode = {
114+
attrs: [
115+
{ name: 'not-id', value: '' }
116+
],
117+
nodeName: 'div',
118+
parentNode: null
119+
};
120+
expect(selector.match(node1)).toBe(false);
121+
expect(selector.match(node2)).toBe(false);
122+
});
123+
124+
});
125+
});
126+

0 commit comments

Comments
 (0)