├── .gitignore
├── .jshintrc
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── index.js
├── package.json
├── src
├── adapters
│ ├── browser-adapter.js
│ └── parser-adapter.js
├── attr.js
├── character-data.js
├── comment.js
├── document-type.js
├── document.js
├── dom-exception.js
├── element.js
├── elements
│ ├── html-anchor-element.js
│ ├── html-button-element.js
│ ├── html-image-element.js
│ ├── html-input-element.js
│ ├── html-link-element.js
│ ├── html-option-element.js
│ ├── html-select-element.js
│ ├── html-text-area-element.js
│ ├── html-unknown-element.js
│ └── index.js
├── html-element.js
├── named-node-map.js
├── node-list.js
├── node.js
├── style-sheet-list.js
├── style
│ ├── cascade.js
│ ├── config
│ │ └── inherit.json
│ ├── each-css-style-rule.js
│ ├── get-computed-style.js
│ ├── index.js
│ └── load-css-files.js
├── text.js
├── utils
│ ├── decode.js
│ └── resource.js
└── window.js
└── test
├── document
├── html
│ ├── blank.html
│ ├── tag-base.html
│ └── test.html
└── index.js
├── index.js
├── selector
├── W3C-Selector-tests
│ ├── W3C-Selector-tests.html
│ └── index.js
├── css3-compat
│ ├── css3-compat.html
│ └── index.js
└── index.js
├── style
├── get-computed-style.js
├── html
│ ├── index.html
│ ├── pseudo1.html
│ ├── pseudo2.html
│ ├── pseudo3.html
│ ├── pseudo4.html
│ ├── pseudo5.html
│ └── pseudo6.html
└── index.js
└── utils
├── decode.js
├── file
├── test.js
└── test.txt
├── index.js
└── resource.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "esnext": false,
3 | "moz": true,
4 | "boss": true,
5 | "node": true,
6 | "validthis": true,
7 | "browser": false,
8 | "globals": {
9 | "describe": true,
10 | "it": true
11 | }
12 | }
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | test
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | before_script:
3 | - npm install -g mocha
4 | node_js:
5 | - "4.0.0"
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 糖饼
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BrowserX
2 |
3 | [![NPM Version][npm-image]][npm-url]
4 | [![NPM Downloads][downloads-image]][downloads-url]
5 | [![Node.js Version][node-version-image]][node-version-url]
6 | [![Build Status][travis-ci-image]][travis-ci-url]
7 |
8 | BrowserX 是一个基于 NodeJS 实现的轻量级“浏览器”,它的目标是高效的实现 DOM 中最核心的特性,以便开发者能够在 NodeJS 中使用 W3C 标准方法来操作文档与样式。
9 |
10 | * 提供 DOM 核心 API
11 | * 完整支持 CSS3 选择器来查询节点
12 | * 支持样式解析,如 `element.style`、`document.styleSheets`、`window.getComputedStyle()` 以及 CSSOM 相关构造器访问
13 |
14 | ## 安装
15 |
16 | ``` shell
17 | npm install browser-x
18 | ```
19 |
20 | ## 接口
21 |
22 | ### browser(options, callback)
23 |
24 | 返回:`Promise`
25 |
26 | ``` javascript
27 | var browser = require('browser-x');
28 |
29 | var url = __dirname + '/debug.html';
30 | browser({
31 | url: url,
32 | loadCssFile: true,
33 | silent: false
34 | }, function (errors, window) {
35 | if (errors) {
36 | throw errors;
37 | }
38 | var document = window.document;
39 | var element = document.querySelector('#banner h2');
40 | var fontFamily = window.getComputedStyle(element, '::after').fontFamily;
41 | console.log(fontFamily);
42 | });
43 | ```
44 |
45 | ## options
46 |
47 | ``` javascript
48 | {
49 | /**
50 | * 文件基础路径 - 支持本地或远程地址
51 | */
52 | url: 'about:blank',
53 |
54 | /*
55 | * HTML 文本内容
56 | */
57 | html: null,
58 |
59 | /**
60 | * 是否支持加载外部 CSS 文件
61 | */
62 | loadCssFile: false,
63 |
64 | /**
65 | * 是否忽略内部解析错误-关闭它有利于开发调试
66 | * @type {Boolean}
67 | */
68 | silent: true,
69 |
70 | /**
71 | * 请求超时限制
72 | * @type {Number} 毫秒
73 | */
74 | resourceTimeout: 8000,
75 |
76 | /**
77 | * 最大的文件加载数量限制
78 | * @type {Number} 数量
79 | */
80 | resourceMaxNumber: 64,
81 |
82 | /**
83 | * 是否缓存请求成功的资源
84 | * @return {Object}
85 | */
86 | resourceCache: true,
87 |
88 | /**
89 | * 映射资源路径
90 | * @param {String} 旧文件地址
91 | * @return {String} 新文件地址
92 | */
93 | resourceMap: function(file) {
94 | return file;
95 | },
96 |
97 | /**
98 | * 忽略资源
99 | * @param {String} 文件地址
100 | * @return {Boolean} 如果返回`true`则忽略当当前文件的加载
101 | */
102 | resourceIgnore: function(file) {
103 | return false;
104 | },
105 |
106 | /**
107 | * 资源加载前的事件
108 | * @param {String} 文件地址
109 | */
110 | resourceBeforeLoad: function(file) {
111 | },
112 |
113 | /**
114 | * 加载远程资源的自定义请求头
115 | * @param {String} 文件地址
116 | * @return {Object}
117 | */
118 | resourceRequestHeaders: function(file) {
119 | return {
120 | 'accept-encoding': 'gzip,deflate'
121 | };
122 | }
123 | }
124 | ```
125 |
126 | ## 支持的 DOM API
127 |
128 | * Window
129 | - [getComputedStyle()](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/getComputedStyle)
130 | - [CSSStyleDeclaration()](https://developer.mozilla.org/zh-CN/docs/Web/API/CSSStyleDeclaration)
131 | - [CSSRule()](https://developer.mozilla.org/zh-CN/docs/Web/API/CSSRule)
132 | - [CSSStyleRule()](https://developer.mozilla.org/zh-CN/docs/Web/API/CSSStyleRule)
133 | - [MediaList()](https://developer.mozilla.org/zh-CN/docs/Web/API/MediaList)
134 | - [CSSMediaRule()](https://developer.mozilla.org/zh-CN/docs/Web/API/CSSMediaRule)
135 | - [CSSImportRule()](https://developer.mozilla.org/zh-CN/docs/Web/API/CSSImportRule)
136 | - [CSSFontFaceRule()](https://developer.mozilla.org/zh-CN/docs/Web/API/CSSFontFaceRule)
137 | - [StyleSheet()](https://developer.mozilla.org/zh-CN/docs/Web/API/StyleSheet)
138 | - [CSSStyleSheet()](https://developer.mozilla.org/zh-CN/docs/Web/API/CSSStyleSheet)
139 | - [CSSKeyframesRule()](https://developer.mozilla.org/zh-CN/docs/Web/API/CSSKeyframesRule)
140 | - [CSSKeyframeRule()](https://developer.mozilla.org/zh-CN/docs/Web/API/CSSKeyframeRule)
141 | * Document
142 | - [URL](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/URL)
143 | - [documentElement](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/documentElement)
144 | - [head](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/head)
145 | - [body](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/body)
146 | - [title](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/title)
147 | - [styleSheets](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/styleSheets)
148 | - [getElementsByTagName()](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/getElementsByTagName)
149 | - [getElementById()](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/getElementById)
150 | - [querySelector()](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/querySelector)
151 | - [querySelectorAll()](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/querySelectorAll)
152 | * Element
153 | - [id](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/id)
154 | - [tagName](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/tagName)
155 | - [className](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/className)
156 | - [innerHTML](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/innerHTML)
157 | - [outerHTML](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/outerHTML)
158 | - [attributes](https://developer.mozilla.org/zh-CN/docs/Web/API/Node.attributes)
159 | - [hasAttribute()](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/hasAttribute)
160 | - [getAttribute()](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getAttribute)
161 | - [querySelector()](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/querySelector)
162 | - [querySelectorAll()](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/querySelectorAll)
163 | - [getElementsByTagName()](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getElementsByTagName)
164 | - [matches()](https://developer.mozilla.org/zh-CN/docs/Web/API/Element/matches)
165 | * HTMLElement
166 | - [style](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/style)
167 | * Node
168 | - [nodeName](https://developer.mozilla.org/zh-CN/docs/Web/API/Node.nodeName)
169 | - [nodeType](https://developer.mozilla.org/zh-CN/docs/Web/API/Node.nodeType)
170 | - [childNodes](https://developer.mozilla.org/zh-CN/docs/Web/API/Node.childNodes)
171 | - [parentNode](https://developer.mozilla.org/zh-CN/docs/Web/API/Node.parentNode)
172 | - [firstChild](https://developer.mozilla.org/zh-CN/docs/Web/API/Node.firstChild)
173 | - [lastChild](https://developer.mozilla.org/zh-CN/docs/Web/API/Node.lastChild)
174 | - [nextSibling](https://developer.mozilla.org/zh-CN/docs/Web/API/Node.nextSibling)
175 | - [previousSibling](https://developer.mozilla.org/zh-CN/docs/Web/API/Node.previousSibling)
176 | - [textContent](https://developer.mozilla.org/zh-CN/docs/Web/API/Node.textContent)
177 | - [baseURI](https://developer.mozilla.org/zh-CN/docs/Web/API/Node.baseURI)
178 |
179 | ## 注意事项
180 |
181 | 1. 不支持 XML 文档解析
182 | 2. 所有的 DOM 属性均为只读(*计划在未来版本支持写入*)
183 | 3. window.getComputedStyle() 仅能获取元素或伪元素在 CSS 中定义的原始值或继承属性,但没有进行计算输出(例如 em \> px)
184 | 4. document.styleSheets 在浏览器中无法跨域访问 CSSOM,BrowserX 没有做此限制(外部样式需要打开 `loadCssFile` 选项)
185 | 5. 不支持浏览器怪异模式
186 |
187 | ## 为什么使用 BrowserX
188 |
189 | BrowserX 适合做这些事情:
190 |
191 | 1. 高效的爬虫程序,使用 CSS 选择器来收集内容
192 | 2. 分析元素的样式使用情况,例如和 CSS 相关的开发工具
193 |
194 | 例如:WebFont 压缩工具——[font-spider](https://github.com/aui/font-spider)
195 |
196 | 如果需要更多的 DOM 特性,例如跑基于 DOM 的测试脚本、甚至载入 jQuery 等,那么 [jsdom](https://github.com/tmpvar/jsdom) 这个项目可能会更适合你(注意:它没有完整解析样式表)。
197 |
198 |
199 | [npm-image]: https://img.shields.io/npm/v/browser-x.svg
200 | [npm-url]: https://npmjs.org/package/browser-x
201 | [node-version-image]: https://img.shields.io/node/v/browser-x.svg
202 | [node-version-url]: http://nodejs.org/download/
203 | [downloads-image]: https://img.shields.io/npm/dm/browser-x.svg
204 | [downloads-url]: https://npmjs.org/package/browser-x
205 | [travis-ci-image]: https://travis-ci.org/aui/browser-x.svg?branch=master
206 | [travis-ci-url]: https://travis-ci.org/aui/browser-x
207 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var parse5 = require('parse5');
4 |
5 | var Resource = require('./src/utils/resource');
6 | var ParserAdapter = require('./src/adapters/parser-adapter');
7 | var BrowserAdapter = require('./src/adapters/browser-adapter');
8 | var loadCssFiles = require('./src/style/load-css-files');
9 |
10 |
11 |
12 | /**
13 | * @param {Object} 选项 @see ./src/adapters/browser-adapter
14 | * @param {Function} 回调函数(可选)
15 | * @param {Promise}
16 | */
17 | function browser(options, callback) {
18 | options = new BrowserAdapter(options);
19 | callback = callback || function() {};
20 |
21 |
22 | return new Promise(function(resolve, reject) {
23 |
24 | if (options.html) {
25 | var window = browser.sync(options.html, options);
26 | start(window);
27 | } else if (options.url) {
28 | new Resource(options).get(options.url).then(function(html) {
29 | options.html = html;
30 | var window = browser.sync(options.html, options);
31 | start(window);
32 | });
33 | }
34 |
35 | function start(window) {
36 | window.onload = function() {
37 | resolve(window);
38 | callback(null, window);
39 | };
40 | window.onerror = function(errors) {
41 | reject(errors);
42 | callback(errors);
43 | };
44 | }
45 | });
46 | }
47 |
48 |
49 |
50 | /**
51 | * @param {String} HTML
52 | * @param {Object} 选项(可选)@see ./src/adapters/browser-adapter
53 | * @param {Window}
54 | */
55 | browser.sync = function(html, options) {
56 | options = new BrowserAdapter(options);
57 | options.parserAdapter = {
58 | treeAdapter: new ParserAdapter(options)
59 | };
60 |
61 | var document = parse5.parse(html, options.parserAdapter);
62 | var window = document.defaultView;
63 |
64 | Object.defineProperty(window, 'onload', {
65 | get: function() {
66 | return this._onload;
67 | },
68 | set: function(onload) {
69 | this._onload = onload;
70 |
71 | if (options.loadCssFile) {
72 | loadCssFiles(document, new Resource(options)).then(function() {
73 | process.nextTick(onload);
74 | }, function(errors) {
75 | process.nextTick(function() {
76 | if (typeof window.onerror === 'function') {
77 | window.onerror(errors);
78 | }
79 | });
80 | return Promise.reject(errors);
81 | });
82 | } else {
83 | process.nextTick(onload);
84 | }
85 | }
86 | });
87 |
88 |
89 | Object.defineProperty(window, 'onerror', {
90 | get: function() {
91 | return this._onerror;
92 | },
93 | set: function(onerror) {
94 | this._onerror = onerror;
95 | }
96 | });
97 |
98 | return window;
99 | };
100 |
101 |
102 | browser.BrowserAdapter = BrowserAdapter;
103 |
104 |
105 | module.exports = browser;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "browser-x",
3 | "version": "0.0.1-beta6",
4 | "description": "A partial implementation of the W3C DOM API on top of an HTML5 parser and serializer.",
5 | "homepage": "https://github.com/aui/browser-x",
6 | "author": {
7 | "name": "aui",
8 | "email": "sugarpie.tang@gmail.com"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git://github.com/aui/browser-x.git"
13 | },
14 | "bugs": {
15 | "url": "https://github.com/aui/browser-x/issues"
16 | },
17 | "main": "index.js",
18 | "dependencies": {
19 | "css-mediaquery": "^0.1.2",
20 | "cssom": "^0.3.1",
21 | "cssstyle": "^0.2.34",
22 | "nwmatcher": "^1.3.7",
23 | "parse5": "^2.1.5",
24 | "specificity": "^0.1.5",
25 | "verror": "^1.6.1"
26 | },
27 | "scripts": {
28 | "test": "mocha ./test/index"
29 | },
30 | "keywords": [
31 | "dom",
32 | "browser",
33 | "cssom",
34 | "css",
35 | "css3",
36 | "selector",
37 | "spider",
38 | "style",
39 | "getComputedStyle"
40 | ],
41 | "engines": {
42 | "node": ">= 4.0.0"
43 | },
44 | "license": "MIT"
45 | }
--------------------------------------------------------------------------------
/src/adapters/browser-adapter.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function BrowserAdapter(options) {
4 |
5 | options = options || {};
6 |
7 | if (options instanceof BrowserAdapter) {
8 | return options;
9 | }
10 |
11 | for (var key in options) {
12 | this[key] = options[key];
13 | }
14 | }
15 |
16 | BrowserAdapter.prototype = {
17 |
18 | constructor: BrowserAdapter,
19 |
20 | /**
21 | * 文件基础路径 - 支持本地或远程地址
22 | * @type {String}
23 | */
24 | url: 'about:blank',
25 |
26 | /*
27 | * HTML 内容
28 | * @type {String}
29 | */
30 | html: null,
31 |
32 | /**
33 | * 是否支持加载外部 CSS 文件
34 | * @type {Boolean}
35 | */
36 | loadCssFile: false,
37 |
38 | /**
39 | * 是否忽略内部解析错误-关闭它有利于开发调试
40 | * @type {Boolean}
41 | */
42 | silent: true,
43 |
44 | /**
45 | * 请求超时限制
46 | * @type {Number} 毫秒
47 | */
48 | resourceTimeout: 8000,
49 |
50 | /**
51 | * 最大的文件加载数量限制
52 | * @type {Number} 数量
53 | */
54 | resourceMaxNumber: 64,
55 |
56 | /**
57 | * 是否缓存请求成功的资源
58 | * @type {Boolean}
59 | */
60 | resourceCache: true,
61 |
62 | /**
63 | * 映射资源路径
64 | * @param {String} 旧文件地址
65 | * @return {String} 新文件地址
66 | */
67 | resourceMap: function(file) {
68 | return file;
69 | },
70 |
71 | /**
72 | * 忽略资源
73 | * @param {String} 文件地址
74 | * @return {Boolean} 如果返回`true`则忽略当当前文件的加载
75 | */
76 | resourceIgnore: function(file) {// jshint ignore:line
77 | return false;
78 | },
79 |
80 | /**
81 | * 资源加载前的事件
82 | * @param {String} 文件地址
83 | */
84 | resourceBeforeLoad: function(file) {// jshint ignore:line
85 | },
86 |
87 | /**
88 | * 加载远程资源的自定义请求头
89 | * @param {String} 文件地址
90 | * @return {Object}
91 | */
92 | resourceRequestHeaders: function(file) {// jshint ignore:line
93 | return {
94 | 'accept-encoding': 'gzip,deflate'
95 | };
96 | }
97 | };
98 |
99 |
100 | module.exports = BrowserAdapter;
--------------------------------------------------------------------------------
/src/adapters/parser-adapter.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Document = require('../document');
4 | var Comment = require('../comment');
5 | var Node = require('../node');
6 | var DocumentType = require('../document-type');
7 |
8 | function ParserAdapter(options) {
9 |
10 | if (options instanceof ParserAdapter) {
11 | return options;
12 | }
13 |
14 | this.document = null;
15 | this.options = options;
16 | }
17 |
18 |
19 | ParserAdapter.prototype = {
20 |
21 | constructor: ParserAdapter,
22 |
23 | createDocument: function() {
24 | return this.document = new Document(this.options);
25 | },
26 |
27 | createDocumentFragment: function() {
28 | return this.document.createDocumentFragment();
29 | },
30 |
31 | createElement: function(tagName, namespaceURI, attrs) {
32 | var element = this.document.createElementNS(namespaceURI, tagName);
33 | attrs.forEach(function(a) {
34 | element.setAttribute(a.name, a.value);
35 | });
36 | return element;
37 | },
38 |
39 | createCommentNode: function(data) {
40 | return new Comment(this.document, data);
41 | },
42 |
43 | appendChild: function(parentNode, newChild) {
44 | if (!parentNode) {
45 | parentNode = this.document;
46 | }
47 | parentNode.appendChild(newChild);
48 | },
49 |
50 | insertBefore: function(parentNode, newNode, referenceNode) { // jshint ignore:line
51 | parentNode.insertBefore(parentNode, newNode);
52 | },
53 |
54 | setTemplateContent: function(templateElement, contentElement) { // jshint ignore:line
55 | },
56 |
57 | getTemplateContent: function() {},
58 |
59 | setDocumentType: function(document, name, publicId, systemId) {
60 | document.doctype = new DocumentType(document, name, publicId, systemId);
61 | },
62 |
63 | setQuirksMode: function(document) { // jshint ignore:line
64 | },
65 |
66 | isQuirksMode: function(document) { // jshint ignore:line
67 | return false;
68 | },
69 |
70 | detachNode: function(node) { // jshint ignore:line
71 | },
72 |
73 | insertText: function(node, data) {
74 | node.appendChild(this.document.createTextNode(data));
75 | },
76 |
77 | insertTextBefore: function(parentNode, text, referenceNode) { // jshint ignore:line
78 | },
79 |
80 | adoptAttributes: function(recipientNode, attrs) { // jshint ignore:line
81 | },
82 |
83 | getFirstChild: function(node) {
84 | return node.firstChild;
85 | },
86 |
87 | getChildNodes: function(node) {
88 | return node.childNodes;
89 | },
90 |
91 | getParentNode: function(node) {
92 | return node.parentNode;
93 | },
94 |
95 | getAttrList: function(node) {
96 | return node.attributes;
97 | },
98 |
99 | getTagName: function(node) {
100 | return node.tagName.toLowerCase();
101 | },
102 |
103 | getNamespaceURI: function(node) {
104 | return node.namespaceURI;
105 | },
106 |
107 | getTextNodeContent: function(node) {
108 | return node.data;
109 | },
110 |
111 | getCommentNodeContent: function(comment) {
112 | return comment.data;
113 | },
114 |
115 | getDocumentTypeNodeName: function(documentType) {
116 | return documentType.nodeName;
117 | },
118 |
119 | getDocumentTypeNodePublicId: function(documentType) {
120 | return documentType.publicId;
121 | },
122 |
123 | getDocumentTypeNodeSystemId: function(documentType) {
124 | return documentType.systemId;
125 | },
126 |
127 | isTextNode: function(node) {
128 | return node.nodeType === Node.TEXT_NODE;
129 | },
130 |
131 | isCommentNode: function(node) {
132 | return node.nodeType === Node.COMMENT_NODE;
133 | },
134 |
135 | isDocumentTypeNode: function(node) {
136 | return node.nodeType === Node.DOCUMENT_TYPE_NODE;
137 | },
138 |
139 | isElementNode: function(node) {
140 | return node.nodeType === Node.ELEMENT_NODE;
141 | }
142 |
143 | };
144 |
145 |
146 | module.exports = ParserAdapter;
--------------------------------------------------------------------------------
/src/attr.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Node = require('./node');
4 |
5 | function Attr(ownerDocument, name, specified, value) {
6 | Node.call(this, ownerDocument, name, value, Node.ATTRIBUTE_NODE);
7 | Object.defineProperties(this, {
8 | specified: {
9 | value: specified
10 | }
11 | });
12 | }
13 |
14 | Attr.prototype = Object.create(Node.prototype, {
15 | name: {
16 | get: function() {
17 | return this.nodeName;
18 | }
19 | },
20 | value: {
21 | get: function() {
22 | return this.nodeValue;
23 | }
24 | }
25 | });
26 |
27 | Attr.prototype.constructor = Attr;
28 |
29 | module.exports = Attr;
--------------------------------------------------------------------------------
/src/character-data.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Node = require('./node');
4 |
5 | function CharacterData(ownerDocument, name, data, type) {
6 | Node.call(this, ownerDocument, name, data, type);
7 | }
8 |
9 | CharacterData.prototype = Object.create(Node.prototype, {
10 | length: {
11 | get: function() {
12 | return this.nodeValue.length;
13 | }
14 | },
15 | data: {
16 | get: function() {
17 | return this.nodeValue;
18 | }
19 | }
20 | });
21 |
22 | CharacterData.prototype.constructor = CharacterData;
23 |
24 | module.exports = CharacterData;
--------------------------------------------------------------------------------
/src/comment.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var CharacterData = require('./character-data');
4 | var Node = require('./node');
5 |
6 | function Comment(ownerDocument, data) {
7 | CharacterData.call(this, ownerDocument, '#comment', data, Node.COMMENT_NODE);
8 | }
9 |
10 | Comment.prototype = Object.create(CharacterData.prototype);
11 | Comment.prototype.constructor = Comment;
12 |
13 | module.exports = Comment;
--------------------------------------------------------------------------------
/src/document-type.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var NamedNodeMap = require('./named-node-map');
4 | var Node = require('./node');
5 |
6 | function DocumentType(ownerDocument, name, publicId, systemId) {
7 | Node.call(this, ownerDocument, name, null, Node.DOCUMENT_TYPE_NODE);
8 | Object.defineProperties(this, {
9 | entities: {
10 | value: new NamedNodeMap()
11 | },
12 | notations: {
13 | value: new NamedNodeMap()
14 | },
15 | publicId: {
16 | value: publicId
17 | },
18 | systemId: {
19 | value: systemId
20 | }
21 | });
22 | }
23 |
24 | DocumentType.prototype = Object.create(Node.prototype, {
25 | name: {
26 | get: function() {
27 | return this.nodeName;
28 | }
29 | }
30 | });
31 |
32 | DocumentType.prototype.constructor = DocumentType;
33 |
34 | module.exports = DocumentType;
--------------------------------------------------------------------------------
/src/document.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var VError = require('verror');
4 | var path = require('path');
5 | var cssom = require('./style');
6 | var Attr = require('./attr');
7 | var Comment = require('./comment');
8 | var Element = require('./element');
9 | var Node = require('./node');
10 | var Text = require('./text');
11 | var Window = require('./window');
12 | var StyleSheetList = require('./style-sheet-list');
13 | var createElement = require('./elements').create;
14 |
15 | function Document(options) {
16 | Node.call(this, this, '#document', null, Node.DOCUMENT_NODE);
17 | this.defaultView = new Window(this);
18 | this._doctype = null;
19 | this._documentElement = null;
20 | this._styleSheets = null;
21 | this._options = options;
22 | this._url = options.url;
23 |
24 | // TODO about:blank
25 | if (!/^https?\:/.test(this._url)) {
26 | this._url = path.resolve(this._url);
27 | }
28 | }
29 |
30 | Document.prototype = Object.create(Node.prototype, {
31 | URL: {
32 | get: function() {
33 | // TODO
34 | return this._url;
35 | }
36 | },
37 | doctype: {
38 | get: function() {
39 | return this._doctype;
40 | },
41 | set: function(doctype) {
42 | // 设置一次后就不许修改了
43 | if (!this._doctype) {
44 | this._doctype = doctype;
45 | }
46 | }
47 | },
48 | compatMode: {
49 | get: function() {
50 | return 'CSS1Compat';
51 | }
52 | },
53 | documentElement: {
54 | get: function() {
55 | if (this._documentElement) {
56 | return this._documentElement;
57 | } else {
58 | var child = this._first;
59 | while (child) {
60 | if (child.nodeType === Node.ELEMENT_NODE) {
61 | return this._documentElement = child;
62 | }
63 | child = child._next;
64 | }
65 | return null;
66 | }
67 | }
68 | },
69 | head: {
70 | get: function() {
71 | return this.getElementsByTagName('head').item(0);
72 | }
73 | },
74 | title: {
75 | get: function() {
76 | return this.getElementsByTagName('title').item(0).textContent;
77 | }
78 | },
79 | body: {
80 | get: function() {
81 | return this.getElementsByTagName('body').item(0);
82 | }
83 | },
84 | styleSheets: {
85 | get: function() {
86 | var silent = this._options.silent;
87 |
88 | if (!this._styleSheets) {
89 | this._styleSheets = new StyleSheetList();
90 |
91 | // TODO test media
92 | var nodeList = this.querySelectorAll('style,link[rel=stylesheet]:not([disabled])');
93 |
94 | for (var i = 0; i < nodeList.length; i++) {
95 |
96 | var ownerNode = nodeList.item(i);
97 | var textContent = ownerNode.textContent;
98 | var cssStyleSheet = cssParse(textContent, this.URL, silent);
99 |
100 | if (ownerNode.nodeName === 'LINK') {
101 | cssStyleSheet.cssRules = null;
102 | cssStyleSheet.href = ownerNode.href;
103 | } else {
104 | cssStyleSheet.href = null;
105 | }
106 |
107 | cssStyleSheet.ownerNode = ownerNode;
108 | this._styleSheets.push(cssStyleSheet);
109 | }
110 | }
111 |
112 |
113 | return this._styleSheets;
114 |
115 | function cssParse(data, file, silent) {
116 | try {
117 | return cssom.parse(data);
118 | } catch (errors) {
119 | if (!silent) {
120 | throw new VError(errors, 'parse "%s" failed. ', file, data);
121 | }
122 | return cssom.parse('');
123 | }
124 | }
125 | }
126 | }
127 | });
128 |
129 | Document.prototype.constructor = Document;
130 |
131 | Document.prototype.createElement = function(tagName) {
132 | var namespaceURI = this.documentElement.namespaceURI;
133 | return this.createElementNS(namespaceURI, tagName);
134 | };
135 |
136 | Document.prototype.createElementNS = createElement;
137 |
138 | Document.prototype.createDocumentFragment = function() {
139 | throw new Error('not yet implemented');
140 | };
141 |
142 | Document.prototype.createTextNode = function(data) {
143 | return new Text(this, data);
144 | };
145 |
146 | Document.prototype.createComment = function(data) {
147 | return new Comment(this, data);
148 | };
149 |
150 | Document.prototype.createAttribute = function(name) {
151 | return new Attr(this, name, false, '');
152 | };
153 |
154 | Document.prototype.getElementsByTagName = function(tagName) {
155 | return Element.prototype.getElementsByTagName.call(this, tagName);
156 | };
157 |
158 | // TODO restrict to just Element types
159 | // TODO 性能优化
160 | Document.prototype.getElementById = function(id) {
161 | var child = this.firstChild;
162 |
163 | out: while (child) {
164 | if (child.id === id) {
165 | return child;
166 | }
167 | if (child.firstChild) {
168 | child = child.firstChild;
169 | } else if (child.nextSibling) {
170 | child = child.nextSibling;
171 | } else {
172 | do {
173 | child = child.parentNode;
174 | if (child === this) break out;
175 | } while (!child.nextSibling);
176 | child = child.nextSibling;
177 | }
178 | }
179 |
180 | return null;
181 | };
182 |
183 | Document.prototype.querySelector = function(selector) {
184 | return Element.prototype.querySelector.call(this, selector);
185 | };
186 |
187 | Document.prototype.querySelectorAll = function(selector) {
188 | return Element.prototype.querySelectorAll.call(this, selector);
189 | };
190 |
191 |
192 | module.exports = Document;
--------------------------------------------------------------------------------
/src/dom-exception.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function DOMException(code) {
4 | this.code = code;
5 | }
6 |
7 | DOMException.INDEX_SIZE_ERR = 1;
8 | DOMException.DOMSTRING_SIZE_ERR = 2;
9 | DOMException.HIERARCHY_REQUEST_ERR = 3;
10 | DOMException.WRONG_DOCUMENT_ERR = 4;
11 | DOMException.INVALID_CHARACTER_ERR = 5;
12 | DOMException.NO_DATA_ALLOWED_ERR = 6;
13 | DOMException.NO_MODIFICATION_ALLOWED_ERR = 7;
14 | DOMException.NOT_FOUND_ERR = 8;
15 | DOMException.NOT_SUPPORTED_ERR = 9;
16 | DOMException.INUSE_ATTRIBUTE_ERR = 10;
17 |
18 | module.exports = DOMException;
--------------------------------------------------------------------------------
/src/element.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var nwmatcher = require('nwmatcher/src/nwmatcher-noqsa');
3 | var parse5 = require('parse5');
4 |
5 |
6 | var Attr = require('./attr');
7 | var Node = require('./node');
8 | var NodeList = require('./node-list');
9 | var NamedNodeMap = require('./named-node-map');
10 |
11 | function Element(ownerDocument, name, namespaceURI) {
12 | Node.call(this, ownerDocument, name, null, Node.ELEMENT_NODE);
13 | Object.defineProperties(this, {
14 | namespaceURI: {
15 | enumerable: true,
16 | value: namespaceURI
17 | },
18 | _attributes: {
19 | value: new NamedNodeMap()
20 | }
21 | });
22 | }
23 |
24 | Element.prototype = Object.create(Node.prototype, {
25 | attributes: {
26 | get: function() {
27 | return this._attributes;
28 | }
29 | },
30 | id: {
31 | get: function() {
32 | return this.getAttribute('id') || '';
33 | }
34 | },
35 | className: {
36 | get: function() {
37 | return this.getAttribute('class') || '';
38 | }
39 | },
40 | tagName: {
41 | get: function() {
42 | return this.nodeName;
43 | }
44 | },
45 | innerHTML: {
46 | get: function() {
47 | return parse5.serialize(this, this.ownerDocument._options.parserAdapter);
48 | }
49 | },
50 | outerHTML: {
51 | get: function() {
52 | // @see parse5: SerializerOptions.getChildNodes
53 | return parse5.serialize({
54 | childNodes: new NodeList([this])
55 | }, this.ownerDocument._options.parserAdapter);
56 | }
57 | }
58 |
59 | });
60 |
61 | Element.prototype.constructor = Element;
62 |
63 | Element.prototype.hasAttribute = function(name) {
64 | return this._attributes.getNamedItem(name) !== null;
65 | };
66 |
67 | Element.prototype.getAttribute = function(name) {
68 | var attr = this._attributes.getNamedItem(name);
69 | return attr && attr.nodeValue;
70 | };
71 |
72 | Element.prototype.setAttribute = function(name, value) {
73 | var attr = new Attr(this.ownerDocument, name, true, value);
74 | this._attributes.setNamedItem(attr);
75 | };
76 |
77 | Element.prototype.removeAttribute = function(name) {
78 | this._attributes.removeNamedItem(name);
79 | };
80 |
81 | Element.prototype.getAttributeNode = function(name) {
82 | return this._attributes.getNamedItem(name);
83 | };
84 |
85 | Element.prototype.setAttributeNode = function() {
86 | throw new Error('not yet implemented');
87 | };
88 |
89 | Element.prototype.removeAttributeNode = function() {
90 | throw new Error('not yet implemented');
91 | };
92 |
93 | Element.prototype.getElementsByClassName = function() {
94 | throw new Error('not yet implemented');
95 | };
96 |
97 | Element.prototype.querySelector = function(selector) {
98 | return matcher(this).first(selector, this);
99 | };
100 |
101 | Element.prototype.querySelectorAll = function(selector) {
102 | return new NodeList(matcher(this).select(selector, this));
103 | };
104 |
105 | Element.prototype.matches = function(selector) {
106 | return matcher(this).match(this, selector);
107 | };
108 |
109 | Element.prototype.getElementsByTagName = function(tagName) {
110 | var nodes = [];
111 | var child = this.firstChild;
112 |
113 | tagName = tagName.toUpperCase();
114 |
115 | out: while (child) {
116 |
117 | if (child.nodeType === Node.ELEMENT_NODE) {
118 | if (tagName === '*' || child.tagName === tagName) {
119 | nodes.push(child);
120 | }
121 | }
122 |
123 | if (child.firstChild) {
124 | child = child.firstChild;
125 | } else if (child.nextSibling) {
126 | child = child.nextSibling;
127 | } else {
128 | do {
129 | child = child.parentNode;
130 | if (child === this) {
131 | break out;
132 | }
133 | } while (!child.nextSibling);
134 |
135 | child = child.nextSibling;
136 | }
137 | }
138 |
139 | return new NodeList(nodes);
140 | };
141 |
142 |
143 | function matcher(element) {
144 | var document = element.ownerDocument;
145 | var matcher = document._matcher;
146 |
147 | if (matcher) {
148 | return matcher;
149 | }
150 |
151 | matcher = nwmatcher({
152 | document: document
153 | });
154 |
155 | Object.defineProperty(document, '_matcher', {
156 | value: matcher
157 | });
158 |
159 | return matcher;
160 | }
161 |
162 | module.exports = Element;
--------------------------------------------------------------------------------
/src/elements/html-anchor-element.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var url = require('url');
4 | var HTMLElement = require('../html-element');
5 |
6 | function HTMLAnchorElement(document, name, namespaceURI) {
7 | HTMLElement.call(this, document, name, namespaceURI);
8 | }
9 |
10 | HTMLAnchorElement.prototype = Object.create(HTMLElement.prototype, {
11 | href: {
12 | get: function() {
13 | var href = this.getAttribute('href');
14 | // TODO file://
15 | return url.resolve(this.baseURI, href);
16 | }
17 | }
18 | });
19 |
20 | HTMLAnchorElement.prototype.constructor = HTMLAnchorElement;
21 |
22 | module.exports = HTMLAnchorElement;
--------------------------------------------------------------------------------
/src/elements/html-button-element.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var HTMLElement = require('../html-element');
4 |
5 | function HTMLButtonElement(document, name, namespaceURI) {
6 | HTMLElement.call(this, document, name, namespaceURI);
7 | }
8 |
9 | HTMLButtonElement.prototype = Object.create(HTMLElement.prototype, {
10 | type: {
11 | get: function() {
12 | var type = this.getAttribute('type');
13 | return type || 'submit';
14 | }
15 | },
16 | value: {
17 | get: function() {
18 | return this.getAttribute('value') || '';
19 | }
20 | },
21 | disabled: {
22 | get: function() {
23 | return this.hasAttribute('disabled');
24 | }
25 | },
26 | form: {
27 | get: function() {
28 | var e = this;
29 | while (e) {
30 | if (e.nodeName === 'FORM') {
31 | return e;
32 | }
33 | e = e.parentNode;
34 | }
35 |
36 | return null;
37 | }
38 | }
39 | });
40 |
41 | HTMLButtonElement.prototype.constructor = HTMLButtonElement;
42 |
43 | module.exports = HTMLButtonElement;
--------------------------------------------------------------------------------
/src/elements/html-image-element.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var url = require('url');
4 | var HTMLElement = require('../html-element');
5 |
6 | function HTMLImageElement(document, name, namespaceURI) {
7 | HTMLElement.call(this, document, name, namespaceURI);
8 | }
9 |
10 | HTMLImageElement.prototype = Object.create(HTMLElement.prototype, {
11 | src: {
12 | get: function() {
13 | var src = this.getAttribute('src');
14 | // TODO file://
15 | return url.resolve(this.baseURI, src);
16 | }
17 | }
18 | });
19 |
20 | HTMLImageElement.prototype.constructor = HTMLImageElement;
21 |
22 | module.exports = HTMLImageElement;
--------------------------------------------------------------------------------
/src/elements/html-input-element.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var HTMLElement = require('../html-element');
4 |
5 | function HTMLInputElement(document, name, namespaceURI) {
6 | HTMLElement.call(this, document, name, namespaceURI);
7 | }
8 |
9 | HTMLInputElement.prototype = Object.create(HTMLElement.prototype, {
10 | type: {
11 | get: function() {
12 | var type = this.getAttribute('type');
13 | return type || 'text';
14 | }
15 | },
16 | value: {
17 | get: function() {
18 | return this.getAttribute('value') || '';
19 | }
20 | },
21 | defaultValue: {
22 | get: function() {
23 | return this.value;
24 | }
25 | },
26 | checked: {
27 | get: function() {
28 | return this.hasAttribute('checked');
29 | }
30 | },
31 | defaultChecked: {
32 | get: function() {
33 | return this.checked;
34 | }
35 | },
36 | disabled: {
37 | get: function() {
38 | return this.hasAttribute('disabled');
39 | }
40 | },
41 | form: {
42 | get: function() {
43 | var e = this;
44 | while (e) {
45 | if (e.nodeName === 'FORM') {
46 | return e;
47 | }
48 | e = e.parentNode;
49 | }
50 |
51 | return null;
52 | }
53 | }
54 | });
55 |
56 | HTMLInputElement.prototype.constructor = HTMLInputElement;
57 |
58 | module.exports = HTMLInputElement;
--------------------------------------------------------------------------------
/src/elements/html-link-element.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var url = require('url');
4 | var HTMLElement = require('../html-element');
5 |
6 | function HTMLLinkElement(document, name, namespaceURI) {
7 | HTMLElement.call(this, document, name, namespaceURI);
8 | }
9 |
10 | HTMLLinkElement.prototype = Object.create(HTMLElement.prototype, {
11 | href: {
12 | get: function() {
13 | var href = this.getAttribute('href');
14 | // TODO file://
15 | return url.resolve(this.baseURI, href);
16 | }
17 | },
18 | disabled: {
19 | get: function() {
20 | return this.hasAttribute('disabled');
21 | }
22 | }
23 | });
24 |
25 | HTMLLinkElement.prototype.constructor = HTMLLinkElement;
26 |
27 | module.exports = HTMLLinkElement;
--------------------------------------------------------------------------------
/src/elements/html-option-element.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var HTMLElement = require('../html-element');
4 |
5 | function HTMLOptionElement(document, name, namespaceURI) {
6 | HTMLElement.call(this, document, name, namespaceURI);
7 | }
8 |
9 | HTMLOptionElement.prototype = Object.create(HTMLElement.prototype, {
10 | index: {
11 | get: function() {
12 | var select = closest('SELECT');
13 | if (select) {
14 | return Array.prototype.indexOf.call(select.options, this);
15 | } else {
16 | return 0;
17 | }
18 | }
19 | },
20 | value: {
21 | get: function() {
22 | return this.hasAttribute('value') ? this.getAttribute('value') : this.innerHTML;
23 | }
24 | },
25 | selected: {
26 | // TODO 如果兄弟 option 标签也有 selected 属性,
27 | // 且 select 无 multiple 属性,则需要处理冲突
28 | get: function() {
29 | return this.hasAttribute('selected');
30 | }
31 | },
32 | defaultSelected: {
33 | get: function() {
34 | return this.selected;
35 | }
36 | },
37 | disabled: {
38 | get: function() {
39 | return this.hasAttribute('disabled');
40 | }
41 | },
42 | form: {
43 | get: function() {
44 | return closest('FORM');
45 | }
46 | }
47 | });
48 |
49 | HTMLOptionElement.prototype.constructor = HTMLOptionElement;
50 |
51 | function closest(e, nodeName) {
52 | while (e) {
53 | if (e.nodeName === nodeName) {
54 | return e;
55 | }
56 | e = e.parentNode;
57 | }
58 |
59 | return null;
60 | }
61 |
62 | module.exports = HTMLOptionElement;
--------------------------------------------------------------------------------
/src/elements/html-select-element.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var HTMLElement = require('../html-element');
4 |
5 | function HTMLSelectElement(document, name, namespaceURI) {
6 | HTMLElement.call(this, document, name, namespaceURI);
7 | }
8 |
9 | HTMLSelectElement.prototype = Object.create(HTMLElement.prototype, {
10 | options: {
11 | // TODO return HTMLOptionsCollection
12 | get: function() {
13 | return this.getElementsByTagName('option');
14 | }
15 | },
16 | multiple: {
17 | get: function() {
18 | return this.hasAttribute('multiple');
19 | }
20 | },
21 | type: {
22 | get: function() {
23 | return this.multiple ? 'select-multiple' : 'select-one';
24 | }
25 | },
26 | selectedIndex: {
27 | get: function() {
28 | return Array.prototype.reduceRight.call(this.options, function(prev, option, i) {
29 | return option.selected ? i : prev;
30 | }, -1);
31 | }
32 | },
33 | length: {
34 | get: function() {
35 | return this.options.length;
36 | }
37 | },
38 | value: {
39 | get: function() {
40 | var i = this.selectedIndex;
41 | if (this.options.length && (i === -1)) {
42 | i = 0;
43 | }
44 | if (i === -1) {
45 | return '';
46 | }
47 | return this.options[i].value;
48 | }
49 | },
50 | disabled: {
51 | get: function() {
52 | return this.hasAttribute('disabled');
53 | }
54 | },
55 | form: {
56 | get: function() {
57 | return closest('FORM');
58 | }
59 | }
60 | });
61 |
62 | HTMLSelectElement.prototype.constructor = HTMLSelectElement;
63 |
64 | function closest(e, nodeName) {
65 | while (e) {
66 | if (e.nodeName === nodeName) {
67 | return e;
68 | }
69 | e = e.parentNode;
70 | }
71 |
72 | return null;
73 | }
74 |
75 | module.exports = HTMLSelectElement;
--------------------------------------------------------------------------------
/src/elements/html-text-area-element.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var HTMLElement = require('../html-element');
4 |
5 | function HTMLTextAreaElement(document, name, namespaceURI) {
6 | HTMLElement.call(this, document, name, namespaceURI);
7 | }
8 |
9 | HTMLTextAreaElement.prototype = Object.create(HTMLElement.prototype, {
10 | type: {
11 | get: function() {
12 | return 'textarea';
13 | }
14 | },
15 | value: {
16 | get: function() {
17 | return this.textContent;
18 | }
19 | },
20 | defaultValue: {
21 | get: function() {
22 | return this.value;
23 | }
24 | },
25 | disabled: {
26 | get: function() {
27 | return this.hasAttribute('disabled');
28 | }
29 | },
30 | form: {
31 | get: function() {
32 | var e = this;
33 | while (e) {
34 | if (e.nodeName === 'FORM') {
35 | return e;
36 | }
37 | e = e.parentNode;
38 | }
39 |
40 | return null;
41 | }
42 | }
43 | });
44 |
45 | HTMLTextAreaElement.prototype.constructor = HTMLTextAreaElement;
46 |
47 | module.exports = HTMLTextAreaElement;
--------------------------------------------------------------------------------
/src/elements/html-unknown-element.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var HTMLElement = require('../html-element');
4 |
5 | function HTMLUnknownElement(document, name, namespaceURI) {
6 | HTMLElement.call(this, document, name, namespaceURI);
7 | }
8 |
9 | HTMLUnknownElement.prototype = Object.create(HTMLElement.prototype, {});
10 | HTMLUnknownElement.prototype.constructor = HTMLUnknownElement;
11 |
12 | module.exports = HTMLUnknownElement;
--------------------------------------------------------------------------------
/src/elements/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var elements = {
4 | '*': require('./html-unknown-element'),
5 | 'A': require('./html-anchor-element'),
6 | 'IMG': require('./html-image-element'),
7 | 'LINK': require('./html-link-element'),
8 |
9 | // support nwmatcher
10 | 'BUTTON': require('./html-button-element'),
11 | 'INPUT': require('./html-input-element'),
12 | 'SELECT': require('./html-select-element'),
13 | 'OPTION': require('./html-option-element'),
14 | 'TEXTAREA': require('./html-text-area-element')
15 | };
16 |
17 |
18 | module.exports = {
19 | create: function(namespaceURI, tagName) {
20 | tagName = tagName.toUpperCase();
21 | if (elements.hasOwnProperty(tagName)) {
22 | return new elements[tagName](this, tagName, namespaceURI);
23 | } else {
24 | return new elements['*'](this, tagName, namespaceURI);
25 | }
26 | }
27 | };
--------------------------------------------------------------------------------
/src/html-element.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Element = require('./element');
4 | var CSSStyleDeclaration = require('cssstyle').CSSStyleDeclaration;
5 |
6 | function HTMLElement(document, name, namespaceURI) {
7 | Element.call(this, document, name, namespaceURI);
8 | }
9 |
10 | HTMLElement.prototype = Object.create(Element.prototype, {
11 |
12 | style: {
13 | get: function() {
14 | if (this._style) {
15 | return this._style;
16 | } else {
17 | var style = this._style = new CSSStyleDeclaration();
18 | var cssText = this.getAttribute('style');
19 |
20 | if (cssText) {
21 | style.cssText = cssText;
22 | }
23 |
24 | return this._style;
25 | }
26 | }
27 | },
28 |
29 | // support nwmatcher
30 | lang: {
31 | get: function() {
32 | return this.getAttribute('lang') || '';
33 | }
34 | }
35 | });
36 |
37 | HTMLElement.prototype.constructor = HTMLElement;
38 |
39 | module.exports = HTMLElement;
--------------------------------------------------------------------------------
/src/named-node-map.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function NamedNodeMap() {
4 | Object.defineProperties(this, {
5 | _indexs: {
6 | value: {}
7 | }
8 | });
9 | }
10 |
11 | NamedNodeMap.prototype = {
12 |
13 | constructor: NamedNodeMap,
14 |
15 | length: 0,
16 |
17 | getNamedItem: function(name) {
18 | return this[this._indexs[name]] || null;
19 | },
20 |
21 | setNamedItem: function(node) {
22 | var name = node.nodeName;
23 | var index = this._indexs[name];
24 |
25 | this[name] = node;
26 |
27 | if (typeof index === 'number') {
28 | var oldNode = this[index];
29 | this[index] = node;
30 |
31 | return oldNode;
32 | } else {
33 | this[this.length] = node;
34 | this._indexs[name] = this.length;
35 | this.length++;
36 |
37 | return null;
38 | }
39 | },
40 |
41 | removeNamedItem: function(name) {
42 | var index = this._indexs[name];
43 |
44 | delete this[name];;
45 |
46 | if (typeof index === 'number') {
47 | var node = Array.prototype.splice.call(this, index, 1)[0];
48 | var length = this.length;
49 | index = -1;
50 |
51 | // 重建索引
52 | while (++index < length) {
53 | this._indexs[this[index].nodeName] = index;
54 | }
55 |
56 | return node;
57 | } else {
58 | return null;
59 | }
60 | },
61 |
62 | item: function(index) {
63 | return this[index] || null;
64 | }
65 | };
66 |
67 |
68 | module.exports = NamedNodeMap;
--------------------------------------------------------------------------------
/src/node-list.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function NodeList(nodes) {
4 | Array.prototype.push.apply(this, nodes);
5 | }
6 |
7 | NodeList.prototype = Object.create(Object.prototype, {});
8 | NodeList.prototype.constructor = NodeList;
9 |
10 |
11 | NodeList.prototype.item = function(index) {
12 | return this[index] || null;
13 | };
14 |
15 |
16 | module.exports = NodeList;
--------------------------------------------------------------------------------
/src/node.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var url = require('url');
4 | var DOMException = require('./dom-exception');
5 | var NodeList = require('./node-list');
6 |
7 | function Node(ownerDocument, nodeName, nodeValue, nodeType) {
8 | // TODO hasAttributes()
9 | // TODO contains()
10 | // TODO replaceChild()
11 | Object.defineProperties(this, {
12 | _parent: {
13 | writable: true
14 | },
15 | _first: {
16 | writable: true
17 | },
18 | _last: {
19 | writable: true
20 | },
21 | _next: {
22 | writable: true
23 | },
24 | _previous: {
25 | writable: true
26 | },
27 | childNodes: {
28 | enumerable: true,
29 | value: new NodeList([])
30 | },
31 | nodeName: {
32 | enumerable: true,
33 | value: nodeName
34 | },
35 | nodeValue: {
36 | enumerable: true,
37 | value: nodeValue
38 | },
39 | nodeType: {
40 | enumerable: true,
41 | value: nodeType
42 | },
43 | ownerDocument: {
44 | enumerable: true,
45 | value: ownerDocument
46 | }
47 |
48 | });
49 | }
50 |
51 | Node.ELEMENT_NODE = 1;
52 | Node.ATTRIBUTE_NODE = 2; // 废弃
53 | Node.TEXT_NODE = 3;
54 | Node.CDATA_SECTION_NODE = 4; // 废弃
55 | Node.ENTITY_REFERENCE_NODE = 5; // 废弃
56 | Node.ENTITY_NODE = 6; // 废弃
57 | Node.PROCESSING_INSTRUCTION_NODE = 7;
58 | Node.COMMENT_NODE = 8;
59 | Node.DOCUMENT_NODE = 9;
60 | Node.DOCUMENT_TYPE_NODE = 10;
61 | Node.DOCUMENT_FRAGMENT_NODE = 11;
62 | Node.NOTATION_NODE = 12; // 废弃
63 |
64 | Node.prototype = Object.create(Object.prototype, {
65 | parentNode: {
66 | enumerable: true,
67 | get: function() {
68 | return this._parent;
69 | }
70 | },
71 | firstChild: {
72 | enumerable: true,
73 | get: function() {
74 | return this._first;
75 | }
76 | },
77 | lastChild: {
78 | enumerable: true,
79 | get: function() {
80 | return this._last;
81 | }
82 | },
83 | nextSibling: {
84 | enumerable: true,
85 | get: function() {
86 | return this._next;
87 | }
88 | },
89 | previousSibling: {
90 | enumerable: true,
91 | get: function() {
92 | return this._previous;
93 | }
94 | },
95 | baseURI: {
96 | enumerable: true,
97 | get: function() {
98 | var ownerDocument = this.ownerDocument;
99 | var base = ownerDocument.getElementsByTagName('base').item(0);
100 | if (base) {
101 | return url.resolve(ownerDocument.URL, base.getAttribute('href'));
102 | } else {
103 | return ownerDocument.URL;
104 | }
105 | }
106 | },
107 | textContent: {
108 | enumerable: true,
109 | get: function() {
110 | switch (this.nodeType) {
111 | case Node.COMMENT_NODE:
112 | case Node.CDATA_SECTION_NODE:
113 | case Node.PROCESSING_INSTRUCTION_NODE:
114 | case Node.TEXT_NODE:
115 | return this.nodeValue;
116 |
117 | case Node.ATTRIBUTE_NODE:
118 | case Node.DOCUMENT_FRAGMENT_NODE:
119 | case Node.ELEMENT_NODE:
120 | case Node.ENTITY_NODE:
121 | case Node.ENTITY_REFERENCE_NODE:
122 | var out = '';
123 | for (var i = 0; i < this.childNodes.length; ++i) {
124 | if (this.childNodes.item(i).nodeType !== Node.COMMENT_NODE &&
125 | this.childNodes.item(i).nodeType !== Node.PROCESSING_INSTRUCTION_NODE) {
126 | out += this.childNodes.item(i).textContent || '';
127 | }
128 | }
129 | return out;
130 |
131 | default:
132 | return null;
133 | }
134 | }
135 | }
136 | });
137 |
138 | Node.prototype.constructor = Node;
139 |
140 |
141 | // Node.prototype.contains = function(element) {
142 | // while ((element = element.parentNode) && element.nodeType == 1) {
143 | // if (element === this) return true;
144 | // }
145 | // return false;
146 | // };
147 |
148 | Node.prototype.insertBefore = function(newChild) { // jshint ignore:line
149 | throw new Error('not yet implemented');
150 |
151 | // if (newChild._parent) {
152 | // newChild._parent.removeChild(newChild);
153 | // }
154 |
155 | // newChild._parent = this;
156 |
157 | // if (newChild._previous = this._first) {
158 | // this._first._previous = newChild;
159 | // }
160 |
161 | // if (!this._last) {
162 | // this._last = newChild;
163 | // }
164 |
165 | // this._first = newChild;
166 |
167 | // Array.prototype.push.call(this.childNodes, newChild);
168 |
169 | // return newChild;
170 | };
171 |
172 | Node.prototype.removeChild = function(oldChild) {
173 |
174 | if (oldChild._parent !== this) {
175 | throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR);
176 | }
177 |
178 | // TODO 性能优化
179 | Array.prototype.splice.call(this.childNodes, Array.prototype.indexOf.call(oldChild), 1);
180 |
181 | if (oldChild._previous) {
182 | oldChild._previous._next = oldChild._next;
183 | } else {
184 | this._first = oldChild._next;
185 | }
186 |
187 | if (oldChild._next) {
188 | oldChild._next._previous = oldChild._previous;
189 | } else {
190 | this._last = oldChild._previous;
191 | }
192 |
193 | oldChild._previous = oldChild._next = oldChild._parent = null;
194 |
195 | return oldChild;
196 | };
197 |
198 | Node.prototype.appendChild = function(newChild) {
199 |
200 | if (newChild._parent) {
201 | newChild._parent.removeChild(newChild);
202 | }
203 |
204 | newChild._parent = this;
205 |
206 | if (newChild._previous = this._last) {
207 | this._last._next = newChild;
208 | }
209 |
210 | if (!this._first) {
211 | this._first = newChild;
212 | }
213 |
214 | this._last = newChild;
215 |
216 | // TODO 性能优化
217 | Array.prototype.push.call(this.childNodes, newChild);
218 |
219 | return newChild;
220 | };
221 |
222 | Node.prototype.hasChildNodes = function() {
223 | return !!this._first;
224 | };
225 |
226 |
227 | module.exports = Node;
--------------------------------------------------------------------------------
/src/style-sheet-list.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function StyleSheetList() {}
4 |
5 | StyleSheetList.prototype = {
6 | constructor: StyleSheetList,
7 | length: 0,
8 | item: function(i) {
9 | return this[i];
10 | },
11 | push: function(sheet) {
12 | this[this.length] = sheet;
13 | this.length++;
14 | }
15 | };
16 |
17 |
18 | module.exports = StyleSheetList;
--------------------------------------------------------------------------------
/src/style/cascade.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var specificity = require('specificity');
4 | var CSSStyleDeclaration = require('cssstyle').CSSStyleDeclaration;
5 | var eachCssStyleRule = require('./each-css-style-rule');
6 |
7 |
8 | /**
9 | * 获取优先级信息
10 | * @param {String} 选择器文本
11 | * @return {Array}
12 | */
13 | function getSpecificity(selectorText) {
14 | return specificity
15 | .calculate(selectorText)[0]
16 | .specificity
17 | .split(',')
18 | .map(function(s) {
19 | return parseInt(s, 10);
20 | });
21 | }
22 |
23 |
24 | /**
25 | * 优先级比较算法
26 | * @see getSpecificity
27 | * @return {Number}
28 | */
29 | function compareSpecificity(first, second) {
30 | for (var i = 0; i < first.length; i++) {
31 | var a = first[i];
32 | var b = second[i];
33 |
34 | if (a === b) {
35 | continue;
36 | }
37 |
38 | return a - b;
39 | }
40 |
41 | return 0;
42 | }
43 |
44 |
45 | /**
46 | * 获取标准化伪元素名称
47 | * @param {String} 伪名称
48 | * @return {String} 格式化后的名称
49 | */
50 | function normalizePseudo(value) {
51 | return value ? value.toLowerCase().replace(/\:+(\w+)$/, '$1') : '';
52 | }
53 |
54 |
55 | /**
56 | * 获取元素应用的样式,包括 style 属性、style 与 link 标签所引入的样式
57 | * @param {HTMLElement} 元素
58 | * @param {String}
59 | * @return {CSSStyleDeclaration}
60 | */
61 | function cascade(element, pseudo) {
62 | pseudo = normalizePseudo(pseudo);
63 |
64 | var cacheKey = '_cascade::' + pseudo;
65 |
66 | if (element[cacheKey]) {
67 | return element[cacheKey];
68 | }
69 |
70 | var document = element.ownerDocument;
71 | var cssStyleDeclarations = [];
72 | var style = new CSSStyleDeclaration();
73 | var importants = {};
74 | var forEach = Array.prototype.forEach;
75 | var pseudoRe = pseudo ? new RegExp('\\:+' + pseudo + '$', 'i') : null;
76 |
77 | // 行内样式
78 | cssStyleDeclarations.push(element.style);
79 | if (!element.style._specificity) {
80 | element.style._specificity = [1, 0, 0, 0];
81 | }
82 |
83 | // 非行内样式
84 | // StyleSheetList > CSSStyleSheet.cssRules > CSSStyleRule
85 | eachCssStyleRule(document, function(rule) {
86 | var selectorText = rule.selectorText;
87 | if (matches(element, selectorText)) {
88 | rule.style._specificity = getSpecificity(selectorText);
89 | cssStyleDeclarations.push(rule.style);
90 | }
91 | });
92 |
93 |
94 | // 优先级排序
95 | cssStyleDeclarations.sort(function(a, b) {
96 | return compareSpecificity(a._specificity, b._specificity);
97 | }).forEach(function(cssStyleDeclaration) {
98 | forEach.call(cssStyleDeclaration, cssStyleDeclarationForEach, cssStyleDeclaration);
99 | });
100 |
101 |
102 | function matches(element, selector) {
103 | if (pseudoRe) {
104 | // *::after
105 | // ::after
106 | // element::after
107 | selector = selector.replace(pseudoRe, '') || '*';
108 | }
109 | try {
110 | return element.matches(selector);
111 | } catch (e) {}
112 | }
113 |
114 |
115 | function cssStyleDeclarationForEach(key) {
116 | var cssStyleDeclaration = this;
117 | var important = cssStyleDeclaration.getPropertyPriority(key);
118 |
119 | // 当前样式属性覆盖规则,任意之一:
120 | // 1.当前声明了重要 2.之前没有声明重要 3.之前没有定义
121 | if (important || !importants[key] || !style[key]) {
122 | style[key] = cssStyleDeclaration[key];
123 | }
124 |
125 | importants[key] = important;
126 | }
127 |
128 |
129 | element[cacheKey] = style;
130 |
131 | return style;
132 | }
133 |
134 | module.exports = cascade;
--------------------------------------------------------------------------------
/src/style/config/inherit.json:
--------------------------------------------------------------------------------
1 | ["font-family", "font-size", "font-style", "font-variant", "font-weight", "font",
2 | "letter-spacing", "line-height", "text-align", "text-indent", "texttransform",
3 | "word-spacing", "list-style-image", "list-style-position", "list-style-type",
4 | "list-style", "color"]
--------------------------------------------------------------------------------
/src/style/each-css-style-rule.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var cssom = require('cssom');
4 | var cssMediaQuery = require('css-mediaquery');
5 | var CSSStyleRule = cssom.CSSStyleRule;
6 | var CSSImportRule = cssom.CSSImportRule;
7 | var CSSMediaRule = cssom.CSSMediaRule;
8 |
9 | var SCREEN_CONFIG = {
10 | type: 'screen',
11 | width: '1440px'
12 | };
13 |
14 | function cssRuleListFor(cssRuleList, callback) {
15 | for (var n = 0; n < cssRuleList.length; n++) {
16 | var cssRule = cssRuleList[n];
17 |
18 | if (cssRule instanceof CSSStyleRule) {
19 | callback(cssRule);
20 | } else if (cssRule instanceof CSSImportRule) {
21 |
22 | var cssStyleSheet = cssRule.styleSheet;
23 | cssRuleListFor(cssStyleSheet.cssRules || [], callback);
24 |
25 | } else if (cssRule instanceof CSSMediaRule) {
26 | Array.prototype.forEach.call(cssRule.media, function(media) {
27 | if (cssMediaQuery.match(media, SCREEN_CONFIG)) {
28 | cssRuleListFor(cssRule.cssRules || [], callback);
29 | }
30 | });
31 | }
32 |
33 | }
34 | }
35 |
36 |
37 | module.exports = function eachCssStyleRule(document, callback) {
38 | var window = document.defaultView;
39 | var styleSheetList = document.styleSheets;
40 |
41 | for (var i = 0; i < styleSheetList.length; i++) {
42 | var cssStyleSheet = styleSheetList[i];
43 | var cssRuleList = cssStyleSheet.cssRules || [];
44 | cssRuleListFor(cssRuleList, callback);
45 | }
46 | };
--------------------------------------------------------------------------------
/src/style/get-computed-style.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var cascade = require('./cascade');
4 | var inherit = require('./config/inherit.json');
5 |
6 | // TODO 计算值
7 | module.exports = function getComputedStyle(node, pseudo) {
8 |
9 | var cacheKey = '_computedStyle::' + pseudo;
10 |
11 | if (node[cacheKey]) {
12 | return node[cacheKey];
13 | }
14 |
15 | var currentStyle = cascade(node, pseudo);
16 | var parentNode = pseudo ? node : node.parentNode;
17 | var INHERIT = 'inherit';
18 | var DOCUMENT = '#document';
19 |
20 | inherit.forEach(function(key) {
21 | var currentValue = currentStyle[key];
22 |
23 | var parentStyle;
24 | var parentStyleValue;
25 | var styles = [];
26 |
27 | if (!currentValue || currentValue === INHERIT) {
28 | out: while (parentNode && parentNode.nodeName !== DOCUMENT) {
29 |
30 | parentStyle = cascade(parentNode);
31 | parentStyleValue = parentStyle[key];
32 |
33 | if (parentStyleValue && parentStyleValue !== INHERIT) {
34 | styles.push(currentStyle);
35 | setInheritValue(styles, key, parentStyleValue);
36 | break out;
37 | }
38 |
39 | styles.push(parentStyle);
40 | parentNode = parentNode.parentNode;
41 | }
42 | }
43 | });
44 |
45 |
46 | function setInheritValue(styles, key, value) {
47 | styles.forEach(function(style) {
48 | style[key] = value;
49 | });
50 | }
51 |
52 |
53 | node[cacheKey] = currentStyle;
54 |
55 | return currentStyle;
56 | };
--------------------------------------------------------------------------------
/src/style/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var cssom = require('cssom');
3 | //var CSSStyleDeclaration = require('cssstyle').CSSStyleDeclaration;
4 |
5 | var CSSOM = {
6 | CSSStyleDeclaration: cssom.CSSStyleDeclaration,
7 | CSSRule: cssom.CSSRule,
8 | CSSStyleRule: cssom.CSSStyleRule,
9 | MediaList: cssom.MediaList,
10 | CSSMediaRule: cssom.CSSMediaRule,
11 | CSSImportRule: cssom.CSSImportRule,
12 | CSSFontFaceRule: cssom.CSSFontFaceRule,
13 | StyleSheet: cssom.StyleSheet,
14 | CSSStyleSheet: cssom.CSSStyleSheet,
15 | CSSKeyframesRule: cssom.CSSKeyframesRule,
16 | CSSKeyframeRule: cssom.CSSKeyframeRule,
17 | //MatcherList: cssom.MatcherList,
18 | //CSSDocumentRule: cssom.CSSDocumentRule,
19 | //CSSValue: cssom.CSSValue,
20 | //CSSValueExpression: cssom.CSSValueExpression,
21 | parse: cssom.parse,
22 | clone: cssom.clone,
23 | };
24 |
25 | module.exports = CSSOM;
--------------------------------------------------------------------------------
/src/style/load-css-files.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var cssom = require('cssom');
4 | var url = require('url');
5 | var VError = require('verror');
6 | var Resource = require('../utils/resource');
7 | var decode = require('../utils/decode');
8 |
9 | function loadCssFiles(document, resource) {
10 |
11 | if (!(resource instanceof Resource)) {
12 | throw new Error('require `Resource`');
13 | }
14 |
15 | var styleSheets = document.styleSheets;
16 | var baseURI = document.baseURI;
17 | var silent = document._options.silent;
18 | var forEach = Array.prototype.forEach;
19 | var loadQueue = [];
20 |
21 | forEach.call(styleSheets, function(cssStyleSheet, index) {
22 | var ownerNode = cssStyleSheet.ownerNode;
23 | var nodeName = ownerNode.nodeName;
24 |
25 | if (nodeName === 'STYLE') {
26 | loadQueue.push(loadImportFile(baseURI, cssStyleSheet));
27 | } else if (nodeName === 'LINK') {
28 | var href = ownerNode.href;
29 | var file = decodeURIComponent(href);
30 | loadQueue.push(resource.get(file).then(function(data) {
31 | data = getContent(data);
32 |
33 | var cssStyleSheet = cssParse(data, file);
34 | cssStyleSheet.href = href;
35 |
36 | // TODO 在真正的浏览器中跨域,cssStyleSheet.cssRules 会等于 null
37 | styleSheets[index] = cssStyleSheet;
38 |
39 | return loadImportFile(href, cssStyleSheet);
40 | }, onerror));
41 | }
42 | });
43 |
44 |
45 | function loadImportFile(baseURI, cssStyleSheet) {
46 | var loadQueue = [];
47 | var forEach = Array.prototype.forEach;
48 | forEach.call(cssStyleSheet.cssRules, function(cssStyleRule) {
49 | if (cssStyleRule instanceof cssom.CSSImportRule) {
50 |
51 | var href = cssStyleRule.href;
52 | href = url.resolve(baseURI, href);
53 |
54 | var file = decodeURIComponent(href);
55 |
56 | loadQueue.push(resource.get(file).then(function(data) {
57 | data = getContent(data);
58 | var baseURI = href;
59 | var cssStyleSheet = cssParse(data, file);
60 | cssStyleSheet.parentStyleSheet = cssStyleSheet;
61 | cssStyleSheet.href = href;
62 | cssStyleRule.styleSheet = cssStyleSheet;
63 | return loadImportFile(baseURI, cssStyleSheet);
64 | }, onerror));
65 | }
66 | });
67 |
68 | return Promise.all(loadQueue);
69 | }
70 |
71 |
72 | function onerror(errors) {
73 | if (silent) {
74 | return Promise.resolve('');
75 | } else {
76 | return Promise.reject(errors);
77 | }
78 | }
79 |
80 |
81 | function cssParse(data, file) {
82 | try {
83 | return cssom.parse(data);
84 | } catch (errors) {
85 |
86 | if (!silent) {
87 | throw new VError(errors, 'parse "%s" failed"', file);
88 | }
89 |
90 | return cssom.parse('');
91 | }
92 | }
93 |
94 |
95 |
96 | return Promise.all(loadQueue);
97 | }
98 |
99 |
100 |
101 | function getContent(content) {
102 | // 去掉 @charset,因为它可能触发 cssom 库的 bug
103 | // 使用空格占位避免改动代码位置
104 | content = decode(content, '\\');
105 | return content.replace(/^(\@charset\b.+?;)(.*?)/i, function($0, $1, $2) {
106 | var placeholder = new Array($1.length + 1).join(' ');
107 | return placeholder + $2;
108 | });
109 | }
110 |
111 |
112 |
113 | module.exports = loadCssFiles;
--------------------------------------------------------------------------------
/src/text.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var CharacterData = require('./character-data');
4 | var Node = require('./node');
5 |
6 | function Text(ownerDocument, data) {
7 | CharacterData.call(this, ownerDocument, '#text', data, Node.TEXT_NODE);
8 | }
9 |
10 | Text.prototype = Object.create(CharacterData.prototype);
11 | Text.prototype.constructor = Text;
12 |
13 | module.exports = Text;
--------------------------------------------------------------------------------
/src/utils/decode.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 |
4 | /**
5 | * unicode 字符串解码
6 | * @see http://andyyou.github.io/javascript/2015/05/21/js-unicode-issue.html
7 | * @param {String}
8 | * @param {String} 编码开始标记,默认`\u`
9 | */
10 | function decode(string, tag) {
11 |
12 | tag = tag || '\\u';
13 |
14 | var newString = '';
15 | var open = false;
16 | var char, charCode;
17 | var index = -1;
18 | var length = string.length;
19 | while (++index < length) {
20 | char = string.charAt(index);
21 | charCode = char.charCodeAt(0);
22 | if (open) {
23 | if (char === tag) {
24 | newString += tag;
25 | } else {
26 | var hex = parseInt(string.substr(index, 4), 16);
27 | if (isNaN(hex)) {
28 | newString += char;
29 | } else {
30 | newString += String.fromCharCode(hex);
31 | index += 3;
32 | }
33 | }
34 | open = false;
35 | } else {
36 | if (char === tag) {
37 | open = true;
38 | } else {
39 | newString += char;
40 | }
41 | }
42 | }
43 |
44 | return newString;
45 |
46 | }
47 |
48 |
49 | module.exports = decode;
--------------------------------------------------------------------------------
/src/utils/resource.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var fs = require('fs');
4 | var http = require('http');
5 | var https = require('https');
6 | var url = require('url');
7 | var path = require('path');
8 | var zlib = require('zlib');
9 | var VError = require('verror');
10 |
11 | var BrowserAdapter = require('../adapters/browser-adapter');
12 |
13 |
14 |
15 | function Resource(adapter) {
16 | this.adapter = new BrowserAdapter(adapter);
17 | this.number = 0;
18 | this.cache = {};
19 | }
20 |
21 | Resource.prototype = {
22 |
23 | constructor: Resource,
24 |
25 | /**
26 | * 加载本地或远程资源
27 | * @param {String} 路径
28 | * @return {Promise}
29 | */
30 | get: function(file) {
31 | this.number++;
32 |
33 | var resource;
34 | var adapter = this.adapter;
35 | var resourceCache = adapter.resourceCache;
36 | var that = this;
37 |
38 | // ignore > map > normalize
39 | if (adapter.resourceIgnore(file)) {
40 | return Promise.resolve('');
41 | }
42 | file = adapter.resourceMap(file);
43 | file = this.normalize(file);
44 |
45 | if (this.number > this.adapter.resourceMaxNumber) {
46 | var errors = new Error('Exceed the maximum load limit on the number of resources');
47 | errors = new Resource.Error(errors, file);
48 | return Promise.reject(errors);
49 | }
50 |
51 | adapter.resourceBeforeLoad(file);
52 |
53 | if (resourceCache && this.cache[file]) {
54 | resource = this.cache[file];
55 | if (resource) {
56 | return resource;
57 | }
58 | }
59 |
60 |
61 | resource = new Promise(function(resolve, reject) {
62 |
63 | if (that.isRemoteFile(file)) {
64 | that.loadRemoteFile(file, onload);
65 | } else {
66 | that.loadLocalFile(file, onload);
67 | }
68 |
69 | function onload(errors, data) {
70 | if (errors) {
71 | errors = new Resource.Error(errors, file);
72 | reject(errors);
73 | } else {
74 | resolve(data.toString());
75 | }
76 | }
77 |
78 | });
79 |
80 |
81 | if (resourceCache) {
82 | this.cache[file] = resource;
83 | }
84 |
85 | return resource;
86 | },
87 |
88 | /**
89 | * 加载本地资源
90 | * @param {String} 路径
91 | * @param {String} 回调
92 | */
93 | loadLocalFile: function(file, callback) {
94 | fs.readFile(file, 'utf8', callback);
95 | },
96 |
97 |
98 | /**
99 | * 加载远程资源
100 | * @param {String} 路径
101 | * @param {String} 回调
102 | */
103 | loadRemoteFile: function(file, callback) {
104 | var number = arguments[2] || 0;
105 | var location = url.parse(file);
106 | var protocol = location.protocol === 'http:' ? http : https;
107 | var timeoutEventId;
108 | var that = this;
109 | var REDIRECTION_MAX = 3;
110 |
111 | var done = function(errors, data) {
112 | clearTimeout(timeoutEventId);
113 | callback(errors, data);
114 | }
115 |
116 |
117 | var request = protocol.request({
118 | method: 'GET',
119 | host: location.host,
120 | hostname: location.hostname,
121 | path: location.path,
122 | port: location.port,
123 | headers: this.adapter.resourceRequestHeaders(file)
124 | }, function(res) {
125 |
126 | var encoding = res.headers['content-encoding'];
127 | var type = res.headers['content-type'];
128 | var statusCode = res.statusCode;
129 | var errors = null;
130 |
131 | if (/3\d\d/.test(statusCode) && res.headers.location && number < REDIRECTION_MAX) {
132 | clearTimeout(timeoutEventId);
133 | var file = res.headers.location;
134 | number++;
135 | that.loadRemoteFile(file, callback, number);
136 | return;
137 | } else if (!/2\d\d/.test(statusCode)) {
138 | errors = new Error(res.statusMessage);
139 | }
140 |
141 | if (type.indexOf('text/') !== 0) {
142 | errors = new Error('only supports `text/*` resources');
143 | }
144 |
145 |
146 | if (errors) {
147 | done(errors);
148 | } else {
149 |
150 | var buffer = new Buffer([]);
151 |
152 |
153 | if (encoding === 'undefined') {
154 | res.setEncoding('utf-8');
155 | }
156 |
157 |
158 | res.on('data', function(chunk) {
159 | buffer = Buffer.concat([buffer, chunk]);
160 | });
161 |
162 | res.on('end', function() {
163 |
164 | if (encoding === 'gzip') {
165 |
166 | zlib.unzip(buffer, function(errors, buffer) {
167 | if (errors) {
168 | done(errors);
169 | } else {
170 | done(null, buffer);
171 | }
172 | });
173 |
174 | } else if (encoding == 'deflate') {
175 |
176 | zlib.inflate(buffer, function(errors, decoded) {
177 | if (errors) {
178 | done(errors);
179 | } else {
180 | done(null, decoded);
181 | }
182 | });
183 |
184 | } else {
185 | done(null, buffer);
186 | }
187 |
188 | });
189 |
190 | }
191 |
192 | })
193 | .on('error', done)
194 | .on('timeout', function(errors) {
195 | request.abort();
196 | done(errors);
197 | });
198 |
199 |
200 | timeoutEventId = setTimeout(function() {
201 | request.emit('timeout', new Error('have been timeout'));
202 | }, this.adapter.resourceTimeout);
203 |
204 |
205 | request.end();
206 |
207 | },
208 |
209 | /**
210 | * 标准化路径
211 | * @param {String} 路径
212 | * @return {String} 标准化路径
213 | */
214 | normalize: function(src) {
215 |
216 | if (!src) {
217 | return src;
218 | }
219 |
220 | if (this.isRemoteFile(src)) {
221 | // http://font/font?name=xxx#x
222 | // http://font/font?
223 | return src.replace(/#.*$/, '').replace(/\?$/, '');
224 | } else {
225 | // ../font/font.eot?#font-spider
226 | src = src.replace(/[#?].*$/g, '');
227 | return path.normalize(src);
228 | }
229 | },
230 |
231 |
232 | /**
233 | * 判断是否为远程 URL
234 | * @param {String} 路径
235 | * @return {Boolean}
236 | */
237 | isRemoteFile: function(src) {
238 | var RE_SERVER = /^https?\:\/\//i;
239 | return RE_SERVER.test(src);
240 | }
241 | };
242 |
243 |
244 | Resource.Error = ResourceError;
245 |
246 | function ResourceError(errors, file) {
247 | VError.call(this, new VError(errors, 'ENOENT, load "%s" failed', file));
248 |
249 | if (!this.path) {
250 | this.path = file;
251 | }
252 | }
253 | ResourceError.prototype = Object.create(VError.prototype);
254 | ResourceError.prototype.constructor = ResourceError;
255 |
256 |
257 | module.exports = Resource;
--------------------------------------------------------------------------------
/src/window.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var cssom = require('./style');
4 | var getComputedStyle = require('./style/get-computed-style');
5 |
6 |
7 | function Window(document) {
8 | this.document = document;
9 | this.onerror = null;
10 | this.onload = null;
11 | this.getComputedStyle = getComputedStyle;
12 |
13 | this.CSSStyleDeclaration = cssom.CSSStyleDeclaration;
14 | this.CSSRule = cssom.CSSRule;
15 | this.CSSStyleRule = cssom.CSSStyleRule;
16 | this.MediaList = cssom.MediaList;
17 | this.CSSMediaRule = cssom.CSSMediaRule;
18 | this.CSSImportRule = cssom.CSSImportRule;
19 | this.CSSFontFaceRule = cssom.CSSFontFaceRule;
20 | this.StyleSheet = cssom.StyleSheet;
21 | this.CSSStyleSheet = cssom.CSSStyleSheet;
22 | this.CSSKeyframesRule = cssom.CSSKeyframesRule;
23 | this.CSSKeyframeRule = cssom.CSSKeyframeRule;
24 |
25 | nwmatcherFix(this);
26 | }
27 |
28 |
29 | // 针对 nwmatcher 的一些特殊处理
30 | function nwmatcherFix(window) {
31 |
32 | var document = window.document;
33 |
34 | // :target 选择器支持
35 | window.location = document.location = {
36 | hash: ''
37 | };
38 |
39 | // 避免判断为低版本浏览器
40 | document.constructor.prototype.addEventListener = function() {
41 | throw new Error('not yet implemented');
42 | };
43 | }
44 |
45 | module.exports = Window;
--------------------------------------------------------------------------------
/test/document/html/blank.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/test/document/html/tag-base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
7 |
8 |
9 | link1
10 | link2
11 |
12 |
--------------------------------------------------------------------------------
/test/document/html/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | test
7 |
8 |
16 |
17 |
18 | h1
19 | hello
20 |
21 |
22 |
23 |
39 |
40 | link
41 | link
42 |
43 | text-content
44 | text-content-textarea
45 | <>"'&
46 |
47 |
--------------------------------------------------------------------------------
/test/document/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var browser = require('../../');
4 | var assert = require('assert');
5 | var fs = require('fs');
6 | var path = require('path');
7 |
8 |
9 | describe('Document', function() {
10 |
11 | var url = __dirname + '/html/blank.html';
12 | var html = fs.readFileSync(url, 'utf8');
13 |
14 | var window = browser.sync(html, {
15 | url: url,
16 | loadCssFile: false
17 | });
18 |
19 | var document = window.document;
20 |
21 |
22 | it('document', function() {
23 | assert.equal('object', typeof document);
24 | });
25 |
26 | it('document.documentElement', function() {
27 | assert.equal('HTML', document.documentElement.nodeName);
28 | });
29 |
30 | it('document.head', function() {
31 | assert.equal('HEAD', document.head.nodeName);
32 | });
33 |
34 | it('document.body', function() {
35 | assert.equal('BODY', document.body.nodeName);
36 | });
37 |
38 | it('document.nodeName', function() {
39 | assert.equal('#document', document.nodeName);
40 | });
41 |
42 | it('document.title', function() {
43 | assert.equal('Document', document.title);
44 | });
45 |
46 | it('document.styleSheets', function() {
47 | assert.equal('object', typeof document.styleSheets);
48 | assert.equal(0, document.styleSheets.length);
49 | });
50 |
51 | it('document.baseURI', function() {
52 | assert.equal(url, document.baseURI);
53 |
54 | var file = __dirname + '/html/tag-base.html';
55 | var html = fs.readFileSync(file, 'utf8');
56 |
57 | var window = browser.sync(html, {
58 | url: file,
59 | loadCssFile: false
60 | });
61 |
62 | assert.equal(file + '#', window.document.baseURI);
63 | });
64 |
65 | describe('#createElement()', function() {
66 |
67 | it('document.createElement("div")', function() {
68 | var div = document.createElement('div');
69 | assert.equal('DIV', div.nodeName);
70 | assert.equal(false, !!div.parentNode);
71 | });
72 |
73 | it('document.createElement("DIV")', function() {
74 | var div = document.createElement('DIV');
75 | assert.equal('DIV', div.nodeName);
76 | });
77 |
78 | it('document.createElement("a")', function() {
79 | var a = document.createElement('a');
80 | a.setAttribute('href', 'doc.html');
81 |
82 | assert.equal('A', a.nodeName);
83 | assert.equal(path.dirname(url) + '/doc.html', a.href);
84 | });
85 | });
86 |
87 | describe('#getElementById()', function() {
88 | it('document.getElementById("sDksf")', function() {
89 | assert.equal(null, document.getElementById("sDksf"));
90 | });
91 | });
92 |
93 | describe('#getElementsByTagName()', function() {
94 | it('document.getElementsByTagName("body")', function() {
95 | assert.equal(1, document.getElementsByTagName("body").length);
96 | });
97 | });
98 |
99 | describe('#querySelector()', function() {
100 | it('document.querySelector("body")', function() {
101 | assert.equal('BODY', document.querySelector("body").nodeName);
102 | });
103 | });
104 |
105 | describe('#querySelectorAll()', function() {
106 | it('document.querySelectorAll("*")', function() {
107 | assert.equal(5, document.querySelectorAll("*").length);
108 | });
109 |
110 | it('document.querySelectorAll("[charset*=\'UTF-8\']")', function() {
111 | assert.equal(1, document.querySelectorAll("[charset*='UTF-8']").length);
112 | });
113 | });
114 |
115 | });
116 |
117 | describe('Element', function() {
118 |
119 | var url = __dirname + '/html/test.html';
120 | var html = fs.readFileSync(url, 'utf8');
121 |
122 | var window = browser.sync(html, {
123 | url: url,
124 | loadCssFile: false
125 | });
126 |
127 | var document = window.document;
128 |
129 |
130 | it('element.id', function() {
131 | assert.equal('', document.head.id);
132 | assert.equal('', document.body.id);
133 | assert.equal('div', document.getElementById('div').id);
134 | assert.equal('DIV2', document.getElementById('DIV2').id);
135 | });
136 |
137 | it('element.nodeName', function() {
138 | assert.equal('HTML', document.documentElement.nodeName);
139 | assert.equal('DIV', document.getElementById('DIV2').nodeName);
140 | });
141 |
142 | it('element.tagName', function() {
143 | assert.equal('HTML', document.documentElement.tagName);
144 | assert.equal('DIV', document.getElementById('DIV2').tagName);
145 | });
146 |
147 | it('element.innerHTML', function() {
148 | assert.equal('.test::after{content:\'<>\'}', document.getElementById('style').innerHTML);
149 | assert.equal('h1 ', document.getElementById('h1').innerHTML);
150 | assert.equal('<code>...</code>', document.getElementById('textarea').innerHTML);
151 | assert.equal('console.log(\'<>\')', document.getElementById('script').innerHTML);
152 | });
153 |
154 | it('element.outerHTML', function() {
155 | assert.equal('', document.getElementById('style').outerHTML);
156 | assert.equal('h1 ', document.getElementById('h1').outerHTML);
157 | assert.equal('<code>...</code> ', document.getElementById('textarea').outerHTML);
158 | assert.equal('', document.getElementById('script').outerHTML);
159 | });
160 |
161 |
162 | describe('HTMLAnchorElement', function() {
163 | it('element.href', function() {
164 | assert.equal('htts://www.font-spider.org:80/api/test.html?id=1#title', document.getElementById('a').href);
165 | });
166 | });
167 |
168 |
169 | describe('HTMLInputElement', function() {
170 | it('element.type', function() {
171 | assert.equal('text', document.getElementById('input').type);
172 | assert.equal('checkbox', document.getElementById('checkbox').type);
173 | });
174 | it('element.disabled', function() {
175 | assert.equal(true, document.getElementById('input').disabled);
176 | assert.equal(true, document.getElementById('radio').disabled);
177 | assert.equal(false, document.getElementById('checkbox').disabled);
178 | });
179 | });
180 |
181 | });
182 |
183 |
184 | describe('Node', function() {
185 | var url = __dirname + '/html/test.html';
186 | var html = fs.readFileSync(url, 'utf8');
187 |
188 | var window = browser.sync(html, {
189 | url: url,
190 | loadCssFile: false
191 | });
192 |
193 | var document = window.document;
194 |
195 | it('node.textContent', function() {
196 | assert.equal('text-content', document.getElementById('text-content').textContent);
197 | });
198 |
199 | it('node.textContent: textarea', function() {
200 | assert.equal('text-content-textarea', document.getElementById('text-content-textarea').textContent);
201 | });
202 |
203 | it('node.textContent: Escape Sequence', function() {
204 | assert.equal('<>"\'&', document.getElementById('text-content-escape').textContent);
205 | });
206 |
207 | it('node.baseURI', function() {
208 | assert.equal('htts://www.font-spider.org:80/', document.documentElement.baseURI);
209 | });
210 |
211 | it('node.constructor.name', function() {
212 | assert.equal('HTMLUnknownElement', document.createElement('hue').constructor.name);
213 | });
214 |
215 |
216 | });
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('./utils');
4 | require('./document');
5 | require('./selector');
6 | require('./style');
7 |
--------------------------------------------------------------------------------
/test/selector/W3C-Selector-tests/W3C-Selector-tests.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | selectorTest
6 |
7 |
39 |
195 |
225 |
226 |
227 |
228 | Testrunner by John Resig , tests by John Resig , Disruptive Innovations , W3C CSS Working Group , jQuery JavaScript Library .
229 | Show only failing tests.
230 |
231 |
236 |
237 |
240 |
241 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
291 |
292 |
297 |
298 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
385 |
386 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
420 |
421 |
422 | the previous square should be green when the checkbox is checked and become red when you uncheck it
423 |
424 |
425 |
426 | the previous square should be green when the checkbox is NOT checked and become red when you check it
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
the three last squares should be green and become red when the pointer hovers over the white square
435 |
436 |
437 |
438 |
439 |
440 |
the last square should be green and become red when the pointer hovers over the FIRST white square
441 |
442 |
443 |
444 |
449 |
450 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
466 |
467 |
468 |
469 |
470 |
471 |
472 |
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
500 |
501 |
506 |
507 |
512 |
513 |
514 |
515 |
516 |
517 |
518 |
519 |
520 |
521 |
522 |
523 |
524 |
525 |
526 |
527 |
528 |
529 |
530 |
531 |
532 |
533 |
534 |
535 |
536 |
537 |
538 |
539 |
540 |
541 |
542 |
543 |
544 |
545 |
546 |
547 |
548 |
549 |
550 |
551 |
552 |
553 |
554 |
555 |
556 |
557 |
558 |
559 |
560 |
561 |
562 |
563 |
564 |
565 |
566 |
567 |
568 |
569 |
573 |
574 |
575 |
576 |
577 |
578 |
579 |
580 |
581 |
582 |
583 |
584 |
585 |
586 |
587 |
588 |
594 |
595 |
602 |
603 |
604 |
605 |
606 |
607 |
608 |
609 |
610 |
611 |
612 |
613 |
614 |
615 |
616 |
617 |
618 |
619 |
620 |
621 |
622 |
623 |
624 |
625 |
629 |
630 |
631 | the previous square should be green when the checkbox is checked and become red when you uncheck it
632 |
633 |
634 |
635 | the previous square should be green when the checkbox is NOT checked and become red when you check it
636 |
637 |
638 |
639 |
640 |
641 |
642 |
643 |
the three last squares should be green and become red when the pointer hovers over the white square
644 |
645 |
646 |
647 |
648 |
649 |
the last square should be green and become red when the pointer hovers over the FIRST white square
650 |
651 |
652 |
653 |
654 |
655 |
656 | Example circle01 - circle filled with red and stroked with blue
657 |
658 |
659 |
660 |
661 |
662 | Example circle01 - circle filled with red and stroked with blue
663 |
664 |
665 |
666 |
667 |
668 | Example circle01 - circle filled with red and stroked with blue
669 |
670 |
671 |
672 |
673 |
674 |
675 |
676 |
677 |
678 |
679 |
683 |
684 |
685 |
686 |
867 |
868 |
869 |
870 |
871 |
872 |
873 |
874 |
--------------------------------------------------------------------------------
/test/selector/W3C-Selector-tests/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var fs = require('fs');
3 | var browser = require('../../../');
4 | var Assert = require('assert');
5 | var NW = require('nwmatcher/src/nwmatcher-noqsa');
6 |
7 | describe('W3C-Selector-tests', function() {
8 |
9 | // this.timeout(20000);
10 |
11 | // it('tests', function() {
12 | var html = fs.readFileSync(__dirname + '/W3C-Selector-tests.html', 'utf8');
13 |
14 | var window = browser.sync(html, {
15 | url: __dirname + '/W3C-Selector-tests.html',
16 | loadCssFile: true
17 | });
18 | var document = window.document;
19 | /******************************************/
20 |
21 | var $ = NW({
22 | document: document
23 | }).select;
24 |
25 | if (window.location.hash.indexOf("target") == -1)
26 | window.location.hash = "#target";
27 |
28 | var root = document.getElementById("root");
29 | var root2 = document.getElementById("root2");
30 | var root3 = document.getElementById("root3");
31 | var results = [];
32 | //var tests = 0,
33 | // passed = 0;
34 | var cache = {};
35 |
36 | var cssElem = document.getElementById("test");
37 | var css = (cssElem.innerHTML || cssElem.textContent || cssElem.innerText).split("\n");
38 | for (var i = 0; i < css.length; i++) {
39 | css[i] = css[i].replace(/\/\*.*?\*\//g, "")
40 | .replace(/^\s*|\s*$/g, "").split(/\s*{/);
41 | }
42 |
43 | var ecssElem = document.getElementById("error");
44 | var ecss = (ecssElem.innerHTML || ecssElem.textContent || ecssElem.innerText).split("\n");
45 | for (var i = 0; i < ecss.length; i++) {
46 | ecss[i] = ecss[i].replace(/\/\*.*?\*\//g, "")
47 | .replace(/^\s*|\s*$/g, "").split(/\s*{/);
48 | }
49 |
50 | // interfaceCheck(root, "Element");
51 | // runTest(css, "Element", root, true);
52 | // check("Inside Element", root, true, false);
53 | // cacheCheck("Element", root);
54 | // check("Outside Element", root2, passed === 0 ? "autofail" : false, false);
55 | // runTest(ecss, "Syntax Error: Element", root, false);
56 | // jqTests("Element", root3, "querySelectorAll");
57 |
58 | // 已经删除了 cloneNode 方法
59 | // var root4 = root2.cloneNode(true);
60 | // interfaceCheck(root4, "Disconnected Element");
61 | // runTest(css, "Disconnected Element", root4, true);
62 | // check("Disconnected Element", root4, true, true);
63 | // cacheCheck("Disconnected Element", root4);
64 | // runTest(ecss, "Syntax Error: Disconnected Element", root4, false);
65 | // jqTests("Disconnected Element", root3.cloneNode(true), "querySelectorAll");
66 |
67 | // var fragment = document.createDocumentFragment();
68 | // fragment.appendChild(root2.cloneNode(true));
69 |
70 | // interfaceCheck(fragment, "Fragment");
71 | // runTest(css, "Fragment", fragment, true);
72 | // check("Fragment", fragment, true, true);
73 | // runTest(ecss, "Syntax Error: Fragment", fragment, false);
74 | // cacheCheck("Fragment", fragment);
75 |
76 | root.parentNode.removeChild(root);
77 |
78 | interfaceCheck(document, "Document");
79 | runTest(css, "Document", document, true);
80 | check("Document", document, true, false);
81 | runTest(ecss, "Syntax Error: Document", document, false);
82 | jqTests("Document", document, "querySelectorAll");
83 | cacheCheck("Document", document);
84 |
85 | //done();
86 |
87 | function interfaceCheck(obj, type) {
88 | var q = typeof obj.querySelector === "function" || typeof obj.querySelector === 'object';
89 | assert(q, type + " supports querySelector");
90 | var qa = typeof obj.querySelectorAll === "function" || typeof obj.querySelectorAll === 'object';
91 | assert(qa, type + " supports querySelectorAll");
92 | return q && qa;
93 | }
94 |
95 | // function done() {
96 | // var r = document.getElementById("results");
97 | // var li = document.createElement("li");
98 | // var b = document.createElement("b");
99 | // b.appendChild(document.createTextNode(((passed / tests) * 100).toFixed(1) + "%"));
100 | // li.appendChild(b);
101 | // li.appendChild(document.createTextNode(": " + passed + " passed, " + (tests - passed) + " failed"));
102 | // r.appendChild(li);
103 |
104 | // for (var i = 0; i < results.length; i++) {
105 | // var li = document.createElement("li");
106 | // li.className = (results[i][0] === "FAIL" ? "fail" : "pass");
107 | // var span = document.createElement("span");
108 | // span.style.color = (results[i][0] === "FAIL" ? "red" : "green");
109 | // span.appendChild(document.createTextNode(results[i][0]));
110 | // li.appendChild(span);
111 | // li.appendChild(document.createTextNode(" " + results[i][1]));
112 | // r.appendChild(li);
113 | // }
114 | // }
115 |
116 | // function done() {
117 | // console.log(((passed / tests) * 100).toFixed(1) + "%", ": " + passed + " passed, " + (tests - passed) + " failed")
118 |
119 | // for (var i = 0; i < results.length; i++) {
120 | // if (results[i][0] === 'FAIL') {
121 | // console.error(results[i][0], " ", results[i][1]);
122 | // }
123 | // }
124 | // }
125 |
126 |
127 | function cacheCheck(type, root) {
128 | try {
129 | var pre = $("div", root),
130 | preLength = pre.length;
131 |
132 | var div = document.createElement("div");
133 | (root.body || root).appendChild(div);
134 |
135 | var post = $("div", root),
136 | postLength = post.length;
137 |
138 | assert(pre.length == preLength, type + ": StaticNodeList");
139 | assert(post.length != pre.length, type + ": StaticNodeList");
140 | } catch (e) {
141 | assert(false, type + ": StaticNodeList");
142 | assert(false, type + ": StaticNodeList");
143 | }
144 |
145 | if (div)
146 | (root.body || root).removeChild(div);
147 | }
148 |
149 |
150 | function runTest(css, type, root, expect) {
151 | var pass = false;
152 | try {
153 | $("", root);
154 | } catch (e) {
155 | pass = /syntax|error/i.test(e.name);
156 | }
157 | assert(pass, type + ".querySelectorAll Empty String");
158 |
159 | pass = false;
160 | try {
161 | pass = $(null, root).length === 0;
162 | } catch (e) {
163 | pass = false;
164 | }
165 | assert(pass, type + ".querySelectorAll null");
166 |
167 | pass = false;
168 | try {
169 | pass = $(undefined, root).length === 0;
170 | } catch (e) {
171 | pass = false;
172 | }
173 | assert(pass, type + ".querySelectorAll undefined");
174 |
175 | pass = false;
176 | try {
177 | if ($)
178 | pass = $();
179 | } catch (e) {
180 | pass = true;
181 | }
182 | assert(pass, type + ".querySelectorAll no value");
183 |
184 | var pass = false;
185 | try {
186 | $("", root)[0];
187 | } catch (e) {
188 | pass = /syntax|error/i.test(e.name);
189 | }
190 | assert(pass, type + ".querySelector Empty String");
191 |
192 | pass = false;
193 | try {
194 | pass = !$(null, root)[0];
195 | } catch (e) {
196 | pass = false;
197 | }
198 | assert(pass, type + ".querySelector null");
199 |
200 | pass = false;
201 | try {
202 | pass = !$(undefined, root)[0];
203 | } catch (e) {
204 | pass = false;
205 | }
206 | assert(pass, type + ".querySelector undefined");
207 |
208 | pass = false;
209 | try {
210 | if ($)
211 | $()[0];
212 | } catch (e) {
213 | pass = true;
214 | }
215 | assert(pass, type + ".querySelector no value");
216 |
217 | for (var i = 0; i < css.length; i++) {
218 | var test = css[i];
219 | if (test.length == 2) {
220 | var query = test[0],
221 | color = test[1].match(/: ([^\s;]+)/)[1];
222 |
223 | try {
224 | var found = $(query, root);
225 |
226 | for (var f = 0; f < found.length; f++) {
227 | found[f].style.backgroundColor = color;
228 | }
229 |
230 | var pass = color != "red" || found.length === 0;
231 |
232 | assert(expect && pass, type + ".querySelectorAll: " + query);
233 | } catch (e) {
234 | var pass = !expect && /syntax|error/i.test(e.name);
235 | assert(pass, type + ".querySelectorAll: " + query);
236 | }
237 |
238 | if (expect) {
239 | var pass = false;
240 |
241 | try {
242 | var found2 = $(" \t\r\n " + query + " \t\r\n ", root);
243 | pass = found2.length == found.length;
244 | } catch (e) {}
245 |
246 | assert(pass, type + ".querySelectorAll Whitespace Trim: " + query);
247 | }
248 |
249 | try {
250 | var single = $(query, root)[0];
251 |
252 | var pass = found.length == 0 && single === null ||
253 | found.length && found[0] == single;
254 |
255 | assert(expect, type + ".querySelector: " + query);
256 | } catch (e) {
257 | var pass = !expect && /syntax|error/i.test(e.name);
258 | assert(pass, type + ".querySelector: " + query);
259 | }
260 | }
261 | }
262 |
263 | }
264 |
265 |
266 | function check(type, root, expect, fragment) {
267 | traverse(root, function(div) {
268 | if ((div.getAttribute("class") || "").toString().indexOf("unitTest") > -1 &&
269 | (!fragment || div.getAttribute("id") !== "nofragment")) {
270 | var bg;
271 |
272 | if (document.defaultView) {
273 | var view = document.defaultView.getComputedStyle(div, null);
274 | bg = view.getPropertyValue("background-color") || div.style.backgroundColor;
275 | } else if (div.currentStyle) {
276 | bg = div.currentStyle.backgroundColor || div.style.backgroundColor;
277 | }
278 |
279 | var pass = bg && bg.indexOf("(255, 0, 0") == -1 && bg.indexOf("#ff0000") == -1 && bg.indexOf("red") == -1;
280 | assert(pass === expect, type + ": " + (div.title || div.parentNode.title));
281 | }
282 | });
283 | }
284 |
285 | function traverse(elem, fn) {
286 | if (elem.nodeType === 1) {
287 | fn(elem);
288 |
289 | elem = elem.firstChild;
290 | while (elem) {
291 | traverse(elem, fn);
292 | elem = elem.nextSibling;
293 | }
294 | }
295 | }
296 |
297 | function assert(pass, title) {
298 | //results.push([(!pass ? "FAIL" : "PASS"), title]);
299 |
300 |
301 | it(title, function() {
302 | Assert(pass, true);
303 | });
304 |
305 | //tests++;
306 | //passed += (pass ? 1 : 0);
307 | }
308 |
309 | function jqTests(type, root, select) {
310 |
311 | function query(q) {
312 | return $(q, root);
313 | }
314 |
315 | function t(name, q, ids, restrict, expectError) {
316 | var pass = true;
317 |
318 | if (restrict === false && root != document)
319 | return;
320 |
321 | var prepend = "#root3 ";
322 | q = (restrict === false || restrict === ":root" ? "" : prepend) + q.replace(/,/g, ", " + prepend);
323 | var nq = q.replace(/>/g, ">").replace(/ 2 ? "," : "") + "'" + (results[i].id || results[i]) + "'";
358 | }
359 | }
360 |
361 | extra += "]";
362 | return extra;
363 | }
364 | }
365 |
366 | var all = query("*");
367 | assert(all && all.length > 30, type + ": Select all");
368 | var good = all && all.length;
369 | for (var i = 0; all && i < all.length; i++)
370 | if (all[i].nodeType != 1)
371 | good = false;
372 | assert(good, type + ": Select all elements, no comment nodes");
373 |
374 | if (root == document) {
375 | t(":root Selector", ":root", ["html"], false);
376 | } else {
377 | t(":root Selector", ":root", [], ":root");
378 |
379 | if (!root.parentNode) {
380 | t(":root All Selector", ":root *", [], ":root");
381 | }
382 | }
383 |
384 | if (root.parentNode || root == document) {
385 | var rootQuery = $(":root *", root);
386 | assert(rootQuery && rootQuery.length == $("*", root).length - (root == document ? 1 : 0), type + ": :root All Selector");
387 | }
388 |
389 | t("Element Selector", "p", ["firstp", "ap", "sndp", "en", "sap", "first"]);
390 | t("Element Selector", "body", ["body"], false);
391 | t("Element Selector", "html", ["html"], false);
392 | t("Parent Element", "div p", ["firstp", "ap", "sndp", "en", "sap", "first"]);
393 | var param = query("#object1 param");
394 | assert(param && param.length == 2, type + ": Object/param as context");
395 |
396 | var l = query("#length");
397 | assert(l && l.length, type + ': <input name="length"> cannot be found under IE');
398 | var lin = query("#lengthtest input");
399 | assert(lin && lin.length, type + ': <input name="length"> cannot be found under IE');
400 |
401 | t("Broken Selector", "[", undefined, undefined, true);
402 | t("Broken Selector", "(", undefined, undefined, true);
403 | t("Broken Selector", "{", undefined, undefined, true);
404 | t("Broken Selector", "<", undefined, undefined, true);
405 | t("Broken Selector", "()", undefined, undefined, true);
406 | t("Broken Selector", "<>", undefined, undefined, true);
407 | t("Broken Selector", "{}", undefined, undefined, true);
408 |
409 | t("ID Selector", "#body", ["body"], false);
410 | t("ID Selector w/ Element", "body#body", ["body"], false);
411 | t("ID Selector w/ Element", "ul#first", []);
412 | t("ID selector with existing ID descendant", "#firstp #simon1", ["simon1"]);
413 | t("ID selector with non-existant descendant", "#firstp #foobar", []);
414 |
415 | t("ID selector using UTF8", "#台北Táiběi", ["台北Táiběi"]);
416 | t("Multiple ID selectors using UTF8", "#台北Táiběi, #台北", ["台北Táiběi", "台北"]);
417 | t("Descendant ID selector using UTF8", "div #台北", ["台北"]);
418 | t("Child ID selector using UTF8", "form > #台北", ["台北"]);
419 |
420 | t("Escaped ID", "#foo\\:bar", ["foo:bar"]);
421 | t("Escaped ID", "#test\\.foo\\[5\\]bar", ["test.foo[5]bar"]);
422 | t("Descendant escaped ID", "div #foo\\:bar", ["foo:bar"]);
423 | t("Descendant escaped ID", "div #test\\.foo\\[5\\]bar", ["test.foo[5]bar"]);
424 | t("Child escaped ID", "form > #foo\\:bar", ["foo:bar"]);
425 | t("Child escaped ID", "form > #test\\.foo\\[5\\]bar", ["test.foo[5]bar"]);
426 |
427 | t("ID Selector, child ID present", "#form > #radio1", ["radio1"]); // bug #267
428 | t("ID Selector, not an ancestor ID", "#form #first", []);
429 | t("ID Selector, not a child ID", "#form > #option1a", []);
430 |
431 | t("All Children of ID", "#foo > *", ["sndp", "en", "sap"]);
432 | t("All Children of ID with no children", "#firstUL > *", []);
433 |
434 | t("ID selector with non-existant ancestor", "#asdfasdf #foobar", []); // bug #986
435 |
436 | //t( "body div#form", [], "ID selector within the context of another element" );
437 |
438 | t("Class Selector", ".blog", ["mark", "simon"]);
439 | t("Class Selector", ".blog.link", ["simon"]);
440 | t("Class Selector w/ Element", "a.blog", ["mark", "simon"]);
441 | t("Parent Class Selector", "p .blog", ["mark", "simon"]);
442 |
443 | t("Class selector using UTF8", ".台北Táiběi", ["utf8class1"]);
444 | t("Class selector using UTF8", ".台北", ["utf8class1", "utf8class2"]);
445 | t("Class selector using UTF8", ".台北Táiběi.台北", ["utf8class1"]);
446 | t("Class selector using UTF8", ".台北Táiběi, .台北", ["utf8class1", "utf8class2"]);
447 | t("Descendant class selector using UTF8", "div .台北Táiběi", ["utf8class1"]);
448 | t("Child class selector using UTF8", "form > .台北Táiběi", ["utf8class1"]);
449 |
450 | t("Escaped Class", ".foo\\:bar", ["foo:bar"]);
451 | t("Escaped Class", ".test\\.foo\\[5\\]bar", ["test.foo[5]bar"]);
452 | t("Descendant escaped Class", "div .foo\\:bar", ["foo:bar"]);
453 | t("Descendant escaped Class", "div .test\\.foo\\[5\\]bar", ["test.foo[5]bar"]);
454 | t("Child escaped Class", "form > .foo\\:bar", ["foo:bar"]);
455 | t("Child escaped Class", "form > .test\\.foo\\[5\\]bar", ["test.foo[5]bar"]);
456 |
457 | t("Comma Support", "a.blog, p", ['firstp', 'ap', 'mark', 'sndp', 'en', 'sap', 'simon', 'first']);
458 | t("Comma Support", "a.blog , p", ['firstp', 'ap', 'mark', 'sndp', 'en', 'sap', 'simon', 'first']);
459 | t("Comma Support", "a.blog ,p", ['firstp', 'ap', 'mark', 'sndp', 'en', 'sap', 'simon', 'first']);
460 | t("Comma Support", "a.blog,p", ['firstp', 'ap', 'mark', 'sndp', 'en', 'sap', 'simon', 'first']);
461 |
462 | t("Child", "p > a", ["simon1", "google", "groups", "mark", "yahoo", "simon"]);
463 | t("Child", "p> a", ["simon1", "google", "groups", "mark", "yahoo", "simon"]);
464 | t("Child", "p >a", ["simon1", "google", "groups", "mark", "yahoo", "simon"]);
465 | t("Child", "p>a", ["simon1", "google", "groups", "mark", "yahoo", "simon"]);
466 | t("Child w/ Class", "p > a.blog", ["mark", "simon"]);
467 | t("All Children", "code > *", ["anchor1", "anchor2"]);
468 | t("All Grandchildren", "p > * > *", ["anchor1", "anchor2"]);
469 | t("Adjacent", "a + a", ["groups"]);
470 | t("Adjacent", "a +a", ["groups"]);
471 | t("Adjacent", "a+ a", ["groups"]);
472 | t("Adjacent", "a+a", ["groups"]);
473 | t("Adjacent", "p + p", ["ap", "en", "sap"]);
474 | t("Comma, Child, and Adjacent", "a + a, code > a", ["groups", "anchor1", "anchor2"]);
475 |
476 | t("First Child", "p:first-child", ["firstp", "sndp"]);
477 | t("Nth Child", "p:nth-child(1)", ["firstp", "sndp"]);
478 |
479 | t("Last Child", "p:last-child", ["sap"]);
480 | t("Last Child", "a:last-child", ["simon1", "anchor1", "mark", "yahoo", "anchor2", "simon"]);
481 |
482 | t("Nth-child", "#main form#form > *:nth-child(2)", ["text2"]);
483 | t("Nth-child", "#main form#form > :nth-child(2)", ["text2"]);
484 |
485 | t("Nth-child", "#form #select1 option:nth-child(3)", ["option1c"]);
486 | t("Nth-child", "#form #select1 option:nth-child(0n+3)", ["option1c"]);
487 | t("Nth-child", "#form #select1 option:nth-child(1n+0)", ["option1a", "option1b", "option1c", "option1d"]);
488 | t("Nth-child", "#form #select1 option:nth-child(1n)", ["option1a", "option1b", "option1c", "option1d"]);
489 | t("Nth-child", "#form #select1 option:nth-child(n)", ["option1a", "option1b", "option1c", "option1d"]);
490 | t("Nth-child", "#form #select1 option:nth-child(even)", ["option1b", "option1d"]);
491 | t("Nth-child", "#form #select1 option:nth-child(odd)", ["option1a", "option1c"]);
492 | t("Nth-child", "#form #select1 option:nth-child(2n)", ["option1b", "option1d"]);
493 | t("Nth-child", "#form #select1 option:nth-child(2n+1)", ["option1a", "option1c"]);
494 | t("Nth-child", "#form #select1 option:nth-child(3n)", ["option1c"]);
495 | t("Nth-child", "#form #select1 option:nth-child(3n+1)", ["option1a", "option1d"]);
496 | t("Nth-child", "#form #select1 option:nth-child(3n+2)", ["option1b"]);
497 | t("Nth-child", "#form #select1 option:nth-child(3n+3)", ["option1c"]);
498 | t("Nth-child", "#form #select1 option:nth-child(3n-1)", ["option1b"]);
499 | t("Nth-child", "#form #select1 option:nth-child(3n-2)", ["option1a", "option1d"]);
500 | t("Nth-child", "#form #select1 option:nth-child(3n-3)", ["option1c"]);
501 | t("Nth-child", "#form #select1 option:nth-child(3n+0)", ["option1c"]);
502 | t("Nth-child", "#form #select1 option:nth-child(-n+3)", ["option1a", "option1b", "option1c"]);
503 |
504 | t("Attribute Exists", "a[title]", ["google"]);
505 | t("Attribute Exists", "*[title]", ["google"]);
506 | t("Attribute Exists", "[title]", ["google"]);
507 |
508 | t("Attribute Equals", "a[rel='bookmark']", ["simon1"]);
509 | t("Attribute Equals", 'a[rel="bookmark"]', ["simon1"]);
510 | t("Attribute Equals", "a[rel=bookmark]", ["simon1"]);
511 | t("Multiple Attribute Equals", "#form input[type='hidden'],#form input[type='radio']", ['radio1', 'radio2', 'hidden1']);
512 | t("Multiple Attribute Equals", "#form input[type=\"hidden\"],#form input[type='radio']", ['radio1', 'radio2', 'hidden1']);
513 | t("Multiple Attribute Equals", "#form input[type=hidden],#form input[type=radio]", ['radio1', 'radio2', 'hidden1']);
514 |
515 | t("Attribute selector using UTF8", "span[lang=中文]", ["台北"]);
516 |
517 | t("Attribute Begins With", "a[href ^= 'http://www']", ["google", "yahoo"]);
518 | t("Attribute Ends With", "a[href $= 'org/']", ["mark"]);
519 | t("Attribute Contains", "a[href *= 'google']", ["google", "groups"]);
520 |
521 | // t("Select options via [selected]", "#select1 option[selected]", ["option1a"] );
522 | t("Select options via [selected]", "#select1 option[selected]", []);
523 | t("Select options via [selected]", "#select2 option[selected]", ["option2d"]);
524 | t("Select options via [selected]", "#select3 option[selected]", ["option3b", "option3c"]);
525 |
526 | t("Grouped Form Elements", "input[name='foo[bar]']", ["hidden2"]);
527 |
528 | t(":not() Existing attribute", "#form select:not([multiple])", ["select1", "select2"]);
529 | t(":not() Equals attribute", "#form select:not([name=select1])", ["select2", "select3"]);
530 | t(":not() Equals quoted attribute", "#form select:not([name='select1'])", ["select2", "select3"]);
531 |
532 | t("First Child", "p:first-child", ["firstp", "sndp"]);
533 | t("Last Child", "p:last-child", ["sap"]);
534 | t("Only Child", "a:only-child", ["simon1", "anchor1", "yahoo", "anchor2"]);
535 | t("Empty", "ul:empty", ["firstUL"]);
536 | //t( "Enabled UI Element", "#form input:enabled", ["text1","radio1","radio2","check1","check2","hidden2","name"] );
537 | t("Disabled UI Element", "#form input:disabled", ["text2"]);
538 | t("Checked UI Element", "#form input:checked", ["radio2", "check1"]);
539 | t("Element Preceded By", "p ~ div", ["foo", "fx-queue", "fx-tests", "moretests"]);
540 | t("Not", "a.blog:not(.link)", ["mark"]);
541 |
542 | t("Attribute string containing a closing bracket", '[data-test="foo]bar"]', ["escapedSelectorClosingBracket"]);
543 | t("Attribute string containing a double quote", '[data-test="foo\\"bar"]', ["escapedSelectorDoubleQuote"]);
544 | t("Attribute string containing a single quote", "[data-test='foo\\'bar']", ["escapedSelectorSingleQuote"]);
545 | t("Attribute string containing a backslash", "[data-test='foo\\\\bar']", ["escapedSelectorBackslash"]);
546 | t("Attribute string containing a line feed", "[data-test='foo\\a bar']", ["escapedSelectorLF"]);
547 | t("Attribute string containing a carriage return ", "[data-test='foo\\00000d bar']", ["escapedSelectorCR"]);
548 | t("Attribute string containing a supplementary code point", "[data-test='foo\\24B62 bar']", ["escapedSupplementary"]);
549 | t("Attribute string containing a backslash and quotes", "[data-test='foo\\\\\\\"\\'\\62 ar']", ["escapedCombined"]);
550 |
551 | t("Attribute identifier containing a closing bracket", '[data-test=foo\\]bar]', ["escapedSelectorClosingBracket"]);
552 | t("Attribute identifier containing a double quote", '[data-test=foo\\"bar]', ["escapedSelectorDoubleQuote"]);
553 | t("Attribute identifier containing a single quote", "[data-test=foo\\'bar]", ["escapedSelectorSingleQuote"]);
554 | t("Attribute identifier containing a backslash", "[data-test=foo\\\\bar]", ["escapedSelectorBackslash"]);
555 | t("Attribute identifier containing a line feed", "[data-test=foo\\a bar]", ["escapedSelectorLF"]);
556 | t("Attribute identifier containing a carriage return ", "[data-test=foo\\00000d bar]", ["escapedSelectorCR"]);
557 | t("Attribute identifier containing a supplementary code point", "[data-test=foo\\24B62 bar]", ["escapedSupplementary"]);
558 | t("Attribute identifier containing a backslash and quotes", "[data-test=foo\\\\\\\"\\'\\62 ar]", ["escapedCombined"]);
559 | }
560 |
561 | /******************************************/
562 |
563 | });
--------------------------------------------------------------------------------
/test/selector/css3-compat/css3-compat.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | CSS 3 Selectors Test
9 |
10 |
144 |
145 |
146 |
147 |
156 |
157 |
160 |
161 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
211 |
212 |
217 |
218 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
305 |
306 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
345 |
346 |
347 |
348 |
--------------------------------------------------------------------------------
/test/selector/css3-compat/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var fs = require('fs');
3 | var browser = require('../../../');
4 | var eachCssStyleRule = require('../../../src/style/each-css-style-rule');
5 | var assert = require('assert');
6 |
7 | describe('css3-compat', function() {
8 | var url = __dirname + '/css3-compat.html';
9 | var html = fs.readFileSync(url, 'utf8');
10 | var window = browser.sync(html, {
11 | url: url,
12 | loadCssFile: false
13 | });
14 |
15 | var document = window.document;
16 | window.location.hash = '#target';
17 |
18 | eachCssStyleRule(document, function(cssRule) {
19 | var selectorText = cssRule.selectorText;
20 | it(selectorText, function() {
21 | var elements = document.querySelectorAll(selectorText);
22 | assert.equal(true, elements.length > 0);
23 | });
24 | });
25 | });
--------------------------------------------------------------------------------
/test/selector/index.js:
--------------------------------------------------------------------------------
1 | describe('Selector', function() {
2 | require('./css3-compat');
3 | require('./W3C-Selector-tests');
4 | });
--------------------------------------------------------------------------------
/test/style/get-computed-style.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var fs = require('fs');
3 | var browser = require('../../');
4 | var assert = require('assert');
5 |
6 |
7 |
8 | describe('getComputedStyle', function() {
9 | describe('getComputedStyle(node, pseudo)', function() {
10 |
11 | describe('index.html', function() {
12 | var url = __dirname + '/html/index.html';
13 | var html = fs.readFileSync(url, 'utf8');
14 | var window = browser.sync(html, {
15 | url: url,
16 | loadCssFile: false
17 | });
18 | it('#test::after', function() {
19 | assert.equal('"hello world\\\"" attr(id) "\\\""', window.document.styleSheets[0].cssRules[0].style.content);
20 | });
21 | });
22 |
23 | describe('pseudo1.html', function() {
24 | var url = __dirname + '/html/pseudo1.html';
25 | var html = fs.readFileSync(url, 'utf8');
26 |
27 | var window = browser.sync(html, {
28 | url: url,
29 | loadCssFile: false
30 | });
31 |
32 | var document = window.document;
33 | var getComputedStyle = window.getComputedStyle;
34 |
35 | it('#test1::after', function() {
36 | var node = document.querySelector('#test1');
37 | var style = getComputedStyle(node, '::after');
38 | assert.equal('\'hello world\'', style.content);
39 |
40 | style = getComputedStyle(node, ':after');
41 | assert.equal('\'hello world\'', style.content);
42 | });
43 |
44 | it('#test2:after', function() {
45 | var node = document.querySelector('#test2');
46 | var style = getComputedStyle(node, ':after');
47 | assert.equal('\'hello world\'', style.content);
48 |
49 | style = getComputedStyle(node, '::after');
50 | assert.equal('\'hello world\'', style.content);
51 | });
52 |
53 | it('#test3::AFTER', function() {
54 | var node = document.querySelector('#test3');
55 | var style = getComputedStyle(node, ':after');
56 | assert.equal('\'hello world\'', style.content);
57 |
58 | style = getComputedStyle(node, '::after');
59 | assert.equal('\'hello world\'', style.content);
60 |
61 | style = getComputedStyle(node, ':AFTER');
62 | assert.equal('\'hello world\'', style.content);
63 |
64 | style = getComputedStyle(node, '::AFTER');
65 | assert.equal('\'hello world\'', style.content);
66 |
67 | });
68 | });
69 |
70 | describe('pseudo2.html', function() {
71 | var url = __dirname + '/html/pseudo2.html';
72 | var html = fs.readFileSync(url, 'utf8');
73 |
74 | var window = browser.sync(html, {
75 | url: url,
76 | loadCssFile: false
77 | });
78 |
79 | var document = window.document;
80 | var getComputedStyle = window.getComputedStyle;
81 |
82 | it('*::after', function() {
83 | var node = document.querySelector('body');
84 | var style = getComputedStyle(node, '::after');
85 | assert.equal('\'hello world\'', style.content);
86 | });
87 |
88 | it('*:after', function() {
89 | var node = document.querySelector('body');
90 | var style = getComputedStyle(node, ':after');
91 | assert.equal('\'hello world\'', style.content);
92 | });
93 | });
94 |
95 | describe('pseudo3.html', function() {
96 | var url = __dirname + '/html/pseudo3.html';
97 | var html = fs.readFileSync(url, 'utf8');
98 |
99 | var window = browser.sync(html, {
100 | url: url,
101 | loadCssFile: false
102 | });
103 |
104 | var document = window.document;
105 | var getComputedStyle = window.getComputedStyle;
106 |
107 | it('*::after', function() {
108 | var node = document.querySelector('body');
109 | var style = getComputedStyle(node, '::after');
110 | assert.equal('\'hello world\'', style.content);
111 | });
112 |
113 | it('*:after', function() {
114 | var node = document.querySelector('body');
115 | var style = getComputedStyle(node, ':after');
116 | assert.equal('\'hello world\'', style.content);
117 | });
118 | });
119 |
120 | describe('pseudo4.html', function() {
121 | var url = __dirname + '/html/pseudo4.html';
122 | var html = fs.readFileSync(url, 'utf8');
123 |
124 | var window = browser.sync(html, {
125 | url: url,
126 | loadCssFile: false
127 | });
128 |
129 | var document = window.document;
130 | var getComputedStyle = window.getComputedStyle;
131 |
132 | it('::after', function() {
133 | var node = document.querySelector('body');
134 | var style = getComputedStyle(node, '::after');
135 | assert.equal('\'hello world\'', style.content);
136 | });
137 |
138 | it(':after', function() {
139 | var node = document.querySelector('body');
140 | var style = getComputedStyle(node, ':after');
141 |
142 | assert.equal('\'hello world\'', style.content);
143 | });
144 | });
145 |
146 | describe('pseudo5.html', function() {
147 | var url = __dirname + '/html/pseudo5.html';
148 | var html = fs.readFileSync(url, 'utf8');
149 |
150 | var window = browser.sync(html, {
151 | url: url,
152 | loadCssFile: false
153 | });
154 |
155 | var document = window.document;
156 | var getComputedStyle = window.getComputedStyle;
157 |
158 | it('::after', function() {
159 | var node = document.querySelector('body');
160 | var style = getComputedStyle(node, '::after');
161 | assert.equal('\'hello world\'', style.content);
162 | });
163 |
164 | it(':after', function() {
165 | var node = document.querySelector('body');
166 | var style = getComputedStyle(node, ':after');
167 | assert.equal('\'hello world\'', style.content);
168 | });
169 | });
170 |
171 | // 测试继承属性
172 | describe('pseudo6.html', function() {
173 | var url = __dirname + '/html/pseudo6.html';
174 | var html = fs.readFileSync(url, 'utf8');
175 |
176 | var window = browser.sync(html, {
177 | url: url,
178 | loadCssFile: false
179 | });
180 |
181 | var document = window.document;
182 | var getComputedStyle = window.getComputedStyle;
183 |
184 | it('#test1::after', function() {
185 | var node = document.querySelector('#test1');
186 | var style = getComputedStyle(node, '::after');
187 | assert.equal('aaa', style.fontFamily);
188 | });
189 |
190 | it('#test2::after', function() {
191 | var node = document.querySelector('#test2');
192 | var style = getComputedStyle(node, '::after');
193 | assert.equal('bbb', style.fontFamily);
194 | });
195 |
196 | it('#test3::after', function() {
197 | var node = document.querySelector('#test3');
198 | var style = getComputedStyle(node, '::after');
199 | assert.equal('ccc', style.fontFamily);
200 | });
201 | });
202 |
203 | });
204 | });
--------------------------------------------------------------------------------
/test/style/html/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test/style/html/pseudo1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/test/style/html/pseudo2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test/style/html/pseudo3.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test/style/html/pseudo4.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test/style/html/pseudo5.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test/style/html/pseudo6.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/test/style/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | describe('style', function() {
3 | require('./get-computed-style');
4 | });
--------------------------------------------------------------------------------
/test/utils/decode.js:
--------------------------------------------------------------------------------
1 | var decode = require('../../src/utils/decode');
2 | var assert = require('assert');
3 |
4 | describe('Decode', function() {
5 | it("#1", function() {
6 | assert.equal(decode('\\7cd6', '\\'), '糖');
7 | });
8 | it("#2", function() {
9 | assert.equal(decode('\\\\7cd6', '\\'), '\\7cd6');
10 | });
11 | it("#3", function() {
12 | assert.equal(decode('\\7cd6\\997c', '\\'), '糖饼');
13 | });
14 | it("#4", function() {
15 | assert.equal(decode('\\u7cd6\\u997c', '\\'), 'u7cd6u997c');
16 | });
17 | it("#5", function() {
18 | assert.equal(decode('--\\7cd6--\\997c--', '\\'), '--糖--饼--');
19 | });
20 | it("#6", function() {
21 | assert.equal(decode('\\7cd6\\\\997c', '\\'), '糖\\997c');
22 | });
23 | it("#7", function() {
24 | assert.equal(decode('\\\\7cd6\\997c', '\\'), '\\7cd6饼');
25 | });
26 | it("#8", function() {
27 | assert.equal(decode('\\7cd6\\997cba', '\\'), '糖饼ba'); // TODO: chrome 把 饼ba 解析为乱码
28 | });
29 | it("#9", function() {
30 | assert.equal(decode('dd\\7cd6\\997cba', '\\'), 'dd糖饼ba'); // TODO: chrome 把 饼ba 解析为乱码
31 | });
32 | it("#10", function() {
33 | assert.equal(decode('💩\\d834\\df06\\d83d\\dca9', '\\'), '💩𝌆💩');
34 | });
35 | });
--------------------------------------------------------------------------------
/test/utils/file/test.js:
--------------------------------------------------------------------------------
1 | console.log('hello world')
--------------------------------------------------------------------------------
/test/utils/file/test.txt:
--------------------------------------------------------------------------------
1 | hello world
--------------------------------------------------------------------------------
/test/utils/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | describe('utils', function() {
3 | require('./resource');
4 | require('./decode');
5 | });
--------------------------------------------------------------------------------
/test/utils/resource.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Resource = require('../../src/utils/resource');
4 | var assert = require('assert');
5 |
6 | describe('Resource', function() {
7 | var resource = new Resource({
8 | resourceTimeout: 500,
9 | resourceCache: function() {
10 | if (!this._resourceCache) {
11 | this._resourceCache = {};
12 | }
13 |
14 | return this._resourceCache;
15 | },
16 | resourceIgnore: function(file) {
17 | if (/\.js$/i.test(file)) {
18 | return true;
19 | } else {
20 | return false;
21 | }
22 | },
23 | resourceMap: function(file) {
24 | file = file.replace('https://file.google.com/', __dirname + '/file/')
25 | return file;
26 | },
27 | resourceBeforeLoad: function() {}
28 | });
29 |
30 | describe('#get', function() {
31 |
32 | it('get file', function() {
33 | return resource.get(__dirname + '/file/test.txt').then(function(data) {
34 | if (data !== 'hello world') {
35 | throw new Error('No data');
36 | }
37 | });
38 | });
39 |
40 | it('file not found', function(done) {
41 | resource.get(__dirname + '/file/404.html').then(function(data) {
42 | throw new Error('success');
43 | }, function(errors) {
44 | if (errors.path === __dirname + '/file/404.html') {
45 | done();
46 | } else {
47 | throw new Error('errors.path');
48 | }
49 | });
50 | });
51 |
52 | it('file not found[http]', function(done) {
53 | resource.get('http://facebook/404/404').then(function(data) {
54 | throw new Error('success');
55 | }, function(errors) {
56 | if (errors.path === 'http://facebook/404/404') {
57 | done();
58 | } else {
59 | throw new Error('errors.path');
60 | }
61 | });
62 | });
63 |
64 | it('adapter: resourceIgnore', function() {
65 | return resource.get(__dirname + '/file/test.js').then(function(data) {
66 | if (data !== '') {
67 | throw new Error('Adapter: resourceIgnore');
68 | }
69 | });
70 | });
71 |
72 | it('adapter: resourceMap', function() {
73 | return resource.get('https://file.google.com/test.txt').then(function(data) {
74 | if (data !== 'hello world') {
75 | throw new Error('No data');
76 | }
77 | });
78 | });
79 |
80 | });
81 |
82 |
83 |
84 | });
--------------------------------------------------------------------------------