added all files to project

This commit is contained in:
2022-03-10 10:36:59 +01:00
parent 09dd957b33
commit 46a936d7de
25351 changed files with 3883356 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) Facebook, Inc. and its affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+33
View File
@@ -0,0 +1,33 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/// <reference types="node" />
import type { Context } from 'vm';
import { JSDOM } from 'jsdom';
import type { EnvironmentContext, JestEnvironment } from '@jest/environment';
import { LegacyFakeTimers, ModernFakeTimers } from '@jest/fake-timers';
import type { Config, Global } from '@jest/types';
import { ModuleMocker } from 'jest-mock';
declare type Win = Window & Global.Global & {
Error: {
stackTraceLimit: number;
};
};
declare class JSDOMEnvironment implements JestEnvironment<number> {
dom: JSDOM | null;
fakeTimers: LegacyFakeTimers<number> | null;
fakeTimersModern: ModernFakeTimers | null;
global: Win;
errorEventListener: ((event: Event & {
error: Error;
}) => void) | null;
moduleMocker: ModuleMocker | null;
constructor(config: Config.ProjectConfig, options?: EnvironmentContext);
setup(): Promise<void>;
teardown(): Promise<void>;
getVmContext(): Context | null;
}
export = JSDOMEnvironment;
+186
View File
@@ -0,0 +1,186 @@
'use strict';
function _jsdom() {
const data = require('jsdom');
_jsdom = function () {
return data;
};
return data;
}
function _fakeTimers() {
const data = require('@jest/fake-timers');
_fakeTimers = function () {
return data;
};
return data;
}
function _jestMock() {
const data = require('jest-mock');
_jestMock = function () {
return data;
};
return data;
}
function _jestUtil() {
const data = require('jest-util');
_jestUtil = function () {
return data;
};
return data;
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
class JSDOMEnvironment {
constructor(config, options) {
_defineProperty(this, 'dom', void 0);
_defineProperty(this, 'fakeTimers', void 0);
_defineProperty(this, 'fakeTimersModern', void 0);
_defineProperty(this, 'global', void 0);
_defineProperty(this, 'errorEventListener', void 0);
_defineProperty(this, 'moduleMocker', void 0);
this.dom = new (_jsdom().JSDOM)('<!DOCTYPE html>', {
pretendToBeVisual: true,
resources:
typeof config.testEnvironmentOptions.userAgent === 'string'
? new (_jsdom().ResourceLoader)({
userAgent: config.testEnvironmentOptions.userAgent
})
: undefined,
runScripts: 'dangerously',
url: config.testURL,
virtualConsole: new (_jsdom().VirtualConsole)().sendTo(
(options === null || options === void 0 ? void 0 : options.console) ||
console
),
...config.testEnvironmentOptions
});
const global = (this.global = this.dom.window.document.defaultView);
if (!global) {
throw new Error('JSDOM did not return a Window object');
} // for "universal" code (code should use `globalThis`)
global.global = global; // Node's error-message stack size is limited at 10, but it's pretty useful
// to see more than that when a test fails.
this.global.Error.stackTraceLimit = 100;
(0, _jestUtil().installCommonGlobals)(global, config.globals); // TODO: remove this ASAP, but it currently causes tests to run really slow
global.Buffer = Buffer; // Report uncaught errors.
this.errorEventListener = event => {
if (userErrorListenerCount === 0 && event.error) {
process.emit('uncaughtException', event.error);
}
};
global.addEventListener('error', this.errorEventListener); // However, don't report them as uncaught if the user listens to 'error' event.
// In that case, we assume the might have custom error handling logic.
const originalAddListener = global.addEventListener;
const originalRemoveListener = global.removeEventListener;
let userErrorListenerCount = 0;
global.addEventListener = function (...args) {
if (args[0] === 'error') {
userErrorListenerCount++;
}
return originalAddListener.apply(this, args);
};
global.removeEventListener = function (...args) {
if (args[0] === 'error') {
userErrorListenerCount--;
}
return originalRemoveListener.apply(this, args);
};
this.moduleMocker = new (_jestMock().ModuleMocker)(global);
const timerConfig = {
idToRef: id => id,
refToId: ref => ref
};
this.fakeTimers = new (_fakeTimers().LegacyFakeTimers)({
config,
global: global,
moduleMocker: this.moduleMocker,
timerConfig
});
this.fakeTimersModern = new (_fakeTimers().ModernFakeTimers)({
config,
global: global
});
}
async setup() {}
async teardown() {
if (this.fakeTimers) {
this.fakeTimers.dispose();
}
if (this.fakeTimersModern) {
this.fakeTimersModern.dispose();
}
if (this.global) {
if (this.errorEventListener) {
this.global.removeEventListener('error', this.errorEventListener);
} // Dispose "document" to prevent "load" event from triggering.
Object.defineProperty(this.global, 'document', {
value: null
});
this.global.close();
}
this.errorEventListener = null; // @ts-expect-error
this.global = null;
this.dom = null;
this.fakeTimers = null;
this.fakeTimersModern = null;
}
getVmContext() {
if (this.dom) {
return this.dom.getInternalVMContext();
}
return null;
}
}
module.exports = JSDOMEnvironment;
+764
View File
@@ -0,0 +1,764 @@
## 8.5.0 (2021-09-06)
### Bug fixes
Improve context-dependent tokenization in a number of corner cases.
Fix location tracking after a 0x2028 or 0x2029 character in a string literal (which before did not increase the line number).
Fix an issue where arrow function bodies in for loop context would inappropriately consume `in` operators.
Fix wrong end locations stored on SequenceExpression nodes.
Implement restriction that `for`/`of` loop LHS can't start with `let`.
### New features
Add support for ES2022 class static blocks.
Allow multiple input files to be passed to the CLI tool.
## 8.4.1 (2021-06-24)
### Bug fixes
Fix a bug where `allowAwaitOutsideFunction` would allow `await` in class field initializers, and setting `ecmaVersion` to 13 or higher would allow top-level await in non-module sources.
## 8.4.0 (2021-06-11)
### New features
A new option, `allowSuperOutsideMethod`, can be used to suppress the error when `super` is used in the wrong context.
## 8.3.0 (2021-05-31)
### New features
Default `allowAwaitOutsideFunction` to true for ECMAScript 2022 an higher.
Add support for the `p` ([indices](https://github.com/tc39/proposal-regexp-match-indices)) regexp flag.
## 8.2.4 (2021-05-04)
### Bug fixes
Fix spec conformity in corner case 'for await (async of ...)'.
## 8.2.3 (2021-05-04)
### Bug fixes
Fix an issue where the library couldn't parse 'for (async of ...)'.
Fix a bug in UTF-16 decoding that would read characters incorrectly in some circumstances.
## 8.2.2 (2021-04-29)
### Bug fixes
Fix a bug where a class field initialized to an async arrow function wouldn't allow await inside it. Same issue existed for generator arrow functions with yield.
## 8.2.1 (2021-04-24)
### Bug fixes
Fix a regression introduced in 8.2.0 where static or async class methods with keyword names fail to parse.
## 8.2.0 (2021-04-24)
### New features
Add support for ES2022 class fields and private methods.
## 8.1.1 (2021-04-12)
### Various
Stop shipping source maps in the NPM package.
## 8.1.0 (2021-03-09)
### Bug fixes
Fix a spurious error in nested destructuring arrays.
### New features
Expose `allowAwaitOutsideFunction` in CLI interface.
Make `allowImportExportAnywhere` also apply to `import.meta`.
## 8.0.5 (2021-01-25)
### Bug fixes
Adjust package.json to work with Node 12.16.0 and 13.0-13.6.
## 8.0.4 (2020-10-05)
### Bug fixes
Make `await x ** y` an error, following the spec.
Fix potentially exponential regular expression.
## 8.0.3 (2020-10-02)
### Bug fixes
Fix a wasteful loop during `Parser` creation when setting `ecmaVersion` to `"latest"`.
## 8.0.2 (2020-09-30)
### Bug fixes
Make the TypeScript types reflect the current allowed values for `ecmaVersion`.
Fix another regexp/division tokenizer issue.
## 8.0.1 (2020-08-12)
### Bug fixes
Provide the correct value in the `version` export.
## 8.0.0 (2020-08-12)
### Bug fixes
Disallow expressions like `(a = b) = c`.
Make non-octal escape sequences a syntax error in strict mode.
### New features
The package can now be loaded directly as an ECMAScript module in node 13+.
Update to the set of Unicode properties from ES2021.
### Breaking changes
The `ecmaVersion` option is now required. For the moment, omitting it will still work with a warning, but that will change in a future release.
Some changes to method signatures that may be used by plugins.
## 7.4.0 (2020-08-03)
### New features
Add support for logical assignment operators.
Add support for numeric separators.
## 7.3.1 (2020-06-11)
### Bug fixes
Make the string in the `version` export match the actual library version.
## 7.3.0 (2020-06-11)
### Bug fixes
Fix a bug that caused parsing of object patterns with a property named `set` that had a default value to fail.
### New features
Add support for optional chaining (`?.`).
## 7.2.0 (2020-05-09)
### Bug fixes
Fix precedence issue in parsing of async arrow functions.
### New features
Add support for nullish coalescing.
Add support for `import.meta`.
Support `export * as ...` syntax.
Upgrade to Unicode 13.
## 6.4.1 (2020-03-09)
### Bug fixes
More carefully check for valid UTF16 surrogate pairs in regexp validator.
## 7.1.1 (2020-03-01)
### Bug fixes
Treat `\8` and `\9` as invalid escapes in template strings.
Allow unicode escapes in property names that are keywords.
Don't error on an exponential operator expression as argument to `await`.
More carefully check for valid UTF16 surrogate pairs in regexp validator.
## 7.1.0 (2019-09-24)
### Bug fixes
Disallow trailing object literal commas when ecmaVersion is less than 5.
### New features
Add a static `acorn` property to the `Parser` class that contains the entire module interface, to allow plugins to access the instance of the library that they are acting on.
## 7.0.0 (2019-08-13)
### Breaking changes
Changes the node format for dynamic imports to use the `ImportExpression` node type, as defined in [ESTree](https://github.com/estree/estree/blob/master/es2020.md#importexpression).
Makes 10 (ES2019) the default value for the `ecmaVersion` option.
## 6.3.0 (2019-08-12)
### New features
`sourceType: "module"` can now be used even when `ecmaVersion` is less than 6, to parse module-style code that otherwise conforms to an older standard.
## 6.2.1 (2019-07-21)
### Bug fixes
Fix bug causing Acorn to treat some characters as identifier characters that shouldn't be treated as such.
Fix issue where setting the `allowReserved` option to `"never"` allowed reserved words in some circumstances.
## 6.2.0 (2019-07-04)
### Bug fixes
Improve valid assignment checking in `for`/`in` and `for`/`of` loops.
Disallow binding `let` in patterns.
### New features
Support bigint syntax with `ecmaVersion` >= 11.
Support dynamic `import` syntax with `ecmaVersion` >= 11.
Upgrade to Unicode version 12.
## 6.1.1 (2019-02-27)
### Bug fixes
Fix bug that caused parsing default exports of with names to fail.
## 6.1.0 (2019-02-08)
### Bug fixes
Fix scope checking when redefining a `var` as a lexical binding.
### New features
Split up `parseSubscripts` to use an internal `parseSubscript` method to make it easier to extend with plugins.
## 6.0.7 (2019-02-04)
### Bug fixes
Check that exported bindings are defined.
Don't treat `\u180e` as a whitespace character.
Check for duplicate parameter names in methods.
Don't allow shorthand properties when they are generators or async methods.
Forbid binding `await` in async arrow function's parameter list.
## 6.0.6 (2019-01-30)
### Bug fixes
The content of class declarations and expressions is now always parsed in strict mode.
Don't allow `let` or `const` to bind the variable name `let`.
Treat class declarations as lexical.
Don't allow a generator function declaration as the sole body of an `if` or `else`.
Ignore `"use strict"` when after an empty statement.
Allow string line continuations with special line terminator characters.
Treat `for` bodies as part of the `for` scope when checking for conflicting bindings.
Fix bug with parsing `yield` in a `for` loop initializer.
Implement special cases around scope checking for functions.
## 6.0.5 (2019-01-02)
### Bug fixes
Fix TypeScript type for `Parser.extend` and add `allowAwaitOutsideFunction` to options type.
Don't treat `let` as a keyword when the next token is `{` on the next line.
Fix bug that broke checking for parentheses around an object pattern in a destructuring assignment when `preserveParens` was on.
## 6.0.4 (2018-11-05)
### Bug fixes
Further improvements to tokenizing regular expressions in corner cases.
## 6.0.3 (2018-11-04)
### Bug fixes
Fix bug in tokenizing an expression-less return followed by a function followed by a regular expression.
Remove stray symlink in the package tarball.
## 6.0.2 (2018-09-26)
### Bug fixes
Fix bug where default expressions could fail to parse inside an object destructuring assignment expression.
## 6.0.1 (2018-09-14)
### Bug fixes
Fix wrong value in `version` export.
## 6.0.0 (2018-09-14)
### Bug fixes
Better handle variable-redefinition checks for catch bindings and functions directly under if statements.
Forbid `new.target` in top-level arrow functions.
Fix issue with parsing a regexp after `yield` in some contexts.
### New features
The package now comes with TypeScript definitions.
### Breaking changes
The default value of the `ecmaVersion` option is now 9 (2018).
Plugins work differently, and will have to be rewritten to work with this version.
The loose parser and walker have been moved into separate packages (`acorn-loose` and `acorn-walk`).
## 5.7.3 (2018-09-10)
### Bug fixes
Fix failure to tokenize regexps after expressions like `x.of`.
Better error message for unterminated template literals.
## 5.7.2 (2018-08-24)
### Bug fixes
Properly handle `allowAwaitOutsideFunction` in for statements.
Treat function declarations at the top level of modules like let bindings.
Don't allow async function declarations as the only statement under a label.
## 5.7.0 (2018-06-15)
### New features
Upgraded to Unicode 11.
## 5.6.0 (2018-05-31)
### New features
Allow U+2028 and U+2029 in string when ECMAVersion >= 10.
Allow binding-less catch statements when ECMAVersion >= 10.
Add `allowAwaitOutsideFunction` option for parsing top-level `await`.
## 5.5.3 (2018-03-08)
### Bug fixes
A _second_ republish of the code in 5.5.1, this time with yarn, to hopefully get valid timestamps.
## 5.5.2 (2018-03-08)
### Bug fixes
A republish of the code in 5.5.1 in an attempt to solve an issue with the file timestamps in the npm package being 0.
## 5.5.1 (2018-03-06)
### Bug fixes
Fix misleading error message for octal escapes in template strings.
## 5.5.0 (2018-02-27)
### New features
The identifier character categorization is now based on Unicode version 10.
Acorn will now validate the content of regular expressions, including new ES9 features.
## 5.4.0 (2018-02-01)
### Bug fixes
Disallow duplicate or escaped flags on regular expressions.
Disallow octal escapes in strings in strict mode.
### New features
Add support for async iteration.
Add support for object spread and rest.
## 5.3.0 (2017-12-28)
### Bug fixes
Fix parsing of floating point literals with leading zeroes in loose mode.
Allow duplicate property names in object patterns.
Don't allow static class methods named `prototype`.
Disallow async functions directly under `if` or `else`.
Parse right-hand-side of `for`/`of` as an assignment expression.
Stricter parsing of `for`/`in`.
Don't allow unicode escapes in contextual keywords.
### New features
Parsing class members was factored into smaller methods to allow plugins to hook into it.
## 5.2.1 (2017-10-30)
### Bug fixes
Fix a token context corruption bug.
## 5.2.0 (2017-10-30)
### Bug fixes
Fix token context tracking for `class` and `function` in property-name position.
Make sure `%*` isn't parsed as a valid operator.
Allow shorthand properties `get` and `set` to be followed by default values.
Disallow `super` when not in callee or object position.
### New features
Support [`directive` property](https://github.com/estree/estree/compare/b3de58c9997504d6fba04b72f76e6dd1619ee4eb...1da8e603237144f44710360f8feb7a9977e905e0) on directive expression statements.
## 5.1.2 (2017-09-04)
### Bug fixes
Disable parsing of legacy HTML-style comments in modules.
Fix parsing of async methods whose names are keywords.
## 5.1.1 (2017-07-06)
### Bug fixes
Fix problem with disambiguating regexp and division after a class.
## 5.1.0 (2017-07-05)
### Bug fixes
Fix tokenizing of regexps in an object-desctructuring `for`/`of` loop and after `yield`.
Parse zero-prefixed numbers with non-octal digits as decimal.
Allow object/array patterns in rest parameters.
Don't error when `yield` is used as a property name.
Allow `async` as a shorthand object property.
### New features
Implement the [template literal revision proposal](https://github.com/tc39/proposal-template-literal-revision) for ES9.
## 5.0.3 (2017-04-01)
### Bug fixes
Fix spurious duplicate variable definition errors for named functions.
## 5.0.2 (2017-03-30)
### Bug fixes
A binary operator after a parenthesized arrow expression is no longer incorrectly treated as an error.
## 5.0.0 (2017-03-28)
### Bug fixes
Raise an error for duplicated lexical bindings.
Fix spurious error when an assignement expression occurred after a spread expression.
Accept regular expressions after `of` (in `for`/`of`), `yield` (in a generator), and braced arrow functions.
Allow labels in front or `var` declarations, even in strict mode.
### Breaking changes
Parse declarations following `export default` as declaration nodes, not expressions. This means that class and function declarations nodes can now have `null` as their `id`.
## 4.0.11 (2017-02-07)
### Bug fixes
Allow all forms of member expressions to be parenthesized as lvalue.
## 4.0.10 (2017-02-07)
### Bug fixes
Don't expect semicolons after default-exported functions or classes, even when they are expressions.
Check for use of `'use strict'` directives in non-simple parameter functions, even when already in strict mode.
## 4.0.9 (2017-02-06)
### Bug fixes
Fix incorrect error raised for parenthesized simple assignment targets, so that `(x) = 1` parses again.
## 4.0.8 (2017-02-03)
### Bug fixes
Solve spurious parenthesized pattern errors by temporarily erring on the side of accepting programs that our delayed errors don't handle correctly yet.
## 4.0.7 (2017-02-02)
### Bug fixes
Accept invalidly rejected code like `(x).y = 2` again.
Don't raise an error when a function _inside_ strict code has a non-simple parameter list.
## 4.0.6 (2017-02-02)
### Bug fixes
Fix exponential behavior (manifesting itself as a complete hang for even relatively small source files) introduced by the new 'use strict' check.
## 4.0.5 (2017-02-02)
### Bug fixes
Disallow parenthesized pattern expressions.
Allow keywords as export names.
Don't allow the `async` keyword to be parenthesized.
Properly raise an error when a keyword contains a character escape.
Allow `"use strict"` to appear after other string literal expressions.
Disallow labeled declarations.
## 4.0.4 (2016-12-19)
### Bug fixes
Fix crash when `export` was followed by a keyword that can't be
exported.
## 4.0.3 (2016-08-16)
### Bug fixes
Allow regular function declarations inside single-statement `if` branches in loose mode. Forbid them entirely in strict mode.
Properly parse properties named `async` in ES2017 mode.
Fix bug where reserved words were broken in ES2017 mode.
## 4.0.2 (2016-08-11)
### Bug fixes
Don't ignore period or 'e' characters after octal numbers.
Fix broken parsing for call expressions in default parameter values of arrow functions.
## 4.0.1 (2016-08-08)
### Bug fixes
Fix false positives in duplicated export name errors.
## 4.0.0 (2016-08-07)
### Breaking changes
The default `ecmaVersion` option value is now 7.
A number of internal method signatures changed, so plugins might need to be updated.
### Bug fixes
The parser now raises errors on duplicated export names.
`arguments` and `eval` can now be used in shorthand properties.
Duplicate parameter names in non-simple argument lists now always produce an error.
### New features
The `ecmaVersion` option now also accepts year-style version numbers
(2015, etc).
Support for `async`/`await` syntax when `ecmaVersion` is >= 8.
Support for trailing commas in call expressions when `ecmaVersion` is >= 8.
## 3.3.0 (2016-07-25)
### Bug fixes
Fix bug in tokenizing of regexp operator after a function declaration.
Fix parser crash when parsing an array pattern with a hole.
### New features
Implement check against complex argument lists in functions that enable strict mode in ES7.
## 3.2.0 (2016-06-07)
### Bug fixes
Improve handling of lack of unicode regexp support in host
environment.
Properly reject shorthand properties whose name is a keyword.
### New features
Visitors created with `visit.make` now have their base as _prototype_, rather than copying properties into a fresh object.
## 3.1.0 (2016-04-18)
### Bug fixes
Properly tokenize the division operator directly after a function expression.
Allow trailing comma in destructuring arrays.
## 3.0.4 (2016-02-25)
### Fixes
Allow update expressions as left-hand-side of the ES7 exponential operator.
## 3.0.2 (2016-02-10)
### Fixes
Fix bug that accidentally made `undefined` a reserved word when parsing ES7.
## 3.0.0 (2016-02-10)
### Breaking changes
The default value of the `ecmaVersion` option is now 6 (used to be 5).
Support for comprehension syntax (which was dropped from the draft spec) has been removed.
### Fixes
`let` and `yield` are now “contextual keywords”, meaning you can mostly use them as identifiers in ES5 non-strict code.
A parenthesized class or function expression after `export default` is now parsed correctly.
### New features
When `ecmaVersion` is set to 7, Acorn will parse the exponentiation operator (`**`).
The identifier character ranges are now based on Unicode 8.0.0.
Plugins can now override the `raiseRecoverable` method to override the way non-critical errors are handled.
## 2.7.0 (2016-01-04)
### Fixes
Stop allowing rest parameters in setters.
Disallow `y` rexexp flag in ES5.
Disallow `\00` and `\000` escapes in strict mode.
Raise an error when an import name is a reserved word.
## 2.6.2 (2015-11-10)
### Fixes
Don't crash when no options object is passed.
## 2.6.0 (2015-11-09)
### Fixes
Add `await` as a reserved word in module sources.
Disallow `yield` in a parameter default value for a generator.
Forbid using a comma after a rest pattern in an array destructuring.
### New features
Support parsing stdin in command-line tool.
## 2.5.0 (2015-10-27)
### Fixes
Fix tokenizer support in the command-line tool.
Stop allowing `new.target` outside of functions.
Remove legacy `guard` and `guardedHandler` properties from try nodes.
Stop allowing multiple `__proto__` properties on an object literal in strict mode.
Don't allow rest parameters to be non-identifier patterns.
Check for duplicate paramter names in arrow functions.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (C) 2012-2020 by various contributors (see AUTHORS)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+280
View File
@@ -0,0 +1,280 @@
# Acorn
A tiny, fast JavaScript parser written in JavaScript.
## Community
Acorn is open source software released under an
[MIT license](https://github.com/acornjs/acorn/blob/master/acorn/LICENSE).
You are welcome to
[report bugs](https://github.com/acornjs/acorn/issues) or create pull
requests on [github](https://github.com/acornjs/acorn). For questions
and discussion, please use the
[Tern discussion forum](https://discuss.ternjs.net).
## Installation
The easiest way to install acorn is from [`npm`](https://www.npmjs.com/):
```sh
npm install acorn
```
Alternately, you can download the source and build acorn yourself:
```sh
git clone https://github.com/acornjs/acorn.git
cd acorn
npm install
```
## Interface
**parse**`(input, options)` is the main interface to the library. The
`input` parameter is a string, `options` must be an object setting
some of the options listed below. The return value will be an abstract
syntax tree object as specified by the [ESTree
spec](https://github.com/estree/estree).
```javascript
let acorn = require("acorn");
console.log(acorn.parse("1 + 1", {ecmaVersion: 2020}));
```
When encountering a syntax error, the parser will raise a
`SyntaxError` object with a meaningful message. The error object will
have a `pos` property that indicates the string offset at which the
error occurred, and a `loc` object that contains a `{line, column}`
object referring to that same position.
Options are provided by in a second argument, which should be an
object containing any of these fields (only `ecmaVersion` is
required):
- **ecmaVersion**: Indicates the ECMAScript version to parse. Must be
either 3, 5, 6 (or 2015), 7 (2016), 8 (2017), 9 (2018), 10 (2019),
11 (2020), 12 (2021), 13 (2022, partial support)
or `"latest"` (the latest the library supports). This influences
support for strict mode, the set of reserved words, and support
for new syntax features.
**NOTE**: Only 'stage 4' (finalized) ECMAScript features are being
implemented by Acorn. Other proposed new features must be
implemented through plugins.
- **sourceType**: Indicate the mode the code should be parsed in. Can be
either `"script"` or `"module"`. This influences global strict mode
and parsing of `import` and `export` declarations.
**NOTE**: If set to `"module"`, then static `import` / `export` syntax
will be valid, even if `ecmaVersion` is less than 6.
- **onInsertedSemicolon**: If given a callback, that callback will be
called whenever a missing semicolon is inserted by the parser. The
callback will be given the character offset of the point where the
semicolon is inserted as argument, and if `locations` is on, also a
`{line, column}` object representing this position.
- **onTrailingComma**: Like `onInsertedSemicolon`, but for trailing
commas.
- **allowReserved**: If `false`, using a reserved word will generate
an error. Defaults to `true` for `ecmaVersion` 3, `false` for higher
versions. When given the value `"never"`, reserved words and
keywords can also not be used as property names (as in Internet
Explorer's old parser).
- **allowReturnOutsideFunction**: By default, a return statement at
the top level raises an error. Set this to `true` to accept such
code.
- **allowImportExportEverywhere**: By default, `import` and `export`
declarations can only appear at a program's top level. Setting this
option to `true` allows them anywhere where a statement is allowed,
and also allows `import.meta` expressions to appear in scripts
(when `sourceType` is not `"module"`).
- **allowAwaitOutsideFunction**: If `false`, `await` expressions can
only appear inside `async` functions. Defaults to `true` for
`ecmaVersion` 2022 and later, `false` for lower versions. Setting this option to
`true` allows to have top-level `await` expressions. They are
still not allowed in non-`async` functions, though.
- **allowSuperOutsideMethod**: By default, `super` outside a method
raises an error. Set this to `true` to accept such code.
- **allowHashBang**: When this is enabled (off by default), if the
code starts with the characters `#!` (as in a shellscript), the
first line will be treated as a comment.
- **locations**: When `true`, each node has a `loc` object attached
with `start` and `end` subobjects, each of which contains the
one-based line and zero-based column numbers in `{line, column}`
form. Default is `false`.
- **onToken**: If a function is passed for this option, each found
token will be passed in same format as tokens returned from
`tokenizer().getToken()`.
If array is passed, each found token is pushed to it.
Note that you are not allowed to call the parser from the
callback—that will corrupt its internal state.
- **onComment**: If a function is passed for this option, whenever a
comment is encountered the function will be called with the
following parameters:
- `block`: `true` if the comment is a block comment, false if it
is a line comment.
- `text`: The content of the comment.
- `start`: Character offset of the start of the comment.
- `end`: Character offset of the end of the comment.
When the `locations` options is on, the `{line, column}` locations
of the comment’s start and end are passed as two additional
parameters.
If array is passed for this option, each found comment is pushed
to it as object in Esprima format:
```javascript
{
"type": "Line" | "Block",
"value": "comment text",
"start": Number,
"end": Number,
// If `locations` option is on:
"loc": {
"start": {line: Number, column: Number}
"end": {line: Number, column: Number}
},
// If `ranges` option is on:
"range": [Number, Number]
}
```
Note that you are not allowed to call the parser from the
callback—that will corrupt its internal state.
- **ranges**: Nodes have their start and end characters offsets
recorded in `start` and `end` properties (directly on the node,
rather than the `loc` object, which holds line/column data. To also
add a
[semi-standardized](https://bugzilla.mozilla.org/show_bug.cgi?id=745678)
`range` property holding a `[start, end]` array with the same
numbers, set the `ranges` option to `true`.
- **program**: It is possible to parse multiple files into a single
AST by passing the tree produced by parsing the first file as the
`program` option in subsequent parses. This will add the toplevel
forms of the parsed file to the "Program" (top) node of an existing
parse tree.
- **sourceFile**: When the `locations` option is `true`, you can pass
this option to add a `source` attribute in every node’s `loc`
object. Note that the contents of this option are not examined or
processed in any way; you are free to use whatever format you
choose.
- **directSourceFile**: Like `sourceFile`, but a `sourceFile` property
will be added (regardless of the `location` option) directly to the
nodes, rather than the `loc` object.
- **preserveParens**: If this option is `true`, parenthesized expressions
are represented by (non-standard) `ParenthesizedExpression` nodes
that have a single `expression` property containing the expression
inside parentheses.
**parseExpressionAt**`(input, offset, options)` will parse a single
expression in a string, and return its AST. It will not complain if
there is more of the string left after the expression.
**tokenizer**`(input, options)` returns an object with a `getToken`
method that can be called repeatedly to get the next token, a `{start,
end, type, value}` object (with added `loc` property when the
`locations` option is enabled and `range` property when the `ranges`
option is enabled). When the token's type is `tokTypes.eof`, you
should stop calling the method, since it will keep returning that same
token forever.
In ES6 environment, returned result can be used as any other
protocol-compliant iterable:
```javascript
for (let token of acorn.tokenizer(str)) {
// iterate over the tokens
}
// transform code to array of tokens:
var tokens = [...acorn.tokenizer(str)];
```
**tokTypes** holds an object mapping names to the token type objects
that end up in the `type` properties of tokens.
**getLineInfo**`(input, offset)` can be used to get a `{line,
column}` object for a given program string and offset.
### The `Parser` class
Instances of the **`Parser`** class contain all the state and logic
that drives a parse. It has static methods `parse`,
`parseExpressionAt`, and `tokenizer` that match the top-level
functions by the same name.
When extending the parser with plugins, you need to call these methods
on the extended version of the class. To extend a parser with plugins,
you can use its static `extend` method.
```javascript
var acorn = require("acorn");
var jsx = require("acorn-jsx");
var JSXParser = acorn.Parser.extend(jsx());
JSXParser.parse("foo(<bar/>)", {ecmaVersion: 2020});
```
The `extend` method takes any number of plugin values, and returns a
new `Parser` class that includes the extra parser logic provided by
the plugins.
## Command line interface
The `bin/acorn` utility can be used to parse a file from the command
line. It accepts as arguments its input file and the following
options:
- `--ecma3|--ecma5|--ecma6|--ecma7|--ecma8|--ecma9|--ecma10`: Sets the ECMAScript version
to parse. Default is version 9.
- `--module`: Sets the parsing mode to `"module"`. Is set to `"script"` otherwise.
- `--locations`: Attaches a "loc" object to each node with "start" and
"end" subobjects, each of which contains the one-based line and
zero-based column numbers in `{line, column}` form.
- `--allow-hash-bang`: If the code starts with the characters #! (as
in a shellscript), the first line will be treated as a comment.
- `--allow-await-outside-function`: Allows top-level `await` expressions.
See the `allowAwaitOutsideFunction` option for more information.
- `--compact`: No whitespace is used in the AST output.
- `--silent`: Do not output the AST, just return the exit status.
- `--help`: Print the usage information and quit.
The utility spits out the syntax tree as JSON data.
## Existing plugins
- [`acorn-jsx`](https://github.com/RReverser/acorn-jsx): Parse [Facebook JSX syntax extensions](https://github.com/facebook/jsx)
Plugins for ECMAScript proposals:
- [`acorn-stage3`](https://github.com/acornjs/acorn-stage3): Parse most stage 3 proposals, bundling:
- [`acorn-class-fields`](https://github.com/acornjs/acorn-class-fields): Parse [class fields proposal](https://github.com/tc39/proposal-class-fields)
- [`acorn-import-meta`](https://github.com/acornjs/acorn-import-meta): Parse [import.meta proposal](https://github.com/tc39/proposal-import-meta)
- [`acorn-private-methods`](https://github.com/acornjs/acorn-private-methods): parse [private methods, getters and setters proposal](https://github.com/tc39/proposal-private-methods)n
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env node
'use strict';
require('../dist/bin.js');
+214
View File
@@ -0,0 +1,214 @@
export as namespace acorn
export = acorn
declare namespace acorn {
function parse(input: string, options: Options): Node
function parseExpressionAt(input: string, pos: number, options: Options): Node
function tokenizer(input: string, options: Options): {
getToken(): Token
[Symbol.iterator](): Iterator<Token>
}
interface Options {
ecmaVersion: 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 'latest'
sourceType?: 'script' | 'module'
onInsertedSemicolon?: (lastTokEnd: number, lastTokEndLoc?: Position) => void
onTrailingComma?: (lastTokEnd: number, lastTokEndLoc?: Position) => void
allowReserved?: boolean | 'never'
allowReturnOutsideFunction?: boolean
allowImportExportEverywhere?: boolean
allowAwaitOutsideFunction?: boolean
allowSuperOutsideMethod?: boolean
allowHashBang?: boolean
locations?: boolean
onToken?: ((token: Token) => any) | Token[]
onComment?: ((
isBlock: boolean, text: string, start: number, end: number, startLoc?: Position,
endLoc?: Position
) => void) | Comment[]
ranges?: boolean
program?: Node
sourceFile?: string
directSourceFile?: string
preserveParens?: boolean
}
class Parser {
constructor(options: Options, input: string, startPos?: number)
parse(this: Parser): Node
static parse(this: typeof Parser, input: string, options: Options): Node
static parseExpressionAt(this: typeof Parser, input: string, pos: number, options: Options): Node
static tokenizer(this: typeof Parser, input: string, options: Options): {
getToken(): Token
[Symbol.iterator](): Iterator<Token>
}
static extend(this: typeof Parser, ...plugins: ((BaseParser: typeof Parser) => typeof Parser)[]): typeof Parser
}
interface Position { line: number; column: number; offset: number }
const defaultOptions: Options
function getLineInfo(input: string, offset: number): Position
class SourceLocation {
start: Position
end: Position
source?: string | null
constructor(p: Parser, start: Position, end: Position)
}
class Node {
type: string
start: number
end: number
loc?: SourceLocation
sourceFile?: string
range?: [number, number]
constructor(parser: Parser, pos: number, loc?: SourceLocation)
}
class TokenType {
label: string
keyword: string
beforeExpr: boolean
startsExpr: boolean
isLoop: boolean
isAssign: boolean
prefix: boolean
postfix: boolean
binop: number
updateContext?: (prevType: TokenType) => void
constructor(label: string, conf?: any)
}
const tokTypes: {
num: TokenType
regexp: TokenType
string: TokenType
name: TokenType
privateId: TokenType
eof: TokenType
bracketL: TokenType
bracketR: TokenType
braceL: TokenType
braceR: TokenType
parenL: TokenType
parenR: TokenType
comma: TokenType
semi: TokenType
colon: TokenType
dot: TokenType
question: TokenType
arrow: TokenType
template: TokenType
ellipsis: TokenType
backQuote: TokenType
dollarBraceL: TokenType
eq: TokenType
assign: TokenType
incDec: TokenType
prefix: TokenType
logicalOR: TokenType
logicalAND: TokenType
bitwiseOR: TokenType
bitwiseXOR: TokenType
bitwiseAND: TokenType
equality: TokenType
relational: TokenType
bitShift: TokenType
plusMin: TokenType
modulo: TokenType
star: TokenType
slash: TokenType
starstar: TokenType
_break: TokenType
_case: TokenType
_catch: TokenType
_continue: TokenType
_debugger: TokenType
_default: TokenType
_do: TokenType
_else: TokenType
_finally: TokenType
_for: TokenType
_function: TokenType
_if: TokenType
_return: TokenType
_switch: TokenType
_throw: TokenType
_try: TokenType
_var: TokenType
_const: TokenType
_while: TokenType
_with: TokenType
_new: TokenType
_this: TokenType
_super: TokenType
_class: TokenType
_extends: TokenType
_export: TokenType
_import: TokenType
_null: TokenType
_true: TokenType
_false: TokenType
_in: TokenType
_instanceof: TokenType
_typeof: TokenType
_void: TokenType
_delete: TokenType
}
class TokContext {
constructor(token: string, isExpr: boolean, preserveSpace: boolean, override?: (p: Parser) => void)
}
const tokContexts: {
b_stat: TokContext
b_expr: TokContext
b_tmpl: TokContext
p_stat: TokContext
p_expr: TokContext
q_tmpl: TokContext
f_expr: TokContext
f_stat: TokContext
f_expr_gen: TokContext
f_gen: TokContext
}
function isIdentifierStart(code: number, astral?: boolean): boolean
function isIdentifierChar(code: number, astral?: boolean): boolean
interface AbstractToken {
}
interface Comment extends AbstractToken {
type: string
value: string
start: number
end: number
loc?: SourceLocation
range?: [number, number]
}
class Token {
type: TokenType
value: any
start: number
end: number
loc?: SourceLocation
range?: [number, number]
constructor(p: Parser)
}
function isNewLine(code: number): boolean
const lineBreak: RegExp
const lineBreakG: RegExp
const version: string
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
import * as acorn from "./acorn";
export = acorn;
+71
View File
@@ -0,0 +1,71 @@
'use strict';
var path = require('path');
var fs = require('fs');
var acorn = require('./acorn.js');
var inputFilePaths = [], forceFileName = false, fileMode = false, silent = false, compact = false, tokenize = false;
var options = {};
function help(status) {
var print = (status === 0) ? console.log : console.error;
print("usage: " + path.basename(process.argv[1]) + " [--ecma3|--ecma5|--ecma6|--ecma7|--ecma8|--ecma9|...|--ecma2015|--ecma2016|--ecma2017|--ecma2018|...]");
print(" [--tokenize] [--locations] [---allow-hash-bang] [--allow-await-outside-function] [--compact] [--silent] [--module] [--help] [--] [<infile>...]");
process.exit(status);
}
for (var i = 2; i < process.argv.length; ++i) {
var arg = process.argv[i];
if (arg[0] !== "-" || arg === "-") { inputFilePaths.push(arg); }
else if (arg === "--") {
inputFilePaths.push.apply(inputFilePaths, process.argv.slice(i + 1));
forceFileName = true;
break
} else if (arg === "--locations") { options.locations = true; }
else if (arg === "--allow-hash-bang") { options.allowHashBang = true; }
else if (arg === "--allow-await-outside-function") { options.allowAwaitOutsideFunction = true; }
else if (arg === "--silent") { silent = true; }
else if (arg === "--compact") { compact = true; }
else if (arg === "--help") { help(0); }
else if (arg === "--tokenize") { tokenize = true; }
else if (arg === "--module") { options.sourceType = "module"; }
else {
var match = arg.match(/^--ecma(\d+)$/);
if (match)
{ options.ecmaVersion = +match[1]; }
else
{ help(1); }
}
}
function run(codeList) {
var result = [], fileIdx = 0;
try {
codeList.forEach(function (code, idx) {
fileIdx = idx;
if (!tokenize) {
result = acorn.parse(code, options);
options.program = result;
} else {
var tokenizer = acorn.tokenizer(code, options), token;
do {
token = tokenizer.getToken();
result.push(token);
} while (token.type !== acorn.tokTypes.eof)
}
});
} catch (e) {
console.error(fileMode ? e.message.replace(/\(\d+:\d+\)$/, function (m) { return m.slice(0, 1) + inputFilePaths[fileIdx] + " " + m.slice(1); }) : e.message);
process.exit(1);
}
if (!silent) { console.log(JSON.stringify(result, null, compact ? null : 2)); }
}
if (fileMode = inputFilePaths.length && (forceFileName || !inputFilePaths.includes("-") || inputFilePaths.length !== 1)) {
run(inputFilePaths.map(function (path) { return fs.readFileSync(path, "utf8"); }));
} else {
var code = "";
process.stdin.resume();
process.stdin.on("data", function (chunk) { return code += chunk; });
process.stdin.on("end", function () { return run([code]); });
}
+82
View File
@@ -0,0 +1,82 @@
{
"_args": [
[
"acorn@8.5.0",
"/home/pi/Desktop/smartMirrorTestEnvironment/MagicMirror"
]
],
"_development": true,
"_from": "acorn@8.5.0",
"_id": "acorn@8.5.0",
"_inBundle": false,
"_integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==",
"_location": "/jest-environment-jsdom/acorn",
"_phantomChildren": {},
"_requested": {
"type": "version",
"registry": true,
"raw": "acorn@8.5.0",
"name": "acorn",
"escapedName": "acorn",
"rawSpec": "8.5.0",
"saveSpec": null,
"fetchSpec": "8.5.0"
},
"_requiredBy": [
"/jest-environment-jsdom/jsdom"
],
"_resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz",
"_spec": "8.5.0",
"_where": "/home/pi/Desktop/smartMirrorTestEnvironment/MagicMirror",
"bin": {
"acorn": "./bin/acorn"
},
"bugs": {
"url": "https://github.com/acornjs/acorn/issues"
},
"description": "ECMAScript parser",
"engines": {
"node": ">=0.4.0"
},
"exports": {
".": [
{
"import": "./dist/acorn.mjs",
"require": "./dist/acorn.js",
"default": "./dist/acorn.js"
},
"./dist/acorn.js"
],
"./package.json": "./package.json"
},
"homepage": "https://github.com/acornjs/acorn",
"license": "MIT",
"main": "dist/acorn.js",
"maintainers": [
{
"name": "Marijn Haverbeke",
"email": "marijnh@gmail.com",
"url": "https://marijnhaverbeke.nl"
},
{
"name": "Ingvar Stepanyan",
"email": "me@rreverser.com",
"url": "https://rreverser.com/"
},
{
"name": "Adrian Heine",
"url": "http://adrianheine.de"
}
],
"module": "dist/acorn.mjs",
"name": "acorn",
"repository": {
"type": "git",
"url": "git+https://github.com/acornjs/acorn.git"
},
"scripts": {
"prepare": "cd ..; npm run build:main && npm run build:bin"
},
"types": "dist/acorn.d.ts",
"version": "8.5.0"
}
+20
View File
@@ -0,0 +1,20 @@
Copyright (c) Nikita Vasilyev
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+67
View File
@@ -0,0 +1,67 @@
# CSSOM
CSSOM.js is a CSS parser written in pure JavaScript. It is also a partial implementation of [CSS Object Model](http://dev.w3.org/csswg/cssom/).
CSSOM.parse("body {color: black}")
-> {
cssRules: [
{
selectorText: "body",
style: {
0: "color",
color: "black",
length: 1
}
}
]
}
## [Parser demo](http://nv.github.com/CSSOM/docs/parse.html)
Works well in Google Chrome 6+, Safari 5+, Firefox 3.6+, Opera 10.63+.
Doesn't work in IE < 9 because of unsupported getters/setters.
To use CSSOM.js in the browser you might want to build a one-file version that exposes a single `CSSOM` global variable:
➤ git clone https://github.com/NV/CSSOM.git
➤ cd CSSOM
➤ node build.js
build/CSSOM.js is done
To use it with Node.js or any other CommonJS loader:
➤ npm install cssom
## Don’t use it if...
You parse CSS to mungle, minify or reformat code like this:
```css
div {
background: gray;
background: linear-gradient(to bottom, white 0%, black 100%);
}
```
This pattern is often used to give browsers that don’t understand linear gradients a fallback solution (e.g. gray color in the example).
In CSSOM, `background: gray` [gets overwritten](http://nv.github.io/CSSOM/docs/parse.html#css=div%20%7B%0A%20%20%20%20%20%20background%3A%20gray%3B%0A%20%20%20%20background%3A%20linear-gradient(to%20bottom%2C%20white%200%25%2C%20black%20100%25)%3B%0A%7D).
It doesn't get preserved.
If you do CSS mungling, minification, image inlining, and such, CSSOM.js is no good for you, considere using one of the following:
* [postcss](https://github.com/postcss/postcss)
* [reworkcss/css](https://github.com/reworkcss/css)
* [csso](https://github.com/css/csso)
* [mensch](https://github.com/brettstimmerman/mensch)
## [Tests](http://nv.github.com/CSSOM/spec/)
To run tests locally:
➤ git submodule init
➤ git submodule update
## [Who uses CSSOM.js](https://github.com/NV/CSSOM/wiki/Who-uses-CSSOM.js)
@@ -0,0 +1,39 @@
//.CommonJS
var CSSOM = {
CSSRule: require("./CSSRule").CSSRule,
MatcherList: require("./MatcherList").MatcherList
};
///CommonJS
/**
* @constructor
* @see https://developer.mozilla.org/en/CSS/@-moz-document
*/
CSSOM.CSSDocumentRule = function CSSDocumentRule() {
CSSOM.CSSRule.call(this);
this.matcher = new CSSOM.MatcherList();
this.cssRules = [];
};
CSSOM.CSSDocumentRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSDocumentRule.prototype.constructor = CSSOM.CSSDocumentRule;
CSSOM.CSSDocumentRule.prototype.type = 10;
//FIXME
//CSSOM.CSSDocumentRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
//CSSOM.CSSDocumentRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
Object.defineProperty(CSSOM.CSSDocumentRule.prototype, "cssText", {
get: function() {
var cssTexts = [];
for (var i=0, length=this.cssRules.length; i < length; i++) {
cssTexts.push(this.cssRules[i].cssText);
}
return "@-moz-document " + this.matcher.matcherText + " {" + cssTexts.join("") + "}";
}
});
//.CommonJS
exports.CSSDocumentRule = CSSOM.CSSDocumentRule;
///CommonJS
@@ -0,0 +1,36 @@
//.CommonJS
var CSSOM = {
CSSStyleDeclaration: require("./CSSStyleDeclaration").CSSStyleDeclaration,
CSSRule: require("./CSSRule").CSSRule
};
///CommonJS
/**
* @constructor
* @see http://dev.w3.org/csswg/cssom/#css-font-face-rule
*/
CSSOM.CSSFontFaceRule = function CSSFontFaceRule() {
CSSOM.CSSRule.call(this);
this.style = new CSSOM.CSSStyleDeclaration();
this.style.parentRule = this;
};
CSSOM.CSSFontFaceRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSFontFaceRule.prototype.constructor = CSSOM.CSSFontFaceRule;
CSSOM.CSSFontFaceRule.prototype.type = 5;
//FIXME
//CSSOM.CSSFontFaceRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
//CSSOM.CSSFontFaceRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
// http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSFontFaceRule.cpp
Object.defineProperty(CSSOM.CSSFontFaceRule.prototype, "cssText", {
get: function() {
return "@font-face {" + this.style.cssText + "}";
}
});
//.CommonJS
exports.CSSFontFaceRule = CSSOM.CSSFontFaceRule;
///CommonJS
@@ -0,0 +1,37 @@
//.CommonJS
var CSSOM = {
CSSRule: require("./CSSRule").CSSRule
};
///CommonJS
/**
* @constructor
* @see http://www.w3.org/TR/shadow-dom/#host-at-rule
*/
CSSOM.CSSHostRule = function CSSHostRule() {
CSSOM.CSSRule.call(this);
this.cssRules = [];
};
CSSOM.CSSHostRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSHostRule.prototype.constructor = CSSOM.CSSHostRule;
CSSOM.CSSHostRule.prototype.type = 1001;
//FIXME
//CSSOM.CSSHostRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
//CSSOM.CSSHostRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
Object.defineProperty(CSSOM.CSSHostRule.prototype, "cssText", {
get: function() {
var cssTexts = [];
for (var i=0, length=this.cssRules.length; i < length; i++) {
cssTexts.push(this.cssRules[i].cssText);
}
return "@host {" + cssTexts.join("") + "}";
}
});
//.CommonJS
exports.CSSHostRule = CSSOM.CSSHostRule;
///CommonJS
@@ -0,0 +1,132 @@
//.CommonJS
var CSSOM = {
CSSRule: require("./CSSRule").CSSRule,
CSSStyleSheet: require("./CSSStyleSheet").CSSStyleSheet,
MediaList: require("./MediaList").MediaList
};
///CommonJS
/**
* @constructor
* @see http://dev.w3.org/csswg/cssom/#cssimportrule
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSImportRule
*/
CSSOM.CSSImportRule = function CSSImportRule() {
CSSOM.CSSRule.call(this);
this.href = "";
this.media = new CSSOM.MediaList();
this.styleSheet = new CSSOM.CSSStyleSheet();
};
CSSOM.CSSImportRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSImportRule.prototype.constructor = CSSOM.CSSImportRule;
CSSOM.CSSImportRule.prototype.type = 3;
Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
get: function() {
var mediaText = this.media.mediaText;
return "@import url(" + this.href + ")" + (mediaText ? " " + mediaText : "") + ";";
},
set: function(cssText) {
var i = 0;
/**
* @import url(partial.css) screen, handheld;
* || |
* after-import media
* |
* url
*/
var state = '';
var buffer = '';
var index;
for (var character; (character = cssText.charAt(i)); i++) {
switch (character) {
case ' ':
case '\t':
case '\r':
case '\n':
case '\f':
if (state === 'after-import') {
state = 'url';
} else {
buffer += character;
}
break;
case '@':
if (!state && cssText.indexOf('@import', i) === i) {
state = 'after-import';
i += 'import'.length;
buffer = '';
}
break;
case 'u':
if (state === 'url' && cssText.indexOf('url(', i) === i) {
index = cssText.indexOf(')', i + 1);
if (index === -1) {
throw i + ': ")" not found';
}
i += 'url('.length;
var url = cssText.slice(i, index);
if (url[0] === url[url.length - 1]) {
if (url[0] === '"' || url[0] === "'") {
url = url.slice(1, -1);
}
}
this.href = url;
i = index;
state = 'media';
}
break;
case '"':
if (state === 'url') {
index = cssText.indexOf('"', i + 1);
if (!index) {
throw i + ": '\"' not found";
}
this.href = cssText.slice(i + 1, index);
i = index;
state = 'media';
}
break;
case "'":
if (state === 'url') {
index = cssText.indexOf("'", i + 1);
if (!index) {
throw i + ': "\'" not found';
}
this.href = cssText.slice(i + 1, index);
i = index;
state = 'media';
}
break;
case ';':
if (state === 'media') {
if (buffer) {
this.media.mediaText = buffer.trim();
}
}
break;
default:
if (state === 'media') {
buffer += character;
}
break;
}
}
}
});
//.CommonJS
exports.CSSImportRule = CSSOM.CSSImportRule;
///CommonJS
@@ -0,0 +1,37 @@
//.CommonJS
var CSSOM = {
CSSRule: require("./CSSRule").CSSRule,
CSSStyleDeclaration: require('./CSSStyleDeclaration').CSSStyleDeclaration
};
///CommonJS
/**
* @constructor
* @see http://www.w3.org/TR/css3-animations/#DOM-CSSKeyframeRule
*/
CSSOM.CSSKeyframeRule = function CSSKeyframeRule() {
CSSOM.CSSRule.call(this);
this.keyText = '';
this.style = new CSSOM.CSSStyleDeclaration();
this.style.parentRule = this;
};
CSSOM.CSSKeyframeRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSKeyframeRule.prototype.constructor = CSSOM.CSSKeyframeRule;
CSSOM.CSSKeyframeRule.prototype.type = 8;
//FIXME
//CSSOM.CSSKeyframeRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
//CSSOM.CSSKeyframeRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
// http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSKeyframeRule.cpp
Object.defineProperty(CSSOM.CSSKeyframeRule.prototype, "cssText", {
get: function() {
return this.keyText + " {" + this.style.cssText + "} ";
}
});
//.CommonJS
exports.CSSKeyframeRule = CSSOM.CSSKeyframeRule;
///CommonJS
@@ -0,0 +1,39 @@
//.CommonJS
var CSSOM = {
CSSRule: require("./CSSRule").CSSRule
};
///CommonJS
/**
* @constructor
* @see http://www.w3.org/TR/css3-animations/#DOM-CSSKeyframesRule
*/
CSSOM.CSSKeyframesRule = function CSSKeyframesRule() {
CSSOM.CSSRule.call(this);
this.name = '';
this.cssRules = [];
};
CSSOM.CSSKeyframesRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSKeyframesRule.prototype.constructor = CSSOM.CSSKeyframesRule;
CSSOM.CSSKeyframesRule.prototype.type = 7;
//FIXME
//CSSOM.CSSKeyframesRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
//CSSOM.CSSKeyframesRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
// http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSKeyframesRule.cpp
Object.defineProperty(CSSOM.CSSKeyframesRule.prototype, "cssText", {
get: function() {
var cssTexts = [];
for (var i=0, length=this.cssRules.length; i < length; i++) {
cssTexts.push(" " + this.cssRules[i].cssText);
}
return "@" + (this._vendorPrefix || '') + "keyframes " + this.name + " { \n" + cssTexts.join("\n") + "\n}";
}
});
//.CommonJS
exports.CSSKeyframesRule = CSSOM.CSSKeyframesRule;
///CommonJS
@@ -0,0 +1,41 @@
//.CommonJS
var CSSOM = {
CSSRule: require("./CSSRule").CSSRule,
MediaList: require("./MediaList").MediaList
};
///CommonJS
/**
* @constructor
* @see http://dev.w3.org/csswg/cssom/#cssmediarule
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSMediaRule
*/
CSSOM.CSSMediaRule = function CSSMediaRule() {
CSSOM.CSSRule.call(this);
this.media = new CSSOM.MediaList();
this.cssRules = [];
};
CSSOM.CSSMediaRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSMediaRule.prototype.constructor = CSSOM.CSSMediaRule;
CSSOM.CSSMediaRule.prototype.type = 4;
//FIXME
//CSSOM.CSSMediaRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
//CSSOM.CSSMediaRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
// http://opensource.apple.com/source/WebCore/WebCore-658.28/css/CSSMediaRule.cpp
Object.defineProperty(CSSOM.CSSMediaRule.prototype, "cssText", {
get: function() {
var cssTexts = [];
for (var i=0, length=this.cssRules.length; i < length; i++) {
cssTexts.push(this.cssRules[i].cssText);
}
return "@media " + this.media.mediaText + " {" + cssTexts.join("") + "}";
}
});
//.CommonJS
exports.CSSMediaRule = CSSOM.CSSMediaRule;
///CommonJS
+3
View File
@@ -0,0 +1,3 @@
var CSSOM = {};
+43
View File
@@ -0,0 +1,43 @@
//.CommonJS
var CSSOM = {};
///CommonJS
/**
* @constructor
* @see http://dev.w3.org/csswg/cssom/#the-cssrule-interface
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSRule
*/
CSSOM.CSSRule = function CSSRule() {
this.parentRule = null;
this.parentStyleSheet = null;
};
CSSOM.CSSRule.UNKNOWN_RULE = 0; // obsolete
CSSOM.CSSRule.STYLE_RULE = 1;
CSSOM.CSSRule.CHARSET_RULE = 2; // obsolete
CSSOM.CSSRule.IMPORT_RULE = 3;
CSSOM.CSSRule.MEDIA_RULE = 4;
CSSOM.CSSRule.FONT_FACE_RULE = 5;
CSSOM.CSSRule.PAGE_RULE = 6;
CSSOM.CSSRule.KEYFRAMES_RULE = 7;
CSSOM.CSSRule.KEYFRAME_RULE = 8;
CSSOM.CSSRule.MARGIN_RULE = 9;
CSSOM.CSSRule.NAMESPACE_RULE = 10;
CSSOM.CSSRule.COUNTER_STYLE_RULE = 11;
CSSOM.CSSRule.SUPPORTS_RULE = 12;
CSSOM.CSSRule.DOCUMENT_RULE = 13;
CSSOM.CSSRule.FONT_FEATURE_VALUES_RULE = 14;
CSSOM.CSSRule.VIEWPORT_RULE = 15;
CSSOM.CSSRule.REGION_STYLE_RULE = 16;
CSSOM.CSSRule.prototype = {
constructor: CSSOM.CSSRule
//FIXME
};
//.CommonJS
exports.CSSRule = CSSOM.CSSRule;
///CommonJS
@@ -0,0 +1,148 @@
//.CommonJS
var CSSOM = {};
///CommonJS
/**
* @constructor
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration
*/
CSSOM.CSSStyleDeclaration = function CSSStyleDeclaration(){
this.length = 0;
this.parentRule = null;
// NON-STANDARD
this._importants = {};
};
CSSOM.CSSStyleDeclaration.prototype = {
constructor: CSSOM.CSSStyleDeclaration,
/**
*
* @param {string} name
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-getPropertyValue
* @return {string} the value of the property if it has been explicitly set for this declaration block.
* Returns the empty string if the property has not been set.
*/
getPropertyValue: function(name) {
return this[name] || "";
},
/**
*
* @param {string} name
* @param {string} value
* @param {string} [priority=null] "important" or null
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-setProperty
*/
setProperty: function(name, value, priority) {
if (this[name]) {
// Property already exist. Overwrite it.
var index = Array.prototype.indexOf.call(this, name);
if (index < 0) {
this[this.length] = name;
this.length++;
}
} else {
// New property.
this[this.length] = name;
this.length++;
}
this[name] = value + "";
this._importants[name] = priority;
},
/**
*
* @param {string} name
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-removeProperty
* @return {string} the value of the property if it has been explicitly set for this declaration block.
* Returns the empty string if the property has not been set or the property name does not correspond to a known CSS property.
*/
removeProperty: function(name) {
if (!(name in this)) {
return "";
}
var index = Array.prototype.indexOf.call(this, name);
if (index < 0) {
return "";
}
var prevValue = this[name];
this[name] = "";
// That's what WebKit and Opera do
Array.prototype.splice.call(this, index, 1);
// That's what Firefox does
//this[index] = ""
return prevValue;
},
getPropertyCSSValue: function() {
//FIXME
},
/**
*
* @param {String} name
*/
getPropertyPriority: function(name) {
return this._importants[name] || "";
},
/**
* element.style.overflow = "auto"
* element.style.getPropertyShorthand("overflow-x")
* -> "overflow"
*/
getPropertyShorthand: function() {
//FIXME
},
isPropertyImplicit: function() {
//FIXME
},
// Doesn't work in IE < 9
get cssText(){
var properties = [];
for (var i=0, length=this.length; i < length; ++i) {
var name = this[i];
var value = this.getPropertyValue(name);
var priority = this.getPropertyPriority(name);
if (priority) {
priority = " !" + priority;
}
properties[i] = name + ": " + value + priority + ";";
}
return properties.join(" ");
},
set cssText(text){
var i, name;
for (i = this.length; i--;) {
name = this[i];
this[name] = "";
}
Array.prototype.splice.call(this, 0, this.length);
this._importants = {};
var dummyRule = CSSOM.parse('#bogus{' + text + '}').cssRules[0].style;
var length = dummyRule.length;
for (i = 0; i < length; ++i) {
name = dummyRule[i];
this.setProperty(dummyRule[i], dummyRule.getPropertyValue(name), dummyRule.getPropertyPriority(name));
}
}
};
//.CommonJS
exports.CSSStyleDeclaration = CSSOM.CSSStyleDeclaration;
CSSOM.parse = require('./parse').parse; // Cannot be included sooner due to the mutual dependency between parse.js and CSSStyleDeclaration.js
///CommonJS
@@ -0,0 +1,190 @@
//.CommonJS
var CSSOM = {
CSSStyleDeclaration: require("./CSSStyleDeclaration").CSSStyleDeclaration,
CSSRule: require("./CSSRule").CSSRule
};
///CommonJS
/**
* @constructor
* @see http://dev.w3.org/csswg/cssom/#cssstylerule
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleRule
*/
CSSOM.CSSStyleRule = function CSSStyleRule() {
CSSOM.CSSRule.call(this);
this.selectorText = "";
this.style = new CSSOM.CSSStyleDeclaration();
this.style.parentRule = this;
};
CSSOM.CSSStyleRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSStyleRule.prototype.constructor = CSSOM.CSSStyleRule;
CSSOM.CSSStyleRule.prototype.type = 1;
Object.defineProperty(CSSOM.CSSStyleRule.prototype, "cssText", {
get: function() {
var text;
if (this.selectorText) {
text = this.selectorText + " {" + this.style.cssText + "}";
} else {
text = "";
}
return text;
},
set: function(cssText) {
var rule = CSSOM.CSSStyleRule.parse(cssText);
this.style = rule.style;
this.selectorText = rule.selectorText;
}
});
/**
* NON-STANDARD
* lightweight version of parse.js.
* @param {string} ruleText
* @return CSSStyleRule
*/
CSSOM.CSSStyleRule.parse = function(ruleText) {
var i = 0;
var state = "selector";
var index;
var j = i;
var buffer = "";
var SIGNIFICANT_WHITESPACE = {
"selector": true,
"value": true
};
var styleRule = new CSSOM.CSSStyleRule();
var name, priority="";
for (var character; (character = ruleText.charAt(i)); i++) {
switch (character) {
case " ":
case "\t":
case "\r":
case "\n":
case "\f":
if (SIGNIFICANT_WHITESPACE[state]) {
// Squash 2 or more white-spaces in the row into 1
switch (ruleText.charAt(i - 1)) {
case " ":
case "\t":
case "\r":
case "\n":
case "\f":
break;
default:
buffer += " ";
break;
}
}
break;
// String
case '"':
j = i + 1;
index = ruleText.indexOf('"', j) + 1;
if (!index) {
throw '" is missing';
}
buffer += ruleText.slice(i, index);
i = index - 1;
break;
case "'":
j = i + 1;
index = ruleText.indexOf("'", j) + 1;
if (!index) {
throw "' is missing";
}
buffer += ruleText.slice(i, index);
i = index - 1;
break;
// Comment
case "/":
if (ruleText.charAt(i + 1) === "*") {
i += 2;
index = ruleText.indexOf("*/", i);
if (index === -1) {
throw new SyntaxError("Missing */");
} else {
i = index + 1;
}
} else {
buffer += character;
}
break;
case "{":
if (state === "selector") {
styleRule.selectorText = buffer.trim();
buffer = "";
state = "name";
}
break;
case ":":
if (state === "name") {
name = buffer.trim();
buffer = "";
state = "value";
} else {
buffer += character;
}
break;
case "!":
if (state === "value" && ruleText.indexOf("!important", i) === i) {
priority = "important";
i += "important".length;
} else {
buffer += character;
}
break;
case ";":
if (state === "value") {
styleRule.style.setProperty(name, buffer.trim(), priority);
priority = "";
buffer = "";
state = "name";
} else {
buffer += character;
}
break;
case "}":
if (state === "value") {
styleRule.style.setProperty(name, buffer.trim(), priority);
priority = "";
buffer = "";
} else if (state === "name") {
break;
} else {
buffer += character;
}
state = "selector";
break;
default:
buffer += character;
break;
}
}
return styleRule;
};
//.CommonJS
exports.CSSStyleRule = CSSOM.CSSStyleRule;
///CommonJS
@@ -0,0 +1,88 @@
//.CommonJS
var CSSOM = {
StyleSheet: require("./StyleSheet").StyleSheet,
CSSStyleRule: require("./CSSStyleRule").CSSStyleRule
};
///CommonJS
/**
* @constructor
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet
*/
CSSOM.CSSStyleSheet = function CSSStyleSheet() {
CSSOM.StyleSheet.call(this);
this.cssRules = [];
};
CSSOM.CSSStyleSheet.prototype = new CSSOM.StyleSheet();
CSSOM.CSSStyleSheet.prototype.constructor = CSSOM.CSSStyleSheet;
/**
* Used to insert a new rule into the style sheet. The new rule now becomes part of the cascade.
*
* sheet = new Sheet("body {margin: 0}")
* sheet.toString()
* -> "body{margin:0;}"
* sheet.insertRule("img {border: none}", 0)
* -> 0
* sheet.toString()
* -> "img{border:none;}body{margin:0;}"
*
* @param {string} rule
* @param {number} index
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet-insertRule
* @return {number} The index within the style sheet's rule collection of the newly inserted rule.
*/
CSSOM.CSSStyleSheet.prototype.insertRule = function(rule, index) {
if (index < 0 || index > this.cssRules.length) {
throw new RangeError("INDEX_SIZE_ERR");
}
var cssRule = CSSOM.parse(rule).cssRules[0];
cssRule.parentStyleSheet = this;
this.cssRules.splice(index, 0, cssRule);
return index;
};
/**
* Used to delete a rule from the style sheet.
*
* sheet = new Sheet("img{border:none} body{margin:0}")
* sheet.toString()
* -> "img{border:none;}body{margin:0;}"
* sheet.deleteRule(0)
* sheet.toString()
* -> "body{margin:0;}"
*
* @param {number} index within the style sheet's rule list of the rule to remove.
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet-deleteRule
*/
CSSOM.CSSStyleSheet.prototype.deleteRule = function(index) {
if (index < 0 || index >= this.cssRules.length) {
throw new RangeError("INDEX_SIZE_ERR");
}
this.cssRules.splice(index, 1);
};
/**
* NON-STANDARD
* @return {string} serialize stylesheet
*/
CSSOM.CSSStyleSheet.prototype.toString = function() {
var result = "";
var rules = this.cssRules;
for (var i=0; i<rules.length; i++) {
result += rules[i].cssText + "\n";
}
return result;
};
//.CommonJS
exports.CSSStyleSheet = CSSOM.CSSStyleSheet;
CSSOM.parse = require('./parse').parse; // Cannot be included sooner due to the mutual dependency between parse.js and CSSStyleSheet.js
///CommonJS
@@ -0,0 +1,36 @@
//.CommonJS
var CSSOM = {
CSSRule: require("./CSSRule").CSSRule,
};
///CommonJS
/**
* @constructor
* @see https://drafts.csswg.org/css-conditional-3/#the-csssupportsrule-interface
*/
CSSOM.CSSSupportsRule = function CSSSupportsRule() {
CSSOM.CSSRule.call(this);
this.conditionText = '';
this.cssRules = [];
};
CSSOM.CSSSupportsRule.prototype = new CSSOM.CSSRule();
CSSOM.CSSSupportsRule.prototype.constructor = CSSOM.CSSSupportsRule;
CSSOM.CSSSupportsRule.prototype.type = 12;
Object.defineProperty(CSSOM.CSSSupportsRule.prototype, "cssText", {
get: function() {
var cssTexts = [];
for (var i = 0, length = this.cssRules.length; i < length; i++) {
cssTexts.push(this.cssRules[i].cssText);
}
return "@supports " + this.conditionText + " {" + cssTexts.join("") + "}";
}
});
//.CommonJS
exports.CSSSupportsRule = CSSOM.CSSSupportsRule;
///CommonJS
+43
View File
@@ -0,0 +1,43 @@
//.CommonJS
var CSSOM = {};
///CommonJS
/**
* @constructor
* @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSValue
*
* TODO: add if needed
*/
CSSOM.CSSValue = function CSSValue() {
};
CSSOM.CSSValue.prototype = {
constructor: CSSOM.CSSValue,
// @see: http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSValue
set cssText(text) {
var name = this._getConstructorName();
throw new Error('DOMException: property "cssText" of "' + name + '" is readonly and can not be replaced with "' + text + '"!');
},
get cssText() {
var name = this._getConstructorName();
throw new Error('getter "cssText" of "' + name + '" is not implemented!');
},
_getConstructorName: function() {
var s = this.constructor.toString(),
c = s.match(/function\s([^\(]+)/),
name = c[1];
return name;
}
};
//.CommonJS
exports.CSSValue = CSSOM.CSSValue;
///CommonJS
@@ -0,0 +1,344 @@
//.CommonJS
var CSSOM = {
CSSValue: require('./CSSValue').CSSValue
};
///CommonJS
/**
* @constructor
* @see http://msdn.microsoft.com/en-us/library/ms537634(v=vs.85).aspx
*
*/
CSSOM.CSSValueExpression = function CSSValueExpression(token, idx) {
this._token = token;
this._idx = idx;
};
CSSOM.CSSValueExpression.prototype = new CSSOM.CSSValue();
CSSOM.CSSValueExpression.prototype.constructor = CSSOM.CSSValueExpression;
/**
* parse css expression() value
*
* @return {Object}
* - error:
* or
* - idx:
* - expression:
*
* Example:
*
* .selector {
* zoom: expression(documentElement.clientWidth > 1000 ? '1000px' : 'auto');
* }
*/
CSSOM.CSSValueExpression.prototype.parse = function() {
var token = this._token,
idx = this._idx;
var character = '',
expression = '',
error = '',
info,
paren = [];
for (; ; ++idx) {
character = token.charAt(idx);
// end of token
if (character === '') {
error = 'css expression error: unfinished expression!';
break;
}
switch(character) {
case '(':
paren.push(character);
expression += character;
break;
case ')':
paren.pop(character);
expression += character;
break;
case '/':
if ((info = this._parseJSComment(token, idx))) { // comment?
if (info.error) {
error = 'css expression error: unfinished comment in expression!';
} else {
idx = info.idx;
// ignore the comment
}
} else if ((info = this._parseJSRexExp(token, idx))) { // regexp
idx = info.idx;
expression += info.text;
} else { // other
expression += character;
}
break;
case "'":
case '"':
info = this._parseJSString(token, idx, character);
if (info) { // string
idx = info.idx;
expression += info.text;
} else {
expression += character;
}
break;
default:
expression += character;
break;
}
if (error) {
break;
}
// end of expression
if (paren.length === 0) {
break;
}
}
var ret;
if (error) {
ret = {
error: error
};
} else {
ret = {
idx: idx,
expression: expression
};
}
return ret;
};
/**
*
* @return {Object|false}
* - idx:
* - text:
* or
* - error:
* or
* false
*
*/
CSSOM.CSSValueExpression.prototype._parseJSComment = function(token, idx) {
var nextChar = token.charAt(idx + 1),
text;
if (nextChar === '/' || nextChar === '*') {
var startIdx = idx,
endIdx,
commentEndChar;
if (nextChar === '/') { // line comment
commentEndChar = '\n';
} else if (nextChar === '*') { // block comment
commentEndChar = '*/';
}
endIdx = token.indexOf(commentEndChar, startIdx + 1 + 1);
if (endIdx !== -1) {
endIdx = endIdx + commentEndChar.length - 1;
text = token.substring(idx, endIdx + 1);
return {
idx: endIdx,
text: text
};
} else {
var error = 'css expression error: unfinished comment in expression!';
return {
error: error
};
}
} else {
return false;
}
};
/**
*
* @return {Object|false}
* - idx:
* - text:
* or
* false
*
*/
CSSOM.CSSValueExpression.prototype._parseJSString = function(token, idx, sep) {
var endIdx = this._findMatchedIdx(token, idx, sep),
text;
if (endIdx === -1) {
return false;
} else {
text = token.substring(idx, endIdx + sep.length);
return {
idx: endIdx,
text: text
};
}
};
/**
* parse regexp in css expression
*
* @return {Object|false}
* - idx:
* - regExp:
* or
* false
*/
/*
all legal RegExp
/a/
(/a/)
[/a/]
[12, /a/]
!/a/
+/a/
-/a/
* /a/
/ /a/
%/a/
===/a/
!==/a/
==/a/
!=/a/
>/a/
>=/a/
</a/
<=/a/
&/a/
|/a/
^/a/
~/a/
<</a/
>>/a/
>>>/a/
&&/a/
||/a/
?/a/
=/a/
,/a/
delete /a/
in /a/
instanceof /a/
new /a/
typeof /a/
void /a/
*/
CSSOM.CSSValueExpression.prototype._parseJSRexExp = function(token, idx) {
var before = token.substring(0, idx).replace(/\s+$/, ""),
legalRegx = [
/^$/,
/\($/,
/\[$/,
/\!$/,
/\+$/,
/\-$/,
/\*$/,
/\/\s+/,
/\%$/,
/\=$/,
/\>$/,
/<$/,
/\&$/,
/\|$/,
/\^$/,
/\~$/,
/\?$/,
/\,$/,
/delete$/,
/in$/,
/instanceof$/,
/new$/,
/typeof$/,
/void$/
];
var isLegal = legalRegx.some(function(reg) {
return reg.test(before);
});
if (!isLegal) {
return false;
} else {
var sep = '/';
// same logic as string
return this._parseJSString(token, idx, sep);
}
};
/**
*
* find next sep(same line) index in `token`
*
* @return {Number}
*
*/
CSSOM.CSSValueExpression.prototype._findMatchedIdx = function(token, idx, sep) {
var startIdx = idx,
endIdx;
var NOT_FOUND = -1;
while(true) {
endIdx = token.indexOf(sep, startIdx + 1);
if (endIdx === -1) { // not found
endIdx = NOT_FOUND;
break;
} else {
var text = token.substring(idx + 1, endIdx),
matched = text.match(/\\+$/);
if (!matched || matched[0] % 2 === 0) { // not escaped
break;
} else {
startIdx = endIdx;
}
}
}
// boundary must be in the same line(js sting or regexp)
var nextNewLineIdx = token.indexOf('\n', idx + 1);
if (nextNewLineIdx < endIdx) {
endIdx = NOT_FOUND;
}
return endIdx;
};
//.CommonJS
exports.CSSValueExpression = CSSOM.CSSValueExpression;
///CommonJS
@@ -0,0 +1,62 @@
//.CommonJS
var CSSOM = {};
///CommonJS
/**
* @constructor
* @see https://developer.mozilla.org/en/CSS/@-moz-document
*/
CSSOM.MatcherList = function MatcherList(){
this.length = 0;
};
CSSOM.MatcherList.prototype = {
constructor: CSSOM.MatcherList,
/**
* @return {string}
*/
get matcherText() {
return Array.prototype.join.call(this, ", ");
},
/**
* @param {string} value
*/
set matcherText(value) {
// just a temporary solution, actually it may be wrong by just split the value with ',', because a url can include ','.
var values = value.split(",");
var length = this.length = values.length;
for (var i=0; i<length; i++) {
this[i] = values[i].trim();
}
},
/**
* @param {string} matcher
*/
appendMatcher: function(matcher) {
if (Array.prototype.indexOf.call(this, matcher) === -1) {
this[this.length] = matcher;
this.length++;
}
},
/**
* @param {string} matcher
*/
deleteMatcher: function(matcher) {
var index = Array.prototype.indexOf.call(this, matcher);
if (index !== -1) {
Array.prototype.splice.call(this, index, 1);
}
}
};
//.CommonJS
exports.MatcherList = CSSOM.MatcherList;
///CommonJS
@@ -0,0 +1,61 @@
//.CommonJS
var CSSOM = {};
///CommonJS
/**
* @constructor
* @see http://dev.w3.org/csswg/cssom/#the-medialist-interface
*/
CSSOM.MediaList = function MediaList(){
this.length = 0;
};
CSSOM.MediaList.prototype = {
constructor: CSSOM.MediaList,
/**
* @return {string}
*/
get mediaText() {
return Array.prototype.join.call(this, ", ");
},
/**
* @param {string} value
*/
set mediaText(value) {
var values = value.split(",");
var length = this.length = values.length;
for (var i=0; i<length; i++) {
this[i] = values[i].trim();
}
},
/**
* @param {string} medium
*/
appendMedium: function(medium) {
if (Array.prototype.indexOf.call(this, medium) === -1) {
this[this.length] = medium;
this.length++;
}
},
/**
* @param {string} medium
*/
deleteMedium: function(medium) {
var index = Array.prototype.indexOf.call(this, medium);
if (index !== -1) {
Array.prototype.splice.call(this, index, 1);
}
}
};
//.CommonJS
exports.MediaList = CSSOM.MediaList;
///CommonJS
@@ -0,0 +1,17 @@
//.CommonJS
var CSSOM = {};
///CommonJS
/**
* @constructor
* @see http://dev.w3.org/csswg/cssom/#the-stylesheet-interface
*/
CSSOM.StyleSheet = function StyleSheet() {
this.parentStyleSheet = null;
};
//.CommonJS
exports.StyleSheet = CSSOM.StyleSheet;
///CommonJS
+71
View File
@@ -0,0 +1,71 @@
//.CommonJS
var CSSOM = {
CSSStyleSheet: require("./CSSStyleSheet").CSSStyleSheet,
CSSStyleRule: require("./CSSStyleRule").CSSStyleRule,
CSSMediaRule: require("./CSSMediaRule").CSSMediaRule,
CSSSupportsRule: require("./CSSSupportsRule").CSSSupportsRule,
CSSStyleDeclaration: require("./CSSStyleDeclaration").CSSStyleDeclaration,
CSSKeyframeRule: require('./CSSKeyframeRule').CSSKeyframeRule,
CSSKeyframesRule: require('./CSSKeyframesRule').CSSKeyframesRule
};
///CommonJS
/**
* Produces a deep copy of stylesheet — the instance variables of stylesheet are copied recursively.
* @param {CSSStyleSheet|CSSOM.CSSStyleSheet} stylesheet
* @nosideeffects
* @return {CSSOM.CSSStyleSheet}
*/
CSSOM.clone = function clone(stylesheet) {
var cloned = new CSSOM.CSSStyleSheet();
var rules = stylesheet.cssRules;
if (!rules) {
return cloned;
}
for (var i = 0, rulesLength = rules.length; i < rulesLength; i++) {
var rule = rules[i];
var ruleClone = cloned.cssRules[i] = new rule.constructor();
var style = rule.style;
if (style) {
var styleClone = ruleClone.style = new CSSOM.CSSStyleDeclaration();
for (var j = 0, styleLength = style.length; j < styleLength; j++) {
var name = styleClone[j] = style[j];
styleClone[name] = style[name];
styleClone._importants[name] = style.getPropertyPriority(name);
}
styleClone.length = style.length;
}
if (rule.hasOwnProperty('keyText')) {
ruleClone.keyText = rule.keyText;
}
if (rule.hasOwnProperty('selectorText')) {
ruleClone.selectorText = rule.selectorText;
}
if (rule.hasOwnProperty('mediaText')) {
ruleClone.mediaText = rule.mediaText;
}
if (rule.hasOwnProperty('conditionText')) {
ruleClone.conditionText = rule.conditionText;
}
if (rule.hasOwnProperty('cssRules')) {
ruleClone.cssRules = clone(rule).cssRules;
}
}
return cloned;
};
//.CommonJS
exports.clone = CSSOM.clone;
///CommonJS
+21
View File
@@ -0,0 +1,21 @@
'use strict';
exports.CSSStyleDeclaration = require('./CSSStyleDeclaration').CSSStyleDeclaration;
exports.CSSRule = require('./CSSRule').CSSRule;
exports.CSSStyleRule = require('./CSSStyleRule').CSSStyleRule;
exports.MediaList = require('./MediaList').MediaList;
exports.CSSMediaRule = require('./CSSMediaRule').CSSMediaRule;
exports.CSSSupportsRule = require('./CSSSupportsRule').CSSSupportsRule;
exports.CSSImportRule = require('./CSSImportRule').CSSImportRule;
exports.CSSFontFaceRule = require('./CSSFontFaceRule').CSSFontFaceRule;
exports.CSSHostRule = require('./CSSHostRule').CSSHostRule;
exports.StyleSheet = require('./StyleSheet').StyleSheet;
exports.CSSStyleSheet = require('./CSSStyleSheet').CSSStyleSheet;
exports.CSSKeyframesRule = require('./CSSKeyframesRule').CSSKeyframesRule;
exports.CSSKeyframeRule = require('./CSSKeyframeRule').CSSKeyframeRule;
exports.MatcherList = require('./MatcherList').MatcherList;
exports.CSSDocumentRule = require('./CSSDocumentRule').CSSDocumentRule;
exports.CSSValue = require('./CSSValue').CSSValue;
exports.CSSValueExpression = require('./CSSValueExpression').CSSValueExpression;
exports.parse = require('./parse').parse;
exports.clone = require('./clone').clone;
+463
View File
@@ -0,0 +1,463 @@
//.CommonJS
var CSSOM = {};
///CommonJS
/**
* @param {string} token
*/
CSSOM.parse = function parse(token) {
var i = 0;
/**
"before-selector" or
"selector" or
"atRule" or
"atBlock" or
"conditionBlock" or
"before-name" or
"name" or
"before-value" or
"value"
*/
var state = "before-selector";
var index;
var buffer = "";
var valueParenthesisDepth = 0;
var SIGNIFICANT_WHITESPACE = {
"selector": true,
"value": true,
"value-parenthesis": true,
"atRule": true,
"importRule-begin": true,
"importRule": true,
"atBlock": true,
"conditionBlock": true,
'documentRule-begin': true
};
var styleSheet = new CSSOM.CSSStyleSheet();
// @type CSSStyleSheet|CSSMediaRule|CSSSupportsRule|CSSFontFaceRule|CSSKeyframesRule|CSSDocumentRule
var currentScope = styleSheet;
// @type CSSMediaRule|CSSSupportsRule|CSSKeyframesRule|CSSDocumentRule
var parentRule;
var ancestorRules = [];
var hasAncestors = false;
var prevScope;
var name, priority="", styleRule, mediaRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule;
var atKeyframesRegExp = /@(-(?:\w+-)+)?keyframes/g;
var parseError = function(message) {
var lines = token.substring(0, i).split('\n');
var lineCount = lines.length;
var charCount = lines.pop().length + 1;
var error = new Error(message + ' (line ' + lineCount + ', char ' + charCount + ')');
error.line = lineCount;
/* jshint sub : true */
error['char'] = charCount;
error.styleSheet = styleSheet;
throw error;
};
for (var character; (character = token.charAt(i)); i++) {
switch (character) {
case " ":
case "\t":
case "\r":
case "\n":
case "\f":
if (SIGNIFICANT_WHITESPACE[state]) {
buffer += character;
}
break;
// String
case '"':
index = i + 1;
do {
index = token.indexOf('"', index) + 1;
if (!index) {
parseError('Unmatched "');
}
} while (token[index - 2] === '\\');
buffer += token.slice(i, index);
i = index - 1;
switch (state) {
case 'before-value':
state = 'value';
break;
case 'importRule-begin':
state = 'importRule';
break;
}
break;
case "'":
index = i + 1;
do {
index = token.indexOf("'", index) + 1;
if (!index) {
parseError("Unmatched '");
}
} while (token[index - 2] === '\\');
buffer += token.slice(i, index);
i = index - 1;
switch (state) {
case 'before-value':
state = 'value';
break;
case 'importRule-begin':
state = 'importRule';
break;
}
break;
// Comment
case "/":
if (token.charAt(i + 1) === "*") {
i += 2;
index = token.indexOf("*/", i);
if (index === -1) {
parseError("Missing */");
} else {
i = index + 1;
}
} else {
buffer += character;
}
if (state === "importRule-begin") {
buffer += " ";
state = "importRule";
}
break;
// At-rule
case "@":
if (token.indexOf("@-moz-document", i) === i) {
state = "documentRule-begin";
documentRule = new CSSOM.CSSDocumentRule();
documentRule.__starts = i;
i += "-moz-document".length;
buffer = "";
break;
} else if (token.indexOf("@media", i) === i) {
state = "atBlock";
mediaRule = new CSSOM.CSSMediaRule();
mediaRule.__starts = i;
i += "media".length;
buffer = "";
break;
} else if (token.indexOf("@supports", i) === i) {
state = "conditionBlock";
supportsRule = new CSSOM.CSSSupportsRule();
supportsRule.__starts = i;
i += "supports".length;
buffer = "";
break;
} else if (token.indexOf("@host", i) === i) {
state = "hostRule-begin";
i += "host".length;
hostRule = new CSSOM.CSSHostRule();
hostRule.__starts = i;
buffer = "";
break;
} else if (token.indexOf("@import", i) === i) {
state = "importRule-begin";
i += "import".length;
buffer += "@import";
break;
} else if (token.indexOf("@font-face", i) === i) {
state = "fontFaceRule-begin";
i += "font-face".length;
fontFaceRule = new CSSOM.CSSFontFaceRule();
fontFaceRule.__starts = i;
buffer = "";
break;
} else {
atKeyframesRegExp.lastIndex = i;
var matchKeyframes = atKeyframesRegExp.exec(token);
if (matchKeyframes && matchKeyframes.index === i) {
state = "keyframesRule-begin";
keyframesRule = new CSSOM.CSSKeyframesRule();
keyframesRule.__starts = i;
keyframesRule._vendorPrefix = matchKeyframes[1]; // Will come out as undefined if no prefix was found
i += matchKeyframes[0].length - 1;
buffer = "";
break;
} else if (state === "selector") {
state = "atRule";
}
}
buffer += character;
break;
case "{":
if (state === "selector" || state === "atRule") {
styleRule.selectorText = buffer.trim();
styleRule.style.__starts = i;
buffer = "";
state = "before-name";
} else if (state === "atBlock") {
mediaRule.media.mediaText = buffer.trim();
if (parentRule) {
ancestorRules.push(parentRule);
}
currentScope = parentRule = mediaRule;
mediaRule.parentStyleSheet = styleSheet;
buffer = "";
state = "before-selector";
} else if (state === "conditionBlock") {
supportsRule.conditionText = buffer.trim();
if (parentRule) {
ancestorRules.push(parentRule);
}
currentScope = parentRule = supportsRule;
supportsRule.parentStyleSheet = styleSheet;
buffer = "";
state = "before-selector";
} else if (state === "hostRule-begin") {
if (parentRule) {
ancestorRules.push(parentRule);
}
currentScope = parentRule = hostRule;
hostRule.parentStyleSheet = styleSheet;
buffer = "";
state = "before-selector";
} else if (state === "fontFaceRule-begin") {
if (parentRule) {
fontFaceRule.parentRule = parentRule;
}
fontFaceRule.parentStyleSheet = styleSheet;
styleRule = fontFaceRule;
buffer = "";
state = "before-name";
} else if (state === "keyframesRule-begin") {
keyframesRule.name = buffer.trim();
if (parentRule) {
ancestorRules.push(parentRule);
keyframesRule.parentRule = parentRule;
}
keyframesRule.parentStyleSheet = styleSheet;
currentScope = parentRule = keyframesRule;
buffer = "";
state = "keyframeRule-begin";
} else if (state === "keyframeRule-begin") {
styleRule = new CSSOM.CSSKeyframeRule();
styleRule.keyText = buffer.trim();
styleRule.__starts = i;
buffer = "";
state = "before-name";
} else if (state === "documentRule-begin") {
// FIXME: what if this '{' is in the url text of the match function?
documentRule.matcher.matcherText = buffer.trim();
if (parentRule) {
ancestorRules.push(parentRule);
documentRule.parentRule = parentRule;
}
currentScope = parentRule = documentRule;
documentRule.parentStyleSheet = styleSheet;
buffer = "";
state = "before-selector";
}
break;
case ":":
if (state === "name") {
name = buffer.trim();
buffer = "";
state = "before-value";
} else {
buffer += character;
}
break;
case "(":
if (state === 'value') {
// ie css expression mode
if (buffer.trim() === 'expression') {
var info = (new CSSOM.CSSValueExpression(token, i)).parse();
if (info.error) {
parseError(info.error);
} else {
buffer += info.expression;
i = info.idx;
}
} else {
state = 'value-parenthesis';
//always ensure this is reset to 1 on transition
//from value to value-parenthesis
valueParenthesisDepth = 1;
buffer += character;
}
} else if (state === 'value-parenthesis') {
valueParenthesisDepth++;
buffer += character;
} else {
buffer += character;
}
break;
case ")":
if (state === 'value-parenthesis') {
valueParenthesisDepth--;
if (valueParenthesisDepth === 0) state = 'value';
}
buffer += character;
break;
case "!":
if (state === "value" && token.indexOf("!important", i) === i) {
priority = "important";
i += "important".length;
} else {
buffer += character;
}
break;
case ";":
switch (state) {
case "value":
styleRule.style.setProperty(name, buffer.trim(), priority);
priority = "";
buffer = "";
state = "before-name";
break;
case "atRule":
buffer = "";
state = "before-selector";
break;
case "importRule":
importRule = new CSSOM.CSSImportRule();
importRule.parentStyleSheet = importRule.styleSheet.parentStyleSheet = styleSheet;
importRule.cssText = buffer + character;
styleSheet.cssRules.push(importRule);
buffer = "";
state = "before-selector";
break;
default:
buffer += character;
break;
}
break;
case "}":
switch (state) {
case "value":
styleRule.style.setProperty(name, buffer.trim(), priority);
priority = "";
/* falls through */
case "before-name":
case "name":
styleRule.__ends = i + 1;
if (parentRule) {
styleRule.parentRule = parentRule;
}
styleRule.parentStyleSheet = styleSheet;
currentScope.cssRules.push(styleRule);
buffer = "";
if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
state = "keyframeRule-begin";
} else {
state = "before-selector";
}
break;
case "keyframeRule-begin":
case "before-selector":
case "selector":
// End of media/supports/document rule.
if (!parentRule) {
parseError("Unexpected }");
}
// Handle rules nested in @media or @supports
hasAncestors = ancestorRules.length > 0;
while (ancestorRules.length > 0) {
parentRule = ancestorRules.pop();
if (
parentRule.constructor.name === "CSSMediaRule"
|| parentRule.constructor.name === "CSSSupportsRule"
) {
prevScope = currentScope;
currentScope = parentRule;
currentScope.cssRules.push(prevScope);
break;
}
if (ancestorRules.length === 0) {
hasAncestors = false;
}
}
if (!hasAncestors) {
currentScope.__ends = i + 1;
styleSheet.cssRules.push(currentScope);
currentScope = styleSheet;
parentRule = null;
}
buffer = "";
state = "before-selector";
break;
}
break;
default:
switch (state) {
case "before-selector":
state = "selector";
styleRule = new CSSOM.CSSStyleRule();
styleRule.__starts = i;
break;
case "before-name":
state = "name";
break;
case "before-value":
state = "value";
break;
case "importRule-begin":
state = "importRule";
break;
}
buffer += character;
break;
}
}
return styleSheet;
};
//.CommonJS
exports.parse = CSSOM.parse;
// The following modules cannot be included sooner due to the mutual dependency with parse.js
CSSOM.CSSStyleSheet = require("./CSSStyleSheet").CSSStyleSheet;
CSSOM.CSSStyleRule = require("./CSSStyleRule").CSSStyleRule;
CSSOM.CSSImportRule = require("./CSSImportRule").CSSImportRule;
CSSOM.CSSMediaRule = require("./CSSMediaRule").CSSMediaRule;
CSSOM.CSSSupportsRule = require("./CSSSupportsRule").CSSSupportsRule;
CSSOM.CSSFontFaceRule = require("./CSSFontFaceRule").CSSFontFaceRule;
CSSOM.CSSHostRule = require("./CSSHostRule").CSSHostRule;
CSSOM.CSSStyleDeclaration = require('./CSSStyleDeclaration').CSSStyleDeclaration;
CSSOM.CSSKeyframeRule = require('./CSSKeyframeRule').CSSKeyframeRule;
CSSOM.CSSKeyframesRule = require('./CSSKeyframesRule').CSSKeyframesRule;
CSSOM.CSSValueExpression = require('./CSSValueExpression').CSSValueExpression;
CSSOM.CSSDocumentRule = require('./CSSDocumentRule').CSSDocumentRule;
///CommonJS
+57
View File
@@ -0,0 +1,57 @@
{
"_args": [
[
"cssom@0.4.4",
"/home/pi/Desktop/smartMirrorTestEnvironment/MagicMirror"
]
],
"_development": true,
"_from": "cssom@0.4.4",
"_id": "cssom@0.4.4",
"_inBundle": false,
"_integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==",
"_location": "/jest-environment-jsdom/cssom",
"_phantomChildren": {},
"_requested": {
"type": "version",
"registry": true,
"raw": "cssom@0.4.4",
"name": "cssom",
"escapedName": "cssom",
"rawSpec": "0.4.4",
"saveSpec": null,
"fetchSpec": "0.4.4"
},
"_requiredBy": [
"/jest-environment-jsdom/jsdom"
],
"_resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
"_spec": "0.4.4",
"_where": "/home/pi/Desktop/smartMirrorTestEnvironment/MagicMirror",
"author": {
"name": "Nikita Vasilyev",
"email": "me@elv1s.ru"
},
"bugs": {
"url": "https://github.com/NV/CSSOM/issues"
},
"description": "CSS Object Model implementation and CSS parser",
"files": [
"lib/"
],
"homepage": "https://github.com/NV/CSSOM#readme",
"keywords": [
"CSS",
"CSSOM",
"parser",
"styleSheet"
],
"license": "MIT",
"main": "./lib/index.js",
"name": "cssom",
"repository": {
"type": "git",
"url": "git+https://github.com/NV/CSSOM.git"
},
"version": "0.4.4"
}
@@ -0,0 +1,7 @@
Copyright © 2017–2020 Domenic Denicola <d@domenic.me>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+64
View File
@@ -0,0 +1,64 @@
# Parse `data:` URLs
This package helps you parse `data:` URLs [according to the WHATWG Fetch Standard](https://fetch.spec.whatwg.org/#data-urls):
```js
const parseDataURL = require("data-urls");
const textExample = parseDataURL("data:,Hello%2C%20World!");
console.log(textExample.mimeType.toString()); // "text/plain;charset=US-ASCII"
console.log(textExample.body.toString()); // "Hello, World!"
const htmlExample = dataURL("data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E");
console.log(htmlExample.mimeType.toString()); // "text/html"
console.log(htmlExample.body.toString()); // <h1>Hello, World!</h1>
const pngExample = parseDataURL("data:image/png;base64,iVBORw0KGgoAAA" +
"ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4" +
"//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU" +
"5ErkJggg==");
console.log(pngExample.mimeType.toString()); // "image/png"
console.log(pngExample.body); // <Buffer 89 50 4e 47 0d ... >
```
## API
This package's main module's default export is a function that accepts a string and returns a `{ mimeType, body }` object, or `null` if the result cannot be parsed as a `data:` URL.
- The `mimeType` property is an instance of [whatwg-mimetype](https://www.npmjs.com/package/whatwg-mimetype)'s `MIMEType` class.
- The `body` property is a Node.js [`Buffer`](https://nodejs.org/docs/latest/api/buffer.html) instance.
As shown in the examples above, both of these have useful `toString()` methods for manipulating them as string values. However…
### A word of caution on string decoding
Because Node.js's `Buffer.prototype.toString()` assumes a UTF-8 encoding, simply doing `dataURL.body.toString()` may not work correctly if the `data:` URL's contents were not originally written in UTF-8. This includes if the encoding is "US-ASCII", [aka windows-1252](https://encoding.spec.whatwg.org/#names-and-labels), which is notable for being the default in many cases.
A more complete decoding example would use the [whatwg-encoding](https://www.npmjs.com/package/whatwg-encoding) package as follows:
```js
const parseDataURL = require("data-urls");
const { labelToName, decode } = require("whatwg-encoding");
const dataURL = parseDataURL(arbitraryString);
const encodingName = labelToName(dataURL.mimeType.parameters.get("charset"));
const bodyDecoded = decode(dataURL.body, encodingName);
```
For example, given an `arbitraryString` of `data:,Hello!`, this will produce a `bodyDecoded` of `"Hello!"`, as expected. But given an `arbitraryString` of `"data:,Héllo!"`, this will correctly produce a `bodyDecoded` of `"Héllo!"`, whereas just doing `dataURL.body.toString()` will give back `"Héllo!"`.
In summary, only use `dataURL.body.toString()` when you are very certain your data is inside the ASCII range (i.e. code points within the range U+0000 to U+007F).
### Advanced functionality: parsing from a URL record
If you are using the [whatwg-url](https://github.com/jsdom/whatwg-url) package, you may already have a "URL record" object on hand, as produced by that package's `parseURL` export. In that case, you can use this package's `fromURLRecord` export to save a bit of work:
```js
const { parseURL } = require("whatwg-url");
const dataURLFromURLRecord = require("data-urls").fromURLRecord;
const urlRecord = parseURL("data:,Hello%2C%20World!");
const dataURL = dataURLFromURLRecord(urlRecord);
```
In practice, we expect this functionality only to be used by consumers like [jsdom](https://www.npmjs.com/package/jsdom), which are using these packages at a very low level.
@@ -0,0 +1,74 @@
"use strict";
const MIMEType = require("whatwg-mimetype");
const { parseURL, serializeURL } = require("whatwg-url");
const {
stripLeadingAndTrailingASCIIWhitespace,
stringPercentDecode,
isomorphicDecode,
forgivingBase64Decode
} = require("./utils.js");
module.exports = stringInput => {
const urlRecord = parseURL(stringInput);
if (urlRecord === null) {
return null;
}
return module.exports.fromURLRecord(urlRecord);
};
module.exports.fromURLRecord = urlRecord => {
if (urlRecord.scheme !== "data") {
return null;
}
const input = serializeURL(urlRecord, true).substring("data:".length);
let position = 0;
let mimeType = "";
while (position < input.length && input[position] !== ",") {
mimeType += input[position];
++position;
}
mimeType = stripLeadingAndTrailingASCIIWhitespace(mimeType);
if (position === input.length) {
return null;
}
++position;
const encodedBody = input.substring(position);
let body = stringPercentDecode(encodedBody);
// Can't use /i regexp flag because it isn't restricted to ASCII.
const mimeTypeBase64MatchResult = /(.*); *[Bb][Aa][Ss][Ee]64$/.exec(mimeType);
if (mimeTypeBase64MatchResult) {
const stringBody = isomorphicDecode(body);
body = forgivingBase64Decode(stringBody);
if (body === null) {
return null;
}
mimeType = mimeTypeBase64MatchResult[1];
}
if (mimeType.startsWith(";")) {
mimeType = "text/plain" + mimeType;
}
let mimeTypeRecord;
try {
mimeTypeRecord = new MIMEType(mimeType);
} catch (e) {
mimeTypeRecord = new MIMEType("text/plain;charset=US-ASCII");
}
return {
mimeType: mimeTypeRecord,
body
};
};
@@ -0,0 +1,23 @@
"use strict";
const { percentDecode } = require("whatwg-url");
const { atob } = require("abab");
exports.stripLeadingAndTrailingASCIIWhitespace = string => {
return string.replace(/^[ \t\n\f\r]+/, "").replace(/[ \t\n\f\r]+$/, "");
};
exports.stringPercentDecode = input => {
return percentDecode(Buffer.from(input, "utf-8"));
};
exports.isomorphicDecode = input => {
return input.toString("binary");
};
exports.forgivingBase64Decode = data => {
const asString = atob(data);
if (asString === null) {
return null;
}
return Buffer.from(asString, "binary");
};
@@ -0,0 +1,93 @@
{
"_args": [
[
"data-urls@2.0.0",
"/home/pi/Desktop/smartMirrorTestEnvironment/MagicMirror"
]
],
"_development": true,
"_from": "data-urls@2.0.0",
"_id": "data-urls@2.0.0",
"_inBundle": false,
"_integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==",
"_location": "/jest-environment-jsdom/data-urls",
"_phantomChildren": {},
"_requested": {
"type": "version",
"registry": true,
"raw": "data-urls@2.0.0",
"name": "data-urls",
"escapedName": "data-urls",
"rawSpec": "2.0.0",
"saveSpec": null,
"fetchSpec": "2.0.0"
},
"_requiredBy": [
"/jest-environment-jsdom/jsdom"
],
"_resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz",
"_spec": "2.0.0",
"_where": "/home/pi/Desktop/smartMirrorTestEnvironment/MagicMirror",
"author": {
"name": "Domenic Denicola",
"email": "d@domenic.me",
"url": "https://domenic.me/"
},
"bugs": {
"url": "https://github.com/jsdom/data-urls/issues"
},
"dependencies": {
"abab": "^2.0.3",
"whatwg-mimetype": "^2.3.0",
"whatwg-url": "^8.0.0"
},
"description": "Parses data: URLs",
"devDependencies": {
"eslint": "^6.8.0",
"jest": "^24.9.0",
"request": "^2.88.0"
},
"engines": {
"node": ">=10"
},
"files": [
"lib/"
],
"homepage": "https://github.com/jsdom/data-urls#readme",
"jest": {
"coverageDirectory": "coverage",
"coverageReporters": [
"lcov",
"text-summary"
],
"testEnvironment": "node",
"testMatch": [
"<rootDir>/test/**/*.js"
],
"coveragePathIgnorePatterns": [
"<rootDir>/node_modules/(?!(abab/lib/atob.js))"
]
},
"keywords": [
"data url",
"data uri",
"data:",
"http",
"fetch",
"whatwg"
],
"license": "MIT",
"main": "lib/parser.js",
"name": "data-urls",
"repository": {
"type": "git",
"url": "git+https://github.com/jsdom/data-urls.git"
},
"scripts": {
"coverage": "jest --coverage",
"lint": "eslint .",
"pretest": "node scripts/get-latest-platform-tests.js",
"test": "jest"
},
"version": "2.0.0"
}
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,356 @@
# Form-Data [![NPM Module](https://img.shields.io/npm/v/form-data.svg)](https://www.npmjs.com/package/form-data) [![Join the chat at https://gitter.im/form-data/form-data](http://form-data.github.io/images/gitterbadge.svg)](https://gitter.im/form-data/form-data)
A library to create readable ```"multipart/form-data"``` streams. Can be used to submit forms and file uploads to other web applications.
The API of this library is inspired by the [XMLHttpRequest-2 FormData Interface][xhr2-fd].
[xhr2-fd]: http://dev.w3.org/2006/webapi/XMLHttpRequest-2/Overview.html#the-formdata-interface
[![Linux Build](https://img.shields.io/travis/form-data/form-data/v3.0.1.svg?label=linux:6.x-12.x)](https://travis-ci.org/form-data/form-data)
[![MacOS Build](https://img.shields.io/travis/form-data/form-data/v3.0.1.svg?label=macos:6.x-12.x)](https://travis-ci.org/form-data/form-data)
[![Windows Build](https://img.shields.io/travis/form-data/form-data/v3.0.1.svg?label=windows:6.x-12.x)](https://travis-ci.org/form-data/form-data)
[![Coverage Status](https://img.shields.io/coveralls/form-data/form-data/v3.0.1.svg?label=code+coverage)](https://coveralls.io/github/form-data/form-data?branch=master)
[![Dependency Status](https://img.shields.io/david/form-data/form-data.svg)](https://david-dm.org/form-data/form-data)
## Install
```
npm install --save form-data
```
## Usage
In this example we are constructing a form with 3 fields that contain a string,
a buffer and a file stream.
``` javascript
var FormData = require('form-data');
var fs = require('fs');
var form = new FormData();
form.append('my_field', 'my value');
form.append('my_buffer', new Buffer(10));
form.append('my_file', fs.createReadStream('/foo/bar.jpg'));
```
Also you can use http-response stream:
``` javascript
var FormData = require('form-data');
var http = require('http');
var form = new FormData();
http.request('http://nodejs.org/images/logo.png', function(response) {
form.append('my_field', 'my value');
form.append('my_buffer', new Buffer(10));
form.append('my_logo', response);
});
```
Or @mikeal's [request](https://github.com/request/request) stream:
``` javascript
var FormData = require('form-data');
var request = require('request');
var form = new FormData();
form.append('my_field', 'my value');
form.append('my_buffer', new Buffer(10));
form.append('my_logo', request('http://nodejs.org/images/logo.png'));
```
In order to submit this form to a web application, call ```submit(url, [callback])``` method:
``` javascript
form.submit('http://example.org/', function(err, res) {
// res – response object (http.IncomingMessage) //
res.resume();
});
```
For more advanced request manipulations ```submit()``` method returns ```http.ClientRequest``` object, or you can choose from one of the alternative submission methods.
### Custom options
You can provide custom options, such as `maxDataSize`:
``` javascript
var FormData = require('form-data');
var form = new FormData({ maxDataSize: 20971520 });
form.append('my_field', 'my value');
form.append('my_buffer', /* something big */);
```
List of available options could be found in [combined-stream](https://github.com/felixge/node-combined-stream/blob/master/lib/combined_stream.js#L7-L15)
### Alternative submission methods
You can use node's http client interface:
``` javascript
var http = require('http');
var request = http.request({
method: 'post',
host: 'example.org',
path: '/upload',
headers: form.getHeaders()
});
form.pipe(request);
request.on('response', function(res) {
console.log(res.statusCode);
});
```
Or if you would prefer the `'Content-Length'` header to be set for you:
``` javascript
form.submit('example.org/upload', function(err, res) {
console.log(res.statusCode);
});
```
To use custom headers and pre-known length in parts:
``` javascript
var CRLF = '\r\n';
var form = new FormData();
var options = {
header: CRLF + '--' + form.getBoundary() + CRLF + 'X-Custom-Header: 123' + CRLF + CRLF,
knownLength: 1
};
form.append('my_buffer', buffer, options);
form.submit('http://example.com/', function(err, res) {
if (err) throw err;
console.log('Done');
});
```
Form-Data can recognize and fetch all the required information from common types of streams (```fs.readStream```, ```http.response``` and ```mikeal's request```), for some other types of streams you'd need to provide "file"-related information manually:
``` javascript
someModule.stream(function(err, stdout, stderr) {
if (err) throw err;
var form = new FormData();
form.append('file', stdout, {
filename: 'unicycle.jpg', // ... or:
filepath: 'photos/toys/unicycle.jpg',
contentType: 'image/jpeg',
knownLength: 19806
});
form.submit('http://example.com/', function(err, res) {
if (err) throw err;
console.log('Done');
});
});
```
The `filepath` property overrides `filename` and may contain a relative path. This is typically used when uploading [multiple files from a directory](https://wicg.github.io/entries-api/#dom-htmlinputelement-webkitdirectory).
For edge cases, like POST request to URL with query string or to pass HTTP auth credentials, object can be passed to `form.submit()` as first parameter:
``` javascript
form.submit({
host: 'example.com',
path: '/probably.php?extra=params',
auth: 'username:password'
}, function(err, res) {
console.log(res.statusCode);
});
```
In case you need to also send custom HTTP headers with the POST request, you can use the `headers` key in first parameter of `form.submit()`:
``` javascript
form.submit({
host: 'example.com',
path: '/surelynot.php',
headers: {'x-test-header': 'test-header-value'}
}, function(err, res) {
console.log(res.statusCode);
});
```
### Methods
- [_Void_ append( **String** _field_, **Mixed** _value_ [, **Mixed** _options_] )](https://github.com/form-data/form-data#void-append-string-field-mixed-value--mixed-options-).
- [_Headers_ getHeaders( [**Headers** _userHeaders_] )](https://github.com/form-data/form-data#array-getheaders-array-userheaders-)
- [_String_ getBoundary()](https://github.com/form-data/form-data#string-getboundary)
- [_Void_ setBoundary()](https://github.com/form-data/form-data#void-setboundary)
- [_Buffer_ getBuffer()](https://github.com/form-data/form-data#buffer-getbuffer)
- [_Integer_ getLengthSync()](https://github.com/form-data/form-data#integer-getlengthsync)
- [_Integer_ getLength( **function** _callback_ )](https://github.com/form-data/form-data#integer-getlength-function-callback-)
- [_Boolean_ hasKnownLength()](https://github.com/form-data/form-data#boolean-hasknownlength)
- [_Request_ submit( _params_, **function** _callback_ )](https://github.com/form-data/form-data#request-submit-params-function-callback-)
- [_String_ toString()](https://github.com/form-data/form-data#string-tostring)
#### _Void_ append( **String** _field_, **Mixed** _value_ [, **Mixed** _options_] )
Append data to the form. You can submit about any format (string, integer, boolean, buffer, etc.). However, Arrays are not supported and need to be turned into strings by the user.
```javascript
var form = new FormData();
form.append( 'my_string', 'my value' );
form.append( 'my_integer', 1 );
form.append( 'my_boolean', true );
form.append( 'my_buffer', new Buffer(10) );
form.append( 'my_array_as_json', JSON.stringify( ['bird','cute'] ) )
```
You may provide a string for options, or an object.
```javascript
// Set filename by providing a string for options
form.append( 'my_file', fs.createReadStream('/foo/bar.jpg'), 'bar.jpg' );
// provide an object.
form.append( 'my_file', fs.createReadStream('/foo/bar.jpg'), {filename: 'bar.jpg', contentType: 'image/jpeg', knownLength: 19806} );
```
#### _Headers_ getHeaders( [**Headers** _userHeaders_] )
This method adds the correct `content-type` header to the provided array of `userHeaders`.
#### _String_ getBoundary()
Return the boundary of the formData. By default, the boundary consists of 26 `-` followed by 24 numbers
for example:
```javascript
--------------------------515890814546601021194782
```
#### _Void_ setBoundary(String _boundary_)
Set the boundary string, overriding the default behavior described above.
_Note: The boundary must be unique and may not appear in the data._
#### _Buffer_ getBuffer()
Return the full formdata request package, as a Buffer. You can insert this Buffer in e.g. Axios to send multipart data.
```javascript
var form = new FormData();
form.append( 'my_buffer', Buffer.from([0x4a,0x42,0x20,0x52,0x6f,0x63,0x6b,0x73]) );
form.append( 'my_file', fs.readFileSync('/foo/bar.jpg') );
axios.post( 'https://example.com/path/to/api',
form.getBuffer(),
form.getHeaders()
)
```
**Note:** Because the output is of type Buffer, you can only append types that are accepted by Buffer: *string, Buffer, ArrayBuffer, Array, or Array-like Object*. A ReadStream for example will result in an error.
#### _Integer_ getLengthSync()
Same as `getLength` but synchronous.
_Note: getLengthSync __doesn't__ calculate streams length._
#### _Integer_ getLength( **function** _callback_ )
Returns the `Content-Length` async. The callback is used to handle errors and continue once the length has been calculated
```javascript
this.getLength(function(err, length) {
if (err) {
this._error(err);
return;
}
// add content length
request.setHeader('Content-Length', length);
...
}.bind(this));
```
#### _Boolean_ hasKnownLength()
Checks if the length of added values is known.
#### _Request_ submit( _params_, **function** _callback_ )
Submit the form to a web application.
```javascript
var form = new FormData();
form.append( 'my_string', 'Hello World' );
form.submit( 'http://example.com/', function(err, res) {
// res – response object (http.IncomingMessage) //
res.resume();
} );
```
#### _String_ toString()
Returns the form data as a string. Don't use this if you are sending files or buffers, use `getBuffer()` instead.
### Integration with other libraries
#### Request
Form submission using [request](https://github.com/request/request):
```javascript
var formData = {
my_field: 'my_value',
my_file: fs.createReadStream(__dirname + '/unicycle.jpg'),
};
request.post({url:'http://service.com/upload', formData: formData}, function(err, httpResponse, body) {
if (err) {
return console.error('upload failed:', err);
}
console.log('Upload successful! Server responded with:', body);
});
```
For more details see [request readme](https://github.com/request/request#multipartform-data-multipart-form-uploads).
#### node-fetch
You can also submit a form using [node-fetch](https://github.com/bitinn/node-fetch):
```javascript
var form = new FormData();
form.append('a', 1);
fetch('http://example.com', { method: 'POST', body: form })
.then(function(res) {
return res.json();
}).then(function(json) {
console.log(json);
});
```
#### axios
In Node.js you can post a file using [axios](https://github.com/axios/axios):
```javascript
const form = new FormData();
const stream = fs.createReadStream(PATH_TO_FILE);
form.append('image', stream);
// In Node.js environment you need to set boundary in the header field 'Content-Type' by calling method `getHeaders`
const formHeaders = form.getHeaders();
axios.post('http://example.com', form, {
headers: {
...formHeaders,
},
})
.then(response => response)
.catch(error => error)
```
## Notes
- ```getLengthSync()``` method DOESN'T calculate length for streams, use ```knownLength``` options as workaround.
- Starting version `2.x` FormData has dropped support for `node@0.10.x`.
- Starting version `3.x` FormData has dropped support for `node@4.x`.
## License
Form-Data is released under the [MIT](License) license.
+356
View File
@@ -0,0 +1,356 @@
# Form-Data [![NPM Module](https://img.shields.io/npm/v/form-data.svg)](https://www.npmjs.com/package/form-data) [![Join the chat at https://gitter.im/form-data/form-data](http://form-data.github.io/images/gitterbadge.svg)](https://gitter.im/form-data/form-data)
A library to create readable ```"multipart/form-data"``` streams. Can be used to submit forms and file uploads to other web applications.
The API of this library is inspired by the [XMLHttpRequest-2 FormData Interface][xhr2-fd].
[xhr2-fd]: http://dev.w3.org/2006/webapi/XMLHttpRequest-2/Overview.html#the-formdata-interface
[![Linux Build](https://img.shields.io/travis/form-data/form-data/v3.0.1.svg?label=linux:6.x-12.x)](https://travis-ci.org/form-data/form-data)
[![MacOS Build](https://img.shields.io/travis/form-data/form-data/v3.0.1.svg?label=macos:6.x-12.x)](https://travis-ci.org/form-data/form-data)
[![Windows Build](https://img.shields.io/travis/form-data/form-data/v3.0.1.svg?label=windows:6.x-12.x)](https://travis-ci.org/form-data/form-data)
[![Coverage Status](https://img.shields.io/coveralls/form-data/form-data/v3.0.1.svg?label=code+coverage)](https://coveralls.io/github/form-data/form-data?branch=master)
[![Dependency Status](https://img.shields.io/david/form-data/form-data.svg)](https://david-dm.org/form-data/form-data)
## Install
```
npm install --save form-data
```
## Usage
In this example we are constructing a form with 3 fields that contain a string,
a buffer and a file stream.
``` javascript
var FormData = require('form-data');
var fs = require('fs');
var form = new FormData();
form.append('my_field', 'my value');
form.append('my_buffer', new Buffer(10));
form.append('my_file', fs.createReadStream('/foo/bar.jpg'));
```
Also you can use http-response stream:
``` javascript
var FormData = require('form-data');
var http = require('http');
var form = new FormData();
http.request('http://nodejs.org/images/logo.png', function(response) {
form.append('my_field', 'my value');
form.append('my_buffer', new Buffer(10));
form.append('my_logo', response);
});
```
Or @mikeal's [request](https://github.com/request/request) stream:
``` javascript
var FormData = require('form-data');
var request = require('request');
var form = new FormData();
form.append('my_field', 'my value');
form.append('my_buffer', new Buffer(10));
form.append('my_logo', request('http://nodejs.org/images/logo.png'));
```
In order to submit this form to a web application, call ```submit(url, [callback])``` method:
``` javascript
form.submit('http://example.org/', function(err, res) {
// res – response object (http.IncomingMessage) //
res.resume();
});
```
For more advanced request manipulations ```submit()``` method returns ```http.ClientRequest``` object, or you can choose from one of the alternative submission methods.
### Custom options
You can provide custom options, such as `maxDataSize`:
``` javascript
var FormData = require('form-data');
var form = new FormData({ maxDataSize: 20971520 });
form.append('my_field', 'my value');
form.append('my_buffer', /* something big */);
```
List of available options could be found in [combined-stream](https://github.com/felixge/node-combined-stream/blob/master/lib/combined_stream.js#L7-L15)
### Alternative submission methods
You can use node's http client interface:
``` javascript
var http = require('http');
var request = http.request({
method: 'post',
host: 'example.org',
path: '/upload',
headers: form.getHeaders()
});
form.pipe(request);
request.on('response', function(res) {
console.log(res.statusCode);
});
```
Or if you would prefer the `'Content-Length'` header to be set for you:
``` javascript
form.submit('example.org/upload', function(err, res) {
console.log(res.statusCode);
});
```
To use custom headers and pre-known length in parts:
``` javascript
var CRLF = '\r\n';
var form = new FormData();
var options = {
header: CRLF + '--' + form.getBoundary() + CRLF + 'X-Custom-Header: 123' + CRLF + CRLF,
knownLength: 1
};
form.append('my_buffer', buffer, options);
form.submit('http://example.com/', function(err, res) {
if (err) throw err;
console.log('Done');
});
```
Form-Data can recognize and fetch all the required information from common types of streams (```fs.readStream```, ```http.response``` and ```mikeal's request```), for some other types of streams you'd need to provide "file"-related information manually:
``` javascript
someModule.stream(function(err, stdout, stderr) {
if (err) throw err;
var form = new FormData();
form.append('file', stdout, {
filename: 'unicycle.jpg', // ... or:
filepath: 'photos/toys/unicycle.jpg',
contentType: 'image/jpeg',
knownLength: 19806
});
form.submit('http://example.com/', function(err, res) {
if (err) throw err;
console.log('Done');
});
});
```
The `filepath` property overrides `filename` and may contain a relative path. This is typically used when uploading [multiple files from a directory](https://wicg.github.io/entries-api/#dom-htmlinputelement-webkitdirectory).
For edge cases, like POST request to URL with query string or to pass HTTP auth credentials, object can be passed to `form.submit()` as first parameter:
``` javascript
form.submit({
host: 'example.com',
path: '/probably.php?extra=params',
auth: 'username:password'
}, function(err, res) {
console.log(res.statusCode);
});
```
In case you need to also send custom HTTP headers with the POST request, you can use the `headers` key in first parameter of `form.submit()`:
``` javascript
form.submit({
host: 'example.com',
path: '/surelynot.php',
headers: {'x-test-header': 'test-header-value'}
}, function(err, res) {
console.log(res.statusCode);
});
```
### Methods
- [_Void_ append( **String** _field_, **Mixed** _value_ [, **Mixed** _options_] )](https://github.com/form-data/form-data#void-append-string-field-mixed-value--mixed-options-).
- [_Headers_ getHeaders( [**Headers** _userHeaders_] )](https://github.com/form-data/form-data#array-getheaders-array-userheaders-)
- [_String_ getBoundary()](https://github.com/form-data/form-data#string-getboundary)
- [_Void_ setBoundary()](https://github.com/form-data/form-data#void-setboundary)
- [_Buffer_ getBuffer()](https://github.com/form-data/form-data#buffer-getbuffer)
- [_Integer_ getLengthSync()](https://github.com/form-data/form-data#integer-getlengthsync)
- [_Integer_ getLength( **function** _callback_ )](https://github.com/form-data/form-data#integer-getlength-function-callback-)
- [_Boolean_ hasKnownLength()](https://github.com/form-data/form-data#boolean-hasknownlength)
- [_Request_ submit( _params_, **function** _callback_ )](https://github.com/form-data/form-data#request-submit-params-function-callback-)
- [_String_ toString()](https://github.com/form-data/form-data#string-tostring)
#### _Void_ append( **String** _field_, **Mixed** _value_ [, **Mixed** _options_] )
Append data to the form. You can submit about any format (string, integer, boolean, buffer, etc.). However, Arrays are not supported and need to be turned into strings by the user.
```javascript
var form = new FormData();
form.append( 'my_string', 'my value' );
form.append( 'my_integer', 1 );
form.append( 'my_boolean', true );
form.append( 'my_buffer', new Buffer(10) );
form.append( 'my_array_as_json', JSON.stringify( ['bird','cute'] ) )
```
You may provide a string for options, or an object.
```javascript
// Set filename by providing a string for options
form.append( 'my_file', fs.createReadStream('/foo/bar.jpg'), 'bar.jpg' );
// provide an object.
form.append( 'my_file', fs.createReadStream('/foo/bar.jpg'), {filename: 'bar.jpg', contentType: 'image/jpeg', knownLength: 19806} );
```
#### _Headers_ getHeaders( [**Headers** _userHeaders_] )
This method adds the correct `content-type` header to the provided array of `userHeaders`.
#### _String_ getBoundary()
Return the boundary of the formData. By default, the boundary consists of 26 `-` followed by 24 numbers
for example:
```javascript
--------------------------515890814546601021194782
```
#### _Void_ setBoundary(String _boundary_)
Set the boundary string, overriding the default behavior described above.
_Note: The boundary must be unique and may not appear in the data._
#### _Buffer_ getBuffer()
Return the full formdata request package, as a Buffer. You can insert this Buffer in e.g. Axios to send multipart data.
```javascript
var form = new FormData();
form.append( 'my_buffer', Buffer.from([0x4a,0x42,0x20,0x52,0x6f,0x63,0x6b,0x73]) );
form.append( 'my_file', fs.readFileSync('/foo/bar.jpg') );
axios.post( 'https://example.com/path/to/api',
form.getBuffer(),
form.getHeaders()
)
```
**Note:** Because the output is of type Buffer, you can only append types that are accepted by Buffer: *string, Buffer, ArrayBuffer, Array, or Array-like Object*. A ReadStream for example will result in an error.
#### _Integer_ getLengthSync()
Same as `getLength` but synchronous.
_Note: getLengthSync __doesn't__ calculate streams length._
#### _Integer_ getLength( **function** _callback_ )
Returns the `Content-Length` async. The callback is used to handle errors and continue once the length has been calculated
```javascript
this.getLength(function(err, length) {
if (err) {
this._error(err);
return;
}
// add content length
request.setHeader('Content-Length', length);
...
}.bind(this));
```
#### _Boolean_ hasKnownLength()
Checks if the length of added values is known.
#### _Request_ submit( _params_, **function** _callback_ )
Submit the form to a web application.
```javascript
var form = new FormData();
form.append( 'my_string', 'Hello World' );
form.submit( 'http://example.com/', function(err, res) {
// res – response object (http.IncomingMessage) //
res.resume();
} );
```
#### _String_ toString()
Returns the form data as a string. Don't use this if you are sending files or buffers, use `getBuffer()` instead.
### Integration with other libraries
#### Request
Form submission using [request](https://github.com/request/request):
```javascript
var formData = {
my_field: 'my_value',
my_file: fs.createReadStream(__dirname + '/unicycle.jpg'),
};
request.post({url:'http://service.com/upload', formData: formData}, function(err, httpResponse, body) {
if (err) {
return console.error('upload failed:', err);
}
console.log('Upload successful! Server responded with:', body);
});
```
For more details see [request readme](https://github.com/request/request#multipartform-data-multipart-form-uploads).
#### node-fetch
You can also submit a form using [node-fetch](https://github.com/bitinn/node-fetch):
```javascript
var form = new FormData();
form.append('a', 1);
fetch('http://example.com', { method: 'POST', body: form })
.then(function(res) {
return res.json();
}).then(function(json) {
console.log(json);
});
```
#### axios
In Node.js you can post a file using [axios](https://github.com/axios/axios):
```javascript
const form = new FormData();
const stream = fs.createReadStream(PATH_TO_FILE);
form.append('image', stream);
// In Node.js environment you need to set boundary in the header field 'Content-Type' by calling method `getHeaders`
const formHeaders = form.getHeaders();
axios.post('http://example.com', form, {
headers: {
...formHeaders,
},
})
.then(response => response)
.catch(error => error)
```
## Notes
- ```getLengthSync()``` method DOESN'T calculate length for streams, use ```knownLength``` options as workaround.
- Starting version `2.x` FormData has dropped support for `node@0.10.x`.
- Starting version `3.x` FormData has dropped support for `node@4.x`.
## License
Form-Data is released under the [MIT](License) license.
+62
View File
@@ -0,0 +1,62 @@
// Definitions by: Carlos Ballesteros Velasco <https://github.com/soywiz>
// Leon Yu <https://github.com/leonyu>
// BendingBender <https://github.com/BendingBender>
// Maple Miao <https://github.com/mapleeit>
/// <reference types="node" />
import * as stream from 'stream';
import * as http from 'http';
export = FormData;
// Extracted because @types/node doesn't export interfaces.
interface ReadableOptions {
highWaterMark?: number;
encoding?: string;
objectMode?: boolean;
read?(this: stream.Readable, size: number): void;
destroy?(this: stream.Readable, error: Error | null, callback: (error: Error | null) => void): void;
autoDestroy?: boolean;
}
interface Options extends ReadableOptions {
writable?: boolean;
readable?: boolean;
dataSize?: number;
maxDataSize?: number;
pauseStreams?: boolean;
}
declare class FormData extends stream.Readable {
constructor(options?: Options);
append(key: string, value: any, options?: FormData.AppendOptions | string): void;
getHeaders(userHeaders?: FormData.Headers): FormData.Headers;
submit(
params: string | FormData.SubmitOptions,
callback?: (error: Error | null, response: http.IncomingMessage) => void
): http.ClientRequest;
getBuffer(): Buffer;
setBoundary(boundary: string): void;
getBoundary(): string;
getLength(callback: (err: Error | null, length: number) => void): void;
getLengthSync(): number;
hasKnownLength(): boolean;
}
declare namespace FormData {
interface Headers {
[key: string]: any;
}
interface AppendOptions {
header?: string | Headers;
knownLength?: number;
filename?: string;
filepath?: string;
contentType?: string;
}
interface SubmitOptions extends http.RequestOptions {
protocol?: 'https:' | 'http:';
}
}
@@ -0,0 +1,2 @@
/* eslint-env browser */
module.exports = typeof self == 'object' ? self.FormData : window.FormData;
@@ -0,0 +1,498 @@
var CombinedStream = require('combined-stream');
var util = require('util');
var path = require('path');
var http = require('http');
var https = require('https');
var parseUrl = require('url').parse;
var fs = require('fs');
var mime = require('mime-types');
var asynckit = require('asynckit');
var populate = require('./populate.js');
// Public API
module.exports = FormData;
// make it a Stream
util.inherits(FormData, CombinedStream);
/**
* Create readable "multipart/form-data" streams.
* Can be used to submit forms
* and file uploads to other web applications.
*
* @constructor
* @param {Object} options - Properties to be added/overriden for FormData and CombinedStream
*/
function FormData(options) {
if (!(this instanceof FormData)) {
return new FormData(options);
}
this._overheadLength = 0;
this._valueLength = 0;
this._valuesToMeasure = [];
CombinedStream.call(this);
options = options || {};
for (var option in options) {
this[option] = options[option];
}
}
FormData.LINE_BREAK = '\r\n';
FormData.DEFAULT_CONTENT_TYPE = 'application/octet-stream';
FormData.prototype.append = function(field, value, options) {
options = options || {};
// allow filename as single option
if (typeof options == 'string') {
options = {filename: options};
}
var append = CombinedStream.prototype.append.bind(this);
// all that streamy business can't handle numbers
if (typeof value == 'number') {
value = '' + value;
}
// https://github.com/felixge/node-form-data/issues/38
if (util.isArray(value)) {
// Please convert your array into string
// the way web server expects it
this._error(new Error('Arrays are not supported.'));
return;
}
var header = this._multiPartHeader(field, value, options);
var footer = this._multiPartFooter();
append(header);
append(value);
append(footer);
// pass along options.knownLength
this._trackLength(header, value, options);
};
FormData.prototype._trackLength = function(header, value, options) {
var valueLength = 0;
// used w/ getLengthSync(), when length is known.
// e.g. for streaming directly from a remote server,
// w/ a known file a size, and not wanting to wait for
// incoming file to finish to get its size.
if (options.knownLength != null) {
valueLength += +options.knownLength;
} else if (Buffer.isBuffer(value)) {
valueLength = value.length;
} else if (typeof value === 'string') {
valueLength = Buffer.byteLength(value);
}
this._valueLength += valueLength;
// @check why add CRLF? does this account for custom/multiple CRLFs?
this._overheadLength +=
Buffer.byteLength(header) +
FormData.LINE_BREAK.length;
// empty or either doesn't have path or not an http response
if (!value || ( !value.path && !(value.readable && value.hasOwnProperty('httpVersion')) )) {
return;
}
// no need to bother with the length
if (!options.knownLength) {
this._valuesToMeasure.push(value);
}
};
FormData.prototype._lengthRetriever = function(value, callback) {
if (value.hasOwnProperty('fd')) {
// take read range into a account
// `end` = Infinity –> read file till the end
//
// TODO: Looks like there is bug in Node fs.createReadStream
// it doesn't respect `end` options without `start` options
// Fix it when node fixes it.
// https://github.com/joyent/node/issues/7819
if (value.end != undefined && value.end != Infinity && value.start != undefined) {
// when end specified
// no need to calculate range
// inclusive, starts with 0
callback(null, value.end + 1 - (value.start ? value.start : 0));
// not that fast snoopy
} else {
// still need to fetch file size from fs
fs.stat(value.path, function(err, stat) {
var fileSize;
if (err) {
callback(err);
return;
}
// update final size based on the range options
fileSize = stat.size - (value.start ? value.start : 0);
callback(null, fileSize);
});
}
// or http response
} else if (value.hasOwnProperty('httpVersion')) {
callback(null, +value.headers['content-length']);
// or request stream http://github.com/mikeal/request
} else if (value.hasOwnProperty('httpModule')) {
// wait till response come back
value.on('response', function(response) {
value.pause();
callback(null, +response.headers['content-length']);
});
value.resume();
// something else
} else {
callback('Unknown stream');
}
};
FormData.prototype._multiPartHeader = function(field, value, options) {
// custom header specified (as string)?
// it becomes responsible for boundary
// (e.g. to handle extra CRLFs on .NET servers)
if (typeof options.header == 'string') {
return options.header;
}
var contentDisposition = this._getContentDisposition(value, options);
var contentType = this._getContentType(value, options);
var contents = '';
var headers = {
// add custom disposition as third element or keep it two elements if not
'Content-Disposition': ['form-data', 'name="' + field + '"'].concat(contentDisposition || []),
// if no content type. allow it to be empty array
'Content-Type': [].concat(contentType || [])
};
// allow custom headers.
if (typeof options.header == 'object') {
populate(headers, options.header);
}
var header;
for (var prop in headers) {
if (!headers.hasOwnProperty(prop)) continue;
header = headers[prop];
// skip nullish headers.
if (header == null) {
continue;
}
// convert all headers to arrays.
if (!Array.isArray(header)) {
header = [header];
}
// add non-empty headers.
if (header.length) {
contents += prop + ': ' + header.join('; ') + FormData.LINE_BREAK;
}
}
return '--' + this.getBoundary() + FormData.LINE_BREAK + contents + FormData.LINE_BREAK;
};
FormData.prototype._getContentDisposition = function(value, options) {
var filename
, contentDisposition
;
if (typeof options.filepath === 'string') {
// custom filepath for relative paths
filename = path.normalize(options.filepath).replace(/\\/g, '/');
} else if (options.filename || value.name || value.path) {
// custom filename take precedence
// formidable and the browser add a name property
// fs- and request- streams have path property
filename = path.basename(options.filename || value.name || value.path);
} else if (value.readable && value.hasOwnProperty('httpVersion')) {
// or try http response
filename = path.basename(value.client._httpMessage.path || '');
}
if (filename) {
contentDisposition = 'filename="' + filename + '"';
}
return contentDisposition;
};
FormData.prototype._getContentType = function(value, options) {
// use custom content-type above all
var contentType = options.contentType;
// or try `name` from formidable, browser
if (!contentType && value.name) {
contentType = mime.lookup(value.name);
}
// or try `path` from fs-, request- streams
if (!contentType && value.path) {
contentType = mime.lookup(value.path);
}
// or if it's http-reponse
if (!contentType && value.readable && value.hasOwnProperty('httpVersion')) {
contentType = value.headers['content-type'];
}
// or guess it from the filepath or filename
if (!contentType && (options.filepath || options.filename)) {
contentType = mime.lookup(options.filepath || options.filename);
}
// fallback to the default content type if `value` is not simple value
if (!contentType && typeof value == 'object') {
contentType = FormData.DEFAULT_CONTENT_TYPE;
}
return contentType;
};
FormData.prototype._multiPartFooter = function() {
return function(next) {
var footer = FormData.LINE_BREAK;
var lastPart = (this._streams.length === 0);
if (lastPart) {
footer += this._lastBoundary();
}
next(footer);
}.bind(this);
};
FormData.prototype._lastBoundary = function() {
return '--' + this.getBoundary() + '--' + FormData.LINE_BREAK;
};
FormData.prototype.getHeaders = function(userHeaders) {
var header;
var formHeaders = {
'content-type': 'multipart/form-data; boundary=' + this.getBoundary()
};
for (header in userHeaders) {
if (userHeaders.hasOwnProperty(header)) {
formHeaders[header.toLowerCase()] = userHeaders[header];
}
}
return formHeaders;
};
FormData.prototype.setBoundary = function(boundary) {
this._boundary = boundary;
};
FormData.prototype.getBoundary = function() {
if (!this._boundary) {
this._generateBoundary();
}
return this._boundary;
};
FormData.prototype.getBuffer = function() {
var dataBuffer = new Buffer.alloc( 0 );
var boundary = this.getBoundary();
// Create the form content. Add Line breaks to the end of data.
for (var i = 0, len = this._streams.length; i < len; i++) {
if (typeof this._streams[i] !== 'function') {
// Add content to the buffer.
if(Buffer.isBuffer(this._streams[i])) {
dataBuffer = Buffer.concat( [dataBuffer, this._streams[i]]);
}else {
dataBuffer = Buffer.concat( [dataBuffer, Buffer.from(this._streams[i])]);
}
// Add break after content.
if (typeof this._streams[i] !== 'string' || this._streams[i].substring( 2, boundary.length + 2 ) !== boundary) {
dataBuffer = Buffer.concat( [dataBuffer, Buffer.from(FormData.LINE_BREAK)] );
}
}
}
// Add the footer and return the Buffer object.
return Buffer.concat( [dataBuffer, Buffer.from(this._lastBoundary())] );
};
FormData.prototype._generateBoundary = function() {
// This generates a 50 character boundary similar to those used by Firefox.
// They are optimized for boyer-moore parsing.
var boundary = '--------------------------';
for (var i = 0; i < 24; i++) {
boundary += Math.floor(Math.random() * 10).toString(16);
}
this._boundary = boundary;
};
// Note: getLengthSync DOESN'T calculate streams length
// As workaround one can calculate file size manually
// and add it as knownLength option
FormData.prototype.getLengthSync = function() {
var knownLength = this._overheadLength + this._valueLength;
// Don't get confused, there are 3 "internal" streams for each keyval pair
// so it basically checks if there is any value added to the form
if (this._streams.length) {
knownLength += this._lastBoundary().length;
}
// https://github.com/form-data/form-data/issues/40
if (!this.hasKnownLength()) {
// Some async length retrievers are present
// therefore synchronous length calculation is false.
// Please use getLength(callback) to get proper length
this._error(new Error('Cannot calculate proper length in synchronous way.'));
}
return knownLength;
};
// Public API to check if length of added values is known
// https://github.com/form-data/form-data/issues/196
// https://github.com/form-data/form-data/issues/262
FormData.prototype.hasKnownLength = function() {
var hasKnownLength = true;
if (this._valuesToMeasure.length) {
hasKnownLength = false;
}
return hasKnownLength;
};
FormData.prototype.getLength = function(cb) {
var knownLength = this._overheadLength + this._valueLength;
if (this._streams.length) {
knownLength += this._lastBoundary().length;
}
if (!this._valuesToMeasure.length) {
process.nextTick(cb.bind(this, null, knownLength));
return;
}
asynckit.parallel(this._valuesToMeasure, this._lengthRetriever, function(err, values) {
if (err) {
cb(err);
return;
}
values.forEach(function(length) {
knownLength += length;
});
cb(null, knownLength);
});
};
FormData.prototype.submit = function(params, cb) {
var request
, options
, defaults = {method: 'post'}
;
// parse provided url if it's string
// or treat it as options object
if (typeof params == 'string') {
params = parseUrl(params);
options = populate({
port: params.port,
path: params.pathname,
host: params.hostname,
protocol: params.protocol
}, defaults);
// use custom params
} else {
options = populate(params, defaults);
// if no port provided use default one
if (!options.port) {
options.port = options.protocol == 'https:' ? 443 : 80;
}
}
// put that good code in getHeaders to some use
options.headers = this.getHeaders(params.headers);
// https if specified, fallback to http in any other case
if (options.protocol == 'https:') {
request = https.request(options);
} else {
request = http.request(options);
}
// get content length and fire away
this.getLength(function(err, length) {
if (err) {
this._error(err);
return;
}
// add content length
request.setHeader('Content-Length', length);
this.pipe(request);
if (cb) {
var onResponse;
var callback = function (error, responce) {
request.removeListener('error', callback);
request.removeListener('response', onResponse);
return cb.call(this, error, responce);
};
onResponse = callback.bind(this, null);
request.on('error', callback);
request.on('response', onResponse);
}
}.bind(this));
return request;
};
FormData.prototype._error = function(err) {
if (!this.error) {
this.error = err;
this.pause();
this.emit('error', err);
}
};
FormData.prototype.toString = function () {
return '[object FormData]';
};
@@ -0,0 +1,10 @@
// populates missing values
module.exports = function(dst, src) {
Object.keys(src).forEach(function(prop)
{
dst[prop] = dst[prop] || src[prop];
});
return dst;
};
+105
View File
@@ -0,0 +1,105 @@
{
"_args": [
[
"form-data@3.0.1",
"/home/pi/Desktop/smartMirrorTestEnvironment/MagicMirror"
]
],
"_development": true,
"_from": "form-data@3.0.1",
"_id": "form-data@3.0.1",
"_inBundle": false,
"_integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
"_location": "/jest-environment-jsdom/form-data",
"_phantomChildren": {},
"_requested": {
"type": "version",
"registry": true,
"raw": "form-data@3.0.1",
"name": "form-data",
"escapedName": "form-data",
"rawSpec": "3.0.1",
"saveSpec": null,
"fetchSpec": "3.0.1"
},
"_requiredBy": [
"/jest-environment-jsdom/jsdom"
],
"_resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"_spec": "3.0.1",
"_where": "/home/pi/Desktop/smartMirrorTestEnvironment/MagicMirror",
"author": {
"name": "Felix Geisendörfer",
"email": "felix@debuggable.com",
"url": "http://debuggable.com/"
},
"browser": "./lib/browser",
"bugs": {
"url": "https://github.com/form-data/form-data/issues"
},
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"description": "A library to create readable \"multipart/form-data\" streams. Can be used to submit forms and file uploads to other web applications.",
"devDependencies": {
"@types/node": "^12.0.10",
"browserify": "^13.1.1",
"browserify-istanbul": "^2.0.0",
"coveralls": "^3.0.4",
"cross-spawn": "^6.0.5",
"eslint": "^6.0.1",
"fake": "^0.2.2",
"far": "^0.0.7",
"formidable": "^1.0.17",
"in-publish": "^2.0.0",
"is-node-modern": "^1.0.0",
"istanbul": "^0.4.5",
"obake": "^0.1.2",
"pkgfiles": "^2.3.0",
"pre-commit": "^1.1.3",
"puppeteer": "^1.19.0",
"request": "^2.88.0",
"rimraf": "^2.7.1",
"tape": "^4.6.2",
"typescript": "^3.5.2"
},
"engines": {
"node": ">= 6"
},
"homepage": "https://github.com/form-data/form-data#readme",
"license": "MIT",
"main": "./lib/form_data",
"name": "form-data",
"pre-commit": [
"lint",
"ci-test",
"check"
],
"repository": {
"type": "git",
"url": "git://github.com/form-data/form-data.git"
},
"scripts": {
"browser": "browserify -t browserify-istanbul test/run-browser.js | obake --coverage",
"check": "istanbul check-coverage coverage/coverage*.json",
"ci-lint": "is-node-modern 8 && npm run lint || is-node-not-modern 8",
"ci-test": "npm run test && npm run browser && npm run report",
"debug": "verbose=1 ./test/run.js",
"files": "pkgfiles --sort=name",
"get-version": "node -e \"console.log(require('./package.json').version)\"",
"lint": "eslint lib/*.js test/*.js test/integration/*.js",
"postpublish": "npm run restore-readme",
"posttest": "istanbul report lcov text",
"predebug": "rimraf coverage test/tmp",
"prepublish": "in-publish && npm run update-readme || not-in-publish",
"pretest": "rimraf coverage test/tmp",
"report": "istanbul report lcov text",
"restore-readme": "mv README.md.bak README.md",
"test": "istanbul cover test/run.js",
"update-readme": "sed -i.bak 's/\\/master\\.svg/\\/v'$(npm --silent run get-version)'.svg/g' README.md"
},
"typings": "./index.d.ts",
"version": "3.0.1"
}
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2010 Elijah Insua
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
+522
View File
@@ -0,0 +1,522 @@
<h1 align="center">
<img width="100" height="100" src="logo.svg" alt=""><br>
jsdom
</h1>
jsdom is a pure-JavaScript implementation of many web standards, notably the WHATWG [DOM](https://dom.spec.whatwg.org/) and [HTML](https://html.spec.whatwg.org/multipage/) Standards, for use with Node.js. In general, the goal of the project is to emulate enough of a subset of a web browser to be useful for testing and scraping real-world web applications.
The latest versions of jsdom require Node.js v10 or newer. (Versions of jsdom below v16 still work with previous Node.js versions, but are unsupported.)
## Basic usage
```js
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
```
To use jsdom, you will primarily use the `JSDOM` constructor, which is a named export of the jsdom main module. Pass the constructor a string. You will get back a `JSDOM` object, which has a number of useful properties, notably `window`:
```js
const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
console.log(dom.window.document.querySelector("p").textContent); // "Hello world"
```
(Note that jsdom will parse the HTML you pass it just like a browser does, including implied `<html>`, `<head>`, and `<body>` tags.)
The resulting object is an instance of the `JSDOM` class, which contains a number of useful properties and methods besides `window`. In general, it can be used to act on the jsdom from the "outside," doing things that are not possible with the normal DOM APIs. For simple cases, where you don't need any of this functionality, we recommend a coding pattern like
```js
const { window } = new JSDOM(`...`);
// or even
const { document } = (new JSDOM(`...`)).window;
```
Full documentation on everything you can do with the `JSDOM` class is below, in the section "`JSDOM` Object API".
## Customizing jsdom
The `JSDOM` constructor accepts a second parameter which can be used to customize your jsdom in the following ways.
### Simple options
```js
const dom = new JSDOM(``, {
url: "https://example.org/",
referrer: "https://example.com/",
contentType: "text/html",
includeNodeLocations: true,
storageQuota: 10000000
});
```
- `url` sets the value returned by `window.location`, `document.URL`, and `document.documentURI`, and affects things like resolution of relative URLs within the document and the same-origin restrictions and referrer used while fetching subresources. It defaults to `"about:blank"`.
- `referrer` just affects the value read from `document.referrer`. It defaults to no referrer (which reflects as the empty string).
- `contentType` affects the value read from `document.contentType`, as well as how the document is parsed: as HTML or as XML. Values that are not a [HTML mime type](https://mimesniff.spec.whatwg.org/#html-mime-type) or an [XML mime type](https://mimesniff.spec.whatwg.org/#xml-mime-type) will throw. It defaults to `"text/html"`. If a `charset` parameter is present, it can affect [binary data processing](#encoding-sniffing).
- `includeNodeLocations` preserves the location info produced by the HTML parser, allowing you to retrieve it with the `nodeLocation()` method (described below). It also ensures that line numbers reported in exception stack traces for code running inside `<script>` elements are correct. It defaults to `false` to give the best performance, and cannot be used with an XML content type since our XML parser does not support location info.
- `storageQuota` is the maximum size in code units for the separate storage areas used by `localStorage` and `sessionStorage`. Attempts to store data larger than this limit will cause a `DOMException` to be thrown. By default, it is set to 5,000,000 code units per origin, as inspired by the HTML specification.
Note that both `url` and `referrer` are canonicalized before they're used, so e.g. if you pass in `"https:example.com"`, jsdom will interpret that as if you had given `"https://example.com/"`. If you pass an unparseable URL, the call will throw. (URLs are parsed and serialized according to the [URL Standard](https://url.spec.whatwg.org/).)
### Executing scripts
jsdom's most powerful ability is that it can execute scripts inside the jsdom. These scripts can modify the content of the page and access all the web platform APIs jsdom implements.
However, this is also highly dangerous when dealing with untrusted content. The jsdom sandbox is not foolproof, and code running inside the DOM's `<script>`s can, if it tries hard enough, get access to the Node.js environment, and thus to your machine. As such, the ability to execute scripts embedded in the HTML is disabled by default:
```js
const dom = new JSDOM(`<body>
<script>document.body.appendChild(document.createElement("hr"));</script>
</body>`);
// The script will not be executed, by default:
dom.window.document.body.children.length === 1;
```
To enable executing scripts inside the page, you can use the `runScripts: "dangerously"` option:
```js
const dom = new JSDOM(`<body>
<script>document.body.appendChild(document.createElement("hr"));</script>
</body>`, { runScripts: "dangerously" });
// The script will be executed and modify the DOM:
dom.window.document.body.children.length === 2;
```
Again we emphasize to only use this when feeding jsdom code you know is safe. If you use it on arbitrary user-supplied code, or code from the Internet, you are effectively running untrusted Node.js code, and your machine could be compromised.
If you want to execute _external_ scripts, included via `<script src="">`, you'll also need to ensure that they load them. To do this, add the option `resources: "usable"` [as described below](#loading-subresources). (You'll likely also want to set the `url` option, for the reasons discussed there.)
Event handler attributes, like `<div onclick="">`, are also governed by this setting; they will not function unless `runScripts` is set to `"dangerously"`. (However, event handler _properties_, like `div.onclick = ...`, will function regardless of `runScripts`.)
If you are simply trying to execute script "from the outside", instead of letting `<script>` elements and event handlers attributes run "from the inside", you can use the `runScripts: "outside-only"` option, which enables fresh copies of all the JavaScript spec-provided globals to be installed on `window`. This includes things like `window.Array`, `window.Promise`, etc. It also, notably, includes `window.eval`, which allows running scripts, but with the jsdom `window` as the global:
```js
const { window } = new JSDOM(``, { runScripts: "outside-only" });
window.eval(`document.body.innerHTML = "<p>Hello, world!</p>";`);
window.document.body.children.length === 1;
```
This is turned off by default for performance reasons, but is safe to enable.
(Note that in the default configuration, without setting `runScripts`, the values of `window.Array`, `window.eval`, etc. will be the same as those provided by the outer Node.js environment. That is, `window.eval === eval` will hold, so `window.eval` will not run scripts in a useful way.)
We strongly advise against trying to "execute scripts" by mashing together the jsdom and Node global environments (e.g. by doing `global.window = dom.window`), and then executing scripts or test code inside the Node global environment. Instead, you should treat jsdom like you would a browser, and run all scripts and tests that need access to a DOM inside the jsdom environment, using `window.eval` or `runScripts: "dangerously"`. This might require, for example, creating a browserify bundle to execute as a `<script>` element—just like you would in a browser.
Finally, for advanced use cases you can use the `dom.getInternalVMContext()` method, documented below.
### Pretending to be a visual browser
jsdom does not have the capability to render visual content, and will act like a headless browser by default. It provides hints to web pages through APIs such as `document.hidden` that their content is not visible.
When the `pretendToBeVisual` option is set to `true`, jsdom will pretend that it is rendering and displaying content. It does this by:
* Changing `document.hidden` to return `false` instead of `true`
* Changing `document.visibilityState` to return `"visible"` instead of `"prerender"`
* Enabling `window.requestAnimationFrame()` and `window.cancelAnimationFrame()` methods, which otherwise do not exist
```js
const window = (new JSDOM(``, { pretendToBeVisual: true })).window;
window.requestAnimationFrame(timestamp => {
console.log(timestamp > 0);
});
```
Note that jsdom still [does not do any layout or rendering](#unimplemented-parts-of-the-web-platform), so this is really just about _pretending_ to be visual, not about implementing the parts of the platform a real, visual web browser would implement.
### Loading subresources
#### Basic options
By default, jsdom will not load any subresources such as scripts, stylesheets, images, or iframes. If you'd like jsdom to load such resources, you can pass the `resources: "usable"` option, which will load all usable resources. Those are:
* Frames and iframes, via `<frame>` and `<iframe>`
* Stylesheets, via `<link rel="stylesheet">`
* Scripts, via `<script>`, but only if `runScripts: "dangerously"` is also set
* Images, via `<img>`, but only if the `canvas` npm package is also installed (see "[Canvas Support](#canvas-support)" below)
When attempting to load resources, recall that the default value for the `url` option is `"about:blank"`, which means that any resources included via relative URLs will fail to load. (The result of trying to parse the URL `/something` against the URL `about:blank` is an error.) So, you'll likely want to set a non-default value for the `url` option in those cases, or use one of the [convenience APIs](#convenience-apis) that do so automatically.
#### Advanced configuration
_This resource loader system is new as of jsdom v12.0.0, and we'd love your feedback on whether it meets your needs and how easy it is to use. Please file an issue to discuss!_
To more fully customize jsdom's resource-loading behavior, you can pass an instance of the `ResourceLoader` class as the `resources` option value:
```js
const resourceLoader = new jsdom.ResourceLoader({
proxy: "http://127.0.0.1:9001",
strictSSL: false,
userAgent: "Mellblomenator/9000",
});
const dom = new JSDOM(``, { resources: resourceLoader });
```
The three options to the `ResourceLoader` constructor are:
- `proxy` is the address of an HTTP proxy to be used.
- `strictSSL` can be set to false to disable the requirement that SSL certificates be valid.
- `userAgent` affects the `User-Agent` header sent, and thus the resulting value for `navigator.userAgent`. It defaults to <code>\`Mozilla/5.0 (${process.platform || "unknown OS"}) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/${jsdomVersion}\`</code>.
You can further customize resource fetching by subclassing `ResourceLoader` and overriding the `fetch()` method. For example, here is a version that only returns results for requests to a trusted origin:
```js
class CustomResourceLoader extends jsdom.ResourceLoader {
fetch(url, options) {
// Override the contents of this script to do something unusual.
if (url === "https://example.com/some-specific-script.js") {
return Promise.resolve(Buffer.from("window.someGlobal = 5;"));
}
return super.fetch(url, options);
}
}
```
jsdom will call your custom resource loader's `fetch()` method whenever it encounters a "usable" resource, per the above section. The method takes a URL string, as well as a few options which you should pass through unmodified if calling `super.fetch()`. It must return a promise for a Node.js `Buffer` object, or return `null` if the resource is intentionally not to be loaded. In general, most cases will want to delegate to `super.fetch()`, as shown.
One of the options you will receive in `fetch()` will be the element (if applicable) that is fetching a resource.
```js
class CustomResourceLoader extends jsdom.ResourceLoader {
fetch(url, options) {
if (options.element) {
console.log(`Element ${options.element.localName} is requesting the url ${url}`);
}
return super.fetch(url, options);
}
}
```
### Virtual consoles
Like web browsers, jsdom has the concept of a "console". This records both information directly sent from the page, via scripts executing inside the document, as well as information from the jsdom implementation itself. We call the user-controllable console a "virtual console", to distinguish it from the Node.js `console` API and from the inside-the-page `window.console` API.
By default, the `JSDOM` constructor will return an instance with a virtual console that forwards all its output to the Node.js console. To create your own virtual console and pass it to jsdom, you can override this default by doing
```js
const virtualConsole = new jsdom.VirtualConsole();
const dom = new JSDOM(``, { virtualConsole });
```
Code like this will create a virtual console with no behavior. You can give it behavior by adding event listeners for all the possible console methods:
```js
virtualConsole.on("error", () => { ... });
virtualConsole.on("warn", () => { ... });
virtualConsole.on("info", () => { ... });
virtualConsole.on("dir", () => { ... });
// ... etc. See https://console.spec.whatwg.org/#logging
```
(Note that it is probably best to set up these event listeners *before* calling `new JSDOM()`, since errors or console-invoking script might occur during parsing.)
If you simply want to redirect the virtual console output to another console, like the default Node.js one, you can do
```js
virtualConsole.sendTo(console);
```
There is also a special event, `"jsdomError"`, which will fire with error objects to report errors from jsdom itself. This is similar to how error messages often show up in web browser consoles, even if they are not initiated by `console.error`. So far, the following errors are output this way:
- Errors loading or parsing subresources (scripts, stylesheets, frames, and iframes)
- Script execution errors that are not handled by a window `onerror` event handler that returns `true` or calls `event.preventDefault()`
- Not-implemented errors resulting from calls to methods, like `window.alert`, which jsdom does not implement, but installs anyway for web compatibility
If you're using `sendTo(c)` to send errors to `c`, by default it will call `c.error(errorStack[, errorDetail])` with information from `"jsdomError"` events. If you'd prefer to maintain a strict one-to-one mapping of events to method calls, and perhaps handle `"jsdomError"`s yourself, then you can do
```js
virtualConsole.sendTo(c, { omitJSDOMErrors: true });
```
### Cookie jars
Like web browsers, jsdom has the concept of a cookie jar, storing HTTP cookies. Cookies that have a URL on the same domain as the document, and are not marked HTTP-only, are accessible via the `document.cookie` API. Additionally, all cookies in the cookie jar will impact the fetching of subresources.
By default, the `JSDOM` constructor will return an instance with an empty cookie jar. To create your own cookie jar and pass it to jsdom, you can override this default by doing
```js
const cookieJar = new jsdom.CookieJar(store, options);
const dom = new JSDOM(``, { cookieJar });
```
This is mostly useful if you want to share the same cookie jar among multiple jsdoms, or prime the cookie jar with certain values ahead of time.
Cookie jars are provided by the [tough-cookie](https://www.npmjs.com/package/tough-cookie) package. The `jsdom.CookieJar` constructor is a subclass of the tough-cookie cookie jar which by default sets the `looseMode: true` option, since that [matches better how browsers behave](https://github.com/whatwg/html/issues/804). If you want to use tough-cookie's utilities and classes yourself, you can use the `jsdom.toughCookie` module export to get access to the tough-cookie module instance packaged with jsdom.
### Intervening before parsing
jsdom allows you to intervene in the creation of a jsdom very early: after the `Window` and `Document` objects are created, but before any HTML is parsed to populate the document with nodes:
```js
const dom = new JSDOM(`<p>Hello</p>`, {
beforeParse(window) {
window.document.childNodes.length === 0;
window.someCoolAPI = () => { /* ... */ };
}
});
```
This is especially useful if you are wanting to modify the environment in some way, for example adding shims for web platform APIs jsdom does not support.
## `JSDOM` object API
Once you have constructed a `JSDOM` object, it will have the following useful capabilities:
### Properties
The property `window` retrieves the `Window` object that was created for you.
The properties `virtualConsole` and `cookieJar` reflect the options you pass in, or the defaults created for you if nothing was passed in for those options.
### Serializing the document with `serialize()`
The `serialize()` method will return the [HTML serialization](https://html.spec.whatwg.org/#html-fragment-serialisation-algorithm) of the document, including the doctype:
```js
const dom = new JSDOM(`<!DOCTYPE html>hello`);
dom.serialize() === "<!DOCTYPE html><html><head></head><body>hello</body></html>";
// Contrast with:
dom.window.document.documentElement.outerHTML === "<html><head></head><body>hello</body></html>";
```
### Getting the source location of a node with `nodeLocation(node)`
The `nodeLocation()` method will find where a DOM node is within the source document, returning the [parse5 location info](https://www.npmjs.com/package/parse5#options-locationinfo) for the node:
```js
const dom = new JSDOM(
`<p>Hello
<img src="foo.jpg">
</p>`,
{ includeNodeLocations: true }
);
const document = dom.window.document;
const bodyEl = document.body; // implicitly created
const pEl = document.querySelector("p");
const textNode = pEl.firstChild;
const imgEl = document.querySelector("img");
console.log(dom.nodeLocation(bodyEl)); // null; it's not in the source
console.log(dom.nodeLocation(pEl)); // { startOffset: 0, endOffset: 39, startTag: ..., endTag: ... }
console.log(dom.nodeLocation(textNode)); // { startOffset: 3, endOffset: 13 }
console.log(dom.nodeLocation(imgEl)); // { startOffset: 13, endOffset: 32 }
```
Note that this feature only works if you have set the `includeNodeLocations` option; node locations are off by default for performance reasons.
### Interfacing with the Node.js `vm` module using `getInternalVMContext()`
The built-in [`vm`](https://nodejs.org/api/vm.html) module of Node.js is what underpins jsdom's script-running magic. Some advanced use cases, like pre-compiling a script and then running it multiple times, benefit from using the `vm` module directly with a jsdom-created `Window`.
To get access to the [contextified global object](https://nodejs.org/api/vm.html#vm_what_does_it_mean_to_contextify_an_object), suitable for use with the `vm` APIs, you can use the `getInternalVMContext()` method:
```js
const { Script } = require("vm");
const dom = new JSDOM(``, { runScripts: "outside-only" });
const script = new Script(`
if (!this.ran) {
this.ran = 0;
}
++this.ran;
`);
const vmContext = dom.getInternalVMContext();
script.runInContext(vmContext);
script.runInContext(vmContext);
script.runInContext(vmContext);
console.assert(dom.window.ran === 3);
```
This is somewhat-advanced functionality, and we advise sticking to normal DOM APIs (such as `window.eval()` or `document.createElement("script")`) unless you have very specific needs.
Note that this method will throw an exception if the `JSDOM` instance was created without `runScripts` set, or if you are [using jsdom in a web browser](#running-jsdom-inside-a-web-browser).
### Reconfiguring the jsdom with `reconfigure(settings)`
The `top` property on `window` is marked `[Unforgeable]` in the spec, meaning it is a non-configurable own property and thus cannot be overridden or shadowed by normal code running inside the jsdom, even using `Object.defineProperty`.
Similarly, at present jsdom does not handle navigation (such as setting `window.location.href = "https://example.com/"`); doing so will cause the virtual console to emit a `"jsdomError"` explaining that this feature is not implemented, and nothing will change: there will be no new `Window` or `Document` object, and the existing `window`'s `location` object will still have all the same property values.
However, if you're acting from outside the window, e.g. in some test framework that creates jsdoms, you can override one or both of these using the special `reconfigure()` method:
```js
const dom = new JSDOM();
dom.window.top === dom.window;
dom.window.location.href === "about:blank";
dom.reconfigure({ windowTop: myFakeTopForTesting, url: "https://example.com/" });
dom.window.top === myFakeTopForTesting;
dom.window.location.href === "https://example.com/";
```
Note that changing the jsdom's URL will impact all APIs that return the current document URL, such as `window.location`, `document.URL`, and `document.documentURI`, as well as the resolution of relative URLs within the document, and the same-origin checks and referrer used while fetching subresources. It will not, however, perform navigation to the contents of that URL; the contents of the DOM will remain unchanged, and no new instances of `Window`, `Document`, etc. will be created.
## Convenience APIs
### `fromURL()`
In addition to the `JSDOM` constructor itself, jsdom provides a promise-returning factory method for constructing a jsdom from a URL:
```js
JSDOM.fromURL("https://example.com/", options).then(dom => {
console.log(dom.serialize());
});
```
The returned promise will fulfill with a `JSDOM` instance if the URL is valid and the request is successful. Any redirects will be followed to their ultimate destination.
The options provided to `fromURL()` are similar to those provided to the `JSDOM` constructor, with the following additional restrictions and consequences:
- The `url` and `contentType` options cannot be provided.
- The `referrer` option is used as the HTTP `Referer` request header of the initial request.
- The `resources` option also affects the initial request; this is useful if you want to, for example, configure a proxy (see above).
- The resulting jsdom's URL, content type, and referrer are determined from the response.
- Any cookies set via HTTP `Set-Cookie` response headers are stored in the jsdom's cookie jar. Similarly, any cookies already in a supplied cookie jar are sent as HTTP `Cookie` request headers.
### `fromFile()`
Similar to `fromURL()`, jsdom also provides a `fromFile()` factory method for constructing a jsdom from a filename:
```js
JSDOM.fromFile("stuff.html", options).then(dom => {
console.log(dom.serialize());
});
```
The returned promise will fulfill with a `JSDOM` instance if the given file can be opened. As usual in Node.js APIs, the filename is given relative to the current working directory.
The options provided to `fromFile()` are similar to those provided to the `JSDOM` constructor, with the following additional defaults:
- The `url` option will default to a file URL corresponding to the given filename, instead of to `"about:blank"`.
- The `contentType` option will default to `"application/xhtml+xml"` if the given filename ends in `.xht`, `.xhtml`, or `.xml`; otherwise it will continue to default to `"text/html"`.
### `fragment()`
For the very simplest of cases, you might not need a whole `JSDOM` instance with all its associated power. You might not even need a `Window` or `Document`! Instead, you just need to parse some HTML, and get a DOM object you can manipulate. For that, we have `fragment()`, which creates a `DocumentFragment` from a given string:
```js
const frag = JSDOM.fragment(`<p>Hello</p><p><strong>Hi!</strong>`);
frag.childNodes.length === 2;
frag.querySelector("strong").textContent === "Hi!";
// etc.
```
Here `frag` is a [`DocumentFragment`](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment) instance, whose contents are created by parsing the provided string. The parsing is done using a `<template>` element, so you can include any element there (including ones with weird parsing rules like `<td>`). It's also important to note that the resulting `DocumentFragment` will not have [an associated browsing context](https://html.spec.whatwg.org/multipage/#concept-document-bc): that is, elements' `ownerDocument` will have a null `defaultView` property, resources will not load, etc.
All invocations of the `fragment()` factory result in `DocumentFragment`s that share the same template owner `Document`. This allows many calls to `fragment()` with no extra overhead. But it also means that calls to `fragment()` cannot be customized with any options.
Note that serialization is not as easy with `DocumentFragment`s as it is with full `JSDOM` objects. If you need to serialize your DOM, you should probably use the `JSDOM` constructor more directly. But for the special case of a fragment containing a single element, it's pretty easy to do through normal means:
```js
const frag = JSDOM.fragment(`<p>Hello</p>`);
console.log(frag.firstChild.outerHTML); // logs "<p>Hello</p>"
```
## Other noteworthy features
### Canvas support
jsdom includes support for using the [`canvas`](https://www.npmjs.com/package/canvas) package to extend any `<canvas>` elements with the canvas API. To make this work, you need to include `canvas` as a dependency in your project, as a peer of `jsdom`. If jsdom can find the `canvas` package, it will use it, but if it's not present, then `<canvas>` elements will behave like `<div>`s. Since jsdom v13, version 2.x of `canvas` is required; version 1.x is no longer supported.
### Encoding sniffing
In addition to supplying a string, the `JSDOM` constructor can also be supplied binary data, in the form of a Node.js [`Buffer`](https://nodejs.org/docs/latest/api/buffer.html) or a standard JavaScript binary data type like `ArrayBuffer`, `Uint8Array`, `DataView`, etc. When this is done, jsdom will [sniff the encoding](https://html.spec.whatwg.org/multipage/syntax.html#encoding-sniffing-algorithm) from the supplied bytes, scanning for `<meta charset>` tags just like a browser does.
If the supplied `contentType` option contains a `charset` parameter, that encoding will override the sniffed encoding—unless a UTF-8 or UTF-16 BOM is present, in which case those take precedence. (Again, this is just like a browser.)
This encoding sniffing also applies to `JSDOM.fromFile()` and `JSDOM.fromURL()`. In the latter case, any `Content-Type` headers sent with the response will take priority, in the same fashion as the constructor's `contentType` option.
Note that in many cases supplying bytes in this fashion can be better than supplying a string. For example, if you attempt to use Node.js's `buffer.toString("utf-8")` API, Node.js will not strip any leading BOMs. If you then give this string to jsdom, it will interpret it verbatim, leaving the BOM intact. But jsdom's binary data decoding code will strip leading BOMs, just like a browser; in such cases, supplying `buffer` directly will give the desired result.
### Closing down a jsdom
Timers in the jsdom (set by `window.setTimeout()` or `window.setInterval()`) will, by definition, execute code in the future in the context of the window. Since there is no way to execute code in the future without keeping the process alive, outstanding jsdom timers will keep your Node.js process alive. Similarly, since there is no way to execute code in the context of an object without keeping that object alive, outstanding jsdom timers will prevent garbage collection of the window on which they are scheduled.
If you want to be sure to shut down a jsdom window, use `window.close()`, which will terminate all running timers (and also remove any event listeners on the window and document).
### Running jsdom inside a web browser
jsdom has some support for being run inside a web browser, using [browserify](https://browserify.org/). That is, inside a web browser, you can use a browserified jsdom to create an entirely self-contained set of plain JavaScript objects which look and act much like the browser's existing DOM objects, while being entirely independent of them. "Virtual DOM", indeed!
jsdom's primary target is still Node.js, and so we use language features that are only present in recent Node.js versions (namely, Node.js v8+). Thus, older browsers will likely not work. (Even transpilation will not help: we use `Proxy`s extensively throughout the jsdom codebase.)
Notably, jsdom works well inside a web worker. The original contributor, [@lawnsea](https://github.com/lawnsea/), who made this possible, has [published a paper](https://pdfs.semanticscholar.org/47f0/6bb6607a975500a30e9e52d7c9fbc0034e27.pdf) about his project which uses this capability.
Not everything works perfectly when running jsdom inside a web browser. Sometimes that is because of fundamental limitations (such as not having filesystem access), but sometimes it is simply because we haven't spent enough time making the appropriate small tweaks. Bug reports are certainly welcome.
### Debugging the DOM using Chrome Devtools
As of Node.js v6 you can debug programs using Chrome Devtools. See the [official documentation](https://nodejs.org/en/docs/inspector/) for how to get started.
By default jsdom elements are formatted as plain old JS objects in the console. To make it easier to debug, you can use [jsdom-devtools-formatter](https://github.com/viddo/jsdom-devtools-formatter), which lets you inspect them like real DOM elements.
## Caveats
### Asynchronous script loading
People often have trouble with asynchronous script loading when using jsdom. Many pages load scripts asynchronously, but there is no way to tell when they're done doing so, and thus when it's a good time to run your code and inspect the resulting DOM structure. This is a fundamental limitation; we cannot predict what scripts on the web page will do, and so cannot tell you when they are done loading more scripts.
This can be worked around in a few ways. The best way, if you control the page in question, is to use whatever mechanisms are given by the script loader to detect when loading is done. For example, if you're using a module loader like RequireJS, the code could look like:
```js
// On the Node.js side:
const window = (new JSDOM(...)).window;
window.onModulesLoaded = () => {
console.log("ready to roll!");
};
```
```html
<!-- Inside the HTML you supply to jsdom -->
<script>
requirejs(["entry-module"], () => {
window.onModulesLoaded();
});
</script>
```
If you do not control the page, you could try workarounds such as polling for the presence of a specific element.
For more details, see the discussion in [#640](https://github.com/jsdom/jsdom/issues/640), especially [@matthewkastor](https://github.com/matthewkastor)'s [insightful comment](https://github.com/jsdom/jsdom/issues/640#issuecomment-22216965).
### Unimplemented parts of the web platform
Although we enjoy adding new features to jsdom and keeping it up to date with the latest web specs, it has many missing APIs. Please feel free to file an issue for anything missing, but we're a small and busy team, so a pull request might work even better.
Beyond just features that we haven't gotten to yet, there are two major features that are currently outside the scope of jsdom. These are:
- **Navigation**: the ability to change the global object, and all other objects, when clicking a link or assigning `location.href` or similar.
- **Layout**: the ability to calculate where elements will be visually laid out as a result of CSS, which impacts methods like `getBoundingClientRects()` or properties like `offsetTop`.
Currently jsdom has dummy behaviors for some aspects of these features, such as sending a "not implemented" `"jsdomError"` to the virtual console for navigation, or returning zeros for many layout-related properties. Often you can work around these limitations in your code, e.g. by creating new `JSDOM` instances for each page you "navigate" to during a crawl, or using `Object.defineProperty()` to change what various layout-related getters and methods return.
Note that other tools in the same space, such as PhantomJS, do support these features. On the wiki, we have a more complete writeup about [jsdom vs. PhantomJS](https://github.com/jsdom/jsdom/wiki/jsdom-vs.-PhantomJS).
## Supporting jsdom
jsdom is a community-driven project maintained by a team of [volunteers](https://github.com/orgs/jsdom/people). You could support jsdom by:
- [Getting professional support for jsdom](https://tidelift.com/subscription/pkg/npm-jsdom?utm_source=npm-jsdom&utm_medium=referral&utm_campaign=readme) as part of a Tidelift subscription. Tidelift helps making open source sustainable for us while giving teams assurances for maintenance, licensing, and security.
- [Contributing](https://github.com/jsdom/jsdom/blob/master/Contributing.md) directly to the project.
## Getting help
If you need help with jsdom, please feel free to use any of the following venues:
- The [mailing list](https://groups.google.com/group/jsdom) (best for "how do I" questions)
- The [issue tracker](https://github.com/jsdom/jsdom/issues) (best for bug reports)
- The Matrix room: [#jsdom:matrix.org](https://matrix.to/#/#jsdom:matrix.org)
+333
View File
@@ -0,0 +1,333 @@
"use strict";
const path = require("path");
const fs = require("fs").promises;
const vm = require("vm");
const toughCookie = require("tough-cookie");
const sniffHTMLEncoding = require("html-encoding-sniffer");
const whatwgURL = require("whatwg-url");
const whatwgEncoding = require("whatwg-encoding");
const { URL } = require("whatwg-url");
const MIMEType = require("whatwg-mimetype");
const idlUtils = require("./jsdom/living/generated/utils.js");
const VirtualConsole = require("./jsdom/virtual-console.js");
const { createWindow } = require("./jsdom/browser/Window.js");
const { parseIntoDocument } = require("./jsdom/browser/parser");
const { fragmentSerialization } = require("./jsdom/living/domparsing/serialization.js");
const ResourceLoader = require("./jsdom/browser/resources/resource-loader.js");
const NoOpResourceLoader = require("./jsdom/browser/resources/no-op-resource-loader.js");
class CookieJar extends toughCookie.CookieJar {
constructor(store, options) {
// jsdom cookie jars must be loose by default
super(store, { looseMode: true, ...options });
}
}
const window = Symbol("window");
let sharedFragmentDocument = null;
class JSDOM {
constructor(input = "", options = {}) {
const mimeType = new MIMEType(options.contentType === undefined ? "text/html" : options.contentType);
const { html, encoding } = normalizeHTML(input, mimeType);
options = transformOptions(options, encoding, mimeType);
this[window] = createWindow(options.windowOptions);
const documentImpl = idlUtils.implForWrapper(this[window]._document);
options.beforeParse(this[window]._globalProxy);
parseIntoDocument(html, documentImpl);
documentImpl.close();
}
get window() {
// It's important to grab the global proxy, instead of just the result of `createWindow(...)`, since otherwise
// things like `window.eval` don't exist.
return this[window]._globalProxy;
}
get virtualConsole() {
return this[window]._virtualConsole;
}
get cookieJar() {
// TODO NEWAPI move _cookieJar to window probably
return idlUtils.implForWrapper(this[window]._document)._cookieJar;
}
serialize() {
return fragmentSerialization(idlUtils.implForWrapper(this[window]._document), { requireWellFormed: false });
}
nodeLocation(node) {
if (!idlUtils.implForWrapper(this[window]._document)._parseOptions.sourceCodeLocationInfo) {
throw new Error("Location information was not saved for this jsdom. Use includeNodeLocations during creation.");
}
return idlUtils.implForWrapper(node).sourceCodeLocation;
}
getInternalVMContext() {
if (!vm.isContext(this[window])) {
throw new TypeError("This jsdom was not configured to allow script running. " +
"Use the runScripts option during creation.");
}
return this[window];
}
reconfigure(settings) {
if ("windowTop" in settings) {
this[window]._top = settings.windowTop;
}
if ("url" in settings) {
const document = idlUtils.implForWrapper(this[window]._document);
const url = whatwgURL.parseURL(settings.url);
if (url === null) {
throw new TypeError(`Could not parse "${settings.url}" as a URL`);
}
document._URL = url;
document._origin = whatwgURL.serializeURLOrigin(document._URL);
}
}
static fragment(string = "") {
if (!sharedFragmentDocument) {
sharedFragmentDocument = (new JSDOM()).window.document;
}
const template = sharedFragmentDocument.createElement("template");
template.innerHTML = string;
return template.content;
}
static fromURL(url, options = {}) {
return Promise.resolve().then(() => {
// Remove the hash while sending this through the research loader fetch().
// It gets added back a few lines down when constructing the JSDOM object.
const parsedURL = new URL(url);
const originalHash = parsedURL.hash;
parsedURL.hash = "";
url = parsedURL.href;
options = normalizeFromURLOptions(options);
const resourceLoader = resourcesToResourceLoader(options.resources);
const resourceLoaderForInitialRequest = resourceLoader.constructor === NoOpResourceLoader ?
new ResourceLoader() :
resourceLoader;
const req = resourceLoaderForInitialRequest.fetch(url, {
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
cookieJar: options.cookieJar,
referrer: options.referrer
});
return req.then(body => {
const res = req.response;
options = Object.assign(options, {
url: req.href + originalHash,
contentType: res.headers["content-type"],
referrer: req.getHeader("referer")
});
return new JSDOM(body, options);
});
});
}
static async fromFile(filename, options = {}) {
options = normalizeFromFileOptions(filename, options);
const buffer = await fs.readFile(filename);
return new JSDOM(buffer, options);
}
}
function normalizeFromURLOptions(options) {
// Checks on options that are invalid for `fromURL`
if (options.url !== undefined) {
throw new TypeError("Cannot supply a url option when using fromURL");
}
if (options.contentType !== undefined) {
throw new TypeError("Cannot supply a contentType option when using fromURL");
}
// Normalization of options which must be done before the rest of the fromURL code can use them, because they are
// given to request()
const normalized = { ...options };
if (options.referrer !== undefined) {
normalized.referrer = (new URL(options.referrer)).href;
}
if (options.cookieJar === undefined) {
normalized.cookieJar = new CookieJar();
}
return normalized;
// All other options don't need to be processed yet, and can be taken care of in the normal course of things when
// `fromURL` calls `new JSDOM(html, options)`.
}
function normalizeFromFileOptions(filename, options) {
const normalized = { ...options };
if (normalized.contentType === undefined) {
const extname = path.extname(filename);
if (extname === ".xhtml" || extname === ".xht" || extname === ".xml") {
normalized.contentType = "application/xhtml+xml";
}
}
if (normalized.url === undefined) {
normalized.url = new URL("file:" + path.resolve(filename));
}
return normalized;
}
function transformOptions(options, encoding, mimeType) {
const transformed = {
windowOptions: {
// Defaults
url: "about:blank",
referrer: "",
contentType: "text/html",
parsingMode: "html",
parseOptions: {
sourceCodeLocationInfo: false,
scriptingEnabled: false
},
runScripts: undefined,
encoding,
pretendToBeVisual: false,
storageQuota: 5000000,
// Defaults filled in later
resourceLoader: undefined,
virtualConsole: undefined,
cookieJar: undefined
},
// Defaults
beforeParse() { }
};
// options.contentType was parsed into mimeType by the caller.
if (!mimeType.isHTML() && !mimeType.isXML()) {
throw new RangeError(`The given content type of "${options.contentType}" was not a HTML or XML content type`);
}
transformed.windowOptions.contentType = mimeType.essence;
transformed.windowOptions.parsingMode = mimeType.isHTML() ? "html" : "xml";
if (options.url !== undefined) {
transformed.windowOptions.url = (new URL(options.url)).href;
}
if (options.referrer !== undefined) {
transformed.windowOptions.referrer = (new URL(options.referrer)).href;
}
if (options.includeNodeLocations) {
if (transformed.windowOptions.parsingMode === "xml") {
throw new TypeError("Cannot set includeNodeLocations to true with an XML content type");
}
transformed.windowOptions.parseOptions = { sourceCodeLocationInfo: true };
}
transformed.windowOptions.cookieJar = options.cookieJar === undefined ?
new CookieJar() :
options.cookieJar;
transformed.windowOptions.virtualConsole = options.virtualConsole === undefined ?
(new VirtualConsole()).sendTo(console) :
options.virtualConsole;
if (!(transformed.windowOptions.virtualConsole instanceof VirtualConsole)) {
throw new TypeError("virtualConsole must be an instance of VirtualConsole");
}
transformed.windowOptions.resourceLoader = resourcesToResourceLoader(options.resources);
if (options.runScripts !== undefined) {
transformed.windowOptions.runScripts = String(options.runScripts);
if (transformed.windowOptions.runScripts === "dangerously") {
transformed.windowOptions.parseOptions.scriptingEnabled = true;
} else if (transformed.windowOptions.runScripts !== "outside-only") {
throw new RangeError(`runScripts must be undefined, "dangerously", or "outside-only"`);
}
}
if (options.beforeParse !== undefined) {
transformed.beforeParse = options.beforeParse;
}
if (options.pretendToBeVisual !== undefined) {
transformed.windowOptions.pretendToBeVisual = Boolean(options.pretendToBeVisual);
}
if (options.storageQuota !== undefined) {
transformed.windowOptions.storageQuota = Number(options.storageQuota);
}
return transformed;
}
function normalizeHTML(html, mimeType) {
let encoding = "UTF-8";
if (ArrayBuffer.isView(html)) {
html = Buffer.from(html.buffer, html.byteOffset, html.byteLength);
} else if (html instanceof ArrayBuffer) {
html = Buffer.from(html);
}
if (Buffer.isBuffer(html)) {
encoding = sniffHTMLEncoding(html, {
defaultEncoding: mimeType.isXML() ? "UTF-8" : "windows-1252",
transportLayerEncodingLabel: mimeType.parameters.get("charset")
});
html = whatwgEncoding.decode(html, encoding);
} else {
html = String(html);
}
return { html, encoding };
}
function resourcesToResourceLoader(resources) {
switch (resources) {
case undefined: {
return new NoOpResourceLoader();
}
case "usable": {
return new ResourceLoader();
}
default: {
if (!(resources instanceof ResourceLoader)) {
throw new TypeError("resources must be an instance of ResourceLoader");
}
return resources;
}
}
}
exports.JSDOM = JSDOM;
exports.VirtualConsole = VirtualConsole;
exports.CookieJar = CookieJar;
exports.ResourceLoader = ResourceLoader;
exports.toughCookie = toughCookie;
@@ -0,0 +1,933 @@
"use strict";
const vm = require("vm");
const webIDLConversions = require("webidl-conversions");
const { CSSStyleDeclaration } = require("cssstyle");
const { Performance: RawPerformance } = require("w3c-hr-time");
const notImplemented = require("./not-implemented");
const { installInterfaces } = require("../living/interfaces");
const { define, mixin } = require("../utils");
const Element = require("../living/generated/Element");
const EventTarget = require("../living/generated/EventTarget");
const EventHandlerNonNull = require("../living/generated/EventHandlerNonNull");
const OnBeforeUnloadEventHandlerNonNull = require("../living/generated/OnBeforeUnloadEventHandlerNonNull");
const OnErrorEventHandlerNonNull = require("../living/generated/OnErrorEventHandlerNonNull");
const PageTransitionEvent = require("../living/generated/PageTransitionEvent");
const namedPropertiesWindow = require("../living/named-properties-window");
const postMessage = require("../living/post-message");
const DOMException = require("domexception/webidl2js-wrapper");
const { btoa, atob } = require("abab");
const idlUtils = require("../living/generated/utils");
const WebSocketImpl = require("../living/websockets/WebSocket-impl").implementation;
const BarProp = require("../living/generated/BarProp");
const documents = require("../living/documents.js");
const External = require("../living/generated/External");
const Navigator = require("../living/generated/Navigator");
const Performance = require("../living/generated/Performance");
const Screen = require("../living/generated/Screen");
const Storage = require("../living/generated/Storage");
const Selection = require("../living/generated/Selection");
const reportException = require("../living/helpers/runtime-script-errors");
const { getCurrentEventHandlerValue } = require("../living/helpers/create-event-accessor.js");
const { fireAnEvent } = require("../living/helpers/events");
const SessionHistory = require("../living/window/SessionHistory");
const { forEachMatchingSheetRuleOfElement, getResolvedValue, propertiesWithResolvedValueImplemented,
SHADOW_DOM_PSEUDO_REGEXP } = require("../living/helpers/style-rules.js");
const CustomElementRegistry = require("../living/generated/CustomElementRegistry");
const jsGlobals = require("./js-globals.json");
const GlobalEventHandlersImpl = require("../living/nodes/GlobalEventHandlers-impl").implementation;
const WindowEventHandlersImpl = require("../living/nodes/WindowEventHandlers-impl").implementation;
const events = new Set([
// GlobalEventHandlers
"abort", "autocomplete",
"autocompleteerror", "blur",
"cancel", "canplay", "canplaythrough",
"change", "click",
"close", "contextmenu",
"cuechange", "dblclick",
"drag", "dragend",
"dragenter",
"dragleave", "dragover",
"dragstart", "drop",
"durationchange", "emptied",
"ended", "focus",
"input", "invalid",
"keydown", "keypress",
"keyup", "load", "loadeddata",
"loadedmetadata", "loadstart",
"mousedown", "mouseenter",
"mouseleave", "mousemove",
"mouseout", "mouseover",
"mouseup", "wheel",
"pause", "play",
"playing", "progress",
"ratechange", "reset",
"resize", "scroll",
"securitypolicyviolation",
"seeked", "seeking",
"select", "sort", "stalled",
"submit", "suspend",
"timeupdate", "toggle",
"volumechange", "waiting",
// WindowEventHandlers
"afterprint",
"beforeprint",
"hashchange",
"languagechange",
"message",
"messageerror",
"offline",
"online",
"pagehide",
"pageshow",
"popstate",
"rejectionhandled",
"storage",
"unhandledrejection",
"unload"
// "error" and "beforeunload" are added separately
]);
exports.createWindow = function (options) {
return new Window(options);
};
const jsGlobalEntriesToInstall = Object.entries(jsGlobals).filter(([name]) => name in global);
// TODO remove when we drop Node v10 support.
const anyNodeVersionQueueMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : process.nextTick;
// https://html.spec.whatwg.org/#the-window-object
function setupWindow(windowInstance, { runScripts }) {
if (runScripts === "outside-only" || runScripts === "dangerously") {
contextifyWindow(windowInstance);
// Without this, these globals will only appear to scripts running inside the context using vm.runScript; they will
// not appear to scripts running from the outside, including to JSDOM implementation code.
for (const [globalName, globalPropDesc] of jsGlobalEntriesToInstall) {
const propDesc = { ...globalPropDesc, value: vm.runInContext(globalName, windowInstance) };
Object.defineProperty(windowInstance, globalName, propDesc);
}
} else {
// Without contextifying the window, none of the globals will exist. So, let's at least alias them from the Node.js
// context. See https://github.com/jsdom/jsdom/issues/2727 for more background and discussion.
for (const [globalName, globalPropDesc] of jsGlobalEntriesToInstall) {
const propDesc = { ...globalPropDesc, value: global[globalName] };
Object.defineProperty(windowInstance, globalName, propDesc);
}
}
installInterfaces(windowInstance, ["Window"]);
const EventTargetConstructor = windowInstance.EventTarget;
// eslint-disable-next-line func-name-matching, func-style, no-shadow
const windowConstructor = function Window() {
throw new TypeError("Illegal constructor");
};
Object.setPrototypeOf(windowConstructor, EventTargetConstructor);
Object.defineProperty(windowInstance, "Window", {
configurable: true,
writable: true,
value: windowConstructor
});
const windowPrototype = Object.create(EventTargetConstructor.prototype);
Object.defineProperties(windowPrototype, {
constructor: {
value: windowConstructor,
writable: true,
configurable: true
},
[Symbol.toStringTag]: {
value: "Window",
configurable: true
}
});
windowConstructor.prototype = windowPrototype;
Object.setPrototypeOf(windowInstance, windowPrototype);
EventTarget.setup(windowInstance, windowInstance);
mixin(windowInstance, WindowEventHandlersImpl.prototype);
mixin(windowInstance, GlobalEventHandlersImpl.prototype);
windowInstance._initGlobalEvents();
Object.defineProperty(windowInstance, "onbeforeunload", {
configurable: true,
enumerable: true,
get() {
return idlUtils.tryWrapperForImpl(getCurrentEventHandlerValue(this, "beforeunload"));
},
set(V) {
if (!idlUtils.isObject(V)) {
V = null;
} else {
V = OnBeforeUnloadEventHandlerNonNull.convert(V, {
context: "Failed to set the 'onbeforeunload' property on 'Window': The provided value"
});
}
this._setEventHandlerFor("beforeunload", V);
}
});
Object.defineProperty(windowInstance, "onerror", {
configurable: true,
enumerable: true,
get() {
return idlUtils.tryWrapperForImpl(getCurrentEventHandlerValue(this, "error"));
},
set(V) {
if (!idlUtils.isObject(V)) {
V = null;
} else {
V = OnErrorEventHandlerNonNull.convert(V, {
context: "Failed to set the 'onerror' property on 'Window': The provided value"
});
}
this._setEventHandlerFor("error", V);
}
});
for (const event of events) {
Object.defineProperty(windowInstance, `on${event}`, {
configurable: true,
enumerable: true,
get() {
return idlUtils.tryWrapperForImpl(getCurrentEventHandlerValue(this, event));
},
set(V) {
if (!idlUtils.isObject(V)) {
V = null;
} else {
V = EventHandlerNonNull.convert(V, {
context: `Failed to set the 'on${event}' property on 'Window': The provided value`
});
}
this._setEventHandlerFor(event, V);
}
});
}
windowInstance._globalObject = windowInstance;
}
// NOTE: per https://heycam.github.io/webidl/#Global, all properties on the Window object must be own-properties.
// That is why we assign everything inside of the constructor, instead of using a shared prototype.
// You can verify this in e.g. Firefox or Internet Explorer, which do a good job with Web IDL compliance.
function Window(options) {
setupWindow(this, { runScripts: options.runScripts });
const rawPerformance = new RawPerformance();
const windowInitialized = rawPerformance.now();
const window = this;
// ### PRIVATE DATA PROPERTIES
this._resourceLoader = options.resourceLoader;
// vm initialization is deferred until script processing is activated
this._globalProxy = this;
Object.defineProperty(idlUtils.implForWrapper(this), idlUtils.wrapperSymbol, { get: () => this._globalProxy });
// List options explicitly to be clear which are passed through
this._document = documents.createWrapper(window, {
parsingMode: options.parsingMode,
contentType: options.contentType,
encoding: options.encoding,
cookieJar: options.cookieJar,
url: options.url,
lastModified: options.lastModified,
referrer: options.referrer,
parseOptions: options.parseOptions,
defaultView: this._globalProxy,
global: this,
parentOrigin: options.parentOrigin
}, { alwaysUseDocumentClass: true });
if (vm.isContext(window)) {
const documentImpl = idlUtils.implForWrapper(window._document);
documentImpl._defaultView = window._globalProxy = vm.runInContext("this", window);
}
const documentOrigin = idlUtils.implForWrapper(this._document)._origin;
this._origin = documentOrigin;
// https://html.spec.whatwg.org/#session-history
this._sessionHistory = new SessionHistory({
document: idlUtils.implForWrapper(this._document),
url: idlUtils.implForWrapper(this._document)._URL,
stateObject: null
}, this);
this._virtualConsole = options.virtualConsole;
this._runScripts = options.runScripts;
// Set up the window as if it's a top level window.
// If it's not, then references will be corrected by frame/iframe code.
this._parent = this._top = this._globalProxy;
this._frameElement = null;
// This implements window.frames.length, since window.frames returns a
// self reference to the window object. This value is incremented in the
// HTMLFrameElement implementation.
this._length = 0;
// https://dom.spec.whatwg.org/#window-current-event
this._currentEvent = undefined;
this._pretendToBeVisual = options.pretendToBeVisual;
this._storageQuota = options.storageQuota;
// Some properties (such as localStorage and sessionStorage) share data
// between windows in the same origin. This object is intended
// to contain such data.
if (options.commonForOrigin && options.commonForOrigin[documentOrigin]) {
this._commonForOrigin = options.commonForOrigin;
} else {
this._commonForOrigin = {
[documentOrigin]: {
localStorageArea: new Map(),
sessionStorageArea: new Map(),
windowsInSameOrigin: [this]
}
};
}
this._currentOriginData = this._commonForOrigin[documentOrigin];
// ### WEB STORAGE
this._localStorage = Storage.create(window, [], {
associatedWindow: this,
storageArea: this._currentOriginData.localStorageArea,
type: "localStorage",
url: this._document.documentURI,
storageQuota: this._storageQuota
});
this._sessionStorage = Storage.create(window, [], {
associatedWindow: this,
storageArea: this._currentOriginData.sessionStorageArea,
type: "sessionStorage",
url: this._document.documentURI,
storageQuota: this._storageQuota
});
// ### SELECTION
// https://w3c.github.io/selection-api/#dfn-selection
this._selection = Selection.createImpl(window);
// https://w3c.github.io/selection-api/#dom-window
this.getSelection = function () {
return window._selection;
};
// ### GETTERS
const locationbar = BarProp.create(window);
const menubar = BarProp.create(window);
const personalbar = BarProp.create(window);
const scrollbars = BarProp.create(window);
const statusbar = BarProp.create(window);
const toolbar = BarProp.create(window);
const external = External.create(window);
const navigator = Navigator.create(window, [], { userAgent: this._resourceLoader._userAgent });
const performance = Performance.create(window, [], { rawPerformance });
const screen = Screen.create(window);
const customElementRegistry = CustomElementRegistry.create(window);
define(this, {
get length() {
return window._length;
},
get window() {
return window._globalProxy;
},
get frameElement() {
return idlUtils.wrapperForImpl(window._frameElement);
},
get frames() {
return window._globalProxy;
},
get self() {
return window._globalProxy;
},
get parent() {
return window._parent;
},
get top() {
return window._top;
},
get document() {
return window._document;
},
get external() {
return external;
},
get location() {
return idlUtils.wrapperForImpl(idlUtils.implForWrapper(window._document)._location);
},
get history() {
return idlUtils.wrapperForImpl(idlUtils.implForWrapper(window._document)._history);
},
get navigator() {
return navigator;
},
get locationbar() {
return locationbar;
},
get menubar() {
return menubar;
},
get personalbar() {
return personalbar;
},
get scrollbars() {
return scrollbars;
},
get statusbar() {
return statusbar;
},
get toolbar() {
return toolbar;
},
get performance() {
return performance;
},
get screen() {
return screen;
},
get origin() {
return window._origin;
},
// The origin IDL attribute is defined with [Replaceable].
set origin(value) {
Object.defineProperty(this, "origin", {
value,
writable: true,
enumerable: true,
configurable: true
});
},
get localStorage() {
if (idlUtils.implForWrapper(this._document)._origin === "null") {
throw DOMException.create(window, [
"localStorage is not available for opaque origins",
"SecurityError"
]);
}
return this._localStorage;
},
get sessionStorage() {
if (idlUtils.implForWrapper(this._document)._origin === "null") {
throw DOMException.create(window, [
"sessionStorage is not available for opaque origins",
"SecurityError"
]);
}
return this._sessionStorage;
},
get customElements() {
return customElementRegistry;
},
get event() {
return window._currentEvent ? idlUtils.wrapperForImpl(window._currentEvent) : undefined;
},
set event(value) {
Object.defineProperty(window, "event", { configurable: true, enumerable: true, writable: true, value });
}
});
namedPropertiesWindow.initializeWindow(this, this._globalProxy);
// ### METHODS
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers
// In the spec the list of active timers is a set of IDs. We make it a map of IDs to Node.js timer objects, so that
// we can call Node.js-side clearTimeout() when clearing, and thus allow process shutdown faster.
const listOfActiveTimers = new Map();
let latestTimerId = 0;
this.setTimeout = function (handler, timeout = 0, ...args) {
if (typeof handler !== "function") {
handler = webIDLConversions.DOMString(handler);
}
timeout = webIDLConversions.long(timeout);
return timerInitializationSteps(handler, timeout, args, { methodContext: window, repeat: false });
};
this.setInterval = function (handler, timeout = 0, ...args) {
if (typeof handler !== "function") {
handler = webIDLConversions.DOMString(handler);
}
timeout = webIDLConversions.long(timeout);
return timerInitializationSteps(handler, timeout, args, { methodContext: window, repeat: true });
};
this.clearTimeout = function (handle = 0) {
handle = webIDLConversions.long(handle);
const nodejsTimer = listOfActiveTimers.get(handle);
if (nodejsTimer) {
clearTimeout(nodejsTimer);
listOfActiveTimers.delete(handle);
}
};
this.clearInterval = function (handle = 0) {
handle = webIDLConversions.long(handle);
const nodejsTimer = listOfActiveTimers.get(handle);
if (nodejsTimer) {
// We use setTimeout() in timerInitializationSteps even for this.setInterval().
clearTimeout(nodejsTimer);
listOfActiveTimers.delete(handle);
}
};
function timerInitializationSteps(handler, timeout, args, { methodContext, repeat, previousHandle }) {
// This appears to be unspecced, but matches browser behavior for close()ed windows.
if (!methodContext._document) {
return 0;
}
// TODO: implement timer nesting level behavior.
const methodContextProxy = methodContext._globalProxy;
const handle = previousHandle !== undefined ? previousHandle : ++latestTimerId;
function task() {
if (!listOfActiveTimers.has(handle)) {
return;
}
try {
if (typeof handler === "function") {
handler.apply(methodContextProxy, args);
} else if (window._runScripts === "dangerously") {
vm.runInContext(handler, window, { filename: window.location.href, displayErrors: false });
}
} catch (e) {
reportException(window, e, window.location.href);
}
if (listOfActiveTimers.has(handle)) {
if (repeat) {
timerInitializationSteps(handler, timeout, args, { methodContext, repeat: true, previousHandle: handle });
} else {
listOfActiveTimers.delete(handle);
}
}
}
if (timeout < 0) {
timeout = 0;
}
const nodejsTimer = setTimeout(task, timeout);
listOfActiveTimers.set(handle, nodejsTimer);
return handle;
}
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#microtask-queuing
this.queueMicrotask = function (callback) {
callback = webIDLConversions.Function(callback);
anyNodeVersionQueueMicrotask(() => {
try {
callback();
} catch (e) {
reportException(window, e, window.location.href);
}
});
};
// https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#animation-frames
let animationFrameCallbackId = 0;
const mapOfAnimationFrameCallbacks = new Map();
let animationFrameNodejsInterval = null;
// Unlike the spec, where an animation frame happens every 60 Hz regardless, we optimize so that if there are no
// requestAnimationFrame() calls outstanding, we don't fire the timer. This helps us track that.
let numberOfOngoingAnimationFrameCallbacks = 0;
if (this._pretendToBeVisual) {
this.requestAnimationFrame = function (callback) {
callback = webIDLConversions.Function(callback);
const handle = ++animationFrameCallbackId;
mapOfAnimationFrameCallbacks.set(handle, callback);
++numberOfOngoingAnimationFrameCallbacks;
if (numberOfOngoingAnimationFrameCallbacks === 1) {
animationFrameNodejsInterval = setInterval(() => {
runAnimationFrameCallbacks(rawPerformance.now() - windowInitialized);
}, 1000 / 60);
}
return handle;
};
this.cancelAnimationFrame = function (handle) {
handle = webIDLConversions["unsigned long"](handle);
removeAnimationFrameCallback(handle);
};
function runAnimationFrameCallbacks(now) {
// Converting to an array is important to get a sync snapshot and thus match spec semantics.
const callbackHandles = [...mapOfAnimationFrameCallbacks.keys()];
for (const handle of callbackHandles) {
// This has() can be false if a callback calls cancelAnimationFrame().
if (mapOfAnimationFrameCallbacks.has(handle)) {
const callback = mapOfAnimationFrameCallbacks.get(handle);
removeAnimationFrameCallback(handle);
try {
callback(now);
} catch (e) {
reportException(window, e, window.location.href);
}
}
}
}
function removeAnimationFrameCallback(handle) {
if (mapOfAnimationFrameCallbacks.has(handle)) {
--numberOfOngoingAnimationFrameCallbacks;
if (numberOfOngoingAnimationFrameCallbacks === 0) {
clearInterval(animationFrameNodejsInterval);
}
}
mapOfAnimationFrameCallbacks.delete(handle);
}
}
function stopAllTimers() {
for (const nodejsTimer of listOfActiveTimers.values()) {
clearTimeout(nodejsTimer);
}
listOfActiveTimers.clear();
clearInterval(animationFrameNodejsInterval);
}
function Option(text, value, defaultSelected, selected) {
if (text === undefined) {
text = "";
}
text = webIDLConversions.DOMString(text);
if (value !== undefined) {
value = webIDLConversions.DOMString(value);
}
defaultSelected = webIDLConversions.boolean(defaultSelected);
selected = webIDLConversions.boolean(selected);
const option = window._document.createElement("option");
const impl = idlUtils.implForWrapper(option);
if (text !== "") {
impl.text = text;
}
if (value !== undefined) {
impl.setAttributeNS(null, "value", value);
}
if (defaultSelected) {
impl.setAttributeNS(null, "selected", "");
}
impl._selectedness = selected;
return option;
}
Object.defineProperty(Option, "prototype", {
value: this.HTMLOptionElement.prototype,
configurable: false,
enumerable: false,
writable: false
});
Object.defineProperty(window, "Option", {
value: Option,
configurable: true,
enumerable: false,
writable: true
});
function Image(...args) {
const img = window._document.createElement("img");
const impl = idlUtils.implForWrapper(img);
if (args.length > 0) {
impl.setAttributeNS(null, "width", String(args[0]));
}
if (args.length > 1) {
impl.setAttributeNS(null, "height", String(args[1]));
}
return img;
}
Object.defineProperty(Image, "prototype", {
value: this.HTMLImageElement.prototype,
configurable: false,
enumerable: false,
writable: false
});
Object.defineProperty(window, "Image", {
value: Image,
configurable: true,
enumerable: false,
writable: true
});
function Audio(src) {
const audio = window._document.createElement("audio");
const impl = idlUtils.implForWrapper(audio);
impl.setAttributeNS(null, "preload", "auto");
if (src !== undefined) {
impl.setAttributeNS(null, "src", String(src));
}
return audio;
}
Object.defineProperty(Audio, "prototype", {
value: this.HTMLAudioElement.prototype,
configurable: false,
enumerable: false,
writable: false
});
Object.defineProperty(window, "Audio", {
value: Audio,
configurable: true,
enumerable: false,
writable: true
});
this.postMessage = postMessage(window);
this.atob = function (str) {
const result = atob(str);
if (result === null) {
throw DOMException.create(window, [
"The string to be decoded contains invalid characters.",
"InvalidCharacterError"
]);
}
return result;
};
this.btoa = function (str) {
const result = btoa(str);
if (result === null) {
throw DOMException.create(window, [
"The string to be encoded contains invalid characters.",
"InvalidCharacterError"
]);
}
return result;
};
this.stop = function () {
const manager = idlUtils.implForWrapper(this._document)._requestManager;
if (manager) {
manager.close();
}
};
this.close = function () {
// Recursively close child frame windows, then ourselves (depth-first).
for (let i = 0; i < this.length; ++i) {
this[i].close();
}
// Clear out all listeners. Any in-flight or upcoming events should not get delivered.
idlUtils.implForWrapper(this)._eventListeners = Object.create(null);
if (this._document) {
if (this._document.body) {
this._document.body.innerHTML = "";
}
if (this._document.close) {
// It's especially important to clear out the listeners here because document.close() causes a "load" event to
// fire.
idlUtils.implForWrapper(this._document)._eventListeners = Object.create(null);
this._document.close();
}
const doc = idlUtils.implForWrapper(this._document);
if (doc._requestManager) {
doc._requestManager.close();
}
delete this._document;
}
stopAllTimers();
WebSocketImpl.cleanUpWindow(this);
};
this.getComputedStyle = function (elt, pseudoElt = undefined) {
elt = Element.convert(elt);
if (pseudoElt !== undefined && pseudoElt !== null) {
pseudoElt = webIDLConversions.DOMString(pseudoElt);
}
if (pseudoElt !== undefined && pseudoElt !== null && pseudoElt !== "") {
// TODO: Parse pseudoElt
if (SHADOW_DOM_PSEUDO_REGEXP.test(pseudoElt)) {
throw new TypeError("Tried to get the computed style of a Shadow DOM pseudo-element.");
}
notImplemented("window.computedStyle(elt, pseudoElt)", this);
}
const declaration = new CSSStyleDeclaration();
const { forEach } = Array.prototype;
const { style } = elt;
forEachMatchingSheetRuleOfElement(elt, rule => {
forEach.call(rule.style, property => {
declaration.setProperty(
property,
rule.style.getPropertyValue(property),
rule.style.getPropertyPriority(property)
);
});
});
// https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle
const declarations = Object.keys(propertiesWithResolvedValueImplemented);
forEach.call(declarations, property => {
declaration.setProperty(property, getResolvedValue(elt, property));
});
forEach.call(style, property => {
declaration.setProperty(property, style.getPropertyValue(property), style.getPropertyPriority(property));
});
return declaration;
};
this.getSelection = function () {
return window._document.getSelection();
};
// The captureEvents() and releaseEvents() methods must do nothing
this.captureEvents = function () {};
this.releaseEvents = function () {};
// ### PUBLIC DATA PROPERTIES (TODO: should be getters)
function wrapConsoleMethod(method) {
return (...args) => {
window._virtualConsole.emit(method, ...args);
};
}
this.console = {
assert: wrapConsoleMethod("assert"),
clear: wrapConsoleMethod("clear"),
count: wrapConsoleMethod("count"),
countReset: wrapConsoleMethod("countReset"),
debug: wrapConsoleMethod("debug"),
dir: wrapConsoleMethod("dir"),
dirxml: wrapConsoleMethod("dirxml"),
error: wrapConsoleMethod("error"),
group: wrapConsoleMethod("group"),
groupCollapsed: wrapConsoleMethod("groupCollapsed"),
groupEnd: wrapConsoleMethod("groupEnd"),
info: wrapConsoleMethod("info"),
log: wrapConsoleMethod("log"),
table: wrapConsoleMethod("table"),
time: wrapConsoleMethod("time"),
timeLog: wrapConsoleMethod("timeLog"),
timeEnd: wrapConsoleMethod("timeEnd"),
trace: wrapConsoleMethod("trace"),
warn: wrapConsoleMethod("warn")
};
function notImplementedMethod(name) {
return function () {
notImplemented(name, window);
};
}
define(this, {
name: "",
status: "",
devicePixelRatio: 1,
innerWidth: 1024,
innerHeight: 768,
outerWidth: 1024,
outerHeight: 768,
pageXOffset: 0,
pageYOffset: 0,
screenX: 0,
screenLeft: 0,
screenY: 0,
screenTop: 0,
scrollX: 0,
scrollY: 0,
alert: notImplementedMethod("window.alert"),
blur: notImplementedMethod("window.blur"),
confirm: notImplementedMethod("window.confirm"),
focus: notImplementedMethod("window.focus"),
moveBy: notImplementedMethod("window.moveBy"),
moveTo: notImplementedMethod("window.moveTo"),
open: notImplementedMethod("window.open"),
print: notImplementedMethod("window.print"),
prompt: notImplementedMethod("window.prompt"),
resizeBy: notImplementedMethod("window.resizeBy"),
resizeTo: notImplementedMethod("window.resizeTo"),
scroll: notImplementedMethod("window.scroll"),
scrollBy: notImplementedMethod("window.scrollBy"),
scrollTo: notImplementedMethod("window.scrollTo")
});
// ### INITIALIZATION
process.nextTick(() => {
if (!window.document) {
return; // window might've been closed already
}
const documentImpl = idlUtils.implForWrapper(window._document);
if (window.document.readyState === "complete") {
fireAnEvent("load", window, undefined, {}, documentImpl);
} else {
window.document.addEventListener("load", () => {
fireAnEvent("load", window, undefined, {}, documentImpl);
if (!documentImpl._pageShowingFlag) {
documentImpl._pageShowingFlag = true;
fireAnEvent("pageshow", window, PageTransitionEvent, { persisted: false }, documentImpl);
}
});
}
});
}
function contextifyWindow(window) {
if (vm.isContext(window)) {
return;
}
vm.createContext(window);
}
@@ -0,0 +1,789 @@
// Ideally, we would use
// https://html.spec.whatwg.org/multipage/rendering.html#the-css-user-agent-style-sheet-and-presentational-hints
// but for now, just use the version from blink. This file is copied from
// https://chromium.googlesource.com/chromium/blink/+/96aa3a280ab7d67178c8f122a04949ce5f8579e0/Source/core/css/html.css
// (removed a line which had octal literals inside since octal literals are not allowed in template strings)
// We use a .js file because otherwise we can't browserify this. (brfs is unusable due to lack of ES2015 support)
module.exports = `
/*
* The default style sheet used to render HTML.
*
* Copyright (C) 2000 Lars Knoll (knoll@kde.org)
* Copyright (C) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011 Apple Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
*/
@namespace "http://www.w3.org/1999/xhtml";
html {
display: block
}
:root {
scroll-blocks-on: start-touch wheel-event
}
/* children of the <head> element all have display:none */
head {
display: none
}
meta {
display: none
}
title {
display: none
}
link {
display: none
}
style {
display: none
}
script {
display: none
}
/* generic block-level elements */
body {
display: block;
margin: 8px
}
p {
display: block;
-webkit-margin-before: 1__qem;
-webkit-margin-after: 1__qem;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
}
div {
display: block
}
layer {
display: block
}
article, aside, footer, header, hgroup, main, nav, section {
display: block
}
marquee {
display: inline-block;
}
address {
display: block
}
blockquote {
display: block;
-webkit-margin-before: 1__qem;
-webkit-margin-after: 1em;
-webkit-margin-start: 40px;
-webkit-margin-end: 40px;
}
figcaption {
display: block
}
figure {
display: block;
-webkit-margin-before: 1em;
-webkit-margin-after: 1em;
-webkit-margin-start: 40px;
-webkit-margin-end: 40px;
}
q {
display: inline
}
/* nwmatcher does not support ::before and ::after, so we can't render q
correctly: https://html.spec.whatwg.org/multipage/rendering.html#phrasing-content-3
TODO: add q::before and q::after selectors
*/
center {
display: block;
/* special centering to be able to emulate the html4/netscape behaviour */
text-align: -webkit-center
}
hr {
display: block;
-webkit-margin-before: 0.5em;
-webkit-margin-after: 0.5em;
-webkit-margin-start: auto;
-webkit-margin-end: auto;
border-style: inset;
border-width: 1px;
box-sizing: border-box
}
map {
display: inline
}
video {
object-fit: contain;
}
/* heading elements */
h1 {
display: block;
font-size: 2em;
-webkit-margin-before: 0.67__qem;
-webkit-margin-after: 0.67em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
font-weight: bold
}
article h1,
aside h1,
nav h1,
section h1 {
font-size: 1.5em;
-webkit-margin-before: 0.83__qem;
-webkit-margin-after: 0.83em;
}
article article h1,
article aside h1,
article nav h1,
article section h1,
aside article h1,
aside aside h1,
aside nav h1,
aside section h1,
nav article h1,
nav aside h1,
nav nav h1,
nav section h1,
section article h1,
section aside h1,
section nav h1,
section section h1 {
font-size: 1.17em;
-webkit-margin-before: 1__qem;
-webkit-margin-after: 1em;
}
/* Remaining selectors are deleted because nwmatcher does not support
:matches() and expanding the selectors manually would be far too verbose.
Also see https://html.spec.whatwg.org/multipage/rendering.html#sections-and-headings
TODO: rewrite to use :matches() when nwmatcher supports it.
*/
h2 {
display: block;
font-size: 1.5em;
-webkit-margin-before: 0.83__qem;
-webkit-margin-after: 0.83em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
font-weight: bold
}
h3 {
display: block;
font-size: 1.17em;
-webkit-margin-before: 1__qem;
-webkit-margin-after: 1em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
font-weight: bold
}
h4 {
display: block;
-webkit-margin-before: 1.33__qem;
-webkit-margin-after: 1.33em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
font-weight: bold
}
h5 {
display: block;
font-size: .83em;
-webkit-margin-before: 1.67__qem;
-webkit-margin-after: 1.67em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
font-weight: bold
}
h6 {
display: block;
font-size: .67em;
-webkit-margin-before: 2.33__qem;
-webkit-margin-after: 2.33em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
font-weight: bold
}
/* tables */
table {
display: table;
border-collapse: separate;
border-spacing: 2px;
border-color: gray
}
thead {
display: table-header-group;
vertical-align: middle;
border-color: inherit
}
tbody {
display: table-row-group;
vertical-align: middle;
border-color: inherit
}
tfoot {
display: table-footer-group;
vertical-align: middle;
border-color: inherit
}
/* for tables without table section elements (can happen with XHTML or dynamically created tables) */
table > tr {
vertical-align: middle;
}
col {
display: table-column
}
colgroup {
display: table-column-group
}
tr {
display: table-row;
vertical-align: inherit;
border-color: inherit
}
td, th {
display: table-cell;
vertical-align: inherit
}
th {
font-weight: bold
}
caption {
display: table-caption;
text-align: -webkit-center
}
/* lists */
ul, menu, dir {
display: block;
list-style-type: disc;
-webkit-margin-before: 1__qem;
-webkit-margin-after: 1em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
-webkit-padding-start: 40px
}
ol {
display: block;
list-style-type: decimal;
-webkit-margin-before: 1__qem;
-webkit-margin-after: 1em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
-webkit-padding-start: 40px
}
li {
display: list-item;
text-align: -webkit-match-parent;
}
ul ul, ol ul {
list-style-type: circle
}
ol ol ul, ol ul ul, ul ol ul, ul ul ul {
list-style-type: square
}
dd {
display: block;
-webkit-margin-start: 40px
}
dl {
display: block;
-webkit-margin-before: 1__qem;
-webkit-margin-after: 1em;
-webkit-margin-start: 0;
-webkit-margin-end: 0;
}
dt {
display: block
}
ol ul, ul ol, ul ul, ol ol {
-webkit-margin-before: 0;
-webkit-margin-after: 0
}
/* form elements */
form {
display: block;
margin-top: 0__qem;
}
label {
cursor: default;
}
legend {
display: block;
-webkit-padding-start: 2px;
-webkit-padding-end: 2px;
border: none
}
fieldset {
display: block;
-webkit-margin-start: 2px;
-webkit-margin-end: 2px;
-webkit-padding-before: 0.35em;
-webkit-padding-start: 0.75em;
-webkit-padding-end: 0.75em;
-webkit-padding-after: 0.625em;
border: 2px groove ThreeDFace;
min-width: -webkit-min-content;
}
button {
-webkit-appearance: button;
}
/* Form controls don't go vertical. */
input, textarea, select, button, meter, progress {
-webkit-writing-mode: horizontal-tb !important;
}
input, textarea, select, button {
margin: 0__qem;
font: -webkit-small-control;
text-rendering: auto; /* FIXME: Remove when tabs work with optimizeLegibility. */
color: initial;
letter-spacing: normal;
word-spacing: normal;
line-height: normal;
text-transform: none;
text-indent: 0;
text-shadow: none;
display: inline-block;
text-align: start;
}
/* TODO: Add " i" to attribute matchers to support case-insensitive matching */
input[type="hidden"] {
display: none
}
input {
-webkit-appearance: textfield;
padding: 1px;
background-color: white;
border: 2px inset;
-webkit-rtl-ordering: logical;
-webkit-user-select: text;
cursor: auto;
}
input[type="search"] {
-webkit-appearance: searchfield;
box-sizing: border-box;
}
select {
border-radius: 5px;
}
textarea {
-webkit-appearance: textarea;
background-color: white;
border: 1px solid;
-webkit-rtl-ordering: logical;
-webkit-user-select: text;
flex-direction: column;
resize: auto;
cursor: auto;
padding: 2px;
white-space: pre-wrap;
word-wrap: break-word;
}
input[type="password"] {
-webkit-text-security: disc !important;
}
input[type="hidden"], input[type="image"], input[type="file"] {
-webkit-appearance: initial;
padding: initial;
background-color: initial;
border: initial;
}
input[type="file"] {
align-items: baseline;
color: inherit;
text-align: start !important;
}
input[type="radio"], input[type="checkbox"] {
margin: 3px 0.5ex;
padding: initial;
background-color: initial;
border: initial;
}
input[type="button"], input[type="submit"], input[type="reset"] {
-webkit-appearance: push-button;
-webkit-user-select: none;
white-space: pre
}
input[type="button"], input[type="submit"], input[type="reset"], button {
align-items: flex-start;
text-align: center;
cursor: default;
color: ButtonText;
padding: 2px 6px 3px 6px;
border: 2px outset ButtonFace;
background-color: ButtonFace;
box-sizing: border-box
}
input[type="range"] {
-webkit-appearance: slider-horizontal;
padding: initial;
border: initial;
margin: 2px;
color: #909090;
}
input[type="button"]:disabled, input[type="submit"]:disabled, input[type="reset"]:disabled,
button:disabled, select:disabled, optgroup:disabled, option:disabled,
select[disabled]>option {
color: GrayText
}
input[type="button"]:active, input[type="submit"]:active, input[type="reset"]:active, button:active {
border-style: inset
}
input[type="button"]:active:disabled, input[type="submit"]:active:disabled, input[type="reset"]:active:disabled, button:active:disabled {
border-style: outset
}
datalist {
display: none
}
area {
display: inline;
cursor: pointer;
}
param {
display: none
}
input[type="checkbox"] {
-webkit-appearance: checkbox;
box-sizing: border-box;
}
input[type="radio"] {
-webkit-appearance: radio;
box-sizing: border-box;
}
input[type="color"] {
-webkit-appearance: square-button;
width: 44px;
height: 23px;
background-color: ButtonFace;
/* Same as native_theme_base. */
border: 1px #a9a9a9 solid;
padding: 1px 2px;
}
input[type="color"][list] {
-webkit-appearance: menulist;
width: 88px;
height: 23px
}
select {
-webkit-appearance: menulist;
box-sizing: border-box;
align-items: center;
border: 1px solid;
white-space: pre;
-webkit-rtl-ordering: logical;
color: black;
background-color: white;
cursor: default;
}
optgroup {
font-weight: bolder;
display: block;
}
option {
font-weight: normal;
display: block;
padding: 0 2px 1px 2px;
white-space: pre;
min-height: 1.2em;
}
output {
display: inline;
}
/* meter */
meter {
-webkit-appearance: meter;
box-sizing: border-box;
display: inline-block;
height: 1em;
width: 5em;
vertical-align: -0.2em;
}
/* progress */
progress {
-webkit-appearance: progress-bar;
box-sizing: border-box;
display: inline-block;
height: 1em;
width: 10em;
vertical-align: -0.2em;
}
/* inline elements */
u, ins {
text-decoration: underline
}
strong, b {
font-weight: bold
}
i, cite, em, var, address, dfn {
font-style: italic
}
tt, code, kbd, samp {
font-family: monospace
}
pre, xmp, plaintext, listing {
display: block;
font-family: monospace;
white-space: pre;
margin: 1__qem 0
}
mark {
background-color: yellow;
color: black
}
big {
font-size: larger
}
small {
font-size: smaller
}
s, strike, del {
text-decoration: line-through
}
sub {
vertical-align: sub;
font-size: smaller
}
sup {
vertical-align: super;
font-size: smaller
}
nobr {
white-space: nowrap
}
/* states */
:focus {
outline: auto 5px -webkit-focus-ring-color
}
/* Read-only text fields do not show a focus ring but do still receive focus */
html:focus, body:focus, input[readonly]:focus {
outline: none
}
embed:focus, iframe:focus, object:focus {
outline: none
}
input:focus, textarea:focus, select:focus {
outline-offset: -2px
}
input[type="button"]:focus,
input[type="checkbox"]:focus,
input[type="file"]:focus,
input[type="hidden"]:focus,
input[type="image"]:focus,
input[type="radio"]:focus,
input[type="reset"]:focus,
input[type="search"]:focus,
input[type="submit"]:focus {
outline-offset: 0
}
/* HTML5 ruby elements */
ruby, rt {
text-indent: 0; /* blocks used for ruby rendering should not trigger this */
}
rt {
line-height: normal;
-webkit-text-emphasis: none;
}
ruby > rt {
display: block;
font-size: 50%;
text-align: start;
}
ruby > rp {
display: none;
}
/* other elements */
noframes {
display: none
}
frameset, frame {
display: block
}
frameset {
border-color: inherit
}
iframe {
border: 2px inset
}
details {
display: block
}
summary {
display: block
}
template {
display: none
}
bdi, output {
unicode-bidi: -webkit-isolate;
}
bdo {
unicode-bidi: bidi-override;
}
textarea[dir=auto] {
unicode-bidi: -webkit-plaintext;
}
dialog:not([open]) {
display: none
}
dialog {
position: absolute;
left: 0;
right: 0;
width: -webkit-fit-content;
height: -webkit-fit-content;
margin: auto;
border: solid;
padding: 1em;
background: white;
color: black
}
[hidden] {
display: none
}
/* noscript is handled internally, as it depends on settings. */
`;
@@ -0,0 +1,307 @@
{
"Object": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Function": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Array": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Number": {
"writable": true,
"enumerable": false,
"configurable": true
},
"parseFloat": {
"writable": true,
"enumerable": false,
"configurable": true
},
"parseInt": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Infinity": {
"writable": false,
"enumerable": false,
"configurable": false
},
"NaN": {
"writable": false,
"enumerable": false,
"configurable": false
},
"undefined": {
"writable": false,
"enumerable": false,
"configurable": false
},
"Boolean": {
"writable": true,
"enumerable": false,
"configurable": true
},
"String": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Symbol": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Date": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Promise": {
"writable": true,
"enumerable": false,
"configurable": true
},
"RegExp": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Error": {
"writable": true,
"enumerable": false,
"configurable": true
},
"AggregateError": {
"writable": true,
"enumerable": false,
"configurable": true
},
"EvalError": {
"writable": true,
"enumerable": false,
"configurable": true
},
"RangeError": {
"writable": true,
"enumerable": false,
"configurable": true
},
"ReferenceError": {
"writable": true,
"enumerable": false,
"configurable": true
},
"SyntaxError": {
"writable": true,
"enumerable": false,
"configurable": true
},
"TypeError": {
"writable": true,
"enumerable": false,
"configurable": true
},
"URIError": {
"writable": true,
"enumerable": false,
"configurable": true
},
"globalThis": {
"writable": true,
"enumerable": false,
"configurable": true
},
"JSON": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Math": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Intl": {
"writable": true,
"enumerable": false,
"configurable": true
},
"ArrayBuffer": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Uint8Array": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Int8Array": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Uint16Array": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Int16Array": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Uint32Array": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Int32Array": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Float32Array": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Float64Array": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Uint8ClampedArray": {
"writable": true,
"enumerable": false,
"configurable": true
},
"BigUint64Array": {
"writable": true,
"enumerable": false,
"configurable": true
},
"BigInt64Array": {
"writable": true,
"enumerable": false,
"configurable": true
},
"DataView": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Map": {
"writable": true,
"enumerable": false,
"configurable": true
},
"BigInt": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Set": {
"writable": true,
"enumerable": false,
"configurable": true
},
"WeakMap": {
"writable": true,
"enumerable": false,
"configurable": true
},
"WeakSet": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Proxy": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Reflect": {
"writable": true,
"enumerable": false,
"configurable": true
},
"FinalizationRegistry": {
"writable": true,
"enumerable": false,
"configurable": true
},
"WeakRef": {
"writable": true,
"enumerable": false,
"configurable": true
},
"decodeURI": {
"writable": true,
"enumerable": false,
"configurable": true
},
"decodeURIComponent": {
"writable": true,
"enumerable": false,
"configurable": true
},
"encodeURI": {
"writable": true,
"enumerable": false,
"configurable": true
},
"encodeURIComponent": {
"writable": true,
"enumerable": false,
"configurable": true
},
"escape": {
"writable": true,
"enumerable": false,
"configurable": true
},
"unescape": {
"writable": true,
"enumerable": false,
"configurable": true
},
"eval": {
"writable": true,
"enumerable": false,
"configurable": true
},
"isFinite": {
"writable": true,
"enumerable": false,
"configurable": true
},
"isNaN": {
"writable": true,
"enumerable": false,
"configurable": true
},
"SharedArrayBuffer": {
"writable": true,
"enumerable": false,
"configurable": true
},
"Atomics": {
"writable": true,
"enumerable": false,
"configurable": true
},
"WebAssembly": {
"writable": true,
"enumerable": false,
"configurable": true
}
}
@@ -0,0 +1,13 @@
"use strict";
module.exports = function (nameForErrorMessage, window) {
if (!window) {
// Do nothing for window-less documents.
return;
}
const error = new Error(`Not implemented: ${nameForErrorMessage}`);
error.type = "not implemented";
window._virtualConsole.emit("jsdomError", error);
};
@@ -0,0 +1,223 @@
"use strict";
const parse5 = require("parse5");
const { createElement } = require("../../living/helpers/create-element");
const { HTML_NS } = require("../../living/helpers/namespaces");
const DocumentType = require("../../living/generated/DocumentType");
const DocumentFragment = require("../../living/generated/DocumentFragment");
const Text = require("../../living/generated/Text");
const Comment = require("../../living/generated/Comment");
const attributes = require("../../living/attributes");
const nodeTypes = require("../../living/node-type");
const serializationAdapter = require("../../living/domparsing/parse5-adapter-serialization");
const {
customElementReactionsStack, invokeCEReactions, lookupCEDefinition
} = require("../../living/helpers/custom-elements");
// Horrible monkey-patch to implement https://github.com/inikulin/parse5/issues/237 and
// https://github.com/inikulin/parse5/issues/285.
const OpenElementStack = require("parse5/lib/parser/open-element-stack");
const openElementStackOriginalPush = OpenElementStack.prototype.push;
OpenElementStack.prototype.push = function (...args) {
openElementStackOriginalPush.apply(this, args);
this.treeAdapter._currentElement = this.current;
const after = this.items[this.stackTop];
if (after._pushedOnStackOfOpenElements) {
after._pushedOnStackOfOpenElements();
}
};
const openElementStackOriginalPop = OpenElementStack.prototype.pop;
OpenElementStack.prototype.pop = function (...args) {
const before = this.items[this.stackTop];
openElementStackOriginalPop.apply(this, args);
this.treeAdapter._currentElement = this.current;
if (before._poppedOffStackOfOpenElements) {
before._poppedOffStackOfOpenElements();
}
};
class JSDOMParse5Adapter {
constructor(documentImpl, options = {}) {
this._documentImpl = documentImpl;
this._globalObject = documentImpl._globalObject;
this._fragment = options.fragment || false;
// Since the createElement hook doesn't provide the parent element, we keep track of this using _currentElement:
// https://github.com/inikulin/parse5/issues/285. See above horrible monkey-patch for how this is maintained.
this._currentElement = undefined;
}
_ownerDocument() {
const { _currentElement } = this;
// The _currentElement is undefined when parsing elements at the root of the document.
if (_currentElement) {
return _currentElement.localName === "template" && _currentElement.namespaceURI === HTML_NS ?
_currentElement.content._ownerDocument :
_currentElement._ownerDocument;
}
return this._documentImpl;
}
createDocument() {
// parse5's model assumes that parse(html) will call into here to create the new Document, then return it. However,
// jsdom's model assumes we can create a Window (and through that create an empty Document), do some other setup
// stuff, and then parse, stuffing nodes into that Document as we go. So to adapt between these two models, we just
// return the already-created Document when asked by parse5 to "create" a Document.
return this._documentImpl;
}
createDocumentFragment() {
const ownerDocument = this._ownerDocument();
return DocumentFragment.createImpl(this._globalObject, [], { ownerDocument });
}
// https://html.spec.whatwg.org/#create-an-element-for-the-token
createElement(localName, namespace, attrs) {
const ownerDocument = this._ownerDocument();
const isAttribute = attrs.find(attr => attr.name === "is");
const isValue = isAttribute ? isAttribute.value : null;
const definition = lookupCEDefinition(ownerDocument, namespace, localName);
let willExecuteScript = false;
if (definition !== null && !this._fragment) {
willExecuteScript = true;
}
if (willExecuteScript) {
ownerDocument._throwOnDynamicMarkupInsertionCounter++;
customElementReactionsStack.push([]);
}
const element = createElement(ownerDocument, localName, namespace, null, isValue, willExecuteScript);
this.adoptAttributes(element, attrs);
if (willExecuteScript) {
const queue = customElementReactionsStack.pop();
invokeCEReactions(queue);
ownerDocument._throwOnDynamicMarkupInsertionCounter--;
}
if ("_parserInserted" in element) {
element._parserInserted = true;
}
return element;
}
createCommentNode(data) {
const ownerDocument = this._ownerDocument();
return Comment.createImpl(this._globalObject, [], { data, ownerDocument });
}
appendChild(parentNode, newNode) {
parentNode._append(newNode);
}
insertBefore(parentNode, newNode, referenceNode) {
parentNode._insert(newNode, referenceNode);
}
setTemplateContent(templateElement, contentFragment) {
// This code makes the glue between jsdom and parse5 HTMLTemplateElement parsing:
//
// * jsdom during the construction of the HTMLTemplateElement (for example when create via
// `document.createElement("template")`), creates a DocumentFragment and set it into _templateContents.
// * parse5 when parsing a <template> tag creates an HTMLTemplateElement (`createElement` adapter hook) and also
// create a DocumentFragment (`createDocumentFragment` adapter hook).
//
// At this point we now have to replace the one created in jsdom with one created by parse5.
const { _ownerDocument, _host } = templateElement._templateContents;
contentFragment._ownerDocument = _ownerDocument;
contentFragment._host = _host;
templateElement._templateContents = contentFragment;
}
setDocumentType(document, name, publicId, systemId) {
const ownerDocument = this._ownerDocument();
const documentType = DocumentType.createImpl(this._globalObject, [], { name, publicId, systemId, ownerDocument });
document._append(documentType);
}
setDocumentMode(document, mode) {
// TODO: the rest of jsdom ignores this
document._mode = mode;
}
detachNode(node) {
node.remove();
}
insertText(parentNode, text) {
const { lastChild } = parentNode;
if (lastChild && lastChild.nodeType === nodeTypes.TEXT_NODE) {
lastChild.data += text;
} else {
const ownerDocument = this._ownerDocument();
const textNode = Text.createImpl(this._globalObject, [], { data: text, ownerDocument });
parentNode._append(textNode);
}
}
insertTextBefore(parentNode, text, referenceNode) {
const { previousSibling } = referenceNode;
if (previousSibling && previousSibling.nodeType === nodeTypes.TEXT_NODE) {
previousSibling.data += text;
} else {
const ownerDocument = this._ownerDocument();
const textNode = Text.createImpl(this._globalObject, [], { data: text, ownerDocument });
parentNode._append(textNode, referenceNode);
}
}
adoptAttributes(element, attrs) {
for (const attr of attrs) {
const prefix = attr.prefix === "" ? null : attr.prefix;
attributes.setAttributeValue(element, attr.name, attr.value, prefix, attr.namespace);
}
}
}
// Assign shared adapters with serializer.
Object.assign(JSDOMParse5Adapter.prototype, serializationAdapter);
function parseFragment(markup, contextElement) {
const ownerDocument = contextElement.localName === "template" && contextElement.namespaceURI === HTML_NS ?
contextElement.content._ownerDocument :
contextElement._ownerDocument;
const config = {
...ownerDocument._parseOptions,
treeAdapter: new JSDOMParse5Adapter(ownerDocument, { fragment: true })
};
return parse5.parseFragment(contextElement, markup, config);
}
function parseIntoDocument(markup, ownerDocument) {
const config = {
...ownerDocument._parseOptions,
treeAdapter: new JSDOMParse5Adapter(ownerDocument)
};
return parse5.parse(markup, config);
}
module.exports = {
parseFragment,
parseIntoDocument
};
@@ -0,0 +1,37 @@
"use strict";
const xmlParser = require("./xml");
const htmlParser = require("./html");
// https://w3c.github.io/DOM-Parsing/#dfn-fragment-parsing-algorithm
function parseFragment(markup, contextElement) {
const { _parsingMode } = contextElement._ownerDocument;
let parseAlgorithm;
if (_parsingMode === "html") {
parseAlgorithm = htmlParser.parseFragment;
} else if (_parsingMode === "xml") {
parseAlgorithm = xmlParser.parseFragment;
}
// Note: HTML and XML fragment parsing algorithm already return a document fragments; no need to do steps 3 and 4
return parseAlgorithm(markup, contextElement);
}
function parseIntoDocument(markup, ownerDocument) {
const { _parsingMode } = ownerDocument;
let parseAlgorithm;
if (_parsingMode === "html") {
parseAlgorithm = htmlParser.parseIntoDocument;
} else if (_parsingMode === "xml") {
parseAlgorithm = xmlParser.parseIntoDocument;
}
return parseAlgorithm(markup, ownerDocument);
}
module.exports = {
parseFragment,
parseIntoDocument
};
@@ -0,0 +1,202 @@
"use strict";
const { SaxesParser } = require("saxes");
const DOMException = require("domexception/webidl2js-wrapper");
const { createElement } = require("../../living/helpers/create-element");
const DocumentFragment = require("../../living/generated/DocumentFragment");
const DocumentType = require("../../living/generated/DocumentType");
const CDATASection = require("../../living/generated/CDATASection");
const Comment = require("../../living/generated/Comment");
const ProcessingInstruction = require("../../living/generated/ProcessingInstruction");
const Text = require("../../living/generated/Text");
const attributes = require("../../living/attributes");
const { HTML_NS } = require("../../living/helpers/namespaces");
const HTML5_DOCTYPE = /<!doctype html>/i;
const PUBLIC_DOCTYPE = /<!doctype\s+([^\s]+)\s+public\s+"([^"]+)"\s+"([^"]+)"/i;
const SYSTEM_DOCTYPE = /<!doctype\s+([^\s]+)\s+system\s+"([^"]+)"/i;
const CUSTOM_NAME_DOCTYPE = /<!doctype\s+([^\s>]+)/i;
function parseDocType(globalObject, ownerDocument, html) {
if (HTML5_DOCTYPE.test(html)) {
return createDocumentType(globalObject, ownerDocument, "html", "", "");
}
const publicPieces = PUBLIC_DOCTYPE.exec(html);
if (publicPieces) {
return createDocumentType(globalObject, ownerDocument, publicPieces[1], publicPieces[2], publicPieces[3]);
}
const systemPieces = SYSTEM_DOCTYPE.exec(html);
if (systemPieces) {
return createDocumentType(globalObject, ownerDocument, systemPieces[1], "", systemPieces[2]);
}
const namePiece = CUSTOM_NAME_DOCTYPE.exec(html)[1] || "html";
return createDocumentType(globalObject, ownerDocument, namePiece, "", "");
}
function createDocumentType(globalObject, ownerDocument, name, publicId, systemId) {
return DocumentType.createImpl(globalObject, [], { ownerDocument, name, publicId, systemId });
}
function isHTMLTemplateElement(element) {
return element.tagName === "template" && element.namespaceURI === HTML_NS;
}
function createParser(rootNode, globalObject, saxesOptions) {
const parser = new SaxesParser({
...saxesOptions,
// Browsers always have namespace support.
xmlns: true,
// We force the parser to treat all documents (even documents declaring themselves to be XML 1.1 documents) as XML
// 1.0 documents. See https://github.com/jsdom/jsdom/issues/2677 for a discussion of the stakes.
defaultXMLVersion: "1.0",
forceXMLVersion: true
});
const openStack = [rootNode];
function getOwnerDocument() {
const currentElement = openStack[openStack.length - 1];
return isHTMLTemplateElement(currentElement) ?
currentElement._templateContents._ownerDocument :
currentElement._ownerDocument;
}
function appendChild(child) {
const parentElement = openStack[openStack.length - 1];
if (isHTMLTemplateElement(parentElement)) {
parentElement._templateContents._insert(child, null);
} else {
parentElement._insert(child, null);
}
}
parser.on("text", saxesOptions.fragment ?
// In a fragment, all text events produced by saxes must result in a text
// node.
data => {
const ownerDocument = getOwnerDocument();
appendChild(Text.createImpl(globalObject, [], { data, ownerDocument }));
} :
// When parsing a whole document, we must ignore those text nodes that are
// produced outside the root element. Saxes produces events for them,
// but DOM trees do not record text outside the root element.
data => {
if (openStack.length > 1) {
const ownerDocument = getOwnerDocument();
appendChild(Text.createImpl(globalObject, [], { data, ownerDocument }));
}
});
parser.on("cdata", data => {
const ownerDocument = getOwnerDocument();
appendChild(CDATASection.createImpl(globalObject, [], { data, ownerDocument }));
});
parser.on("opentag", tag => {
const { local: tagLocal, attributes: tagAttributes } = tag;
const ownerDocument = getOwnerDocument();
const tagNamespace = tag.uri === "" ? null : tag.uri;
const tagPrefix = tag.prefix === "" ? null : tag.prefix;
const isValue = tagAttributes.is === undefined ? null : tagAttributes.is.value;
const elem = createElement(ownerDocument, tagLocal, tagNamespace, tagPrefix, isValue, true);
// We mark a script element as "parser-inserted", which prevents it from
// being immediately executed.
if (tagLocal === "script" && tagNamespace === HTML_NS) {
elem._parserInserted = true;
}
for (const key of Object.keys(tagAttributes)) {
const { prefix, local, uri, value } = tagAttributes[key];
attributes.setAttributeValue(elem, local, value, prefix === "" ? null : prefix, uri === "" ? null : uri);
}
appendChild(elem);
openStack.push(elem);
});
parser.on("closetag", () => {
const elem = openStack.pop();
// Once a script is populated, we can execute it.
if (elem.localName === "script" && elem.namespaceURI === HTML_NS) {
elem._eval();
}
});
parser.on("comment", data => {
const ownerDocument = getOwnerDocument();
appendChild(Comment.createImpl(globalObject, [], { data, ownerDocument }));
});
parser.on("processinginstruction", ({ target, body }) => {
const ownerDocument = getOwnerDocument();
appendChild(ProcessingInstruction.createImpl(globalObject, [], { target, data: body, ownerDocument }));
});
parser.on("doctype", dt => {
const ownerDocument = getOwnerDocument();
appendChild(parseDocType(globalObject, ownerDocument, `<!doctype ${dt}>`));
const entityMatcher = /<!ENTITY ([^ ]+) "([^"]+)">/g;
let result;
while ((result = entityMatcher.exec(dt))) {
const [, name, value] = result;
if (!(name in parser.ENTITIES)) {
parser.ENTITIES[name] = value;
}
}
});
parser.on("error", err => {
throw DOMException.create(globalObject, [err.message, "SyntaxError"]);
});
return parser;
}
function parseFragment(markup, contextElement) {
const { _globalObject, _ownerDocument } = contextElement;
const fragment = DocumentFragment.createImpl(_globalObject, [], { ownerDocument: _ownerDocument });
// Only parseFragment needs resolvePrefix per the saxes documentation:
// https://github.com/lddubeau/saxes#parsing-xml-fragments
const parser = createParser(fragment, _globalObject, {
fragment: true,
resolvePrefix(prefix) {
// saxes wants undefined as the return value if the prefix is not defined, not null.
return contextElement.lookupNamespaceURI(prefix) || undefined;
}
});
parser.write(markup).close();
return fragment;
}
function parseIntoDocument(markup, ownerDocument) {
const { _globalObject } = ownerDocument;
const parser = createParser(ownerDocument, _globalObject, {
fileName: ownerDocument.location && ownerDocument.location.href
});
parser.write(markup).close();
return ownerDocument;
}
module.exports = {
parseFragment,
parseIntoDocument
};
@@ -0,0 +1,114 @@
"use strict";
class QueueItem {
constructor(onLoad, onError, dependentItem) {
this.onLoad = onLoad;
this.onError = onError;
this.data = null;
this.error = null;
this.dependentItem = dependentItem;
}
}
/**
* AsyncResourceQueue is the queue in charge of run the async scripts
* and notify when they finish.
*/
module.exports = class AsyncResourceQueue {
constructor() {
this.items = new Set();
this.dependentItems = new Set();
}
count() {
return this.items.size + this.dependentItems.size;
}
_notify() {
if (this._listener) {
this._listener();
}
}
_check(item) {
let promise;
if (item.onError && item.error) {
promise = item.onError(item.error);
} else if (item.onLoad && item.data) {
promise = item.onLoad(item.data);
}
promise
.then(() => {
this.items.delete(item);
this.dependentItems.delete(item);
if (this.count() === 0) {
this._notify();
}
});
}
setListener(listener) {
this._listener = listener;
}
push(request, onLoad, onError, dependentItem) {
const q = this;
const item = new QueueItem(onLoad, onError, dependentItem);
q.items.add(item);
return request
.then(data => {
item.data = data;
if (dependentItem && !dependentItem.finished) {
q.dependentItems.add(item);
return q.items.delete(item);
}
if (onLoad) {
return q._check(item);
}
q.items.delete(item);
if (q.count() === 0) {
q._notify();
}
return null;
})
.catch(err => {
item.error = err;
if (dependentItem && !dependentItem.finished) {
q.dependentItems.add(item);
return q.items.delete(item);
}
if (onError) {
return q._check(item);
}
q.items.delete(item);
if (q.count() === 0) {
q._notify();
}
return null;
});
}
notifyItem(syncItem) {
for (const item of this.dependentItems) {
if (item.dependentItem === syncItem) {
this._check(item);
}
}
}
};
@@ -0,0 +1,8 @@
"use strict";
const ResourceLoader = require("./resource-loader.js");
module.exports = class NoOpResourceLoader extends ResourceLoader {
fetch() {
return null;
}
};
@@ -0,0 +1,95 @@
"use strict";
const idlUtils = require("../../living/generated/utils");
const { fireAnEvent } = require("../../living/helpers/events");
module.exports = class PerDocumentResourceLoader {
constructor(document) {
this._document = document;
this._defaultEncoding = document._encoding;
this._resourceLoader = document._defaultView ? document._defaultView._resourceLoader : null;
this._requestManager = document._requestManager;
this._queue = document._queue;
this._deferQueue = document._deferQueue;
this._asyncQueue = document._asyncQueue;
}
fetch(url, { element, onLoad, onError }) {
const request = this._resourceLoader.fetch(url, {
cookieJar: this._document._cookieJar,
element: idlUtils.wrapperForImpl(element),
referrer: this._document.URL
});
if (request === null) {
return null;
}
this._requestManager.add(request);
const onErrorWrapped = error => {
this._requestManager.remove(request);
if (onError) {
onError(error);
}
fireAnEvent("error", element);
const err = new Error(`Could not load ${element.localName}: "${url}"`);
err.type = "resource loading";
err.detail = error;
this._document._defaultView._virtualConsole.emit("jsdomError", err);
return Promise.resolve();
};
const onLoadWrapped = data => {
this._requestManager.remove(request);
this._addCookies(url, request.response ? request.response.headers : {});
try {
const result = onLoad ? onLoad(data) : undefined;
return Promise.resolve(result)
.then(() => {
fireAnEvent("load", element);
return Promise.resolve();
})
.catch(err => {
return onErrorWrapped(err);
});
} catch (err) {
return onErrorWrapped(err);
}
};
if (element.localName === "script" && element.hasAttributeNS(null, "async")) {
this._asyncQueue.push(request, onLoadWrapped, onErrorWrapped, this._queue.getLastScript());
} else if (element.localName === "script" && element.hasAttributeNS(null, "defer")) {
this._deferQueue.push(request, onLoadWrapped, onErrorWrapped, false, element);
} else {
this._queue.push(request, onLoadWrapped, onErrorWrapped, false, element);
}
return request;
}
_addCookies(url, headers) {
let cookies = headers["set-cookie"];
if (!cookies) {
return;
}
if (!Array.isArray(cookies)) {
cookies = [cookies];
}
cookies.forEach(cookie => {
this._document._cookieJar.setCookieSync(cookie, url, { http: true, ignoreError: true });
});
}
};
@@ -0,0 +1,33 @@
"use strict";
/**
* Manage all the request and it is able to abort
* all pending request.
*/
module.exports = class RequestManager {
constructor() {
this.openedRequests = [];
}
add(req) {
this.openedRequests.push(req);
}
remove(req) {
const idx = this.openedRequests.indexOf(req);
if (idx !== -1) {
this.openedRequests.splice(idx, 1);
}
}
close() {
for (const openedRequest of this.openedRequests) {
openedRequest.abort();
}
this.openedRequests = [];
}
size() {
return this.openedRequests.length;
}
};
@@ -0,0 +1,142 @@
"use strict";
const fs = require("fs");
const { fileURLToPath } = require("url");
const { parseURL } = require("whatwg-url");
const dataURLFromRecord = require("data-urls").fromURLRecord;
const packageVersion = require("../../../../package.json").version;
const agentFactory = require("../../living/helpers/agent-factory");
const Request = require("../../living/helpers/http-request");
const IS_BROWSER = Object.prototype.toString.call(process) !== "[object process]";
module.exports = class ResourceLoader {
constructor({
strictSSL = true,
proxy = undefined,
userAgent = `Mozilla/5.0 (${process.platform || "unknown OS"}) AppleWebKit/537.36 ` +
`(KHTML, like Gecko) jsdom/${packageVersion}`
} = {}) {
this._strictSSL = strictSSL;
this._proxy = proxy;
this._userAgent = userAgent;
}
_readDataURL(urlRecord) {
const dataURL = dataURLFromRecord(urlRecord);
let timeoutId;
const promise = new Promise(resolve => {
timeoutId = setTimeout(resolve, 0, dataURL.body);
});
promise.abort = () => {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
};
return promise;
}
_readFile(filePath) {
let readableStream, abort; // Native Promises doesn't have an "abort" method.
// Creating a promise for two reason:
// 1. fetch always return a promise.
// 2. We need to add an abort handler.
const promise = new Promise((resolve, reject) => {
readableStream = fs.createReadStream(filePath);
let data = Buffer.alloc(0);
abort = reject;
readableStream.on("error", reject);
readableStream.on("data", chunk => {
data = Buffer.concat([data, chunk]);
});
readableStream.on("end", () => {
resolve(data);
});
});
promise.abort = () => {
readableStream.destroy();
const error = new Error("request canceled by user");
error.isAbortError = true;
abort(error);
};
return promise;
}
fetch(urlString, { accept, cookieJar, referrer } = {}) {
const url = parseURL(urlString);
if (!url) {
return Promise.reject(new Error(`Tried to fetch invalid URL ${urlString}`));
}
switch (url.scheme) {
case "data": {
return this._readDataURL(url);
}
case "http":
case "https": {
const agents = agentFactory(this._proxy, this._strictSSL);
const headers = {
"User-Agent": this._userAgent,
"Accept-Language": "en",
"Accept-Encoding": "gzip",
"Accept": accept || "*/*"
};
if (referrer && !IS_BROWSER) {
headers.Referer = referrer;
}
const requestClient = new Request(
urlString,
{ followRedirects: true, cookieJar, agents },
{ headers }
);
const promise = new Promise((resolve, reject) => {
const accumulated = [];
requestClient.once("response", res => {
promise.response = res;
const { statusCode } = res;
// TODO This deviates from the spec when it comes to
// loading resources such as images
if (statusCode < 200 || statusCode > 299) {
requestClient.abort();
reject(new Error(`Resource was not loaded. Status: ${statusCode}`));
}
});
requestClient.on("data", chunk => {
accumulated.push(chunk);
});
requestClient.on("end", () => resolve(Buffer.concat(accumulated)));
requestClient.on("error", reject);
});
// The method fromURL in lib/api.js crashes without the following four
// properties defined on the Promise instance, causing the test suite to halt
requestClient.on("end", () => {
promise.href = requestClient.currentURL;
});
promise.abort = requestClient.abort.bind(requestClient);
promise.getHeader = name => headers[name] || requestClient.getHeader(name);
requestClient.end();
return promise;
}
case "file": {
try {
return this._readFile(fileURLToPath(urlString));
} catch (e) {
return Promise.reject(e);
}
}
default: {
return Promise.reject(new Error(`Tried to fetch URL ${urlString} with invalid scheme ${url.scheme}`));
}
}
}
};
@@ -0,0 +1,142 @@
"use strict";
/**
* Queue for all the resources to be download except async scripts.
* Async scripts have their own queue AsyncResourceQueue.
*/
module.exports = class ResourceQueue {
constructor({ paused, asyncQueue } = {}) {
this.paused = Boolean(paused);
this._asyncQueue = asyncQueue;
}
getLastScript() {
let head = this.tail;
while (head) {
if (head.isScript) {
return head;
}
head = head.prev;
}
return null;
}
_moreScripts() {
let found = false;
let head = this.tail;
while (head && !found) {
found = head.isScript;
head = head.prev;
}
return found;
}
_notify() {
if (this._listener) {
this._listener();
}
}
setListener(listener) {
this._listener = listener;
}
push(request, onLoad, onError, keepLast, element) {
const isScript = element ? element.localName === "script" : false;
if (!request) {
if (isScript && !this._moreScripts()) {
return onLoad();
}
request = Promise.resolve();
}
const q = this;
const item = {
isScript,
err: null,
element,
fired: false,
data: null,
keepLast,
prev: q.tail,
check() {
if (!q.paused && !this.prev && this.fired) {
let promise;
if (this.err && onError) {
promise = onError(this.err);
}
if (!this.err && onLoad) {
promise = onLoad(this.data);
}
Promise.resolve(promise)
.then(() => {
if (this.next) {
this.next.prev = null;
this.next.check();
} else { // q.tail===this
q.tail = null;
q._notify();
}
this.finished = true;
if (q._asyncQueue) {
q._asyncQueue.notifyItem(this);
}
});
}
}
};
if (q.tail) {
if (q.tail.keepLast) {
// if the tail is the load event in document and we receive a new element to load
// we should add this new request before the load event.
if (q.tail.prev) {
q.tail.prev.next = item;
}
item.prev = q.tail.prev;
q.tail.prev = item;
item.next = q.tail;
} else {
q.tail.next = item;
q.tail = item;
}
} else {
q.tail = item;
}
return request
.then(data => {
item.fired = 1;
item.data = data;
item.check();
})
.catch(err => {
item.fired = true;
item.err = err;
item.check();
});
}
resume() {
if (!this.paused) {
return;
}
this.paused = false;
let head = this.tail;
while (head && head.prev) {
head = head.prev;
}
if (head) {
head.check();
}
}
};
@@ -0,0 +1,57 @@
"use strict";
const cssom = require("cssom");
const cssstyle = require("cssstyle");
exports.addToCore = core => {
// What works now:
// - Accessing the rules defined in individual stylesheets
// - Modifications to style content attribute are reflected in style property
// - Modifications to style property are reflected in style content attribute
// TODO
// - Modifications to style element's textContent are reflected in sheet property.
// - Modifications to style element's sheet property are reflected in textContent.
// - Modifications to link.href property are reflected in sheet property.
// - Less-used features of link: disabled
// - Less-used features of style: disabled, scoped, title
// - CSSOM-View
// - getComputedStyle(): requires default stylesheet, cascading, inheritance,
// filtering by @media (screen? print?), layout for widths/heights
// - Load events are not in the specs, but apparently some browsers
// implement something. Should onload only fire after all @imports have been
// loaded, or only the primary sheet?
core.StyleSheet = cssom.StyleSheet;
core.MediaList = cssom.MediaList;
core.CSSStyleSheet = cssom.CSSStyleSheet;
core.CSSRule = cssom.CSSRule;
core.CSSStyleRule = cssom.CSSStyleRule;
core.CSSMediaRule = cssom.CSSMediaRule;
core.CSSImportRule = cssom.CSSImportRule;
core.CSSStyleDeclaration = cssstyle.CSSStyleDeclaration;
// Relavant specs
// http://www.w3.org/TR/DOM-Level-2-Style (2000)
// http://www.w3.org/TR/cssom-view/ (2008)
// http://dev.w3.org/csswg/cssom/ (2010) Meant to replace DOM Level 2 Style
// http://www.whatwg.org/specs/web-apps/current-work/multipage/ HTML5, of course
// http://dev.w3.org/csswg/css-style-attr/ not sure what's new here
// Objects that aren't in cssom library but should be:
// CSSRuleList (cssom just uses array)
// CSSFontFaceRule
// CSSPageRule
// These rules don't really make sense to implement, so CSSOM draft makes them
// obsolete.
// CSSCharsetRule
// CSSUnknownRule
// These objects are considered obsolete by CSSOM draft, although modern
// browsers implement them.
// CSSValue
// CSSPrimitiveValue
// CSSValueList
// RGBColor
// Rect
// Counter
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,17 @@
"use strict";
const AbortSignal = require("../generated/AbortSignal");
class AbortControllerImpl {
constructor(globalObject) {
this.signal = AbortSignal.createImpl(globalObject, []);
}
abort() {
this.signal._signalAbort();
}
}
module.exports = {
implementation: AbortControllerImpl
};
@@ -0,0 +1,55 @@
"use strict";
const { setupForSimpleEventAccessors } = require("../helpers/create-event-accessor");
const { fireAnEvent } = require("../helpers/events");
const EventTargetImpl = require("../events/EventTarget-impl").implementation;
const AbortSignal = require("../generated/AbortSignal");
class AbortSignalImpl extends EventTargetImpl {
constructor(globalObject, args, privateData) {
super(globalObject, args, privateData);
// make event firing possible
this._ownerDocument = globalObject.document;
this.aborted = false;
this.abortAlgorithms = new Set();
}
static abort(globalObject) {
const abortSignal = AbortSignal.createImpl(globalObject, []);
abortSignal.aborted = true;
return abortSignal;
}
_signalAbort() {
if (this.aborted) {
return;
}
this.aborted = true;
for (const algorithm of this.abortAlgorithms) {
algorithm();
}
this.abortAlgorithms.clear();
fireAnEvent("abort", this);
}
_addAlgorithm(algorithm) {
if (this.aborted) {
return;
}
this.abortAlgorithms.add(algorithm);
}
_removeAlgorithm(algorithm) {
this.abortAlgorithms.delete(algorithm);
}
}
setupForSimpleEventAccessors(AbortSignalImpl.prototype, ["abort"]);
module.exports = {
implementation: AbortSignalImpl
};
@@ -0,0 +1,312 @@
"use strict";
const DOMException = require("domexception/webidl2js-wrapper");
const { HTML_NS } = require("./helpers/namespaces");
const { asciiLowercase } = require("./helpers/strings");
const { queueAttributeMutationRecord } = require("./helpers/mutation-observers");
const { enqueueCECallbackReaction } = require("./helpers/custom-elements");
// The following three are for https://dom.spec.whatwg.org/#concept-element-attribute-has. We don't just have a
// predicate tester since removing that kind of flexibility gives us the potential for better future optimizations.
/* eslint-disable no-restricted-properties */
exports.hasAttribute = function (element, A) {
return element._attributeList.includes(A);
};
exports.hasAttributeByName = function (element, name) {
return element._attributesByNameMap.has(name);
};
exports.hasAttributeByNameNS = function (element, namespace, localName) {
return element._attributeList.some(attribute => {
return attribute._localName === localName && attribute._namespace === namespace;
});
};
// https://dom.spec.whatwg.org/#concept-element-attributes-change
exports.changeAttribute = (element, attribute, value) => {
const { _localName, _namespace, _value } = attribute;
queueAttributeMutationRecord(element, _localName, _namespace, _value);
if (element._ceState === "custom") {
enqueueCECallbackReaction(element, "attributeChangedCallback", [
_localName,
_value,
value,
_namespace
]);
}
attribute._value = value;
// Run jsdom hooks; roughly correspond to spec's "An attribute is set and an attribute is changed."
element._attrModified(attribute._qualifiedName, value, _value);
};
// https://dom.spec.whatwg.org/#concept-element-attributes-append
exports.appendAttribute = function (element, attribute) {
const { _localName, _namespace, _value } = attribute;
queueAttributeMutationRecord(element, _localName, _namespace, null);
if (element._ceState === "custom") {
enqueueCECallbackReaction(element, "attributeChangedCallback", [
_localName,
null,
_value,
_namespace
]);
}
const attributeList = element._attributeList;
attributeList.push(attribute);
attribute._element = element;
// Sync name cache
const name = attribute._qualifiedName;
const cache = element._attributesByNameMap;
let entry = cache.get(name);
if (!entry) {
entry = [];
cache.set(name, entry);
}
entry.push(attribute);
// Run jsdom hooks; roughly correspond to spec's "An attribute is set and an attribute is added."
element._attrModified(name, _value, null);
};
exports.removeAttribute = function (element, attribute) {
// https://dom.spec.whatwg.org/#concept-element-attributes-remove
const { _localName, _namespace, _value } = attribute;
queueAttributeMutationRecord(element, _localName, _namespace, _value);
if (element._ceState === "custom") {
enqueueCECallbackReaction(element, "attributeChangedCallback", [
_localName,
_value,
null,
_namespace
]);
}
const attributeList = element._attributeList;
for (let i = 0; i < attributeList.length; ++i) {
if (attributeList[i] === attribute) {
attributeList.splice(i, 1);
attribute._element = null;
// Sync name cache
const name = attribute._qualifiedName;
const cache = element._attributesByNameMap;
const entry = cache.get(name);
entry.splice(entry.indexOf(attribute), 1);
if (entry.length === 0) {
cache.delete(name);
}
// Run jsdom hooks; roughly correspond to spec's "An attribute is removed."
element._attrModified(name, null, attribute._value);
return;
}
}
};
exports.replaceAttribute = function (element, oldAttr, newAttr) {
// https://dom.spec.whatwg.org/#concept-element-attributes-replace
const { _localName, _namespace, _value } = oldAttr;
queueAttributeMutationRecord(element, _localName, _namespace, _value);
if (element._ceState === "custom") {
enqueueCECallbackReaction(element, "attributeChangedCallback", [
_localName,
_value,
newAttr._value,
_namespace
]);
}
const attributeList = element._attributeList;
for (let i = 0; i < attributeList.length; ++i) {
if (attributeList[i] === oldAttr) {
attributeList.splice(i, 1, newAttr);
oldAttr._element = null;
newAttr._element = element;
// Sync name cache
const name = newAttr._qualifiedName;
const cache = element._attributesByNameMap;
let entry = cache.get(name);
if (!entry) {
entry = [];
cache.set(name, entry);
}
entry.splice(entry.indexOf(oldAttr), 1, newAttr);
// Run jsdom hooks; roughly correspond to spec's "An attribute is set and an attribute is changed."
element._attrModified(name, newAttr._value, oldAttr._value);
return;
}
}
};
exports.getAttributeByName = function (element, name) {
// https://dom.spec.whatwg.org/#concept-element-attributes-get-by-name
if (element._namespaceURI === HTML_NS &&
element._ownerDocument._parsingMode === "html") {
name = asciiLowercase(name);
}
const cache = element._attributesByNameMap;
const entry = cache.get(name);
if (!entry) {
return null;
}
return entry[0];
};
exports.getAttributeByNameNS = function (element, namespace, localName) {
// https://dom.spec.whatwg.org/#concept-element-attributes-get-by-namespace
if (namespace === "") {
namespace = null;
}
const attributeList = element._attributeList;
for (let i = 0; i < attributeList.length; ++i) {
const attr = attributeList[i];
if (attr._namespace === namespace && attr._localName === localName) {
return attr;
}
}
return null;
};
// Both of the following functions implement https://dom.spec.whatwg.org/#concept-element-attributes-get-value.
// Separated them into two to keep symmetry with other functions.
exports.getAttributeValue = function (element, localName) {
const attr = exports.getAttributeByNameNS(element, null, localName);
if (!attr) {
return "";
}
return attr._value;
};
exports.getAttributeValueNS = function (element, namespace, localName) {
const attr = exports.getAttributeByNameNS(element, namespace, localName);
if (!attr) {
return "";
}
return attr._value;
};
exports.setAttribute = function (element, attr) {
// https://dom.spec.whatwg.org/#concept-element-attributes-set
if (attr._element !== null && attr._element !== element) {
throw DOMException.create(element._globalObject, ["The attribute is in use.", "InUseAttributeError"]);
}
const oldAttr = exports.getAttributeByNameNS(element, attr._namespace, attr._localName);
if (oldAttr === attr) {
return attr;
}
if (oldAttr !== null) {
exports.replaceAttribute(element, oldAttr, attr);
} else {
exports.appendAttribute(element, attr);
}
return oldAttr;
};
exports.setAttributeValue = function (element, localName, value, prefix, namespace) {
// https://dom.spec.whatwg.org/#concept-element-attributes-set-value
if (prefix === undefined) {
prefix = null;
}
if (namespace === undefined) {
namespace = null;
}
const attribute = exports.getAttributeByNameNS(element, namespace, localName);
if (attribute === null) {
const newAttribute = element._ownerDocument._createAttribute({
namespace,
namespacePrefix: prefix,
localName,
value
});
exports.appendAttribute(element, newAttribute);
return;
}
exports.changeAttribute(element, attribute, value);
};
// https://dom.spec.whatwg.org/#set-an-existing-attribute-value
exports.setAnExistingAttributeValue = (attribute, value) => {
const element = attribute._element;
if (element === null) {
attribute._value = value;
} else {
exports.changeAttribute(element, attribute, value);
}
};
exports.removeAttributeByName = function (element, name) {
// https://dom.spec.whatwg.org/#concept-element-attributes-remove-by-name
const attr = exports.getAttributeByName(element, name);
if (attr !== null) {
exports.removeAttribute(element, attr);
}
return attr;
};
exports.removeAttributeByNameNS = function (element, namespace, localName) {
// https://dom.spec.whatwg.org/#concept-element-attributes-remove-by-namespace
const attr = exports.getAttributeByNameNS(element, namespace, localName);
if (attr !== null) {
exports.removeAttribute(element, attr);
}
return attr;
};
exports.attributeNames = function (element) {
// Needed by https://dom.spec.whatwg.org/#dom-element-getattributenames
return element._attributeList.map(a => a._qualifiedName);
};
exports.hasAttributes = function (element) {
// Needed by https://dom.spec.whatwg.org/#dom-element-hasattributes
return element._attributeList.length > 0;
};
@@ -0,0 +1,60 @@
"use strict";
const { setAnExistingAttributeValue } = require("../attributes.js");
const NodeImpl = require("../nodes/Node-impl.js").implementation;
const { ATTRIBUTE_NODE } = require("../node-type.js");
exports.implementation = class AttrImpl extends NodeImpl {
constructor(globalObject, args, privateData) {
super(globalObject, args, privateData);
this._namespace = privateData.namespace !== undefined ? privateData.namespace : null;
this._namespacePrefix = privateData.namespacePrefix !== undefined ? privateData.namespacePrefix : null;
this._localName = privateData.localName;
this._value = privateData.value !== undefined ? privateData.value : "";
this._element = privateData.element !== undefined ? privateData.element : null;
this.nodeType = ATTRIBUTE_NODE;
this.specified = true;
}
get namespaceURI() {
return this._namespace;
}
get prefix() {
return this._namespacePrefix;
}
get localName() {
return this._localName;
}
get name() {
return this._qualifiedName;
}
get nodeName() {
return this._qualifiedName;
}
get value() {
return this._value;
}
set value(value) {
setAnExistingAttributeValue(this, value);
}
get ownerElement() {
return this._element;
}
get _qualifiedName() {
// https://dom.spec.whatwg.org/#concept-attribute-qualified-name
if (this._namespacePrefix === null) {
return this._localName;
}
return this._namespacePrefix + ":" + this._localName;
}
};
@@ -0,0 +1,78 @@
"use strict";
const DOMException = require("domexception/webidl2js-wrapper");
const idlUtils = require("../generated/utils.js");
const attributes = require("../attributes.js");
const { HTML_NS } = require("../helpers/namespaces");
exports.implementation = class NamedNodeMapImpl {
constructor(globalObject, args, privateData) {
this._element = privateData.element;
this._globalObject = globalObject;
}
get _attributeList() {
return this._element._attributeList;
}
get [idlUtils.supportedPropertyIndices]() {
return this._attributeList.keys();
}
get length() {
return this._attributeList.length;
}
item(index) {
if (index >= this._attributeList.length) {
return null;
}
return this._attributeList[index];
}
get [idlUtils.supportedPropertyNames]() {
const names = new Set(this._attributeList.map(a => a._qualifiedName));
const el = this._element;
if (el._namespaceURI === HTML_NS && el._ownerDocument._parsingMode === "html") {
for (const name of names) {
const lowercaseName = name.toLowerCase();
if (lowercaseName !== name) {
names.delete(name);
}
}
}
return names;
}
getNamedItem(qualifiedName) {
return attributes.getAttributeByName(this._element, qualifiedName);
}
getNamedItemNS(namespace, localName) {
return attributes.getAttributeByNameNS(this._element, namespace, localName);
}
setNamedItem(attr) {
// eslint-disable-next-line no-restricted-properties
return attributes.setAttribute(this._element, attr);
}
setNamedItemNS(attr) {
// eslint-disable-next-line no-restricted-properties
return attributes.setAttribute(this._element, attr);
}
removeNamedItem(qualifiedName) {
const attr = attributes.removeAttributeByName(this._element, qualifiedName);
if (attr === null) {
throw DOMException.create(this._globalObject, [
"Tried to remove an attribute that was not present",
"NotFoundError"
]);
}
return attr;
}
removeNamedItemNS(namespace, localName) {
const attr = attributes.removeAttributeByNameNS(this._element, namespace, localName);
if (attr === null) {
throw DOMException.create(this._globalObject, [
"Tried to remove an attribute that was not present",
"NotFoundError"
]);
}
return attr;
}
};
@@ -0,0 +1,75 @@
"use strict";
const ValidityState = require("../generated/ValidityState");
const { isDisabled } = require("../helpers/form-controls");
const { closest } = require("../helpers/traversal");
const { fireAnEvent } = require("../helpers/events");
exports.implementation = class DefaultConstraintValidationImpl {
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-cva-willvalidate
get willValidate() {
return this._isCandidateForConstraintValidation();
}
get validity() {
if (!this._validity) {
this._validity = ValidityState.createImpl(this._globalObject, [], {
element: this
});
}
return this._validity;
}
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-cva-checkvalidity
checkValidity() {
if (!this._isCandidateForConstraintValidation()) {
return true;
}
if (this._satisfiesConstraints()) {
return true;
}
fireAnEvent("invalid", this, undefined, { cancelable: true });
return false;
}
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-cva-setcustomvalidity
setCustomValidity(message) {
this._customValidityErrorMessage = message;
}
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-cva-reportvalidity
// Since jsdom has no user interaction, it's the same as #checkValidity
reportValidity() {
return this.checkValidity();
}
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-cva-validationmessage
get validationMessage() {
const { validity } = this;
if (!this._isCandidateForConstraintValidation() || this._satisfiesConstraints()) {
return "";
}
const isSufferingFromCustomError = validity.customError;
if (isSufferingFromCustomError) {
return this._customValidityErrorMessage;
}
return "Constraints not satisfied";
}
_isCandidateForConstraintValidation() {
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-disabled
return !isDisabled(this) &&
// If an element has a datalist element ancestor,
// it is barred from constraint validation.
closest(this, "datalist") === null &&
!this._barredFromConstraintValidationSpecialization();
}
_isBarredFromConstraintValidation() {
return !this._isCandidateForConstraintValidation();
}
_satisfiesConstraints() {
return this.validity.valid;
}
};
@@ -0,0 +1,66 @@
"use strict";
exports.implementation = class ValidityStateImpl {
constructor(globalObject, args, privateData) {
const { element, state = {} } = privateData;
this._element = element;
this._state = state;
}
get badInput() {
return this._failsConstraint("badInput");
}
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#suffering-from-a-custom-error
get customError() {
return this._element._customValidityErrorMessage !== "";
}
get patternMismatch() {
return this._failsConstraint("patternMismatch");
}
get rangeOverflow() {
return this._failsConstraint("rangeOverflow");
}
get rangeUnderflow() {
return this._failsConstraint("rangeUnderflow");
}
get stepMismatch() {
return this._failsConstraint("stepMismatch");
}
get tooLong() {
return this._failsConstraint("tooLong");
}
get tooShort() {
return this._failsConstraint("tooShort");
}
get typeMismatch() {
return this._failsConstraint("typeMismatch");
}
get valueMissing() {
return this._failsConstraint("valueMissing");
}
_failsConstraint(method) {
const validationMethod = this._state[method];
if (validationMethod) {
return validationMethod();
}
return false;
}
get valid() {
return !(this.badInput || this.valueMissing || this.customError ||
this.patternMismatch || this.rangeOverflow || this.rangeUnderflow ||
this.stepMismatch || this.tooLong || this.tooShort || this.typeMismatch);
}
};
@@ -0,0 +1,38 @@
"use strict";
const idlUtils = require("../generated/utils.js");
exports.implementation = class StyleSheetList {
constructor() {
this._list = [];
}
get length() {
return this._list.length;
}
item(index) {
const result = this._list[index];
return result !== undefined ? result : null;
}
get [idlUtils.supportedPropertyIndices]() {
return this._list.keys();
}
_add(sheet) {
const { _list } = this;
if (!_list.includes(sheet)) {
_list.push(sheet);
}
}
_remove(sheet) {
const { _list } = this;
const index = _list.indexOf(sheet);
if (index >= 0) {
_list.splice(index, 1);
}
}
};
@@ -0,0 +1,265 @@
"use strict";
const webIDLConversions = require("webidl-conversions");
const DOMException = require("domexception/webidl2js-wrapper");
const NODE_TYPE = require("../node-type");
const { HTML_NS } = require("../helpers/namespaces");
const { getHTMLElementInterface } = require("../helpers/create-element");
const { shadowIncludingInclusiveDescendantsIterator } = require("../helpers/shadow-dom");
const { isValidCustomElementName, tryUpgradeElement, enqueueCEUpgradeReaction } = require("../helpers/custom-elements");
const idlUtils = require("../generated/utils");
const IDLFunction = require("../generated/Function.js");
const HTMLUnknownElement = require("../generated/HTMLUnknownElement");
const LIFECYCLE_CALLBACKS = [
"connectedCallback",
"disconnectedCallback",
"adoptedCallback",
"attributeChangedCallback"
];
function convertToSequenceDOMString(obj) {
if (!obj || !obj[Symbol.iterator]) {
throw new TypeError("Invalid Sequence");
}
return Array.from(obj).map(webIDLConversions.DOMString);
}
// Returns true is the passed value is a valid constructor.
// Borrowed from: https://stackoverflow.com/a/39336206/3832710
function isConstructor(value) {
if (typeof value !== "function") {
return false;
}
try {
const P = new Proxy(value, {
construct() {
return {};
}
});
// eslint-disable-next-line no-new
new P();
return true;
} catch {
return false;
}
}
// https://html.spec.whatwg.org/#customelementregistry
class CustomElementRegistryImpl {
constructor(globalObject) {
this._customElementDefinitions = [];
this._elementDefinitionIsRunning = false;
this._whenDefinedPromiseMap = Object.create(null);
this._globalObject = globalObject;
}
// https://html.spec.whatwg.org/#dom-customelementregistry-define
define(name, constructor, options) {
const { _globalObject } = this;
const ctor = constructor.objectReference;
if (!isConstructor(ctor)) {
throw new TypeError("Constructor argument is not a constructor.");
}
if (!isValidCustomElementName(name)) {
throw DOMException.create(_globalObject, ["Name argument is not a valid custom element name.", "SyntaxError"]);
}
const nameAlreadyRegistered = this._customElementDefinitions.some(entry => entry.name === name);
if (nameAlreadyRegistered) {
throw DOMException.create(_globalObject, [
"This name has already been registered in the registry.",
"NotSupportedError"
]);
}
const ctorAlreadyRegistered = this._customElementDefinitions.some(entry => entry.objectReference === ctor);
if (ctorAlreadyRegistered) {
throw DOMException.create(_globalObject, [
"This constructor has already been registered in the registry.",
"NotSupportedError"
]);
}
let localName = name;
let extendsOption = null;
if (options !== undefined && options.extends) {
extendsOption = options.extends;
}
if (extendsOption !== null) {
if (isValidCustomElementName(extendsOption)) {
throw DOMException.create(_globalObject, [
"Option extends value can't be a valid custom element name.",
"NotSupportedError"
]);
}
const extendsInterface = getHTMLElementInterface(extendsOption);
if (extendsInterface === HTMLUnknownElement) {
throw DOMException.create(_globalObject, [
`${extendsOption} is an HTMLUnknownElement.`,
"NotSupportedError"
]);
}
localName = extendsOption;
}
if (this._elementDefinitionIsRunning) {
throw DOMException.create(_globalObject, [
"Invalid nested custom element definition.",
"NotSupportedError"
]);
}
this._elementDefinitionIsRunning = true;
let disableShadow = false;
let observedAttributes = [];
const lifecycleCallbacks = {
connectedCallback: null,
disconnectedCallback: null,
adoptedCallback: null,
attributeChangedCallback: null
};
let caughtError;
try {
const { prototype } = ctor;
if (typeof prototype !== "object") {
throw new TypeError("Invalid constructor prototype.");
}
for (const callbackName of LIFECYCLE_CALLBACKS) {
const callbackValue = prototype[callbackName];
if (callbackValue !== undefined) {
lifecycleCallbacks[callbackName] = IDLFunction.convert(callbackValue, {
context: `The lifecycle callback "${callbackName}"`
});
}
}
if (lifecycleCallbacks.attributeChangedCallback !== null) {
const observedAttributesIterable = ctor.observedAttributes;
if (observedAttributesIterable !== undefined) {
observedAttributes = convertToSequenceDOMString(observedAttributesIterable);
}
}
let disabledFeatures = [];
const disabledFeaturesIterable = ctor.disabledFeatures;
if (disabledFeaturesIterable) {
disabledFeatures = convertToSequenceDOMString(disabledFeaturesIterable);
}
disableShadow = disabledFeatures.includes("shadow");
} catch (err) {
caughtError = err;
} finally {
this._elementDefinitionIsRunning = false;
}
if (caughtError !== undefined) {
throw caughtError;
}
const definition = {
name,
localName,
constructor,
objectReference: ctor,
observedAttributes,
lifecycleCallbacks,
disableShadow,
constructionStack: []
};
this._customElementDefinitions.push(definition);
const document = idlUtils.implForWrapper(this._globalObject._document);
const upgradeCandidates = [];
for (const candidate of shadowIncludingInclusiveDescendantsIterator(document)) {
if (
(candidate._namespaceURI === HTML_NS && candidate._localName === localName) &&
(extendsOption === null || candidate._isValue === name)
) {
upgradeCandidates.push(candidate);
}
}
for (const upgradeCandidate of upgradeCandidates) {
enqueueCEUpgradeReaction(upgradeCandidate, definition);
}
if (this._whenDefinedPromiseMap[name] !== undefined) {
this._whenDefinedPromiseMap[name].resolve(ctor);
delete this._whenDefinedPromiseMap[name];
}
}
// https://html.spec.whatwg.org/#dom-customelementregistry-get
get(name) {
const definition = this._customElementDefinitions.find(entry => entry.name === name);
return definition && definition.objectReference;
}
// https://html.spec.whatwg.org/#dom-customelementregistry-whendefined
whenDefined(name) {
if (!isValidCustomElementName(name)) {
return Promise.reject(DOMException.create(
this._globalObject,
["Name argument is not a valid custom element name.", "SyntaxError"]
));
}
const alreadyRegistered = this._customElementDefinitions.find(entry => entry.name === name);
if (alreadyRegistered) {
return Promise.resolve(alreadyRegistered.objectReference);
}
if (this._whenDefinedPromiseMap[name] === undefined) {
let resolve;
const promise = new Promise(r => {
resolve = r;
});
// Store the pending Promise along with the extracted resolve callback to actually resolve the returned Promise,
// once the custom element is registered.
this._whenDefinedPromiseMap[name] = {
promise,
resolve
};
}
return this._whenDefinedPromiseMap[name].promise;
}
// https://html.spec.whatwg.org/#dom-customelementregistry-upgrade
upgrade(root) {
for (const candidate of shadowIncludingInclusiveDescendantsIterator(root)) {
if (candidate.nodeType === NODE_TYPE.ELEMENT_NODE) {
tryUpgradeElement(candidate);
}
}
}
}
module.exports = {
implementation: CustomElementRegistryImpl
};
@@ -0,0 +1,15 @@
"use strict";
const XMLDocument = require("../living/generated/XMLDocument.js");
const Document = require("../living/generated/Document.js");
const { wrapperForImpl } = require("./generated/utils.js");
exports.createImpl = (globalObject, options, { alwaysUseDocumentClass = false } = {}) => {
if (options.parsingMode === "xml" && !alwaysUseDocumentClass) {
return XMLDocument.createImpl(globalObject, [], { options });
}
return Document.createImpl(globalObject, [], { options });
};
exports.createWrapper = (...args) => {
return wrapperForImpl(exports.createImpl(...args));
};
@@ -0,0 +1,58 @@
"use strict";
const { parseIntoDocument } = require("../../browser/parser");
const Document = require("../generated/Document");
exports.implementation = class DOMParserImpl {
constructor(globalObject) {
this._globalObject = globalObject;
}
parseFromString(string, contentType) {
switch (String(contentType)) {
case "text/html": {
return this.createScriptingDisabledDocument("html", contentType, string);
}
case "text/xml":
case "application/xml":
case "application/xhtml+xml":
case "image/svg+xml": {
try {
return this.createScriptingDisabledDocument("xml", contentType, string);
} catch (error) {
const document = this.createScriptingDisabledDocument("xml", contentType);
const element = document.createElementNS("http://www.mozilla.org/newlayout/xml/parsererror.xml", "parsererror");
element.textContent = error.message;
document.appendChild(element);
return document;
}
}
default:
throw new TypeError("Invalid contentType");
}
}
createScriptingDisabledDocument(parsingMode, contentType, string) {
const document = Document.createImpl(this._globalObject, [], {
options: {
parsingMode,
encoding: "UTF-8",
contentType,
readyState: "complete",
scriptingDisabled: true
// TODO: somehow set URL to active document's URL
}
});
if (string !== undefined) {
parseIntoDocument(string, document);
}
return document;
}
};
@@ -0,0 +1,29 @@
"use strict";
const { parseFragment } = require("../../browser/parser");
const { HTML_NS } = require("../helpers/namespaces.js");
const { isShadowRoot } = require("../helpers/shadow-dom.js");
const NODE_TYPE = require("../node-type.js");
const { fragmentSerialization } = require("./serialization.js");
// https://w3c.github.io/DOM-Parsing/#the-innerhtml-mixin
exports.implementation = class InnerHTMLImpl {
// https://w3c.github.io/DOM-Parsing/#dom-innerhtml-innerhtml
get innerHTML() {
return fragmentSerialization(this, {
requireWellFormed: true,
globalObject: this._globalObject
});
}
set innerHTML(markup) {
const contextElement = isShadowRoot(this) ? this.host : this;
const fragment = parseFragment(markup, contextElement);
let contextObject = this;
if (this.nodeType === NODE_TYPE.ELEMENT_NODE && this.localName === "template" && this.namespaceURI === HTML_NS) {
contextObject = this._templateContents;
}
contextObject._replaceAll(fragment);
}
};
@@ -0,0 +1,18 @@
"use strict";
const serialize = require("w3c-xmlserializer");
const DOMException = require("domexception/webidl2js-wrapper");
const utils = require("../generated/utils");
exports.implementation = class XMLSerializerImpl {
constructor(globalObject) {
this._globalObject = globalObject;
}
serializeToString(root) {
try {
return serialize(utils.wrapperForImpl(root), { requireWellFormed: false });
} catch (e) {
throw DOMException.create(this._globalObject, [e.message, "InvalidStateError"]);
}
}
};
@@ -0,0 +1,63 @@
"use strict";
const nodeTypes = require("../node-type");
const { domSymbolTree } = require("../helpers/internal-constants");
// Serialization only requires a subset of the tree adapter interface.
// Tree traversing
exports.getFirstChild = node => node.firstChild;
exports.getChildNodes = node => node.childNodesForSerializing || domSymbolTree.childrenToArray(node);
exports.getParentNode = node => node.parentNode;
exports.getAttrList = element => {
const attributeList = [...element._attributeList];
if (element._isValue && attributeList.every(attr => attr.name !== "is")) {
attributeList.unshift({
name: "is",
namespace: null,
prefix: null,
value: element._isValue
});
}
return attributeList;
};
// Node data
exports.getTagName = element => element._qualifiedName; // https://github.com/inikulin/parse5/issues/231
exports.getNamespaceURI = element => element.namespaceURI;
exports.getTextNodeContent = exports.getCommentNodeContent = node => node.data;
exports.getDocumentTypeNodeName = node => node.name;
exports.getDocumentTypeNodePublicId = node => node.publicId;
exports.getDocumentTypeNodeSystemId = node => node.systemId;
exports.getTemplateContent = templateElement => templateElement._templateContents;
exports.getDocumentMode = document => document._mode;
// Node types
exports.isTextNode = node => node.nodeType === nodeTypes.TEXT_NODE;
exports.isCommentNode = node => node.nodeType === nodeTypes.COMMENT_NODE;
exports.isDocumentTypeNode = node => node.nodeType === nodeTypes.DOCUMENT_TYPE_NODE;
exports.isElementNode = node => node.nodeType === nodeTypes.ELEMENT_NODE;
// Source code location
exports.setNodeSourceCodeLocation = (node, location) => {
node.sourceCodeLocation = location;
};
exports.getNodeSourceCodeLocation = node => node.sourceCodeLocation;
exports.updateNodeSourceCodeLocation = (node, endLocation) => {
Object.assign(node.sourceCodeLocation, endLocation);
};
@@ -0,0 +1,45 @@
"use strict";
const produceXMLSerialization = require("w3c-xmlserializer");
const parse5 = require("parse5");
const DOMException = require("domexception/webidl2js-wrapper");
const utils = require("../generated/utils");
const treeAdapter = require("./parse5-adapter-serialization");
const NODE_TYPE = require("../node-type");
const NAMESPACES = require("../helpers/namespaces");
function htmlSerialization(node) {
if (
node.nodeType === NODE_TYPE.ELEMENT_NODE &&
node.namespaceURI === NAMESPACES.HTML_NS &&
node.tagName === "TEMPLATE"
) {
node = node.content;
}
return parse5.serialize(node, { treeAdapter });
}
module.exports.fragmentSerialization = (node, { requireWellFormed, globalObject }) => {
const contextDocument =
node.nodeType === NODE_TYPE.DOCUMENT_NODE ? node : node._ownerDocument;
if (contextDocument._parsingMode === "html") {
return htmlSerialization(node);
}
const childNodes = node.childNodesForSerializing || node.childNodes;
try {
let serialized = "";
for (let i = 0; i < childNodes.length; ++i) {
serialized += produceXMLSerialization(
utils.wrapperForImpl(childNodes[i] || childNodes.item(i)),
{ requireWellFormed }
);
}
return serialized;
} catch (e) {
throw DOMException.create(globalObject, [e.message, "InvalidStateError"]);
}
};
@@ -0,0 +1,10 @@
"use strict";
const EventImpl = require("./Event-impl").implementation;
const CloseEventInit = require("../generated/CloseEventInit");
class CloseEventImpl extends EventImpl {}
CloseEventImpl.defaultInit = CloseEventInit.convert(undefined);
exports.implementation = CloseEventImpl;
@@ -0,0 +1,20 @@
"use strict";
const UIEventImpl = require("./UIEvent-impl").implementation;
const CompositionEventInit = require("../generated/CompositionEventInit");
class CompositionEventImpl extends UIEventImpl {
initCompositionEvent(type, bubbles, cancelable, view, data) {
if (this._dispatchFlag) {
return;
}
this.initUIEvent(type, bubbles, cancelable, view, 0);
this.data = data;
}
}
CompositionEventImpl.defaultInit = CompositionEventInit.convert(undefined);
module.exports = {
implementation: CompositionEventImpl
};
@@ -0,0 +1,21 @@
"use strict";
const EventImpl = require("./Event-impl").implementation;
const CustomEventInit = require("../generated/CustomEventInit");
class CustomEventImpl extends EventImpl {
initCustomEvent(type, bubbles, cancelable, detail) {
if (this._dispatchFlag) {
return;
}
this.initEvent(type, bubbles, cancelable);
this.detail = detail;
}
}
CustomEventImpl.defaultInit = CustomEventInit.convert(undefined);
module.exports = {
implementation: CustomEventImpl
};
@@ -0,0 +1,14 @@
"use strict";
const EventImpl = require("./Event-impl").implementation;
const ErrorEventInit = require("../generated/ErrorEventInit");
class ErrorEventImpl extends EventImpl {
}
ErrorEventImpl.defaultInit = ErrorEventInit.convert(undefined);
module.exports = {
implementation: ErrorEventImpl
};
@@ -0,0 +1,197 @@
"use strict";
const idlUtils = require("../generated/utils");
const EventInit = require("../generated/EventInit");
class EventImpl {
constructor(globalObject, args, privateData) {
const [type, eventInitDict = this.constructor.defaultInit] = args;
this.type = type;
this.bubbles = false;
this.cancelable = false;
for (const key in eventInitDict) {
if (key in this.constructor.defaultInit) {
this[key] = eventInitDict[key];
}
}
for (const key in this.constructor.defaultInit) {
if (!(key in this)) {
this[key] = this.constructor.defaultInit[key];
}
}
this.target = null;
this.currentTarget = null;
this.eventPhase = 0;
this._globalObject = globalObject;
this._initializedFlag = true;
this._stopPropagationFlag = false;
this._stopImmediatePropagationFlag = false;
this._canceledFlag = false;
this._inPassiveListenerFlag = false;
this._dispatchFlag = false;
this._path = [];
this.isTrusted = privateData.isTrusted || false;
this.timeStamp = Date.now();
}
// https://dom.spec.whatwg.org/#set-the-canceled-flag
_setTheCanceledFlag() {
if (this.cancelable && !this._inPassiveListenerFlag) {
this._canceledFlag = true;
}
}
get srcElement() {
return this.target;
}
get returnValue() {
return !this._canceledFlag;
}
set returnValue(v) {
if (v === false) {
this._setTheCanceledFlag();
}
}
get defaultPrevented() {
return this._canceledFlag;
}
stopPropagation() {
this._stopPropagationFlag = true;
}
get cancelBubble() {
return this._stopPropagationFlag;
}
set cancelBubble(v) {
if (v) {
this._stopPropagationFlag = true;
}
}
stopImmediatePropagation() {
this._stopPropagationFlag = true;
this._stopImmediatePropagationFlag = true;
}
preventDefault() {
this._setTheCanceledFlag();
}
// https://dom.spec.whatwg.org/#dom-event-composedpath
// Current implementation is based of https://whatpr.org/dom/699.html#dom-event-composedpath
// due to a bug in composed path implementation https://github.com/whatwg/dom/issues/684
composedPath() {
const composedPath = [];
const { currentTarget, _path: path } = this;
if (path.length === 0) {
return composedPath;
}
composedPath.push(currentTarget);
let currentTargetIndex = 0;
let currentTargetHiddenSubtreeLevel = 0;
for (let index = path.length - 1; index >= 0; index--) {
const { item, rootOfClosedTree, slotInClosedTree } = path[index];
if (rootOfClosedTree) {
currentTargetHiddenSubtreeLevel++;
}
if (item === idlUtils.implForWrapper(currentTarget)) {
currentTargetIndex = index;
break;
}
if (slotInClosedTree) {
currentTargetHiddenSubtreeLevel--;
}
}
let currentHiddenLevel = currentTargetHiddenSubtreeLevel;
let maxHiddenLevel = currentTargetHiddenSubtreeLevel;
for (let i = currentTargetIndex - 1; i >= 0; i--) {
const { item, rootOfClosedTree, slotInClosedTree } = path[i];
if (rootOfClosedTree) {
currentHiddenLevel++;
}
if (currentHiddenLevel <= maxHiddenLevel) {
composedPath.unshift(idlUtils.wrapperForImpl(item));
}
if (slotInClosedTree) {
currentHiddenLevel--;
if (currentHiddenLevel < maxHiddenLevel) {
maxHiddenLevel = currentHiddenLevel;
}
}
}
currentHiddenLevel = currentTargetHiddenSubtreeLevel;
maxHiddenLevel = currentTargetHiddenSubtreeLevel;
for (let index = currentTargetIndex + 1; index < path.length; index++) {
const { item, rootOfClosedTree, slotInClosedTree } = path[index];
if (slotInClosedTree) {
currentHiddenLevel++;
}
if (currentHiddenLevel <= maxHiddenLevel) {
composedPath.push(idlUtils.wrapperForImpl(item));
}
if (rootOfClosedTree) {
currentHiddenLevel--;
if (currentHiddenLevel < maxHiddenLevel) {
maxHiddenLevel = currentHiddenLevel;
}
}
}
return composedPath;
}
_initialize(type, bubbles, cancelable) {
this.type = type;
this._initializedFlag = true;
this._stopPropagationFlag = false;
this._stopImmediatePropagationFlag = false;
this._canceledFlag = false;
this.isTrusted = false;
this.target = null;
this.bubbles = bubbles;
this.cancelable = cancelable;
}
initEvent(type, bubbles, cancelable) {
if (this._dispatchFlag) {
return;
}
this._initialize(type, bubbles, cancelable);
}
}
EventImpl.defaultInit = EventInit.convert(undefined);
module.exports = {
implementation: EventImpl
};
@@ -0,0 +1,18 @@
"use strict";
// This mixin doesn't have an IDL equivalent, but since MouseEvent and KeyboardEvent implement getModifierState() the
// same way, its implementation is shared here.
class EventModifierMixinImpl {
// Event's constructor assumes all options correspond to IDL attributes with the same names, and sets them on `this`.
// That is not the case for these modifier boolean options, but since the options are set on `this` anyway we'll
// access them that way. The spec doesn't say much about the case where keyArg is not one of the valid ones
// (https://w3c.github.io/uievents-key/#keys-modifier), but at least Chrome returns false for invalid modifiers. Since
// these invalid modifiers will be undefined on `this` (thus `false` after casting it to boolean), we don't need to do
// extra checking for validity.
getModifierState(keyArg) {
return Boolean(this[`modifier${keyArg}`]);
}
}
exports.implementation = EventModifierMixinImpl;
@@ -0,0 +1,403 @@
"use strict";
const DOMException = require("domexception/webidl2js-wrapper");
const reportException = require("../helpers/runtime-script-errors");
const idlUtils = require("../generated/utils");
const { nodeRoot } = require("../helpers/node");
const {
isNode, isShadowRoot, isSlotable, getEventTargetParent,
isShadowInclusiveAncestor, retarget
} = require("../helpers/shadow-dom");
const MouseEvent = require("../generated/MouseEvent");
const EVENT_PHASE = {
NONE: 0,
CAPTURING_PHASE: 1,
AT_TARGET: 2,
BUBBLING_PHASE: 3
};
class EventTargetImpl {
constructor(globalObject) {
this._globalObject = globalObject;
this._eventListeners = Object.create(null);
}
addEventListener(type, callback, options) {
options = normalizeEventHandlerOptions(options, ["capture", "once", "passive"]);
if (callback === null) {
return;
}
if (!this._eventListeners[type]) {
this._eventListeners[type] = [];
}
for (let i = 0; i < this._eventListeners[type].length; ++i) {
const listener = this._eventListeners[type][i];
if (
listener.callback.objectReference === callback.objectReference &&
listener.options.capture === options.capture
) {
return;
}
}
this._eventListeners[type].push({
callback,
options
});
}
removeEventListener(type, callback, options) {
options = normalizeEventHandlerOptions(options, ["capture"]);
if (callback === null) {
// Optimization, not in the spec.
return;
}
if (!this._eventListeners[type]) {
return;
}
for (let i = 0; i < this._eventListeners[type].length; ++i) {
const listener = this._eventListeners[type][i];
if (
listener.callback.objectReference === callback.objectReference &&
listener.options.capture === options.capture
) {
this._eventListeners[type].splice(i, 1);
break;
}
}
}
dispatchEvent(eventImpl) {
if (eventImpl._dispatchFlag || !eventImpl._initializedFlag) {
throw DOMException.create(this._globalObject, [
"Tried to dispatch an uninitialized event",
"InvalidStateError"
]);
}
if (eventImpl.eventPhase !== EVENT_PHASE.NONE) {
throw DOMException.create(this._globalObject, [
"Tried to dispatch a dispatching event",
"InvalidStateError"
]);
}
eventImpl.isTrusted = false;
return this._dispatch(eventImpl);
}
// https://dom.spec.whatwg.org/#get-the-parent
_getTheParent() {
return null;
}
// https://dom.spec.whatwg.org/#concept-event-dispatch
// legacyOutputDidListenersThrowFlag optional parameter is not necessary here since it is only used by indexDB.
_dispatch(eventImpl, targetOverride /* , legacyOutputDidListenersThrowFlag */) {
let targetImpl = this;
let clearTargets = false;
let activationTarget = null;
eventImpl._dispatchFlag = true;
targetOverride = targetOverride || targetImpl;
let relatedTarget = retarget(eventImpl.relatedTarget, targetImpl);
if (targetImpl !== relatedTarget || targetImpl === eventImpl.relatedTarget) {
const touchTargets = [];
appendToEventPath(eventImpl, targetImpl, targetOverride, relatedTarget, touchTargets, false);
const isActivationEvent = MouseEvent.isImpl(eventImpl) && eventImpl.type === "click";
if (isActivationEvent && targetImpl._hasActivationBehavior) {
activationTarget = targetImpl;
}
let slotInClosedTree = false;
let slotable = isSlotable(targetImpl) && targetImpl._assignedSlot ? targetImpl : null;
let parent = getEventTargetParent(targetImpl, eventImpl);
// Populate event path
// https://dom.spec.whatwg.org/#event-path
while (parent !== null) {
if (slotable !== null) {
if (parent.localName !== "slot") {
throw new Error(`JSDOM Internal Error: Expected parent to be a Slot`);
}
slotable = null;
const parentRoot = nodeRoot(parent);
if (isShadowRoot(parentRoot) && parentRoot.mode === "closed") {
slotInClosedTree = true;
}
}
if (isSlotable(parent) && parent._assignedSlot) {
slotable = parent;
}
relatedTarget = retarget(eventImpl.relatedTarget, parent);
if (
(isNode(parent) && isShadowInclusiveAncestor(nodeRoot(targetImpl), parent)) ||
idlUtils.wrapperForImpl(parent).constructor.name === "Window"
) {
if (isActivationEvent && eventImpl.bubbles && activationTarget === null &&
parent._hasActivationBehavior) {
activationTarget = parent;
}
appendToEventPath(eventImpl, parent, null, relatedTarget, touchTargets, slotInClosedTree);
} else if (parent === relatedTarget) {
parent = null;
} else {
targetImpl = parent;
if (isActivationEvent && activationTarget === null && targetImpl._hasActivationBehavior) {
activationTarget = targetImpl;
}
appendToEventPath(eventImpl, parent, targetImpl, relatedTarget, touchTargets, slotInClosedTree);
}
if (parent !== null) {
parent = getEventTargetParent(parent, eventImpl);
}
slotInClosedTree = false;
}
let clearTargetsStructIndex = -1;
for (let i = eventImpl._path.length - 1; i >= 0 && clearTargetsStructIndex === -1; i--) {
if (eventImpl._path[i].target !== null) {
clearTargetsStructIndex = i;
}
}
const clearTargetsStruct = eventImpl._path[clearTargetsStructIndex];
clearTargets =
(isNode(clearTargetsStruct.target) && isShadowRoot(nodeRoot(clearTargetsStruct.target))) ||
(isNode(clearTargetsStruct.relatedTarget) && isShadowRoot(nodeRoot(clearTargetsStruct.relatedTarget)));
if (activationTarget !== null && activationTarget._legacyPreActivationBehavior) {
activationTarget._legacyPreActivationBehavior();
}
for (let i = eventImpl._path.length - 1; i >= 0; --i) {
const struct = eventImpl._path[i];
if (struct.target !== null) {
eventImpl.eventPhase = EVENT_PHASE.AT_TARGET;
} else {
eventImpl.eventPhase = EVENT_PHASE.CAPTURING_PHASE;
}
invokeEventListeners(struct, eventImpl, "capturing");
}
for (let i = 0; i < eventImpl._path.length; i++) {
const struct = eventImpl._path[i];
if (struct.target !== null) {
eventImpl.eventPhase = EVENT_PHASE.AT_TARGET;
} else {
if (!eventImpl.bubbles) {
continue;
}
eventImpl.eventPhase = EVENT_PHASE.BUBBLING_PHASE;
}
invokeEventListeners(struct, eventImpl, "bubbling");
}
}
eventImpl.eventPhase = EVENT_PHASE.NONE;
eventImpl.currentTarget = null;
eventImpl._path = [];
eventImpl._dispatchFlag = false;
eventImpl._stopPropagationFlag = false;
eventImpl._stopImmediatePropagationFlag = false;
if (clearTargets) {
eventImpl.target = null;
eventImpl.relatedTarget = null;
}
if (activationTarget !== null) {
if (!eventImpl._canceledFlag) {
activationTarget._activationBehavior(eventImpl);
} else if (activationTarget._legacyCanceledActivationBehavior) {
activationTarget._legacyCanceledActivationBehavior();
}
}
return !eventImpl._canceledFlag;
}
}
module.exports = {
implementation: EventTargetImpl
};
// https://dom.spec.whatwg.org/#concept-event-listener-invoke
function invokeEventListeners(struct, eventImpl, phase) {
const structIndex = eventImpl._path.indexOf(struct);
for (let i = structIndex; i >= 0; i--) {
const t = eventImpl._path[i];
if (t.target) {
eventImpl.target = t.target;
break;
}
}
eventImpl.relatedTarget = idlUtils.wrapperForImpl(struct.relatedTarget);
if (eventImpl._stopPropagationFlag) {
return;
}
eventImpl.currentTarget = idlUtils.wrapperForImpl(struct.item);
const listeners = struct.item._eventListeners;
innerInvokeEventListeners(eventImpl, listeners, phase, struct.itemInShadowTree);
}
// https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke
function innerInvokeEventListeners(eventImpl, listeners, phase, itemInShadowTree) {
let found = false;
const { type, target } = eventImpl;
const wrapper = idlUtils.wrapperForImpl(target);
if (!listeners || !listeners[type]) {
return found;
}
// Copy event listeners before iterating since the list can be modified during the iteration.
const handlers = listeners[type].slice();
for (let i = 0; i < handlers.length; i++) {
const listener = handlers[i];
const { capture, once, passive } = listener.options;
// Check if the event listener has been removed since the listeners has been cloned.
if (!listeners[type].includes(listener)) {
continue;
}
found = true;
if (
(phase === "capturing" && !capture) ||
(phase === "bubbling" && capture)
) {
continue;
}
if (once) {
listeners[type].splice(listeners[type].indexOf(listener), 1);
}
let window = null;
if (wrapper && wrapper._document) {
// Triggered by Window
window = wrapper;
} else if (target._ownerDocument) {
// Triggered by most webidl2js'ed instances
window = target._ownerDocument._defaultView;
} else if (wrapper._ownerDocument) {
// Currently triggered by some non-webidl2js things
window = wrapper._ownerDocument._defaultView;
}
let currentEvent;
if (window) {
currentEvent = window._currentEvent;
if (!itemInShadowTree) {
window._currentEvent = eventImpl;
}
}
if (passive) {
eventImpl._inPassiveListenerFlag = true;
}
try {
listener.callback.call(eventImpl.currentTarget, eventImpl);
} catch (e) {
if (window) {
reportException(window, e);
}
// Errors in window-less documents just get swallowed... can you think of anything better?
}
eventImpl._inPassiveListenerFlag = false;
if (window) {
window._currentEvent = currentEvent;
}
if (eventImpl._stopImmediatePropagationFlag) {
return found;
}
}
return found;
}
/**
* Normalize the event listeners options argument in order to get always a valid options object
* @param {Object} options - user defined options
* @param {Array} defaultBoolKeys - boolean properties that should belong to the options object
* @returns {Object} object containing at least the "defaultBoolKeys"
*/
function normalizeEventHandlerOptions(options, defaultBoolKeys) {
const returnValue = {};
// no need to go further here
if (typeof options === "boolean" || options === null || typeof options === "undefined") {
returnValue.capture = Boolean(options);
return returnValue;
}
// non objects options so we typecast its value as "capture" value
if (typeof options !== "object") {
returnValue.capture = Boolean(options);
// at this point we don't need to loop the "capture" key anymore
defaultBoolKeys = defaultBoolKeys.filter(k => k !== "capture");
}
for (const key of defaultBoolKeys) {
returnValue[key] = Boolean(options[key]);
}
return returnValue;
}
// https://dom.spec.whatwg.org/#concept-event-path-append
function appendToEventPath(eventImpl, target, targetOverride, relatedTarget, touchTargets, slotInClosedTree) {
const itemInShadowTree = isNode(target) && isShadowRoot(nodeRoot(target));
const rootOfClosedTree = isShadowRoot(target) && target.mode === "closed";
eventImpl._path.push({
item: target,
itemInShadowTree,
target: targetOverride,
relatedTarget,
touchTargets,
rootOfClosedTree,
slotInClosedTree
});
}
@@ -0,0 +1,9 @@
"use strict";
const UIEventImpl = require("./UIEvent-impl").implementation;
const FocusEventInit = require("../generated/FocusEventInit");
class FocusEventImpl extends UIEventImpl {}
FocusEventImpl.defaultInit = FocusEventInit.convert(undefined);
exports.implementation = FocusEventImpl;
@@ -0,0 +1,14 @@
"use strict";
const EventImpl = require("./Event-impl").implementation;
const HashChangeEventInit = require("../generated/HashChangeEventInit");
class HashChangeEventImpl extends EventImpl {
}
HashChangeEventImpl.defaultInit = HashChangeEventInit.convert(undefined);
module.exports = {
implementation: HashChangeEventImpl
};
@@ -0,0 +1,11 @@
"use strict";
const UIEventImpl = require("./UIEvent-impl").implementation;
const InputEventInit = require("../generated/InputEventInit");
// https://w3c.github.io/uievents/#interface-inputevent
class InputEventImpl extends UIEventImpl { }
InputEventImpl.defaultInit = InputEventInit.convert(undefined);
module.exports = {
implementation: InputEventImpl
};
@@ -0,0 +1,29 @@
"use strict";
const { mixin } = require("../../utils");
const EventModifierMixinImpl = require("./EventModifierMixin-impl").implementation;
const UIEventImpl = require("./UIEvent-impl").implementation;
const KeyboardEventInit = require("../generated/KeyboardEventInit");
class KeyboardEventImpl extends UIEventImpl {
initKeyboardEvent(type, bubbles, cancelable, view, key, location, ctrlKey, altKey, shiftKey, metaKey) {
if (this._dispatchFlag) {
return;
}
this.initUIEvent(type, bubbles, cancelable, view, 0);
this.key = key;
this.location = location;
this.ctrlKey = ctrlKey;
this.altKey = altKey;
this.shiftKey = shiftKey;
this.metaKey = metaKey;
}
}
mixin(KeyboardEventImpl.prototype, EventModifierMixinImpl.prototype);
KeyboardEventImpl.defaultInit = KeyboardEventInit.convert(undefined);
module.exports = {
implementation: KeyboardEventImpl
};
@@ -0,0 +1,25 @@
"use strict";
const EventImpl = require("./Event-impl").implementation;
const MessageEventInit = require("../generated/MessageEventInit");
class MessageEventImpl extends EventImpl {
initMessageEvent(type, bubbles, cancelable, data, origin, lastEventId, source, ports) {
if (this._dispatchFlag) {
return;
}
this.initEvent(type, bubbles, cancelable);
this.data = data;
this.origin = origin;
this.lastEventId = lastEventId;
this.source = source;
this.ports = ports;
}
}
MessageEventImpl.defaultInit = MessageEventInit.convert(undefined);
module.exports = {
implementation: MessageEventImpl
};
@@ -0,0 +1,49 @@
"use strict";
const { mixin } = require("../../utils");
const EventModifierMixinImpl = require("./EventModifierMixin-impl").implementation;
const UIEventImpl = require("./UIEvent-impl").implementation;
const MouseEventInit = require("../generated/MouseEventInit");
class MouseEventImpl extends UIEventImpl {
initMouseEvent(
type,
bubbles,
cancelable,
view,
detail,
screenX,
screenY,
clientX,
clientY,
ctrlKey,
altKey,
shiftKey,
metaKey,
button,
relatedTarget
) {
if (this._dispatchFlag) {
return;
}
this.initUIEvent(type, bubbles, cancelable, view, detail);
this.screenX = screenX;
this.screenY = screenY;
this.clientX = clientX;
this.clientY = clientY;
this.ctrlKey = ctrlKey;
this.altKey = altKey;
this.shiftKey = shiftKey;
this.metaKey = metaKey;
this.button = button;
this.relatedTarget = relatedTarget;
}
}
mixin(MouseEventImpl.prototype, EventModifierMixinImpl.prototype);
MouseEventImpl.defaultInit = MouseEventInit.convert(undefined);
module.exports = {
implementation: MouseEventImpl
};
@@ -0,0 +1,20 @@
"use strict";
const EventImpl = require("./Event-impl").implementation;
const PageTransitionEventInit = require("../generated/PageTransitionEventInit");
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#pagetransitionevent
class PageTransitionEventImpl extends EventImpl {
initPageTransitionEvent(type, bubbles, cancelable, persisted) {
if (this._dispatchFlag) {
return;
}
this.initEvent(type, bubbles, cancelable);
this.persisted = persisted;
}
}
PageTransitionEventImpl.defaultInit = PageTransitionEventInit.convert(undefined);
exports.implementation = PageTransitionEventImpl;
@@ -0,0 +1,9 @@
"use strict";
const EventImpl = require("./Event-impl.js").implementation;
const PopStateEventInit = require("../generated/PopStateEventInit");
class PopStateEventImpl extends EventImpl {}
PopStateEventImpl.defaultInit = PopStateEventInit.convert(undefined);
exports.implementation = PopStateEventImpl;
@@ -0,0 +1,14 @@
"use strict";
const EventImpl = require("./Event-impl").implementation;
const ProgressEventInit = require("../generated/ProgressEventInit");
class ProgressEventImpl extends EventImpl {
}
ProgressEventImpl.defaultInit = ProgressEventInit.convert(undefined);
module.exports = {
implementation: ProgressEventImpl
};
@@ -0,0 +1,26 @@
"use strict";
const EventImpl = require("./Event-impl").implementation;
const StorageEventInit = require("../generated/StorageEventInit");
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
class StorageEventImpl extends EventImpl {
initStorageEvent(type, bubbles, cancelable, key, oldValue, newValue, url, storageArea) {
if (this._dispatchFlag) {
return;
}
this.initEvent(type, bubbles, cancelable);
this.key = key;
this.oldValue = oldValue;
this.newValue = newValue;
this.url = url;
this.storageArea = storageArea;
}
}
StorageEventImpl.defaultInit = StorageEventInit.convert(undefined);
module.exports = {
implementation: StorageEventImpl
};

Some files were not shown because too many files have changed in this diff Show More