Typescript conversion (#1828)

* initial typescript conversion

* test: update overflow+transform ref test

* fix: correctly render pseudo element content

* fix: testrunner build

* fix: karma test urls

* test: update underline tests with <u> elements

* test: update to es6-promise polyfill

* test: remove watch from server

* test: remove flow

* format: update prettier for typescript

* test: update eslint to use typescript parser

* test: update linear gradient reftest

* test: update test runner

* test: update testrunner promise polyfill

* fix: handle display: -webkit-flex correctly (fix #1817)

* fix: correctly render gradients with clip & repeat (fix #1773)

* fix: webkit-gradient function support

* fix: implement radial gradients

* fix: text-decoration rendering

* fix: missing scroll positions for elements

* ci: fix ios 11 tests

* fix: ie logging

* ci: improve device availability logging

* fix: lint errors

* ci: update to ios 12

* fix: check for console availability

* ci: fix build dependency

* test: update text reftests

* fix: window reference for unit tests

* feat: add hsl/hsla color support

* fix: render options

* fix: CSSKeyframesRule cssText Permission Denied on Internet Explorer 11 (#1830)

* fix: option lint

* fix: list type rendering

* test: fix platform import

* fix: ie css parsing for numbers

* ci: add minified build

* fix: form element rendering

* fix: iframe rendering

* fix: re-introduce experimental foreignobject renderer

* fix: text-shadow rendering

* feat: improve logging

* fix: unit test logging

* fix: cleanup resources

* test: update overflow scrolling to work with ie

* build: update build to include typings

* fix: do not parse select element children

* test: fix onclone test to work with older IEs

* test: reduce reftest canvas sizes

* test: remove dynamic setUp from list tests

* test: update linear-gradient tests

* build: remove old source files

* build: update docs dependencies

* build: fix typescript definition path

* ci: include test.js on docs website
This commit is contained in:
Niklas von Hertzen 2019-05-25 15:54:41 -07:00 committed by GitHub
parent 20a797cbeb
commit 522a443055
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
221 changed files with 13668 additions and 23699 deletions

View File

@ -1,23 +1,27 @@
{
"parser": "babel-eslint",
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended",
],
"parserOptions": {
"project": "./tsconfig.json",
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"flowtype",
"@typescript-eslint",
"prettier"
],
"rules": {
"no-console": ["error", { "allow": ["warn", "error"] }],
"flowtype/boolean-style": [
2,
"boolean"
],
"flowtype/no-weak-types": 2,
"flowtype/delimiter-dangle": 2,
"prettier/prettier": ["error", {
"singleQuote": true,
"bracketSpacing": false,
"parser": "flow",
"tabWidth": 4,
"printWidth": 100
}]
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/class-name-casing": "off",
"prettier/prettier": "error"
}
}

View File

@ -1,8 +0,0 @@
[ignore]
.*/www/.*
.*/node_modules/@webassemblyjs/.*
[include]
[libs]
./flow-typed
[options]
[lints]

1
.gitignore vendored
View File

@ -16,3 +16,4 @@ npm-debug.log
debug.log
tests/reftests.js
*.log
.rpt2_cache

View File

@ -19,3 +19,4 @@ karma.js
karma.conf.js
rollup.config.js
webpack.config.js
.rpt2_cache

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"trailingComma": "none",
"tabWidth": 4,
"bracketSpacing": false,
"singleQuote": true,
"printWidth": 120
}

View File

@ -51,9 +51,7 @@ jobs:
displayName: Build
- script: npm run lint
displayName: Lint
- script: npm run flow
displayName: Flow
- script: npm run test:node
- script: npm run unittest
displayName: Unit tests
- template: ci/browser-tests.yml
@ -88,10 +86,10 @@ jobs:
- template: ci/browser-tests.yml
parameters:
name: Browser_Tests_OSX_Safari_IOS_11
displayName: iOS Simulator Safari 11
name: Browser_Tests_OSX_Safari_IOS_12
displayName: iOS Simulator Safari 12
vmImage: 'macOS-10.13'
targetBrowser: Safari_IOS_11
targetBrowser: Safari_IOS_12
- template: ci/browser-tests.yml
parameters:
@ -130,7 +128,7 @@ jobs:
- Browser_Tests_Linux_Chrome_Stable
- Browser_Tests_OSX_Safari_IOS_9
- Browser_Tests_OSX_Safari_IOS_10
- Browser_Tests_OSX_Safari_IOS_11
- Browser_Tests_OSX_Safari_IOS_12
- Browser_Tests_OSX_Safari_Stable
- Browser_Tests_Windows_IE9
- Browser_Tests_Windows_IE10
@ -153,7 +151,7 @@ jobs:
inputs:
artifactName: dist
downloadPath: $(System.DefaultWorkingDirectory)
- script: cp -R tests/reftests www/static/tests/reftests && cp -R tests/assets www/static/tests/assets && cp -R ReftestResults ./www/static/results
- script: cp -R tests/reftests www/static/tests/reftests && cp -R tests/assets www/static/tests/assets && cp tests/test.js www/static/tests/test.js && cp -R ReftestResults ./www/static/results
displayName: Copy reftests to docs website
- script: cp -R dist ./www/static/dist
displayName: Copy dist to docs website

View File

@ -1,11 +0,0 @@
declare var __DEV__: boolean;
declare var __VERSION__: string;
declare class SVGSVGElement extends Element {
className: string;
style: CSSStyleDeclaration;
getPresentationAttribute(name: string): any;
}
declare class HTMLBodyElement extends HTMLElement {}

View File

@ -20,10 +20,10 @@ module.exports = function(config) {
name: 'iPhone 5s',
sdk: '10.0'
},
Safari_IOS_11: {
Safari_IOS_12: {
base: 'MobileSafari',
name: 'iPhone 5s',
sdk: '11.4'
sdk: '12.1'
},
SauceLabs_IE9: {
base: 'SauceLabs',
@ -132,6 +132,7 @@ module.exports = function(config) {
if (!d) {
log.error(`No device found for sdk ${args.sdk} with name ${args.name}`);
log.info(`Available devices:`, devices);
this._process.kill();
return;
}
@ -172,7 +173,6 @@ module.exports = function(config) {
// list of files / patterns to load in the browser
files: [
'build/testrunner.js',
'build/RefTestRenderer.js',
{ pattern: './tests/**/*', 'watched': true, 'included': false, 'served': true},
{ pattern: './dist/**/*', 'watched': true, 'included': false, 'served': true},
{ pattern: './node_modules/**/*', 'watched': true, 'included': false, 'served': true},

649
package-lock.json generated
View File

@ -1487,30 +1487,135 @@
"integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==",
"dev": true
},
"@types/chai": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz",
"integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==",
"dev": true
},
"@types/core-js": {
"version": "0.9.46",
"resolved": "https://registry.npmjs.org/@types/core-js/-/core-js-0.9.46.tgz",
"integrity": "sha512-LooLR6XHes9V+kNYRz1Qm8w3atw9QMn7XeZUmIpUelllF9BdryeUKd/u0Wh5ErcjpWfG39NrToU9MF7ngsTFVw==",
"dev": true
},
"@types/estree": {
"version": "0.0.39",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
"dev": true
},
"@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
"dev": true
},
"@types/glob": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
"integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==",
"dev": true,
"requires": {
"@types/events": "*",
"@types/minimatch": "*",
"@types/node": "*"
}
},
"@types/minimatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
"integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
"dev": true
},
"@types/mkdirp": {
"version": "0.3.29",
"resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-0.3.29.tgz",
"integrity": "sha1-fyrX7FX5FEgvybHsS7GuYCjUYGY=",
"dev": true
},
"@types/mocha": {
"version": "5.2.6",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.6.tgz",
"integrity": "sha512-1axi39YdtBI7z957vdqXI4Ac25e7YihYQtJa+Clnxg1zTJEaIRbndt71O3sP4GAMgiAm0pY26/b9BrY4MR/PMw==",
"dev": true
},
"@types/node": {
"version": "11.13.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.2.tgz",
"integrity": "sha512-HOtU5KqROKT7qX/itKHuTtt5fV0iXbheQvrgbLNXFJQBY/eh+VS5vmmTAVlo3qIGMsypm0G4N1t2AXjy1ZicaQ==",
"dev": true
},
"@types/platform": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/platform/-/platform-1.3.2.tgz",
"integrity": "sha512-Tn6OuJDAG7bJbyi4R7HqcxXp1w2lmIxVXqyNhPt1Bm0FO2EWIi3CI87JVzF7ncqK0ZMPuUycS3wTMIk85EeF1Q==",
"dev": true
},
"@types/promise-polyfill": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/promise-polyfill/-/promise-polyfill-6.0.3.tgz",
"integrity": "sha512-f/BFgF9a+cgsMseC7rpv9+9TAE3YNjhfYrtwCo/pIeCDDfQtE6PY0b5bao2eIIEpZCBUy8Y5ToXd4ObjPSJuFw==",
"dev": true
},
"@types/resolve": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
"integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/rimraf": {
"version": "0.0.28",
"resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-0.0.28.tgz",
"integrity": "sha1-VWJRm8eWPKyoq/fxKMrjtZTUHQY=",
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.7.0.tgz",
"integrity": "sha512-NUSz1aTlIzzTjFFVFyzrbo8oFjHg3K/M9MzYByqbMCxeFdErhLAcGITVfXzSz+Yvp5OOpMu3HkIttB0NyKl54Q==",
"dev": true,
"requires": {
"@typescript-eslint/parser": "1.7.0",
"@typescript-eslint/typescript-estree": "1.7.0",
"eslint-utils": "^1.3.1",
"regexpp": "^2.0.1",
"requireindex": "^1.2.0",
"tsutils": "^3.7.0"
}
},
"@typescript-eslint/parser": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.7.0.tgz",
"integrity": "sha512-1QFKxs2V940372srm12ovSE683afqc1jB6zF/f8iKhgLz1yoSjYeGHipasao33VXKI+0a/ob9okeogGdKGvvlg==",
"dev": true,
"requires": {
"@typescript-eslint/typescript-estree": "1.7.0",
"eslint-scope": "^4.0.0",
"eslint-visitor-keys": "^1.0.0"
}
},
"@typescript-eslint/typescript-estree": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.7.0.tgz",
"integrity": "sha512-K5uedUxVmlYrVkFbyV3htDipvLqTE3QMOUQEHYJaKtgzxj6r7c5Ca/DG1tGgFxX+fsbi9nDIrf4arq7Ib7H/Yw==",
"dev": true,
"requires": {
"lodash.unescape": "4.0.1",
"semver": "5.5.0"
},
"dependencies": {
"semver": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
"dev": true
}
}
},
"@webassemblyjs/ast": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
@ -2755,9 +2860,9 @@
}
},
"base64-arraybuffer": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
"integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz",
"integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ=="
},
"base64-js": {
"version": "1.3.0",
@ -3063,6 +3168,12 @@
"integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
"dev": true
},
"builtin-modules": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz",
"integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==",
"dev": true
},
"builtin-status-codes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
@ -4559,11 +4670,11 @@
}
},
"css-line-break": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.0.1.tgz",
"integrity": "sha1-GfIGOjPpX7KDG4ZEbAuAwYivRQo=",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz",
"integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==",
"requires": {
"base64-arraybuffer": "^0.1.5"
"base64-arraybuffer": "^0.2.0"
}
},
"csv-parser": {
@ -5135,6 +5246,14 @@
"base64-arraybuffer": "0.1.5",
"blob": "0.0.5",
"has-binary2": "~1.0.2"
},
"dependencies": {
"base64-arraybuffer": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
"integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=",
"dev": true
}
}
},
"enhanced-resolve": {
@ -5295,26 +5414,6 @@
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@ -5337,50 +5436,18 @@
"ms": "^2.1.1"
}
},
"eslint-scope": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
"integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
"dev": true,
"requires": {
"esrecurse": "^4.1.0",
"estraverse": "^4.1.1"
}
},
"fast-deep-equal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
"dev": true
},
"globals": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.11.0.tgz",
"integrity": "sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==",
"dev": true
},
"js-yaml": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
@ -5401,35 +5468,33 @@
"requires": {
"ansi-regex": "^3.0.0"
}
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"eslint-plugin-flowtype": {
"version": "2.35.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.35.0.tgz",
"integrity": "sha512-zjXGjOsHds8b84C0Ad3VViKh+sUA9PeXKHwPRlSLwwSX0v1iUJf/6IX7wxc+w2T2tnDH8PT6B/YgtcEuNI3ssA==",
"eslint-config-prettier": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-4.2.0.tgz",
"integrity": "sha512-y0uWc/FRfrHhpPZCYflWC8aE0KRJRY04rdZVfl8cL3sEZmOYyaBdhdlQPjKZBnuRMyLVK+JUZr7HaZFClQiH4w==",
"dev": true,
"requires": {
"lodash": "^4.15.0"
"get-stdin": "^6.0.0"
},
"dependencies": {
"get-stdin": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
"integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==",
"dev": true
}
}
},
"eslint-plugin-prettier": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.1.2.tgz",
"integrity": "sha1-S5D07n+Sv74ukmAX4cpA62KJZeo=",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.0.1.tgz",
"integrity": "sha512-/PMttrarPAY78PLvV3xfWibMOdMDl57hmlQ2XqFeA37wd+CJ7WSxV7txqjVPHi/AAFKd2lX0ZqfsOc/i5yFCSQ==",
"dev": true,
"requires": {
"fast-diff": "^1.1.1",
"jest-docblock": "^20.0.1"
"prettier-linter-helpers": "^1.0.0"
}
},
"eslint-scope": {
@ -5463,14 +5528,6 @@
"acorn": "^6.0.7",
"acorn-jsx": "^5.0.0",
"eslint-visitor-keys": "^1.0.0"
},
"dependencies": {
"acorn": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz",
"integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==",
"dev": true
}
}
},
"esprima": {
@ -5503,6 +5560,12 @@
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
"dev": true
},
"estree-walker": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.0.tgz",
"integrity": "sha512-peq1RfVAVzr3PU/jL31RaOjUKLoZJpObQWJJ+LgfcxDUifyLZ1RjPQZTl0pzj2uJ45b7A7XpyppXvxdEqzo4rw==",
"dev": true
},
"esutils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
@ -5855,9 +5918,9 @@
"dev": true
},
"fast-diff": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
"dev": true
},
"fast-json-stable-stringify": {
@ -6385,20 +6448,6 @@
"write": "1.0.3"
},
"dependencies": {
"glob": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"rimraf": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
@ -6416,12 +6465,6 @@
"integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==",
"dev": true
},
"flow-bin": {
"version": "0.56.0",
"resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.56.0.tgz",
"integrity": "sha1-zkMJIgOjRLqb9jwMq+ldlRRfbK0=",
"dev": true
},
"flush-write-stream": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz",
@ -7499,9 +7542,9 @@
}
},
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
@ -7599,6 +7642,12 @@
"which": "^1.2.14"
}
},
"globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true
},
"graceful-fs": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
@ -7612,9 +7661,9 @@
"dev": true
},
"handlebars": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.1.tgz",
"integrity": "sha512-3Zhi6C0euYZL5sM0Zcy7lInLXKQ+YLcF/olbN010mzGQ4XVm50JeyBnMqofHh696GrciGruC7kCcApPDJvVgwA==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
"integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
"dev": true,
"requires": {
"neo-async": "^2.6.0",
@ -7623,19 +7672,6 @@
"uglify-js": "^3.1.4"
},
"dependencies": {
"commander": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
"integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==",
"dev": true,
"optional": true
},
"neo-async": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz",
"integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -7643,13 +7679,13 @@
"dev": true
},
"uglify-js": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.3.tgz",
"integrity": "sha512-rIQPT2UMDnk4jRX+w4WO84/pebU2jiLsjgIyrCktYgSvx28enOE3iYQMr+BD1rHiitWnDmpu0cY/LfIEpKcjcw==",
"version": "3.5.9",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.9.tgz",
"integrity": "sha512-WpT0RqsDtAWPNJK955DEnb6xjymR8Fn0OlK4TT4pS0ASYsVPqr5ELhgwOwLCP5J5vHeJ4xmMmz3DEgdqC10JeQ==",
"dev": true,
"optional": true,
"requires": {
"commander": "~2.19.0",
"commander": "~2.20.0",
"source-map": "~0.6.1"
}
}
@ -8101,9 +8137,9 @@
"dev": true
},
"inquirer": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz",
"integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==",
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.3.1.tgz",
"integrity": "sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA==",
"dev": true,
"requires": {
"ansi-escapes": "^3.2.0",
@ -8117,7 +8153,7 @@
"run-async": "^2.2.0",
"rxjs": "^6.4.0",
"string-width": "^2.1.0",
"strip-ansi": "^5.0.0",
"strip-ansi": "^5.1.0",
"through": "^2.3.6"
},
"dependencies": {
@ -8127,32 +8163,6 @@
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
@ -8161,15 +8171,6 @@
"requires": {
"ansi-regex": "^4.1.0"
}
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
@ -8342,6 +8343,12 @@
"integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU=",
"dev": true
},
"is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
"dev": true
},
"is-negated-glob": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz",
@ -8534,12 +8541,6 @@
"is-object": "^1.0.1"
}
},
"jest-docblock": {
"version": "20.0.3",
"resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-20.0.3.tgz",
"integrity": "sha1-F76phDQswz2DxQ++FUXqDvqkRxI=",
"dev": true
},
"jimp": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/jimp/-/jimp-0.6.1.tgz",
@ -8566,9 +8567,9 @@
"dev": true
},
"jquery": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.2.1.tgz",
"integrity": "sha1-XE2d5lKvbNCncBVKYxu6ErAVx4c=",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.0.tgz",
"integrity": "sha512-ggRCXln9zEqv6OqAGXFEcshF5dSBvCkzj6Gm2gzuR5fWawaX8t7cxKVkkygKODrDAzKdoYw3l/e3pm3vlT4IbQ==",
"dev": true
},
"js-levenshtein": {
@ -8590,9 +8591,9 @@
"dev": true
},
"js-yaml": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.0.tgz",
"integrity": "sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==",
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
@ -9403,6 +9404,12 @@
"lodash._reinterpolate": "~3.0.0"
}
},
"lodash.unescape": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz",
"integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=",
"dev": true
},
"log-symbols": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
@ -9477,6 +9484,15 @@
"yallist": "^2.1.2"
}
},
"magic-string": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.2.tgz",
"integrity": "sha512-iLs9mPjh9IuTtRsqqhNGYcZXGei0Nh/A4xirrsqW7c+QhKVFL2vm7U09ru6cHRD22azaP/wMDgI+HCqbETMTtg==",
"dev": true,
"requires": {
"sourcemap-codec": "^1.4.4"
}
},
"make-dir": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
@ -9844,9 +9860,9 @@
}
},
"mocha": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.0.tgz",
"integrity": "sha512-cyKQPahVzaWsCgH86yWjKKxVgAKeN9MsyooMXmJtJa4nLbWxvXXjnPZU0cr9qRVOutirgfOVDzhVqorm8BBYKQ==",
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz",
"integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==",
"dev": true,
"requires": {
"ansi-colors": "3.2.3",
@ -9858,12 +9874,12 @@
"glob": "7.1.3",
"growl": "1.10.5",
"he": "1.2.0",
"js-yaml": "3.13.0",
"js-yaml": "3.13.1",
"log-symbols": "2.2.0",
"minimatch": "3.0.4",
"mkdirp": "0.5.1",
"ms": "2.1.1",
"node-environment-flags": "1.0.4",
"node-environment-flags": "1.0.5",
"object.assign": "4.1.0",
"strip-json-comments": "2.0.1",
"supports-color": "6.0.0",
@ -10359,12 +10375,21 @@
"dev": true
},
"node-environment-flags": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.4.tgz",
"integrity": "sha512-M9rwCnWVLW7PX+NUWe3ejEdiLYinRpsEre9hMkU/6NS4h+EEulYaDH1gCEZ2gyXsmw+RXYDaV2JkkTNcsPDJ0Q==",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz",
"integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==",
"dev": true,
"requires": {
"object.getownpropertydescriptors": "^2.0.3"
"object.getownpropertydescriptors": "^2.0.3",
"semver": "^5.7.0"
},
"dependencies": {
"semver": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
"dev": true
}
}
},
"node-libs-browser": {
@ -11101,11 +11126,20 @@
"dev": true
},
"prettier": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.5.3.tgz",
"integrity": "sha1-WdrcaDNF7GuI+IuU7Urn4do5S/4=",
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.17.0.tgz",
"integrity": "sha512-sXe5lSt2WQlCbydGETgfm1YBShgOX4HxQkFPvbxkcwgDvGDeqVau8h+12+lmSVlP3rHPz0oavfddSZg/q+Szjw==",
"dev": true
},
"prettier-linter-helpers": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
"dev": true,
"requires": {
"fast-diff": "^1.1.2"
}
},
"private": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
@ -11145,12 +11179,6 @@
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
"dev": true
},
"promise-polyfill": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.0.2.tgz",
"integrity": "sha1-2chtPcTcLfkBboiUbe/Wm0m0EWI=",
"dev": true
},
"proxy-addr": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
@ -11531,6 +11559,12 @@
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
"dev": true
},
"requireindex": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
"integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
"dev": true
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@ -11635,6 +11669,151 @@
"inherits": "^2.0.1"
}
},
"rollup": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-1.10.1.tgz",
"integrity": "sha512-pW353tmBE7QP622ITkGxtqF0d5gSRCVPD9xqM+fcPjudeZfoXMFW2sCzsTe2TU/zU1xamIjiS9xuFCPVT9fESw==",
"dev": true,
"requires": {
"@types/estree": "0.0.39",
"@types/node": "^11.13.5",
"acorn": "^6.1.1"
},
"dependencies": {
"@types/node": {
"version": "11.13.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.8.tgz",
"integrity": "sha512-szA3x/3miL90ZJxUCzx9haNbK5/zmPieGraZEe4WI+3srN0eGLiT22NXeMHmyhNEopn+IrxqMc7wdVwvPl8meg==",
"dev": true
}
}
},
"rollup-plugin-commonjs": {
"version": "9.3.4",
"resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.3.4.tgz",
"integrity": "sha512-DTZOvRoiVIHHLFBCL4pFxOaJt8pagxsVldEXBOn6wl3/V21wVaj17HFfyzTsQUuou3sZL3lEJZVWKPFblJfI6w==",
"dev": true,
"requires": {
"estree-walker": "^0.6.0",
"magic-string": "^0.25.2",
"resolve": "^1.10.0",
"rollup-pluginutils": "^2.6.0"
},
"dependencies": {
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"resolve": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz",
"integrity": "sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
}
}
},
"rollup-plugin-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-json/-/rollup-plugin-json-4.0.0.tgz",
"integrity": "sha512-hgb8N7Cgfw5SZAkb3jf0QXii6QX/FOkiIq2M7BAQIEydjHvTyxXHQiIzZaTFgx1GK0cRCHOCBHIyEkkLdWKxow==",
"dev": true,
"requires": {
"rollup-pluginutils": "^2.5.0"
}
},
"rollup-plugin-node-resolve": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-4.2.3.tgz",
"integrity": "sha512-r+WaesPzdGEynpLZLALFEDugA4ACa5zn7bc/+LVX4vAXQQ8IgDHv0xfsSvJ8tDXUtprfBtrDtRFg27ifKjcJTg==",
"dev": true,
"requires": {
"@types/resolve": "0.0.8",
"builtin-modules": "^3.1.0",
"is-module": "^1.0.0",
"resolve": "^1.10.0"
},
"dependencies": {
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"resolve": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz",
"integrity": "sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
}
}
},
"rollup-plugin-sourcemaps": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.4.2.tgz",
"integrity": "sha1-YhJaqUCHqt97g+9N+vYptHMTXoc=",
"dev": true,
"requires": {
"rollup-pluginutils": "^2.0.1",
"source-map-resolve": "^0.5.0"
}
},
"rollup-plugin-typescript2": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.21.0.tgz",
"integrity": "sha512-fbUAc2bvWxRrg1GGMYCFVf6aSxq3zvWn0sY2ubzFW9shJNT95ztFbM6GHO4/2rDSCjier7IswQnbr1ySqoLNPw==",
"dev": true,
"requires": {
"fs-extra": "7.0.1",
"resolve": "1.10.0",
"rollup-pluginutils": "2.4.1",
"tslib": "1.9.3"
},
"dependencies": {
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"resolve": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz",
"integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
},
"rollup-pluginutils": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.4.1.tgz",
"integrity": "sha512-wesMQ9/172IJDIW/lYWm0vW0LiKe5Ekjws481R7z9WTRtmO59cqyM/2uUlxvf6yzm/fElFmHUobeQOYz46dZJw==",
"dev": true,
"requires": {
"estree-walker": "^0.6.0",
"micromatch": "^3.1.10"
}
}
}
},
"rollup-pluginutils": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.6.0.tgz",
"integrity": "sha512-aGQwspEF8oPKvg37u3p7h0cYNwmJR1sCBMZGZ5b9qy8HGtETknqjzcxrDRrcAnJNXN18lBH4Q9vZYth/p4n8jQ==",
"dev": true,
"requires": {
"estree-walker": "^0.6.0",
"micromatch": "^3.1.10"
}
},
"run-async": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
@ -11654,9 +11833,9 @@
}
},
"rxjs": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz",
"integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==",
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.1.tgz",
"integrity": "sha512-y0j31WJc83wPu31vS1VlAFW5JGrnGC+j+TtGAa1fRQphy48+fDYiDmX8tjGloToEsMkxnouOg/1IzXGKkJnZMg==",
"dev": true,
"requires": {
"tslib": "^1.9.0"
@ -11949,17 +12128,6 @@
"ansi-styles": "^3.2.0",
"astral-regex": "^1.0.0",
"is-fullwidth-code-point": "^2.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
}
}
},
"snapdragon": {
@ -12123,6 +12291,12 @@
"to-array": "0.1.4"
},
"dependencies": {
"base64-arraybuffer": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
"integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=",
"dev": true
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
@ -12220,6 +12394,12 @@
"integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
"dev": true
},
"sourcemap-codec": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.4.tgz",
"integrity": "sha512-CYAPYdBu34781kLHkaW3m6b/uUSyMOC2R61gcYMWooeuaGtjof86ZA/8T+qVPPt7np1085CR9hmMGrySwEc8Xg==",
"dev": true
},
"spdx-correct": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz",
@ -12834,12 +13014,6 @@
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
@ -13451,6 +13625,15 @@
"integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
"dev": true
},
"tsutils": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.10.0.tgz",
"integrity": "sha512-q20XSMq7jutbGB8luhKKsQldRKWvyBO2BGqni3p4yq8Ys9bEP/xQw3KepKmMRt9gJ4lvQSScrihJrcKdKoSU7Q==",
"dev": true,
"requires": {
"tslib": "^1.8.1"
}
},
"tty-browserify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
@ -13534,6 +13717,24 @@
}
}
},
"uglify-js": {
"version": "3.5.11",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.11.tgz",
"integrity": "sha512-izPJg8RsSyqxbdnqX36ExpbH3K7tDBsAU/VfNv89VkMFy3z39zFjunQGsSHOlGlyIfGLGprGeosgQno3bo2/Kg==",
"dev": true,
"requires": {
"commander": "~2.20.0",
"source-map": "~0.6.1"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"uglifyjs-webpack-plugin": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.7.tgz",
@ -14496,12 +14697,6 @@
"path-exists": "^3.0.0"
}
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
},
"mem": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz",

View File

@ -2,8 +2,9 @@
"title": "html2canvas",
"name": "html2canvas",
"description": "Screenshots with JavaScript",
"main": "dist/npm/index.js",
"module": "dist/html2canvas.js",
"main": "dist/html2canvas.js",
"module": "dist/html2canvas.esm.js",
"typings": "dist/types/index.d.ts",
"browser": "dist/html2canvas.js",
"version": "1.0.0-rc.1",
"author": {
@ -26,26 +27,33 @@
"@babel/core": "^7.4.3",
"@babel/preset-env": "^7.4.3",
"@babel/preset-flow": "^7.0.0",
"@types/chai": "^4.1.7",
"@types/glob": "^7.1.1",
"@types/mocha": "^5.2.6",
"@types/node": "^11.13.2",
"@types/platform": "^1.3.2",
"@types/promise-polyfill": "^6.0.3",
"@typescript-eslint/eslint-plugin": "^1.7.0",
"@typescript-eslint/parser": "^1.7.0",
"appium-ios-simulator": "^3.10.0",
"babel-eslint": "^10.0.1",
"babel-loader": "^8.0.5",
"babel-plugin-add-module-exports": "^1.0.0",
"babel-plugin-dev-expression": "^0.2.1",
"base64-arraybuffer": "0.1.5",
"base64-arraybuffer": "0.2.0",
"body-parser": "^1.18.3",
"chai": "4.1.1",
"chromeless": "^1.5.2",
"cors": "2.8.4",
"es6-promise": "^4.2.6",
"eslint": "^5.16.0",
"eslint-plugin-flowtype": "2.35.0",
"eslint-plugin-prettier": "2.1.2",
"eslint-config-prettier": "^4.2.0",
"eslint-plugin-prettier": "3.0.1",
"express": "^4.16.4",
"filenamify-url": "1.0.0",
"flow-bin": "0.56.0",
"glob": "7.1.2",
"glob": "7.1.3",
"html2canvas-proxy": "1.0.1",
"jquery": "3.2.1",
"jquery": "^3.4.0",
"js-polyfills": "^0.1.42",
"karma": "^4.0.1",
"karma-chrome-launcher": "^2.2.0",
@ -56,43 +64,50 @@
"karma-mocha": "^1.3.0",
"karma-safari-launcher": "^1.0.0",
"karma-sauce-launcher": "^2.0.2",
"mocha": "^6.1.0",
"mocha": "^6.1.4",
"node-simctl": "^5.0.0",
"platform": "1.3.4",
"prettier": "1.5.3",
"promise-polyfill": "6.0.2",
"prettier": "1.17.0",
"replace-in-file": "^3.0.0",
"rimraf": "2.6.1",
"rollup": "^1.10.1",
"rollup-plugin-commonjs": "^9.3.4",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-resolve": "^4.2.3",
"rollup-plugin-sourcemaps": "^0.4.2",
"rollup-plugin-typescript2": "^0.21.0",
"serve-index": "^1.9.1",
"slash": "1.0.0",
"standard-version": "^5.0.2",
"ts-loader": "^5.3.3",
"ts-node": "^8.0.3",
"typescript": "^3.4.3",
"uglify-js": "^3.5.11",
"uglifyjs-webpack-plugin": "^1.1.2",
"webpack": "^4.29.6",
"webpack-cli": "^3.3.0"
},
"scripts": {
"build": "rimraf dist/ && node scripts/create-reftest-list && npm run build:npm && npm run build:browser",
"build:npm": "babel src/ -d dist/npm/ --plugins=dev-expression && replace-in-file __VERSION__ '\"$npm_package_version\"' dist/npm/index.js",
"build:browser": "webpack",
"prebuild": "rimraf dist/ && rimraf build/ && mkdirp dist && mkdirp build",
"build": "tsc --module commonjs && rollup -c rollup.config.ts && npm run build:create-reftest-list && npm run build:testrunner && npm run build:minify",
"build:testrunner": "rollup -c tests/rollup.config.ts",
"build:minify": "uglifyjs --compress --comments /^!/ -o dist/html2canvas.min.js --mangle -- dist/html2canvas.js",
"build:reftest-result-list": "ts-node scripts/create-reftest-result-list.ts",
"build:create-reftest-list": "ts-node scripts/create-reftest-list.ts tests/reftests/ignore.txt build/reftests.ts",
"build:reftest-preview": "webpack --config www/webpack.config.js",
"release": "standard-version",
"rollup": "rollup -c",
"format": "prettier --single-quote --no-bracket-spacing --tab-width 4 --print-width 100 --write \"{src,www/src,tests,scripts}/**/*.js\"",
"flow": "flow",
"lint": "eslint src/**",
"test": "npm run flow && npm run lint && npm run test:node && npm run karma",
"test:node": "mocha tests/node/*.js",
"format": "prettier --write \"{src,www/src,tests,scripts}/**/*.ts\"",
"lint": "eslint src/**/*.ts",
"test": "npm run lint && npm run unittest && npm run karma",
"unittest": "mocha --require ts-node/register src/**/__tests__/*.ts",
"karma": "node karma",
"watch": "webpack --progress --colors --watch",
"watch": "rollup -c rollup.config.ts -w",
"watch:unittest": "mocha --require ts-node/register --watch-extensions ts -w src/**/__tests__/*.ts",
"start": "node tests/server"
},
"homepage": "https://html2canvas.hertzen.com",
"license": "MIT",
"dependencies": {
"css-line-break": "1.0.1"
"css-line-break": "1.1.1"
}
}

View File

@ -1,37 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'package.json')));
const banner =
`/*
${pkg.title} ${pkg.version} <${pkg.homepage}>
Copyright (c) ${(new Date()).getFullYear()} ${pkg.author.name} <${pkg.author.url}>
Released under ${pkg.license} License
*/`;
import babel from 'rollup-plugin-babel';
import commonjs from 'rollup-plugin-commonjs';
import resolve from 'rollup-plugin-node-resolve';
export default {
input: './src/index.js',
plugins: [
resolve(),
babel({
exclude: 'node_modules/**'
}),
commonjs({
namedExports: {
'node_modules/css-line-break/dist/index.js': ['toCodePoints', 'fromCodePoint', 'LineBreaker']
}
})
],
output: {
file: './dist/html2canvas.js',
name: 'html2canvas',
format: 'umd',
banner
}
};

42
rollup.config.ts Normal file
View File

@ -0,0 +1,42 @@
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import sourceMaps from 'rollup-plugin-sourcemaps';
import typescript from 'rollup-plugin-typescript2';
import json from 'rollup-plugin-json';
const pkg = require('./package.json');
const banner = `/*!
* ${pkg.title} ${pkg.version} <${pkg.homepage}>
* Copyright (c) ${(new Date()).getFullYear()} ${pkg.author.name} <${pkg.author.url}>
* Released under ${pkg.license} License
*/`;
export default {
input: `src/index.ts`,
output: [
{ file: pkg.main, name: pkg.name, format: 'umd', banner, sourcemap: true },
{ file: pkg.module, format: 'esm', banner, sourcemap: true },
],
external: [],
watch: {
include: 'src/**',
},
plugins: [
// Allow node_modules resolution, so you can use 'external' to control
// which external modules to include in the bundle
// https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve(),
// Allow json resolution
json(),
// Compile TypeScript files
typescript({ useTsconfigDeclarationDir: true }),
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
commonjs({
include: 'node_modules/**'
}),
// Resolve source maps to the original source
sourceMaps(),
],
}

View File

@ -1,53 +0,0 @@
'use strict';
const path = require('path');
const glob = require('glob');
const fs = require('fs');
const slash = require('slash');
const parseRefTest = require('./parse-reftest');
const outputPath = 'tests/reftests.js';
const ignoredTests = fs
.readFileSync(path.resolve(__dirname, `../tests/reftests/ignore.txt`))
.toString()
.split(/\r\n|\r|\n/)
.filter(l => l.length)
.reduce((acc, l) => {
const m = l.match(/^(\[(.+)\])?(.+)$/i);
acc[m[3]] = m[2] ? m[2].split(',') : [];
return acc;
}, {});
glob(
'../tests/reftests/**/*.html',
{
cwd: __dirname,
root: path.resolve(__dirname, '../../')
},
(err, files) => {
if (err) {
console.error(err);
process.exit(1);
}
const testList = files.reduce((acc, filename) => {
const refTestFilename = path.resolve(__dirname, filename.replace(/\.html$/, '.txt'));
const name = `/${slash(path.relative('../', filename))}`;
if (!Array.isArray(ignoredTests[name]) || ignoredTests[name].length) {
acc[name] = fs.existsSync(refTestFilename)
? parseRefTest(fs.readFileSync(refTestFilename).toString())
: null;
} else {
console.log(`IGNORED: ${name}`);
}
return acc;
}, {});
fs.writeFileSync(
path.resolve(__dirname, `../${outputPath}`),
`module.exports = ${JSON.stringify({testList, ignoredTests}, null, 4)};`
);
console.log(`${outputPath} updated`);
}
);

View File

@ -0,0 +1,47 @@
'use strict';
import {readFileSync, writeFileSync} from 'fs';
import {resolve, relative} from 'path';
import {sync} from 'glob';
const slash = require('slash');
if (process.argv.length <= 2) {
console.log('No ignore.txt file provided');
process.exit(1);
}
if (process.argv.length <= 3) {
console.log('No output file provided');
process.exit(1);
}
const path = resolve(__dirname, '../', process.argv[2]);
const outputPath = resolve(__dirname, '../', process.argv[3]);
const ignoredTests = readFileSync(path)
.toString()
.split(/\r\n|\r|\n/)
.filter(l => l.length)
.reduce((acc: {[key: string]: string[]}, l) => {
const m = l.match(/^(\[(.+)\])?(.+)$/i);
if (m) {
acc[m[3]] = m[2] ? m[2].split(',') : [];
}
return acc;
}, {});
const files: string[] = sync('../tests/reftests/**/*.html', {
cwd: __dirname,
root: resolve(__dirname, '../../')
});
const testList = files.map((filename: string) => `/${slash(relative('../', filename))}`);
writeFileSync(
outputPath,
[
`export const testList: string[] = ${JSON.stringify(testList, null, 4)};`,
`export const ignoredTests: {[key: string]: string[]} = ${JSON.stringify(ignoredTests, null, 4)};`
].join('\n')
);
console.log(`${outputPath} updated`);

View File

@ -1,12 +1,12 @@
import {readdirSync, readFileSync, writeFileSync} from 'fs';
import {resolve} from 'path';
if (process.argv.length <= 2){
if (process.argv.length <= 2) {
console.log('No metadata path provided');
process.exit(1);
}
if (process.argv.length <= 3){
if (process.argv.length <= 3) {
console.log('No output file given');
process.exit(1);
}
@ -14,16 +14,14 @@ if (process.argv.length <= 3){
const path = resolve(__dirname, '../', process.argv[2]);
const files = readdirSync(path);
interface RefTestMetadata {
interface RefTestMetadata {}
}
interface RefTestSingleMetadata extends RefTestMetadata{
interface RefTestSingleMetadata extends RefTestMetadata {
test: string;
}
interface RefTestResults {
[key: string]: Array<RefTestMetadata>
[key: string]: Array<RefTestMetadata>;
}
const result: RefTestResults = files.reduce((result: RefTestResults, file) => {

View File

@ -1,24 +0,0 @@
/* @flow */
'use strict';
const ANGLE = /([+-]?\d*\.?\d+)(deg|grad|rad|turn)/i;
export const parseAngle = (angle: string): number | null => {
const match = angle.match(ANGLE);
if (match) {
const value = parseFloat(match[1]);
switch (match[2].toLowerCase()) {
case 'deg':
return Math.PI * value / 180;
case 'grad':
return Math.PI / 200 * value;
case 'rad':
return value;
case 'turn':
return Math.PI * 2 * value;
}
}
return null;
};

View File

@ -1,370 +0,0 @@
/* @flow */
'use strict';
import type {Border, BorderSide} from './parsing/border';
import type {BorderRadius} from './parsing/borderRadius';
import type {Padding} from './parsing/padding';
import type {Path} from './drawing/Path';
import Vector from './drawing/Vector';
import BezierCurve from './drawing/BezierCurve';
const TOP = 0;
const RIGHT = 1;
const BOTTOM = 2;
const LEFT = 3;
const H = 0;
const V = 1;
export type BoundCurves = {
topLeftOuter: BezierCurve | Vector,
topLeftInner: BezierCurve | Vector,
topRightOuter: BezierCurve | Vector,
topRightInner: BezierCurve | Vector,
bottomRightOuter: BezierCurve | Vector,
bottomRightInner: BezierCurve | Vector,
bottomLeftOuter: BezierCurve | Vector,
bottomLeftInner: BezierCurve | Vector
};
export class Bounds {
top: number;
left: number;
width: number;
height: number;
constructor(x: number, y: number, w: number, h: number) {
this.left = x;
this.top = y;
this.width = w;
this.height = h;
}
static fromClientRect(clientRect: ClientRect, scrollX: number, scrollY: number): Bounds {
return new Bounds(
clientRect.left + scrollX,
clientRect.top + scrollY,
clientRect.width,
clientRect.height
);
}
}
export const parseBounds = (
node: HTMLElement | SVGSVGElement,
scrollX: number,
scrollY: number
): Bounds => {
return Bounds.fromClientRect(node.getBoundingClientRect(), scrollX, scrollY);
};
export const calculatePaddingBox = (bounds: Bounds, borders: Array<Border>): Bounds => {
return new Bounds(
bounds.left + borders[LEFT].borderWidth,
bounds.top + borders[TOP].borderWidth,
bounds.width - (borders[RIGHT].borderWidth + borders[LEFT].borderWidth),
bounds.height - (borders[TOP].borderWidth + borders[BOTTOM].borderWidth)
);
};
export const calculateContentBox = (
bounds: Bounds,
padding: Padding,
borders: Array<Border>
): Bounds => {
// TODO support percentage paddings
const paddingTop = padding[TOP].value;
const paddingRight = padding[RIGHT].value;
const paddingBottom = padding[BOTTOM].value;
const paddingLeft = padding[LEFT].value;
return new Bounds(
bounds.left + paddingLeft + borders[LEFT].borderWidth,
bounds.top + paddingTop + borders[TOP].borderWidth,
bounds.width -
(borders[RIGHT].borderWidth + borders[LEFT].borderWidth + paddingLeft + paddingRight),
bounds.height -
(borders[TOP].borderWidth + borders[BOTTOM].borderWidth + paddingTop + paddingBottom)
);
};
export const parseDocumentSize = (document: Document): Bounds => {
const body = document.body;
const documentElement = document.documentElement;
if (!body || !documentElement) {
throw new Error(__DEV__ ? `Unable to get document size` : '');
}
const width = Math.max(
Math.max(body.scrollWidth, documentElement.scrollWidth),
Math.max(body.offsetWidth, documentElement.offsetWidth),
Math.max(body.clientWidth, documentElement.clientWidth)
);
const height = Math.max(
Math.max(body.scrollHeight, documentElement.scrollHeight),
Math.max(body.offsetHeight, documentElement.offsetHeight),
Math.max(body.clientHeight, documentElement.clientHeight)
);
return new Bounds(0, 0, width, height);
};
export const parsePathForBorder = (curves: BoundCurves, borderSide: BorderSide): Path => {
switch (borderSide) {
case TOP:
return createPathFromCurves(
curves.topLeftOuter,
curves.topLeftInner,
curves.topRightOuter,
curves.topRightInner
);
case RIGHT:
return createPathFromCurves(
curves.topRightOuter,
curves.topRightInner,
curves.bottomRightOuter,
curves.bottomRightInner
);
case BOTTOM:
return createPathFromCurves(
curves.bottomRightOuter,
curves.bottomRightInner,
curves.bottomLeftOuter,
curves.bottomLeftInner
);
case LEFT:
default:
return createPathFromCurves(
curves.bottomLeftOuter,
curves.bottomLeftInner,
curves.topLeftOuter,
curves.topLeftInner
);
}
};
const createPathFromCurves = (
outer1: BezierCurve | Vector,
inner1: BezierCurve | Vector,
outer2: BezierCurve | Vector,
inner2: BezierCurve | Vector
): Path => {
const path = [];
if (outer1 instanceof BezierCurve) {
path.push(outer1.subdivide(0.5, false));
} else {
path.push(outer1);
}
if (outer2 instanceof BezierCurve) {
path.push(outer2.subdivide(0.5, true));
} else {
path.push(outer2);
}
if (inner2 instanceof BezierCurve) {
path.push(inner2.subdivide(0.5, true).reverse());
} else {
path.push(inner2);
}
if (inner1 instanceof BezierCurve) {
path.push(inner1.subdivide(0.5, false).reverse());
} else {
path.push(inner1);
}
return path;
};
export const calculateBorderBoxPath = (curves: BoundCurves): Path => {
return [
curves.topLeftOuter,
curves.topRightOuter,
curves.bottomRightOuter,
curves.bottomLeftOuter
];
};
export const calculatePaddingBoxPath = (curves: BoundCurves): Path => {
return [
curves.topLeftInner,
curves.topRightInner,
curves.bottomRightInner,
curves.bottomLeftInner
];
};
export const parseBoundCurves = (
bounds: Bounds,
borders: Array<Border>,
borderRadius: Array<BorderRadius>
): BoundCurves => {
let tlh = borderRadius[CORNER.TOP_LEFT][H].getAbsoluteValue(bounds.width);
let tlv = borderRadius[CORNER.TOP_LEFT][V].getAbsoluteValue(bounds.height);
let trh = borderRadius[CORNER.TOP_RIGHT][H].getAbsoluteValue(bounds.width);
let trv = borderRadius[CORNER.TOP_RIGHT][V].getAbsoluteValue(bounds.height);
let brh = borderRadius[CORNER.BOTTOM_RIGHT][H].getAbsoluteValue(bounds.width);
let brv = borderRadius[CORNER.BOTTOM_RIGHT][V].getAbsoluteValue(bounds.height);
let blh = borderRadius[CORNER.BOTTOM_LEFT][H].getAbsoluteValue(bounds.width);
let blv = borderRadius[CORNER.BOTTOM_LEFT][V].getAbsoluteValue(bounds.height);
const factors = [];
factors.push((tlh + trh) / bounds.width);
factors.push((blh + brh) / bounds.width);
factors.push((tlv + blv) / bounds.height);
factors.push((trv + brv) / bounds.height);
const maxFactor = Math.max(...factors);
if (maxFactor > 1) {
tlh /= maxFactor;
tlv /= maxFactor;
trh /= maxFactor;
trv /= maxFactor;
brh /= maxFactor;
brv /= maxFactor;
blh /= maxFactor;
blv /= maxFactor;
}
const topWidth = bounds.width - trh;
const rightHeight = bounds.height - brv;
const bottomWidth = bounds.width - brh;
const leftHeight = bounds.height - blv;
return {
topLeftOuter:
tlh > 0 || tlv > 0
? getCurvePoints(bounds.left, bounds.top, tlh, tlv, CORNER.TOP_LEFT)
: new Vector(bounds.left, bounds.top),
topLeftInner:
tlh > 0 || tlv > 0
? getCurvePoints(
bounds.left + borders[LEFT].borderWidth,
bounds.top + borders[TOP].borderWidth,
Math.max(0, tlh - borders[LEFT].borderWidth),
Math.max(0, tlv - borders[TOP].borderWidth),
CORNER.TOP_LEFT
)
: new Vector(
bounds.left + borders[LEFT].borderWidth,
bounds.top + borders[TOP].borderWidth
),
topRightOuter:
trh > 0 || trv > 0
? getCurvePoints(bounds.left + topWidth, bounds.top, trh, trv, CORNER.TOP_RIGHT)
: new Vector(bounds.left + bounds.width, bounds.top),
topRightInner:
trh > 0 || trv > 0
? getCurvePoints(
bounds.left + Math.min(topWidth, bounds.width + borders[LEFT].borderWidth),
bounds.top + borders[TOP].borderWidth,
topWidth > bounds.width + borders[LEFT].borderWidth
? 0
: trh - borders[LEFT].borderWidth,
trv - borders[TOP].borderWidth,
CORNER.TOP_RIGHT
)
: new Vector(
bounds.left + bounds.width - borders[RIGHT].borderWidth,
bounds.top + borders[TOP].borderWidth
),
bottomRightOuter:
brh > 0 || brv > 0
? getCurvePoints(
bounds.left + bottomWidth,
bounds.top + rightHeight,
brh,
brv,
CORNER.BOTTOM_RIGHT
)
: new Vector(bounds.left + bounds.width, bounds.top + bounds.height),
bottomRightInner:
brh > 0 || brv > 0
? getCurvePoints(
bounds.left + Math.min(bottomWidth, bounds.width - borders[LEFT].borderWidth),
bounds.top + Math.min(rightHeight, bounds.height + borders[TOP].borderWidth),
Math.max(0, brh - borders[RIGHT].borderWidth),
brv - borders[BOTTOM].borderWidth,
CORNER.BOTTOM_RIGHT
)
: new Vector(
bounds.left + bounds.width - borders[RIGHT].borderWidth,
bounds.top + bounds.height - borders[BOTTOM].borderWidth
),
bottomLeftOuter:
blh > 0 || blv > 0
? getCurvePoints(bounds.left, bounds.top + leftHeight, blh, blv, CORNER.BOTTOM_LEFT)
: new Vector(bounds.left, bounds.top + bounds.height),
bottomLeftInner:
blh > 0 || blv > 0
? getCurvePoints(
bounds.left + borders[LEFT].borderWidth,
bounds.top + leftHeight,
Math.max(0, blh - borders[LEFT].borderWidth),
blv - borders[BOTTOM].borderWidth,
CORNER.BOTTOM_LEFT
)
: new Vector(
bounds.left + borders[LEFT].borderWidth,
bounds.top + bounds.height - borders[BOTTOM].borderWidth
)
};
};
const CORNER = {
TOP_LEFT: 0,
TOP_RIGHT: 1,
BOTTOM_RIGHT: 2,
BOTTOM_LEFT: 3
};
type Corner = $Values<typeof CORNER>;
const getCurvePoints = (
x: number,
y: number,
r1: number,
r2: number,
position: Corner
): BezierCurve => {
const kappa = 4 * ((Math.sqrt(2) - 1) / 3);
const ox = r1 * kappa; // control point offset horizontal
const oy = r2 * kappa; // control point offset vertical
const xm = x + r1; // x-middle
const ym = y + r2; // y-middle
switch (position) {
case CORNER.TOP_LEFT:
return new BezierCurve(
new Vector(x, ym),
new Vector(x, ym - oy),
new Vector(xm - ox, y),
new Vector(xm, y)
);
case CORNER.TOP_RIGHT:
return new BezierCurve(
new Vector(x, y),
new Vector(x + ox, y),
new Vector(xm, ym - oy),
new Vector(xm, ym)
);
case CORNER.BOTTOM_RIGHT:
return new BezierCurve(
new Vector(xm, y),
new Vector(xm, y + oy),
new Vector(x + ox, ym),
new Vector(x, ym)
);
case CORNER.BOTTOM_LEFT:
default:
return new BezierCurve(
new Vector(xm, ym),
new Vector(xm - ox, ym),
new Vector(x, y + oy),
new Vector(x, y)
);
}
};

View File

@ -1,690 +0,0 @@
/* @flow */
'use strict';
import type {Bounds} from './Bounds';
import type {Options} from './index';
import type {PseudoContentData, PseudoContentItem} from './PseudoNodeContent';
import type Logger from './Logger';
import {parseBounds} from './Bounds';
import {Proxy} from './Proxy';
import ResourceLoader from './ResourceLoader';
import {copyCSSStyles} from './Util';
import {parseBackgroundImage} from './parsing/background';
import CanvasRenderer from './renderer/CanvasRenderer';
import {
parseCounterReset,
popCounters,
resolvePseudoContent,
PSEUDO_CONTENT_ITEM_TYPE
} from './PseudoNodeContent';
const IGNORE_ATTRIBUTE = 'data-html2canvas-ignore';
export class DocumentCloner {
scrolledElements: Array<[HTMLElement, number, number]>;
referenceElement: HTMLElement;
clonedReferenceElement: HTMLElement;
documentElement: HTMLElement;
resourceLoader: ResourceLoader;
logger: Logger;
options: Options;
inlineImages: boolean;
copyStyles: boolean;
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>;
pseudoContentData: PseudoContentData;
constructor(
element: HTMLElement,
options: Options,
logger: Logger,
copyInline: boolean,
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>
) {
this.referenceElement = element;
this.scrolledElements = [];
this.copyStyles = copyInline;
this.inlineImages = copyInline;
this.logger = logger;
this.options = options;
this.renderer = renderer;
this.resourceLoader = new ResourceLoader(options, logger, window);
this.pseudoContentData = {
counters: {},
quoteDepth: 0
};
// $FlowFixMe
this.documentElement = this.cloneNode(element.ownerDocument.documentElement);
}
inlineAllImages(node: ?HTMLElement) {
if (this.inlineImages && node) {
const style = node.style;
Promise.all(
parseBackgroundImage(style.backgroundImage).map(backgroundImage => {
if (backgroundImage.method === 'url') {
return this.resourceLoader
.inlineImage(backgroundImage.args[0])
.then(
img =>
img && typeof img.src === 'string'
? `url("${img.src}")`
: 'none'
)
.catch(e => {
if (__DEV__) {
this.logger.log(`Unable to load image`, e);
}
});
}
return Promise.resolve(
`${backgroundImage.prefix}${backgroundImage.method}(${backgroundImage.args.join(
','
)})`
);
})
).then(backgroundImages => {
if (backgroundImages.length > 1) {
// TODO Multiple backgrounds somehow broken in Chrome
style.backgroundColor = '';
}
style.backgroundImage = backgroundImages.join(',');
});
if (node instanceof HTMLImageElement) {
this.resourceLoader
.inlineImage(node.src)
.then(img => {
if (img && node instanceof HTMLImageElement && node.parentNode) {
const parentNode = node.parentNode;
const clonedChild = copyCSSStyles(node.style, img.cloneNode(false));
parentNode.replaceChild(clonedChild, node);
}
})
.catch(e => {
if (__DEV__) {
this.logger.log(`Unable to load image`, e);
}
});
}
}
}
inlineFonts(document: Document): Promise<void> {
return Promise.all(
Array.from(document.styleSheets).map(sheet => {
if (sheet.href) {
return fetch(sheet.href)
.then(res => res.text())
.then(text => createStyleSheetFontsFromText(text, sheet.href))
.catch(e => {
if (__DEV__) {
this.logger.log(`Unable to load stylesheet`, e);
}
return [];
});
}
return getSheetFonts(sheet, document);
})
)
.then(fonts => fonts.reduce((acc, font) => acc.concat(font), []))
.then(fonts =>
Promise.all(
fonts.map(font =>
fetch(font.formats[0].src)
.then(response => response.blob())
.then(
blob =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = reject;
reader.onload = () => {
// $FlowFixMe
const result: string = reader.result;
resolve(result);
};
reader.readAsDataURL(blob);
})
)
.then(dataUri => {
font.fontFace.setProperty('src', `url("${dataUri}")`);
return `@font-face {${font.fontFace.cssText} `;
})
)
)
)
.then(fontCss => {
const style = document.createElement('style');
style.textContent = fontCss.join('\n');
this.documentElement.appendChild(style);
});
}
createElementClone(node: Node) {
if (this.copyStyles && node instanceof HTMLCanvasElement) {
const img = node.ownerDocument.createElement('img');
try {
img.src = node.toDataURL();
return img;
} catch (e) {
if (__DEV__) {
this.logger.log(`Unable to clone canvas contents, canvas is tainted`);
}
}
}
if (node instanceof HTMLIFrameElement) {
const tempIframe = node.cloneNode(false);
const iframeKey = generateIframeKey();
tempIframe.setAttribute('data-html2canvas-internal-iframe-key', iframeKey);
const {width, height} = parseBounds(node, 0, 0);
this.resourceLoader.cache[iframeKey] = getIframeDocumentElement(node, this.options)
.then(documentElement => {
return this.renderer(
documentElement,
{
allowTaint: this.options.allowTaint,
backgroundColor: '#ffffff',
canvas: null,
imageTimeout: this.options.imageTimeout,
logging: this.options.logging,
proxy: this.options.proxy,
removeContainer: this.options.removeContainer,
scale: this.options.scale,
foreignObjectRendering: this.options.foreignObjectRendering,
useCORS: this.options.useCORS,
target: new CanvasRenderer(),
width,
height,
x: 0,
y: 0,
windowWidth: documentElement.ownerDocument.defaultView.innerWidth,
windowHeight: documentElement.ownerDocument.defaultView.innerHeight,
scrollX: documentElement.ownerDocument.defaultView.pageXOffset,
scrollY: documentElement.ownerDocument.defaultView.pageYOffset
},
this.logger.child(iframeKey)
);
})
.then(
canvas =>
new Promise((resolve, reject) => {
const iframeCanvas = document.createElement('img');
iframeCanvas.onload = () => resolve(canvas);
iframeCanvas.onerror = function(event) {
// Empty iframes may result in empty "data:," URLs, which are invalid from the <img>'s point of view
// and instead of `onload` cause `onerror` and unhandled rejection warnings
// https://github.com/niklasvh/html2canvas/issues/1502
iframeCanvas.src == 'data:,' ? resolve(canvas) : reject(event);
};
iframeCanvas.src = canvas.toDataURL();
if (tempIframe.parentNode) {
tempIframe.parentNode.replaceChild(
copyCSSStyles(
node.ownerDocument.defaultView.getComputedStyle(node),
iframeCanvas
),
tempIframe
);
}
})
);
return tempIframe;
}
try {
if (node instanceof HTMLStyleElement && node.sheet && node.sheet.cssRules) {
const css = [].slice.call(node.sheet.cssRules, 0).reduce((css, rule) => {
if (rule && rule.cssText) {
return css + rule.cssText;
}
return css;
}, '');
const style = node.cloneNode(false);
style.textContent = css;
return style;
}
} catch (e) {
// accessing node.sheet.cssRules throws a DOMException
this.logger.log('Unable to access cssRules property');
if (e.name !== 'SecurityError') {
this.logger.log(e);
throw e;
}
}
return node.cloneNode(false);
}
cloneNode(node: Node): Node {
const clone =
node.nodeType === Node.TEXT_NODE
? document.createTextNode(node.nodeValue)
: this.createElementClone(node);
const window = node.ownerDocument.defaultView;
const style = node instanceof window.HTMLElement ? window.getComputedStyle(node) : null;
const styleBefore =
node instanceof window.HTMLElement ? window.getComputedStyle(node, ':before') : null;
const styleAfter =
node instanceof window.HTMLElement ? window.getComputedStyle(node, ':after') : null;
if (this.referenceElement === node && clone instanceof window.HTMLElement) {
this.clonedReferenceElement = clone;
}
if (clone instanceof window.HTMLBodyElement) {
createPseudoHideStyles(clone);
}
const counters = parseCounterReset(style, this.pseudoContentData);
const contentBefore = resolvePseudoContent(node, styleBefore, this.pseudoContentData);
for (let child = node.firstChild; child; child = child.nextSibling) {
if (
child.nodeType !== Node.ELEMENT_NODE ||
(child.nodeName !== 'SCRIPT' &&
// $FlowFixMe
!child.hasAttribute(IGNORE_ATTRIBUTE) &&
(typeof this.options.ignoreElements !== 'function' ||
// $FlowFixMe
!this.options.ignoreElements(child)))
) {
if (!this.copyStyles || child.nodeName !== 'STYLE') {
clone.appendChild(this.cloneNode(child));
}
}
}
const contentAfter = resolvePseudoContent(node, styleAfter, this.pseudoContentData);
popCounters(counters, this.pseudoContentData);
if (node instanceof window.HTMLElement && clone instanceof window.HTMLElement) {
if (styleBefore) {
this.inlineAllImages(
inlinePseudoElement(node, clone, styleBefore, contentBefore, PSEUDO_BEFORE)
);
}
if (styleAfter) {
this.inlineAllImages(
inlinePseudoElement(node, clone, styleAfter, contentAfter, PSEUDO_AFTER)
);
}
if (style && this.copyStyles && !(node instanceof HTMLIFrameElement)) {
copyCSSStyles(style, clone);
}
this.inlineAllImages(clone);
if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
this.scrolledElements.push([clone, node.scrollLeft, node.scrollTop]);
}
switch (node.nodeName) {
case 'CANVAS':
if (!this.copyStyles) {
cloneCanvasContents(node, clone);
}
break;
case 'TEXTAREA':
case 'SELECT':
clone.value = node.value;
break;
}
}
return clone;
}
}
type Font = {
src: string,
format: string
};
type FontFamily = {
formats: Array<Font>,
fontFace: CSSStyleDeclaration
};
const getSheetFonts = (sheet: StyleSheet, document: Document): Array<FontFamily> => {
// $FlowFixMe
return (sheet.cssRules ? Array.from(sheet.cssRules) : [])
.filter(rule => rule.type === CSSRule.FONT_FACE_RULE)
.map(rule => {
const src = parseBackgroundImage(rule.style.getPropertyValue('src'));
const formats = [];
for (let i = 0; i < src.length; i++) {
if (src[i].method === 'url' && src[i + 1] && src[i + 1].method === 'format') {
const a = document.createElement('a');
a.href = src[i].args[0];
if (document.body) {
document.body.appendChild(a);
}
const font = {
src: a.href,
format: src[i + 1].args[0]
};
formats.push(font);
}
}
return {
// TODO select correct format for browser),
formats: formats.filter(font => /^woff/i.test(font.format)),
fontFace: rule.style
};
})
.filter(font => font.formats.length);
};
const createStyleSheetFontsFromText = (text: string, baseHref: string): Array<FontFamily> => {
const doc = document.implementation.createHTMLDocument('');
const base = document.createElement('base');
// $FlowFixMe
base.href = baseHref;
const style = document.createElement('style');
style.textContent = text;
if (doc.head) {
doc.head.appendChild(base);
}
if (doc.body) {
doc.body.appendChild(style);
}
return style.sheet ? getSheetFonts(style.sheet, doc) : [];
};
const restoreOwnerScroll = (ownerDocument: Document, x: number, y: number) => {
if (
ownerDocument.defaultView &&
(x !== ownerDocument.defaultView.pageXOffset || y !== ownerDocument.defaultView.pageYOffset)
) {
ownerDocument.defaultView.scrollTo(x, y);
}
};
const cloneCanvasContents = (canvas: HTMLCanvasElement, clonedCanvas: HTMLCanvasElement) => {
try {
if (clonedCanvas) {
clonedCanvas.width = canvas.width;
clonedCanvas.height = canvas.height;
const ctx = canvas.getContext('2d');
const clonedCtx = clonedCanvas.getContext('2d');
if (ctx) {
clonedCtx.putImageData(ctx.getImageData(0, 0, canvas.width, canvas.height), 0, 0);
} else {
clonedCtx.drawImage(canvas, 0, 0);
}
}
} catch (e) {}
};
const inlinePseudoElement = (
node: HTMLElement,
clone: HTMLElement,
style: CSSStyleDeclaration,
contentItems: ?Array<PseudoContentItem>,
pseudoElt: ':before' | ':after'
): ?HTMLElement => {
if (
!style ||
!style.content ||
style.content === 'none' ||
style.content === '-moz-alt-content' ||
style.display === 'none'
) {
return;
}
const anonymousReplacedElement = clone.ownerDocument.createElement('html2canvaspseudoelement');
copyCSSStyles(style, anonymousReplacedElement);
if (contentItems) {
const len = contentItems.length;
for (var i = 0; i < len; i++) {
const item = contentItems[i];
switch (item.type) {
case PSEUDO_CONTENT_ITEM_TYPE.IMAGE:
const img = clone.ownerDocument.createElement('img');
img.src = parseBackgroundImage(`url(${item.value})`)[0].args[0];
img.style.opacity = '1';
anonymousReplacedElement.appendChild(img);
break;
case PSEUDO_CONTENT_ITEM_TYPE.TEXT:
anonymousReplacedElement.appendChild(
clone.ownerDocument.createTextNode(item.value)
);
break;
}
}
}
anonymousReplacedElement.className = `${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE} ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
clone.className +=
pseudoElt === PSEUDO_BEFORE
? ` ${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}`
: ` ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
if (pseudoElt === PSEUDO_BEFORE) {
clone.insertBefore(anonymousReplacedElement, clone.firstChild);
} else {
clone.appendChild(anonymousReplacedElement);
}
return anonymousReplacedElement;
};
const URL_REGEXP = /^url\((.+)\)$/i;
const PSEUDO_BEFORE = ':before';
const PSEUDO_AFTER = ':after';
const PSEUDO_HIDE_ELEMENT_CLASS_BEFORE = '___html2canvas___pseudoelement_before';
const PSEUDO_HIDE_ELEMENT_CLASS_AFTER = '___html2canvas___pseudoelement_after';
const PSEUDO_HIDE_ELEMENT_STYLE = `{
content: "" !important;
display: none !important;
}`;
const createPseudoHideStyles = (body: HTMLElement) => {
createStyles(
body,
`.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE}
.${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}`
);
};
const createStyles = (body: HTMLElement, styles) => {
const style = body.ownerDocument.createElement('style');
style.innerHTML = styles;
body.appendChild(style);
};
const initNode = ([element, x, y]: [HTMLElement, number, number]) => {
element.scrollLeft = x;
element.scrollTop = y;
};
const generateIframeKey = (): string =>
Math.ceil(Date.now() + Math.random() * 10000000).toString(16);
const DATA_URI_REGEXP = /^data:text\/(.+);(base64)?,(.*)$/i;
const getIframeDocumentElement = (
node: HTMLIFrameElement,
options: Options
): Promise<HTMLElement> => {
try {
return Promise.resolve(node.contentWindow.document.documentElement);
} catch (e) {
return options.proxy
? Proxy(node.src, options)
.then(html => {
const match = html.match(DATA_URI_REGEXP);
if (!match) {
return Promise.reject();
}
return match[2] === 'base64'
? window.atob(decodeURIComponent(match[3]))
: decodeURIComponent(match[3]);
})
.then(html =>
createIframeContainer(
node.ownerDocument,
parseBounds(node, 0, 0)
).then(cloneIframeContainer => {
const cloneWindow = cloneIframeContainer.contentWindow;
const documentClone = cloneWindow.document;
documentClone.open();
documentClone.write(html);
const iframeLoad = iframeLoader(cloneIframeContainer).then(
() => documentClone.documentElement
);
documentClone.close();
return iframeLoad;
})
)
: Promise.reject();
}
};
const createIframeContainer = (
ownerDocument: Document,
bounds: Bounds
): Promise<HTMLIFrameElement> => {
const cloneIframeContainer = ownerDocument.createElement('iframe');
cloneIframeContainer.className = 'html2canvas-container';
cloneIframeContainer.style.visibility = 'hidden';
cloneIframeContainer.style.position = 'fixed';
cloneIframeContainer.style.left = '-10000px';
cloneIframeContainer.style.top = '0px';
cloneIframeContainer.style.border = '0';
cloneIframeContainer.width = bounds.width.toString();
cloneIframeContainer.height = bounds.height.toString();
cloneIframeContainer.scrolling = 'no'; // ios won't scroll without it
cloneIframeContainer.setAttribute(IGNORE_ATTRIBUTE, 'true');
if (!ownerDocument.body) {
return Promise.reject(
__DEV__ ? `Body element not found in Document that is getting rendered` : ''
);
}
ownerDocument.body.appendChild(cloneIframeContainer);
return Promise.resolve(cloneIframeContainer);
};
const iframeLoader = (cloneIframeContainer: HTMLIFrameElement): Promise<HTMLIFrameElement> => {
const cloneWindow = cloneIframeContainer.contentWindow;
const documentClone = cloneWindow.document;
return new Promise((resolve, reject) => {
cloneWindow.onload = cloneIframeContainer.onload = documentClone.onreadystatechange = () => {
const interval = setInterval(() => {
if (
documentClone.body.childNodes.length > 0 &&
documentClone.readyState === 'complete'
) {
clearInterval(interval);
resolve(cloneIframeContainer);
}
}, 50);
};
});
};
export const cloneWindow = (
ownerDocument: Document,
bounds: Bounds,
referenceElement: HTMLElement,
options: Options,
logger: Logger,
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>
): Promise<[HTMLIFrameElement, HTMLElement, ResourceLoader]> => {
const cloner = new DocumentCloner(referenceElement, options, logger, false, renderer);
const scrollX = ownerDocument.defaultView.pageXOffset;
const scrollY = ownerDocument.defaultView.pageYOffset;
return createIframeContainer(ownerDocument, bounds).then(cloneIframeContainer => {
const cloneWindow = cloneIframeContainer.contentWindow;
const documentClone = cloneWindow.document;
/* Chrome doesn't detect relative background-images assigned in inline <style> sheets when fetched through getComputedStyle
if window url is about:blank, we can assign the url to current by writing onto the document
*/
const iframeLoad = iframeLoader(cloneIframeContainer).then(() => {
cloner.scrolledElements.forEach(initNode);
cloneWindow.scrollTo(bounds.left, bounds.top);
if (
/(iPad|iPhone|iPod)/g.test(navigator.userAgent) &&
(cloneWindow.scrollY !== bounds.top || cloneWindow.scrollX !== bounds.left)
) {
documentClone.documentElement.style.top = -bounds.top + 'px';
documentClone.documentElement.style.left = -bounds.left + 'px';
documentClone.documentElement.style.position = 'absolute';
}
const result = Promise.resolve([
cloneIframeContainer,
cloner.clonedReferenceElement,
cloner.resourceLoader
]);
const onclone = options.onclone;
return cloner.clonedReferenceElement instanceof cloneWindow.HTMLElement ||
cloner.clonedReferenceElement instanceof ownerDocument.defaultView.HTMLElement ||
cloner.clonedReferenceElement instanceof HTMLElement
? typeof onclone === 'function'
? Promise.resolve().then(() => onclone(documentClone)).then(() => result)
: result
: Promise.reject(
__DEV__
? `Error finding the ${referenceElement.nodeName} in the cloned document`
: ''
);
});
documentClone.open();
documentClone.write(`${serializeDoctype(document.doctype)}<html></html>`);
// Chrome scrolls the parent document for some reason after the write to the cloned window???
restoreOwnerScroll(referenceElement.ownerDocument, scrollX, scrollY);
documentClone.replaceChild(
documentClone.adoptNode(cloner.documentElement),
documentClone.documentElement
);
documentClone.close();
return iframeLoad;
});
};
const serializeDoctype = (doctype: ?DocumentType): string => {
let str = '';
if (doctype) {
str += '<!DOCTYPE ';
if (doctype.name) {
str += doctype.name;
}
if (doctype.internalSubset) {
str += doctype.internalSubset;
}
if (doctype.publicId) {
str += `"${doctype.publicId}"`;
}
if (doctype.systemId) {
str += `"${doctype.systemId}"`;
}
str += '>';
}
return str;
};

View File

@ -1,251 +0,0 @@
/* @flow */
'use strict';
// http://dev.w3.org/csswg/css-color/
type ColorArray = [number, number, number, number | null];
const HEX3 = /^#([a-f0-9]{3})$/i;
const hex3 = (value: string): ColorArray | false => {
const match = value.match(HEX3);
if (match) {
return [
parseInt(match[1][0] + match[1][0], 16),
parseInt(match[1][1] + match[1][1], 16),
parseInt(match[1][2] + match[1][2], 16),
null
];
}
return false;
};
const HEX6 = /^#([a-f0-9]{6})$/i;
const hex6 = (value: string): ColorArray | false => {
const match = value.match(HEX6);
if (match) {
return [
parseInt(match[1].substring(0, 2), 16),
parseInt(match[1].substring(2, 4), 16),
parseInt(match[1].substring(4, 6), 16),
null
];
}
return false;
};
const RGB = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/;
const rgb = (value: string): ColorArray | false => {
const match = value.match(RGB);
if (match) {
return [Number(match[1]), Number(match[2]), Number(match[3]), null];
}
return false;
};
const RGBA = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d?\.?\d+)\s*\)$/;
const rgba = (value: string): ColorArray | false => {
const match = value.match(RGBA);
if (match && match.length > 4) {
return [Number(match[1]), Number(match[2]), Number(match[3]), Number(match[4])];
}
return false;
};
const fromArray = (array: Array<number>): ColorArray => {
return [
Math.min(array[0], 255),
Math.min(array[1], 255),
Math.min(array[2], 255),
array.length > 3 ? array[3] : null
];
};
const namedColor = (name: string): ColorArray | false => {
const color: ColorArray | void = NAMED_COLORS[name.toLowerCase()];
return color ? color : false;
};
export default class Color {
r: number;
g: number;
b: number;
a: number | null;
constructor(value: string | Array<number>) {
const [r, g, b, a] = Array.isArray(value)
? fromArray(value)
: hex3(value) ||
rgb(value) ||
rgba(value) ||
namedColor(value) ||
hex6(value) || [0, 0, 0, null];
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
isTransparent(): boolean {
return this.a === 0;
}
toString(): string {
return this.a !== null && this.a !== 1
? `rgba(${this.r},${this.g},${this.b},${this.a})`
: `rgb(${this.r},${this.g},${this.b})`;
}
}
const NAMED_COLORS = {
transparent: [0, 0, 0, 0],
aliceblue: [240, 248, 255, null],
antiquewhite: [250, 235, 215, null],
aqua: [0, 255, 255, null],
aquamarine: [127, 255, 212, null],
azure: [240, 255, 255, null],
beige: [245, 245, 220, null],
bisque: [255, 228, 196, null],
black: [0, 0, 0, null],
blanchedalmond: [255, 235, 205, null],
blue: [0, 0, 255, null],
blueviolet: [138, 43, 226, null],
brown: [165, 42, 42, null],
burlywood: [222, 184, 135, null],
cadetblue: [95, 158, 160, null],
chartreuse: [127, 255, 0, null],
chocolate: [210, 105, 30, null],
coral: [255, 127, 80, null],
cornflowerblue: [100, 149, 237, null],
cornsilk: [255, 248, 220, null],
crimson: [220, 20, 60, null],
cyan: [0, 255, 255, null],
darkblue: [0, 0, 139, null],
darkcyan: [0, 139, 139, null],
darkgoldenrod: [184, 134, 11, null],
darkgray: [169, 169, 169, null],
darkgreen: [0, 100, 0, null],
darkgrey: [169, 169, 169, null],
darkkhaki: [189, 183, 107, null],
darkmagenta: [139, 0, 139, null],
darkolivegreen: [85, 107, 47, null],
darkorange: [255, 140, 0, null],
darkorchid: [153, 50, 204, null],
darkred: [139, 0, 0, null],
darksalmon: [233, 150, 122, null],
darkseagreen: [143, 188, 143, null],
darkslateblue: [72, 61, 139, null],
darkslategray: [47, 79, 79, null],
darkslategrey: [47, 79, 79, null],
darkturquoise: [0, 206, 209, null],
darkviolet: [148, 0, 211, null],
deeppink: [255, 20, 147, null],
deepskyblue: [0, 191, 255, null],
dimgray: [105, 105, 105, null],
dimgrey: [105, 105, 105, null],
dodgerblue: [30, 144, 255, null],
firebrick: [178, 34, 34, null],
floralwhite: [255, 250, 240, null],
forestgreen: [34, 139, 34, null],
fuchsia: [255, 0, 255, null],
gainsboro: [220, 220, 220, null],
ghostwhite: [248, 248, 255, null],
gold: [255, 215, 0, null],
goldenrod: [218, 165, 32, null],
gray: [128, 128, 128, null],
green: [0, 128, 0, null],
greenyellow: [173, 255, 47, null],
grey: [128, 128, 128, null],
honeydew: [240, 255, 240, null],
hotpink: [255, 105, 180, null],
indianred: [205, 92, 92, null],
indigo: [75, 0, 130, null],
ivory: [255, 255, 240, null],
khaki: [240, 230, 140, null],
lavender: [230, 230, 250, null],
lavenderblush: [255, 240, 245, null],
lawngreen: [124, 252, 0, null],
lemonchiffon: [255, 250, 205, null],
lightblue: [173, 216, 230, null],
lightcoral: [240, 128, 128, null],
lightcyan: [224, 255, 255, null],
lightgoldenrodyellow: [250, 250, 210, null],
lightgray: [211, 211, 211, null],
lightgreen: [144, 238, 144, null],
lightgrey: [211, 211, 211, null],
lightpink: [255, 182, 193, null],
lightsalmon: [255, 160, 122, null],
lightseagreen: [32, 178, 170, null],
lightskyblue: [135, 206, 250, null],
lightslategray: [119, 136, 153, null],
lightslategrey: [119, 136, 153, null],
lightsteelblue: [176, 196, 222, null],
lightyellow: [255, 255, 224, null],
lime: [0, 255, 0, null],
limegreen: [50, 205, 50, null],
linen: [250, 240, 230, null],
magenta: [255, 0, 255, null],
maroon: [128, 0, 0, null],
mediumaquamarine: [102, 205, 170, null],
mediumblue: [0, 0, 205, null],
mediumorchid: [186, 85, 211, null],
mediumpurple: [147, 112, 219, null],
mediumseagreen: [60, 179, 113, null],
mediumslateblue: [123, 104, 238, null],
mediumspringgreen: [0, 250, 154, null],
mediumturquoise: [72, 209, 204, null],
mediumvioletred: [199, 21, 133, null],
midnightblue: [25, 25, 112, null],
mintcream: [245, 255, 250, null],
mistyrose: [255, 228, 225, null],
moccasin: [255, 228, 181, null],
navajowhite: [255, 222, 173, null],
navy: [0, 0, 128, null],
oldlace: [253, 245, 230, null],
olive: [128, 128, 0, null],
olivedrab: [107, 142, 35, null],
orange: [255, 165, 0, null],
orangered: [255, 69, 0, null],
orchid: [218, 112, 214, null],
palegoldenrod: [238, 232, 170, null],
palegreen: [152, 251, 152, null],
paleturquoise: [175, 238, 238, null],
palevioletred: [219, 112, 147, null],
papayawhip: [255, 239, 213, null],
peachpuff: [255, 218, 185, null],
peru: [205, 133, 63, null],
pink: [255, 192, 203, null],
plum: [221, 160, 221, null],
powderblue: [176, 224, 230, null],
purple: [128, 0, 128, null],
rebeccapurple: [102, 51, 153, null],
red: [255, 0, 0, null],
rosybrown: [188, 143, 143, null],
royalblue: [65, 105, 225, null],
saddlebrown: [139, 69, 19, null],
salmon: [250, 128, 114, null],
sandybrown: [244, 164, 96, null],
seagreen: [46, 139, 87, null],
seashell: [255, 245, 238, null],
sienna: [160, 82, 45, null],
silver: [192, 192, 192, null],
skyblue: [135, 206, 235, null],
slateblue: [106, 90, 205, null],
slategray: [112, 128, 144, null],
slategrey: [112, 128, 144, null],
snow: [255, 250, 250, null],
springgreen: [0, 255, 127, null],
steelblue: [70, 130, 180, null],
tan: [210, 180, 140, null],
teal: [0, 128, 128, null],
thistle: [216, 191, 216, null],
tomato: [255, 99, 71, null],
turquoise: [64, 224, 208, null],
violet: [238, 130, 238, null],
wheat: [245, 222, 179, null],
white: [255, 255, 255, null],
whitesmoke: [245, 245, 245, null],
yellow: [255, 255, 0, null],
yellowgreen: [154, 205, 50, null]
};
export const TRANSPARENT = new Color([0, 0, 0, 0]);

View File

@ -1,558 +0,0 @@
/* @flow */
'use strict';
import type {BackgroundSource} from './parsing/background';
import type {Bounds} from './Bounds';
import NodeContainer from './NodeContainer';
import {parseAngle} from './Angle';
import Color from './Color';
import Length, {LENGTH_TYPE, calculateLengthFromValueWithUnit} from './Length';
import {distance} from './Util';
const SIDE_OR_CORNER = /^(to )?(left|top|right|bottom)( (left|top|right|bottom))?$/i;
const PERCENTAGE_ANGLES = /^([+-]?\d*\.?\d+)% ([+-]?\d*\.?\d+)%$/i;
const ENDS_WITH_LENGTH = /(px)|%|( 0)$/i;
const FROM_TO_COLORSTOP = /^(from|to|color-stop)\((?:([\d.]+)(%)?,\s*)?(.+?)\)$/i;
const RADIAL_SHAPE_DEFINITION = /^\s*(circle|ellipse)?\s*((?:([\d.]+)(px|r?em|%)\s*(?:([\d.]+)(px|r?em|%))?)|closest-side|closest-corner|farthest-side|farthest-corner)?\s*(?:at\s*(?:(left|center|right)|([\d.]+)(px|r?em|%))\s+(?:(top|center|bottom)|([\d.]+)(px|r?em|%)))?(?:\s|$)/i;
export type Point = {
x: number,
y: number
};
export type Direction = {
x0: number,
x1: number,
y0: number,
y1: number
};
export type ColorStop = {
color: Color,
stop: number
};
export interface Gradient {
type: GradientType,
colorStops: Array<ColorStop>
}
export const GRADIENT_TYPE = {
LINEAR_GRADIENT: 0,
RADIAL_GRADIENT: 1
};
export type GradientType = $Values<typeof GRADIENT_TYPE>;
export const RADIAL_GRADIENT_SHAPE = {
CIRCLE: 0,
ELLIPSE: 1
};
export type RadialGradientShapeType = $Values<typeof RADIAL_GRADIENT_SHAPE>;
const LENGTH_FOR_POSITION = {
left: new Length('0%'),
top: new Length('0%'),
center: new Length('50%'),
right: new Length('100%'),
bottom: new Length('100%')
};
export class LinearGradient implements Gradient {
type: GradientType;
colorStops: Array<ColorStop>;
direction: Direction;
constructor(colorStops: Array<ColorStop>, direction: Direction) {
this.type = GRADIENT_TYPE.LINEAR_GRADIENT;
this.colorStops = colorStops;
this.direction = direction;
}
}
export class RadialGradient implements Gradient {
type: GradientType;
colorStops: Array<ColorStop>;
shape: RadialGradientShapeType;
center: Point;
radius: Point;
constructor(
colorStops: Array<ColorStop>,
shape: RadialGradientShapeType,
center: Point,
radius: Point
) {
this.type = GRADIENT_TYPE.RADIAL_GRADIENT;
this.colorStops = colorStops;
this.shape = shape;
this.center = center;
this.radius = radius;
}
}
export const parseGradient = (
container: NodeContainer,
{args, method, prefix}: BackgroundSource,
bounds: Bounds
): ?Gradient => {
if (method === 'linear-gradient') {
return parseLinearGradient(args, bounds, !!prefix);
} else if (method === 'gradient' && args[0] === 'linear') {
// TODO handle correct angle
return parseLinearGradient(
['to bottom'].concat(transformObsoleteColorStops(args.slice(3))),
bounds,
!!prefix
);
} else if (method === 'radial-gradient') {
return parseRadialGradient(
container,
prefix === '-webkit-' ? transformWebkitRadialGradientArgs(args) : args,
bounds
);
} else if (method === 'gradient' && args[0] === 'radial') {
return parseRadialGradient(
container,
transformObsoleteColorStops(transformWebkitRadialGradientArgs(args.slice(1))),
bounds
);
}
};
const parseColorStops = (args: Array<string>, firstColorStopIndex: number, lineLength: number) => {
const colorStops = [];
for (let i = firstColorStopIndex; i < args.length; i++) {
const value = args[i];
const HAS_LENGTH = ENDS_WITH_LENGTH.test(value);
const lastSpaceIndex = value.lastIndexOf(' ');
const color = new Color(HAS_LENGTH ? value.substring(0, lastSpaceIndex) : value);
const stop = HAS_LENGTH
? new Length(value.substring(lastSpaceIndex + 1))
: i === firstColorStopIndex
? new Length('0%')
: i === args.length - 1 ? new Length('100%') : null;
colorStops.push({color, stop});
}
const absoluteValuedColorStops = colorStops.map(({color, stop}) => {
const absoluteStop =
lineLength === 0 ? 0 : stop ? stop.getAbsoluteValue(lineLength) / lineLength : null;
return {
color,
// $FlowFixMe
stop: absoluteStop
};
});
let previousColorStop = absoluteValuedColorStops[0].stop;
for (let i = 0; i < absoluteValuedColorStops.length; i++) {
if (previousColorStop !== null) {
const stop = absoluteValuedColorStops[i].stop;
if (stop === null) {
let n = i;
while (absoluteValuedColorStops[n].stop === null) {
n++;
}
const steps = n - i + 1;
const nextColorStep = absoluteValuedColorStops[n].stop;
const stepSize = (nextColorStep - previousColorStop) / steps;
for (; i < n; i++) {
previousColorStop = absoluteValuedColorStops[i].stop =
previousColorStop + stepSize;
}
} else {
previousColorStop = stop;
}
}
}
return absoluteValuedColorStops;
};
const parseLinearGradient = (
args: Array<string>,
bounds: Bounds,
hasPrefix: boolean
): LinearGradient => {
const angle = parseAngle(args[0]);
const HAS_SIDE_OR_CORNER = SIDE_OR_CORNER.test(args[0]);
const HAS_DIRECTION = HAS_SIDE_OR_CORNER || angle !== null || PERCENTAGE_ANGLES.test(args[0]);
const direction = HAS_DIRECTION
? angle !== null
? calculateGradientDirection(
// if there is a prefix, the 0° angle points due East (instead of North per W3C)
hasPrefix ? angle - Math.PI * 0.5 : angle,
bounds
)
: HAS_SIDE_OR_CORNER
? parseSideOrCorner(args[0], bounds)
: parsePercentageAngle(args[0], bounds)
: calculateGradientDirection(Math.PI, bounds);
const firstColorStopIndex = HAS_DIRECTION ? 1 : 0;
// TODO: Fix some inaccuracy with color stops with px values
const lineLength = Math.min(
distance(
Math.abs(direction.x0) + Math.abs(direction.x1),
Math.abs(direction.y0) + Math.abs(direction.y1)
),
bounds.width * 2,
bounds.height * 2
);
return new LinearGradient(parseColorStops(args, firstColorStopIndex, lineLength), direction);
};
const parseRadialGradient = (
container: NodeContainer,
args: Array<string>,
bounds: Bounds
): RadialGradient => {
const m = args[0].match(RADIAL_SHAPE_DEFINITION);
const shape =
m &&
(m[1] === 'circle' || // explicit shape specification
(m[3] !== undefined && m[5] === undefined)) // only one radius coordinate
? RADIAL_GRADIENT_SHAPE.CIRCLE
: RADIAL_GRADIENT_SHAPE.ELLIPSE;
const radius = {};
const center = {};
if (m) {
// Radius
if (m[3] !== undefined) {
radius.x = calculateLengthFromValueWithUnit(container, m[3], m[4]).getAbsoluteValue(
bounds.width
);
}
if (m[5] !== undefined) {
radius.y = calculateLengthFromValueWithUnit(container, m[5], m[6]).getAbsoluteValue(
bounds.height
);
}
// Position
if (m[7]) {
center.x = LENGTH_FOR_POSITION[m[7].toLowerCase()];
} else if (m[8] !== undefined) {
center.x = calculateLengthFromValueWithUnit(container, m[8], m[9]);
}
if (m[10]) {
center.y = LENGTH_FOR_POSITION[m[10].toLowerCase()];
} else if (m[11] !== undefined) {
center.y = calculateLengthFromValueWithUnit(container, m[11], m[12]);
}
}
const gradientCenter = {
x: center.x === undefined ? bounds.width / 2 : center.x.getAbsoluteValue(bounds.width),
y: center.y === undefined ? bounds.height / 2 : center.y.getAbsoluteValue(bounds.height)
};
const gradientRadius = calculateRadius(
(m && m[2]) || 'farthest-corner',
shape,
gradientCenter,
radius,
bounds
);
return new RadialGradient(
parseColorStops(args, m ? 1 : 0, Math.min(gradientRadius.x, gradientRadius.y)),
shape,
gradientCenter,
gradientRadius
);
};
const calculateGradientDirection = (radian: number, bounds: Bounds): Direction => {
const width = bounds.width;
const height = bounds.height;
const HALF_WIDTH = width * 0.5;
const HALF_HEIGHT = height * 0.5;
const lineLength = Math.abs(width * Math.sin(radian)) + Math.abs(height * Math.cos(radian));
const HALF_LINE_LENGTH = lineLength / 2;
const x0 = HALF_WIDTH + Math.sin(radian) * HALF_LINE_LENGTH;
const y0 = HALF_HEIGHT - Math.cos(radian) * HALF_LINE_LENGTH;
const x1 = width - x0;
const y1 = height - y0;
return {x0, x1, y0, y1};
};
const parseTopRight = (bounds: Bounds) =>
Math.acos(bounds.width / 2 / (distance(bounds.width, bounds.height) / 2));
const parseSideOrCorner = (side: string, bounds: Bounds): Direction => {
switch (side) {
case 'bottom':
case 'to top':
return calculateGradientDirection(0, bounds);
case 'left':
case 'to right':
return calculateGradientDirection(Math.PI / 2, bounds);
case 'right':
case 'to left':
return calculateGradientDirection(3 * Math.PI / 2, bounds);
case 'top right':
case 'right top':
case 'to bottom left':
case 'to left bottom':
return calculateGradientDirection(Math.PI + parseTopRight(bounds), bounds);
case 'top left':
case 'left top':
case 'to bottom right':
case 'to right bottom':
return calculateGradientDirection(Math.PI - parseTopRight(bounds), bounds);
case 'bottom left':
case 'left bottom':
case 'to top right':
case 'to right top':
return calculateGradientDirection(parseTopRight(bounds), bounds);
case 'bottom right':
case 'right bottom':
case 'to top left':
case 'to left top':
return calculateGradientDirection(2 * Math.PI - parseTopRight(bounds), bounds);
case 'top':
case 'to bottom':
default:
return calculateGradientDirection(Math.PI, bounds);
}
};
const parsePercentageAngle = (angle: string, bounds: Bounds): Direction => {
const [left, top] = angle.split(' ').map(parseFloat);
const ratio = left / 100 * bounds.width / (top / 100 * bounds.height);
return calculateGradientDirection(Math.atan(isNaN(ratio) ? 1 : ratio) + Math.PI / 2, bounds);
};
const findCorner = (bounds: Bounds, x: number, y: number, closest: boolean): Point => {
var corners = [
{x: 0, y: 0},
{x: 0, y: bounds.height},
{x: bounds.width, y: 0},
{x: bounds.width, y: bounds.height}
];
// $FlowFixMe
return corners.reduce(
(stat, corner) => {
const d = distance(x - corner.x, y - corner.y);
if (closest ? d < stat.optimumDistance : d > stat.optimumDistance) {
return {
optimumCorner: corner,
optimumDistance: d
};
}
return stat;
},
{
optimumDistance: closest ? Infinity : -Infinity,
optimumCorner: null
}
).optimumCorner;
};
const calculateRadius = (
extent: string,
shape: RadialGradientShapeType,
center: Point,
radius: Point,
bounds: Bounds
): Point => {
const x = center.x;
const y = center.y;
let rx = 0;
let ry = 0;
switch (extent) {
case 'closest-side':
// The ending shape is sized so that that it exactly meets the side of the gradient box closest to the gradients center.
// If the shape is an ellipse, it exactly meets the closest side in each dimension.
if (shape === RADIAL_GRADIENT_SHAPE.CIRCLE) {
rx = ry = Math.min(
Math.abs(x),
Math.abs(x - bounds.width),
Math.abs(y),
Math.abs(y - bounds.height)
);
} else if (shape === RADIAL_GRADIENT_SHAPE.ELLIPSE) {
rx = Math.min(Math.abs(x), Math.abs(x - bounds.width));
ry = Math.min(Math.abs(y), Math.abs(y - bounds.height));
}
break;
case 'closest-corner':
// The ending shape is sized so that that it passes through the corner of the gradient box closest to the gradients center.
// If the shape is an ellipse, the ending shape is given the same aspect-ratio it would have if closest-side were specified.
if (shape === RADIAL_GRADIENT_SHAPE.CIRCLE) {
rx = ry = Math.min(
distance(x, y),
distance(x, y - bounds.height),
distance(x - bounds.width, y),
distance(x - bounds.width, y - bounds.height)
);
} else if (shape === RADIAL_GRADIENT_SHAPE.ELLIPSE) {
// Compute the ratio ry/rx (which is to be the same as for "closest-side")
const c =
Math.min(Math.abs(y), Math.abs(y - bounds.height)) /
Math.min(Math.abs(x), Math.abs(x - bounds.width));
const corner = findCorner(bounds, x, y, true);
rx = distance(corner.x - x, (corner.y - y) / c);
ry = c * rx;
}
break;
case 'farthest-side':
// Same as closest-side, except the ending shape is sized based on the farthest side(s)
if (shape === RADIAL_GRADIENT_SHAPE.CIRCLE) {
rx = ry = Math.max(
Math.abs(x),
Math.abs(x - bounds.width),
Math.abs(y),
Math.abs(y - bounds.height)
);
} else if (shape === RADIAL_GRADIENT_SHAPE.ELLIPSE) {
rx = Math.max(Math.abs(x), Math.abs(x - bounds.width));
ry = Math.max(Math.abs(y), Math.abs(y - bounds.height));
}
break;
case 'farthest-corner':
// Same as closest-corner, except the ending shape is sized based on the farthest corner.
// If the shape is an ellipse, the ending shape is given the same aspect ratio it would have if farthest-side were specified.
if (shape === RADIAL_GRADIENT_SHAPE.CIRCLE) {
rx = ry = Math.max(
distance(x, y),
distance(x, y - bounds.height),
distance(x - bounds.width, y),
distance(x - bounds.width, y - bounds.height)
);
} else if (shape === RADIAL_GRADIENT_SHAPE.ELLIPSE) {
// Compute the ratio ry/rx (which is to be the same as for "farthest-side")
const c =
Math.max(Math.abs(y), Math.abs(y - bounds.height)) /
Math.max(Math.abs(x), Math.abs(x - bounds.width));
const corner = findCorner(bounds, x, y, false);
rx = distance(corner.x - x, (corner.y - y) / c);
ry = c * rx;
}
break;
default:
// pixel or percentage values
rx = radius.x || 0;
ry = radius.y !== undefined ? radius.y : rx;
break;
}
return {
x: rx,
y: ry
};
};
export const transformWebkitRadialGradientArgs = (args: Array<string>): Array<string> => {
let shape = '';
let radius = '';
let extent = '';
let position = '';
let idx = 0;
const POSITION = /^(left|center|right|\d+(?:px|r?em|%)?)(?:\s+(top|center|bottom|\d+(?:px|r?em|%)?))?$/i;
const SHAPE_AND_EXTENT = /^(circle|ellipse)?\s*(closest-side|closest-corner|farthest-side|farthest-corner|contain|cover)?$/i;
const RADIUS = /^\d+(px|r?em|%)?(?:\s+\d+(px|r?em|%)?)?$/i;
const matchStartPosition = args[idx].match(POSITION);
if (matchStartPosition) {
idx++;
}
const matchShapeExtent = args[idx].match(SHAPE_AND_EXTENT);
if (matchShapeExtent) {
shape = matchShapeExtent[1] || '';
extent = matchShapeExtent[2] || '';
if (extent === 'contain') {
extent = 'closest-side';
} else if (extent === 'cover') {
extent = 'farthest-corner';
}
idx++;
}
const matchStartRadius = args[idx].match(RADIUS);
if (matchStartRadius) {
idx++;
}
const matchEndPosition = args[idx].match(POSITION);
if (matchEndPosition) {
idx++;
}
const matchEndRadius = args[idx].match(RADIUS);
if (matchEndRadius) {
idx++;
}
const matchPosition = matchEndPosition || matchStartPosition;
if (matchPosition && matchPosition[1]) {
position = matchPosition[1] + (/^\d+$/.test(matchPosition[1]) ? 'px' : '');
if (matchPosition[2]) {
position += ' ' + matchPosition[2] + (/^\d+$/.test(matchPosition[2]) ? 'px' : '');
}
}
const matchRadius = matchEndRadius || matchStartRadius;
if (matchRadius) {
radius = matchRadius[0];
if (!matchRadius[1]) {
radius += 'px';
}
}
if (position && !shape && !radius && !extent) {
radius = position;
position = '';
}
if (position) {
position = `at ${position}`;
}
return [[shape, extent, radius, position].filter(s => !!s).join(' ')].concat(args.slice(idx));
};
const transformObsoleteColorStops = (args: Array<string>): Array<string> => {
return (
args
.map(color => color.match(FROM_TO_COLORSTOP))
// $FlowFixMe
.map((v: Array<string>, index: number) => {
if (!v) {
return args[index];
}
switch (v[1]) {
case 'from':
return `${v[4]} 0%`;
case 'to':
return `${v[4]} 100%`;
case 'color-stop':
if (v[3] === '%') {
return `${v[4]} ${v[2]}`;
}
return `${v[4]} ${parseFloat(v[2]) * 100}%`;
}
})
);
};

View File

@ -1,155 +0,0 @@
/* @flow */
'use strict';
import type NodeContainer from './NodeContainer';
import TextContainer from './TextContainer';
import {BACKGROUND_CLIP, BACKGROUND_ORIGIN} from './parsing/background';
import {BORDER_STYLE} from './parsing/border';
import Circle from './drawing/Circle';
import Vector from './drawing/Vector';
import Color from './Color';
import Length from './Length';
import {Bounds} from './Bounds';
import {TextBounds} from './TextBounds';
import {copyCSSStyles} from './Util';
export const INPUT_COLOR = new Color([42, 42, 42]);
const INPUT_BORDER_COLOR = new Color([165, 165, 165]);
const INPUT_BACKGROUND_COLOR = new Color([222, 222, 222]);
const INPUT_BORDER = {
borderWidth: 1,
borderColor: INPUT_BORDER_COLOR,
borderStyle: BORDER_STYLE.SOLID
};
export const INPUT_BORDERS = [INPUT_BORDER, INPUT_BORDER, INPUT_BORDER, INPUT_BORDER];
export const INPUT_BACKGROUND = {
backgroundColor: INPUT_BACKGROUND_COLOR,
backgroundImage: [],
backgroundClip: BACKGROUND_CLIP.PADDING_BOX,
backgroundOrigin: BACKGROUND_ORIGIN.PADDING_BOX
};
const RADIO_BORDER_RADIUS = new Length('50%');
const RADIO_BORDER_RADIUS_TUPLE = [RADIO_BORDER_RADIUS, RADIO_BORDER_RADIUS];
const INPUT_RADIO_BORDER_RADIUS = [
RADIO_BORDER_RADIUS_TUPLE,
RADIO_BORDER_RADIUS_TUPLE,
RADIO_BORDER_RADIUS_TUPLE,
RADIO_BORDER_RADIUS_TUPLE
];
const CHECKBOX_BORDER_RADIUS = new Length('3px');
const CHECKBOX_BORDER_RADIUS_TUPLE = [CHECKBOX_BORDER_RADIUS, CHECKBOX_BORDER_RADIUS];
const INPUT_CHECKBOX_BORDER_RADIUS = [
CHECKBOX_BORDER_RADIUS_TUPLE,
CHECKBOX_BORDER_RADIUS_TUPLE,
CHECKBOX_BORDER_RADIUS_TUPLE,
CHECKBOX_BORDER_RADIUS_TUPLE
];
export const getInputBorderRadius = (node: HTMLInputElement) => {
return node.type === 'radio' ? INPUT_RADIO_BORDER_RADIUS : INPUT_CHECKBOX_BORDER_RADIUS;
};
export const inlineInputElement = (node: HTMLInputElement, container: NodeContainer): void => {
if (node.type === 'radio' || node.type === 'checkbox') {
if (node.checked) {
const size = Math.min(container.bounds.width, container.bounds.height);
container.childNodes.push(
node.type === 'checkbox'
? [
new Vector(
container.bounds.left + size * 0.39363,
container.bounds.top + size * 0.79
),
new Vector(
container.bounds.left + size * 0.16,
container.bounds.top + size * 0.5549
),
new Vector(
container.bounds.left + size * 0.27347,
container.bounds.top + size * 0.44071
),
new Vector(
container.bounds.left + size * 0.39694,
container.bounds.top + size * 0.5649
),
new Vector(
container.bounds.left + size * 0.72983,
container.bounds.top + size * 0.23
),
new Vector(
container.bounds.left + size * 0.84,
container.bounds.top + size * 0.34085
),
new Vector(
container.bounds.left + size * 0.39363,
container.bounds.top + size * 0.79
)
]
: new Circle(
container.bounds.left + size / 4,
container.bounds.top + size / 4,
size / 4
)
);
}
} else {
inlineFormElement(getInputValue(node), node, container, false);
}
};
export const inlineTextAreaElement = (
node: HTMLTextAreaElement,
container: NodeContainer
): void => {
inlineFormElement(node.value, node, container, true);
};
export const inlineSelectElement = (node: HTMLSelectElement, container: NodeContainer): void => {
const option = node.options[node.selectedIndex || 0];
inlineFormElement(option ? option.text || '' : '', node, container, false);
};
export const reformatInputBounds = (bounds: Bounds): Bounds => {
if (bounds.width > bounds.height) {
bounds.left += (bounds.width - bounds.height) / 2;
bounds.width = bounds.height;
} else if (bounds.width < bounds.height) {
bounds.top += (bounds.height - bounds.width) / 2;
bounds.height = bounds.width;
}
return bounds;
};
const inlineFormElement = (
value: string,
node: HTMLElement,
container: NodeContainer,
allowLinebreak: boolean
): void => {
const body = node.ownerDocument.body;
if (value.length > 0 && body) {
const wrapper = node.ownerDocument.createElement('html2canvaswrapper');
copyCSSStyles(node.ownerDocument.defaultView.getComputedStyle(node, null), wrapper);
wrapper.style.position = 'absolute';
wrapper.style.left = `${container.bounds.left}px`;
wrapper.style.top = `${container.bounds.top}px`;
if (!allowLinebreak) {
wrapper.style.whiteSpace = 'nowrap';
}
const text = node.ownerDocument.createTextNode(value);
wrapper.appendChild(text);
body.appendChild(wrapper);
container.childNodes.push(TextContainer.fromTextNode(text, container));
body.removeChild(wrapper);
}
};
const getInputValue = (node: HTMLInputElement): string => {
const value =
node.type === 'password' ? new Array(node.value.length + 1).join('\u2022') : node.value;
return value.length === 0 ? node.placeholder || '' : value;
};

View File

@ -1,68 +0,0 @@
/* @flow */
'use strict';
import type NodeContainer from './NodeContainer';
const LENGTH_WITH_UNIT = /([\d.]+)(px|r?em|%)/i;
export const LENGTH_TYPE = {
PX: 0,
PERCENTAGE: 1
};
export type LengthType = $Values<typeof LENGTH_TYPE>;
export default class Length {
type: LengthType;
value: number;
constructor(value: string) {
this.type =
value.substr(value.length - 1) === '%' ? LENGTH_TYPE.PERCENTAGE : LENGTH_TYPE.PX;
const parsedValue = parseFloat(value);
if (__DEV__ && isNaN(parsedValue)) {
console.error(`Invalid value given for Length: "${value}"`);
}
this.value = isNaN(parsedValue) ? 0 : parsedValue;
}
isPercentage(): boolean {
return this.type === LENGTH_TYPE.PERCENTAGE;
}
getAbsoluteValue(parentLength: number): number {
return this.isPercentage() ? parentLength * (this.value / 100) : this.value;
}
static create(v): Length {
return new Length(v);
}
}
const getRootFontSize = (container: NodeContainer): number => {
const parent = container.parent;
return parent ? getRootFontSize(parent) : parseFloat(container.style.font.fontSize);
};
export const calculateLengthFromValueWithUnit = (
container: NodeContainer,
value: string,
unit: string
): Length => {
switch (unit) {
case 'px':
case '%':
return new Length(value + unit);
case 'em':
case 'rem':
const length = new Length(value);
length.value *=
unit === 'em'
? parseFloat(container.style.font.fontSize)
: getRootFontSize(container);
return length;
default:
// TODO: handle correctly if unknown unit is used
return new Length('0');
}
};

View File

@ -1,48 +0,0 @@
/* @flow */
'use strict';
export default class Logger {
enabled: boolean;
start: number;
id: ?string;
constructor(enabled: boolean, id: ?string, start: ?number) {
this.enabled = typeof window !== 'undefined' && enabled;
this.start = start ? start : Date.now();
this.id = id;
}
child(id: string) {
return new Logger(this.enabled, id, this.start);
}
// eslint-disable-next-line flowtype/no-weak-types
log(...args: any) {
if (this.enabled && window.console && window.console.log) {
Function.prototype.bind
.call(window.console.log, window.console)
.apply(
window.console,
[
Date.now() - this.start + 'ms',
this.id ? `html2canvas (${this.id}):` : 'html2canvas:'
].concat([].slice.call(args, 0))
);
}
}
// eslint-disable-next-line flowtype/no-weak-types
error(...args: any) {
if (this.enabled && window.console && window.console.error) {
Function.prototype.bind
.call(window.console.error, window.console)
.apply(
window.console,
[
Date.now() - this.start + 'ms',
this.id ? `html2canvas (${this.id}):` : 'html2canvas:'
].concat([].slice.call(args, 0))
);
}
}
}

View File

@ -1,299 +0,0 @@
/* @flow */
'use strict';
import type {Background} from './parsing/background';
import type {Border} from './parsing/border';
import type {BorderRadius} from './parsing/borderRadius';
import type {DisplayBit} from './parsing/display';
import type {Float} from './parsing/float';
import type {Font} from './parsing/font';
import type {LineBreak} from './parsing/lineBreak';
import type {ListStyle} from './parsing/listStyle';
import type {Margin} from './parsing/margin';
import type {Overflow} from './parsing/overflow';
import type {OverflowWrap} from './parsing/overflowWrap';
import type {Padding} from './parsing/padding';
import type {Position} from './parsing/position';
import type {TextShadow} from './parsing/textShadow';
import type {TextTransform} from './parsing/textTransform';
import type {TextDecoration} from './parsing/textDecoration';
import type {Transform} from './parsing/transform';
import type {Visibility} from './parsing/visibility';
import type {WordBreak} from './parsing/word-break';
import type {zIndex} from './parsing/zIndex';
import type {Bounds, BoundCurves} from './Bounds';
import type ResourceLoader, {ImageElement} from './ResourceLoader';
import type {Path} from './drawing/Path';
import type TextContainer from './TextContainer';
import Color from './Color';
import {contains} from './Util';
import {parseBackground} from './parsing/background';
import {parseBorder} from './parsing/border';
import {parseBorderRadius} from './parsing/borderRadius';
import {parseDisplay, DISPLAY} from './parsing/display';
import {parseCSSFloat, FLOAT} from './parsing/float';
import {parseFont} from './parsing/font';
import {parseLetterSpacing} from './parsing/letterSpacing';
import {parseLineBreak} from './parsing/lineBreak';
import {parseListStyle} from './parsing/listStyle';
import {parseMargin} from './parsing/margin';
import {parseOverflow, OVERFLOW} from './parsing/overflow';
import {parseOverflowWrap} from './parsing/overflowWrap';
import {parsePadding} from './parsing/padding';
import {parsePosition, POSITION} from './parsing/position';
import {parseTextDecoration} from './parsing/textDecoration';
import {parseTextShadow} from './parsing/textShadow';
import {parseTextTransform} from './parsing/textTransform';
import {parseTransform} from './parsing/transform';
import {parseVisibility, VISIBILITY} from './parsing/visibility';
import {parseWordBreak} from './parsing/word-break';
import {parseZIndex} from './parsing/zIndex';
import {parseBounds, parseBoundCurves, calculatePaddingBoxPath} from './Bounds';
import {
INPUT_BACKGROUND,
INPUT_BORDERS,
INPUT_COLOR,
getInputBorderRadius,
reformatInputBounds
} from './Input';
import {getListOwner} from './ListItem';
type StyleDeclaration = {
background: Background,
border: Array<Border>,
borderRadius: Array<BorderRadius>,
color: Color,
display: DisplayBit,
float: Float,
font: Font,
letterSpacing: number,
lineBreak: LineBreak,
listStyle: ListStyle | null,
margin: Margin,
opacity: number,
overflow: Overflow,
overflowWrap: OverflowWrap,
padding: Padding,
position: Position,
textDecoration: TextDecoration | null,
textShadow: Array<TextShadow> | null,
textTransform: TextTransform,
transform: Transform,
visibility: Visibility,
wordBreak: WordBreak,
zIndex: zIndex
};
const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT'];
export default class NodeContainer {
name: ?string;
parent: ?NodeContainer;
style: StyleDeclaration;
childNodes: Array<TextContainer | Path>;
listItems: Array<NodeContainer>;
listIndex: ?number;
listStart: ?number;
bounds: Bounds;
curvedBounds: BoundCurves;
image: ?string;
index: number;
tagName: string;
constructor(
node: HTMLElement | SVGSVGElement,
parent: ?NodeContainer,
resourceLoader: ResourceLoader,
index: number
) {
this.parent = parent;
this.tagName = node.tagName;
this.index = index;
this.childNodes = [];
this.listItems = [];
if (typeof node.start === 'number') {
this.listStart = node.start;
}
const defaultView = node.ownerDocument.defaultView;
const scrollX = defaultView.pageXOffset;
const scrollY = defaultView.pageYOffset;
const style = defaultView.getComputedStyle(node, null);
const display = parseDisplay(style.display);
const IS_INPUT = node.type === 'radio' || node.type === 'checkbox';
const position = parsePosition(style.position);
this.style = {
background: IS_INPUT ? INPUT_BACKGROUND : parseBackground(style, resourceLoader),
border: IS_INPUT ? INPUT_BORDERS : parseBorder(style),
borderRadius:
(node instanceof defaultView.HTMLInputElement ||
node instanceof HTMLInputElement) &&
IS_INPUT
? getInputBorderRadius(node)
: parseBorderRadius(style),
color: IS_INPUT ? INPUT_COLOR : new Color(style.color),
display: display,
float: parseCSSFloat(style.float),
font: parseFont(style),
letterSpacing: parseLetterSpacing(style.letterSpacing),
listStyle: display === DISPLAY.LIST_ITEM ? parseListStyle(style) : null,
lineBreak: parseLineBreak(style.lineBreak),
margin: parseMargin(style),
opacity: parseFloat(style.opacity),
overflow:
INPUT_TAGS.indexOf(node.tagName) === -1
? parseOverflow(style.overflow)
: OVERFLOW.HIDDEN,
overflowWrap: parseOverflowWrap(
style.overflowWrap ? style.overflowWrap : style.wordWrap
),
padding: parsePadding(style),
position: position,
textDecoration: parseTextDecoration(style),
textShadow: parseTextShadow(style.textShadow),
textTransform: parseTextTransform(style.textTransform),
transform: parseTransform(style),
visibility: parseVisibility(style.visibility),
wordBreak: parseWordBreak(style.wordBreak),
zIndex: parseZIndex(position !== POSITION.STATIC ? style.zIndex : 'auto')
};
if (this.isTransformed()) {
// getBoundingClientRect provides values post-transform, we want them without the transformation
node.style.transform = 'matrix(1,0,0,1,0,0)';
}
if (display === DISPLAY.LIST_ITEM) {
const listOwner = getListOwner(this);
if (listOwner) {
const listIndex = listOwner.listItems.length;
listOwner.listItems.push(this);
this.listIndex =
node.hasAttribute('value') && typeof node.value === 'number'
? node.value
: listIndex === 0
? typeof listOwner.listStart === 'number' ? listOwner.listStart : 1
: listOwner.listItems[listIndex - 1].listIndex + 1;
}
}
// TODO move bound retrieval for all nodes to a later stage?
if (node.tagName === 'IMG') {
node.addEventListener('load', () => {
this.bounds = parseBounds(node, scrollX, scrollY);
this.curvedBounds = parseBoundCurves(
this.bounds,
this.style.border,
this.style.borderRadius
);
});
}
this.image = getImage(node, resourceLoader);
this.bounds = IS_INPUT
? reformatInputBounds(parseBounds(node, scrollX, scrollY))
: parseBounds(node, scrollX, scrollY);
this.curvedBounds = parseBoundCurves(
this.bounds,
this.style.border,
this.style.borderRadius
);
if (__DEV__) {
this.name = `${node.tagName.toLowerCase()}${node.id
? `#${node.id}`
: ''}${node.className
.toString()
.split(' ')
.map(s => (s.length ? `.${s}` : ''))
.join('')}`;
}
}
getClipPaths(): Array<Path> {
const parentClips = this.parent ? this.parent.getClipPaths() : [];
const isClipped = this.style.overflow !== OVERFLOW.VISIBLE;
return isClipped
? parentClips.concat([calculatePaddingBoxPath(this.curvedBounds)])
: parentClips;
}
isInFlow(): boolean {
return this.isRootElement() && !this.isFloating() && !this.isAbsolutelyPositioned();
}
isVisible(): boolean {
return (
!contains(this.style.display, DISPLAY.NONE) &&
this.style.opacity > 0 &&
this.style.visibility === VISIBILITY.VISIBLE
);
}
isAbsolutelyPositioned(): boolean {
return this.style.position !== POSITION.STATIC && this.style.position !== POSITION.RELATIVE;
}
isPositioned(): boolean {
return this.style.position !== POSITION.STATIC;
}
isFloating(): boolean {
return this.style.float !== FLOAT.NONE;
}
isRootElement(): boolean {
return this.parent === null;
}
isTransformed(): boolean {
return this.style.transform !== null;
}
isPositionedWithZIndex(): boolean {
return this.isPositioned() && !this.style.zIndex.auto;
}
isInlineLevel(): boolean {
return (
contains(this.style.display, DISPLAY.INLINE) ||
contains(this.style.display, DISPLAY.INLINE_BLOCK) ||
contains(this.style.display, DISPLAY.INLINE_FLEX) ||
contains(this.style.display, DISPLAY.INLINE_GRID) ||
contains(this.style.display, DISPLAY.INLINE_LIST_ITEM) ||
contains(this.style.display, DISPLAY.INLINE_TABLE)
);
}
isInlineBlockOrInlineTable(): boolean {
return (
contains(this.style.display, DISPLAY.INLINE_BLOCK) ||
contains(this.style.display, DISPLAY.INLINE_TABLE)
);
}
}
const getImage = (node: HTMLElement | SVGSVGElement, resourceLoader: ResourceLoader): ?string => {
if (
node instanceof node.ownerDocument.defaultView.SVGSVGElement ||
node instanceof SVGSVGElement
) {
const s = new XMLSerializer();
return resourceLoader.loadImage(
`data:image/svg+xml,${encodeURIComponent(s.serializeToString(node))}`
);
}
switch (node.tagName) {
case 'IMG':
// $FlowFixMe
const img: HTMLImageElement = node;
return resourceLoader.loadImage(img.currentSrc || img.src);
case 'CANVAS':
// $FlowFixMe
const canvas: HTMLCanvasElement = node;
return resourceLoader.loadCanvas(canvas);
case 'IFRAME':
const iframeKey = node.getAttribute('data-html2canvas-internal-iframe-key');
if (iframeKey) {
return iframeKey;
}
break;
}
return null;
};

View File

@ -1,165 +0,0 @@
/* @flow */
'use strict';
import type ResourceLoader, {ImageElement} from './ResourceLoader';
import type Logger from './Logger';
import StackingContext from './StackingContext';
import NodeContainer from './NodeContainer';
import TextContainer from './TextContainer';
import {inlineInputElement, inlineTextAreaElement, inlineSelectElement} from './Input';
import {inlineListItemElement} from './ListItem';
import {LIST_STYLE_TYPE} from './parsing/listStyle';
export const NodeParser = (
node: HTMLElement,
resourceLoader: ResourceLoader,
logger: Logger
): StackingContext => {
if (__DEV__) {
logger.log(`Starting node parsing`);
}
let index = 0;
const container = new NodeContainer(node, null, resourceLoader, index++);
const stack = new StackingContext(container, null, true);
parseNodeTree(node, container, stack, resourceLoader, index);
if (__DEV__) {
logger.log(`Finished parsing node tree`);
}
return stack;
};
const IGNORED_NODE_NAMES = ['SCRIPT', 'HEAD', 'TITLE', 'OBJECT', 'BR', 'OPTION'];
const parseNodeTree = (
node: HTMLElement,
parent: NodeContainer,
stack: StackingContext,
resourceLoader: ResourceLoader,
index: number
): void => {
if (__DEV__ && index > 50000) {
throw new Error(`Recursion error while parsing node tree`);
}
for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) {
nextNode = childNode.nextSibling;
const defaultView = childNode.ownerDocument.defaultView;
if (
childNode instanceof defaultView.Text ||
childNode instanceof Text ||
(defaultView.parent && childNode instanceof defaultView.parent.Text)
) {
if (childNode.data.trim().length > 0) {
parent.childNodes.push(TextContainer.fromTextNode(childNode, parent));
}
} else if (
childNode instanceof defaultView.HTMLElement ||
childNode instanceof HTMLElement ||
(defaultView.parent && childNode instanceof defaultView.parent.HTMLElement)
) {
if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) {
const container = new NodeContainer(childNode, parent, resourceLoader, index++);
if (container.isVisible()) {
if (childNode.tagName === 'INPUT') {
// $FlowFixMe
inlineInputElement(childNode, container);
} else if (childNode.tagName === 'TEXTAREA') {
// $FlowFixMe
inlineTextAreaElement(childNode, container);
} else if (childNode.tagName === 'SELECT') {
// $FlowFixMe
inlineSelectElement(childNode, container);
} else if (
container.style.listStyle &&
container.style.listStyle.listStyleType !== LIST_STYLE_TYPE.NONE
) {
inlineListItemElement(childNode, container, resourceLoader);
}
const SHOULD_TRAVERSE_CHILDREN = childNode.tagName !== 'TEXTAREA';
const treatAsRealStackingContext = createsRealStackingContext(
container,
childNode
);
if (treatAsRealStackingContext || createsStackingContext(container)) {
// for treatAsRealStackingContext:false, any positioned descendants and descendants
// which actually create a new stacking context should be considered part of the parent stacking context
const parentStack =
treatAsRealStackingContext || container.isPositioned()
? stack.getRealParentStackingContext()
: stack;
const childStack = new StackingContext(
container,
parentStack,
treatAsRealStackingContext
);
parentStack.contexts.push(childStack);
if (SHOULD_TRAVERSE_CHILDREN) {
parseNodeTree(childNode, container, childStack, resourceLoader, index);
}
} else {
stack.children.push(container);
if (SHOULD_TRAVERSE_CHILDREN) {
parseNodeTree(childNode, container, stack, resourceLoader, index);
}
}
}
}
} else if (
childNode instanceof defaultView.SVGSVGElement ||
childNode instanceof SVGSVGElement ||
(defaultView.parent && childNode instanceof defaultView.parent.SVGSVGElement)
) {
const container = new NodeContainer(childNode, parent, resourceLoader, index++);
const treatAsRealStackingContext = createsRealStackingContext(container, childNode);
if (treatAsRealStackingContext || createsStackingContext(container)) {
// for treatAsRealStackingContext:false, any positioned descendants and descendants
// which actually create a new stacking context should be considered part of the parent stacking context
const parentStack =
treatAsRealStackingContext || container.isPositioned()
? stack.getRealParentStackingContext()
: stack;
const childStack = new StackingContext(
container,
parentStack,
treatAsRealStackingContext
);
parentStack.contexts.push(childStack);
} else {
stack.children.push(container);
}
}
}
};
const createsRealStackingContext = (
container: NodeContainer,
node: HTMLElement | SVGSVGElement
): boolean => {
return (
container.isRootElement() ||
container.isPositionedWithZIndex() ||
container.style.opacity < 1 ||
container.isTransformed() ||
isBodyWithTransparentRoot(container, node)
);
};
const createsStackingContext = (container: NodeContainer): boolean => {
return container.isPositioned() || container.isFloating();
};
const isBodyWithTransparentRoot = (
container: NodeContainer,
node: HTMLElement | SVGSVGElement
): boolean => {
return (
node.nodeName === 'BODY' &&
container.parent instanceof NodeContainer &&
container.parent.style.background.backgroundColor.isTransparent()
);
};

View File

@ -1,62 +0,0 @@
/* @flow */
'use strict';
import type Options from './index';
import FEATURES from './Feature';
export const Proxy = (src: string, options: Options): Promise<string> => {
if (!options.proxy) {
return Promise.reject(__DEV__ ? 'No proxy defined' : null);
}
const proxy = options.proxy;
return new Promise((resolve, reject) => {
const responseType =
FEATURES.SUPPORT_CORS_XHR && FEATURES.SUPPORT_RESPONSE_TYPE ? 'blob' : 'text';
const xhr = FEATURES.SUPPORT_CORS_XHR ? new XMLHttpRequest() : new XDomainRequest();
xhr.onload = () => {
if (xhr instanceof XMLHttpRequest) {
if (xhr.status === 200) {
if (responseType === 'text') {
resolve(xhr.response);
} else {
const reader = new FileReader();
// $FlowFixMe
reader.addEventListener('load', () => resolve(reader.result), false);
// $FlowFixMe
reader.addEventListener('error', e => reject(e), false);
reader.readAsDataURL(xhr.response);
}
} else {
reject(
__DEV__
? `Failed to proxy resource ${src.substring(
0,
256
)} with status code ${xhr.status}`
: ''
);
}
} else {
resolve(xhr.responseText);
}
};
xhr.onerror = reject;
xhr.open('GET', `${proxy}?url=${encodeURIComponent(src)}&responseType=${responseType}`);
if (responseType !== 'text' && xhr instanceof XMLHttpRequest) {
xhr.responseType = responseType;
}
if (options.imageTimeout) {
const timeout = options.imageTimeout;
xhr.timeout = timeout;
xhr.ontimeout = () =>
reject(__DEV__ ? `Timed out (${timeout}ms) proxying ${src.substring(0, 256)}` : '');
}
xhr.send();
});
};

View File

@ -1,341 +0,0 @@
/* @flow */
'use strict';
import {createCounterText} from './ListItem';
import {parseListStyleType} from './parsing/listStyle';
export const PSEUDO_CONTENT_ITEM_TYPE = {
TEXT: 0,
IMAGE: 1
};
export const TOKEN_TYPE = {
STRING: 0,
ATTRIBUTE: 1,
URL: 2,
COUNTER: 3,
COUNTERS: 4,
OPENQUOTE: 5,
CLOSEQUOTE: 6
};
export type PseudoContentData = {
counters: {[string]: Array<number>},
quoteDepth: number
};
export type PseudoContentItem = {
type: $Values<typeof PSEUDO_CONTENT_ITEM_TYPE>,
value: string
};
export type Token = {
type: $Values<typeof TOKEN_TYPE>,
value?: string,
name?: string,
format?: string,
glue?: string
};
export const parseCounterReset = (
style: ?CSSStyleDeclaration,
data: PseudoContentData
): Array<string> => {
if (!style || !style.counterReset || style.counterReset === 'none') {
return [];
}
const counterNames: Array<string> = [];
const counterResets = style.counterReset.split(/\s*,\s*/);
const lenCounterResets = counterResets.length;
for (let i = 0; i < lenCounterResets; i++) {
const [counterName, initialValue] = counterResets[i].split(/\s+/);
counterNames.push(counterName);
let counter = data.counters[counterName];
if (!counter) {
counter = data.counters[counterName] = [];
}
counter.push(parseInt(initialValue || 0, 10));
}
return counterNames;
};
export const popCounters = (counterNames: Array<string>, data: PseudoContentData): void => {
const lenCounters = counterNames.length;
for (let i = 0; i < lenCounters; i++) {
data.counters[counterNames[i]].pop();
}
};
export const resolvePseudoContent = (
node: Node,
style: ?CSSStyleDeclaration,
data: PseudoContentData
): ?Array<PseudoContentItem> => {
if (
!style ||
!style.content ||
style.content === 'none' ||
style.content === '-moz-alt-content' ||
style.display === 'none'
) {
return null;
}
const tokens = parseContent(style.content);
const len = tokens.length;
const contentItems: Array<PseudoContentItem> = [];
let s = '';
// increment the counter (if there is a "counter-increment" declaration)
const counterIncrement = style.counterIncrement;
if (counterIncrement && counterIncrement !== 'none') {
const [counterName, incrementValue] = counterIncrement.split(/\s+/);
const counter = data.counters[counterName];
if (counter) {
counter[counter.length - 1] +=
incrementValue === undefined ? 1 : parseInt(incrementValue, 10);
}
}
// build the content string
for (let i = 0; i < len; i++) {
const token = tokens[i];
switch (token.type) {
case TOKEN_TYPE.STRING:
s += token.value || '';
break;
case TOKEN_TYPE.ATTRIBUTE:
if (node instanceof HTMLElement && token.value) {
s += node.getAttribute(token.value) || '';
}
break;
case TOKEN_TYPE.COUNTER:
const counter = data.counters[token.name || ''];
if (counter) {
s += formatCounterValue([counter[counter.length - 1]], '', token.format);
}
break;
case TOKEN_TYPE.COUNTERS:
const counters = data.counters[token.name || ''];
if (counters) {
s += formatCounterValue(counters, token.glue, token.format);
}
break;
case TOKEN_TYPE.OPENQUOTE:
s += getQuote(style, true, data.quoteDepth);
data.quoteDepth++;
break;
case TOKEN_TYPE.CLOSEQUOTE:
data.quoteDepth--;
s += getQuote(style, false, data.quoteDepth);
break;
case TOKEN_TYPE.URL:
if (s) {
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.TEXT, value: s});
s = '';
}
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.IMAGE, value: token.value || ''});
break;
}
}
if (s) {
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.TEXT, value: s});
}
return contentItems;
};
export const parseContent = (content: string, cache?: {[string]: Array<Token>}): Array<Token> => {
if (cache && cache[content]) {
return cache[content];
}
const tokens: Array<Token> = [];
const len = content.length;
let isString = false;
let isEscaped = false;
let isFunction = false;
let str = '';
let functionName = '';
let args = [];
for (let i = 0; i < len; i++) {
const c = content.charAt(i);
switch (c) {
case "'":
case '"':
if (isEscaped) {
str += c;
} else {
isString = !isString;
if (!isFunction && !isString) {
tokens.push({type: TOKEN_TYPE.STRING, value: str});
str = '';
}
}
break;
case '\\':
if (isEscaped) {
str += c;
isEscaped = false;
} else {
isEscaped = true;
}
break;
case '(':
if (isString) {
str += c;
} else {
isFunction = true;
functionName = str;
str = '';
args = [];
}
break;
case ')':
if (isString) {
str += c;
} else if (isFunction) {
if (str) {
args.push(str);
}
switch (functionName) {
case 'attr':
if (args.length > 0) {
tokens.push({type: TOKEN_TYPE.ATTRIBUTE, value: args[0]});
}
break;
case 'counter':
if (args.length > 0) {
const counter: Token = {
type: TOKEN_TYPE.COUNTER,
name: args[0]
};
if (args.length > 1) {
counter.format = args[1];
}
tokens.push(counter);
}
break;
case 'counters':
if (args.length > 0) {
const counters: Token = {
type: TOKEN_TYPE.COUNTERS,
name: args[0]
};
if (args.length > 1) {
counters.glue = args[1];
}
if (args.length > 2) {
counters.format = args[2];
}
tokens.push(counters);
}
break;
case 'url':
if (args.length > 0) {
tokens.push({type: TOKEN_TYPE.URL, value: args[0]});
}
break;
}
isFunction = false;
str = '';
}
break;
case ',':
if (isString) {
str += c;
} else if (isFunction) {
args.push(str);
str = '';
}
break;
case ' ':
case '\t':
if (isString) {
str += c;
} else if (str) {
addOtherToken(tokens, str);
str = '';
}
break;
default:
str += c;
}
if (c !== '\\') {
isEscaped = false;
}
}
if (str) {
addOtherToken(tokens, str);
}
if (cache) {
cache[content] = tokens;
}
return tokens;
};
const addOtherToken = (tokens: Array<Token>, identifier: string): void => {
switch (identifier) {
case 'open-quote':
tokens.push({type: TOKEN_TYPE.OPENQUOTE});
break;
case 'close-quote':
tokens.push({type: TOKEN_TYPE.CLOSEQUOTE});
break;
}
};
const getQuote = (style: CSSStyleDeclaration, isOpening: boolean, quoteDepth: number): string => {
const quotes = style.quotes ? style.quotes.split(/\s+/) : ["'\"'", "'\"'"];
let idx = quoteDepth * 2;
if (idx >= quotes.length) {
idx = quotes.length - 2;
}
if (!isOpening) {
++idx;
}
return quotes[idx].replace(/^["']|["']$/g, '');
};
const formatCounterValue = (counter, glue: ?string, format: ?string): string => {
const len = counter.length;
let result = '';
for (let i = 0; i < len; i++) {
if (i > 0) {
result += glue || '';
}
result += createCounterText(counter[i], parseListStyleType(format || 'decimal'), false);
}
return result;
};

View File

@ -1,450 +0,0 @@
/* @flow */
'use strict';
import type Color from './Color';
import type {Path} from './drawing/Path';
import type Size from './drawing/Size';
import type Logger from './Logger';
import type {BackgroundImage} from './parsing/background';
import type {Border, BorderSide} from './parsing/border';
import type {Font} from './parsing/font';
import type {TextDecoration} from './parsing/textDecoration';
import type {TextShadow} from './parsing/textShadow';
import type {Matrix} from './parsing/transform';
import type {BoundCurves} from './Bounds';
import type {LinearGradient, RadialGradient} from './Gradient';
import type {ResourceStore, ImageElement} from './ResourceLoader';
import type NodeContainer from './NodeContainer';
import type StackingContext from './StackingContext';
import type {TextBounds} from './TextBounds';
import {Bounds, parsePathForBorder, calculateContentBox, calculatePaddingBoxPath} from './Bounds';
import {FontMetrics} from './Font';
import {parseGradient, GRADIENT_TYPE} from './Gradient';
import TextContainer from './TextContainer';
import {
calculateBackgroungPositioningArea,
calculateBackgroungPaintingArea,
calculateBackgroundPosition,
calculateBackgroundRepeatPath,
calculateBackgroundSize,
calculateGradientBackgroundSize
} from './parsing/background';
import {BORDER_STYLE} from './parsing/border';
export type RenderOptions = {
scale: number,
backgroundColor: ?Color,
imageStore: ResourceStore,
fontMetrics: FontMetrics,
logger: Logger,
x: number,
y: number,
width: number,
height: number
};
export interface RenderTarget<Output> {
clip(clipPaths: Array<Path>, callback: () => void): void,
drawImage(image: ImageElement, source: Bounds, destination: Bounds): void,
drawShape(path: Path, color: Color): void,
fill(color: Color): void,
getTarget(): Promise<Output>,
rectangle(x: number, y: number, width: number, height: number, color: Color): void,
render(options: RenderOptions): void,
renderLinearGradient(bounds: Bounds, gradient: LinearGradient): void,
renderRadialGradient(bounds: Bounds, gradient: RadialGradient): void,
renderRepeat(
path: Path,
image: ImageElement,
imageSize: Size,
offsetX: number,
offsetY: number
): void,
renderTextNode(
textBounds: Array<TextBounds>,
color: Color,
font: Font,
textDecoration: TextDecoration | null,
textShadows: Array<TextShadow> | null
): void,
setOpacity(opacity: number): void,
transform(offsetX: number, offsetY: number, matrix: Matrix, callback: () => void): void
}
export default class Renderer {
target: RenderTarget<*>;
options: RenderOptions;
_opacity: ?number;
constructor(target: RenderTarget<*>, options: RenderOptions) {
this.target = target;
this.options = options;
target.render(options);
}
renderNode(container: NodeContainer) {
if (container.isVisible()) {
this.renderNodeBackgroundAndBorders(container);
this.renderNodeContent(container);
}
}
renderNodeContent(container: NodeContainer) {
const callback = () => {
if (container.childNodes.length) {
container.childNodes.forEach(child => {
if (child instanceof TextContainer) {
const style = child.parent.style;
this.target.renderTextNode(
child.bounds,
style.color,
style.font,
style.textDecoration,
style.textShadow
);
} else {
this.target.drawShape(child, container.style.color);
}
});
}
if (container.image) {
const image = this.options.imageStore.get(container.image);
if (image) {
const contentBox = calculateContentBox(
container.bounds,
container.style.padding,
container.style.border
);
const width =
typeof image.width === 'number' && image.width > 0
? image.width
: contentBox.width;
const height =
typeof image.height === 'number' && image.height > 0
? image.height
: contentBox.height;
if (width > 0 && height > 0) {
this.target.clip([calculatePaddingBoxPath(container.curvedBounds)], () => {
this.target.drawImage(
image,
new Bounds(0, 0, width, height),
contentBox
);
});
}
}
}
};
const paths = container.getClipPaths();
if (paths.length) {
this.target.clip(paths, callback);
} else {
callback();
}
}
renderNodeBackgroundAndBorders(container: NodeContainer) {
const HAS_BACKGROUND =
!container.style.background.backgroundColor.isTransparent() ||
container.style.background.backgroundImage.length;
const hasRenderableBorders = container.style.border.some(
border =>
border.borderStyle !== BORDER_STYLE.NONE && !border.borderColor.isTransparent()
);
const callback = () => {
const backgroundPaintingArea = calculateBackgroungPaintingArea(
container.curvedBounds,
container.style.background.backgroundClip
);
if (HAS_BACKGROUND) {
this.target.clip([backgroundPaintingArea], () => {
if (!container.style.background.backgroundColor.isTransparent()) {
this.target.fill(container.style.background.backgroundColor);
}
this.renderBackgroundImage(container);
});
}
container.style.border.forEach((border, side) => {
if (
border.borderStyle !== BORDER_STYLE.NONE &&
!border.borderColor.isTransparent()
) {
this.renderBorder(border, side, container.curvedBounds);
}
});
};
if (HAS_BACKGROUND || hasRenderableBorders) {
const paths = container.parent ? container.parent.getClipPaths() : [];
if (paths.length) {
this.target.clip(paths, callback);
} else {
callback();
}
}
}
renderBackgroundImage(container: NodeContainer) {
container.style.background.backgroundImage.slice(0).reverse().forEach(backgroundImage => {
if (backgroundImage.source.method === 'url' && backgroundImage.source.args.length) {
this.renderBackgroundRepeat(container, backgroundImage);
} else if (/gradient/i.test(backgroundImage.source.method)) {
this.renderBackgroundGradient(container, backgroundImage);
}
});
}
renderBackgroundRepeat(container: NodeContainer, background: BackgroundImage) {
const image = this.options.imageStore.get(background.source.args[0]);
if (image) {
const backgroundPositioningArea = calculateBackgroungPositioningArea(
container.style.background.backgroundOrigin,
container.bounds,
container.style.padding,
container.style.border
);
const backgroundImageSize = calculateBackgroundSize(
background,
image,
backgroundPositioningArea
);
const position = calculateBackgroundPosition(
background.position,
backgroundImageSize,
backgroundPositioningArea
);
const path = calculateBackgroundRepeatPath(
background,
position,
backgroundImageSize,
backgroundPositioningArea,
container.bounds
);
const offsetX = Math.round(backgroundPositioningArea.left + position.x);
const offsetY = Math.round(backgroundPositioningArea.top + position.y);
this.target.renderRepeat(path, image, backgroundImageSize, offsetX, offsetY);
}
}
renderBackgroundGradient(container: NodeContainer, background: BackgroundImage) {
const backgroundPositioningArea = calculateBackgroungPositioningArea(
container.style.background.backgroundOrigin,
container.bounds,
container.style.padding,
container.style.border
);
const backgroundImageSize = calculateGradientBackgroundSize(
background,
backgroundPositioningArea
);
const position = calculateBackgroundPosition(
background.position,
backgroundImageSize,
backgroundPositioningArea
);
const gradientBounds = new Bounds(
Math.round(backgroundPositioningArea.left + position.x),
Math.round(backgroundPositioningArea.top + position.y),
backgroundImageSize.width,
backgroundImageSize.height
);
const gradient = parseGradient(container, background.source, gradientBounds);
if (gradient) {
switch (gradient.type) {
case GRADIENT_TYPE.LINEAR_GRADIENT:
// $FlowFixMe
this.target.renderLinearGradient(gradientBounds, gradient);
break;
case GRADIENT_TYPE.RADIAL_GRADIENT:
// $FlowFixMe
this.target.renderRadialGradient(gradientBounds, gradient);
break;
}
}
}
renderBorder(border: Border, side: BorderSide, curvePoints: BoundCurves) {
this.target.drawShape(parsePathForBorder(curvePoints, side), border.borderColor);
}
renderStack(stack: StackingContext) {
if (stack.container.isVisible()) {
const opacity = stack.getOpacity();
if (opacity !== this._opacity) {
this.target.setOpacity(stack.getOpacity());
this._opacity = opacity;
}
const transform = stack.container.style.transform;
if (transform !== null) {
this.target.transform(
stack.container.bounds.left + transform.transformOrigin[0].value,
stack.container.bounds.top + transform.transformOrigin[1].value,
transform.transform,
() => this.renderStackContent(stack)
);
} else {
this.renderStackContent(stack);
}
}
}
renderStackContent(stack: StackingContext) {
const [
negativeZIndex,
zeroOrAutoZIndexOrTransformedOrOpacity,
positiveZIndex,
nonPositionedFloats,
nonPositionedInlineLevel
] = splitStackingContexts(stack);
const [inlineLevel, nonInlineLevel] = splitDescendants(stack);
// https://www.w3.org/TR/css-position-3/#painting-order
// 1. the background and borders of the element forming the stacking context.
this.renderNodeBackgroundAndBorders(stack.container);
// 2. the child stacking contexts with negative stack levels (most negative first).
negativeZIndex.sort(sortByZIndex).forEach(this.renderStack, this);
// 3. For all its in-flow, non-positioned, block-level descendants in tree order:
this.renderNodeContent(stack.container);
nonInlineLevel.forEach(this.renderNode, this);
// 4. All non-positioned floating descendants, in tree order. For each one of these,
// treat the element as if it created a new stacking context, but any positioned descendants and descendants
// which actually create a new stacking context should be considered part of the parent stacking context,
// not this new one.
nonPositionedFloats.forEach(this.renderStack, this);
// 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
nonPositionedInlineLevel.forEach(this.renderStack, this);
inlineLevel.forEach(this.renderNode, this);
// 6. All positioned, opacity or transform descendants, in tree order that fall into the following categories:
// All positioned descendants with 'z-index: auto' or 'z-index: 0', in tree order.
// For those with 'z-index: auto', treat the element as if it created a new stacking context,
// but any positioned descendants and descendants which actually create a new stacking context should be
// considered part of the parent stacking context, not this new one. For those with 'z-index: 0',
// treat the stacking context generated atomically.
//
// All opacity descendants with opacity less than 1
//
// All transform descendants with transform other than none
zeroOrAutoZIndexOrTransformedOrOpacity.forEach(this.renderStack, this);
// 7. Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index
// order (smallest first) then tree order.
positiveZIndex.sort(sortByZIndex).forEach(this.renderStack, this);
}
render(stack: StackingContext): Promise<*> {
if (this.options.backgroundColor) {
this.target.rectangle(
this.options.x,
this.options.y,
this.options.width,
this.options.height,
this.options.backgroundColor
);
}
this.renderStack(stack);
const target = this.target.getTarget();
if (__DEV__) {
return target.then(output => {
this.options.logger.log(`Render completed`);
return output;
});
}
return target;
}
}
const splitDescendants = (stack: StackingContext): [Array<NodeContainer>, Array<NodeContainer>] => {
const inlineLevel = [];
const nonInlineLevel = [];
const length = stack.children.length;
for (let i = 0; i < length; i++) {
let child = stack.children[i];
if (child.isInlineLevel()) {
inlineLevel.push(child);
} else {
nonInlineLevel.push(child);
}
}
return [inlineLevel, nonInlineLevel];
};
const splitStackingContexts = (
stack: StackingContext
): [
Array<StackingContext>,
Array<StackingContext>,
Array<StackingContext>,
Array<StackingContext>,
Array<StackingContext>
] => {
const negativeZIndex = [];
const zeroOrAutoZIndexOrTransformedOrOpacity = [];
const positiveZIndex = [];
const nonPositionedFloats = [];
const nonPositionedInlineLevel = [];
const length = stack.contexts.length;
for (let i = 0; i < length; i++) {
let child = stack.contexts[i];
if (
child.container.isPositioned() ||
child.container.style.opacity < 1 ||
child.container.isTransformed()
) {
if (child.container.style.zIndex.order < 0) {
negativeZIndex.push(child);
} else if (child.container.style.zIndex.order > 0) {
positiveZIndex.push(child);
} else {
zeroOrAutoZIndexOrTransformedOrOpacity.push(child);
}
} else {
if (child.container.isFloating()) {
nonPositionedFloats.push(child);
} else {
nonPositionedInlineLevel.push(child);
}
}
}
return [
negativeZIndex,
zeroOrAutoZIndexOrTransformedOrOpacity,
positiveZIndex,
nonPositionedFloats,
nonPositionedInlineLevel
];
};
const sortByZIndex = (a: StackingContext, b: StackingContext): number => {
if (a.container.style.zIndex.order > b.container.style.zIndex.order) {
return 1;
} else if (a.container.style.zIndex.order < b.container.style.zIndex.order) {
return -1;
}
return a.container.index > b.container.index ? 1 : -1;
};

View File

@ -1,240 +0,0 @@
/* @flow */
'use strict';
import type Options from './index';
import type Logger from './Logger';
export type ImageElement = Image | HTMLCanvasElement;
export type Resource = ImageElement;
type ResourceCache = {[string]: Promise<Resource>};
import FEATURES from './Feature';
import {Proxy} from './Proxy';
export default class ResourceLoader {
origin: string;
options: Options;
_link: HTMLAnchorElement;
cache: ResourceCache;
logger: Logger;
_index: number;
_window: WindowProxy;
constructor(options: Options, logger: Logger, window: WindowProxy) {
this.options = options;
this._window = window;
this.origin = this.getOrigin(window.location.href);
this.cache = {};
this.logger = logger;
this._index = 0;
}
loadImage(src: string): ?string {
if (this.hasResourceInCache(src)) {
return src;
}
if (isBlobImage(src)) {
this.cache[src] = loadImage(src, this.options.imageTimeout || 0);
return src;
}
if (!isSVG(src) || FEATURES.SUPPORT_SVG_DRAWING) {
if (this.options.allowTaint === true || isInlineImage(src) || this.isSameOrigin(src)) {
return this.addImage(src, src, false);
} else if (!this.isSameOrigin(src)) {
if (typeof this.options.proxy === 'string') {
this.cache[src] = Proxy(src, this.options).then(src =>
loadImage(src, this.options.imageTimeout || 0)
);
return src;
} else if (this.options.useCORS === true && FEATURES.SUPPORT_CORS_IMAGES) {
return this.addImage(src, src, true);
}
}
}
}
inlineImage(src: string): Promise<Resource> {
if (isInlineImage(src)) {
return loadImage(src, this.options.imageTimeout || 0);
}
if (this.hasResourceInCache(src)) {
return this.cache[src];
}
if (!this.isSameOrigin(src) && typeof this.options.proxy === 'string') {
return (this.cache[src] = Proxy(src, this.options).then(src =>
loadImage(src, this.options.imageTimeout || 0)
));
}
return this.xhrImage(src);
}
xhrImage(src: string): Promise<Resource> {
this.cache[src] = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status !== 200) {
reject(
`Failed to fetch image ${src.substring(
0,
256
)} with status code ${xhr.status}`
);
} else {
const reader = new FileReader();
reader.addEventListener(
'load',
() => {
// $FlowFixMe
const result: string = reader.result;
resolve(result);
},
false
);
reader.addEventListener('error', (e: Event) => reject(e), false);
reader.readAsDataURL(xhr.response);
}
}
};
xhr.responseType = 'blob';
if (this.options.imageTimeout) {
const timeout = this.options.imageTimeout;
xhr.timeout = timeout;
xhr.ontimeout = () =>
reject(
__DEV__ ? `Timed out (${timeout}ms) fetching ${src.substring(0, 256)}` : ''
);
}
xhr.open('GET', src, true);
xhr.send();
}).then(src => loadImage(src, this.options.imageTimeout || 0));
return this.cache[src];
}
loadCanvas(node: HTMLCanvasElement): string {
const key = String(this._index++);
this.cache[key] = Promise.resolve(node);
return key;
}
hasResourceInCache(key: string): boolean {
return typeof this.cache[key] !== 'undefined';
}
addImage(key: string, src: string, useCORS: boolean): string {
if (__DEV__) {
this.logger.log(`Added image ${key.substring(0, 256)}`);
}
this.cache[key] = new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
//ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
if (isInlineBase64Image(src) || useCORS) {
img.crossOrigin = 'anonymous';
}
img.onerror = reject;
img.src = src;
if (img.complete === true) {
// Inline XML images may fail to parse, throwing an Error later on
setTimeout(() => {
resolve(img);
}, 500);
}
if (this.options.imageTimeout) {
const timeout = this.options.imageTimeout;
setTimeout(
() =>
reject(
__DEV__
? `Timed out (${timeout}ms) fetching ${src.substring(0, 256)}`
: ''
),
timeout
);
}
});
return key;
}
isSameOrigin(url: string): boolean {
return this.getOrigin(url) === this.origin;
}
getOrigin(url: string): string {
const link = this._link || (this._link = this._window.document.createElement('a'));
link.href = url;
link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/
return link.protocol + link.hostname + link.port;
}
ready(): Promise<ResourceStore> {
const keys: Array<string> = Object.keys(this.cache);
const values: Array<Promise<?Resource>> = keys.map(str =>
this.cache[str].catch(e => {
if (__DEV__) {
this.logger.log(`Unable to load image`, e);
}
return null;
})
);
return Promise.all(values).then((images: Array<?Resource>) => {
if (__DEV__) {
this.logger.log(`Finished loading ${images.length} images`, images);
}
return new ResourceStore(keys, images);
});
}
}
export class ResourceStore {
_keys: Array<string>;
_resources: Array<?Resource>;
constructor(keys: Array<string>, resources: Array<?Resource>) {
this._keys = keys;
this._resources = resources;
}
get(key: string): ?Resource {
const index = this._keys.indexOf(key);
return index === -1 ? null : this._resources[index];
}
}
const INLINE_SVG = /^data:image\/svg\+xml/i;
const INLINE_BASE64 = /^data:image\/.*;base64,/i;
const INLINE_IMG = /^data:image\/.*/i;
const isInlineImage = (src: string): boolean => INLINE_IMG.test(src);
const isInlineBase64Image = (src: string): boolean => INLINE_BASE64.test(src);
const isBlobImage = (src: string): boolean => src.substr(0, 4) === 'blob';
const isSVG = (src: string): boolean =>
src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src);
const loadImage = (src: string, timeout: number): Promise<Image> => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
if (img.complete === true) {
// Inline XML images may fail to parse, throwing an Error later on
setTimeout(() => {
resolve(img);
}, 500);
}
if (timeout) {
setTimeout(
() => reject(__DEV__ ? `Timed out (${timeout}ms) loading image` : ''),
timeout
);
}
});
};

View File

@ -1,37 +0,0 @@
/* @flow */
'use strict';
import NodeContainer from './NodeContainer';
import {POSITION} from './parsing/position';
export default class StackingContext {
container: NodeContainer;
parent: ?StackingContext;
contexts: Array<StackingContext>;
children: Array<NodeContainer>;
treatAsRealStackingContext: boolean;
constructor(
container: NodeContainer,
parent: ?StackingContext,
treatAsRealStackingContext: boolean
) {
this.container = container;
this.parent = parent;
this.contexts = [];
this.children = [];
this.treatAsRealStackingContext = treatAsRealStackingContext;
}
getOpacity(): number {
return this.parent
? this.container.style.opacity * this.parent.getOpacity()
: this.container.style.opacity;
}
getRealParentStackingContext(): StackingContext {
return !this.parent || this.treatAsRealStackingContext
? this
: this.parent.getRealParentStackingContext();
}
}

View File

@ -1,85 +0,0 @@
/* @flow */
'use strict';
import type NodeContainer from './NodeContainer';
import {Bounds, parseBounds} from './Bounds';
import {TEXT_DECORATION} from './parsing/textDecoration';
import FEATURES from './Feature';
import {breakWords, toCodePoints, fromCodePoint} from './Unicode';
export class TextBounds {
text: string;
bounds: Bounds;
constructor(text: string, bounds: Bounds) {
this.text = text;
this.bounds = bounds;
}
}
export const parseTextBounds = (
value: string,
parent: NodeContainer,
node: Text
): Array<TextBounds> => {
const letterRendering = parent.style.letterSpacing !== 0;
const textList = letterRendering
? toCodePoints(value).map(i => fromCodePoint(i))
: breakWords(value, parent);
const length = textList.length;
const defaultView = node.parentNode ? node.parentNode.ownerDocument.defaultView : null;
const scrollX = defaultView ? defaultView.pageXOffset : 0;
const scrollY = defaultView ? defaultView.pageYOffset : 0;
const textBounds = [];
let offset = 0;
for (let i = 0; i < length; i++) {
let text = textList[i];
if (parent.style.textDecoration !== TEXT_DECORATION.NONE || text.trim().length > 0) {
if (FEATURES.SUPPORT_RANGE_BOUNDS) {
textBounds.push(
new TextBounds(
text,
getRangeBounds(node, offset, text.length, scrollX, scrollY)
)
);
} else {
const replacementNode = node.splitText(text.length);
textBounds.push(new TextBounds(text, getWrapperBounds(node, scrollX, scrollY)));
node = replacementNode;
}
} else if (!FEATURES.SUPPORT_RANGE_BOUNDS) {
node = node.splitText(text.length);
}
offset += text.length;
}
return textBounds;
};
const getWrapperBounds = (node: Text, scrollX: number, scrollY: number): Bounds => {
const wrapper = node.ownerDocument.createElement('html2canvaswrapper');
wrapper.appendChild(node.cloneNode(true));
const parentNode = node.parentNode;
if (parentNode) {
parentNode.replaceChild(wrapper, node);
const bounds = parseBounds(wrapper, scrollX, scrollY);
if (wrapper.firstChild) {
parentNode.replaceChild(wrapper.firstChild, wrapper);
}
return bounds;
}
return new Bounds(0, 0, 0, 0);
};
const getRangeBounds = (
node: Text,
offset: number,
length: number,
scrollX: number,
scrollY: number
): Bounds => {
const range = node.ownerDocument.createRange();
range.setStart(node, offset);
range.setEnd(node, offset + length);
return Bounds.fromClientRect(range.getBoundingClientRect(), scrollX, scrollY);
};

View File

@ -1,48 +0,0 @@
/* @flow */
'use strict';
import type NodeContainer from './NodeContainer';
import type {TextTransform} from './parsing/textTransform';
import type {TextBounds} from './TextBounds';
import {TEXT_TRANSFORM} from './parsing/textTransform';
import {parseTextBounds} from './TextBounds';
export default class TextContainer {
text: string;
parent: NodeContainer;
bounds: Array<TextBounds>;
constructor(text: string, parent: NodeContainer, bounds: Array<TextBounds>) {
this.text = text;
this.parent = parent;
this.bounds = bounds;
}
static fromTextNode(node: Text, parent: NodeContainer) {
const text = transform(node.data, parent.style.textTransform);
return new TextContainer(text, parent, parseTextBounds(text, parent, node));
}
}
const CAPITALIZE = /(^|\s|:|-|\(|\))([a-z])/g;
const transform = (text: string, transform: TextTransform) => {
switch (transform) {
case TEXT_TRANSFORM.LOWERCASE:
return text.toLowerCase();
case TEXT_TRANSFORM.CAPITALIZE:
return text.replace(CAPITALIZE, capitalize);
case TEXT_TRANSFORM.UPPERCASE:
return text.toUpperCase();
default:
return text;
}
};
function capitalize(m, p1, p2) {
if (m.length > 0) {
return p1 + p2.toUpperCase();
}
return m;
}

View File

@ -1,27 +0,0 @@
/* @flow */
'use strict';
import type NodeContainer from './NodeContainer';
import {LineBreaker, fromCodePoint, toCodePoints} from 'css-line-break';
import {OVERFLOW_WRAP} from './parsing/overflowWrap';
export {toCodePoints, fromCodePoint} from 'css-line-break';
export const breakWords = (str: string, parent: NodeContainer): Array<string> => {
const breaker = LineBreaker(str, {
lineBreak: parent.style.lineBreak,
wordBreak:
parent.style.overflowWrap === OVERFLOW_WRAP.BREAK_WORD
? 'break-word'
: parent.style.wordBreak
});
const words = [];
let bk;
while (!(bk = breaker.next()).done) {
words.push(bk.value.slice());
}
return words;
};

View File

@ -1,21 +0,0 @@
/* @flow */
'use strict';
export const contains = (bit: number, value: number): boolean => (bit & value) !== 0;
export const distance = (a: number, b: number): number => Math.sqrt(a * a + b * b);
export const copyCSSStyles = (style: CSSStyleDeclaration, target: HTMLElement): HTMLElement => {
// Edge does not provide value for cssText
for (let i = style.length - 1; i >= 0; i--) {
const property = style.item(i);
// Safari shows pseudoelements if content is set
if (property !== 'content') {
target.style.setProperty(property, style.getPropertyValue(property));
}
}
return target;
};
export const SMALL_IMAGE =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';

View File

@ -1,178 +0,0 @@
/* @flow */
'use strict';
import type {Options} from './index';
import Logger from './Logger';
import {NodeParser} from './NodeParser';
import Renderer from './Renderer';
import ForeignObjectRenderer from './renderer/ForeignObjectRenderer';
import Feature from './Feature';
import {Bounds} from './Bounds';
import {cloneWindow, DocumentCloner} from './Clone';
import {FontMetrics} from './Font';
import Color, {TRANSPARENT} from './Color';
import {parseBounds, parseDocumentSize} from './Bounds';
export const renderElement = (
element: HTMLElement,
options: Options,
logger: Logger
): Promise<*> => {
const ownerDocument = element.ownerDocument;
const windowBounds = new Bounds(
options.scrollX,
options.scrollY,
options.windowWidth,
options.windowHeight
);
// http://www.w3.org/TR/css3-background/#special-backgrounds
const documentBackgroundColor = ownerDocument.documentElement
? new Color(getComputedStyle(ownerDocument.documentElement).backgroundColor)
: TRANSPARENT;
const bodyBackgroundColor = ownerDocument.body
? new Color(getComputedStyle(ownerDocument.body).backgroundColor)
: TRANSPARENT;
const backgroundColor =
element === ownerDocument.documentElement
? documentBackgroundColor.isTransparent()
? bodyBackgroundColor.isTransparent()
? options.backgroundColor ? new Color(options.backgroundColor) : null
: bodyBackgroundColor
: documentBackgroundColor
: options.backgroundColor ? new Color(options.backgroundColor) : null;
return (options.foreignObjectRendering
? // $FlowFixMe
Feature.SUPPORT_FOREIGNOBJECT_DRAWING
: Promise.resolve(false)).then(
supportForeignObject =>
supportForeignObject
? (cloner => {
if (__DEV__) {
logger.log(`Document cloned, using foreignObject rendering`);
}
return cloner
.inlineFonts(ownerDocument)
.then(() => cloner.resourceLoader.ready())
.then(() => {
const renderer = new ForeignObjectRenderer(cloner.documentElement);
const defaultView = ownerDocument.defaultView;
const scrollX = defaultView.pageXOffset;
const scrollY = defaultView.pageYOffset;
const isDocument =
element.tagName === 'HTML' || element.tagName === 'BODY';
const {width, height, left, top} = isDocument
? parseDocumentSize(ownerDocument)
: parseBounds(element, scrollX, scrollY);
return renderer.render({
backgroundColor,
logger,
scale: options.scale,
x: typeof options.x === 'number' ? options.x : left,
y: typeof options.y === 'number' ? options.y : top,
width:
typeof options.width === 'number'
? options.width
: Math.ceil(width),
height:
typeof options.height === 'number'
? options.height
: Math.ceil(height),
windowWidth: options.windowWidth,
windowHeight: options.windowHeight,
scrollX: options.scrollX,
scrollY: options.scrollY
});
});
})(new DocumentCloner(element, options, logger, true, renderElement))
: cloneWindow(
ownerDocument,
windowBounds,
element,
options,
logger,
renderElement
).then(([container, clonedElement, resourceLoader]) => {
if (__DEV__) {
logger.log(`Document cloned, using computed rendering`);
}
const stack = NodeParser(clonedElement, resourceLoader, logger);
const clonedDocument = clonedElement.ownerDocument;
if (backgroundColor === stack.container.style.background.backgroundColor) {
stack.container.style.background.backgroundColor = TRANSPARENT;
}
return resourceLoader.ready().then(imageStore => {
const fontMetrics = new FontMetrics(clonedDocument);
if (__DEV__) {
logger.log(`Starting renderer`);
}
const defaultView = clonedDocument.defaultView;
const scrollX = defaultView.pageXOffset;
const scrollY = defaultView.pageYOffset;
const isDocument =
clonedElement.tagName === 'HTML' || clonedElement.tagName === 'BODY';
const {width, height, left, top} = isDocument
? parseDocumentSize(ownerDocument)
: parseBounds(clonedElement, scrollX, scrollY);
const renderOptions = {
backgroundColor,
fontMetrics,
imageStore,
logger,
scale: options.scale,
x: typeof options.x === 'number' ? options.x : left,
y: typeof options.y === 'number' ? options.y : top,
width:
typeof options.width === 'number'
? options.width
: Math.ceil(width),
height:
typeof options.height === 'number'
? options.height
: Math.ceil(height)
};
if (Array.isArray(options.target)) {
return Promise.all(
options.target.map(target => {
const renderer = new Renderer(target, renderOptions);
return renderer.render(stack);
})
);
} else {
const renderer = new Renderer(options.target, renderOptions);
const canvas = renderer.render(stack);
if (options.removeContainer === true) {
if (container.parentNode) {
container.parentNode.removeChild(container);
} else if (__DEV__) {
logger.log(
`Cannot detach cloned iframe as it is not in the DOM anymore`
);
}
}
return canvas;
}
});
})
);
};

View File

@ -0,0 +1,225 @@
import {deepStrictEqual, fail} from 'assert';
import {FEATURES} from '../features';
import {createMockContext, proxy} from './mock-context';
const images: ImageMock[] = [];
const xhr: XMLHttpRequestMock[] = [];
const sleep = async (timeout: number) => await new Promise(resolve => setTimeout(resolve, timeout));
class ImageMock {
src?: string;
crossOrigin?: string;
onload?: () => {};
constructor() {
images.push(this);
}
}
class XMLHttpRequestMock {
sent: boolean;
status: number;
timeout: number;
method?: string;
url?: string;
response?: string;
onload?: () => {};
ontimeout?: () => {};
constructor() {
this.sent = false;
this.status = 500;
this.timeout = 5000;
xhr.push(this);
}
async load(status: number, response: string) {
this.response = response;
this.status = status;
if (this.onload) {
this.onload();
}
await sleep(0);
}
open(method: string, url: string) {
this.method = method;
this.url = url;
}
send() {
this.sent = true;
}
}
Object.defineProperty(global, 'Image', {value: ImageMock, writable: true});
Object.defineProperty(global, 'XMLHttpRequest', {
value: XMLHttpRequestMock,
writable: true
});
const setFeatures = (opts: {[key: string]: boolean} = {}) => {
const defaults: {[key: string]: boolean} = {
SUPPORT_SVG_DRAWING: true,
SUPPORT_CORS_IMAGES: true,
SUPPORT_CORS_XHR: true,
SUPPORT_RESPONSE_TYPE: false
};
Object.keys(defaults).forEach(key => {
Object.defineProperty(FEATURES, key, {
value: typeof opts[key] === 'boolean' ? opts[key] : defaults[key],
writable: true
});
});
};
describe('cache-storage', () => {
beforeEach(() => setFeatures());
afterEach(() => {
xhr.splice(0, xhr.length);
images.splice(0, images.length);
});
it('addImage adds images to cache', async () => {
const cache = createMockContext('http://example.com', {proxy: null});
await cache.addImage('http://example.com/test.jpg');
await cache.addImage('http://example.com/test2.jpg');
deepStrictEqual(images.length, 2);
deepStrictEqual(images[0].src, 'http://example.com/test.jpg');
deepStrictEqual(images[1].src, 'http://example.com/test2.jpg');
});
it('addImage should not add duplicate entries', async () => {
const cache = createMockContext('http://example.com');
await cache.addImage('http://example.com/test.jpg');
await cache.addImage('http://example.com/test.jpg');
deepStrictEqual(images.length, 1);
deepStrictEqual(images[0].src, 'http://example.com/test.jpg');
});
describe('svg', () => {
it('should add svg images correctly', async () => {
const cache = createMockContext('http://example.com');
await cache.addImage('http://example.com/test.svg');
await cache.addImage('http://example.com/test2.svg');
deepStrictEqual(images.length, 2);
deepStrictEqual(images[0].src, 'http://example.com/test.svg');
deepStrictEqual(images[1].src, 'http://example.com/test2.svg');
});
it('should omit svg images if not supported', async () => {
setFeatures({SUPPORT_SVG_DRAWING: false});
const cache = createMockContext('http://example.com');
await cache.addImage('http://example.com/test.svg');
await cache.addImage('http://example.com/test2.svg');
deepStrictEqual(images.length, 0);
});
});
describe('cross-origin', () => {
it('addImage should not add images it cannot load/render', async () => {
const cache = createMockContext('http://example.com', {
proxy: undefined
});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 0);
});
it('addImage should add images if tainting enabled', async () => {
const cache = createMockContext('http://example.com', {
allowTaint: true,
proxy: undefined
});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 1);
deepStrictEqual(images[0].src, 'http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images[0].crossOrigin, undefined);
});
it('addImage should add images if cors enabled', async () => {
const cache = createMockContext('http://example.com', {useCORS: true});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 1);
deepStrictEqual(images[0].src, 'http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images[0].crossOrigin, 'anonymous');
});
it('addImage should not add images if cors enabled but not supported', async () => {
setFeatures({SUPPORT_CORS_IMAGES: false});
const cache = createMockContext('http://example.com', {
useCORS: true,
proxy: undefined
});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 0);
});
it('addImage should not add images to proxy if cors enabled', async () => {
const cache = createMockContext('http://example.com', {useCORS: true});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 1);
deepStrictEqual(images[0].src, 'http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images[0].crossOrigin, 'anonymous');
});
it('addImage should use proxy ', async () => {
const cache = createMockContext('http://example.com');
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(xhr.length, 1);
deepStrictEqual(
xhr[0].url,
`${proxy}?url=${encodeURIComponent('http://html2canvas.hertzen.com/test.jpg')}&responseType=text`
);
await xhr[0].load(200, '<data response>');
deepStrictEqual(images.length, 1);
deepStrictEqual(images[0].src, '<data response>');
});
it('proxy should respect imageTimeout', async () => {
const cache = createMockContext('http://example.com', {
imageTimeout: 10
});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(xhr.length, 1);
deepStrictEqual(
xhr[0].url,
`${proxy}?url=${encodeURIComponent('http://html2canvas.hertzen.com/test.jpg')}&responseType=text`
);
deepStrictEqual(xhr[0].timeout, 10);
if (xhr[0].ontimeout) {
xhr[0].ontimeout();
}
try {
await cache.match('http://html2canvas.hertzen.com/test.jpg');
fail('Expected result to timeout');
} catch (e) {}
});
});
it('match should return cache entry', async () => {
const cache = createMockContext('http://example.com');
await cache.addImage('http://example.com/test.jpg');
if (images[0].onload) {
images[0].onload();
}
const response = await cache.match('http://example.com/test.jpg');
deepStrictEqual(response.src, 'http://example.com/test.jpg');
});
it('image should respect imageTimeout', async () => {
const cache = createMockContext('http://example.com', {imageTimeout: 10});
cache.addImage('http://example.com/test.jpg');
try {
await cache.match('http://example.com/test.jpg');
fail('Expected result to timeout');
} catch (e) {}
});
});

View File

@ -0,0 +1,45 @@
import {CacheStorage} from '../cache-storage';
import {URL} from 'url';
import {Logger} from '../logger';
export const proxy = 'http://example.com/proxy';
export const createMockContext = (origin: string, opts = {}) => {
const context = {
location: {
href: origin
},
document: {
createElement(_name: string) {
let _href = '';
return {
set href(value: string) {
_href = value;
},
get href() {
return _href;
},
get protocol() {
return new URL(_href).protocol;
},
get hostname() {
return new URL(_href).hostname;
},
get port() {
return new URL(_href).port;
}
};
}
}
};
CacheStorage.setContext(context as Window);
Logger.create('test');
return CacheStorage.create('test', {
imageTimeout: 0,
useCORS: false,
allowTaint: false,
proxy,
...opts
});
};

1
src/core/bitwise.ts Normal file
View File

@ -0,0 +1 @@
export const contains = (bit: number, value: number): boolean => (bit & value) !== 0;

207
src/core/cache-storage.ts Normal file
View File

@ -0,0 +1,207 @@
import {FEATURES} from './features';
import {Logger} from './logger';
export class CacheStorage {
private static _caches: {[key: string]: Cache} = {};
private static _link?: HTMLAnchorElement;
private static _origin: string = 'about:blank';
private static _current: Cache | null = null;
static create(name: string, options: ResourceOptions): Cache {
return (CacheStorage._caches[name] = new Cache(name, options));
}
static destroy(name: string): void {
delete CacheStorage._caches[name];
}
static open(name: string): Cache {
const cache = CacheStorage._caches[name];
if (typeof cache !== 'undefined') {
return cache;
}
throw new Error(`Cache with key "${name}" not found`);
}
static getOrigin(url: string): string {
const link = CacheStorage._link;
if (!link) {
return 'about:blank';
}
link.href = url;
link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/
return link.protocol + link.hostname + link.port;
}
static isSameOrigin(src: string): boolean {
return CacheStorage.getOrigin(src) === CacheStorage._origin;
}
static setContext(window: Window) {
CacheStorage._link = window.document.createElement('a');
CacheStorage._origin = CacheStorage.getOrigin(window.location.href);
}
static getInstance(): Cache {
const current = CacheStorage._current;
if (current === null) {
throw new Error(`No cache instance attached`);
}
return current;
}
static attachInstance(cache: Cache) {
CacheStorage._current = cache;
}
static detachInstance() {
CacheStorage._current = null;
}
}
interface ResourceOptions {
imageTimeout: number;
useCORS: boolean;
allowTaint: boolean;
proxy?: string;
}
export class Cache {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private readonly _cache: {[key: string]: Promise<any>};
private readonly _options: ResourceOptions;
private readonly id: string;
constructor(id: string, options: ResourceOptions) {
this.id = id;
this._options = options;
this._cache = {};
}
addImage(src: string): Promise<void> {
const result = Promise.resolve();
if (this.has(src)) {
return result;
}
if (isBlobImage(src) || isRenderable(src)) {
this._cache[src] = this.loadImage(src);
return result;
}
return result;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
match(src: string): Promise<any> {
return this._cache[src];
}
private async loadImage(key: string) {
const isSameOrigin = CacheStorage.isSameOrigin(key);
const useCORS =
!isInlineImage(key) && this._options.useCORS === true && FEATURES.SUPPORT_CORS_IMAGES && !isSameOrigin;
const useProxy =
!isInlineImage(key) &&
!isSameOrigin &&
typeof this._options.proxy === 'string' &&
FEATURES.SUPPORT_CORS_XHR &&
!useCORS;
if (!isSameOrigin && this._options.allowTaint === false && !isInlineImage(key) && !useProxy && !useCORS) {
return;
}
let src = key;
if (useProxy) {
src = await this.proxy(src);
}
Logger.getInstance(this.id).debug(`Added image ${key.substring(0, 256)}`);
return await new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
//ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
if (isInlineBase64Image(src) || useCORS) {
img.crossOrigin = 'anonymous';
}
img.src = src;
if (img.complete === true) {
// Inline XML images may fail to parse, throwing an Error later on
setTimeout(() => resolve(img), 500);
}
if (this._options.imageTimeout > 0) {
setTimeout(
() => reject(`Timed out (${this._options.imageTimeout}ms) loading image`),
this._options.imageTimeout
);
}
});
}
private has(key: string): boolean {
return typeof this._cache[key] !== 'undefined';
}
keys(): Promise<string[]> {
return Promise.resolve(Object.keys(this._cache));
}
private proxy(src: string): Promise<string> {
const proxy = this._options.proxy;
if (!proxy) {
throw new Error('No proxy defined');
}
const key = src.substring(0, 256);
return new Promise((resolve, reject) => {
const responseType = FEATURES.SUPPORT_RESPONSE_TYPE ? 'blob' : 'text';
const xhr = new XMLHttpRequest();
xhr.onload = () => {
if (xhr.status === 200) {
if (responseType === 'text') {
resolve(xhr.response);
} else {
const reader = new FileReader();
reader.addEventListener('load', () => resolve(reader.result as string), false);
reader.addEventListener('error', e => reject(e), false);
reader.readAsDataURL(xhr.response);
}
} else {
reject(`Failed to proxy resource ${key} with status code ${xhr.status}`);
}
};
xhr.onerror = reject;
xhr.open('GET', `${proxy}?url=${encodeURIComponent(src)}&responseType=${responseType}`);
if (responseType !== 'text' && xhr instanceof XMLHttpRequest) {
xhr.responseType = responseType;
}
if (this._options.imageTimeout) {
const timeout = this._options.imageTimeout;
xhr.timeout = timeout;
xhr.ontimeout = () => reject(`Timed out (${timeout}ms) proxying ${key}`);
}
xhr.send();
});
}
}
const INLINE_SVG = /^data:image\/svg\+xml/i;
const INLINE_BASE64 = /^data:image\/.*;base64,/i;
const INLINE_IMG = /^data:image\/.*/i;
const isRenderable = (src: string): boolean => FEATURES.SUPPORT_SVG_DRAWING || !isSVG(src);
const isInlineImage = (src: string): boolean => INLINE_IMG.test(src);
const isInlineBase64Image = (src: string): boolean => INLINE_BASE64.test(src);
const isBlobImage = (src: string): boolean => src.substr(0, 4) === 'blob';
const isSVG = (src: string): boolean => src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src);

View File

@ -1,9 +1,4 @@
/* @flow */
'use strict';
import {createForeignObjectSVG, loadSerializedSVG} from './renderer/ForeignObjectRenderer';
const testRangeBounds = document => {
const testRangeBounds = (document: Document) => {
const TEST_HEIGHT = 123;
if (document.createRange) {
@ -27,14 +22,18 @@ const testRangeBounds = document => {
return false;
};
const testCORS = () => typeof new Image().crossOrigin !== 'undefined';
const testCORS = (): boolean => typeof new Image().crossOrigin !== 'undefined';
const testResponseType = () => typeof new XMLHttpRequest().responseType === 'string';
const testResponseType = (): boolean => typeof new XMLHttpRequest().responseType === 'string';
const testSVG = document => {
const testSVG = (document: Document): boolean => {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
return false;
}
img.src = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'></svg>`;
try {
@ -46,14 +45,18 @@ const testSVG = document => {
return true;
};
const isGreenPixel = data => data[0] === 0 && data[1] === 255 && data[2] === 0 && data[3] === 255;
const isGreenPixel = (data: Uint8ClampedArray): boolean =>
data[0] === 0 && data[1] === 255 && data[2] === 0 && data[3] === 255;
const testForeignObject = document => {
const testForeignObject = (document: Document): Promise<boolean> => {
const canvas = document.createElement('canvas');
const size = 100;
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
if (!ctx) {
return Promise.reject(false);
}
ctx.fillStyle = 'rgb(0, 255, 0)';
ctx.fillRect(0, 0, size, size);
@ -65,7 +68,7 @@ const testForeignObject = document => {
ctx.fillRect(0, 0, size, size);
return loadSerializedSVG(svg)
.then(img => {
.then((img: HTMLImageElement) => {
ctx.drawImage(img, 0, 0);
const data = ctx.getImageData(0, 0, size, size).data;
ctx.fillStyle = 'red';
@ -79,30 +82,56 @@ const testForeignObject = document => {
? loadSerializedSVG(createForeignObjectSVG(size, size, 0, 0, node))
: Promise.reject(false);
})
.then(img => {
.then((img: HTMLImageElement) => {
ctx.drawImage(img, 0, 0);
// Edge does not render background-images
return isGreenPixel(ctx.getImageData(0, 0, size, size).data);
})
.catch(e => false);
.catch(() => false);
};
const FEATURES = {
// $FlowFixMe - get/set properties not yet supported
export const createForeignObjectSVG = (width: number, height: number, x: number, y: number, node: Node) => {
const xmlns = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(xmlns, 'svg');
const foreignObject = document.createElementNS(xmlns, 'foreignObject');
svg.setAttributeNS(null, 'width', width.toString());
svg.setAttributeNS(null, 'height', height.toString());
foreignObject.setAttributeNS(null, 'width', '100%');
foreignObject.setAttributeNS(null, 'height', '100%');
foreignObject.setAttributeNS(null, 'x', x.toString());
foreignObject.setAttributeNS(null, 'y', y.toString());
foreignObject.setAttributeNS(null, 'externalResourcesRequired', 'true');
svg.appendChild(foreignObject);
foreignObject.appendChild(node);
return svg;
};
export const loadSerializedSVG = (svg: Node): Promise<HTMLImageElement> => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
});
};
export const FEATURES = {
get SUPPORT_RANGE_BOUNDS() {
'use strict';
const value = testRangeBounds(document);
Object.defineProperty(FEATURES, 'SUPPORT_RANGE_BOUNDS', {value});
return value;
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_SVG_DRAWING() {
'use strict';
const value = testSVG(document);
Object.defineProperty(FEATURES, 'SUPPORT_SVG_DRAWING', {value});
return value;
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_FOREIGNOBJECT_DRAWING() {
'use strict';
const value =
@ -112,21 +141,18 @@ const FEATURES = {
Object.defineProperty(FEATURES, 'SUPPORT_FOREIGNOBJECT_DRAWING', {value});
return value;
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_CORS_IMAGES() {
'use strict';
const value = testCORS();
Object.defineProperty(FEATURES, 'SUPPORT_CORS_IMAGES', {value});
return value;
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_RESPONSE_TYPE() {
'use strict';
const value = testResponseType();
Object.defineProperty(FEATURES, 'SUPPORT_RESPONSE_TYPE', {value});
return value;
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_CORS_XHR() {
'use strict';
const value = 'withCredentials' in new XMLHttpRequest();
@ -134,5 +160,3 @@ const FEATURES = {
return value;
}
};
export default FEATURES;

62
src/core/logger.ts Normal file
View File

@ -0,0 +1,62 @@
export class Logger {
static instances: {[key: string]: Logger} = {};
private readonly id: string;
private readonly start: number;
constructor(id: string) {
this.id = id;
this.start = Date.now();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
debug(...args: any) {
// eslint-disable-next-line no-console
if (typeof window !== 'undefined' && window.console && typeof console.debug === 'function') {
// eslint-disable-next-line no-console
console.debug(this.id, `${this.getTime()}ms`, ...args);
} else {
this.info(...args);
}
}
getTime(): number {
return Date.now() - this.start;
}
static create(id: string) {
Logger.instances[id] = new Logger(id);
}
static destroy(id: string) {
delete Logger.instances[id];
}
static getInstance(id: string): Logger {
const instance = Logger.instances[id];
if (typeof instance === 'undefined') {
throw new Error(`No logger instance found with id ${id}`);
}
return instance;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
info(...args: any) {
// eslint-disable-next-line no-console
if (typeof window !== 'undefined' && window.console && typeof console.info === 'function') {
// eslint-disable-next-line no-console
console.info(this.id, `${this.getTime()}ms`, ...args);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error(...args: any) {
// eslint-disable-next-line no-console
if (typeof window !== 'undefined' && window.console && typeof console.error === 'function') {
// eslint-disable-next-line no-console
console.error(this.id, `${this.getTime()}ms`, ...args);
} else {
this.info(...args);
}
}
}

1
src/core/util.ts Normal file
View File

@ -0,0 +1 @@
export const SMALL_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';

View File

@ -0,0 +1,48 @@
import {CSSValue} from './syntax/parser';
import {CSSTypes} from './types/index';
export enum PropertyDescriptorParsingType {
VALUE,
LIST,
IDENT_VALUE,
TYPE_VALUE,
TOKEN_VALUE
}
export interface IPropertyDescriptor {
name: string;
type: PropertyDescriptorParsingType;
initialValue: string;
prefix: boolean;
}
export interface IPropertyIdentValueDescriptor<T> extends IPropertyDescriptor {
type: PropertyDescriptorParsingType.IDENT_VALUE;
parse: (token: string) => T;
}
export interface IPropertyTypeValueDescriptor extends IPropertyDescriptor {
type: PropertyDescriptorParsingType.TYPE_VALUE;
format: CSSTypes;
}
export interface IPropertyValueDescriptor<T> extends IPropertyDescriptor {
type: PropertyDescriptorParsingType.VALUE;
parse: (token: CSSValue) => T;
}
export interface IPropertyListDescriptor<T> extends IPropertyDescriptor {
type: PropertyDescriptorParsingType.LIST;
parse: (tokens: CSSValue[]) => T;
}
export interface IPropertyTokenValueDescriptor extends IPropertyDescriptor {
type: PropertyDescriptorParsingType.TOKEN_VALUE;
}
export type CSSPropertyDescriptor<T> =
| IPropertyValueDescriptor<T>
| IPropertyListDescriptor<T>
| IPropertyIdentValueDescriptor<T>
| IPropertyTypeValueDescriptor
| IPropertyTokenValueDescriptor;

View File

@ -0,0 +1,6 @@
import {CSSValue} from './syntax/parser';
export interface ITypeDescriptor<T> {
name: string;
parse: (value: CSSValue) => T;
}

289
src/css/index.ts Normal file
View File

@ -0,0 +1,289 @@
import {CSSPropertyDescriptor, PropertyDescriptorParsingType} from './IPropertyDescriptor';
import {backgroundClip} from './property-descriptors/background-clip';
import {backgroundColor} from './property-descriptors/background-color';
import {backgroundImage} from './property-descriptors/background-image';
import {backgroundOrigin} from './property-descriptors/background-origin';
import {backgroundPosition} from './property-descriptors/background-position';
import {backgroundRepeat} from './property-descriptors/background-repeat';
import {backgroundSize} from './property-descriptors/background-size';
import {
borderBottomColor,
borderLeftColor,
borderRightColor,
borderTopColor
} from './property-descriptors/border-color';
import {
borderBottomLeftRadius,
borderBottomRightRadius,
borderTopLeftRadius,
borderTopRightRadius
} from './property-descriptors/border-radius';
import {
borderBottomStyle,
borderLeftStyle,
borderRightStyle,
borderTopStyle
} from './property-descriptors/border-style';
import {
borderBottomWidth,
borderLeftWidth,
borderRightWidth,
borderTopWidth
} from './property-descriptors/border-width';
import {color} from './property-descriptors/color';
import {display, DISPLAY} from './property-descriptors/display';
import {float, FLOAT} from './property-descriptors/float';
import {letterSpacing} from './property-descriptors/letter-spacing';
import {lineBreak} from './property-descriptors/line-break';
import {lineHeight} from './property-descriptors/line-height';
import {listStyleImage} from './property-descriptors/list-style-image';
import {listStylePosition} from './property-descriptors/list-style-position';
import {listStyleType} from './property-descriptors/list-style-type';
import {marginBottom, marginLeft, marginRight, marginTop} from './property-descriptors/margin';
import {overflow} from './property-descriptors/overflow';
import {overflowWrap} from './property-descriptors/overflow-wrap';
import {paddingBottom, paddingLeft, paddingRight, paddingTop} from './property-descriptors/padding';
import {textAlign} from './property-descriptors/text-align';
import {position, POSITION} from './property-descriptors/position';
import {textShadow} from './property-descriptors/text-shadow';
import {textTransform} from './property-descriptors/text-transform';
import {transform} from './property-descriptors/transform';
import {transformOrigin} from './property-descriptors/transform-origin';
import {visibility, VISIBILITY} from './property-descriptors/visibility';
import {wordBreak} from './property-descriptors/word-break';
import {zIndex} from './property-descriptors/z-index';
import {CSSValue, isIdentToken, Parser} from './syntax/parser';
import {Tokenizer} from './syntax/tokenizer';
import {Color, color as colorType, isTransparent} from './types/color';
import {angle} from './types/angle';
import {image} from './types/image';
import {opacity} from './property-descriptors/opacity';
import {textDecorationColor} from './property-descriptors/text-decoration-color';
import {textDecorationLine} from './property-descriptors/text-decoration-line';
import {isLengthPercentage, LengthPercentage, ZERO_LENGTH} from './types/length-percentage';
import {fontFamily} from './property-descriptors/font-family';
import {fontSize} from './property-descriptors/font-size';
import {isLength} from './types/length';
import {fontWeight} from './property-descriptors/font-weight';
import {fontVariant} from './property-descriptors/font-variant';
import {fontStyle} from './property-descriptors/font-style';
import {contains} from '../core/bitwise';
import {content} from './property-descriptors/content';
import {counterIncrement} from './property-descriptors/counter-increment';
import {counterReset} from './property-descriptors/counter-reset';
import {quotes} from './property-descriptors/quotes';
export class CSSParsedDeclaration {
backgroundClip: ReturnType<typeof backgroundClip.parse>;
backgroundColor: Color;
backgroundImage: ReturnType<typeof backgroundImage.parse>;
backgroundOrigin: ReturnType<typeof backgroundOrigin.parse>;
backgroundPosition: ReturnType<typeof backgroundPosition.parse>;
backgroundRepeat: ReturnType<typeof backgroundRepeat.parse>;
backgroundSize: ReturnType<typeof backgroundSize.parse>;
borderTopColor: Color;
borderRightColor: Color;
borderBottomColor: Color;
borderLeftColor: Color;
borderTopLeftRadius: ReturnType<typeof borderTopLeftRadius.parse>;
borderTopRightRadius: ReturnType<typeof borderTopRightRadius.parse>;
borderBottomRightRadius: ReturnType<typeof borderBottomRightRadius.parse>;
borderBottomLeftRadius: ReturnType<typeof borderBottomLeftRadius.parse>;
borderTopStyle: ReturnType<typeof borderTopStyle.parse>;
borderRightStyle: ReturnType<typeof borderRightStyle.parse>;
borderBottomStyle: ReturnType<typeof borderBottomStyle.parse>;
borderLeftStyle: ReturnType<typeof borderLeftStyle.parse>;
borderTopWidth: ReturnType<typeof borderTopWidth.parse>;
borderRightWidth: ReturnType<typeof borderRightWidth.parse>;
borderBottomWidth: ReturnType<typeof borderBottomWidth.parse>;
borderLeftWidth: ReturnType<typeof borderLeftWidth.parse>;
color: Color;
display: ReturnType<typeof display.parse>;
float: ReturnType<typeof float.parse>;
fontFamily: ReturnType<typeof fontFamily.parse>;
fontSize: LengthPercentage;
fontStyle: ReturnType<typeof fontStyle.parse>;
fontVariant: ReturnType<typeof fontVariant.parse>;
fontWeight: ReturnType<typeof fontWeight.parse>;
letterSpacing: ReturnType<typeof letterSpacing.parse>;
lineBreak: ReturnType<typeof lineBreak.parse>;
lineHeight: CSSValue;
listStyleImage: ReturnType<typeof listStyleImage.parse>;
listStylePosition: ReturnType<typeof listStylePosition.parse>;
listStyleType: ReturnType<typeof listStyleType.parse>;
marginTop: CSSValue;
marginRight: CSSValue;
marginBottom: CSSValue;
marginLeft: CSSValue;
opacity: ReturnType<typeof opacity.parse>;
overflow: ReturnType<typeof overflow.parse>;
overflowWrap: ReturnType<typeof overflowWrap.parse>;
paddingTop: LengthPercentage;
paddingRight: LengthPercentage;
paddingBottom: LengthPercentage;
paddingLeft: LengthPercentage;
position: ReturnType<typeof position.parse>;
textAlign: ReturnType<typeof textAlign.parse>;
textDecorationColor: Color;
textDecorationLine: ReturnType<typeof textDecorationLine.parse>;
textShadow: ReturnType<typeof textShadow.parse>;
textTransform: ReturnType<typeof textTransform.parse>;
transform: ReturnType<typeof transform.parse>;
transformOrigin: ReturnType<typeof transformOrigin.parse>;
visibility: ReturnType<typeof visibility.parse>;
wordBreak: ReturnType<typeof wordBreak.parse>;
zIndex: ReturnType<typeof zIndex.parse>;
constructor(declaration: CSSStyleDeclaration) {
this.backgroundClip = parse(backgroundClip, declaration.backgroundClip);
this.backgroundColor = parse(backgroundColor, declaration.backgroundColor);
this.backgroundImage = parse(backgroundImage, declaration.backgroundImage);
this.backgroundOrigin = parse(backgroundOrigin, declaration.backgroundOrigin);
this.backgroundPosition = parse(backgroundPosition, declaration.backgroundPosition);
this.backgroundRepeat = parse(backgroundRepeat, declaration.backgroundRepeat);
this.backgroundSize = parse(backgroundSize, declaration.backgroundSize);
this.borderTopColor = parse(borderTopColor, declaration.borderTopColor);
this.borderRightColor = parse(borderRightColor, declaration.borderRightColor);
this.borderBottomColor = parse(borderBottomColor, declaration.borderBottomColor);
this.borderLeftColor = parse(borderLeftColor, declaration.borderLeftColor);
this.borderTopLeftRadius = parse(borderTopLeftRadius, declaration.borderTopLeftRadius);
this.borderTopRightRadius = parse(borderTopRightRadius, declaration.borderTopRightRadius);
this.borderBottomRightRadius = parse(borderBottomRightRadius, declaration.borderBottomRightRadius);
this.borderBottomLeftRadius = parse(borderBottomLeftRadius, declaration.borderBottomLeftRadius);
this.borderTopStyle = parse(borderTopStyle, declaration.borderTopStyle);
this.borderRightStyle = parse(borderRightStyle, declaration.borderRightStyle);
this.borderBottomStyle = parse(borderBottomStyle, declaration.borderBottomStyle);
this.borderLeftStyle = parse(borderLeftStyle, declaration.borderLeftStyle);
this.borderTopWidth = parse(borderTopWidth, declaration.borderTopWidth);
this.borderRightWidth = parse(borderRightWidth, declaration.borderRightWidth);
this.borderBottomWidth = parse(borderBottomWidth, declaration.borderBottomWidth);
this.borderLeftWidth = parse(borderLeftWidth, declaration.borderLeftWidth);
this.color = parse(color, declaration.color);
this.display = parse(display, declaration.display);
this.float = parse(float, declaration.cssFloat);
this.fontFamily = parse(fontFamily, declaration.fontFamily);
this.fontSize = parse(fontSize, declaration.fontSize);
this.fontStyle = parse(fontStyle, declaration.fontStyle);
this.fontVariant = parse(fontVariant, declaration.fontVariant);
this.fontWeight = parse(fontWeight, declaration.fontWeight);
this.letterSpacing = parse(letterSpacing, declaration.letterSpacing);
this.lineBreak = parse(lineBreak, declaration.lineBreak);
this.lineHeight = parse(lineHeight, declaration.lineHeight);
this.listStyleImage = parse(listStyleImage, declaration.listStyleImage);
this.listStylePosition = parse(listStylePosition, declaration.listStylePosition);
this.listStyleType = parse(listStyleType, declaration.listStyleType);
this.marginTop = parse(marginTop, declaration.marginTop);
this.marginRight = parse(marginRight, declaration.marginRight);
this.marginBottom = parse(marginBottom, declaration.marginBottom);
this.marginLeft = parse(marginLeft, declaration.marginLeft);
this.opacity = parse(opacity, declaration.opacity);
this.overflow = parse(overflow, declaration.overflow);
this.overflowWrap = parse(overflowWrap, declaration.overflowWrap);
this.paddingTop = parse(paddingTop, declaration.paddingTop);
this.paddingRight = parse(paddingRight, declaration.paddingRight);
this.paddingBottom = parse(paddingBottom, declaration.paddingBottom);
this.paddingLeft = parse(paddingLeft, declaration.paddingLeft);
this.position = parse(position, declaration.position);
this.textAlign = parse(textAlign, declaration.textAlign);
this.textDecorationColor = parse(textDecorationColor, declaration.textDecorationColor || declaration.color);
this.textDecorationLine = parse(textDecorationLine, declaration.textDecorationLine);
this.textShadow = parse(textShadow, declaration.textShadow);
this.textTransform = parse(textTransform, declaration.textTransform);
this.transform = parse(transform, declaration.transform);
this.transformOrigin = parse(transformOrigin, declaration.transformOrigin);
this.visibility = parse(visibility, declaration.visibility);
this.wordBreak = parse(wordBreak, declaration.wordBreak);
this.zIndex = parse(zIndex, declaration.zIndex);
}
isVisible(): boolean {
return this.display > 0 && this.opacity > 0 && this.visibility === VISIBILITY.VISIBLE;
}
isTransparent(): boolean {
return isTransparent(this.backgroundColor);
}
isTransformed(): boolean {
return this.transform !== null;
}
isPositioned(): boolean {
return this.position !== POSITION.STATIC;
}
isPositionedWithZIndex(): boolean {
return this.isPositioned() && !this.zIndex.auto;
}
isFloating(): boolean {
return this.float !== FLOAT.NONE;
}
isInlineLevel(): boolean {
return (
contains(this.display, DISPLAY.INLINE) ||
contains(this.display, DISPLAY.INLINE_BLOCK) ||
contains(this.display, DISPLAY.INLINE_FLEX) ||
contains(this.display, DISPLAY.INLINE_GRID) ||
contains(this.display, DISPLAY.INLINE_LIST_ITEM) ||
contains(this.display, DISPLAY.INLINE_TABLE)
);
}
}
export class CSSParsedPseudoDeclaration {
content: ReturnType<typeof content.parse>;
quotes: ReturnType<typeof quotes.parse>;
constructor(declaration: CSSStyleDeclaration) {
this.content = parse(content, declaration.content);
this.quotes = parse(quotes, declaration.quotes);
}
}
export class CSSParsedCounterDeclaration {
counterIncrement: ReturnType<typeof counterIncrement.parse>;
counterReset: ReturnType<typeof counterReset.parse>;
constructor(declaration: CSSStyleDeclaration) {
this.counterIncrement = parse(counterIncrement, declaration.counterIncrement);
this.counterReset = parse(counterReset, declaration.counterReset);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parse = (descriptor: CSSPropertyDescriptor<any>, style?: string | null) => {
const tokenizer = new Tokenizer();
const value = style !== null && typeof style !== 'undefined' ? style.toString() : descriptor.initialValue;
tokenizer.write(value);
const parser = new Parser(tokenizer.read());
switch (descriptor.type) {
case PropertyDescriptorParsingType.IDENT_VALUE:
const token = parser.parseComponentValue();
return descriptor.parse(isIdentToken(token) ? token.value : descriptor.initialValue);
case PropertyDescriptorParsingType.VALUE:
return descriptor.parse(parser.parseComponentValue());
case PropertyDescriptorParsingType.LIST:
return descriptor.parse(parser.parseComponentValues());
case PropertyDescriptorParsingType.TOKEN_VALUE:
return parser.parseComponentValue();
case PropertyDescriptorParsingType.TYPE_VALUE:
switch (descriptor.format) {
case 'angle':
return angle.parse(parser.parseComponentValue());
case 'color':
return colorType.parse(parser.parseComponentValue());
case 'image':
return image.parse(parser.parseComponentValue());
case 'length':
const length = parser.parseComponentValue();
return isLength(length) ? length : ZERO_LENGTH;
case 'length-percentage':
const value = parser.parseComponentValue();
return isLengthPercentage(value) ? value : ZERO_LENGTH;
}
}
throw new Error(`Attempting to parse unsupported css format type ${descriptor.format}`);
};

47
src/css/layout/bounds.ts Normal file
View File

@ -0,0 +1,47 @@
export class Bounds {
readonly top: number;
readonly left: number;
readonly width: number;
readonly height: number;
constructor(x: number, y: number, w: number, h: number) {
this.left = x;
this.top = y;
this.width = w;
this.height = h;
}
add(x: number, y: number, w: number, h: number): Bounds {
return new Bounds(this.left + x, this.top + y, this.width + w, this.height + h);
}
static fromClientRect(clientRect: ClientRect): Bounds {
return new Bounds(clientRect.left, clientRect.top, clientRect.width, clientRect.height);
}
}
export const parseBounds = (node: Element): Bounds => {
return Bounds.fromClientRect(node.getBoundingClientRect());
};
export const parseDocumentSize = (document: Document): Bounds => {
const body = document.body;
const documentElement = document.documentElement;
if (!body || !documentElement) {
throw new Error(`Unable to get document size`);
}
const width = Math.max(
Math.max(body.scrollWidth, documentElement.scrollWidth),
Math.max(body.offsetWidth, documentElement.offsetWidth),
Math.max(body.clientWidth, documentElement.clientWidth)
);
const height = Math.max(
Math.max(body.scrollHeight, documentElement.scrollHeight),
Math.max(body.offsetHeight, documentElement.offsetHeight),
Math.max(body.clientHeight, documentElement.clientHeight)
);
return new Bounds(0, 0, width, height);
};

89
src/css/layout/text.ts Normal file
View File

@ -0,0 +1,89 @@
import {OVERFLOW_WRAP} from '../property-descriptors/overflow-wrap';
import {CSSParsedDeclaration} from '../index';
import {fromCodePoint, LineBreaker, toCodePoints} from 'css-line-break';
import {Bounds, parseBounds} from './bounds';
import {FEATURES} from '../../core/features';
export class TextBounds {
readonly text: string;
readonly bounds: Bounds;
constructor(text: string, bounds: Bounds) {
this.text = text;
this.bounds = bounds;
}
}
export const parseTextBounds = (value: string, styles: CSSParsedDeclaration, node: Text): TextBounds[] => {
const textList = breakText(value, styles);
const textBounds: TextBounds[] = [];
let offset = 0;
textList.forEach(text => {
if (styles.textDecorationLine.length || text.trim().length > 0) {
if (FEATURES.SUPPORT_RANGE_BOUNDS) {
textBounds.push(new TextBounds(text, getRangeBounds(node, offset, text.length)));
} else {
const replacementNode = node.splitText(text.length);
textBounds.push(new TextBounds(text, getWrapperBounds(node)));
node = replacementNode;
}
} else if (!FEATURES.SUPPORT_RANGE_BOUNDS) {
node = node.splitText(text.length);
}
offset += text.length;
});
return textBounds;
};
const getWrapperBounds = (node: Text): Bounds => {
const ownerDocument = node.ownerDocument;
if (ownerDocument) {
const wrapper = ownerDocument.createElement('html2canvaswrapper');
wrapper.appendChild(node.cloneNode(true));
const parentNode = node.parentNode;
if (parentNode) {
parentNode.replaceChild(wrapper, node);
const bounds = parseBounds(wrapper);
if (wrapper.firstChild) {
parentNode.replaceChild(wrapper.firstChild, wrapper);
}
return bounds;
}
}
return new Bounds(0, 0, 0, 0);
};
const getRangeBounds = (node: Text, offset: number, length: number): Bounds => {
const ownerDocument = node.ownerDocument;
if (!ownerDocument) {
throw new Error('Node has no owner document');
}
const range = ownerDocument.createRange();
range.setStart(node, offset);
range.setEnd(node, offset + length);
return Bounds.fromClientRect(range.getBoundingClientRect());
};
const breakText = (value: string, styles: CSSParsedDeclaration): string[] => {
return styles.letterSpacing !== 0 ? toCodePoints(value).map(i => fromCodePoint(i)) : breakWords(value, styles);
};
const breakWords = (str: string, styles: CSSParsedDeclaration): string[] => {
const breaker = LineBreaker(str, {
lineBreak: styles.lineBreak,
wordBreak: styles.overflowWrap === OVERFLOW_WRAP.BREAK_WORD ? 'break-word' : styles.wordBreak
});
const words = [];
let bk;
while (!(bk = breaker.next()).done) {
if (bk.value) {
words.push(bk.value.slice());
}
}
return words;
};

View File

@ -0,0 +1,43 @@
import {deepStrictEqual} from 'assert';
import {Parser} from '../../syntax/parser';
import {backgroundImage} from '../background-image';
import {CSSImageType} from '../../types/image';
import {pack} from '../../types/color';
import {deg} from '../../types/angle';
import {createMockContext} from '../../../core/__tests__/mock-context';
import {CacheStorage} from '../../../core/cache-storage';
const backgroundImageParse = (value: string) => backgroundImage.parse(Parser.parseValues(value));
describe('property-descriptors', () => {
before(() => {
CacheStorage.attachInstance(createMockContext('http://example.com'));
});
describe('background-image', () => {
it('none', () => deepStrictEqual(backgroundImageParse('none'), []));
it('url(test.jpg), url(test2.jpg)', () =>
deepStrictEqual(
backgroundImageParse('url(http://example.com/test.jpg), url(http://example.com/test2.jpg)'),
[
{url: 'http://example.com/test.jpg', type: CSSImageType.URL},
{url: 'http://example.com/test2.jpg', type: CSSImageType.URL}
]
));
it(`linear-gradient(to bottom, rgba(255,255,0,0.5), rgba(0,0,255,0.5)), url('https://html2canvas.hertzen.com')`, () =>
deepStrictEqual(
backgroundImageParse(
`linear-gradient(to bottom, rgba(255,255,0,0.5), rgba(0,0,255,0.5)), url('https://html2canvas.hertzen.com')`
),
[
{
angle: deg(180),
type: CSSImageType.LINEAR_GRADIENT,
stops: [{color: pack(255, 255, 0, 0.5), stop: null}, {color: pack(0, 0, 255, 0.5), stop: null}]
},
{url: 'https://html2canvas.hertzen.com', type: CSSImageType.URL}
]
));
});
});

View File

@ -0,0 +1,93 @@
import {deepStrictEqual} from 'assert';
import {Parser} from '../../syntax/parser';
import {color, COLORS} from '../../types/color';
import {textShadow} from '../text-shadow';
import {FLAG_INTEGER, DimensionToken, TokenType} from '../../syntax/tokenizer';
import {ZERO_LENGTH} from '../../types/length-percentage';
const textShadowParse = (value: string) => textShadow.parse(Parser.parseValues(value));
const colorParse = (value: string) => color.parse(Parser.parseValue(value));
const dimension = (number: number, unit: string): DimensionToken => ({
flags: FLAG_INTEGER,
number,
unit,
type: TokenType.DIMENSION_TOKEN
});
describe('property-descriptors', () => {
describe('text-shadow', () => {
it('none', () => deepStrictEqual(textShadowParse('none'), []));
it('1px 1px 2px pink', () =>
deepStrictEqual(textShadowParse('1px 1px 2px pink'), [
{
color: colorParse('pink'),
offsetX: dimension(1, 'px'),
offsetY: dimension(1, 'px'),
blur: dimension(2, 'px')
}
]));
it('#fc0 1px 0 10px', () =>
deepStrictEqual(textShadowParse('#fc0 1px 0 10px'), [
{
color: colorParse('#fc0'),
offsetX: dimension(1, 'px'),
offsetY: ZERO_LENGTH,
blur: dimension(10, 'px')
}
]));
it('5px 5px #558abb', () =>
deepStrictEqual(textShadowParse('5px 5px #558abb'), [
{
color: colorParse('#558abb'),
offsetX: dimension(5, 'px'),
offsetY: dimension(5, 'px'),
blur: ZERO_LENGTH
}
]));
it('white 2px 5px', () =>
deepStrictEqual(textShadowParse('white 2px 5px'), [
{
color: colorParse('#fff'),
offsetX: dimension(2, 'px'),
offsetY: dimension(5, 'px'),
blur: ZERO_LENGTH
}
]));
it('white 2px 5px', () =>
deepStrictEqual(textShadowParse('5px 10px'), [
{
color: COLORS.TRANSPARENT,
offsetX: dimension(5, 'px'),
offsetY: dimension(10, 'px'),
blur: ZERO_LENGTH
}
]));
it('1px 1px 2px red, 0 0 1em blue, 0 0 2em blue', () =>
deepStrictEqual(textShadowParse('1px 1px 2px red, 0 0 1em blue, 0 0 2em blue'), [
{
color: colorParse('red'),
offsetX: dimension(1, 'px'),
offsetY: dimension(1, 'px'),
blur: dimension(2, 'px')
},
{
color: colorParse('blue'),
offsetX: ZERO_LENGTH,
offsetY: ZERO_LENGTH,
blur: dimension(1, 'em')
},
{
color: colorParse('blue'),
offsetX: ZERO_LENGTH,
offsetY: ZERO_LENGTH,
blur: dimension(2, 'em')
}
]));
});
});

View File

@ -0,0 +1,21 @@
import {transform} from '../transform';
import {Parser} from '../../syntax/parser';
import {deepStrictEqual} from 'assert';
const parseValue = (value: string) => transform.parse(Parser.parseValue(value));
describe('property-descriptors', () => {
describe('transform', () => {
it('none', () => deepStrictEqual(parseValue('none'), null));
it('matrix(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)', () =>
deepStrictEqual(parseValue('matrix(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)'), [1, 2, 3, 4, 5, 6]));
it('matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)', () =>
deepStrictEqual(parseValue('matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)'), [
1,
0,
0,
1,
0,
0
]));
});
});

View File

@ -0,0 +1,29 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken} from '../syntax/parser';
export enum BACKGROUND_CLIP {
BORDER_BOX = 0,
PADDING_BOX = 1,
CONTENT_BOX = 2
}
export type BackgroundClip = BACKGROUND_CLIP[];
export const backgroundClip: IPropertyListDescriptor<BackgroundClip> = {
name: 'background-clip',
initialValue: 'border-box',
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]): BackgroundClip => {
return tokens.map(token => {
if (isIdentToken(token)) {
switch (token.value) {
case 'padding-box':
return BACKGROUND_CLIP.PADDING_BOX;
case 'content-box':
return BACKGROUND_CLIP.CONTENT_BOX;
}
}
return BACKGROUND_CLIP.BORDER_BOX;
});
}
};

View File

@ -0,0 +1,9 @@
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export const backgroundColor: IPropertyTypeValueDescriptor = {
name: `background-color`,
initialValue: 'transparent',
prefix: false,
type: PropertyDescriptorParsingType.TYPE_VALUE,
format: 'color'
};

View File

@ -0,0 +1,24 @@
import {TokenType} from '../syntax/tokenizer';
import {ICSSImage, image} from '../types/image';
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, nonFunctionArgSeperator} from '../syntax/parser';
export const backgroundImage: IPropertyListDescriptor<ICSSImage[]> = {
name: 'background-image',
initialValue: 'none',
type: PropertyDescriptorParsingType.LIST,
prefix: false,
parse: (tokens: CSSValue[]) => {
if (tokens.length === 0) {
return [];
}
const first = tokens[0];
if (first.type === TokenType.IDENT_TOKEN && first.value === 'none') {
return [];
}
return tokens.filter(nonFunctionArgSeperator).map(image.parse);
}
};

View File

@ -0,0 +1,30 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken} from '../syntax/parser';
export const enum BACKGROUND_ORIGIN {
BORDER_BOX = 0,
PADDING_BOX = 1,
CONTENT_BOX = 2
}
export type BackgroundOrigin = BACKGROUND_ORIGIN[];
export const backgroundOrigin: IPropertyListDescriptor<BackgroundOrigin> = {
name: 'background-origin',
initialValue: 'border-box',
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]): BackgroundOrigin => {
return tokens.map(token => {
if (isIdentToken(token)) {
switch (token.value) {
case 'padding-box':
return BACKGROUND_ORIGIN.PADDING_BOX;
case 'content-box':
return BACKGROUND_ORIGIN.CONTENT_BOX;
}
}
return BACKGROUND_ORIGIN.BORDER_BOX;
});
}
};

View File

@ -0,0 +1,18 @@
import {PropertyDescriptorParsingType, IPropertyListDescriptor} from '../IPropertyDescriptor';
import {CSSValue, parseFunctionArgs} from '../syntax/parser';
import {isLengthPercentage, LengthPercentageTuple, parseLengthPercentageTuple} from '../types/length-percentage';
export type BackgroundPosition = BackgroundImagePosition[];
export type BackgroundImagePosition = LengthPercentageTuple;
export const backgroundPosition: IPropertyListDescriptor<BackgroundPosition> = {
name: 'background-position',
initialValue: '0% 0%',
type: PropertyDescriptorParsingType.LIST,
prefix: false,
parse: (tokens: CSSValue[]): BackgroundPosition => {
return parseFunctionArgs(tokens)
.map((values: CSSValue[]) => values.filter(isLengthPercentage))
.map(parseLengthPercentageTuple);
}
};

View File

@ -0,0 +1,43 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken, parseFunctionArgs} from '../syntax/parser';
export type BackgroundRepeat = BACKGROUND_REPEAT[];
export enum BACKGROUND_REPEAT {
REPEAT = 0,
NO_REPEAT = 1,
REPEAT_X = 2,
REPEAT_Y = 3
}
export const backgroundRepeat: IPropertyListDescriptor<BackgroundRepeat> = {
name: 'background-repeat',
initialValue: 'repeat',
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]): BackgroundRepeat => {
return parseFunctionArgs(tokens)
.map(values =>
values
.filter(isIdentToken)
.map(token => token.value)
.join(' ')
)
.map(parseBackgroundRepeat);
}
};
const parseBackgroundRepeat = (value: string): BACKGROUND_REPEAT => {
switch (value) {
case 'no-repeat':
return BACKGROUND_REPEAT.NO_REPEAT;
case 'repeat-x':
case 'repeat no-repeat':
return BACKGROUND_REPEAT.REPEAT_X;
case 'repeat-y':
case 'no-repeat repeat':
return BACKGROUND_REPEAT.REPEAT_Y;
case 'repeat':
default:
return BACKGROUND_REPEAT.REPEAT;
}
};

View File

@ -0,0 +1,26 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken, parseFunctionArgs} from '../syntax/parser';
import {isLengthPercentage, LengthPercentage} from '../types/length-percentage';
import {StringValueToken} from '../syntax/tokenizer';
export enum BACKGROUND_SIZE {
AUTO = 'auto',
CONTAIN = 'contain',
COVER = 'cover'
}
export type BackgroundSizeInfo = LengthPercentage | StringValueToken;
export type BackgroundSize = BackgroundSizeInfo[][];
export const backgroundSize: IPropertyListDescriptor<BackgroundSize> = {
name: 'background-size',
initialValue: '0',
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]): BackgroundSize => {
return parseFunctionArgs(tokens).map(values => values.filter(isBackgroundSizeInfoToken));
}
};
const isBackgroundSizeInfoToken = (value: CSSValue): value is BackgroundSizeInfo =>
isIdentToken(value) || isLengthPercentage(value);

View File

@ -0,0 +1,13 @@
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
const borderColorForSide = (side: string): IPropertyTypeValueDescriptor => ({
name: `border-${side}-color`,
initialValue: 'transparent',
prefix: false,
type: PropertyDescriptorParsingType.TYPE_VALUE,
format: 'color'
});
export const borderTopColor: IPropertyTypeValueDescriptor = borderColorForSide('top');
export const borderRightColor: IPropertyTypeValueDescriptor = borderColorForSide('right');
export const borderBottomColor: IPropertyTypeValueDescriptor = borderColorForSide('bottom');
export const borderLeftColor: IPropertyTypeValueDescriptor = borderColorForSide('left');

View File

@ -0,0 +1,17 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue} from '../syntax/parser';
import {isLengthPercentage, LengthPercentageTuple, parseLengthPercentageTuple} from '../types/length-percentage';
export type BorderRadius = LengthPercentageTuple;
const borderRadiusForSide = (side: string): IPropertyListDescriptor<BorderRadius> => ({
name: `border-radius-${side}`,
initialValue: '0 0',
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]): BorderRadius => parseLengthPercentageTuple(tokens.filter(isLengthPercentage))
});
export const borderTopLeftRadius: IPropertyListDescriptor<BorderRadius> = borderRadiusForSide('top-left');
export const borderTopRightRadius: IPropertyListDescriptor<BorderRadius> = borderRadiusForSide('top-right');
export const borderBottomRightRadius: IPropertyListDescriptor<BorderRadius> = borderRadiusForSide('bottom-right');
export const borderBottomLeftRadius: IPropertyListDescriptor<BorderRadius> = borderRadiusForSide('bottom-left');

View File

@ -0,0 +1,24 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum BORDER_STYLE {
NONE = 0,
SOLID = 1
}
const borderStyleForSide = (side: string): IPropertyIdentValueDescriptor<BORDER_STYLE> => ({
name: `border-${side}-style`,
initialValue: 'solid',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (style: string): BORDER_STYLE => {
switch (style) {
case 'none':
return BORDER_STYLE.NONE;
}
return BORDER_STYLE.SOLID;
}
});
export const borderTopStyle: IPropertyIdentValueDescriptor<BORDER_STYLE> = borderStyleForSide('top');
export const borderRightStyle: IPropertyIdentValueDescriptor<BORDER_STYLE> = borderStyleForSide('right');
export const borderBottomStyle: IPropertyIdentValueDescriptor<BORDER_STYLE> = borderStyleForSide('bottom');
export const borderLeftStyle: IPropertyIdentValueDescriptor<BORDER_STYLE> = borderStyleForSide('left');

View File

@ -0,0 +1,19 @@
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isDimensionToken} from '../syntax/parser';
const borderWidthForSide = (side: string): IPropertyValueDescriptor<number> => ({
name: `border-${side}-width`,
initialValue: '0',
type: PropertyDescriptorParsingType.VALUE,
prefix: false,
parse: (token: CSSValue): number => {
if (isDimensionToken(token)) {
return token.number;
}
return 0;
}
});
export const borderTopWidth: IPropertyValueDescriptor<number> = borderWidthForSide('top');
export const borderRightWidth: IPropertyValueDescriptor<number> = borderWidthForSide('right');
export const borderBottomWidth: IPropertyValueDescriptor<number> = borderWidthForSide('bottom');
export const borderLeftWidth: IPropertyValueDescriptor<number> = borderWidthForSide('left');

View File

@ -0,0 +1,9 @@
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export const color: IPropertyTypeValueDescriptor = {
name: `color`,
initialValue: 'transparent',
prefix: false,
type: PropertyDescriptorParsingType.TYPE_VALUE,
format: 'color'
};

View File

@ -0,0 +1,25 @@
import {TokenType} from '../syntax/tokenizer';
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue} from '../syntax/parser';
export type Content = CSSValue[];
export const content: IPropertyListDescriptor<Content> = {
name: 'content',
initialValue: 'none',
type: PropertyDescriptorParsingType.LIST,
prefix: false,
parse: (tokens: CSSValue[]) => {
if (tokens.length === 0) {
return [];
}
const first = tokens[0];
if (first.type === TokenType.IDENT_TOKEN && first.value === 'none') {
return [];
}
return tokens;
}
};

View File

@ -0,0 +1,42 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isNumberToken, nonWhiteSpace} from '../syntax/parser';
import {TokenType} from '../syntax/tokenizer';
export interface COUNTER_INCREMENT {
counter: string;
increment: number;
}
export type CounterIncrement = COUNTER_INCREMENT[] | null;
export const counterIncrement: IPropertyListDescriptor<CounterIncrement> = {
name: 'counter-increment',
initialValue: 'none',
prefix: true,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]) => {
if (tokens.length === 0) {
return null;
}
const first = tokens[0];
if (first.type === TokenType.IDENT_TOKEN && first.value === 'none') {
return null;
}
const increments = [];
const filtered = tokens.filter(nonWhiteSpace);
for (let i = 0; i < filtered.length; i++) {
const counter = filtered[i];
const next = filtered[i + 1];
if (counter.type === TokenType.IDENT_TOKEN) {
const increment = next && isNumberToken(next) ? next.number : 1;
increments.push({counter: counter.value, increment});
}
}
return increments;
}
};

View File

@ -0,0 +1,35 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken, isNumberToken, nonWhiteSpace} from '../syntax/parser';
export interface COUNTER_RESET {
counter: string;
reset: number;
}
export type CounterReset = COUNTER_RESET[];
export const counterReset: IPropertyListDescriptor<CounterReset> = {
name: 'counter-reset',
initialValue: 'none',
prefix: true,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]) => {
if (tokens.length === 0) {
return [];
}
const resets = [];
const filtered = tokens.filter(nonWhiteSpace);
for (let i = 0; i < filtered.length; i++) {
const counter = filtered[i];
const next = filtered[i + 1];
if (isIdentToken(counter) && counter.value !== 'none') {
const reset = next && isNumberToken(next) ? next.number : 0;
resets.push({counter: counter.value, reset});
}
}
return resets;
}
};

View File

@ -1,42 +1,52 @@
/* @flow */
'use strict';
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken} from '../syntax/parser';
export const enum DISPLAY {
NONE = 0,
BLOCK = 1 << 1,
INLINE = 1 << 2,
RUN_IN = 1 << 3,
FLOW = 1 << 4,
FLOW_ROOT = 1 << 5,
TABLE = 1 << 6,
FLEX = 1 << 7,
GRID = 1 << 8,
RUBY = 1 << 9,
SUBGRID = 1 << 10,
LIST_ITEM = 1 << 11,
TABLE_ROW_GROUP = 1 << 12,
TABLE_HEADER_GROUP = 1 << 13,
TABLE_FOOTER_GROUP = 1 << 14,
TABLE_ROW = 1 << 15,
TABLE_CELL = 1 << 16,
TABLE_COLUMN_GROUP = 1 << 17,
TABLE_COLUMN = 1 << 18,
TABLE_CAPTION = 1 << 19,
RUBY_BASE = 1 << 20,
RUBY_TEXT = 1 << 21,
RUBY_BASE_CONTAINER = 1 << 22,
RUBY_TEXT_CONTAINER = 1 << 23,
CONTENTS = 1 << 24,
INLINE_BLOCK = 1 << 25,
INLINE_LIST_ITEM = 1 << 26,
INLINE_TABLE = 1 << 27,
INLINE_FLEX = 1 << 28,
INLINE_GRID = 1 << 29
}
export const DISPLAY = {
NONE: 1 << 0,
BLOCK: 1 << 1,
INLINE: 1 << 2,
RUN_IN: 1 << 3,
FLOW: 1 << 4,
FLOW_ROOT: 1 << 5,
TABLE: 1 << 6,
FLEX: 1 << 7,
GRID: 1 << 8,
RUBY: 1 << 9,
SUBGRID: 1 << 10,
LIST_ITEM: 1 << 11,
TABLE_ROW_GROUP: 1 << 12,
TABLE_HEADER_GROUP: 1 << 13,
TABLE_FOOTER_GROUP: 1 << 14,
TABLE_ROW: 1 << 15,
TABLE_CELL: 1 << 16,
TABLE_COLUMN_GROUP: 1 << 17,
TABLE_COLUMN: 1 << 18,
TABLE_CAPTION: 1 << 19,
RUBY_BASE: 1 << 20,
RUBY_TEXT: 1 << 21,
RUBY_BASE_CONTAINER: 1 << 22,
RUBY_TEXT_CONTAINER: 1 << 23,
CONTENTS: 1 << 24,
INLINE_BLOCK: 1 << 25,
INLINE_LIST_ITEM: 1 << 26,
INLINE_TABLE: 1 << 27,
INLINE_FLEX: 1 << 28,
INLINE_GRID: 1 << 29
export type Display = number;
export const display: IPropertyListDescriptor<Display> = {
name: 'display',
initialValue: 'inline-block',
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]): Display => {
return tokens.filter(isIdentToken).reduce((bit, token) => {
return bit | parseDisplayValue(token.value);
}, DISPLAY.NONE);
}
};
export type Display = $Values<typeof DISPLAY>;
export type DisplayBit = number;
const parseDisplayValue = (display: string): Display => {
switch (display) {
case 'block':
@ -52,6 +62,7 @@ const parseDisplayValue = (display: string): Display => {
case 'table':
return DISPLAY.TABLE;
case 'flex':
case '-webkit-flex':
return DISPLAY.FLEX;
case 'grid':
return DISPLAY.GRID;
@ -101,11 +112,3 @@ const parseDisplayValue = (display: string): Display => {
return DISPLAY.NONE;
};
const setDisplayBit = (bit: DisplayBit, display: string): DisplayBit => {
return bit | parseDisplayValue(display);
};
export const parseDisplay = (display: string): Display => {
return display.split(' ').reduce(setDisplayBit, 0);
};

View File

@ -0,0 +1,28 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum FLOAT {
NONE = 0,
LEFT = 1,
RIGHT = 2,
INLINE_START = 3,
INLINE_END = 4
}
export const float: IPropertyIdentValueDescriptor<FLOAT> = {
name: 'float',
initialValue: 'none',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (float: string) => {
switch (float) {
case 'left':
return FLOAT.LEFT;
case 'right':
return FLOAT.RIGHT;
case 'inline-start':
return FLOAT.INLINE_START;
case 'inline-end':
return FLOAT.INLINE_END;
}
return FLOAT.NONE;
}
};

View File

@ -0,0 +1,20 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue} from '../syntax/parser';
import {StringValueToken, TokenType} from '../syntax/tokenizer';
export type FONT_FAMILY = string;
export type FontFamily = FONT_FAMILY[];
export const fontFamily: IPropertyListDescriptor<FontFamily> = {
name: `font-family`,
initialValue: '',
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]) => {
return tokens.filter(isStringToken).map(token => token.value);
}
};
const isStringToken = (token: CSSValue): token is StringValueToken =>
token.type === TokenType.STRING_TOKEN || token.type === TokenType.IDENT_TOKEN;

View File

@ -0,0 +1,9 @@
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export const fontSize: IPropertyTypeValueDescriptor = {
name: `font-size`,
initialValue: '0',
prefix: false,
type: PropertyDescriptorParsingType.TYPE_VALUE,
format: 'length'
};

View File

@ -0,0 +1,24 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum FONT_STYLE {
NORMAL = 'normal',
ITALIC = 'italic',
OBLIQUE = 'oblique'
}
export const fontStyle: IPropertyIdentValueDescriptor<FONT_STYLE> = {
name: 'font-style',
initialValue: 'normal',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (overflow: string) => {
switch (overflow) {
case 'oblique':
return FONT_STYLE.OBLIQUE;
case 'italic':
return FONT_STYLE.ITALIC;
case 'normal':
default:
return FONT_STYLE.NORMAL;
}
}
};

View File

@ -0,0 +1,11 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken} from '../syntax/parser';
export const fontVariant: IPropertyListDescriptor<string[]> = {
name: 'font-variant',
initialValue: 'none',
type: PropertyDescriptorParsingType.LIST,
prefix: false,
parse: (tokens: CSSValue[]): string[] => {
return tokens.filter(isIdentToken).map(token => token.value);
}
};

View File

@ -0,0 +1,25 @@
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken, isNumberToken} from '../syntax/parser';
export const fontWeight: IPropertyValueDescriptor<number> = {
name: 'font-weight',
initialValue: 'normal',
type: PropertyDescriptorParsingType.VALUE,
prefix: false,
parse: (token: CSSValue): number => {
if (isNumberToken(token)) {
return token.number;
}
if (isIdentToken(token)) {
switch (token.value) {
case 'bold':
return 700;
case 'normal':
default:
return 400;
}
}
return 400;
}
};

View File

@ -0,0 +1,24 @@
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue} from '../syntax/parser';
import {TokenType} from '../syntax/tokenizer';
export const letterSpacing: IPropertyValueDescriptor<number> = {
name: 'letter-spacing',
initialValue: '0',
prefix: false,
type: PropertyDescriptorParsingType.VALUE,
parse: (token: CSSValue) => {
if (token.type === TokenType.IDENT_TOKEN && token.value === 'normal') {
return 0;
}
if (token.type === TokenType.NUMBER_TOKEN) {
return token.number;
}
if (token.type === TokenType.DIMENSION_TOKEN) {
return token.number;
}
return 0;
}
};

View File

@ -0,0 +1,21 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum LINE_BREAK {
NORMAL = 'normal',
STRICT = 'strict'
}
export const lineBreak: IPropertyIdentValueDescriptor<LINE_BREAK> = {
name: 'line-break',
initialValue: 'normal',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (lineBreak: string): LINE_BREAK => {
switch (lineBreak) {
case 'strict':
return LINE_BREAK.STRICT;
case 'normal':
default:
return LINE_BREAK.NORMAL;
}
}
};

View File

@ -0,0 +1,22 @@
import {IPropertyTokenValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken} from '../syntax/parser';
import {TokenType} from '../syntax/tokenizer';
import {getAbsoluteValue, isLengthPercentage} from '../types/length-percentage';
export const lineHeight: IPropertyTokenValueDescriptor = {
name: 'line-height',
initialValue: 'normal',
prefix: false,
type: PropertyDescriptorParsingType.TOKEN_VALUE
};
export const computeLineHeight = (token: CSSValue, fontSize: number): number => {
if (isIdentToken(token) && token.value === 'normal') {
return 1.2 * fontSize;
} else if (token.type === TokenType.NUMBER_TOKEN) {
return fontSize * token.number;
} else if (isLengthPercentage(token)) {
return getAbsoluteValue(token, fontSize);
}
return fontSize;
};

View File

@ -0,0 +1,18 @@
import {TokenType} from '../syntax/tokenizer';
import {ICSSImage, image} from '../types/image';
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue} from '../syntax/parser';
export const listStyleImage: IPropertyValueDescriptor<ICSSImage | null> = {
name: 'list-style-image',
initialValue: 'none',
type: PropertyDescriptorParsingType.VALUE,
prefix: false,
parse: (token: CSSValue) => {
if (token.type === TokenType.IDENT_TOKEN && token.value === 'none') {
return null;
}
return image.parse(token);
}
};

View File

@ -0,0 +1,21 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum LIST_STYLE_POSITION {
INSIDE = 0,
OUTSIDE = 1
}
export const listStylePosition: IPropertyIdentValueDescriptor<LIST_STYLE_POSITION> = {
name: 'list-style-position',
initialValue: 'outside',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (position: string) => {
switch (position) {
case 'inside':
return LIST_STYLE_POSITION.INSIDE;
case 'outside':
default:
return LIST_STYLE_POSITION.OUTSIDE;
}
}
};

View File

@ -0,0 +1,177 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum LIST_STYLE_TYPE {
NONE = -1,
DISC = 0,
CIRCLE = 1,
SQUARE = 2,
DECIMAL = 3,
CJK_DECIMAL = 4,
DECIMAL_LEADING_ZERO = 5,
LOWER_ROMAN = 6,
UPPER_ROMAN = 7,
LOWER_GREEK = 8,
LOWER_ALPHA = 9,
UPPER_ALPHA = 10,
ARABIC_INDIC = 11,
ARMENIAN = 12,
BENGALI = 13,
CAMBODIAN = 14,
CJK_EARTHLY_BRANCH = 15,
CJK_HEAVENLY_STEM = 16,
CJK_IDEOGRAPHIC = 17,
DEVANAGARI = 18,
ETHIOPIC_NUMERIC = 19,
GEORGIAN = 20,
GUJARATI = 21,
GURMUKHI = 22,
HEBREW = 22,
HIRAGANA = 23,
HIRAGANA_IROHA = 24,
JAPANESE_FORMAL = 25,
JAPANESE_INFORMAL = 26,
KANNADA = 27,
KATAKANA = 28,
KATAKANA_IROHA = 29,
KHMER = 30,
KOREAN_HANGUL_FORMAL = 31,
KOREAN_HANJA_FORMAL = 32,
KOREAN_HANJA_INFORMAL = 33,
LAO = 34,
LOWER_ARMENIAN = 35,
MALAYALAM = 36,
MONGOLIAN = 37,
MYANMAR = 38,
ORIYA = 39,
PERSIAN = 40,
SIMP_CHINESE_FORMAL = 41,
SIMP_CHINESE_INFORMAL = 42,
TAMIL = 43,
TELUGU = 44,
THAI = 45,
TIBETAN = 46,
TRAD_CHINESE_FORMAL = 47,
TRAD_CHINESE_INFORMAL = 48,
UPPER_ARMENIAN = 49,
DISCLOSURE_OPEN = 50,
DISCLOSURE_CLOSED = 51
}
export const listStyleType: IPropertyIdentValueDescriptor<LIST_STYLE_TYPE> = {
name: 'list-style-type',
initialValue: 'none',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (type: string) => {
switch (type) {
case 'disc':
return LIST_STYLE_TYPE.DISC;
case 'circle':
return LIST_STYLE_TYPE.CIRCLE;
case 'square':
return LIST_STYLE_TYPE.SQUARE;
case 'decimal':
return LIST_STYLE_TYPE.DECIMAL;
case 'cjk-decimal':
return LIST_STYLE_TYPE.CJK_DECIMAL;
case 'decimal-leading-zero':
return LIST_STYLE_TYPE.DECIMAL_LEADING_ZERO;
case 'lower-roman':
return LIST_STYLE_TYPE.LOWER_ROMAN;
case 'upper-roman':
return LIST_STYLE_TYPE.UPPER_ROMAN;
case 'lower-greek':
return LIST_STYLE_TYPE.LOWER_GREEK;
case 'lower-alpha':
return LIST_STYLE_TYPE.LOWER_ALPHA;
case 'upper-alpha':
return LIST_STYLE_TYPE.UPPER_ALPHA;
case 'arabic-indic':
return LIST_STYLE_TYPE.ARABIC_INDIC;
case 'armenian':
return LIST_STYLE_TYPE.ARMENIAN;
case 'bengali':
return LIST_STYLE_TYPE.BENGALI;
case 'cambodian':
return LIST_STYLE_TYPE.CAMBODIAN;
case 'cjk-earthly-branch':
return LIST_STYLE_TYPE.CJK_EARTHLY_BRANCH;
case 'cjk-heavenly-stem':
return LIST_STYLE_TYPE.CJK_HEAVENLY_STEM;
case 'cjk-ideographic':
return LIST_STYLE_TYPE.CJK_IDEOGRAPHIC;
case 'devanagari':
return LIST_STYLE_TYPE.DEVANAGARI;
case 'ethiopic-numeric':
return LIST_STYLE_TYPE.ETHIOPIC_NUMERIC;
case 'georgian':
return LIST_STYLE_TYPE.GEORGIAN;
case 'gujarati':
return LIST_STYLE_TYPE.GUJARATI;
case 'gurmukhi':
return LIST_STYLE_TYPE.GURMUKHI;
case 'hebrew':
return LIST_STYLE_TYPE.HEBREW;
case 'hiragana':
return LIST_STYLE_TYPE.HIRAGANA;
case 'hiragana-iroha':
return LIST_STYLE_TYPE.HIRAGANA_IROHA;
case 'japanese-formal':
return LIST_STYLE_TYPE.JAPANESE_FORMAL;
case 'japanese-informal':
return LIST_STYLE_TYPE.JAPANESE_INFORMAL;
case 'kannada':
return LIST_STYLE_TYPE.KANNADA;
case 'katakana':
return LIST_STYLE_TYPE.KATAKANA;
case 'katakana-iroha':
return LIST_STYLE_TYPE.KATAKANA_IROHA;
case 'khmer':
return LIST_STYLE_TYPE.KHMER;
case 'korean-hangul-formal':
return LIST_STYLE_TYPE.KOREAN_HANGUL_FORMAL;
case 'korean-hanja-formal':
return LIST_STYLE_TYPE.KOREAN_HANJA_FORMAL;
case 'korean-hanja-informal':
return LIST_STYLE_TYPE.KOREAN_HANJA_INFORMAL;
case 'lao':
return LIST_STYLE_TYPE.LAO;
case 'lower-armenian':
return LIST_STYLE_TYPE.LOWER_ARMENIAN;
case 'malayalam':
return LIST_STYLE_TYPE.MALAYALAM;
case 'mongolian':
return LIST_STYLE_TYPE.MONGOLIAN;
case 'myanmar':
return LIST_STYLE_TYPE.MYANMAR;
case 'oriya':
return LIST_STYLE_TYPE.ORIYA;
case 'persian':
return LIST_STYLE_TYPE.PERSIAN;
case 'simp-chinese-formal':
return LIST_STYLE_TYPE.SIMP_CHINESE_FORMAL;
case 'simp-chinese-informal':
return LIST_STYLE_TYPE.SIMP_CHINESE_INFORMAL;
case 'tamil':
return LIST_STYLE_TYPE.TAMIL;
case 'telugu':
return LIST_STYLE_TYPE.TELUGU;
case 'thai':
return LIST_STYLE_TYPE.THAI;
case 'tibetan':
return LIST_STYLE_TYPE.TIBETAN;
case 'trad-chinese-formal':
return LIST_STYLE_TYPE.TRAD_CHINESE_FORMAL;
case 'trad-chinese-informal':
return LIST_STYLE_TYPE.TRAD_CHINESE_INFORMAL;
case 'upper-armenian':
return LIST_STYLE_TYPE.UPPER_ARMENIAN;
case 'disclosure-open':
return LIST_STYLE_TYPE.DISCLOSURE_OPEN;
case 'disclosure-closed':
return LIST_STYLE_TYPE.DISCLOSURE_CLOSED;
case 'none':
default:
return LIST_STYLE_TYPE.NONE;
}
}
};

View File

@ -0,0 +1,13 @@
import {IPropertyTokenValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
const marginForSide = (side: string): IPropertyTokenValueDescriptor => ({
name: `margin-${side}`,
initialValue: '0',
prefix: false,
type: PropertyDescriptorParsingType.TOKEN_VALUE
});
export const marginTop: IPropertyTokenValueDescriptor = marginForSide('top');
export const marginRight: IPropertyTokenValueDescriptor = marginForSide('right');
export const marginBottom: IPropertyTokenValueDescriptor = marginForSide('bottom');
export const marginLeft: IPropertyTokenValueDescriptor = marginForSide('left');

View File

@ -0,0 +1,14 @@
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isNumberToken} from '../syntax/parser';
export const opacity: IPropertyValueDescriptor<number> = {
name: 'opacity',
initialValue: '1',
type: PropertyDescriptorParsingType.VALUE,
prefix: false,
parse: (token: CSSValue): number => {
if (isNumberToken(token)) {
return token.number;
}
return 1;
}
};

View File

@ -0,0 +1,21 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum OVERFLOW_WRAP {
NORMAL = 'normal',
BREAK_WORD = 'break-word'
}
export const overflowWrap: IPropertyIdentValueDescriptor<OVERFLOW_WRAP> = {
name: 'overflow-wrap',
initialValue: 'normal',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (overflow: string) => {
switch (overflow) {
case 'break-word':
return OVERFLOW_WRAP.BREAK_WORD;
case 'normal':
default:
return OVERFLOW_WRAP.NORMAL;
}
}
};

View File

@ -0,0 +1,27 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum OVERFLOW {
VISIBLE = 0,
HIDDEN = 1,
SCROLL = 2,
AUTO = 3
}
export const overflow: IPropertyIdentValueDescriptor<OVERFLOW> = {
name: 'overflow',
initialValue: 'visible',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (overflow: string) => {
switch (overflow) {
case 'hidden':
return OVERFLOW.HIDDEN;
case 'scroll':
return OVERFLOW.SCROLL;
case 'auto':
return OVERFLOW.AUTO;
case 'visible':
default:
return OVERFLOW.VISIBLE;
}
}
};

View File

@ -0,0 +1,14 @@
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
const paddingForSide = (side: string): IPropertyTypeValueDescriptor => ({
name: `padding-${side}`,
initialValue: '0',
prefix: false,
type: PropertyDescriptorParsingType.TYPE_VALUE,
format: 'length-percentage'
});
export const paddingTop: IPropertyTypeValueDescriptor = paddingForSide('top');
export const paddingRight: IPropertyTypeValueDescriptor = paddingForSide('right');
export const paddingBottom: IPropertyTypeValueDescriptor = paddingForSide('bottom');
export const paddingLeft: IPropertyTypeValueDescriptor = paddingForSide('left');

View File

@ -0,0 +1,29 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum POSITION {
STATIC = 0,
RELATIVE = 1,
ABSOLUTE = 2,
FIXED = 3,
STICKY = 4
}
export const position: IPropertyIdentValueDescriptor<POSITION> = {
name: 'position',
initialValue: 'static',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (position: string) => {
switch (position) {
case 'relative':
return POSITION.RELATIVE;
case 'absolute':
return POSITION.ABSOLUTE;
case 'fixed':
return POSITION.FIXED;
case 'sticky':
return POSITION.STICKY;
}
return POSITION.STATIC;
}
};

View File

@ -0,0 +1,56 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isStringToken} from '../syntax/parser';
import {TokenType} from '../syntax/tokenizer';
export interface QUOTE {
open: string;
close: string;
}
export type Quotes = QUOTE[] | null;
export const quotes: IPropertyListDescriptor<Quotes> = {
name: 'quotes',
initialValue: 'none',
prefix: true,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]) => {
if (tokens.length === 0) {
return null;
}
const first = tokens[0];
if (first.type === TokenType.IDENT_TOKEN && first.value === 'none') {
return null;
}
const quotes = [];
const filtered = tokens.filter(isStringToken);
if (filtered.length % 2 !== 0) {
return null;
}
for (let i = 0; i < filtered.length; i += 2) {
const open = filtered[i].value;
const close = filtered[i + 1].value;
quotes.push({open, close});
}
return quotes;
}
};
export const getQuote = (quotes: Quotes, depth: number, open: boolean): string => {
if (!quotes) {
return '';
}
const quote = quotes[Math.min(depth, quotes.length - 1)];
if (!quote) {
return '';
}
return open ? quote.open : quote.close;
};

View File

@ -0,0 +1,25 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum TEXT_ALIGN {
LEFT = 0,
CENTER = 1,
RIGHT = 2
}
export const textAlign: IPropertyIdentValueDescriptor<TEXT_ALIGN> = {
name: 'text-align',
initialValue: 'left',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (textAlign: string) => {
switch (textAlign) {
case 'right':
return TEXT_ALIGN.RIGHT;
case 'center':
case 'justify':
return TEXT_ALIGN.CENTER;
case 'left':
default:
return TEXT_ALIGN.LEFT;
}
}
};

View File

@ -0,0 +1,9 @@
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export const textDecorationColor: IPropertyTypeValueDescriptor = {
name: `text-decoration-color`,
initialValue: 'transparent',
prefix: false,
type: PropertyDescriptorParsingType.TYPE_VALUE,
format: 'color'
};

View File

@ -0,0 +1,37 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken} from '../syntax/parser';
export const enum TEXT_DECORATION_LINE {
NONE = 0,
UNDERLINE = 1,
OVERLINE = 2,
LINE_THROUGH = 3,
BLINK = 4
}
export type TextDecorationLine = TEXT_DECORATION_LINE[];
export const textDecorationLine: IPropertyListDescriptor<TextDecorationLine> = {
name: 'text-decoration-line',
initialValue: 'none',
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]): TextDecorationLine => {
return tokens
.filter(isIdentToken)
.map(token => {
switch (token.value) {
case 'underline':
return TEXT_DECORATION_LINE.UNDERLINE;
case 'overline':
return TEXT_DECORATION_LINE.OVERLINE;
case 'line-through':
return TEXT_DECORATION_LINE.LINE_THROUGH;
case 'none':
return TEXT_DECORATION_LINE.BLINK;
}
return TEXT_DECORATION_LINE.NONE;
})
.filter(line => line !== TEXT_DECORATION_LINE.NONE);
}
};

View File

@ -0,0 +1,51 @@
import {PropertyDescriptorParsingType, IPropertyListDescriptor} from '../IPropertyDescriptor';
import {CSSValue, isIdentWithValue, parseFunctionArgs} from '../syntax/parser';
import {ZERO_LENGTH} from '../types/length-percentage';
import {color, Color, COLORS} from '../types/color';
import {isLength, Length} from '../types/length';
export type TextShadow = TextShadowItem[];
interface TextShadowItem {
color: Color;
offsetX: Length;
offsetY: Length;
blur: Length;
}
export const textShadow: IPropertyListDescriptor<TextShadow> = {
name: 'text-shadow',
initialValue: 'none',
type: PropertyDescriptorParsingType.LIST,
prefix: false,
parse: (tokens: CSSValue[]): TextShadow => {
if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) {
return [];
}
return parseFunctionArgs(tokens).map((values: CSSValue[]) => {
const shadow: TextShadowItem = {
color: COLORS.TRANSPARENT,
offsetX: ZERO_LENGTH,
offsetY: ZERO_LENGTH,
blur: ZERO_LENGTH
};
let c = 0;
for (let i = 0; i < values.length; i++) {
const token = values[i];
if (isLength(token)) {
if (c === 0) {
shadow.offsetX = token;
} else if (c === 1) {
shadow.offsetY = token;
} else {
shadow.blur = token;
}
c++;
} else {
shadow.color = color.parse(token);
}
}
return shadow;
});
}
};

View File

@ -0,0 +1,26 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum TEXT_TRANSFORM {
NONE = 0,
LOWERCASE = 1,
UPPERCASE = 2,
CAPITALIZE = 3
}
export const textTransform: IPropertyIdentValueDescriptor<TEXT_TRANSFORM> = {
name: 'text-transform',
initialValue: 'none',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (textTransform: string) => {
switch (textTransform) {
case 'uppercase':
return TEXT_TRANSFORM.UPPERCASE;
case 'lowercase':
return TEXT_TRANSFORM.LOWERCASE;
case 'capitalize':
return TEXT_TRANSFORM.CAPITALIZE;
}
return TEXT_TRANSFORM.NONE;
}
};

View File

@ -0,0 +1,28 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue} from '../syntax/parser';
import {isLengthPercentage, LengthPercentage} from '../types/length-percentage';
import {FLAG_INTEGER, TokenType} from '../syntax/tokenizer';
export type TransformOrigin = [LengthPercentage, LengthPercentage];
const DEFAULT_VALUE: LengthPercentage = {
type: TokenType.PERCENTAGE_TOKEN,
number: 50,
flags: FLAG_INTEGER
};
const DEFAULT: TransformOrigin = [DEFAULT_VALUE, DEFAULT_VALUE];
export const transformOrigin: IPropertyListDescriptor<TransformOrigin> = {
name: 'transform-origin',
initialValue: '50% 50%',
prefix: true,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]) => {
const origins: LengthPercentage[] = tokens.filter(isLengthPercentage);
if (origins.length !== 2) {
return DEFAULT;
}
return [origins[0], origins[1]];
}
};

View File

@ -0,0 +1,49 @@
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue} from '../syntax/parser';
import {NumberValueToken, TokenType} from '../syntax/tokenizer';
export type Matrix = [number, number, number, number, number, number];
export type Transform = Matrix | null;
export const transform: IPropertyValueDescriptor<Transform> = {
name: 'transform',
initialValue: 'none',
prefix: true,
type: PropertyDescriptorParsingType.VALUE,
parse: (token: CSSValue) => {
if (token.type === TokenType.IDENT_TOKEN && token.value === 'none') {
return null;
}
if (token.type === TokenType.FUNCTION) {
const transformFunction = SUPPORTED_TRANSFORM_FUNCTIONS[token.name];
if (typeof transformFunction === 'undefined') {
throw new Error(`Attempting to parse an unsupported transform function "${token.name}"`);
}
return transformFunction(token.values);
}
return null;
}
};
const matrix = (args: CSSValue[]): Transform => {
const values = args.filter(arg => arg.type === TokenType.NUMBER_TOKEN).map((arg: NumberValueToken) => arg.number);
return values.length === 6 ? (values as Matrix) : null;
};
// doesn't support 3D transforms at the moment
const matrix3d = (args: CSSValue[]): Transform => {
const values = args.filter(arg => arg.type === TokenType.NUMBER_TOKEN).map((arg: NumberValueToken) => arg.number);
const [a1, b1, {}, {}, a2, b2, {}, {}, {}, {}, {}, {}, a4, b4, {}, {}] = values;
return values.length === 16 ? [a1, b1, a2, b2, a4, b4] : null;
};
const SUPPORTED_TRANSFORM_FUNCTIONS: {
[key: string]: (args: CSSValue[]) => Transform;
} = {
matrix: matrix,
matrix3d: matrix3d
};

View File

@ -0,0 +1,24 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum VISIBILITY {
VISIBLE = 0,
HIDDEN = 1,
COLLAPSE = 2
}
export const visibility: IPropertyIdentValueDescriptor<VISIBILITY> = {
name: 'visible',
initialValue: 'none',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (visibility: string) => {
switch (visibility) {
case 'hidden':
return VISIBILITY.HIDDEN;
case 'collapse':
return VISIBILITY.COLLAPSE;
case 'visible':
default:
return VISIBILITY.VISIBLE;
}
}
};

View File

@ -0,0 +1,24 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum WORD_BREAK {
NORMAL = 'normal',
BREAK_ALL = 'break-all',
KEEP_ALL = 'keep-all'
}
export const wordBreak: IPropertyIdentValueDescriptor<WORD_BREAK> = {
name: 'word-break',
initialValue: 'normal',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (wordBreak: string): WORD_BREAK => {
switch (wordBreak) {
case 'break-all':
return WORD_BREAK.BREAK_ALL;
case 'keep-all':
return WORD_BREAK.KEEP_ALL;
case 'normal':
default:
return WORD_BREAK.NORMAL;
}
}
};

View File

@ -0,0 +1,26 @@
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isNumberToken} from '../syntax/parser';
import {TokenType} from '../syntax/tokenizer';
interface zIndex {
order: number;
auto: boolean;
}
export const zIndex: IPropertyValueDescriptor<zIndex> = {
name: 'z-index',
initialValue: 'auto',
prefix: false,
type: PropertyDescriptorParsingType.VALUE,
parse: (token: CSSValue): zIndex => {
if (token.type === TokenType.IDENT_TOKEN) {
return {auto: true, order: 0};
}
if (isNumberToken(token)) {
return {auto: false, order: token.number};
}
throw new Error(`Invalid z-index number parsed`);
}
};

View File

@ -0,0 +1,29 @@
import {deepEqual} from 'assert';
import {Tokenizer, TokenType} from '../tokenizer';
const tokenize = (value: string) => {
const tokenizer = new Tokenizer();
tokenizer.write(value);
return tokenizer.read();
};
describe('tokenizer', () => {
describe('<ident>', () => {
it('auto', () => deepEqual(tokenize('auto'), [{type: TokenType.IDENT_TOKEN, value: 'auto'}]));
it('url', () => deepEqual(tokenize('url'), [{type: TokenType.IDENT_TOKEN, value: 'url'}]));
it('auto test', () =>
deepEqual(tokenize('auto test'), [
{type: TokenType.IDENT_TOKEN, value: 'auto'},
{type: TokenType.WHITESPACE_TOKEN},
{type: TokenType.IDENT_TOKEN, value: 'test'}
]));
});
describe('<url-token>', () => {
it('url(test.jpg)', () =>
deepEqual(tokenize('url(test.jpg)'), [{type: TokenType.URL_TOKEN, value: 'test.jpg'}]));
it('url("test.jpg")', () =>
deepEqual(tokenize('url("test.jpg")'), [{type: TokenType.URL_TOKEN, value: 'test.jpg'}]));
it("url('test.jpg')", () =>
deepEqual(tokenize("url('test.jpg')"), [{type: TokenType.URL_TOKEN, value: 'test.jpg'}]));
});
});

188
src/css/syntax/parser.ts Normal file
View File

@ -0,0 +1,188 @@
import {
CSSToken,
DimensionToken,
EOF_TOKEN,
NumberValueToken,
StringValueToken,
Tokenizer,
TokenType
} from './tokenizer';
export type CSSBlockType =
| TokenType.LEFT_PARENTHESIS_TOKEN
| TokenType.LEFT_SQUARE_BRACKET_TOKEN
| TokenType.LEFT_CURLY_BRACKET_TOKEN;
export interface CSSBlock {
type: CSSBlockType;
values: CSSValue[];
}
export interface CSSFunction {
type: TokenType.FUNCTION;
name: string;
values: CSSValue[];
}
export type CSSValue = CSSFunction | CSSToken | CSSBlock;
export class Parser {
private _tokens: CSSToken[];
constructor(tokens: CSSToken[]) {
this._tokens = tokens;
}
static create(value: string): Parser {
const tokenizer = new Tokenizer();
tokenizer.write(value);
return new Parser(tokenizer.read());
}
static parseValue(value: string): CSSValue {
return Parser.create(value).parseComponentValue();
}
static parseValues(value: string): CSSValue[] {
return Parser.create(value).parseComponentValues();
}
parseComponentValue(): CSSValue {
let token = this.consumeToken();
while (token.type === TokenType.WHITESPACE_TOKEN) {
token = this.consumeToken();
}
if (token.type === TokenType.EOF_TOKEN) {
throw new SyntaxError(`Error parsing CSS component value, unexpected EOF`);
}
this.reconsumeToken(token);
const value = this.consumeComponentValue();
do {
token = this.consumeToken();
} while (token.type === TokenType.WHITESPACE_TOKEN);
if (token.type === TokenType.EOF_TOKEN) {
return value;
}
throw new SyntaxError(`Error parsing CSS component value, multiple values found when expecting only one`);
}
parseComponentValues(): CSSValue[] {
const values = [];
while (true) {
let value = this.consumeComponentValue();
if (value.type === TokenType.EOF_TOKEN) {
return values;
}
values.push(value);
values.push();
}
}
private consumeComponentValue(): CSSValue {
const token = this.consumeToken();
switch (token.type) {
case TokenType.LEFT_CURLY_BRACKET_TOKEN:
case TokenType.LEFT_SQUARE_BRACKET_TOKEN:
case TokenType.LEFT_PARENTHESIS_TOKEN:
return this.consumeSimpleBlock(token.type);
case TokenType.FUNCTION_TOKEN:
return this.consumeFunction(token);
}
return token;
}
private consumeSimpleBlock(type: CSSBlockType): CSSBlock {
const block: CSSBlock = {type, values: []};
let token = this.consumeToken();
while (true) {
if (token.type === TokenType.EOF_TOKEN || isEndingTokenFor(token, type)) {
return block;
}
this.reconsumeToken(token);
block.values.push(this.consumeComponentValue());
token = this.consumeToken();
}
}
private consumeFunction(functionToken: StringValueToken): CSSFunction {
const cssFunction: CSSFunction = {
name: functionToken.value,
values: [],
type: TokenType.FUNCTION
};
while (true) {
const token = this.consumeToken();
if (token.type === TokenType.EOF_TOKEN || token.type === TokenType.RIGHT_PARENTHESIS_TOKEN) {
return cssFunction;
}
this.reconsumeToken(token);
cssFunction.values.push(this.consumeComponentValue());
}
}
private consumeToken(): CSSToken {
const token = this._tokens.shift();
return typeof token === 'undefined' ? EOF_TOKEN : token;
}
private reconsumeToken(token: CSSToken): void {
this._tokens.unshift(token);
}
}
export const isDimensionToken = (token: CSSValue): token is DimensionToken => token.type === TokenType.DIMENSION_TOKEN;
export const isNumberToken = (token: CSSValue): token is NumberValueToken => token.type === TokenType.NUMBER_TOKEN;
export const isIdentToken = (token: CSSValue): token is StringValueToken => token.type === TokenType.IDENT_TOKEN;
export const isStringToken = (token: CSSValue): token is StringValueToken => token.type === TokenType.STRING_TOKEN;
export const isIdentWithValue = (token: CSSValue, value: string): boolean =>
isIdentToken(token) && token.value === value;
export const nonWhiteSpace = (token: CSSValue) => token.type !== TokenType.WHITESPACE_TOKEN;
export const nonFunctionArgSeperator = (token: CSSValue) =>
token.type !== TokenType.WHITESPACE_TOKEN && token.type !== TokenType.COMMA_TOKEN;
export const parseFunctionArgs = (tokens: CSSValue[]): CSSValue[][] => {
const args: CSSValue[][] = [];
let arg: CSSValue[] = [];
tokens.forEach(token => {
if (token.type === TokenType.COMMA_TOKEN) {
if (arg.length === 0) {
throw new Error(`Error parsing function args, zero tokens for arg`);
}
args.push(arg);
arg = [];
return;
}
if (token.type !== TokenType.WHITESPACE_TOKEN) {
arg.push(token);
}
});
if (arg.length) {
args.push(arg);
}
return args;
};
const isEndingTokenFor = (token: CSSToken, type: CSSBlockType): boolean => {
if (type === TokenType.LEFT_CURLY_BRACKET_TOKEN && token.type === TokenType.RIGHT_CURLY_BRACKET_TOKEN) {
return true;
}
if (type === TokenType.LEFT_SQUARE_BRACKET_TOKEN && token.type === TokenType.RIGHT_SQUARE_BRACKET_TOKEN) {
return true;
}
return type === TokenType.LEFT_PARENTHESIS_TOKEN && token.type === TokenType.RIGHT_PARENTHESIS_TOKEN;
};

784
src/css/syntax/tokenizer.ts Normal file
View File

@ -0,0 +1,784 @@
// https://www.w3.org/TR/css-syntax-3
import {fromCodePoint, toCodePoints} from 'css-line-break';
export enum TokenType {
STRING_TOKEN,
BAD_STRING_TOKEN,
LEFT_PARENTHESIS_TOKEN,
RIGHT_PARENTHESIS_TOKEN,
COMMA_TOKEN,
HASH_TOKEN,
DELIM_TOKEN,
AT_KEYWORD_TOKEN,
PREFIX_MATCH_TOKEN,
DASH_MATCH_TOKEN,
INCLUDE_MATCH_TOKEN,
LEFT_CURLY_BRACKET_TOKEN,
RIGHT_CURLY_BRACKET_TOKEN,
SUFFIX_MATCH_TOKEN,
SUBSTRING_MATCH_TOKEN,
DIMENSION_TOKEN,
PERCENTAGE_TOKEN,
NUMBER_TOKEN,
FUNCTION,
FUNCTION_TOKEN,
IDENT_TOKEN,
COLUMN_TOKEN,
URL_TOKEN,
BAD_URL_TOKEN,
CDC_TOKEN,
CDO_TOKEN,
COLON_TOKEN,
SEMICOLON_TOKEN,
LEFT_SQUARE_BRACKET_TOKEN,
RIGHT_SQUARE_BRACKET_TOKEN,
UNICODE_RANGE_TOKEN,
WHITESPACE_TOKEN,
EOF_TOKEN
}
interface IToken {
type: TokenType;
}
export interface Token extends IToken {
type:
| TokenType.BAD_URL_TOKEN
| TokenType.BAD_STRING_TOKEN
| TokenType.LEFT_PARENTHESIS_TOKEN
| TokenType.RIGHT_PARENTHESIS_TOKEN
| TokenType.COMMA_TOKEN
| TokenType.SUBSTRING_MATCH_TOKEN
| TokenType.PREFIX_MATCH_TOKEN
| TokenType.SUFFIX_MATCH_TOKEN
| TokenType.COLON_TOKEN
| TokenType.SEMICOLON_TOKEN
| TokenType.LEFT_SQUARE_BRACKET_TOKEN
| TokenType.RIGHT_SQUARE_BRACKET_TOKEN
| TokenType.LEFT_CURLY_BRACKET_TOKEN
| TokenType.RIGHT_CURLY_BRACKET_TOKEN
| TokenType.DASH_MATCH_TOKEN
| TokenType.INCLUDE_MATCH_TOKEN
| TokenType.COLUMN_TOKEN
| TokenType.WHITESPACE_TOKEN
| TokenType.CDC_TOKEN
| TokenType.CDO_TOKEN
| TokenType.EOF_TOKEN;
}
export interface StringValueToken extends IToken {
type:
| TokenType.STRING_TOKEN
| TokenType.DELIM_TOKEN
| TokenType.FUNCTION_TOKEN
| TokenType.IDENT_TOKEN
| TokenType.URL_TOKEN
| TokenType.AT_KEYWORD_TOKEN;
value: string;
}
export interface HashToken extends IToken {
type: TokenType.HASH_TOKEN;
flags: number;
value: string;
}
export interface NumberValueToken extends IToken {
type: TokenType.PERCENTAGE_TOKEN | TokenType.NUMBER_TOKEN;
flags: number;
number: number;
}
export interface DimensionToken extends IToken {
type: TokenType.DIMENSION_TOKEN;
flags: number;
unit: string;
number: number;
}
export interface UnicodeRangeToken extends IToken {
type: TokenType.UNICODE_RANGE_TOKEN;
start: number;
end: number;
}
export type CSSToken = Token | StringValueToken | NumberValueToken | DimensionToken | UnicodeRangeToken | HashToken;
export const FLAG_UNRESTRICTED = 1 << 0;
export const FLAG_ID = 1 << 1;
export const FLAG_INTEGER = 1 << 2;
export const FLAG_NUMBER = 1 << 3;
const LINE_FEED = 0x000a;
const SOLIDUS = 0x002f;
const REVERSE_SOLIDUS = 0x005c;
const CHARACTER_TABULATION = 0x0009;
const SPACE = 0x0020;
const QUOTATION_MARK = 0x0022;
const EQUALS_SIGN = 0x003d;
const NUMBER_SIGN = 0x0023;
const DOLLAR_SIGN = 0x0024;
const PERCENTAGE_SIGN = 0x0025;
const APOSTROPHE = 0x0027;
const LEFT_PARENTHESIS = 0x0028;
const RIGHT_PARENTHESIS = 0x0029;
const LOW_LINE = 0x005f;
const HYPHEN_MINUS = 0x002d;
const EXCLAMATION_MARK = 0x0021;
const LESS_THAN_SIGN = 0x003c;
const GREATER_THAN_SIGN = 0x003e;
const COMMERCIAL_AT = 0x0040;
const LEFT_SQUARE_BRACKET = 0x005b;
const RIGHT_SQUARE_BRACKET = 0x005d;
const CIRCUMFLEX_ACCENT = 0x003d;
const LEFT_CURLY_BRACKET = 0x007b;
const QUESTION_MARK = 0x003f;
const RIGHT_CURLY_BRACKET = 0x007d;
const VERTICAL_LINE = 0x007c;
const TILDE = 0x007e;
const CONTROL = 0x0080;
const REPLACEMENT_CHARACTER = 0xfffd;
const ASTERISK = 0x002a;
const PLUS_SIGN = 0x002b;
const COMMA = 0x002c;
const COLON = 0x003a;
const SEMICOLON = 0x003b;
const FULL_STOP = 0x002e;
const NULL = 0x0000;
const BACKSPACE = 0x0008;
const LINE_TABULATION = 0x000b;
const SHIFT_OUT = 0x000e;
const INFORMATION_SEPARATOR_ONE = 0x001f;
const DELETE = 0x007f;
const EOF = -1;
const ZERO = 0x0030;
const a = 0x0061;
const e = 0x0065;
const f = 0x0066;
const u = 0x0075;
const z = 0x007a;
const A = 0x0041;
const E = 0x0045;
const F = 0x0046;
const U = 0x0055;
const Z = 0x005a;
const isDigit = (codePoint: number) => codePoint >= ZERO && codePoint <= 0x0039;
const isSurrogateCodePoint = (codePoint: number) => codePoint >= 0xd800 && codePoint <= 0xdfff;
const isHex = (codePoint: number) =>
isDigit(codePoint) || (codePoint >= A && codePoint <= F) || (codePoint >= a && codePoint <= f);
const isLowerCaseLetter = (codePoint: number) => codePoint >= a && codePoint <= z;
const isUpperCaseLetter = (codePoint: number) => codePoint >= A && codePoint <= Z;
const isLetter = (codePoint: number) => isLowerCaseLetter(codePoint) || isUpperCaseLetter(codePoint);
const isNonASCIICodePoint = (codePoint: number) => codePoint >= CONTROL;
const isWhiteSpace = (codePoint: number): boolean =>
codePoint === LINE_FEED || codePoint === CHARACTER_TABULATION || codePoint === SPACE;
const isNameStartCodePoint = (codePoint: number): boolean =>
isLetter(codePoint) || isNonASCIICodePoint(codePoint) || codePoint === LOW_LINE;
const isNameCodePoint = (codePoint: number): boolean =>
isNameStartCodePoint(codePoint) || isDigit(codePoint) || codePoint === HYPHEN_MINUS;
const isNonPrintableCodePoint = (codePoint: number): boolean => {
return (
(codePoint >= NULL && codePoint <= BACKSPACE) ||
codePoint === LINE_TABULATION ||
(codePoint >= SHIFT_OUT && codePoint <= INFORMATION_SEPARATOR_ONE) ||
codePoint === DELETE
);
};
const isValidEscape = (c1: number, c2: number): boolean => {
if (c1 !== REVERSE_SOLIDUS) {
return false;
}
return c2 !== LINE_FEED;
};
const isIdentifierStart = (c1: number, c2: number, c3: number): boolean => {
if (c1 === HYPHEN_MINUS) {
return isNameStartCodePoint(c2) || isValidEscape(c2, c3);
} else if (isNameStartCodePoint(c1)) {
return true;
} else if (c1 === REVERSE_SOLIDUS && isValidEscape(c1, c2)) {
return true;
}
return false;
};
const isNumberStart = (c1: number, c2: number, c3: number): boolean => {
if (c1 === PLUS_SIGN || c1 === HYPHEN_MINUS) {
if (isDigit(c2)) {
return true;
}
return c2 === FULL_STOP && isDigit(c3);
}
if (c1 === FULL_STOP) {
return isDigit(c2);
}
return isDigit(c1);
};
const stringToNumber = (codePoints: number[]): number => {
let c = 0;
let sign = 1;
if (codePoints[c] === PLUS_SIGN || codePoints[c] === HYPHEN_MINUS) {
if (codePoints[c] === HYPHEN_MINUS) {
sign = -1;
}
c++;
}
const integers = [];
while (isDigit(codePoints[c])) {
integers.push(codePoints[c++]);
}
const int = integers.length ? parseInt(fromCodePoint(...integers), 10) : 0;
if (codePoints[c] === FULL_STOP) {
c++;
}
const fraction = [];
while (isDigit(codePoints[c])) {
fraction.push(codePoints[c++]);
}
const fracd = fraction.length;
const frac = fracd ? parseInt(fromCodePoint(...fraction), 10) : 0;
if (codePoints[c] === E || codePoints[c] === e) {
c++;
}
let expsign = 1;
if (codePoints[c] === PLUS_SIGN || codePoints[c] === HYPHEN_MINUS) {
if (codePoints[c] === HYPHEN_MINUS) {
expsign = -1;
}
c++;
}
const exponent = [];
while (isDigit(codePoints[c])) {
exponent.push(codePoints[c++]);
}
const exp = exponent.length ? parseInt(fromCodePoint(...exponent), 10) : 0;
return sign * (int + frac * Math.pow(10, -fracd)) * Math.pow(10, expsign * exp);
};
const LEFT_PARENTHESIS_TOKEN: Token = {
type: TokenType.LEFT_PARENTHESIS_TOKEN
};
const RIGHT_PARENTHESIS_TOKEN: Token = {
type: TokenType.RIGHT_PARENTHESIS_TOKEN
};
const COMMA_TOKEN: Token = {type: TokenType.COMMA_TOKEN};
const SUFFIX_MATCH_TOKEN: Token = {type: TokenType.SUFFIX_MATCH_TOKEN};
const PREFIX_MATCH_TOKEN: Token = {type: TokenType.PREFIX_MATCH_TOKEN};
const COLUMN_TOKEN: Token = {type: TokenType.COLUMN_TOKEN};
const DASH_MATCH_TOKEN: Token = {type: TokenType.DASH_MATCH_TOKEN};
const INCLUDE_MATCH_TOKEN: Token = {type: TokenType.INCLUDE_MATCH_TOKEN};
const LEFT_CURLY_BRACKET_TOKEN: Token = {
type: TokenType.LEFT_CURLY_BRACKET_TOKEN
};
const RIGHT_CURLY_BRACKET_TOKEN: Token = {
type: TokenType.RIGHT_CURLY_BRACKET_TOKEN
};
const SUBSTRING_MATCH_TOKEN: Token = {type: TokenType.SUBSTRING_MATCH_TOKEN};
const BAD_URL_TOKEN: Token = {type: TokenType.BAD_URL_TOKEN};
const BAD_STRING_TOKEN: Token = {type: TokenType.BAD_STRING_TOKEN};
const CDO_TOKEN: Token = {type: TokenType.CDO_TOKEN};
const CDC_TOKEN: Token = {type: TokenType.CDC_TOKEN};
const COLON_TOKEN: Token = {type: TokenType.COLON_TOKEN};
const SEMICOLON_TOKEN: Token = {type: TokenType.SEMICOLON_TOKEN};
const LEFT_SQUARE_BRACKET_TOKEN: Token = {
type: TokenType.LEFT_SQUARE_BRACKET_TOKEN
};
const RIGHT_SQUARE_BRACKET_TOKEN: Token = {
type: TokenType.RIGHT_SQUARE_BRACKET_TOKEN
};
const WHITESPACE_TOKEN: Token = {type: TokenType.WHITESPACE_TOKEN};
export const EOF_TOKEN: Token = {type: TokenType.EOF_TOKEN};
export class Tokenizer {
private _value: number[];
constructor() {
this._value = [];
}
write(chunk: string) {
this._value.push(...toCodePoints(chunk));
}
read(): CSSToken[] {
const tokens = [];
let token = this.consumeToken();
while (token !== EOF_TOKEN) {
tokens.push(token);
token = this.consumeToken();
}
return tokens;
}
private consumeToken(): CSSToken {
const codePoint = this.consumeCodePoint();
switch (codePoint) {
case QUOTATION_MARK:
return this.consumeStringToken(QUOTATION_MARK);
case NUMBER_SIGN:
const c1 = this.peekCodePoint(0);
const c2 = this.peekCodePoint(1);
const c3 = this.peekCodePoint(2);
if (isNameCodePoint(c1) || isValidEscape(c2, c3)) {
const flags = isIdentifierStart(c1, c2, c3) ? FLAG_ID : FLAG_UNRESTRICTED;
const value = this.consumeName();
return {type: TokenType.HASH_TOKEN, value, flags};
}
break;
case DOLLAR_SIGN:
if (this.peekCodePoint(0) === EQUALS_SIGN) {
this.consumeCodePoint();
return SUFFIX_MATCH_TOKEN;
}
break;
case APOSTROPHE:
return this.consumeStringToken(APOSTROPHE);
case LEFT_PARENTHESIS:
return LEFT_PARENTHESIS_TOKEN;
case RIGHT_PARENTHESIS:
return RIGHT_PARENTHESIS_TOKEN;
case ASTERISK:
if (this.peekCodePoint(0) === EQUALS_SIGN) {
this.consumeCodePoint();
return SUBSTRING_MATCH_TOKEN;
}
break;
case PLUS_SIGN:
if (isNumberStart(codePoint, this.peekCodePoint(0), this.peekCodePoint(1))) {
this.reconsumeCodePoint(codePoint);
return this.consumeNumericToken();
}
break;
break;
case COMMA:
return COMMA_TOKEN;
case HYPHEN_MINUS:
const e1 = codePoint;
const e2 = this.peekCodePoint(0);
const e3 = this.peekCodePoint(1);
if (isNumberStart(e1, e2, e3)) {
this.reconsumeCodePoint(codePoint);
return this.consumeNumericToken();
}
if (isIdentifierStart(e1, e2, e3)) {
this.reconsumeCodePoint(codePoint);
return this.consumeIdentLikeToken();
}
if (e2 === HYPHEN_MINUS && e3 === GREATER_THAN_SIGN) {
this.consumeCodePoint();
this.consumeCodePoint();
return CDC_TOKEN;
}
break;
case FULL_STOP:
if (isNumberStart(codePoint, this.peekCodePoint(0), this.peekCodePoint(1))) {
this.reconsumeCodePoint(codePoint);
return this.consumeNumericToken();
}
break;
case SOLIDUS:
if (this.peekCodePoint(0) === ASTERISK) {
this.consumeCodePoint();
while (true) {
let c = this.consumeCodePoint();
if (c === ASTERISK) {
c = this.consumeCodePoint();
if (c === SOLIDUS) {
return this.consumeToken();
}
}
if (c === EOF) {
return this.consumeToken();
}
}
}
break;
case COLON:
return COLON_TOKEN;
case SEMICOLON:
return SEMICOLON_TOKEN;
case LESS_THAN_SIGN:
if (
this.peekCodePoint(0) === EXCLAMATION_MARK &&
this.peekCodePoint(1) === HYPHEN_MINUS &&
this.peekCodePoint(2) === HYPHEN_MINUS
) {
this.consumeCodePoint();
this.consumeCodePoint();
return CDO_TOKEN;
}
break;
case COMMERCIAL_AT:
const a1 = this.peekCodePoint(0);
const a2 = this.peekCodePoint(1);
const a3 = this.peekCodePoint(2);
if (isIdentifierStart(a1, a2, a3)) {
const value = this.consumeName();
return {type: TokenType.AT_KEYWORD_TOKEN, value};
}
break;
case LEFT_SQUARE_BRACKET:
return LEFT_SQUARE_BRACKET_TOKEN;
case REVERSE_SOLIDUS:
if (isValidEscape(codePoint, this.peekCodePoint(0))) {
this.reconsumeCodePoint(codePoint);
return this.consumeIdentLikeToken();
}
break;
case RIGHT_SQUARE_BRACKET:
return RIGHT_SQUARE_BRACKET_TOKEN;
case CIRCUMFLEX_ACCENT:
if (this.peekCodePoint(0) === EQUALS_SIGN) {
this.consumeCodePoint();
return PREFIX_MATCH_TOKEN;
}
break;
case LEFT_CURLY_BRACKET:
return LEFT_CURLY_BRACKET_TOKEN;
case RIGHT_CURLY_BRACKET:
return RIGHT_CURLY_BRACKET_TOKEN;
case u:
case U:
const u1 = this.peekCodePoint(0);
const u2 = this.peekCodePoint(1);
if (u1 === PLUS_SIGN && (isHex(u2) || u2 === QUESTION_MARK)) {
this.consumeCodePoint();
this.consumeUnicodeRangeToken();
}
this.reconsumeCodePoint(codePoint);
return this.consumeIdentLikeToken();
break;
case VERTICAL_LINE:
if (this.peekCodePoint(0) === EQUALS_SIGN) {
this.consumeCodePoint();
return DASH_MATCH_TOKEN;
}
if (this.peekCodePoint(0) === VERTICAL_LINE) {
this.consumeCodePoint();
return COLUMN_TOKEN;
}
break;
case TILDE:
if (this.peekCodePoint(0) === EQUALS_SIGN) {
this.consumeCodePoint();
return INCLUDE_MATCH_TOKEN;
}
break;
case EOF:
return EOF_TOKEN;
}
if (isWhiteSpace(codePoint)) {
this.consumeWhiteSpace();
return WHITESPACE_TOKEN;
}
if (isDigit(codePoint)) {
this.reconsumeCodePoint(codePoint);
return this.consumeNumericToken();
}
if (isNameStartCodePoint(codePoint)) {
this.reconsumeCodePoint(codePoint);
return this.consumeIdentLikeToken();
}
return {type: TokenType.DELIM_TOKEN, value: fromCodePoint(codePoint)};
}
private consumeCodePoint(): number {
const value = this._value.shift();
return typeof value === 'undefined' ? -1 : value;
}
private reconsumeCodePoint(codePoint: number) {
this._value.unshift(codePoint);
}
private peekCodePoint(delta: number): number {
if (delta >= this._value.length) {
return -1;
}
return this._value[delta];
}
private consumeUnicodeRangeToken(): UnicodeRangeToken {
const digits = [];
let codePoint = this.consumeCodePoint();
while (isHex(codePoint) && digits.length < 6) {
digits.push(codePoint);
codePoint = this.consumeCodePoint();
}
let questionMarks = false;
while (codePoint === QUESTION_MARK && digits.length < 6) {
digits.push(codePoint);
codePoint = this.consumeCodePoint();
questionMarks = true;
}
if (questionMarks) {
const start = parseInt(fromCodePoint(...digits.map(digit => (digit === QUESTION_MARK ? ZERO : digit))), 16);
const end = parseInt(fromCodePoint(...digits.map(digit => (digit === QUESTION_MARK ? F : digit))), 16);
return {type: TokenType.UNICODE_RANGE_TOKEN, start, end};
}
const start = parseInt(fromCodePoint(...digits), 16);
if (this.peekCodePoint(0) === HYPHEN_MINUS && isHex(this.peekCodePoint(1))) {
this.consumeCodePoint();
codePoint = this.consumeCodePoint();
const endDigits = [];
while (isHex(codePoint) && endDigits.length < 6) {
endDigits.push(codePoint);
codePoint = this.consumeCodePoint();
}
const end = parseInt(fromCodePoint(...endDigits), 16);
return {type: TokenType.UNICODE_RANGE_TOKEN, start, end};
} else {
return {type: TokenType.UNICODE_RANGE_TOKEN, start, end: start};
}
}
private consumeIdentLikeToken(): StringValueToken | Token {
const value = this.consumeName();
if (value.toLowerCase() === 'url' && this.peekCodePoint(0) === LEFT_PARENTHESIS) {
this.consumeCodePoint();
return this.consumeUrlToken();
} else if (this.peekCodePoint(0) === LEFT_PARENTHESIS) {
this.consumeCodePoint();
return {type: TokenType.FUNCTION_TOKEN, value};
}
return {type: TokenType.IDENT_TOKEN, value};
}
private consumeUrlToken(): StringValueToken | Token {
const value = [];
this.consumeWhiteSpace();
if (this.peekCodePoint(0) === EOF) {
return {type: TokenType.URL_TOKEN, value: ''};
}
const next = this.peekCodePoint(0);
if (next === APOSTROPHE || next === QUOTATION_MARK) {
const stringToken = this.consumeStringToken(this.consumeCodePoint());
if (stringToken.type === TokenType.STRING_TOKEN) {
this.consumeWhiteSpace();
if (this.peekCodePoint(0) === EOF || this.peekCodePoint(0) === RIGHT_PARENTHESIS) {
this.consumeCodePoint();
return {type: TokenType.URL_TOKEN, value: stringToken.value};
}
}
this.consumeBadUrlRemnants();
return BAD_URL_TOKEN;
}
while (true) {
const codePoint = this.consumeCodePoint();
if (codePoint === EOF || codePoint === RIGHT_PARENTHESIS) {
return {type: TokenType.URL_TOKEN, value: fromCodePoint(...value)};
} else if (isWhiteSpace(codePoint)) {
this.consumeWhiteSpace();
if (this.peekCodePoint(0) === EOF || this.peekCodePoint(0) === RIGHT_PARENTHESIS) {
this.consumeCodePoint();
return {type: TokenType.URL_TOKEN, value: fromCodePoint(...value)};
}
this.consumeBadUrlRemnants();
return BAD_URL_TOKEN;
} else if (
codePoint === QUOTATION_MARK ||
codePoint === APOSTROPHE ||
codePoint === LEFT_PARENTHESIS ||
isNonPrintableCodePoint(codePoint)
) {
this.consumeBadUrlRemnants();
return BAD_URL_TOKEN;
} else if (codePoint === REVERSE_SOLIDUS) {
if (isValidEscape(codePoint, this.peekCodePoint(0))) {
value.push(this.consumeEscapedCodePoint());
} else {
this.consumeBadUrlRemnants();
return BAD_URL_TOKEN;
}
} else {
value.push(codePoint);
}
}
}
private consumeWhiteSpace(): void {
while (isWhiteSpace(this.peekCodePoint(0))) {
this.consumeCodePoint();
}
}
private consumeBadUrlRemnants(): void {
while (true) {
let codePoint = this.consumeCodePoint();
if (codePoint === RIGHT_PARENTHESIS || codePoint === EOF) {
return;
}
if (isValidEscape(codePoint, this.peekCodePoint(0))) {
this.consumeEscapedCodePoint();
}
}
}
private consumeStringToken(endingCodePoint: number): StringValueToken | Token {
let value = '';
do {
const codePoint = this.consumeCodePoint();
if (codePoint === EOF || codePoint === endingCodePoint) {
return {type: TokenType.STRING_TOKEN, value};
}
if (codePoint === LINE_FEED) {
this.reconsumeCodePoint(codePoint);
return BAD_STRING_TOKEN;
}
if (codePoint === REVERSE_SOLIDUS) {
const next = this.peekCodePoint(0);
if (next !== EOF) {
if (next === LINE_FEED) {
this.consumeCodePoint();
} else if (isValidEscape(codePoint, next)) {
value += fromCodePoint(this.consumeEscapedCodePoint());
}
}
} else {
value += fromCodePoint(codePoint);
}
} while (true);
}
private consumeNumber() {
let repr = [];
let type = FLAG_INTEGER;
let c1 = this.peekCodePoint(0);
if (c1 === PLUS_SIGN || c1 === HYPHEN_MINUS) {
repr.push(this.consumeCodePoint());
}
while (isDigit(this.peekCodePoint(0))) {
repr.push(this.consumeCodePoint());
}
c1 = this.peekCodePoint(0);
let c2 = this.peekCodePoint(1);
if (c1 === FULL_STOP && isDigit(c2)) {
repr.push(this.consumeCodePoint(), this.consumeCodePoint());
type = FLAG_NUMBER;
while (isDigit(this.peekCodePoint(0))) {
repr.push(this.consumeCodePoint());
}
}
c1 = this.peekCodePoint(0);
c2 = this.peekCodePoint(1);
let c3 = this.peekCodePoint(2);
if ((c1 === E || c1 === e) && (((c2 === PLUS_SIGN || c2 === HYPHEN_MINUS) && isDigit(c3)) || isDigit(c2))) {
repr.push(this.consumeCodePoint(), this.consumeCodePoint());
type = FLAG_NUMBER;
while (isDigit(this.peekCodePoint(0))) {
repr.push(this.consumeCodePoint());
}
}
return [stringToNumber(repr), type];
}
private consumeNumericToken(): NumberValueToken | DimensionToken {
const [number, flags] = this.consumeNumber();
const c1 = this.peekCodePoint(0);
const c2 = this.peekCodePoint(1);
const c3 = this.peekCodePoint(2);
if (isIdentifierStart(c1, c2, c3)) {
let unit = this.consumeName();
return {type: TokenType.DIMENSION_TOKEN, number, flags, unit};
}
if (c1 === PERCENTAGE_SIGN) {
this.consumeCodePoint();
return {type: TokenType.PERCENTAGE_TOKEN, number, flags};
}
return {type: TokenType.NUMBER_TOKEN, number, flags};
}
private consumeEscapedCodePoint(): number {
const codePoint = this.consumeCodePoint();
if (isHex(codePoint)) {
let hex = fromCodePoint(codePoint);
while (isHex(this.peekCodePoint(0)) && hex.length < 6) {
hex += fromCodePoint(this.consumeCodePoint());
}
if (isWhiteSpace(this.peekCodePoint(0))) {
this.consumeCodePoint();
}
const hexCodePoint = parseInt(hex, 16);
if (hexCodePoint === 0 || isSurrogateCodePoint(hexCodePoint) || hexCodePoint > 0x10ffff) {
return REPLACEMENT_CHARACTER;
}
return hexCodePoint;
}
if (codePoint === EOF) {
return REPLACEMENT_CHARACTER;
}
return codePoint;
}
private consumeName(): string {
let result = '';
while (true) {
const codePoint = this.consumeCodePoint();
if (isNameCodePoint(codePoint)) {
result += fromCodePoint(codePoint);
} else if (isValidEscape(codePoint, this.peekCodePoint(0))) {
result += fromCodePoint(this.consumeEscapedCodePoint());
} else {
this.reconsumeCodePoint(codePoint);
return result;
}
}
}
}

View File

@ -0,0 +1,63 @@
import {strictEqual} from 'assert';
import {asString, color, isTransparent, pack} from '../color';
import {Parser} from '../../syntax/parser';
const parse = (value: string) => color.parse(Parser.parseValue(value));
describe('types', () => {
describe('<color>', () => {
describe('parsing', () => {
it('#000', () => strictEqual(parse('#000'), pack(0, 0, 0, 1)));
it('#0000', () => strictEqual(parse('#0000'), pack(0, 0, 0, 0)));
it('#000f', () => strictEqual(parse('#000f'), pack(0, 0, 0, 1)));
it('#fff', () => strictEqual(parse('#fff'), pack(255, 255, 255, 1)));
it('#000000', () => strictEqual(parse('#000000'), pack(0, 0, 0, 1)));
it('#00000000', () => strictEqual(parse('#00000000'), pack(0, 0, 0, 0)));
it('#ffffff', () => strictEqual(parse('#ffffff'), pack(255, 255, 255, 1)));
it('#ffffffff', () => strictEqual(parse('#ffffffff'), pack(255, 255, 255, 1)));
it('#7FFFD4', () => strictEqual(parse('#7FFFD4'), pack(127, 255, 212, 1)));
it('#f0ffff', () => strictEqual(parse('#f0ffff'), pack(240, 255, 255, 1)));
it('transparent', () => strictEqual(parse('transparent'), pack(0, 0, 0, 0)));
it('bisque', () => strictEqual(parse('bisque'), pack(255, 228, 196, 1)));
it('BLUE', () => strictEqual(parse('BLUE'), pack(0, 0, 255, 1)));
it('rgb(1, 3, 5)', () => strictEqual(parse('rgb(1, 3, 5)'), pack(1, 3, 5, 1)));
it('rgb(0% 0% 0%)', () => strictEqual(parse('rgb(0% 0% 0%)'), pack(0, 0, 0, 1)));
it('rgb(50% 50% 50%)', () => strictEqual(parse('rgb(50% 50% 50%)'), pack(128, 128, 128, 1)));
it('rgba(50% 50% 50% 50%)', () => strictEqual(parse('rgba(50% 50% 50% 50%)'), pack(128, 128, 128, 0.5)));
it('rgb(100% 100% 100%)', () => strictEqual(parse('rgb(100% 100% 100%)'), pack(255, 255, 255, 1)));
it('rgb(222 111 50)', () => strictEqual(parse('rgb(222 111 50)'), pack(222, 111, 50, 1)));
it('rgba(200, 3, 5, 1)', () => strictEqual(parse('rgba(200, 3, 5, 1)'), pack(200, 3, 5, 1)));
it('rgba(222, 111, 50, 0.22)', () =>
strictEqual(parse('rgba(222, 111, 50, 0.22)'), pack(222, 111, 50, 0.22)));
it('rgba(222 111 50 0.123)', () => strictEqual(parse('rgba(222 111 50 0.123)'), pack(222, 111, 50, 0.123)));
it('hsl(270,60%,70%)', () => strictEqual(parse('hsl(270,60%,70%)'), parse('rgb(178,132,224)')));
it('hsl(270, 60%, 70%)', () => strictEqual(parse('hsl(270, 60%, 70%)'), parse('rgb(178,132,224)')));
it('hsl(270 60% 70%)', () => strictEqual(parse('hsl(270 60% 70%)'), parse('rgb(178,132,224)')));
it('hsl(270deg, 60%, 70%)', () => strictEqual(parse('hsl(270deg, 60%, 70%)'), parse('rgb(178,132,224)')));
it('hsl(4.71239rad, 60%, 70%)', () =>
strictEqual(parse('hsl(4.71239rad, 60%, 70%)'), parse('rgb(178,132,224)')));
it('hsl(.75turn, 60%, 70%)', () => strictEqual(parse('hsl(.75turn, 60%, 70%)'), parse('rgb(178,132,224)')));
it('hsla(.75turn, 60%, 70%, 50%)', () =>
strictEqual(parse('hsl(.75turn, 60%, 70%, 50%)'), parse('rgba(178,132,224, 0.5)')));
});
describe('util', () => {
describe('isTransparent', () => {
it('transparent', () => strictEqual(isTransparent(parse('transparent')), true));
it('#000', () => strictEqual(isTransparent(parse('#000')), false));
it('#000f', () => strictEqual(isTransparent(parse('#000f')), false));
it('#0001', () => strictEqual(isTransparent(parse('#0001')), false));
it('#0000', () => strictEqual(isTransparent(parse('#0000')), true));
});
describe('toString', () => {
it('transparent', () => strictEqual(asString(parse('transparent')), 'rgba(0,0,0,0)'));
it('#000', () => strictEqual(asString(parse('#000')), 'rgb(0,0,0)'));
it('#000f', () => strictEqual(asString(parse('#000f')), 'rgb(0,0,0)'));
it('#000f', () => strictEqual(asString(parse('#000c')), 'rgba(0,0,0,0.8)'));
it('#fff', () => strictEqual(asString(parse('#fff')), 'rgb(255,255,255)'));
it('#ffff', () => strictEqual(asString(parse('#ffff')), 'rgb(255,255,255)'));
it('#fffc', () => strictEqual(asString(parse('#fffc')), 'rgba(255,255,255,0.8)'));
});
});
});
});

View File

@ -0,0 +1,203 @@
import {deepStrictEqual} from 'assert';
import {Parser} from '../../syntax/parser';
import {CSSImageType, image} from '../image';
import {color, pack} from '../color';
import {FLAG_INTEGER, TokenType} from '../../syntax/tokenizer';
import {deg} from '../angle';
const parse = (value: string) => image.parse(Parser.parseValue(value));
const colorParse = (value: string) => color.parse(Parser.parseValue(value));
describe('types', () => {
describe('<image>', () => {
describe('parsing', () => {
describe('url', () => {
it('url(test.jpg)', () =>
deepStrictEqual(parse('url(http://example.com/test.jpg)'), {
url: 'http://example.com/test.jpg',
type: CSSImageType.URL
}));
it('url("test.jpg")', () =>
deepStrictEqual(parse('url("http://example.com/test.jpg")'), {
url: 'http://example.com/test.jpg',
type: CSSImageType.URL
}));
});
describe('linear-gradient', () => {
it('linear-gradient(#f69d3c, #3f87a6)', () =>
deepStrictEqual(parse('linear-gradient(#f69d3c, #3f87a6)'), {
angle: deg(180),
type: CSSImageType.LINEAR_GRADIENT,
stops: [
{color: pack(0xf6, 0x9d, 0x3c, 1), stop: null},
{color: pack(0x3f, 0x87, 0xa6, 1), stop: null}
]
}));
it('linear-gradient(yellow, blue)', () =>
deepStrictEqual(parse('linear-gradient(yellow, blue)'), {
angle: deg(180),
type: CSSImageType.LINEAR_GRADIENT,
stops: [{color: colorParse('yellow'), stop: null}, {color: colorParse('blue'), stop: null}]
}));
it('linear-gradient(to bottom, yellow, blue)', () =>
deepStrictEqual(parse('linear-gradient(to bottom, yellow, blue)'), {
angle: deg(180),
type: CSSImageType.LINEAR_GRADIENT,
stops: [{color: colorParse('yellow'), stop: null}, {color: colorParse('blue'), stop: null}]
}));
it('linear-gradient(180deg, yellow, blue)', () =>
deepStrictEqual(parse('linear-gradient(180deg, yellow, blue)'), {
angle: deg(180),
type: CSSImageType.LINEAR_GRADIENT,
stops: [{color: colorParse('yellow'), stop: null}, {color: colorParse('blue'), stop: null}]
}));
it('linear-gradient(to top, blue, yellow)', () =>
deepStrictEqual(parse('linear-gradient(to top, blue, yellow)'), {
angle: 0,
type: CSSImageType.LINEAR_GRADIENT,
stops: [{color: colorParse('blue'), stop: null}, {color: colorParse('yellow'), stop: null}]
}));
it('linear-gradient(to top right, blue, yellow)', () =>
deepStrictEqual(parse('linear-gradient(to top right, blue, yellow)'), {
angle: [
{type: TokenType.PERCENTAGE_TOKEN, number: 100, flags: 4},
{type: TokenType.NUMBER_TOKEN, number: 0, flags: 4}
],
type: CSSImageType.LINEAR_GRADIENT,
stops: [{color: colorParse('blue'), stop: null}, {color: colorParse('yellow'), stop: null}]
}));
it('linear-gradient(to bottom, yellow 0%, blue 100%)', () =>
deepStrictEqual(parse('linear-gradient(to bottom, yellow 0%, blue 100%)'), {
angle: deg(180),
type: CSSImageType.LINEAR_GRADIENT,
stops: [
{
color: colorParse('yellow'),
stop: {
type: TokenType.PERCENTAGE_TOKEN,
number: 0,
flags: FLAG_INTEGER
}
},
{
color: colorParse('blue'),
stop: {
type: TokenType.PERCENTAGE_TOKEN,
number: 100,
flags: FLAG_INTEGER
}
}
]
}));
it('linear-gradient(to top left, lightpink, lightpink 5px, white 5px, white 10px)', () =>
deepStrictEqual(
parse('linear-gradient(to top left, lightpink, lightpink 5px, white 5px, white 10px)'),
{
angle: [
{type: TokenType.PERCENTAGE_TOKEN, number: 100, flags: 4},
{type: TokenType.PERCENTAGE_TOKEN, number: 100, flags: 4}
],
type: CSSImageType.LINEAR_GRADIENT,
stops: [
{color: colorParse('lightpink'), stop: null},
{
color: colorParse('lightpink'),
stop: {
type: TokenType.DIMENSION_TOKEN,
number: 5,
flags: FLAG_INTEGER,
unit: 'px'
}
},
{
color: colorParse('white'),
stop: {
type: TokenType.DIMENSION_TOKEN,
number: 5,
flags: FLAG_INTEGER,
unit: 'px'
}
},
{
color: colorParse('white'),
stop: {
type: TokenType.DIMENSION_TOKEN,
number: 10,
flags: FLAG_INTEGER,
unit: 'px'
}
}
]
}
));
});
describe('-prefix-linear-gradient', () => {
it('-webkit-linear-gradient(left, #cedbe9 0%, #aac5de 17%, #3a8bc2 84%, #26558b 100%)', () =>
deepStrictEqual(
parse('-webkit-linear-gradient(left, #cedbe9 0%, #aac5de 17%, #3a8bc2 84%, #26558b 100%)'),
{
angle: deg(90),
type: CSSImageType.LINEAR_GRADIENT,
stops: [
{
color: colorParse('#cedbe9'),
stop: {
type: TokenType.PERCENTAGE_TOKEN,
number: 0,
flags: FLAG_INTEGER
}
},
{
color: colorParse('#aac5de'),
stop: {
type: TokenType.PERCENTAGE_TOKEN,
number: 17,
flags: FLAG_INTEGER
}
},
{
color: colorParse('#3a8bc2'),
stop: {
type: TokenType.PERCENTAGE_TOKEN,
number: 84,
flags: FLAG_INTEGER
}
},
{
color: colorParse('#26558b'),
stop: {
type: TokenType.PERCENTAGE_TOKEN,
number: 100,
flags: FLAG_INTEGER
}
}
]
}
));
it('-moz-linear-gradient(top, #cce5f4 0%, #00263c 100%)', () =>
deepStrictEqual(parse('-moz-linear-gradient(top, #cce5f4 0%, #00263c 100%)'), {
angle: deg(180),
type: CSSImageType.LINEAR_GRADIENT,
stops: [
{
color: colorParse('#cce5f4'),
stop: {
type: TokenType.PERCENTAGE_TOKEN,
number: 0,
flags: FLAG_INTEGER
}
},
{
color: colorParse('#00263c'),
stop: {
type: TokenType.PERCENTAGE_TOKEN,
number: 100,
flags: FLAG_INTEGER
}
}
]
}));
});
});
});
});

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