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:
parent
20a797cbeb
commit
522a443055
34
.eslintrc
34
.eslintrc
|
@ -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": [
|
"plugins": [
|
||||||
"flowtype",
|
"@typescript-eslint",
|
||||||
"prettier"
|
"prettier"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-console": ["error", { "allow": ["warn", "error"] }],
|
"no-console": ["error", { "allow": ["warn", "error"] }],
|
||||||
"flowtype/boolean-style": [
|
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
|
||||||
2,
|
"@typescript-eslint/interface-name-prefix": "off",
|
||||||
"boolean"
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
],
|
"@typescript-eslint/no-use-before-define": "off",
|
||||||
"flowtype/no-weak-types": 2,
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
"flowtype/delimiter-dangle": 2,
|
"@typescript-eslint/class-name-casing": "off",
|
||||||
"prettier/prettier": ["error", {
|
"prettier/prettier": "error"
|
||||||
"singleQuote": true,
|
|
||||||
"bracketSpacing": false,
|
|
||||||
"parser": "flow",
|
|
||||||
"tabWidth": 4,
|
|
||||||
"printWidth": 100
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
[ignore]
|
|
||||||
.*/www/.*
|
|
||||||
.*/node_modules/@webassemblyjs/.*
|
|
||||||
[include]
|
|
||||||
[libs]
|
|
||||||
./flow-typed
|
|
||||||
[options]
|
|
||||||
[lints]
|
|
|
@ -16,3 +16,4 @@ npm-debug.log
|
||||||
debug.log
|
debug.log
|
||||||
tests/reftests.js
|
tests/reftests.js
|
||||||
*.log
|
*.log
|
||||||
|
.rpt2_cache
|
||||||
|
|
|
@ -19,3 +19,4 @@ karma.js
|
||||||
karma.conf.js
|
karma.conf.js
|
||||||
rollup.config.js
|
rollup.config.js
|
||||||
webpack.config.js
|
webpack.config.js
|
||||||
|
.rpt2_cache
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "none",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"bracketSpacing": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
|
@ -51,9 +51,7 @@ jobs:
|
||||||
displayName: Build
|
displayName: Build
|
||||||
- script: npm run lint
|
- script: npm run lint
|
||||||
displayName: Lint
|
displayName: Lint
|
||||||
- script: npm run flow
|
- script: npm run unittest
|
||||||
displayName: Flow
|
|
||||||
- script: npm run test:node
|
|
||||||
displayName: Unit tests
|
displayName: Unit tests
|
||||||
|
|
||||||
- template: ci/browser-tests.yml
|
- template: ci/browser-tests.yml
|
||||||
|
@ -88,10 +86,10 @@ jobs:
|
||||||
|
|
||||||
- template: ci/browser-tests.yml
|
- template: ci/browser-tests.yml
|
||||||
parameters:
|
parameters:
|
||||||
name: Browser_Tests_OSX_Safari_IOS_11
|
name: Browser_Tests_OSX_Safari_IOS_12
|
||||||
displayName: iOS Simulator Safari 11
|
displayName: iOS Simulator Safari 12
|
||||||
vmImage: 'macOS-10.13'
|
vmImage: 'macOS-10.13'
|
||||||
targetBrowser: Safari_IOS_11
|
targetBrowser: Safari_IOS_12
|
||||||
|
|
||||||
- template: ci/browser-tests.yml
|
- template: ci/browser-tests.yml
|
||||||
parameters:
|
parameters:
|
||||||
|
@ -130,7 +128,7 @@ jobs:
|
||||||
- Browser_Tests_Linux_Chrome_Stable
|
- Browser_Tests_Linux_Chrome_Stable
|
||||||
- Browser_Tests_OSX_Safari_IOS_9
|
- Browser_Tests_OSX_Safari_IOS_9
|
||||||
- Browser_Tests_OSX_Safari_IOS_10
|
- 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_OSX_Safari_Stable
|
||||||
- Browser_Tests_Windows_IE9
|
- Browser_Tests_Windows_IE9
|
||||||
- Browser_Tests_Windows_IE10
|
- Browser_Tests_Windows_IE10
|
||||||
|
@ -153,7 +151,7 @@ jobs:
|
||||||
inputs:
|
inputs:
|
||||||
artifactName: dist
|
artifactName: dist
|
||||||
downloadPath: $(System.DefaultWorkingDirectory)
|
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
|
displayName: Copy reftests to docs website
|
||||||
- script: cp -R dist ./www/static/dist
|
- script: cp -R dist ./www/static/dist
|
||||||
displayName: Copy dist to docs website
|
displayName: Copy dist to docs website
|
||||||
|
|
|
@ -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 {}
|
|
|
@ -20,10 +20,10 @@ module.exports = function(config) {
|
||||||
name: 'iPhone 5s',
|
name: 'iPhone 5s',
|
||||||
sdk: '10.0'
|
sdk: '10.0'
|
||||||
},
|
},
|
||||||
Safari_IOS_11: {
|
Safari_IOS_12: {
|
||||||
base: 'MobileSafari',
|
base: 'MobileSafari',
|
||||||
name: 'iPhone 5s',
|
name: 'iPhone 5s',
|
||||||
sdk: '11.4'
|
sdk: '12.1'
|
||||||
},
|
},
|
||||||
SauceLabs_IE9: {
|
SauceLabs_IE9: {
|
||||||
base: 'SauceLabs',
|
base: 'SauceLabs',
|
||||||
|
@ -132,6 +132,7 @@ module.exports = function(config) {
|
||||||
|
|
||||||
if (!d) {
|
if (!d) {
|
||||||
log.error(`No device found for sdk ${args.sdk} with name ${args.name}`);
|
log.error(`No device found for sdk ${args.sdk} with name ${args.name}`);
|
||||||
|
log.info(`Available devices:`, devices);
|
||||||
this._process.kill();
|
this._process.kill();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -172,7 +173,6 @@ module.exports = function(config) {
|
||||||
// list of files / patterns to load in the browser
|
// list of files / patterns to load in the browser
|
||||||
files: [
|
files: [
|
||||||
'build/testrunner.js',
|
'build/testrunner.js',
|
||||||
'build/RefTestRenderer.js',
|
|
||||||
{ pattern: './tests/**/*', 'watched': true, 'included': false, 'served': true},
|
{ pattern: './tests/**/*', 'watched': true, 'included': false, 'served': true},
|
||||||
{ pattern: './dist/**/*', 'watched': true, 'included': false, 'served': true},
|
{ pattern: './dist/**/*', 'watched': true, 'included': false, 'served': true},
|
||||||
{ pattern: './node_modules/**/*', 'watched': true, 'included': false, 'served': true},
|
{ pattern: './node_modules/**/*', 'watched': true, 'included': false, 'served': true},
|
||||||
|
|
|
@ -1487,30 +1487,135 @@
|
||||||
"integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==",
|
"integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==",
|
||||||
"dev": true
|
"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": {
|
"@types/core-js": {
|
||||||
"version": "0.9.46",
|
"version": "0.9.46",
|
||||||
"resolved": "https://registry.npmjs.org/@types/core-js/-/core-js-0.9.46.tgz",
|
"resolved": "https://registry.npmjs.org/@types/core-js/-/core-js-0.9.46.tgz",
|
||||||
"integrity": "sha512-LooLR6XHes9V+kNYRz1Qm8w3atw9QMn7XeZUmIpUelllF9BdryeUKd/u0Wh5ErcjpWfG39NrToU9MF7ngsTFVw==",
|
"integrity": "sha512-LooLR6XHes9V+kNYRz1Qm8w3atw9QMn7XeZUmIpUelllF9BdryeUKd/u0Wh5ErcjpWfG39NrToU9MF7ngsTFVw==",
|
||||||
"dev": true
|
"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": {
|
"@types/mkdirp": {
|
||||||
"version": "0.3.29",
|
"version": "0.3.29",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-0.3.29.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-0.3.29.tgz",
|
||||||
"integrity": "sha1-fyrX7FX5FEgvybHsS7GuYCjUYGY=",
|
"integrity": "sha1-fyrX7FX5FEgvybHsS7GuYCjUYGY=",
|
||||||
"dev": true
|
"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": {
|
"@types/node": {
|
||||||
"version": "11.13.2",
|
"version": "11.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.2.tgz",
|
||||||
"integrity": "sha512-HOtU5KqROKT7qX/itKHuTtt5fV0iXbheQvrgbLNXFJQBY/eh+VS5vmmTAVlo3qIGMsypm0G4N1t2AXjy1ZicaQ==",
|
"integrity": "sha512-HOtU5KqROKT7qX/itKHuTtt5fV0iXbheQvrgbLNXFJQBY/eh+VS5vmmTAVlo3qIGMsypm0G4N1t2AXjy1ZicaQ==",
|
||||||
"dev": true
|
"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": {
|
"@types/rimraf": {
|
||||||
"version": "0.0.28",
|
"version": "0.0.28",
|
||||||
"resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-0.0.28.tgz",
|
"resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-0.0.28.tgz",
|
||||||
"integrity": "sha1-VWJRm8eWPKyoq/fxKMrjtZTUHQY=",
|
"integrity": "sha1-VWJRm8eWPKyoq/fxKMrjtZTUHQY=",
|
||||||
"dev": true
|
"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": {
|
"@webassemblyjs/ast": {
|
||||||
"version": "1.8.5",
|
"version": "1.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
|
||||||
|
@ -2755,9 +2860,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"base64-arraybuffer": {
|
"base64-arraybuffer": {
|
||||||
"version": "0.1.5",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz",
|
||||||
"integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
|
"integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ=="
|
||||||
},
|
},
|
||||||
"base64-js": {
|
"base64-js": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
|
@ -3063,6 +3168,12 @@
|
||||||
"integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
|
"integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
|
||||||
"dev": true
|
"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": {
|
"builtin-status-codes": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
|
||||||
|
@ -4559,11 +4670,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"css-line-break": {
|
"css-line-break": {
|
||||||
"version": "1.0.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz",
|
||||||
"integrity": "sha1-GfIGOjPpX7KDG4ZEbAuAwYivRQo=",
|
"integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"base64-arraybuffer": "^0.1.5"
|
"base64-arraybuffer": "^0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"csv-parser": {
|
"csv-parser": {
|
||||||
|
@ -5135,6 +5246,14 @@
|
||||||
"base64-arraybuffer": "0.1.5",
|
"base64-arraybuffer": "0.1.5",
|
||||||
"blob": "0.0.5",
|
"blob": "0.0.5",
|
||||||
"has-binary2": "~1.0.2"
|
"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": {
|
"enhanced-resolve": {
|
||||||
|
@ -5295,26 +5414,6 @@
|
||||||
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
|
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
|
||||||
"dev": true
|
"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": {
|
"cross-spawn": {
|
||||||
"version": "6.0.5",
|
"version": "6.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||||
|
@ -5337,50 +5436,18 @@
|
||||||
"ms": "^2.1.1"
|
"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": {
|
"fast-deep-equal": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
|
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
|
||||||
"dev": true
|
"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": {
|
"json-schema-traverse": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||||
"dev": true
|
"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": {
|
"ms": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||||
|
@ -5401,35 +5468,33 @@
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-regex": "^3.0.0"
|
"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": {
|
"eslint-config-prettier": {
|
||||||
"version": "2.35.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.35.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-4.2.0.tgz",
|
||||||
"integrity": "sha512-zjXGjOsHds8b84C0Ad3VViKh+sUA9PeXKHwPRlSLwwSX0v1iUJf/6IX7wxc+w2T2tnDH8PT6B/YgtcEuNI3ssA==",
|
"integrity": "sha512-y0uWc/FRfrHhpPZCYflWC8aE0KRJRY04rdZVfl8cL3sEZmOYyaBdhdlQPjKZBnuRMyLVK+JUZr7HaZFClQiH4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"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": {
|
"eslint-plugin-prettier": {
|
||||||
"version": "2.1.2",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.0.1.tgz",
|
||||||
"integrity": "sha1-S5D07n+Sv74ukmAX4cpA62KJZeo=",
|
"integrity": "sha512-/PMttrarPAY78PLvV3xfWibMOdMDl57hmlQ2XqFeA37wd+CJ7WSxV7txqjVPHi/AAFKd2lX0ZqfsOc/i5yFCSQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"fast-diff": "^1.1.1",
|
"prettier-linter-helpers": "^1.0.0"
|
||||||
"jest-docblock": "^20.0.1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eslint-scope": {
|
"eslint-scope": {
|
||||||
|
@ -5463,14 +5528,6 @@
|
||||||
"acorn": "^6.0.7",
|
"acorn": "^6.0.7",
|
||||||
"acorn-jsx": "^5.0.0",
|
"acorn-jsx": "^5.0.0",
|
||||||
"eslint-visitor-keys": "^1.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": {
|
"esprima": {
|
||||||
|
@ -5503,6 +5560,12 @@
|
||||||
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
|
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
|
||||||
"dev": true
|
"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": {
|
"esutils": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
|
||||||
|
@ -5855,9 +5918,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"fast-diff": {
|
"fast-diff": {
|
||||||
"version": "1.1.2",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
|
||||||
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
|
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"fast-json-stable-stringify": {
|
"fast-json-stable-stringify": {
|
||||||
|
@ -6385,20 +6448,6 @@
|
||||||
"write": "1.0.3"
|
"write": "1.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": {
|
"rimraf": {
|
||||||
"version": "2.6.3",
|
"version": "2.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
|
||||||
|
@ -6416,12 +6465,6 @@
|
||||||
"integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==",
|
"integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==",
|
||||||
"dev": true
|
"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": {
|
"flush-write-stream": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz",
|
||||||
|
@ -7499,9 +7542,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
|
||||||
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"fs.realpath": "^1.0.0",
|
"fs.realpath": "^1.0.0",
|
||||||
|
@ -7599,6 +7642,12 @@
|
||||||
"which": "^1.2.14"
|
"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": {
|
"graceful-fs": {
|
||||||
"version": "4.1.11",
|
"version": "4.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
|
||||||
|
@ -7612,9 +7661,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"handlebars": {
|
"handlebars": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
|
||||||
"integrity": "sha512-3Zhi6C0euYZL5sM0Zcy7lInLXKQ+YLcF/olbN010mzGQ4XVm50JeyBnMqofHh696GrciGruC7kCcApPDJvVgwA==",
|
"integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"neo-async": "^2.6.0",
|
"neo-async": "^2.6.0",
|
||||||
|
@ -7623,19 +7672,6 @@
|
||||||
"uglify-js": "^3.1.4"
|
"uglify-js": "^3.1.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": {
|
"source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
@ -7643,13 +7679,13 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"uglify-js": {
|
"uglify-js": {
|
||||||
"version": "3.5.3",
|
"version": "3.5.9",
|
||||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.9.tgz",
|
||||||
"integrity": "sha512-rIQPT2UMDnk4jRX+w4WO84/pebU2jiLsjgIyrCktYgSvx28enOE3iYQMr+BD1rHiitWnDmpu0cY/LfIEpKcjcw==",
|
"integrity": "sha512-WpT0RqsDtAWPNJK955DEnb6xjymR8Fn0OlK4TT4pS0ASYsVPqr5ELhgwOwLCP5J5vHeJ4xmMmz3DEgdqC10JeQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"commander": "~2.19.0",
|
"commander": "~2.20.0",
|
||||||
"source-map": "~0.6.1"
|
"source-map": "~0.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8101,9 +8137,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"inquirer": {
|
"inquirer": {
|
||||||
"version": "6.2.2",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.3.1.tgz",
|
||||||
"integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==",
|
"integrity": "sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-escapes": "^3.2.0",
|
"ansi-escapes": "^3.2.0",
|
||||||
|
@ -8117,7 +8153,7 @@
|
||||||
"run-async": "^2.2.0",
|
"run-async": "^2.2.0",
|
||||||
"rxjs": "^6.4.0",
|
"rxjs": "^6.4.0",
|
||||||
"string-width": "^2.1.0",
|
"string-width": "^2.1.0",
|
||||||
"strip-ansi": "^5.0.0",
|
"strip-ansi": "^5.1.0",
|
||||||
"through": "^2.3.6"
|
"through": "^2.3.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -8127,32 +8163,6 @@
|
||||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
|
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
|
||||||
"dev": true
|
"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": {
|
"strip-ansi": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
|
||||||
|
@ -8161,15 +8171,6 @@
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-regex": "^4.1.0"
|
"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=",
|
"integrity": "sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU=",
|
||||||
"dev": true
|
"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": {
|
"is-negated-glob": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz",
|
||||||
|
@ -8534,12 +8541,6 @@
|
||||||
"is-object": "^1.0.1"
|
"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": {
|
"jimp": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/jimp/-/jimp-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/jimp/-/jimp-0.6.1.tgz",
|
||||||
|
@ -8566,9 +8567,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"jquery": {
|
"jquery": {
|
||||||
"version": "3.2.1",
|
"version": "3.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.0.tgz",
|
||||||
"integrity": "sha1-XE2d5lKvbNCncBVKYxu6ErAVx4c=",
|
"integrity": "sha512-ggRCXln9zEqv6OqAGXFEcshF5dSBvCkzj6Gm2gzuR5fWawaX8t7cxKVkkygKODrDAzKdoYw3l/e3pm3vlT4IbQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"js-levenshtein": {
|
"js-levenshtein": {
|
||||||
|
@ -8590,9 +8591,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"js-yaml": {
|
"js-yaml": {
|
||||||
"version": "3.13.0",
|
"version": "3.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
|
||||||
"integrity": "sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==",
|
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"argparse": "^1.0.7",
|
"argparse": "^1.0.7",
|
||||||
|
@ -9403,6 +9404,12 @@
|
||||||
"lodash._reinterpolate": "~3.0.0"
|
"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": {
|
"log-symbols": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
|
||||||
|
@ -9477,6 +9484,15 @@
|
||||||
"yallist": "^2.1.2"
|
"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": {
|
"make-dir": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
|
||||||
|
@ -9844,9 +9860,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mocha": {
|
"mocha": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz",
|
||||||
"integrity": "sha512-cyKQPahVzaWsCgH86yWjKKxVgAKeN9MsyooMXmJtJa4nLbWxvXXjnPZU0cr9qRVOutirgfOVDzhVqorm8BBYKQ==",
|
"integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-colors": "3.2.3",
|
"ansi-colors": "3.2.3",
|
||||||
|
@ -9858,12 +9874,12 @@
|
||||||
"glob": "7.1.3",
|
"glob": "7.1.3",
|
||||||
"growl": "1.10.5",
|
"growl": "1.10.5",
|
||||||
"he": "1.2.0",
|
"he": "1.2.0",
|
||||||
"js-yaml": "3.13.0",
|
"js-yaml": "3.13.1",
|
||||||
"log-symbols": "2.2.0",
|
"log-symbols": "2.2.0",
|
||||||
"minimatch": "3.0.4",
|
"minimatch": "3.0.4",
|
||||||
"mkdirp": "0.5.1",
|
"mkdirp": "0.5.1",
|
||||||
"ms": "2.1.1",
|
"ms": "2.1.1",
|
||||||
"node-environment-flags": "1.0.4",
|
"node-environment-flags": "1.0.5",
|
||||||
"object.assign": "4.1.0",
|
"object.assign": "4.1.0",
|
||||||
"strip-json-comments": "2.0.1",
|
"strip-json-comments": "2.0.1",
|
||||||
"supports-color": "6.0.0",
|
"supports-color": "6.0.0",
|
||||||
|
@ -10359,12 +10375,21 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node-environment-flags": {
|
"node-environment-flags": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz",
|
||||||
"integrity": "sha512-M9rwCnWVLW7PX+NUWe3ejEdiLYinRpsEre9hMkU/6NS4h+EEulYaDH1gCEZ2gyXsmw+RXYDaV2JkkTNcsPDJ0Q==",
|
"integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"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": {
|
"node-libs-browser": {
|
||||||
|
@ -11101,11 +11126,20 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"version": "1.5.3",
|
"version": "1.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.17.0.tgz",
|
||||||
"integrity": "sha1-WdrcaDNF7GuI+IuU7Urn4do5S/4=",
|
"integrity": "sha512-sXe5lSt2WQlCbydGETgfm1YBShgOX4HxQkFPvbxkcwgDvGDeqVau8h+12+lmSVlP3rHPz0oavfddSZg/q+Szjw==",
|
||||||
"dev": true
|
"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": {
|
"private": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
|
||||||
|
@ -11145,12 +11179,6 @@
|
||||||
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
|
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
|
||||||
"dev": true
|
"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": {
|
"proxy-addr": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
|
||||||
|
@ -11531,6 +11559,12 @@
|
||||||
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
|
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
|
||||||
"dev": true
|
"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": {
|
"requires-port": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
|
@ -11635,6 +11669,151 @@
|
||||||
"inherits": "^2.0.1"
|
"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": {
|
"run-async": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
|
||||||
|
@ -11654,9 +11833,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rxjs": {
|
"rxjs": {
|
||||||
"version": "6.4.0",
|
"version": "6.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.1.tgz",
|
||||||
"integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==",
|
"integrity": "sha512-y0j31WJc83wPu31vS1VlAFW5JGrnGC+j+TtGAa1fRQphy48+fDYiDmX8tjGloToEsMkxnouOg/1IzXGKkJnZMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"tslib": "^1.9.0"
|
"tslib": "^1.9.0"
|
||||||
|
@ -11949,17 +12128,6 @@
|
||||||
"ansi-styles": "^3.2.0",
|
"ansi-styles": "^3.2.0",
|
||||||
"astral-regex": "^1.0.0",
|
"astral-regex": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^2.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": {
|
"snapdragon": {
|
||||||
|
@ -12123,6 +12291,12 @@
|
||||||
"to-array": "0.1.4"
|
"to-array": "0.1.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": {
|
"debug": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||||
|
@ -12220,6 +12394,12 @@
|
||||||
"integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
|
"integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
|
||||||
"dev": true
|
"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": {
|
"spdx-correct": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz",
|
||||||
|
@ -12834,12 +13014,6 @@
|
||||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||||
"dev": true
|
"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": {
|
"string-width": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
|
||||||
|
@ -13451,6 +13625,15 @@
|
||||||
"integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
|
"integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
|
||||||
"dev": true
|
"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": {
|
"tty-browserify": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
|
"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": {
|
"uglifyjs-webpack-plugin": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.7.tgz",
|
||||||
|
@ -14496,12 +14697,6 @@
|
||||||
"path-exists": "^3.0.0"
|
"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": {
|
"mem": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz",
|
||||||
|
|
59
package.json
59
package.json
|
@ -2,8 +2,9 @@
|
||||||
"title": "html2canvas",
|
"title": "html2canvas",
|
||||||
"name": "html2canvas",
|
"name": "html2canvas",
|
||||||
"description": "Screenshots with JavaScript",
|
"description": "Screenshots with JavaScript",
|
||||||
"main": "dist/npm/index.js",
|
"main": "dist/html2canvas.js",
|
||||||
"module": "dist/html2canvas.js",
|
"module": "dist/html2canvas.esm.js",
|
||||||
|
"typings": "dist/types/index.d.ts",
|
||||||
"browser": "dist/html2canvas.js",
|
"browser": "dist/html2canvas.js",
|
||||||
"version": "1.0.0-rc.1",
|
"version": "1.0.0-rc.1",
|
||||||
"author": {
|
"author": {
|
||||||
|
@ -26,26 +27,33 @@
|
||||||
"@babel/core": "^7.4.3",
|
"@babel/core": "^7.4.3",
|
||||||
"@babel/preset-env": "^7.4.3",
|
"@babel/preset-env": "^7.4.3",
|
||||||
"@babel/preset-flow": "^7.0.0",
|
"@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/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",
|
"appium-ios-simulator": "^3.10.0",
|
||||||
"babel-eslint": "^10.0.1",
|
"babel-eslint": "^10.0.1",
|
||||||
"babel-loader": "^8.0.5",
|
"babel-loader": "^8.0.5",
|
||||||
"babel-plugin-add-module-exports": "^1.0.0",
|
"babel-plugin-add-module-exports": "^1.0.0",
|
||||||
"babel-plugin-dev-expression": "^0.2.1",
|
"babel-plugin-dev-expression": "^0.2.1",
|
||||||
"base64-arraybuffer": "0.1.5",
|
"base64-arraybuffer": "0.2.0",
|
||||||
"body-parser": "^1.18.3",
|
"body-parser": "^1.18.3",
|
||||||
"chai": "4.1.1",
|
"chai": "4.1.1",
|
||||||
"chromeless": "^1.5.2",
|
"chromeless": "^1.5.2",
|
||||||
"cors": "2.8.4",
|
"cors": "2.8.4",
|
||||||
|
"es6-promise": "^4.2.6",
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^5.16.0",
|
||||||
"eslint-plugin-flowtype": "2.35.0",
|
"eslint-config-prettier": "^4.2.0",
|
||||||
"eslint-plugin-prettier": "2.1.2",
|
"eslint-plugin-prettier": "3.0.1",
|
||||||
"express": "^4.16.4",
|
"express": "^4.16.4",
|
||||||
"filenamify-url": "1.0.0",
|
"filenamify-url": "1.0.0",
|
||||||
"flow-bin": "0.56.0",
|
"glob": "7.1.3",
|
||||||
"glob": "7.1.2",
|
|
||||||
"html2canvas-proxy": "1.0.1",
|
"html2canvas-proxy": "1.0.1",
|
||||||
"jquery": "3.2.1",
|
"jquery": "^3.4.0",
|
||||||
"js-polyfills": "^0.1.42",
|
"js-polyfills": "^0.1.42",
|
||||||
"karma": "^4.0.1",
|
"karma": "^4.0.1",
|
||||||
"karma-chrome-launcher": "^2.2.0",
|
"karma-chrome-launcher": "^2.2.0",
|
||||||
|
@ -56,43 +64,50 @@
|
||||||
"karma-mocha": "^1.3.0",
|
"karma-mocha": "^1.3.0",
|
||||||
"karma-safari-launcher": "^1.0.0",
|
"karma-safari-launcher": "^1.0.0",
|
||||||
"karma-sauce-launcher": "^2.0.2",
|
"karma-sauce-launcher": "^2.0.2",
|
||||||
"mocha": "^6.1.0",
|
"mocha": "^6.1.4",
|
||||||
"node-simctl": "^5.0.0",
|
"node-simctl": "^5.0.0",
|
||||||
"platform": "1.3.4",
|
"platform": "1.3.4",
|
||||||
"prettier": "1.5.3",
|
"prettier": "1.17.0",
|
||||||
"promise-polyfill": "6.0.2",
|
|
||||||
"replace-in-file": "^3.0.0",
|
"replace-in-file": "^3.0.0",
|
||||||
"rimraf": "2.6.1",
|
"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",
|
"serve-index": "^1.9.1",
|
||||||
"slash": "1.0.0",
|
"slash": "1.0.0",
|
||||||
"standard-version": "^5.0.2",
|
"standard-version": "^5.0.2",
|
||||||
"ts-loader": "^5.3.3",
|
"ts-loader": "^5.3.3",
|
||||||
"ts-node": "^8.0.3",
|
"ts-node": "^8.0.3",
|
||||||
"typescript": "^3.4.3",
|
"typescript": "^3.4.3",
|
||||||
|
"uglify-js": "^3.5.11",
|
||||||
"uglifyjs-webpack-plugin": "^1.1.2",
|
"uglifyjs-webpack-plugin": "^1.1.2",
|
||||||
"webpack": "^4.29.6",
|
"webpack": "^4.29.6",
|
||||||
"webpack-cli": "^3.3.0"
|
"webpack-cli": "^3.3.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rimraf dist/ && node scripts/create-reftest-list && npm run build:npm && npm run build:browser",
|
"prebuild": "rimraf dist/ && rimraf build/ && mkdirp dist && mkdirp build",
|
||||||
"build:npm": "babel src/ -d dist/npm/ --plugins=dev-expression && replace-in-file __VERSION__ '\"$npm_package_version\"' dist/npm/index.js",
|
"build": "tsc --module commonjs && rollup -c rollup.config.ts && npm run build:create-reftest-list && npm run build:testrunner && npm run build:minify",
|
||||||
"build:browser": "webpack",
|
"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: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",
|
"build:reftest-preview": "webpack --config www/webpack.config.js",
|
||||||
"release": "standard-version",
|
"release": "standard-version",
|
||||||
"rollup": "rollup -c",
|
"format": "prettier --write \"{src,www/src,tests,scripts}/**/*.ts\"",
|
||||||
"format": "prettier --single-quote --no-bracket-spacing --tab-width 4 --print-width 100 --write \"{src,www/src,tests,scripts}/**/*.js\"",
|
"lint": "eslint src/**/*.ts",
|
||||||
"flow": "flow",
|
"test": "npm run lint && npm run unittest && npm run karma",
|
||||||
"lint": "eslint src/**",
|
"unittest": "mocha --require ts-node/register src/**/__tests__/*.ts",
|
||||||
"test": "npm run flow && npm run lint && npm run test:node && npm run karma",
|
|
||||||
"test:node": "mocha tests/node/*.js",
|
|
||||||
"karma": "node karma",
|
"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"
|
"start": "node tests/server"
|
||||||
},
|
},
|
||||||
"homepage": "https://html2canvas.hertzen.com",
|
"homepage": "https://html2canvas.hertzen.com",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"css-line-break": "1.0.1"
|
"css-line-break": "1.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -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(),
|
||||||
|
],
|
||||||
|
}
|
|
@ -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`);
|
|
||||||
}
|
|
||||||
);
|
|
|
@ -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`);
|
|
@ -1,12 +1,12 @@
|
||||||
import {readdirSync, readFileSync, writeFileSync} from 'fs';
|
import {readdirSync, readFileSync, writeFileSync} from 'fs';
|
||||||
import {resolve} from 'path';
|
import {resolve} from 'path';
|
||||||
|
|
||||||
if (process.argv.length <= 2){
|
if (process.argv.length <= 2) {
|
||||||
console.log('No metadata path provided');
|
console.log('No metadata path provided');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.argv.length <= 3){
|
if (process.argv.length <= 3) {
|
||||||
console.log('No output file given');
|
console.log('No output file given');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
@ -14,16 +14,14 @@ if (process.argv.length <= 3){
|
||||||
const path = resolve(__dirname, '../', process.argv[2]);
|
const path = resolve(__dirname, '../', process.argv[2]);
|
||||||
const files = readdirSync(path);
|
const files = readdirSync(path);
|
||||||
|
|
||||||
interface RefTestMetadata {
|
interface RefTestMetadata {}
|
||||||
|
|
||||||
}
|
interface RefTestSingleMetadata extends RefTestMetadata {
|
||||||
|
|
||||||
interface RefTestSingleMetadata extends RefTestMetadata{
|
|
||||||
test: string;
|
test: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RefTestResults {
|
interface RefTestResults {
|
||||||
[key: string]: Array<RefTestMetadata>
|
[key: string]: Array<RefTestMetadata>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: RefTestResults = files.reduce((result: RefTestResults, file) => {
|
const result: RefTestResults = files.reduce((result: RefTestResults, file) => {
|
||||||
|
|
24
src/Angle.js
24
src/Angle.js
|
@ -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;
|
|
||||||
};
|
|
370
src/Bounds.js
370
src/Bounds.js
|
@ -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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
690
src/Clone.js
690
src/Clone.js
|
@ -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;
|
|
||||||
};
|
|
251
src/Color.js
251
src/Color.js
|
@ -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]);
|
|
558
src/Gradient.js
558
src/Gradient.js
|
@ -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 gradient’s 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 gradient’s 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}%`;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
155
src/Input.js
155
src/Input.js
|
@ -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;
|
|
||||||
};
|
|
|
@ -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');
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -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))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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()
|
|
||||||
);
|
|
||||||
};
|
|
62
src/Proxy.js
62
src/Proxy.js
|
@ -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();
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
};
|
|
450
src/Renderer.js
450
src/Renderer.js
|
@ -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;
|
|
||||||
};
|
|
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
};
|
|
21
src/Util.js
21
src/Util.js
|
@ -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';
|
|
178
src/Window.js
178
src/Window.js
|
@ -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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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) {}
|
||||||
|
});
|
||||||
|
});
|
|
@ -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
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export const contains = (bit: number, value: number): boolean => (bit & value) !== 0;
|
|
@ -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);
|
|
@ -1,9 +1,4 @@
|
||||||
/* @flow */
|
const testRangeBounds = (document: Document) => {
|
||||||
'use strict';
|
|
||||||
|
|
||||||
import {createForeignObjectSVG, loadSerializedSVG} from './renderer/ForeignObjectRenderer';
|
|
||||||
|
|
||||||
const testRangeBounds = document => {
|
|
||||||
const TEST_HEIGHT = 123;
|
const TEST_HEIGHT = 123;
|
||||||
|
|
||||||
if (document.createRange) {
|
if (document.createRange) {
|
||||||
|
@ -27,14 +22,18 @@ const testRangeBounds = document => {
|
||||||
return false;
|
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 img = new Image();
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
img.src = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'></svg>`;
|
img.src = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'></svg>`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -46,14 +45,18 @@ const testSVG = document => {
|
||||||
return true;
|
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 canvas = document.createElement('canvas');
|
||||||
const size = 100;
|
const size = 100;
|
||||||
canvas.width = size;
|
canvas.width = size;
|
||||||
canvas.height = size;
|
canvas.height = size;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
return Promise.reject(false);
|
||||||
|
}
|
||||||
ctx.fillStyle = 'rgb(0, 255, 0)';
|
ctx.fillStyle = 'rgb(0, 255, 0)';
|
||||||
ctx.fillRect(0, 0, size, size);
|
ctx.fillRect(0, 0, size, size);
|
||||||
|
|
||||||
|
@ -65,7 +68,7 @@ const testForeignObject = document => {
|
||||||
ctx.fillRect(0, 0, size, size);
|
ctx.fillRect(0, 0, size, size);
|
||||||
|
|
||||||
return loadSerializedSVG(svg)
|
return loadSerializedSVG(svg)
|
||||||
.then(img => {
|
.then((img: HTMLImageElement) => {
|
||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0);
|
||||||
const data = ctx.getImageData(0, 0, size, size).data;
|
const data = ctx.getImageData(0, 0, size, size).data;
|
||||||
ctx.fillStyle = 'red';
|
ctx.fillStyle = 'red';
|
||||||
|
@ -79,30 +82,56 @@ const testForeignObject = document => {
|
||||||
? loadSerializedSVG(createForeignObjectSVG(size, size, 0, 0, node))
|
? loadSerializedSVG(createForeignObjectSVG(size, size, 0, 0, node))
|
||||||
: Promise.reject(false);
|
: Promise.reject(false);
|
||||||
})
|
})
|
||||||
.then(img => {
|
.then((img: HTMLImageElement) => {
|
||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0);
|
||||||
// Edge does not render background-images
|
// Edge does not render background-images
|
||||||
return isGreenPixel(ctx.getImageData(0, 0, size, size).data);
|
return isGreenPixel(ctx.getImageData(0, 0, size, size).data);
|
||||||
})
|
})
|
||||||
.catch(e => false);
|
.catch(() => false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FEATURES = {
|
export const createForeignObjectSVG = (width: number, height: number, x: number, y: number, node: Node) => {
|
||||||
// $FlowFixMe - get/set properties not yet supported
|
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() {
|
get SUPPORT_RANGE_BOUNDS() {
|
||||||
'use strict';
|
'use strict';
|
||||||
const value = testRangeBounds(document);
|
const value = testRangeBounds(document);
|
||||||
Object.defineProperty(FEATURES, 'SUPPORT_RANGE_BOUNDS', {value});
|
Object.defineProperty(FEATURES, 'SUPPORT_RANGE_BOUNDS', {value});
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
// $FlowFixMe - get/set properties not yet supported
|
|
||||||
get SUPPORT_SVG_DRAWING() {
|
get SUPPORT_SVG_DRAWING() {
|
||||||
'use strict';
|
'use strict';
|
||||||
const value = testSVG(document);
|
const value = testSVG(document);
|
||||||
Object.defineProperty(FEATURES, 'SUPPORT_SVG_DRAWING', {value});
|
Object.defineProperty(FEATURES, 'SUPPORT_SVG_DRAWING', {value});
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
// $FlowFixMe - get/set properties not yet supported
|
|
||||||
get SUPPORT_FOREIGNOBJECT_DRAWING() {
|
get SUPPORT_FOREIGNOBJECT_DRAWING() {
|
||||||
'use strict';
|
'use strict';
|
||||||
const value =
|
const value =
|
||||||
|
@ -112,21 +141,18 @@ const FEATURES = {
|
||||||
Object.defineProperty(FEATURES, 'SUPPORT_FOREIGNOBJECT_DRAWING', {value});
|
Object.defineProperty(FEATURES, 'SUPPORT_FOREIGNOBJECT_DRAWING', {value});
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
// $FlowFixMe - get/set properties not yet supported
|
|
||||||
get SUPPORT_CORS_IMAGES() {
|
get SUPPORT_CORS_IMAGES() {
|
||||||
'use strict';
|
'use strict';
|
||||||
const value = testCORS();
|
const value = testCORS();
|
||||||
Object.defineProperty(FEATURES, 'SUPPORT_CORS_IMAGES', {value});
|
Object.defineProperty(FEATURES, 'SUPPORT_CORS_IMAGES', {value});
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
// $FlowFixMe - get/set properties not yet supported
|
|
||||||
get SUPPORT_RESPONSE_TYPE() {
|
get SUPPORT_RESPONSE_TYPE() {
|
||||||
'use strict';
|
'use strict';
|
||||||
const value = testResponseType();
|
const value = testResponseType();
|
||||||
Object.defineProperty(FEATURES, 'SUPPORT_RESPONSE_TYPE', {value});
|
Object.defineProperty(FEATURES, 'SUPPORT_RESPONSE_TYPE', {value});
|
||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
// $FlowFixMe - get/set properties not yet supported
|
|
||||||
get SUPPORT_CORS_XHR() {
|
get SUPPORT_CORS_XHR() {
|
||||||
'use strict';
|
'use strict';
|
||||||
const value = 'withCredentials' in new XMLHttpRequest();
|
const value = 'withCredentials' in new XMLHttpRequest();
|
||||||
|
@ -134,5 +160,3 @@ const FEATURES = {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FEATURES;
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export const SMALL_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
|
@ -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;
|
|
@ -0,0 +1,6 @@
|
||||||
|
import {CSSValue} from './syntax/parser';
|
||||||
|
|
||||||
|
export interface ITypeDescriptor<T> {
|
||||||
|
name: string;
|
||||||
|
parse: (value: CSSValue) => T;
|
||||||
|
}
|
|
@ -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}`);
|
||||||
|
};
|
|
@ -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);
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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}
|
||||||
|
]
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
|
@ -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')
|
||||||
|
}
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
});
|
|
@ -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
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -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'
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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);
|
|
@ -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');
|
|
@ -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');
|
|
@ -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');
|
|
@ -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');
|
|
@ -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'
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,42 +1,52 @@
|
||||||
/* @flow */
|
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||||
'use strict';
|
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 = {
|
export type Display = number;
|
||||||
NONE: 1 << 0,
|
|
||||||
BLOCK: 1 << 1,
|
export const display: IPropertyListDescriptor<Display> = {
|
||||||
INLINE: 1 << 2,
|
name: 'display',
|
||||||
RUN_IN: 1 << 3,
|
initialValue: 'inline-block',
|
||||||
FLOW: 1 << 4,
|
prefix: false,
|
||||||
FLOW_ROOT: 1 << 5,
|
type: PropertyDescriptorParsingType.LIST,
|
||||||
TABLE: 1 << 6,
|
parse: (tokens: CSSValue[]): Display => {
|
||||||
FLEX: 1 << 7,
|
return tokens.filter(isIdentToken).reduce((bit, token) => {
|
||||||
GRID: 1 << 8,
|
return bit | parseDisplayValue(token.value);
|
||||||
RUBY: 1 << 9,
|
}, DISPLAY.NONE);
|
||||||
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 = $Values<typeof DISPLAY>;
|
|
||||||
export type DisplayBit = number;
|
|
||||||
|
|
||||||
const parseDisplayValue = (display: string): Display => {
|
const parseDisplayValue = (display: string): Display => {
|
||||||
switch (display) {
|
switch (display) {
|
||||||
case 'block':
|
case 'block':
|
||||||
|
@ -52,6 +62,7 @@ const parseDisplayValue = (display: string): Display => {
|
||||||
case 'table':
|
case 'table':
|
||||||
return DISPLAY.TABLE;
|
return DISPLAY.TABLE;
|
||||||
case 'flex':
|
case 'flex':
|
||||||
|
case '-webkit-flex':
|
||||||
return DISPLAY.FLEX;
|
return DISPLAY.FLEX;
|
||||||
case 'grid':
|
case 'grid':
|
||||||
return DISPLAY.GRID;
|
return DISPLAY.GRID;
|
||||||
|
@ -101,11 +112,3 @@ const parseDisplayValue = (display: string): Display => {
|
||||||
|
|
||||||
return DISPLAY.NONE;
|
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);
|
|
||||||
};
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
|
@ -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'
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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');
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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');
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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'
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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]];
|
||||||
|
}
|
||||||
|
};
|
|
@ -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
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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`);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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'}]));
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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
Loading…
Reference in New Issue