├── .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 |
24 | 25 | 26 | 27 | 28 | 29 | 34 | 35 |
36 | 37 |
38 |
39 | 40 | link 41 | link 42 | 43 |
text-content
44 | 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('', 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 |

Selectors API Test Suite

228 |

Testrunner by John Resig, tests by John Resig, Disruptive Innovations, W3C CSS Working Group, jQuery JavaScript Library.

229 |

230 |
231 |
232 |

CSS 3 Selectors tests

233 |

(c) Disruptive Innovations 2008
234 | Last update: 2008-06-06

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 |
281 |
282 |
283 |
284 |
285 | 286 |
287 |
288 |
289 |
290 |
291 | 292 |
293 |
294 |
295 |
296 |
297 | 298 |
299 |
300 |

301 |

302 |
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 |
361 |

362 |
  363 |
364 | 365 |
366 |

367 |
368 |

369 |
370 |
371 | 372 |
373 |

374 |
375 |

376 |
377 |
378 | 379 |
380 |
381 |
382 |
383 |
384 |
385 | 386 |
387 |

388 |
389 |
390 |
391 |
392 |
393 | 394 |
395 |
396 |
397 |
398 |
399 |
 
400 |
401 | 402 |
403 |
404 |
405 |
406 |
407 |
408 | 409 |
410 |
411 |
412 |
413 |
414 |
415 | 416 |
417 | 418 | 419 |
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 |
445 |

CSS 3 Selectors tests

446 |

(c) Disruptive Innovations 2008
447 | Last update: 2008-06-06

448 |
449 | 450 |
451 |
452 |
453 | 454 |
455 |
456 |
457 |
458 |
459 |
460 | 461 |
462 |
463 |
464 |
465 |
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 |
496 |
497 |
498 |
499 |
500 | 501 |
502 |
503 |
504 |
505 |
506 | 507 |
508 |
509 |

510 |

511 |
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 |
570 |

571 |
  572 |
573 | 574 |
575 |

576 |
577 |

578 |
579 |
580 | 581 |
582 |

583 |
584 |

585 |
586 |
587 | 588 |
589 |
590 |
591 |
592 |
593 |
594 | 595 |
596 |

597 |
598 |
599 |
600 |
601 |
602 | 603 |
604 |
605 |
606 |
607 |
608 |
 
609 |
610 | 611 |
612 |
613 |
614 |
615 |
616 |
617 | 618 |
619 |
620 |
621 |
622 |
623 |
624 | 625 |
626 | 627 | 628 |
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 |

jQuery Test Suite

675 | 676 |

677 | 678 | 679 |
680 | 681 |
682 |
683 | 684 | 685 | 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 |
      148 |

      CSS 3 Selectors tests

      149 |

      Original CSS work by 150 | Daniel Glazman (c) Disruptive Innovations 2008

      151 |

      Testing code written by Diego Perini. It should help improve the 152 | consistency with querySelectorAll results and match browsers internal CSS selectors behavior. 153 | Selection method calls are wrapped in a try/catch block for all libs to avoid possible errors preventing the 154 | tests to complete. Moving the mouse over the red/lime blocks will show the tooltips with info on the tests.

      155 |
      156 | 157 |
      158 |
      159 |
      160 | 161 |
      162 |
      163 |
      164 | 165 |
      166 |
      167 |
      168 |
      169 |
      170 |
      171 | 172 |
      173 |
      174 |
      175 |
      176 |
      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 |
      207 |
      208 |
      209 |
      210 |
      211 | 212 |
      213 |
      214 |
      215 |
      216 |
      217 | 218 |
      219 |
      220 |

      221 |

      222 |
      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 |
      281 |

      282 |
        283 |
      284 | 285 |
      286 |

      287 |
      288 |

      289 |
      290 |
      291 | 292 |
      293 |

      294 |
      295 |

      296 |
      297 |
      298 | 299 |
      300 |
      301 |
      302 |
      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 | 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 | }); --------------------------------------------------------------------------------