mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
Compare commits
34 Commits
v1.1.2
...
grapheme-b
Author | SHA1 | Date | |
---|---|---|---|
7b7cbc6672 | |||
9629afd7d6 | |||
e429e0443a | |||
f43f942fcd | |||
7a06d0c2c2 | |||
e36408ad03 | |||
a0dd38a8be | |||
b988d9d657 | |||
c5c6fa00d7 | |||
6651fc6789 | |||
df223c3ff2 | |||
95a46b00c5 | |||
878e37a242 | |||
1338c7b203 | |||
f284752295 | |||
96e23d1851 | |||
7d788c6f3d | |||
5dea36bd69 | |||
11d16d2b77 | |||
e9f7f48d57 | |||
4c360fc1f0 | |||
578bb771bf | |||
522e5aac5f | |||
dd6d8856ec | |||
45efe54da8 | |||
cd99f11b1b | |||
fa60716d07 | |||
99b687c412 | |||
1d00bfe175 | |||
58b4591174 | |||
92fa448913 | |||
1acdc827a4 | |||
acb4cd24b8 | |||
eeb5a3ea1d |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -113,6 +113,10 @@ jobs:
|
||||
name: iOS Simulator Safari 14
|
||||
targetBrowser: Safari_IOS_14
|
||||
xcode: /Applications/Xcode_12_beta.app
|
||||
- os: macos-11
|
||||
name: iOS Simulator Safari 15
|
||||
targetBrowser: Safari_IOS_15
|
||||
xcode: /Applications/Xcode_13.0.app
|
||||
- os: windows-latest
|
||||
name: Windows Internet Explorer 9 (Emulated)
|
||||
targetBrowser: IE_9
|
||||
|
80
CHANGELOG.md
80
CHANGELOG.md
@ -2,6 +2,86 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## [1.2.1](https://github.com/niklasvh/html2canvas/compare/v1.2.0...v1.2.1) (2021-08-05)
|
||||
|
||||
|
||||
### fix
|
||||
|
||||
* none image (#2627) ([6651fc6](https://github.com/niklasvh/html2canvas/commit/6651fc6789d5902d171dc53b4094887870433018)), closes [#2627](https://github.com/niklasvh/html2canvas/issues/2627)
|
||||
* type import that is only available ts 3.8 or higher (#2629) ([c5c6fa0](https://github.com/niklasvh/html2canvas/commit/c5c6fa00d71f36ef963ba5170ebc7b668d39c407)), closes [#2629](https://github.com/niklasvh/html2canvas/issues/2629)
|
||||
|
||||
|
||||
|
||||
# [1.2.0](https://github.com/niklasvh/html2canvas/compare/v1.1.5...v1.2.0) (2021-08-04)
|
||||
|
||||
|
||||
### fix
|
||||
|
||||
* element cropping & scrolling (#2625) ([878e37a](https://github.com/niklasvh/html2canvas/commit/878e37a24272d0412fe589975ef8eed931c56e0b)), closes [#2625](https://github.com/niklasvh/html2canvas/issues/2625)
|
||||
* overflow-wrap break-word (#2626) ([95a46b0](https://github.com/niklasvh/html2canvas/commit/95a46b00c53563722c035a0e45fdf5fb507275e4)), closes [#2626](https://github.com/niklasvh/html2canvas/issues/2626)
|
||||
|
||||
### test
|
||||
|
||||
* element with scrolled window (#2624) ([1338c7b](https://github.com/niklasvh/html2canvas/commit/1338c7b203535d53509416358d74014200a994eb)), closes [#2624](https://github.com/niklasvh/html2canvas/issues/2624)
|
||||
|
||||
|
||||
|
||||
## [1.1.5](https://github.com/niklasvh/html2canvas/compare/v1.1.4...v1.1.5) (2021-08-02)
|
||||
|
||||
|
||||
### docs
|
||||
|
||||
* update README to github discussion Q/A ([5dea36b](https://github.com/niklasvh/html2canvas/commit/5dea36bd6964164e8ba3f8780309e792f5d16255))
|
||||
|
||||
### fix
|
||||
|
||||
* emoji line breaking (fix #1813) (#2621) ([7d788c6](https://github.com/niklasvh/html2canvas/commit/7d788c6f3d221b87f6b59fcda8517731340b2d1f)), closes [#1813](https://github.com/niklasvh/html2canvas/issues/1813) [#2621](https://github.com/niklasvh/html2canvas/issues/2621) [#1813](https://github.com/niklasvh/html2canvas/issues/1813)
|
||||
* natural sizes for images with srcset (#2622) ([96e23d1](https://github.com/niklasvh/html2canvas/commit/96e23d185198b7131cf0cfa31c14c165790464e9)), closes [#2622](https://github.com/niklasvh/html2canvas/issues/2622)
|
||||
|
||||
|
||||
|
||||
## [1.1.4](https://github.com/niklasvh/html2canvas/compare/v1.1.3...v1.1.4) (2021-07-15)
|
||||
|
||||
|
||||
### feat
|
||||
|
||||
* add support for webkit-text-stroke and paint-order (#2591) ([522e5aa](https://github.com/niklasvh/html2canvas/commit/522e5aac5fdad090953d095b5d558053a5e2d43d)), closes [#2591](https://github.com/niklasvh/html2canvas/issues/2591)
|
||||
|
||||
### fix
|
||||
|
||||
* don't copy 'all' css property (#2586) ([fa60716](https://github.com/niklasvh/html2canvas/commit/fa60716d07ed590ec64543a586a7960cbc8557df)), closes [#2586](https://github.com/niklasvh/html2canvas/issues/2586)
|
||||
* svg d path getting truncated on copy (#2589) ([dd6d885](https://github.com/niklasvh/html2canvas/commit/dd6d8856eca820a13a0990c467b9e531433fd4a9)), closes [#2589](https://github.com/niklasvh/html2canvas/issues/2589)
|
||||
* text position for form elements and list markers (#2588) ([cd99f11](https://github.com/niklasvh/html2canvas/commit/cd99f11b1b9eb1260a548a63e2a370a0a5ddafa0)), closes [#2588](https://github.com/niklasvh/html2canvas/issues/2588)
|
||||
* this.canvas.ownerDocument is undefined (#2590) ([45efe54](https://github.com/niklasvh/html2canvas/commit/45efe54da8145f97b9ee0463e686103280e3c8b1)), closes [#2590](https://github.com/niklasvh/html2canvas/issues/2590)
|
||||
* word-break seperators (#2593) ([e9f7f48](https://github.com/niklasvh/html2canvas/commit/e9f7f48d571304be14610a181feedca3c3b42864)), closes [#2593](https://github.com/niklasvh/html2canvas/issues/2593)
|
||||
|
||||
### test
|
||||
|
||||
* refactor language tests (#2594) ([4c360fc](https://github.com/niklasvh/html2canvas/commit/4c360fc1f059f4dcab71a79f9dc8a5b2e25411ea)), closes [#2594](https://github.com/niklasvh/html2canvas/issues/2594)
|
||||
* update box-shadow with radius ([578bb77](https://github.com/niklasvh/html2canvas/commit/578bb771bfeb7e81362e9e355d6cc9ae910e3920))
|
||||
|
||||
|
||||
|
||||
## [1.1.3](https://github.com/niklasvh/html2canvas/compare/v1.1.2...v1.1.3) (2021-07-14)
|
||||
|
||||
|
||||
### feat
|
||||
|
||||
* allow access to reference element in onclone (#2584) ([58b4591](https://github.com/niklasvh/html2canvas/commit/58b45911741c0dbbccd462b2976560bb3999eaef)), closes [#2584](https://github.com/niklasvh/html2canvas/issues/2584)
|
||||
* support for custom and slot elements (#2581) ([acb4cd2](https://github.com/niklasvh/html2canvas/commit/acb4cd24b85527908c02a60794768949578678f0)), closes [#2581](https://github.com/niklasvh/html2canvas/issues/2581)
|
||||
|
||||
### fix
|
||||
|
||||
* iframe load to ensure images are loaded (#2577) ([eeb5a3e](https://github.com/niklasvh/html2canvas/commit/eeb5a3ea1d6c94e0f6dcfd40695eb88ebb3e0041)), closes [#2577](https://github.com/niklasvh/html2canvas/issues/2577)
|
||||
* image blob rendering ([1acdc82](https://github.com/niklasvh/html2canvas/commit/1acdc827a4e05933c2f7c9558405c66b7cd82f58))
|
||||
* responsive svg images (#2583) ([92fa448](https://github.com/niklasvh/html2canvas/commit/92fa448913192d5e4e82bfe14f6644b669d4e6ef)), closes [#2583](https://github.com/niklasvh/html2canvas/issues/2583)
|
||||
|
||||
### test
|
||||
|
||||
* add test cases for text-stroke and textarea from (#1540 and #2132) (#2585) ([1d00bfe](https://github.com/niklasvh/html2canvas/commit/1d00bfe175d51e663d0bae88b6dbd10a266a71f1)), closes [#1540](https://github.com/niklasvh/html2canvas/issues/1540) [#2132](https://github.com/niklasvh/html2canvas/issues/2132) [#2585](https://github.com/niklasvh/html2canvas/issues/2585)
|
||||
|
||||
|
||||
|
||||
## [1.1.2](https://github.com/niklasvh/html2canvas/compare/v1.1.1...v1.1.2) (2021-07-13)
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
html2canvas
|
||||
===========
|
||||
|
||||
[Homepage](https://html2canvas.hertzen.com) | [Downloads](https://github.com/niklasvh/html2canvas/releases) | [Questions](http://stackoverflow.com/questions/tagged/html2canvas?sort=newest)
|
||||
[Homepage](https://html2canvas.hertzen.com) | [Downloads](https://github.com/niklasvh/html2canvas/releases) | [Questions](https://github.com/niklasvh/html2canvas/discussions/categories/q-a)
|
||||
|
||||
[](https://gitter.im/niklasvh/html2canvas?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||

|
||||
|
@ -12,9 +12,9 @@ Below is a list of all the supported CSS properties and values.
|
||||
- url()
|
||||
- linear-gradient()
|
||||
- radial-gradient()
|
||||
- background-origin
|
||||
- background-origin
|
||||
- background-position
|
||||
- background-size
|
||||
- background-size
|
||||
- border
|
||||
- border-color
|
||||
- border-radius
|
||||
@ -50,6 +50,7 @@ Below is a list of all the supported CSS properties and values.
|
||||
- overflow
|
||||
- overflow-wrap
|
||||
- padding
|
||||
- paint-order
|
||||
- position
|
||||
- right
|
||||
- text-align
|
||||
@ -58,17 +59,18 @@ Below is a list of all the supported CSS properties and values.
|
||||
- text-decoration-line
|
||||
- text-decoration-style (**Only supports `solid`**)
|
||||
- text-shadow
|
||||
- text-transform
|
||||
- text-transform
|
||||
- top
|
||||
- transform (**Limited support**)
|
||||
- visibility
|
||||
- white-space
|
||||
- width
|
||||
- webkit-text-stroke
|
||||
- word-break
|
||||
- word-spacing
|
||||
- word-wrap
|
||||
- z-index
|
||||
|
||||
|
||||
## Unsupported CSS properties
|
||||
These CSS properties are **NOT** currently supported
|
||||
- [background-blend-mode](https://github.com/niklasvh/html2canvas/issues/966)
|
||||
|
@ -42,6 +42,12 @@ module.exports = function(config) {
|
||||
platform: 'iOS',
|
||||
sdk: '14.0'
|
||||
},
|
||||
Safari_IOS_15: {
|
||||
base: 'MobileSafari',
|
||||
name: 'iPhone 8',
|
||||
platform: 'iOS',
|
||||
sdk: '15.0'
|
||||
},
|
||||
SauceLabs_IE9: {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'internet explorer',
|
||||
|
80
package-lock.json
generated
80
package-lock.json
generated
@ -1,14 +1,15 @@
|
||||
{
|
||||
"name": "html2canvas",
|
||||
"version": "1.1.2",
|
||||
"version": "1.2.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-line-break": "1.1.1"
|
||||
"css-line-break": "2.0.1",
|
||||
"text-segmentation": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.4.3",
|
||||
@ -36,7 +37,7 @@
|
||||
"babel-loader": "^8.0.5",
|
||||
"babel-plugin-add-module-exports": "^1.0.2",
|
||||
"babel-plugin-dev-expression": "^0.2.1",
|
||||
"base64-arraybuffer": "0.2.0",
|
||||
"base64-arraybuffer": "1.0.1",
|
||||
"body-parser": "^1.19.0",
|
||||
"chai": "4.1.1",
|
||||
"chromeless": "^1.5.2",
|
||||
@ -5784,9 +5785,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz",
|
||||
"integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz",
|
||||
"integrity": "sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
@ -8630,13 +8631,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz",
|
||||
"integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.0.1.tgz",
|
||||
"integrity": "sha512-gwKYIMUn7xodIcb346wgUhE2Dt5O1Kmrc16PWi8sL4FTfyDj8P5095rzH7+O8CTZudJr+uw2GCI/hwEkDJFI2w==",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-line-break/node_modules/base64-arraybuffer": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz",
|
||||
"integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cssom": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz",
|
||||
@ -23441,6 +23450,14 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.2.tgz",
|
||||
"integrity": "sha512-uTqvLxdBrVnx/CFQOtnf8tfzSXFm+1Qxau7Xi54j4OPTZokuDOX8qncQzrg2G8ZicAMOM8TgzFAYTb+AqNO4Cw==",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@ -24517,6 +24534,14 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.1.tgz",
|
||||
"integrity": "sha512-JPaDXF3vzgZxfeEwutdGzlrNoVFL5UvZcbO6Qo9D4GoahrieUPoMU8GCpVpR7MQqcKhmShIh8VlbEN3PLM3EBg==",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
|
||||
@ -30457,9 +30482,9 @@
|
||||
}
|
||||
},
|
||||
"base64-arraybuffer": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz",
|
||||
"integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ=="
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz",
|
||||
"integrity": "sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA=="
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.3.0",
|
||||
@ -32811,11 +32836,18 @@
|
||||
}
|
||||
},
|
||||
"css-line-break": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz",
|
||||
"integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.0.1.tgz",
|
||||
"integrity": "sha512-gwKYIMUn7xodIcb346wgUhE2Dt5O1Kmrc16PWi8sL4FTfyDj8P5095rzH7+O8CTZudJr+uw2GCI/hwEkDJFI2w==",
|
||||
"requires": {
|
||||
"base64-arraybuffer": "^0.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz",
|
||||
"integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"cssom": {
|
||||
@ -44509,6 +44541,14 @@
|
||||
"integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==",
|
||||
"dev": true
|
||||
},
|
||||
"text-segmentation": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.2.tgz",
|
||||
"integrity": "sha512-uTqvLxdBrVnx/CFQOtnf8tfzSXFm+1Qxau7Xi54j4OPTZokuDOX8qncQzrg2G8ZicAMOM8TgzFAYTb+AqNO4Cw==",
|
||||
"requires": {
|
||||
"utrie": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@ -45332,6 +45372,14 @@
|
||||
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
|
||||
"dev": true
|
||||
},
|
||||
"utrie": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.1.tgz",
|
||||
"integrity": "sha512-JPaDXF3vzgZxfeEwutdGzlrNoVFL5UvZcbO6Qo9D4GoahrieUPoMU8GCpVpR7MQqcKhmShIh8VlbEN3PLM3EBg==",
|
||||
"requires": {
|
||||
"base64-arraybuffer": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
|
||||
|
@ -6,7 +6,7 @@
|
||||
"module": "dist/html2canvas.esm.js",
|
||||
"typings": "dist/types/index.d.ts",
|
||||
"browser": "dist/html2canvas.js",
|
||||
"version": "1.1.2",
|
||||
"version": "1.2.1",
|
||||
"author": {
|
||||
"name": "Niklas von Hertzen",
|
||||
"email": "niklasvh@gmail.com",
|
||||
@ -48,7 +48,7 @@
|
||||
"babel-loader": "^8.0.5",
|
||||
"babel-plugin-add-module-exports": "^1.0.2",
|
||||
"babel-plugin-dev-expression": "^0.2.1",
|
||||
"base64-arraybuffer": "0.2.0",
|
||||
"base64-arraybuffer": "1.0.1",
|
||||
"body-parser": "^1.19.0",
|
||||
"chai": "4.1.1",
|
||||
"chromeless": "^1.5.2",
|
||||
@ -118,6 +118,7 @@
|
||||
"homepage": "https://html2canvas.hertzen.com",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-line-break": "1.1.1"
|
||||
"css-line-break": "2.0.1",
|
||||
"text-segmentation": "^1.0.2"
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,11 @@ describe('html2canvas', () => {
|
||||
DocumentCloner.destroy = jest.fn().mockReturnValue(true);
|
||||
await html2canvas(element);
|
||||
expect(CanvasRenderer).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
cache: expect.any(Object),
|
||||
logger: expect.any(Object),
|
||||
windowBounds: expect.objectContaining({left: 12, top: 34})
|
||||
}),
|
||||
expect.objectContaining({
|
||||
backgroundColor: 0xffffffff,
|
||||
scale: 1,
|
||||
@ -41,8 +46,6 @@ describe('html2canvas', () => {
|
||||
width: 200,
|
||||
x: 0,
|
||||
y: 0,
|
||||
scrollX: 12,
|
||||
scrollY: 34,
|
||||
canvas: undefined
|
||||
})
|
||||
);
|
||||
@ -52,6 +55,7 @@ describe('html2canvas', () => {
|
||||
it('should have transparent background with backgroundColor: null', async () => {
|
||||
await html2canvas(element, {backgroundColor: null});
|
||||
expect(CanvasRenderer).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
backgroundColor: COLORS.TRANSPARENT
|
||||
})
|
||||
@ -62,6 +66,7 @@ describe('html2canvas', () => {
|
||||
const canvas = {} as HTMLCanvasElement;
|
||||
await html2canvas(element, {canvas});
|
||||
expect(CanvasRenderer).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
canvas
|
||||
})
|
||||
@ -72,6 +77,7 @@ describe('html2canvas', () => {
|
||||
DocumentCloner.destroy = jest.fn();
|
||||
await html2canvas(element, {removeContainer: false});
|
||||
expect(CanvasRenderer).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
backgroundColor: 0xffffffff,
|
||||
scale: 1,
|
||||
@ -79,8 +85,6 @@ describe('html2canvas', () => {
|
||||
width: 200,
|
||||
x: 0,
|
||||
y: 0,
|
||||
scrollX: 12,
|
||||
scrollY: 34,
|
||||
canvas: undefined
|
||||
})
|
||||
);
|
||||
|
@ -1,22 +1 @@
|
||||
class MockCache {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private readonly _cache: {[key: string]: Promise<any>};
|
||||
|
||||
constructor() {
|
||||
this._cache = {};
|
||||
}
|
||||
|
||||
addImage(src: string): Promise<void> {
|
||||
const result = Promise.resolve();
|
||||
this._cache[src] = result;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const current = new MockCache();
|
||||
|
||||
export class CacheStorage {
|
||||
static getInstance(): MockCache {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
export class CacheStorage {}
|
||||
|
19
src/core/__mocks__/context.ts
Normal file
19
src/core/__mocks__/context.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {logger, Logger} from './logger';
|
||||
|
||||
export class Context {
|
||||
readonly logger: Logger = logger;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
readonly _cache: {[key: string]: Promise<any>} = {};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
readonly cache: any;
|
||||
|
||||
constructor() {
|
||||
this.cache = {
|
||||
addImage: jest.fn().mockImplementation((src: string): Promise<void> => {
|
||||
const result = Promise.resolve();
|
||||
this._cache[src] = result;
|
||||
return result;
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
@ -19,4 +19,4 @@ export class Logger {
|
||||
error(): void {}
|
||||
}
|
||||
|
||||
const logger = new Logger();
|
||||
export const logger = new Logger();
|
||||
|
@ -1,7 +1,8 @@
|
||||
import {deepStrictEqual, fail} from 'assert';
|
||||
import {FEATURES} from '../features';
|
||||
import {CacheStorage} from '../cache-storage';
|
||||
import {Logger} from '../logger';
|
||||
import {Context} from '../context';
|
||||
import {Bounds} from '../../css/layout/bounds';
|
||||
|
||||
const proxy = 'http://example.com/proxy';
|
||||
|
||||
@ -35,14 +36,18 @@ const createMockContext = (origin: string, opts = {}) => {
|
||||
};
|
||||
|
||||
CacheStorage.setContext(context as Window);
|
||||
Logger.create({id: 'test', enabled: false});
|
||||
return CacheStorage.create('test', {
|
||||
imageTimeout: 0,
|
||||
useCORS: false,
|
||||
allowTaint: false,
|
||||
proxy,
|
||||
...opts
|
||||
});
|
||||
|
||||
return new Context(
|
||||
{
|
||||
logging: false,
|
||||
imageTimeout: 0,
|
||||
useCORS: false,
|
||||
allowTaint: false,
|
||||
proxy,
|
||||
...opts
|
||||
},
|
||||
new Bounds(0, 0, 0, 0)
|
||||
);
|
||||
};
|
||||
|
||||
const images: ImageMock[] = [];
|
||||
@ -121,7 +126,7 @@ describe('cache-storage', () => {
|
||||
images.splice(0, images.length);
|
||||
});
|
||||
it('addImage adds images to cache', async () => {
|
||||
const cache = createMockContext('http://example.com', {proxy: null});
|
||||
const {cache} = createMockContext('http://example.com', {proxy: null});
|
||||
await cache.addImage('http://example.com/test.jpg');
|
||||
await cache.addImage('http://example.com/test2.jpg');
|
||||
|
||||
@ -131,7 +136,7 @@ describe('cache-storage', () => {
|
||||
});
|
||||
|
||||
it('addImage should not add duplicate entries', async () => {
|
||||
const cache = createMockContext('http://example.com');
|
||||
const {cache} = createMockContext('http://example.com');
|
||||
await cache.addImage('http://example.com/test.jpg');
|
||||
await cache.addImage('http://example.com/test.jpg');
|
||||
|
||||
@ -141,7 +146,7 @@ describe('cache-storage', () => {
|
||||
|
||||
describe('svg', () => {
|
||||
it('should add svg images correctly', async () => {
|
||||
const cache = createMockContext('http://example.com');
|
||||
const {cache} = createMockContext('http://example.com');
|
||||
await cache.addImage('http://example.com/test.svg');
|
||||
await cache.addImage('http://example.com/test2.svg');
|
||||
|
||||
@ -152,7 +157,7 @@ describe('cache-storage', () => {
|
||||
|
||||
it('should omit svg images if not supported', async () => {
|
||||
setFeatures({SUPPORT_SVG_DRAWING: false});
|
||||
const cache = createMockContext('http://example.com');
|
||||
const {cache} = createMockContext('http://example.com');
|
||||
await cache.addImage('http://example.com/test.svg');
|
||||
await cache.addImage('http://example.com/test2.svg');
|
||||
|
||||
@ -162,7 +167,7 @@ describe('cache-storage', () => {
|
||||
|
||||
describe('cross-origin', () => {
|
||||
it('addImage should not add images it cannot load/render', async () => {
|
||||
const cache = createMockContext('http://example.com', {
|
||||
const {cache} = createMockContext('http://example.com', {
|
||||
proxy: undefined
|
||||
});
|
||||
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
|
||||
@ -170,7 +175,7 @@ describe('cache-storage', () => {
|
||||
});
|
||||
|
||||
it('addImage should add images if tainting enabled', async () => {
|
||||
const cache = createMockContext('http://example.com', {
|
||||
const {cache} = createMockContext('http://example.com', {
|
||||
allowTaint: true,
|
||||
proxy: undefined
|
||||
});
|
||||
@ -181,7 +186,7 @@ describe('cache-storage', () => {
|
||||
});
|
||||
|
||||
it('addImage should add images if cors enabled', async () => {
|
||||
const cache = createMockContext('http://example.com', {useCORS: true});
|
||||
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');
|
||||
@ -191,7 +196,7 @@ describe('cache-storage', () => {
|
||||
it('addImage should not add images if cors enabled but not supported', async () => {
|
||||
setFeatures({SUPPORT_CORS_IMAGES: false});
|
||||
|
||||
const cache = createMockContext('http://example.com', {
|
||||
const {cache} = createMockContext('http://example.com', {
|
||||
useCORS: true,
|
||||
proxy: undefined
|
||||
});
|
||||
@ -200,7 +205,7 @@ describe('cache-storage', () => {
|
||||
});
|
||||
|
||||
it('addImage should not add images to proxy if cors enabled', async () => {
|
||||
const cache = createMockContext('http://example.com', {useCORS: true});
|
||||
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');
|
||||
@ -208,7 +213,7 @@ describe('cache-storage', () => {
|
||||
});
|
||||
|
||||
it('addImage should use proxy ', async () => {
|
||||
const cache = createMockContext('http://example.com');
|
||||
const {cache} = createMockContext('http://example.com');
|
||||
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
|
||||
deepStrictEqual(xhr.length, 1);
|
||||
deepStrictEqual(
|
||||
@ -222,7 +227,7 @@ describe('cache-storage', () => {
|
||||
});
|
||||
|
||||
it('proxy should respect imageTimeout', async () => {
|
||||
const cache = createMockContext('http://example.com', {
|
||||
const {cache} = createMockContext('http://example.com', {
|
||||
imageTimeout: 10
|
||||
});
|
||||
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
|
||||
@ -244,7 +249,7 @@ describe('cache-storage', () => {
|
||||
});
|
||||
|
||||
it('match should return cache entry', async () => {
|
||||
const cache = createMockContext('http://example.com');
|
||||
const {cache} = createMockContext('http://example.com');
|
||||
await cache.addImage('http://example.com/test.jpg');
|
||||
|
||||
if (images[0].onload) {
|
||||
@ -257,7 +262,7 @@ describe('cache-storage', () => {
|
||||
});
|
||||
|
||||
it('image should respect imageTimeout', async () => {
|
||||
const cache = createMockContext('http://example.com', {imageTimeout: 10});
|
||||
const {cache} = createMockContext('http://example.com', {imageTimeout: 10});
|
||||
cache.addImage('http://example.com/test.jpg');
|
||||
|
||||
try {
|
||||
|
@ -1,28 +1,9 @@
|
||||
import {FEATURES} from './features';
|
||||
import {Logger} from './logger';
|
||||
import {Context} from './context';
|
||||
|
||||
export class CacheStorage {
|
||||
private static _caches: {[key: string]: Cache} = {};
|
||||
private static _link?: HTMLAnchorElement;
|
||||
private static _origin = '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;
|
||||
@ -43,22 +24,6 @@ export class CacheStorage {
|
||||
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): void {
|
||||
CacheStorage._current = cache;
|
||||
}
|
||||
|
||||
static detachInstance(): void {
|
||||
CacheStorage._current = null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ResourceOptions {
|
||||
@ -70,15 +35,9 @@ export interface ResourceOptions {
|
||||
|
||||
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;
|
||||
private readonly _cache: {[key: string]: Promise<any>} = {};
|
||||
|
||||
constructor(id: string, options: ResourceOptions) {
|
||||
this.id = id;
|
||||
this._options = options;
|
||||
this._cache = {};
|
||||
}
|
||||
constructor(private readonly context: Context, private readonly _options: ResourceOptions) {}
|
||||
|
||||
addImage(src: string): Promise<void> {
|
||||
const result = Promise.resolve();
|
||||
@ -108,10 +67,18 @@ export class Cache {
|
||||
const useProxy =
|
||||
!isInlineImage(key) &&
|
||||
!isSameOrigin &&
|
||||
!isBlobImage(key) &&
|
||||
typeof this._options.proxy === 'string' &&
|
||||
FEATURES.SUPPORT_CORS_XHR &&
|
||||
!useCORS;
|
||||
if (!isSameOrigin && this._options.allowTaint === false && !isInlineImage(key) && !useProxy && !useCORS) {
|
||||
if (
|
||||
!isSameOrigin &&
|
||||
this._options.allowTaint === false &&
|
||||
!isInlineImage(key) &&
|
||||
!isBlobImage(key) &&
|
||||
!useProxy &&
|
||||
!useCORS
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -120,7 +87,7 @@ export class Cache {
|
||||
src = await this.proxy(src);
|
||||
}
|
||||
|
||||
Logger.getInstance(this.id).debug(`Added image ${key.substring(0, 256)}`);
|
||||
this.context.logger.debug(`Added image ${key.substring(0, 256)}`);
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
|
21
src/core/context.ts
Normal file
21
src/core/context.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import {Logger} from './logger';
|
||||
import {Cache, ResourceOptions} from './cache-storage';
|
||||
import {Bounds} from '../css/layout/bounds';
|
||||
|
||||
export type ContextOptions = {
|
||||
logging: boolean;
|
||||
cache?: Cache;
|
||||
} & ResourceOptions;
|
||||
|
||||
export class Context {
|
||||
private readonly instanceName = `#${Context.instanceCount++}`;
|
||||
readonly logger: Logger;
|
||||
readonly cache: Cache;
|
||||
|
||||
private static instanceCount = 1;
|
||||
|
||||
constructor(options: ContextOptions, public windowBounds: Bounds) {
|
||||
this.logger = new Logger({id: this.instanceName, enabled: options.logging});
|
||||
this.cache = options.cache ?? new Cache(this, options);
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import {fromCodePoint, toCodePoints} from 'text-segmentation';
|
||||
|
||||
const testRangeBounds = (document: Document) => {
|
||||
const TEST_HEIGHT = 123;
|
||||
|
||||
@ -22,6 +24,45 @@ const testRangeBounds = (document: Document) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const testIOSLineBreak = (document: Document) => {
|
||||
const testElement = document.createElement('boundtest');
|
||||
testElement.style.width = '50px';
|
||||
testElement.style.display = 'block';
|
||||
testElement.style.fontSize = '12px';
|
||||
testElement.style.letterSpacing = '0px';
|
||||
testElement.style.wordSpacing = '0px';
|
||||
document.body.appendChild(testElement);
|
||||
const range = document.createRange();
|
||||
|
||||
testElement.innerHTML = typeof ''.repeat === 'function' ? '👨'.repeat(10) : '';
|
||||
|
||||
const node = testElement.firstChild as Text;
|
||||
|
||||
const textList = toCodePoints(node.data).map((i) => fromCodePoint(i));
|
||||
let offset = 0;
|
||||
let prev: DOMRect = {} as DOMRect;
|
||||
|
||||
// ios 13 does not handle range getBoundingClientRect line changes correctly #2177
|
||||
const supports = textList.every((text, i) => {
|
||||
range.setStart(node, offset);
|
||||
range.setEnd(node, offset + text.length);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
offset += text.length;
|
||||
const boundAhead = rect.x > prev.x || rect.y > prev.y;
|
||||
|
||||
prev = rect;
|
||||
if (i === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return boundAhead;
|
||||
});
|
||||
|
||||
document.body.removeChild(testElement);
|
||||
return supports;
|
||||
};
|
||||
|
||||
const testCORS = (): boolean => typeof new Image().crossOrigin !== 'undefined';
|
||||
|
||||
const testResponseType = (): boolean => typeof new XMLHttpRequest().responseType === 'string';
|
||||
@ -132,6 +173,12 @@ export const FEATURES = {
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_RANGE_BOUNDS', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_WORD_BREAKING(): boolean {
|
||||
'use strict';
|
||||
const value = FEATURES.SUPPORT_RANGE_BOUNDS && testIOSLineBreak(document);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_WORD_BREAKING', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_SVG_DRAWING(): boolean {
|
||||
'use strict';
|
||||
const value = testSVG(document);
|
||||
|
@ -33,22 +33,6 @@ export class Logger {
|
||||
return Date.now() - this.start;
|
||||
}
|
||||
|
||||
static create(options: LoggerOptions): void {
|
||||
Logger.instances[options.id] = new Logger(options);
|
||||
}
|
||||
|
||||
static destroy(id: string): void {
|
||||
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: unknown[]): void {
|
||||
if (this.enabled) {
|
||||
@ -60,6 +44,19 @@ export class Logger {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
warn(...args: unknown[]): void {
|
||||
if (this.enabled) {
|
||||
// eslint-disable-next-line no-console
|
||||
if (typeof window !== 'undefined' && window.console && typeof console.warn === 'function') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(this.id, `${this.getTime()}ms`, ...args);
|
||||
} else {
|
||||
this.info(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error(...args: unknown[]): void {
|
||||
if (this.enabled) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {CSSValue} from './syntax/parser';
|
||||
import {CSSTypes} from './types/index';
|
||||
import {Context} from '../core/context';
|
||||
|
||||
export enum PropertyDescriptorParsingType {
|
||||
VALUE,
|
||||
@ -18,7 +19,7 @@ export interface IPropertyDescriptor {
|
||||
|
||||
export interface IPropertyIdentValueDescriptor<T> extends IPropertyDescriptor {
|
||||
type: PropertyDescriptorParsingType.IDENT_VALUE;
|
||||
parse: (token: string) => T;
|
||||
parse: (context: Context, token: string) => T;
|
||||
}
|
||||
|
||||
export interface IPropertyTypeValueDescriptor extends IPropertyDescriptor {
|
||||
@ -28,12 +29,12 @@ export interface IPropertyTypeValueDescriptor extends IPropertyDescriptor {
|
||||
|
||||
export interface IPropertyValueDescriptor<T> extends IPropertyDescriptor {
|
||||
type: PropertyDescriptorParsingType.VALUE;
|
||||
parse: (token: CSSValue) => T;
|
||||
parse: (context: Context, token: CSSValue) => T;
|
||||
}
|
||||
|
||||
export interface IPropertyListDescriptor<T> extends IPropertyDescriptor {
|
||||
type: PropertyDescriptorParsingType.LIST;
|
||||
parse: (tokens: CSSValue[]) => T;
|
||||
parse: (context: Context, tokens: CSSValue[]) => T;
|
||||
}
|
||||
|
||||
export interface IPropertyTokenValueDescriptor extends IPropertyDescriptor {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {CSSValue} from './syntax/parser';
|
||||
import {Context} from '../core/context';
|
||||
|
||||
export interface ITypeDescriptor<T> {
|
||||
name: string;
|
||||
parse: (value: CSSValue) => T;
|
||||
parse: (context: Context, value: CSSValue) => T;
|
||||
}
|
||||
|
161
src/css/index.ts
161
src/css/index.ts
@ -73,6 +73,10 @@ import {counterIncrement} from './property-descriptors/counter-increment';
|
||||
import {counterReset} from './property-descriptors/counter-reset';
|
||||
import {quotes} from './property-descriptors/quotes';
|
||||
import {boxShadow} from './property-descriptors/box-shadow';
|
||||
import {paintOrder} from './property-descriptors/paint-order';
|
||||
import {webkitTextStrokeColor} from './property-descriptors/webkit-text-stroke-color';
|
||||
import {webkitTextStrokeWidth} from './property-descriptors/webkit-text-stroke-width';
|
||||
import {Context} from '../core/context';
|
||||
|
||||
export class CSSParsedDeclaration {
|
||||
backgroundClip: ReturnType<typeof backgroundClip.parse>;
|
||||
@ -125,6 +129,7 @@ export class CSSParsedDeclaration {
|
||||
paddingRight: LengthPercentage;
|
||||
paddingBottom: LengthPercentage;
|
||||
paddingLeft: LengthPercentage;
|
||||
paintOrder: ReturnType<typeof paintOrder.parse>;
|
||||
position: ReturnType<typeof position.parse>;
|
||||
textAlign: ReturnType<typeof textAlign.parse>;
|
||||
textDecorationColor: Color;
|
||||
@ -134,75 +139,85 @@ export class CSSParsedDeclaration {
|
||||
transform: ReturnType<typeof transform.parse>;
|
||||
transformOrigin: ReturnType<typeof transformOrigin.parse>;
|
||||
visibility: ReturnType<typeof visibility.parse>;
|
||||
webkitTextStrokeColor: Color;
|
||||
webkitTextStrokeWidth: ReturnType<typeof webkitTextStrokeWidth.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.boxShadow = parse(boxShadow, declaration.boxShadow);
|
||||
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);
|
||||
const overflowTuple = parse(overflow, declaration.overflow);
|
||||
constructor(context: Context, declaration: CSSStyleDeclaration) {
|
||||
this.backgroundClip = parse(context, backgroundClip, declaration.backgroundClip);
|
||||
this.backgroundColor = parse(context, backgroundColor, declaration.backgroundColor);
|
||||
this.backgroundImage = parse(context, backgroundImage, declaration.backgroundImage);
|
||||
this.backgroundOrigin = parse(context, backgroundOrigin, declaration.backgroundOrigin);
|
||||
this.backgroundPosition = parse(context, backgroundPosition, declaration.backgroundPosition);
|
||||
this.backgroundRepeat = parse(context, backgroundRepeat, declaration.backgroundRepeat);
|
||||
this.backgroundSize = parse(context, backgroundSize, declaration.backgroundSize);
|
||||
this.borderTopColor = parse(context, borderTopColor, declaration.borderTopColor);
|
||||
this.borderRightColor = parse(context, borderRightColor, declaration.borderRightColor);
|
||||
this.borderBottomColor = parse(context, borderBottomColor, declaration.borderBottomColor);
|
||||
this.borderLeftColor = parse(context, borderLeftColor, declaration.borderLeftColor);
|
||||
this.borderTopLeftRadius = parse(context, borderTopLeftRadius, declaration.borderTopLeftRadius);
|
||||
this.borderTopRightRadius = parse(context, borderTopRightRadius, declaration.borderTopRightRadius);
|
||||
this.borderBottomRightRadius = parse(context, borderBottomRightRadius, declaration.borderBottomRightRadius);
|
||||
this.borderBottomLeftRadius = parse(context, borderBottomLeftRadius, declaration.borderBottomLeftRadius);
|
||||
this.borderTopStyle = parse(context, borderTopStyle, declaration.borderTopStyle);
|
||||
this.borderRightStyle = parse(context, borderRightStyle, declaration.borderRightStyle);
|
||||
this.borderBottomStyle = parse(context, borderBottomStyle, declaration.borderBottomStyle);
|
||||
this.borderLeftStyle = parse(context, borderLeftStyle, declaration.borderLeftStyle);
|
||||
this.borderTopWidth = parse(context, borderTopWidth, declaration.borderTopWidth);
|
||||
this.borderRightWidth = parse(context, borderRightWidth, declaration.borderRightWidth);
|
||||
this.borderBottomWidth = parse(context, borderBottomWidth, declaration.borderBottomWidth);
|
||||
this.borderLeftWidth = parse(context, borderLeftWidth, declaration.borderLeftWidth);
|
||||
this.boxShadow = parse(context, boxShadow, declaration.boxShadow);
|
||||
this.color = parse(context, color, declaration.color);
|
||||
this.display = parse(context, display, declaration.display);
|
||||
this.float = parse(context, float, declaration.cssFloat);
|
||||
this.fontFamily = parse(context, fontFamily, declaration.fontFamily);
|
||||
this.fontSize = parse(context, fontSize, declaration.fontSize);
|
||||
this.fontStyle = parse(context, fontStyle, declaration.fontStyle);
|
||||
this.fontVariant = parse(context, fontVariant, declaration.fontVariant);
|
||||
this.fontWeight = parse(context, fontWeight, declaration.fontWeight);
|
||||
this.letterSpacing = parse(context, letterSpacing, declaration.letterSpacing);
|
||||
this.lineBreak = parse(context, lineBreak, declaration.lineBreak);
|
||||
this.lineHeight = parse(context, lineHeight, declaration.lineHeight);
|
||||
this.listStyleImage = parse(context, listStyleImage, declaration.listStyleImage);
|
||||
this.listStylePosition = parse(context, listStylePosition, declaration.listStylePosition);
|
||||
this.listStyleType = parse(context, listStyleType, declaration.listStyleType);
|
||||
this.marginTop = parse(context, marginTop, declaration.marginTop);
|
||||
this.marginRight = parse(context, marginRight, declaration.marginRight);
|
||||
this.marginBottom = parse(context, marginBottom, declaration.marginBottom);
|
||||
this.marginLeft = parse(context, marginLeft, declaration.marginLeft);
|
||||
this.opacity = parse(context, opacity, declaration.opacity);
|
||||
const overflowTuple = parse(context, overflow, declaration.overflow);
|
||||
this.overflowX = overflowTuple[0];
|
||||
this.overflowY = overflowTuple[overflowTuple.length > 1 ? 1 : 0];
|
||||
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.overflowWrap = parse(context, overflowWrap, declaration.overflowWrap);
|
||||
this.paddingTop = parse(context, paddingTop, declaration.paddingTop);
|
||||
this.paddingRight = parse(context, paddingRight, declaration.paddingRight);
|
||||
this.paddingBottom = parse(context, paddingBottom, declaration.paddingBottom);
|
||||
this.paddingLeft = parse(context, paddingLeft, declaration.paddingLeft);
|
||||
this.paintOrder = parse(context, paintOrder, declaration.paintOrder);
|
||||
this.position = parse(context, position, declaration.position);
|
||||
this.textAlign = parse(context, textAlign, declaration.textAlign);
|
||||
this.textDecorationColor = parse(
|
||||
context,
|
||||
textDecorationColor,
|
||||
declaration.textDecorationColor ?? declaration.color
|
||||
);
|
||||
this.textDecorationLine = parse(
|
||||
context,
|
||||
textDecorationLine,
|
||||
declaration.textDecorationLine ?? declaration.textDecoration
|
||||
);
|
||||
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);
|
||||
this.textShadow = parse(context, textShadow, declaration.textShadow);
|
||||
this.textTransform = parse(context, textTransform, declaration.textTransform);
|
||||
this.transform = parse(context, transform, declaration.transform);
|
||||
this.transformOrigin = parse(context, transformOrigin, declaration.transformOrigin);
|
||||
this.visibility = parse(context, visibility, declaration.visibility);
|
||||
this.webkitTextStrokeColor = parse(context, webkitTextStrokeColor, declaration.webkitTextStrokeColor);
|
||||
this.webkitTextStrokeWidth = parse(context, webkitTextStrokeWidth, declaration.webkitTextStrokeWidth);
|
||||
this.wordBreak = parse(context, wordBreak, declaration.wordBreak);
|
||||
this.zIndex = parse(context, zIndex, declaration.zIndex);
|
||||
}
|
||||
|
||||
isVisible(): boolean {
|
||||
@ -245,9 +260,9 @@ 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);
|
||||
constructor(context: Context, declaration: CSSStyleDeclaration) {
|
||||
this.content = parse(context, content, declaration.content);
|
||||
this.quotes = parse(context, quotes, declaration.quotes);
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,14 +270,14 @@ 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);
|
||||
constructor(context: Context, declaration: CSSStyleDeclaration) {
|
||||
this.counterIncrement = parse(context, counterIncrement, declaration.counterIncrement);
|
||||
this.counterReset = parse(context, counterReset, declaration.counterReset);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const parse = (descriptor: CSSPropertyDescriptor<any>, style?: string | null) => {
|
||||
const parse = (context: Context, descriptor: CSSPropertyDescriptor<any>, style?: string | null) => {
|
||||
const tokenizer = new Tokenizer();
|
||||
const value = style !== null && typeof style !== 'undefined' ? style.toString() : descriptor.initialValue;
|
||||
tokenizer.write(value);
|
||||
@ -270,21 +285,21 @@ const parse = (descriptor: CSSPropertyDescriptor<any>, style?: string | null) =>
|
||||
switch (descriptor.type) {
|
||||
case PropertyDescriptorParsingType.IDENT_VALUE:
|
||||
const token = parser.parseComponentValue();
|
||||
return descriptor.parse(isIdentToken(token) ? token.value : descriptor.initialValue);
|
||||
return descriptor.parse(context, isIdentToken(token) ? token.value : descriptor.initialValue);
|
||||
case PropertyDescriptorParsingType.VALUE:
|
||||
return descriptor.parse(parser.parseComponentValue());
|
||||
return descriptor.parse(context, parser.parseComponentValue());
|
||||
case PropertyDescriptorParsingType.LIST:
|
||||
return descriptor.parse(parser.parseComponentValues());
|
||||
return descriptor.parse(context, parser.parseComponentValues());
|
||||
case PropertyDescriptorParsingType.TOKEN_VALUE:
|
||||
return parser.parseComponentValue();
|
||||
case PropertyDescriptorParsingType.TYPE_VALUE:
|
||||
switch (descriptor.format) {
|
||||
case 'angle':
|
||||
return angle.parse(parser.parseComponentValue());
|
||||
return angle.parse(context, parser.parseComponentValue());
|
||||
case 'color':
|
||||
return colorType.parse(parser.parseComponentValue());
|
||||
return colorType.parse(context, parser.parseComponentValue());
|
||||
case 'image':
|
||||
return image.parse(parser.parseComponentValue());
|
||||
return image.parse(context, parser.parseComponentValue());
|
||||
case 'length':
|
||||
const length = parser.parseComponentValue();
|
||||
return isLength(length) ? length : ZERO_LENGTH;
|
||||
|
@ -1,27 +1,38 @@
|
||||
export class Bounds {
|
||||
readonly top: number;
|
||||
readonly left: number;
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
constructor(x: number, y: number, w: number, h: number) {
|
||||
this.left = x;
|
||||
this.top = y;
|
||||
this.width = w;
|
||||
this.height = h;
|
||||
}
|
||||
export class Bounds {
|
||||
constructor(readonly left: number, readonly top: number, readonly width: number, readonly height: number) {}
|
||||
|
||||
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);
|
||||
static fromClientRect(context: Context, clientRect: ClientRect): Bounds {
|
||||
return new Bounds(
|
||||
clientRect.left + context.windowBounds.left,
|
||||
clientRect.top + context.windowBounds.top,
|
||||
clientRect.width,
|
||||
clientRect.height
|
||||
);
|
||||
}
|
||||
|
||||
static fromDOMRectList(context: Context, domRectList: DOMRectList): Bounds {
|
||||
const domRect = domRectList[0];
|
||||
return domRect
|
||||
? new Bounds(
|
||||
domRect.x + context.windowBounds.left,
|
||||
domRect.y + context.windowBounds.top,
|
||||
domRect.width,
|
||||
domRect.height
|
||||
)
|
||||
: Bounds.EMPTY;
|
||||
}
|
||||
|
||||
static EMPTY = new Bounds(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
export const parseBounds = (node: Element): Bounds => {
|
||||
return Bounds.fromClientRect(node.getBoundingClientRect());
|
||||
export const parseBounds = (context: Context, node: Element): Bounds => {
|
||||
return Bounds.fromClientRect(context, node.getBoundingClientRect());
|
||||
};
|
||||
|
||||
export const parseDocumentSize = (document: Document): Bounds => {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {OVERFLOW_WRAP} from '../property-descriptors/overflow-wrap';
|
||||
import {CSSParsedDeclaration} from '../index';
|
||||
import {fromCodePoint, LineBreaker, toCodePoints} from 'css-line-break';
|
||||
import {splitGraphemes} from 'text-segmentation';
|
||||
import {Bounds, parseBounds} from './bounds';
|
||||
import {FEATURES} from '../../core/features';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export class TextBounds {
|
||||
readonly text: string;
|
||||
@ -14,17 +14,31 @@ export class TextBounds {
|
||||
}
|
||||
}
|
||||
|
||||
export const parseTextBounds = (value: string, styles: CSSParsedDeclaration, node: Text): TextBounds[] => {
|
||||
const textList = breakText(value, styles);
|
||||
export const parseTextBounds = (
|
||||
context: Context,
|
||||
value: string,
|
||||
styles: CSSParsedDeclaration,
|
||||
node: Text
|
||||
): TextBounds[] => {
|
||||
const textList = splitGraphemes(value);
|
||||
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)));
|
||||
if (!FEATURES.SUPPORT_WORD_BREAKING) {
|
||||
textBounds.push(
|
||||
new TextBounds(
|
||||
text,
|
||||
Bounds.fromDOMRectList(context, createRange(node, offset, text.length).getClientRects())
|
||||
)
|
||||
);
|
||||
} else {
|
||||
textBounds.push(new TextBounds(text, getRangeBounds(context, node, offset, text.length)));
|
||||
}
|
||||
} else {
|
||||
const replacementNode = node.splitText(text.length);
|
||||
textBounds.push(new TextBounds(text, getWrapperBounds(node)));
|
||||
textBounds.push(new TextBounds(text, getWrapperBounds(context, node)));
|
||||
node = replacementNode;
|
||||
}
|
||||
} else if (!FEATURES.SUPPORT_RANGE_BOUNDS) {
|
||||
@ -36,7 +50,7 @@ export const parseTextBounds = (value: string, styles: CSSParsedDeclaration, nod
|
||||
return textBounds;
|
||||
};
|
||||
|
||||
const getWrapperBounds = (node: Text): Bounds => {
|
||||
const getWrapperBounds = (context: Context, node: Text): Bounds => {
|
||||
const ownerDocument = node.ownerDocument;
|
||||
if (ownerDocument) {
|
||||
const wrapper = ownerDocument.createElement('html2canvaswrapper');
|
||||
@ -44,7 +58,7 @@ const getWrapperBounds = (node: Text): Bounds => {
|
||||
const parentNode = node.parentNode;
|
||||
if (parentNode) {
|
||||
parentNode.replaceChild(wrapper, node);
|
||||
const bounds = parseBounds(wrapper);
|
||||
const bounds = parseBounds(context, wrapper);
|
||||
if (wrapper.firstChild) {
|
||||
parentNode.replaceChild(wrapper.firstChild, wrapper);
|
||||
}
|
||||
@ -52,10 +66,10 @@ const getWrapperBounds = (node: Text): Bounds => {
|
||||
}
|
||||
}
|
||||
|
||||
return new Bounds(0, 0, 0, 0);
|
||||
return Bounds.EMPTY;
|
||||
};
|
||||
|
||||
const getRangeBounds = (node: Text, offset: number, length: number): Bounds => {
|
||||
const createRange = (node: Text, offset: number, length: number): Range => {
|
||||
const ownerDocument = node.ownerDocument;
|
||||
if (!ownerDocument) {
|
||||
throw new Error('Node has no owner document');
|
||||
@ -63,27 +77,9 @@ const getRangeBounds = (node: Text, offset: number, length: number): Bounds => {
|
||||
const range = ownerDocument.createRange();
|
||||
range.setStart(node, offset);
|
||||
range.setEnd(node, offset + length);
|
||||
return Bounds.fromClientRect(range.getBoundingClientRect());
|
||||
return range;
|
||||
};
|
||||
|
||||
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;
|
||||
const getRangeBounds = (context: Context, node: Text, offset: number, length: number): Bounds => {
|
||||
return Bounds.fromClientRect(context, createRange(node, offset, length).getBoundingClientRect());
|
||||
};
|
||||
|
@ -5,27 +5,42 @@ import {CSSImageType} from '../../types/image';
|
||||
import {pack} from '../../types/color';
|
||||
import {deg} from '../../types/angle';
|
||||
|
||||
jest.mock('../../../core/cache-storage');
|
||||
jest.mock('../../../core/context');
|
||||
import {Context} from '../../../core/context';
|
||||
|
||||
jest.mock('../../../core/features');
|
||||
|
||||
const backgroundImageParse = (value: string) => backgroundImage.parse(Parser.parseValues(value));
|
||||
const backgroundImageParse = (context: Context, value: string) =>
|
||||
backgroundImage.parse(context, Parser.parseValues(value));
|
||||
|
||||
describe('property-descriptors', () => {
|
||||
let context: Context;
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
context = new Context({} as any, {} as any);
|
||||
});
|
||||
describe('background-image', () => {
|
||||
it('none', () => deepStrictEqual(backgroundImageParse('none'), []));
|
||||
it('none', () => {
|
||||
deepStrictEqual(backgroundImageParse(context, 'none'), []);
|
||||
expect(context.cache.addImage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('url(test.jpg), url(test2.jpg)', () =>
|
||||
it('url(test.jpg), url(test2.jpg)', () => {
|
||||
deepStrictEqual(
|
||||
backgroundImageParse('url(http://example.com/test.jpg), url(http://example.com/test2.jpg)'),
|
||||
backgroundImageParse(context, '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}
|
||||
]
|
||||
));
|
||||
);
|
||||
expect(context.cache.addImage).toHaveBeenCalledWith('http://example.com/test.jpg');
|
||||
expect(context.cache.addImage).toHaveBeenCalledWith('http://example.com/test2.jpg');
|
||||
});
|
||||
|
||||
it(`linear-gradient(to bottom, rgba(255,255,0,0.5), rgba(0,0,255,0.5)), url('https://html2canvas.hertzen.com')`, () =>
|
||||
deepStrictEqual(
|
||||
backgroundImageParse(
|
||||
context,
|
||||
`linear-gradient(to bottom, rgba(255,255,0,0.5), rgba(0,0,255,0.5)), url('https://html2canvas.hertzen.com')`
|
||||
),
|
||||
[
|
||||
|
@ -1,8 +1,9 @@
|
||||
import {deepEqual} from 'assert';
|
||||
import {Parser} from '../../syntax/parser';
|
||||
import {fontFamily} from '../font-family';
|
||||
import {Context} from '../../../core/context';
|
||||
|
||||
const fontFamilyParse = (value: string) => fontFamily.parse(Parser.parseValues(value));
|
||||
const fontFamilyParse = (value: string) => fontFamily.parse({} as Context, Parser.parseValues(value));
|
||||
|
||||
describe('property-descriptors', () => {
|
||||
describe('font-family', () => {
|
||||
|
87
src/css/property-descriptors/__tests__/paint-order.ts
Normal file
87
src/css/property-descriptors/__tests__/paint-order.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import {deepStrictEqual} from 'assert';
|
||||
import {Parser} from '../../syntax/parser';
|
||||
import {paintOrder, PAINT_ORDER_LAYER} from '../paint-order';
|
||||
import {Context} from '../../../core/context';
|
||||
|
||||
const paintOrderParse = (value: string) => paintOrder.parse({} as Context, Parser.parseValues(value));
|
||||
|
||||
describe('property-descriptors', () => {
|
||||
describe('paint-order', () => {
|
||||
it('none', () =>
|
||||
deepStrictEqual(paintOrderParse('none'), [
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.MARKERS
|
||||
]));
|
||||
|
||||
it('EMPTY', () =>
|
||||
deepStrictEqual(paintOrderParse(''), [
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.MARKERS
|
||||
]));
|
||||
|
||||
it('other values', () =>
|
||||
deepStrictEqual(paintOrderParse('other values'), [
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.MARKERS
|
||||
]));
|
||||
|
||||
it('normal', () =>
|
||||
deepStrictEqual(paintOrderParse('normal'), [
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.MARKERS
|
||||
]));
|
||||
|
||||
it('stroke', () =>
|
||||
deepStrictEqual(paintOrderParse('stroke'), [
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.MARKERS
|
||||
]));
|
||||
|
||||
it('fill', () =>
|
||||
deepStrictEqual(paintOrderParse('fill'), [
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.MARKERS
|
||||
]));
|
||||
|
||||
it('markers', () =>
|
||||
deepStrictEqual(paintOrderParse('markers'), [
|
||||
PAINT_ORDER_LAYER.MARKERS,
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.STROKE
|
||||
]));
|
||||
|
||||
it('stroke fill', () =>
|
||||
deepStrictEqual(paintOrderParse('stroke fill'), [
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.MARKERS
|
||||
]));
|
||||
|
||||
it('markers stroke', () =>
|
||||
deepStrictEqual(paintOrderParse('markers stroke'), [
|
||||
PAINT_ORDER_LAYER.MARKERS,
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.FILL
|
||||
]));
|
||||
|
||||
it('markers stroke fill', () =>
|
||||
deepStrictEqual(paintOrderParse('markers stroke fill'), [
|
||||
PAINT_ORDER_LAYER.MARKERS,
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.FILL
|
||||
]));
|
||||
|
||||
it('stroke fill markers', () =>
|
||||
deepStrictEqual(paintOrderParse('stroke fill markers'), [
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.MARKERS
|
||||
]));
|
||||
});
|
||||
});
|
@ -4,9 +4,10 @@ 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';
|
||||
import {Context} from '../../../core/context';
|
||||
|
||||
const textShadowParse = (value: string) => textShadow.parse(Parser.parseValues(value));
|
||||
const colorParse = (value: string) => color.parse(Parser.parseValue(value));
|
||||
const textShadowParse = (value: string) => textShadow.parse({} as Context, Parser.parseValues(value));
|
||||
const colorParse = (value: string) => color.parse({} as Context, Parser.parseValue(value));
|
||||
const dimension = (number: number, unit: string): DimensionToken => ({
|
||||
flags: FLAG_INTEGER,
|
||||
number,
|
||||
|
@ -1,7 +1,8 @@
|
||||
import {transform} from '../transform';
|
||||
import {Parser} from '../../syntax/parser';
|
||||
import {deepStrictEqual} from 'assert';
|
||||
const parseValue = (value: string) => transform.parse(Parser.parseValue(value));
|
||||
import {Context} from '../../../core/context';
|
||||
const parseValue = (value: string) => transform.parse({} as Context, Parser.parseValue(value));
|
||||
|
||||
describe('property-descriptors', () => {
|
||||
describe('transform', () => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
export enum BACKGROUND_CLIP {
|
||||
BORDER_BOX = 0,
|
||||
PADDING_BOX = 1,
|
||||
@ -13,7 +14,7 @@ export const backgroundClip: IPropertyListDescriptor<BackgroundClip> = {
|
||||
initialValue: 'border-box',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
parse: (tokens: CSSValue[]): BackgroundClip => {
|
||||
parse: (_context: Context, tokens: CSSValue[]): BackgroundClip => {
|
||||
return tokens.map((token) => {
|
||||
if (isIdentToken(token)) {
|
||||
switch (token.value) {
|
||||
|
@ -2,13 +2,14 @@ import {TokenType} from '../syntax/tokenizer';
|
||||
import {ICSSImage, image, isSupportedImage} from '../types/image';
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, nonFunctionArgSeparator} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export const backgroundImage: IPropertyListDescriptor<ICSSImage[]> = {
|
||||
name: 'background-image',
|
||||
initialValue: 'none',
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
prefix: false,
|
||||
parse: (tokens: CSSValue[]) => {
|
||||
parse: (context: Context, tokens: CSSValue[]) => {
|
||||
if (tokens.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@ -19,6 +20,8 @@ export const backgroundImage: IPropertyListDescriptor<ICSSImage[]> = {
|
||||
return [];
|
||||
}
|
||||
|
||||
return tokens.filter((value) => nonFunctionArgSeparator(value) && isSupportedImage(value)).map(image.parse);
|
||||
return tokens
|
||||
.filter((value) => nonFunctionArgSeparator(value) && isSupportedImage(value))
|
||||
.map((value) => image.parse(context, value));
|
||||
}
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export const enum BACKGROUND_ORIGIN {
|
||||
BORDER_BOX = 0,
|
||||
@ -14,7 +15,7 @@ export const backgroundOrigin: IPropertyListDescriptor<BackgroundOrigin> = {
|
||||
initialValue: 'border-box',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
parse: (tokens: CSSValue[]): BackgroundOrigin => {
|
||||
parse: (_context: Context, tokens: CSSValue[]): BackgroundOrigin => {
|
||||
return tokens.map((token) => {
|
||||
if (isIdentToken(token)) {
|
||||
switch (token.value) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {PropertyDescriptorParsingType, IPropertyListDescriptor} from '../IPropertyDescriptor';
|
||||
import {CSSValue, parseFunctionArgs} from '../syntax/parser';
|
||||
import {isLengthPercentage, LengthPercentageTuple, parseLengthPercentageTuple} from '../types/length-percentage';
|
||||
import {Context} from '../../core/context';
|
||||
export type BackgroundPosition = BackgroundImagePosition[];
|
||||
|
||||
export type BackgroundImagePosition = LengthPercentageTuple;
|
||||
@ -10,7 +11,7 @@ export const backgroundPosition: IPropertyListDescriptor<BackgroundPosition> = {
|
||||
initialValue: '0% 0%',
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
prefix: false,
|
||||
parse: (tokens: CSSValue[]): BackgroundPosition => {
|
||||
parse: (_context: Context, tokens: CSSValue[]): BackgroundPosition => {
|
||||
return parseFunctionArgs(tokens)
|
||||
.map((values: CSSValue[]) => values.filter(isLengthPercentage))
|
||||
.map(parseLengthPercentageTuple);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken, parseFunctionArgs} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
export type BackgroundRepeat = BACKGROUND_REPEAT[];
|
||||
|
||||
export enum BACKGROUND_REPEAT {
|
||||
@ -14,7 +15,7 @@ export const backgroundRepeat: IPropertyListDescriptor<BackgroundRepeat> = {
|
||||
initialValue: 'repeat',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
parse: (tokens: CSSValue[]): BackgroundRepeat => {
|
||||
parse: (_context: Context, tokens: CSSValue[]): BackgroundRepeat => {
|
||||
return parseFunctionArgs(tokens)
|
||||
.map((values) =>
|
||||
values
|
||||
|
@ -2,6 +2,7 @@ import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IProper
|
||||
import {CSSValue, isIdentToken, parseFunctionArgs} from '../syntax/parser';
|
||||
import {isLengthPercentage, LengthPercentage} from '../types/length-percentage';
|
||||
import {StringValueToken} from '../syntax/tokenizer';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export enum BACKGROUND_SIZE {
|
||||
AUTO = 'auto',
|
||||
@ -17,7 +18,7 @@ export const backgroundSize: IPropertyListDescriptor<BackgroundSize> = {
|
||||
initialValue: '0',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
parse: (tokens: CSSValue[]): BackgroundSize => {
|
||||
parse: (_context: Context, tokens: CSSValue[]): BackgroundSize => {
|
||||
return parseFunctionArgs(tokens).map((values) => values.filter(isBackgroundSizeInfoToken));
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue} from '../syntax/parser';
|
||||
import {isLengthPercentage, LengthPercentageTuple, parseLengthPercentageTuple} from '../types/length-percentage';
|
||||
import {Context} from '../../core/context';
|
||||
export type BorderRadius = LengthPercentageTuple;
|
||||
|
||||
const borderRadiusForSide = (side: string): IPropertyListDescriptor<BorderRadius> => ({
|
||||
@ -8,7 +9,8 @@ const borderRadiusForSide = (side: string): IPropertyListDescriptor<BorderRadius
|
||||
initialValue: '0 0',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
parse: (tokens: CSSValue[]): BorderRadius => parseLengthPercentageTuple(tokens.filter(isLengthPercentage))
|
||||
parse: (_context: Context, tokens: CSSValue[]): BorderRadius =>
|
||||
parseLengthPercentageTuple(tokens.filter(isLengthPercentage))
|
||||
});
|
||||
|
||||
export const borderTopLeftRadius: IPropertyListDescriptor<BorderRadius> = borderRadiusForSide('top-left');
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
export enum BORDER_STYLE {
|
||||
NONE = 0,
|
||||
SOLID = 1,
|
||||
@ -12,7 +13,7 @@ const borderStyleForSide = (side: string): IPropertyIdentValueDescriptor<BORDER_
|
||||
initialValue: 'solid',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.IDENT_VALUE,
|
||||
parse: (style: string): BORDER_STYLE => {
|
||||
parse: (_context: Context, style: string): BORDER_STYLE => {
|
||||
switch (style) {
|
||||
case 'none':
|
||||
return BORDER_STYLE.NONE;
|
||||
|
@ -1,11 +1,12 @@
|
||||
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isDimensionToken} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
const borderWidthForSide = (side: string): IPropertyValueDescriptor<number> => ({
|
||||
name: `border-${side}-width`,
|
||||
initialValue: '0',
|
||||
type: PropertyDescriptorParsingType.VALUE,
|
||||
prefix: false,
|
||||
parse: (token: CSSValue): number => {
|
||||
parse: (_context: Context, token: CSSValue): number => {
|
||||
if (isDimensionToken(token)) {
|
||||
return token.number;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import {CSSValue, isIdentWithValue, parseFunctionArgs} from '../syntax/parser';
|
||||
import {ZERO_LENGTH} from '../types/length-percentage';
|
||||
import {color, Color} from '../types/color';
|
||||
import {isLength, Length} from '../types/length';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export type BoxShadow = BoxShadowItem[];
|
||||
interface BoxShadowItem {
|
||||
@ -19,7 +20,7 @@ export const boxShadow: IPropertyListDescriptor<BoxShadow> = {
|
||||
initialValue: 'none',
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
prefix: false,
|
||||
parse: (tokens: CSSValue[]): BoxShadow => {
|
||||
parse: (context: Context, tokens: CSSValue[]): BoxShadow => {
|
||||
if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) {
|
||||
return [];
|
||||
}
|
||||
@ -50,7 +51,7 @@ export const boxShadow: IPropertyListDescriptor<BoxShadow> = {
|
||||
}
|
||||
c++;
|
||||
} else {
|
||||
shadow.color = color.parse(token);
|
||||
shadow.color = color.parse(context, token);
|
||||
}
|
||||
}
|
||||
return shadow;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {TokenType} from '../syntax/tokenizer';
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export type Content = CSSValue[];
|
||||
|
||||
@ -9,7 +10,7 @@ export const content: IPropertyListDescriptor<Content> = {
|
||||
initialValue: 'none',
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
prefix: false,
|
||||
parse: (tokens: CSSValue[]) => {
|
||||
parse: (_context: Context, tokens: CSSValue[]) => {
|
||||
if (tokens.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isNumberToken, nonWhiteSpace} from '../syntax/parser';
|
||||
import {TokenType} from '../syntax/tokenizer';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export interface COUNTER_INCREMENT {
|
||||
counter: string;
|
||||
@ -14,7 +15,7 @@ export const counterIncrement: IPropertyListDescriptor<CounterIncrement> = {
|
||||
initialValue: 'none',
|
||||
prefix: true,
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
parse: (tokens: CSSValue[]) => {
|
||||
parse: (_context: Context, tokens: CSSValue[]) => {
|
||||
if (tokens.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken, isNumberToken, nonWhiteSpace} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export interface COUNTER_RESET {
|
||||
counter: string;
|
||||
@ -13,7 +14,7 @@ export const counterReset: IPropertyListDescriptor<CounterReset> = {
|
||||
initialValue: 'none',
|
||||
prefix: true,
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
parse: (tokens: CSSValue[]) => {
|
||||
parse: (_context: Context, tokens: CSSValue[]) => {
|
||||
if (tokens.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
export const enum DISPLAY {
|
||||
NONE = 0,
|
||||
BLOCK = 1 << 1,
|
||||
@ -40,7 +41,7 @@ export const display: IPropertyListDescriptor<Display> = {
|
||||
initialValue: 'inline-block',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
parse: (tokens: CSSValue[]): Display => {
|
||||
parse: (_context: Context, tokens: CSSValue[]): Display => {
|
||||
return tokens.filter(isIdentToken).reduce((bit, token) => {
|
||||
return bit | parseDisplayValue(token.value);
|
||||
}, DISPLAY.NONE);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
export enum FLOAT {
|
||||
NONE = 0,
|
||||
LEFT = 1,
|
||||
@ -12,7 +13,7 @@ export const float: IPropertyIdentValueDescriptor<FLOAT> = {
|
||||
initialValue: 'none',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.IDENT_VALUE,
|
||||
parse: (float: string) => {
|
||||
parse: (_context: Context, float: string) => {
|
||||
switch (float) {
|
||||
case 'left':
|
||||
return FLOAT.LEFT;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue} from '../syntax/parser';
|
||||
import {TokenType} from '../syntax/tokenizer';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export type FONT_FAMILY = string;
|
||||
|
||||
@ -11,7 +12,7 @@ export const fontFamily: IPropertyListDescriptor<FontFamily> = {
|
||||
initialValue: '',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
parse: (tokens: CSSValue[]) => {
|
||||
parse: (_context: Context, tokens: CSSValue[]) => {
|
||||
const accumulator: string[] = [];
|
||||
const results: string[] = [];
|
||||
tokens.forEach((token) => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
export enum FONT_STYLE {
|
||||
NORMAL = 'normal',
|
||||
ITALIC = 'italic',
|
||||
@ -10,7 +11,7 @@ export const fontStyle: IPropertyIdentValueDescriptor<FONT_STYLE> = {
|
||||
initialValue: 'normal',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.IDENT_VALUE,
|
||||
parse: (overflow: string) => {
|
||||
parse: (_context: Context, overflow: string) => {
|
||||
switch (overflow) {
|
||||
case 'oblique':
|
||||
return FONT_STYLE.OBLIQUE;
|
||||
|
@ -1,11 +1,12 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
export const fontVariant: IPropertyListDescriptor<string[]> = {
|
||||
name: 'font-variant',
|
||||
initialValue: 'none',
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
prefix: false,
|
||||
parse: (tokens: CSSValue[]): string[] => {
|
||||
parse: (_context: Context, tokens: CSSValue[]): string[] => {
|
||||
return tokens.filter(isIdentToken).map((token) => token.value);
|
||||
}
|
||||
};
|
||||
|
@ -1,11 +1,12 @@
|
||||
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken, isNumberToken} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
export const fontWeight: IPropertyValueDescriptor<number> = {
|
||||
name: 'font-weight',
|
||||
initialValue: 'normal',
|
||||
type: PropertyDescriptorParsingType.VALUE,
|
||||
prefix: false,
|
||||
parse: (token: CSSValue): number => {
|
||||
parse: (_context: Context, token: CSSValue): number => {
|
||||
if (isNumberToken(token)) {
|
||||
return token.number;
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue} from '../syntax/parser';
|
||||
import {TokenType} from '../syntax/tokenizer';
|
||||
import {Context} from '../../core/context';
|
||||
export const letterSpacing: IPropertyValueDescriptor<number> = {
|
||||
name: 'letter-spacing',
|
||||
initialValue: '0',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.VALUE,
|
||||
parse: (token: CSSValue) => {
|
||||
parse: (_context: Context, token: CSSValue) => {
|
||||
if (token.type === TokenType.IDENT_TOKEN && token.value === 'normal') {
|
||||
return 0;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
export enum LINE_BREAK {
|
||||
NORMAL = 'normal',
|
||||
STRICT = 'strict'
|
||||
@ -9,7 +10,7 @@ export const lineBreak: IPropertyIdentValueDescriptor<LINE_BREAK> = {
|
||||
initialValue: 'normal',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.IDENT_VALUE,
|
||||
parse: (lineBreak: string): LINE_BREAK => {
|
||||
parse: (_context: Context, lineBreak: string): LINE_BREAK => {
|
||||
switch (lineBreak) {
|
||||
case 'strict':
|
||||
return LINE_BREAK.STRICT;
|
||||
|
@ -2,17 +2,18 @@ import {TokenType} from '../syntax/tokenizer';
|
||||
import {ICSSImage, image} from '../types/image';
|
||||
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export const listStyleImage: IPropertyValueDescriptor<ICSSImage | null> = {
|
||||
name: 'list-style-image',
|
||||
initialValue: 'none',
|
||||
type: PropertyDescriptorParsingType.VALUE,
|
||||
prefix: false,
|
||||
parse: (token: CSSValue) => {
|
||||
parse: (context: Context, token: CSSValue) => {
|
||||
if (token.type === TokenType.IDENT_TOKEN && token.value === 'none') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return image.parse(token);
|
||||
return image.parse(context, token);
|
||||
}
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
export enum LIST_STYLE_POSITION {
|
||||
INSIDE = 0,
|
||||
OUTSIDE = 1
|
||||
@ -9,7 +10,7 @@ export const listStylePosition: IPropertyIdentValueDescriptor<LIST_STYLE_POSITIO
|
||||
initialValue: 'outside',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.IDENT_VALUE,
|
||||
parse: (position: string) => {
|
||||
parse: (_context: Context, position: string) => {
|
||||
switch (position) {
|
||||
case 'inside':
|
||||
return LIST_STYLE_POSITION.INSIDE;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
export enum LIST_STYLE_TYPE {
|
||||
NONE = -1,
|
||||
DISC = 0,
|
||||
@ -61,7 +62,7 @@ export const listStyleType: IPropertyIdentValueDescriptor<LIST_STYLE_TYPE> = {
|
||||
initialValue: 'none',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.IDENT_VALUE,
|
||||
parse: (type: string) => {
|
||||
parse: (_context: Context, type: string) => {
|
||||
switch (type) {
|
||||
case 'disc':
|
||||
return LIST_STYLE_TYPE.DISC;
|
||||
|
@ -1,11 +1,12 @@
|
||||
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isNumberToken} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
export const opacity: IPropertyValueDescriptor<number> = {
|
||||
name: 'opacity',
|
||||
initialValue: '1',
|
||||
type: PropertyDescriptorParsingType.VALUE,
|
||||
prefix: false,
|
||||
parse: (token: CSSValue): number => {
|
||||
parse: (_context: Context, token: CSSValue): number => {
|
||||
if (isNumberToken(token)) {
|
||||
return token.number;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
export enum OVERFLOW_WRAP {
|
||||
NORMAL = 'normal',
|
||||
BREAK_WORD = 'break-word'
|
||||
@ -9,7 +10,7 @@ export const overflowWrap: IPropertyIdentValueDescriptor<OVERFLOW_WRAP> = {
|
||||
initialValue: 'normal',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.IDENT_VALUE,
|
||||
parse: (overflow: string) => {
|
||||
parse: (_context: Context, overflow: string) => {
|
||||
switch (overflow) {
|
||||
case 'break-word':
|
||||
return OVERFLOW_WRAP.BREAK_WORD;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
export enum OVERFLOW {
|
||||
VISIBLE = 0,
|
||||
HIDDEN = 1,
|
||||
@ -12,7 +13,7 @@ export const overflow: IPropertyListDescriptor<OVERFLOW[]> = {
|
||||
initialValue: 'visible',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
parse: (tokens: CSSValue[]): OVERFLOW[] => {
|
||||
parse: (_context: Context, tokens: CSSValue[]): OVERFLOW[] => {
|
||||
return tokens.filter(isIdentToken).map((overflow) => {
|
||||
switch (overflow.value) {
|
||||
case 'hidden':
|
||||
|
42
src/css/property-descriptors/paint-order.ts
Normal file
42
src/css/property-descriptors/paint-order.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
export enum PAINT_ORDER_LAYER {
|
||||
FILL,
|
||||
STROKE,
|
||||
MARKERS
|
||||
}
|
||||
|
||||
export type PaintOrder = PAINT_ORDER_LAYER[];
|
||||
|
||||
export const paintOrder: IPropertyListDescriptor<PaintOrder> = {
|
||||
name: 'paint-order',
|
||||
initialValue: 'normal',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
parse: (_context: Context, tokens: CSSValue[]): PaintOrder => {
|
||||
const DEFAULT_VALUE = [PAINT_ORDER_LAYER.FILL, PAINT_ORDER_LAYER.STROKE, PAINT_ORDER_LAYER.MARKERS];
|
||||
const layers: PaintOrder = [];
|
||||
|
||||
tokens.filter(isIdentToken).forEach((token) => {
|
||||
switch (token.value) {
|
||||
case 'stroke':
|
||||
layers.push(PAINT_ORDER_LAYER.STROKE);
|
||||
break;
|
||||
case 'fill':
|
||||
layers.push(PAINT_ORDER_LAYER.FILL);
|
||||
break;
|
||||
case 'markers':
|
||||
layers.push(PAINT_ORDER_LAYER.MARKERS);
|
||||
break;
|
||||
}
|
||||
});
|
||||
DEFAULT_VALUE.forEach((value) => {
|
||||
if (layers.indexOf(value) === -1) {
|
||||
layers.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
return layers;
|
||||
}
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
export enum POSITION {
|
||||
STATIC = 0,
|
||||
RELATIVE = 1,
|
||||
@ -12,7 +13,7 @@ export const position: IPropertyIdentValueDescriptor<POSITION> = {
|
||||
initialValue: 'static',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.IDENT_VALUE,
|
||||
parse: (position: string) => {
|
||||
parse: (_context: Context, position: string) => {
|
||||
switch (position) {
|
||||
case 'relative':
|
||||
return POSITION.RELATIVE;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isStringToken} from '../syntax/parser';
|
||||
import {TokenType} from '../syntax/tokenizer';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export interface QUOTE {
|
||||
open: string;
|
||||
@ -14,7 +15,7 @@ export const quotes: IPropertyListDescriptor<Quotes> = {
|
||||
initialValue: 'none',
|
||||
prefix: true,
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
parse: (tokens: CSSValue[]) => {
|
||||
parse: (_context: Context, tokens: CSSValue[]) => {
|
||||
if (tokens.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
export enum TEXT_ALIGN {
|
||||
LEFT = 0,
|
||||
CENTER = 1,
|
||||
@ -10,7 +11,7 @@ export const textAlign: IPropertyIdentValueDescriptor<TEXT_ALIGN> = {
|
||||
initialValue: 'left',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.IDENT_VALUE,
|
||||
parse: (textAlign: string) => {
|
||||
parse: (_context: Context, textAlign: string) => {
|
||||
switch (textAlign) {
|
||||
case 'right':
|
||||
return TEXT_ALIGN.RIGHT;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export const enum TEXT_DECORATION_LINE {
|
||||
NONE = 0,
|
||||
@ -16,7 +17,7 @@ export const textDecorationLine: IPropertyListDescriptor<TextDecorationLine> = {
|
||||
initialValue: 'none',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
parse: (tokens: CSSValue[]): TextDecorationLine => {
|
||||
parse: (_context: Context, tokens: CSSValue[]): TextDecorationLine => {
|
||||
return tokens
|
||||
.filter(isIdentToken)
|
||||
.map((token) => {
|
||||
|
@ -3,6 +3,7 @@ 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';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export type TextShadow = TextShadowItem[];
|
||||
interface TextShadowItem {
|
||||
@ -17,7 +18,7 @@ export const textShadow: IPropertyListDescriptor<TextShadow> = {
|
||||
initialValue: 'none',
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
prefix: false,
|
||||
parse: (tokens: CSSValue[]): TextShadow => {
|
||||
parse: (context: Context, tokens: CSSValue[]): TextShadow => {
|
||||
if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) {
|
||||
return [];
|
||||
}
|
||||
@ -42,7 +43,7 @@ export const textShadow: IPropertyListDescriptor<TextShadow> = {
|
||||
}
|
||||
c++;
|
||||
} else {
|
||||
shadow.color = color.parse(token);
|
||||
shadow.color = color.parse(context, token);
|
||||
}
|
||||
}
|
||||
return shadow;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
export enum TEXT_TRANSFORM {
|
||||
NONE = 0,
|
||||
LOWERCASE = 1,
|
||||
@ -11,7 +12,7 @@ export const textTransform: IPropertyIdentValueDescriptor<TEXT_TRANSFORM> = {
|
||||
initialValue: 'none',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.IDENT_VALUE,
|
||||
parse: (textTransform: string) => {
|
||||
parse: (_context: Context, textTransform: string) => {
|
||||
switch (textTransform) {
|
||||
case 'uppercase':
|
||||
return TEXT_TRANSFORM.UPPERCASE;
|
||||
|
@ -2,6 +2,7 @@ import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IProper
|
||||
import {CSSValue} from '../syntax/parser';
|
||||
import {isLengthPercentage, LengthPercentage} from '../types/length-percentage';
|
||||
import {FLAG_INTEGER, TokenType} from '../syntax/tokenizer';
|
||||
import {Context} from '../../core/context';
|
||||
export type TransformOrigin = [LengthPercentage, LengthPercentage];
|
||||
|
||||
const DEFAULT_VALUE: LengthPercentage = {
|
||||
@ -16,7 +17,7 @@ export const transformOrigin: IPropertyListDescriptor<TransformOrigin> = {
|
||||
initialValue: '50% 50%',
|
||||
prefix: true,
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
parse: (tokens: CSSValue[]) => {
|
||||
parse: (_context: Context, tokens: CSSValue[]) => {
|
||||
const origins: LengthPercentage[] = tokens.filter(isLengthPercentage);
|
||||
|
||||
if (origins.length !== 2) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue} from '../syntax/parser';
|
||||
import {NumberValueToken, TokenType} from '../syntax/tokenizer';
|
||||
import {Context} from '../../core/context';
|
||||
export type Matrix = [number, number, number, number, number, number];
|
||||
export type Transform = Matrix | null;
|
||||
|
||||
@ -9,7 +10,7 @@ export const transform: IPropertyValueDescriptor<Transform> = {
|
||||
initialValue: 'none',
|
||||
prefix: true,
|
||||
type: PropertyDescriptorParsingType.VALUE,
|
||||
parse: (token: CSSValue) => {
|
||||
parse: (_context: Context, token: CSSValue) => {
|
||||
if (token.type === TokenType.IDENT_TOKEN && token.value === 'none') {
|
||||
return null;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
export enum VISIBILITY {
|
||||
VISIBLE = 0,
|
||||
HIDDEN = 1,
|
||||
@ -10,7 +11,7 @@ export const visibility: IPropertyIdentValueDescriptor<VISIBILITY> = {
|
||||
initialValue: 'none',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.IDENT_VALUE,
|
||||
parse: (visibility: string) => {
|
||||
parse: (_context: Context, visibility: string) => {
|
||||
switch (visibility) {
|
||||
case 'hidden':
|
||||
return VISIBILITY.HIDDEN;
|
||||
|
8
src/css/property-descriptors/webkit-text-stroke-color.ts
Normal file
8
src/css/property-descriptors/webkit-text-stroke-color.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
export const webkitTextStrokeColor: IPropertyTypeValueDescriptor = {
|
||||
name: `-webkit-text-stroke-color`,
|
||||
initialValue: 'currentcolor',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.TYPE_VALUE,
|
||||
format: 'color'
|
||||
};
|
15
src/css/property-descriptors/webkit-text-stroke-width.ts
Normal file
15
src/css/property-descriptors/webkit-text-stroke-width.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {CSSValue, isDimensionToken} from '../syntax/parser';
|
||||
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
export const webkitTextStrokeWidth: IPropertyValueDescriptor<number> = {
|
||||
name: `-webkit-text-stroke-width`,
|
||||
initialValue: '0',
|
||||
type: PropertyDescriptorParsingType.VALUE,
|
||||
prefix: false,
|
||||
parse: (_context: Context, token: CSSValue): number => {
|
||||
if (isDimensionToken(token)) {
|
||||
return token.number;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
export enum WORD_BREAK {
|
||||
NORMAL = 'normal',
|
||||
BREAK_ALL = 'break-all',
|
||||
@ -10,7 +11,7 @@ export const wordBreak: IPropertyIdentValueDescriptor<WORD_BREAK> = {
|
||||
initialValue: 'normal',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.IDENT_VALUE,
|
||||
parse: (wordBreak: string): WORD_BREAK => {
|
||||
parse: (_context: Context, wordBreak: string): WORD_BREAK => {
|
||||
switch (wordBreak) {
|
||||
case 'break-all':
|
||||
return WORD_BREAK.BREAK_ALL;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isNumberToken} from '../syntax/parser';
|
||||
import {TokenType} from '../syntax/tokenizer';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
interface zIndex {
|
||||
order: number;
|
||||
@ -12,7 +13,7 @@ export const zIndex: IPropertyValueDescriptor<zIndex> = {
|
||||
initialValue: 'auto',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.VALUE,
|
||||
parse: (token: CSSValue): zIndex => {
|
||||
parse: (_context: Context, token: CSSValue): zIndex => {
|
||||
if (token.type === TokenType.IDENT_TOKEN) {
|
||||
return {auto: true, order: 0};
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
// https://www.w3.org/TR/css-syntax-3
|
||||
|
||||
import {fromCodePoint, toCodePoints} from 'css-line-break';
|
||||
import {fromCodePoint, toCodePoints} from 'text-segmentation';
|
||||
|
||||
export enum TokenType {
|
||||
STRING_TOKEN,
|
||||
|
@ -1,8 +1,9 @@
|
||||
import {strictEqual} from 'assert';
|
||||
import {asString, color, isTransparent, pack} from '../color';
|
||||
import {Parser} from '../../syntax/parser';
|
||||
import {Context} from '../../../core/context';
|
||||
|
||||
const parse = (value: string) => color.parse(Parser.parseValue(value));
|
||||
const parse = (value: string) => color.parse({} as Context, Parser.parseValue(value));
|
||||
|
||||
describe('types', () => {
|
||||
describe('<color>', () => {
|
||||
|
@ -5,30 +5,38 @@ 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));
|
||||
const parse = (context: Context, value: string) => image.parse(context, Parser.parseValue(value));
|
||||
const colorParse = (context: Context, value: string) => color.parse(context, Parser.parseValue(value));
|
||||
|
||||
jest.mock('../../../core/cache-storage');
|
||||
jest.mock('../../../core/features');
|
||||
|
||||
jest.mock('../../../core/context');
|
||||
import {Context} from '../../../core/context';
|
||||
|
||||
describe('types', () => {
|
||||
let context: Context;
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
context = new Context({} as any, {} as any);
|
||||
});
|
||||
|
||||
describe('<image>', () => {
|
||||
describe('parsing', () => {
|
||||
describe('url', () => {
|
||||
it('url(test.jpg)', () =>
|
||||
deepStrictEqual(parse('url(http://example.com/test.jpg)'), {
|
||||
deepStrictEqual(parse(context, '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")'), {
|
||||
deepStrictEqual(parse(context, '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)'), {
|
||||
deepStrictEqual(parse(context, 'linear-gradient(#f69d3c, #3f87a6)'), {
|
||||
angle: deg(180),
|
||||
type: CSSImageType.LINEAR_GRADIENT,
|
||||
stops: [
|
||||
@ -37,60 +45,60 @@ describe('types', () => {
|
||||
]
|
||||
}));
|
||||
it('linear-gradient(yellow, blue)', () =>
|
||||
deepStrictEqual(parse('linear-gradient(yellow, blue)'), {
|
||||
deepStrictEqual(parse(context, 'linear-gradient(yellow, blue)'), {
|
||||
angle: deg(180),
|
||||
type: CSSImageType.LINEAR_GRADIENT,
|
||||
stops: [
|
||||
{color: colorParse('yellow'), stop: null},
|
||||
{color: colorParse('blue'), stop: null}
|
||||
{color: colorParse(context, 'yellow'), stop: null},
|
||||
{color: colorParse(context, 'blue'), stop: null}
|
||||
]
|
||||
}));
|
||||
it('linear-gradient(to bottom, yellow, blue)', () =>
|
||||
deepStrictEqual(parse('linear-gradient(to bottom, yellow, blue)'), {
|
||||
deepStrictEqual(parse(context, 'linear-gradient(to bottom, yellow, blue)'), {
|
||||
angle: deg(180),
|
||||
type: CSSImageType.LINEAR_GRADIENT,
|
||||
stops: [
|
||||
{color: colorParse('yellow'), stop: null},
|
||||
{color: colorParse('blue'), stop: null}
|
||||
{color: colorParse(context, 'yellow'), stop: null},
|
||||
{color: colorParse(context, 'blue'), stop: null}
|
||||
]
|
||||
}));
|
||||
it('linear-gradient(180deg, yellow, blue)', () =>
|
||||
deepStrictEqual(parse('linear-gradient(180deg, yellow, blue)'), {
|
||||
deepStrictEqual(parse(context, 'linear-gradient(180deg, yellow, blue)'), {
|
||||
angle: deg(180),
|
||||
type: CSSImageType.LINEAR_GRADIENT,
|
||||
stops: [
|
||||
{color: colorParse('yellow'), stop: null},
|
||||
{color: colorParse('blue'), stop: null}
|
||||
{color: colorParse(context, 'yellow'), stop: null},
|
||||
{color: colorParse(context, 'blue'), stop: null}
|
||||
]
|
||||
}));
|
||||
it('linear-gradient(to top, blue, yellow)', () =>
|
||||
deepStrictEqual(parse('linear-gradient(to top, blue, yellow)'), {
|
||||
deepStrictEqual(parse(context, 'linear-gradient(to top, blue, yellow)'), {
|
||||
angle: 0,
|
||||
type: CSSImageType.LINEAR_GRADIENT,
|
||||
stops: [
|
||||
{color: colorParse('blue'), stop: null},
|
||||
{color: colorParse('yellow'), stop: null}
|
||||
{color: colorParse(context, 'blue'), stop: null},
|
||||
{color: colorParse(context, 'yellow'), stop: null}
|
||||
]
|
||||
}));
|
||||
it('linear-gradient(to top right, blue, yellow)', () =>
|
||||
deepStrictEqual(parse('linear-gradient(to top right, blue, yellow)'), {
|
||||
deepStrictEqual(parse(context, '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}
|
||||
{color: colorParse(context, 'blue'), stop: null},
|
||||
{color: colorParse(context, 'yellow'), stop: null}
|
||||
]
|
||||
}));
|
||||
it('linear-gradient(to bottom, yellow 0%, blue 100%)', () =>
|
||||
deepStrictEqual(parse('linear-gradient(to bottom, yellow 0%, blue 100%)'), {
|
||||
deepStrictEqual(parse(context, 'linear-gradient(to bottom, yellow 0%, blue 100%)'), {
|
||||
angle: deg(180),
|
||||
type: CSSImageType.LINEAR_GRADIENT,
|
||||
stops: [
|
||||
{
|
||||
color: colorParse('yellow'),
|
||||
color: colorParse(context, 'yellow'),
|
||||
stop: {
|
||||
type: TokenType.PERCENTAGE_TOKEN,
|
||||
number: 0,
|
||||
@ -98,7 +106,7 @@ describe('types', () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
color: colorParse('blue'),
|
||||
color: colorParse(context, 'blue'),
|
||||
stop: {
|
||||
type: TokenType.PERCENTAGE_TOKEN,
|
||||
number: 100,
|
||||
@ -109,7 +117,7 @@ describe('types', () => {
|
||||
}));
|
||||
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)'),
|
||||
parse(context, 'linear-gradient(to top left, lightpink, lightpink 5px, white 5px, white 10px)'),
|
||||
{
|
||||
angle: [
|
||||
{type: TokenType.PERCENTAGE_TOKEN, number: 100, flags: 4},
|
||||
@ -117,9 +125,9 @@ describe('types', () => {
|
||||
],
|
||||
type: CSSImageType.LINEAR_GRADIENT,
|
||||
stops: [
|
||||
{color: colorParse('lightpink'), stop: null},
|
||||
{color: colorParse(context, 'lightpink'), stop: null},
|
||||
{
|
||||
color: colorParse('lightpink'),
|
||||
color: colorParse(context, 'lightpink'),
|
||||
stop: {
|
||||
type: TokenType.DIMENSION_TOKEN,
|
||||
number: 5,
|
||||
@ -128,7 +136,7 @@ describe('types', () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
color: colorParse('white'),
|
||||
color: colorParse(context, 'white'),
|
||||
stop: {
|
||||
type: TokenType.DIMENSION_TOKEN,
|
||||
number: 5,
|
||||
@ -137,7 +145,7 @@ describe('types', () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
color: colorParse('white'),
|
||||
color: colorParse(context, 'white'),
|
||||
stop: {
|
||||
type: TokenType.DIMENSION_TOKEN,
|
||||
number: 10,
|
||||
@ -152,13 +160,16 @@ describe('types', () => {
|
||||
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%)'),
|
||||
parse(
|
||||
context,
|
||||
'-webkit-linear-gradient(left, #cedbe9 0%, #aac5de 17%, #3a8bc2 84%, #26558b 100%)'
|
||||
),
|
||||
{
|
||||
angle: deg(90),
|
||||
type: CSSImageType.LINEAR_GRADIENT,
|
||||
stops: [
|
||||
{
|
||||
color: colorParse('#cedbe9'),
|
||||
color: colorParse(context, '#cedbe9'),
|
||||
stop: {
|
||||
type: TokenType.PERCENTAGE_TOKEN,
|
||||
number: 0,
|
||||
@ -166,7 +177,7 @@ describe('types', () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
color: colorParse('#aac5de'),
|
||||
color: colorParse(context, '#aac5de'),
|
||||
stop: {
|
||||
type: TokenType.PERCENTAGE_TOKEN,
|
||||
number: 17,
|
||||
@ -174,7 +185,7 @@ describe('types', () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
color: colorParse('#3a8bc2'),
|
||||
color: colorParse(context, '#3a8bc2'),
|
||||
stop: {
|
||||
type: TokenType.PERCENTAGE_TOKEN,
|
||||
number: 84,
|
||||
@ -182,7 +193,7 @@ describe('types', () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
color: colorParse('#26558b'),
|
||||
color: colorParse(context, '#26558b'),
|
||||
stop: {
|
||||
type: TokenType.PERCENTAGE_TOKEN,
|
||||
number: 100,
|
||||
@ -193,12 +204,12 @@ describe('types', () => {
|
||||
}
|
||||
));
|
||||
it('-moz-linear-gradient(top, #cce5f4 0%, #00263c 100%)', () =>
|
||||
deepStrictEqual(parse('-moz-linear-gradient(top, #cce5f4 0%, #00263c 100%)'), {
|
||||
deepStrictEqual(parse(context, '-moz-linear-gradient(top, #cce5f4 0%, #00263c 100%)'), {
|
||||
angle: deg(180),
|
||||
type: CSSImageType.LINEAR_GRADIENT,
|
||||
stops: [
|
||||
{
|
||||
color: colorParse('#cce5f4'),
|
||||
color: colorParse(context, '#cce5f4'),
|
||||
stop: {
|
||||
type: TokenType.PERCENTAGE_TOKEN,
|
||||
number: 0,
|
||||
@ -206,7 +217,7 @@ describe('types', () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
color: colorParse('#00263c'),
|
||||
color: colorParse(context, '#00263c'),
|
||||
stop: {
|
||||
type: TokenType.PERCENTAGE_TOKEN,
|
||||
number: 100,
|
||||
|
@ -3,6 +3,7 @@ import {TokenType} from '../syntax/tokenizer';
|
||||
import {ITypeDescriptor} from '../ITypeDescriptor';
|
||||
import {HUNDRED_PERCENT, ZERO_LENGTH} from './length-percentage';
|
||||
import {GradientCorner} from './image';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
const DEG = 'deg';
|
||||
const GRAD = 'grad';
|
||||
@ -11,7 +12,7 @@ const TURN = 'turn';
|
||||
|
||||
export const angle: ITypeDescriptor<number> = {
|
||||
name: 'angle',
|
||||
parse: (value: CSSValue): number => {
|
||||
parse: (_context: Context, value: CSSValue): number => {
|
||||
if (value.type === TokenType.DIMENSION_TOKEN) {
|
||||
switch (value.unit) {
|
||||
case DEG:
|
||||
|
@ -1,19 +1,20 @@
|
||||
import {CSSValue, nonFunctionArgSeparator} from '../syntax/parser';
|
||||
import {CSSValue, nonFunctionArgSeparator, Parser} from '../syntax/parser';
|
||||
import {TokenType} from '../syntax/tokenizer';
|
||||
import {ITypeDescriptor} from '../ITypeDescriptor';
|
||||
import {angle, deg} from './angle';
|
||||
import {getAbsoluteValue, isLengthPercentage} from './length-percentage';
|
||||
import {Context} from '../../core/context';
|
||||
export type Color = number;
|
||||
|
||||
export const color: ITypeDescriptor<Color> = {
|
||||
name: 'color',
|
||||
parse: (value: CSSValue): Color => {
|
||||
parse: (context: Context, value: CSSValue): Color => {
|
||||
if (value.type === TokenType.FUNCTION) {
|
||||
const colorFunction = SUPPORTED_COLOR_FUNCTIONS[value.name];
|
||||
if (typeof colorFunction === 'undefined') {
|
||||
throw new Error(`Attempting to parse an unsupported color function "${value.name}"`);
|
||||
}
|
||||
return colorFunction(value.values);
|
||||
return colorFunction(context, value.values);
|
||||
}
|
||||
|
||||
if (value.type === TokenType.HASH_TOKEN) {
|
||||
@ -85,7 +86,7 @@ const getTokenColorValue = (token: CSSValue, i: number): number => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
const rgb = (args: CSSValue[]): number => {
|
||||
const rgb = (_context: Context, args: CSSValue[]): number => {
|
||||
const tokens = args.filter(nonFunctionArgSeparator);
|
||||
|
||||
if (tokens.length === 3) {
|
||||
@ -120,11 +121,11 @@ function hue2rgb(t1: number, t2: number, hue: number): number {
|
||||
}
|
||||
}
|
||||
|
||||
const hsl = (args: CSSValue[]): number => {
|
||||
const hsl = (context: Context, args: CSSValue[]): number => {
|
||||
const tokens = args.filter(nonFunctionArgSeparator);
|
||||
const [hue, saturation, lightness, alpha] = tokens;
|
||||
|
||||
const h = (hue.type === TokenType.NUMBER_TOKEN ? deg(hue.number) : angle.parse(hue)) / (Math.PI * 2);
|
||||
const h = (hue.type === TokenType.NUMBER_TOKEN ? deg(hue.number) : angle.parse(context, hue)) / (Math.PI * 2);
|
||||
const s = isLengthPercentage(saturation) ? saturation.number / 100 : 0;
|
||||
const l = isLengthPercentage(lightness) ? lightness.number / 100 : 0;
|
||||
const a = typeof alpha !== 'undefined' && isLengthPercentage(alpha) ? getAbsoluteValue(alpha, 1) : 1;
|
||||
@ -143,7 +144,7 @@ const hsl = (args: CSSValue[]): number => {
|
||||
};
|
||||
|
||||
const SUPPORTED_COLOR_FUNCTIONS: {
|
||||
[key: string]: (args: CSSValue[]) => number;
|
||||
[key: string]: (context: Context, args: CSSValue[]) => number;
|
||||
} = {
|
||||
hsl: hsl,
|
||||
hsla: hsl,
|
||||
@ -151,6 +152,9 @@ const SUPPORTED_COLOR_FUNCTIONS: {
|
||||
rgba: rgb
|
||||
};
|
||||
|
||||
export const parseColor = (context: Context, value: string): Color =>
|
||||
color.parse(context, Parser.create(value).parseComponentValue());
|
||||
|
||||
export const COLORS: {[key: string]: Color} = {
|
||||
ALICEBLUE: 0xf0f8ffff,
|
||||
ANTIQUEWHITE: 0xfaebd7ff,
|
||||
|
@ -3,8 +3,9 @@ import {CSSImageType, CSSLinearGradientImage, GradientCorner, UnprocessedGradien
|
||||
import {TokenType} from '../../syntax/tokenizer';
|
||||
import {isAngle, angle as angleType, parseNamedSide, deg} from '../angle';
|
||||
import {parseColorStop} from './gradient';
|
||||
import {Context} from '../../../core/context';
|
||||
|
||||
export const prefixLinearGradient = (tokens: CSSValue[]): CSSLinearGradientImage => {
|
||||
export const prefixLinearGradient = (context: Context, tokens: CSSValue[]): CSSLinearGradientImage => {
|
||||
let angle: number | GradientCorner = deg(180);
|
||||
const stops: UnprocessedGradientColorStop[] = [];
|
||||
|
||||
@ -18,11 +19,11 @@ export const prefixLinearGradient = (tokens: CSSValue[]): CSSLinearGradientImage
|
||||
angle = parseNamedSide(arg);
|
||||
return;
|
||||
} else if (isAngle(firstToken)) {
|
||||
angle = (angleType.parse(firstToken) + deg(270)) % deg(360);
|
||||
angle = (angleType.parse(context, firstToken) + deg(270)) % deg(360);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const colorStop = parseColorStop(arg);
|
||||
const colorStop = parseColorStop(context, arg);
|
||||
stops.push(colorStop);
|
||||
});
|
||||
|
||||
|
@ -20,8 +20,9 @@ import {
|
||||
FARTHEST_CORNER,
|
||||
FARTHEST_SIDE
|
||||
} from './radial-gradient';
|
||||
import {Context} from '../../../core/context';
|
||||
|
||||
export const prefixRadialGradient = (tokens: CSSValue[]): CSSRadialGradientImage => {
|
||||
export const prefixRadialGradient = (context: Context, tokens: CSSValue[]): CSSRadialGradientImage => {
|
||||
let shape: CSSRadialShape = CSSRadialShape.CIRCLE;
|
||||
let size: CSSRadialSize = CSSRadialExtent.FARTHEST_CORNER;
|
||||
const stops: UnprocessedGradientColorStop[] = [];
|
||||
@ -90,7 +91,7 @@ export const prefixRadialGradient = (tokens: CSSValue[]): CSSRadialGradientImage
|
||||
}
|
||||
|
||||
if (isColorStop) {
|
||||
const colorStop = parseColorStop(arg);
|
||||
const colorStop = parseColorStop(context, arg);
|
||||
stops.push(colorStop);
|
||||
}
|
||||
});
|
||||
|
@ -12,8 +12,12 @@ import {deg} from '../angle';
|
||||
import {TokenType} from '../../syntax/tokenizer';
|
||||
import {color as colorType} from '../color';
|
||||
import {HUNDRED_PERCENT, LengthPercentage, ZERO_LENGTH} from '../length-percentage';
|
||||
import {Context} from '../../../core/context';
|
||||
|
||||
export const webkitGradient = (tokens: CSSValue[]): CSSLinearGradientImage | CSSRadialGradientImage => {
|
||||
export const webkitGradient = (
|
||||
context: Context,
|
||||
tokens: CSSValue[]
|
||||
): CSSLinearGradientImage | CSSRadialGradientImage => {
|
||||
const angle = deg(180);
|
||||
const stops: UnprocessedGradientColorStop[] = [];
|
||||
let type = CSSImageType.LINEAR_GRADIENT;
|
||||
@ -34,15 +38,15 @@ export const webkitGradient = (tokens: CSSValue[]): CSSLinearGradientImage | CSS
|
||||
|
||||
if (firstToken.type === TokenType.FUNCTION) {
|
||||
if (firstToken.name === 'from') {
|
||||
const color = colorType.parse(firstToken.values[0]);
|
||||
const color = colorType.parse(context, firstToken.values[0]);
|
||||
stops.push({stop: ZERO_LENGTH, color});
|
||||
} else if (firstToken.name === 'to') {
|
||||
const color = colorType.parse(firstToken.values[0]);
|
||||
const color = colorType.parse(context, firstToken.values[0]);
|
||||
stops.push({stop: HUNDRED_PERCENT, color});
|
||||
} else if (firstToken.name === 'color-stop') {
|
||||
const values = firstToken.values.filter(nonFunctionArgSeparator);
|
||||
if (values.length === 2) {
|
||||
const color = colorType.parse(values[1]);
|
||||
const color = colorType.parse(context, values[1]);
|
||||
const stop = values[0];
|
||||
if (isNumberToken(stop)) {
|
||||
stops.push({
|
||||
|
@ -5,9 +5,10 @@ import {CSSImageType, CSSRadialExtent, CSSRadialShape} from '../../image';
|
||||
import {color} from '../../color';
|
||||
import {TokenType} from '../../../syntax/tokenizer';
|
||||
import {FIFTY_PERCENT, HUNDRED_PERCENT} from '../../length-percentage';
|
||||
import {Context} from '../../../../core/context';
|
||||
|
||||
const parse = (value: string) => radialGradient((Parser.parseValues(value)[0] as CSSFunction).values);
|
||||
const colorParse = (value: string) => color.parse(Parser.parseValue(value));
|
||||
const parse = (value: string) => radialGradient({} as Context, (Parser.parseValues(value)[0] as CSSFunction).values);
|
||||
const colorParse = (value: string) => color.parse({} as Context, Parser.parseValue(value));
|
||||
|
||||
describe('functions', () => {
|
||||
describe('radial-gradient', () => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {LIST_STYLE_TYPE} from '../../property-descriptors/list-style-type';
|
||||
import {fromCodePoint} from 'css-line-break';
|
||||
import {fromCodePoint} from 'text-segmentation';
|
||||
import {contains} from '../../../core/bitwise';
|
||||
import {CSSParsedCounterDeclaration} from '../../index';
|
||||
|
||||
|
@ -9,9 +9,10 @@ import {
|
||||
} from '../image';
|
||||
import {color as colorType} from '../color';
|
||||
import {getAbsoluteValue, HUNDRED_PERCENT, isLengthPercentage, ZERO_LENGTH} from '../length-percentage';
|
||||
import {Context} from '../../../core/context';
|
||||
|
||||
export const parseColorStop = (args: CSSValue[]): UnprocessedGradientColorStop => {
|
||||
const color = colorType.parse(args[0]);
|
||||
export const parseColorStop = (context: Context, args: CSSValue[]): UnprocessedGradientColorStop => {
|
||||
const color = colorType.parse(context, args[0]);
|
||||
const stop = args[1];
|
||||
return stop && isLengthPercentage(stop) ? {color, stop} : {color, stop: null};
|
||||
};
|
||||
|
@ -3,8 +3,9 @@ import {TokenType} from '../../syntax/tokenizer';
|
||||
import {isAngle, angle as angleType, parseNamedSide, deg} from '../angle';
|
||||
import {CSSImageType, CSSLinearGradientImage, GradientCorner, UnprocessedGradientColorStop} from '../image';
|
||||
import {parseColorStop} from './gradient';
|
||||
import {Context} from '../../../core/context';
|
||||
|
||||
export const linearGradient = (tokens: CSSValue[]): CSSLinearGradientImage => {
|
||||
export const linearGradient = (context: Context, tokens: CSSValue[]): CSSLinearGradientImage => {
|
||||
let angle: number | GradientCorner = deg(180);
|
||||
const stops: UnprocessedGradientColorStop[] = [];
|
||||
|
||||
@ -15,11 +16,11 @@ export const linearGradient = (tokens: CSSValue[]): CSSLinearGradientImage => {
|
||||
angle = parseNamedSide(arg);
|
||||
return;
|
||||
} else if (isAngle(firstToken)) {
|
||||
angle = angleType.parse(firstToken);
|
||||
angle = angleType.parse(context, firstToken);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const colorStop = parseColorStop(arg);
|
||||
const colorStop = parseColorStop(context, arg);
|
||||
stops.push(colorStop);
|
||||
});
|
||||
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
import {parseColorStop} from './gradient';
|
||||
import {FIFTY_PERCENT, HUNDRED_PERCENT, isLengthPercentage, LengthPercentage, ZERO_LENGTH} from '../length-percentage';
|
||||
import {isLength} from '../length';
|
||||
import {Context} from '../../../core/context';
|
||||
export const CLOSEST_SIDE = 'closest-side';
|
||||
export const FARTHEST_SIDE = 'farthest-side';
|
||||
export const CLOSEST_CORNER = 'closest-corner';
|
||||
@ -19,7 +20,7 @@ export const ELLIPSE = 'ellipse';
|
||||
export const COVER = 'cover';
|
||||
export const CONTAIN = 'contain';
|
||||
|
||||
export const radialGradient = (tokens: CSSValue[]): CSSRadialGradientImage => {
|
||||
export const radialGradient = (context: Context, tokens: CSSValue[]): CSSRadialGradientImage => {
|
||||
let shape: CSSRadialShape = CSSRadialShape.CIRCLE;
|
||||
let size: CSSRadialSize = CSSRadialExtent.FARTHEST_CORNER;
|
||||
const stops: UnprocessedGradientColorStop[] = [];
|
||||
@ -85,7 +86,7 @@ export const radialGradient = (tokens: CSSValue[]): CSSRadialGradientImage => {
|
||||
}
|
||||
|
||||
if (isColorStop) {
|
||||
const colorStop = parseColorStop(arg);
|
||||
const colorStop = parseColorStop(context, arg);
|
||||
stops.push(colorStop);
|
||||
}
|
||||
});
|
||||
|
@ -4,11 +4,11 @@ import {Color} from './color';
|
||||
import {linearGradient} from './functions/linear-gradient';
|
||||
import {prefixLinearGradient} from './functions/-prefix-linear-gradient';
|
||||
import {ITypeDescriptor} from '../ITypeDescriptor';
|
||||
import {CacheStorage} from '../../core/cache-storage';
|
||||
import {LengthPercentage} from './length-percentage';
|
||||
import {webkitGradient} from './functions/-webkit-gradient';
|
||||
import {radialGradient} from './functions/radial-gradient';
|
||||
import {prefixRadialGradient} from './functions/-prefix-radial-gradient';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export enum CSSImageType {
|
||||
URL,
|
||||
@ -79,10 +79,10 @@ export interface CSSRadialGradientImage extends ICSSGradientImage {
|
||||
|
||||
export const image: ITypeDescriptor<ICSSImage> = {
|
||||
name: 'image',
|
||||
parse: (value: CSSValue): ICSSImage => {
|
||||
parse: (context: Context, value: CSSValue): ICSSImage => {
|
||||
if (value.type === TokenType.URL_TOKEN) {
|
||||
const image: CSSURLImage = {url: value.value, type: CSSImageType.URL};
|
||||
CacheStorage.getInstance().addImage(value.value);
|
||||
context.cache.addImage(value.value);
|
||||
return image;
|
||||
}
|
||||
|
||||
@ -91,18 +91,21 @@ export const image: ITypeDescriptor<ICSSImage> = {
|
||||
if (typeof imageFunction === 'undefined') {
|
||||
throw new Error(`Attempting to parse an unsupported image function "${value.name}"`);
|
||||
}
|
||||
return imageFunction(value.values);
|
||||
return imageFunction(context, value.values);
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported image type`);
|
||||
throw new Error(`Unsupported image type ${value.type}`);
|
||||
}
|
||||
};
|
||||
|
||||
export function isSupportedImage(value: CSSValue): boolean {
|
||||
return value.type !== TokenType.FUNCTION || !!SUPPORTED_IMAGE_FUNCTIONS[value.name];
|
||||
return (
|
||||
!(value.type === TokenType.IDENT_TOKEN && value.value === 'none') &&
|
||||
(value.type !== TokenType.FUNCTION || !!SUPPORTED_IMAGE_FUNCTIONS[value.name])
|
||||
);
|
||||
}
|
||||
|
||||
const SUPPORTED_IMAGE_FUNCTIONS: Record<string, (args: CSSValue[]) => ICSSImage> = {
|
||||
const SUPPORTED_IMAGE_FUNCTIONS: Record<string, (context: Context, args: CSSValue[]) => ICSSImage> = {
|
||||
'linear-gradient': linearGradient,
|
||||
'-moz-linear-gradient': prefixLinearGradient,
|
||||
'-ms-linear-gradient': prefixLinearGradient,
|
||||
|
@ -2,7 +2,14 @@ export class DocumentCloner {
|
||||
clonedReferenceElement?: HTMLElement;
|
||||
|
||||
constructor() {
|
||||
this.clonedReferenceElement = {} as HTMLElement;
|
||||
this.clonedReferenceElement = {
|
||||
ownerDocument: {
|
||||
defaultView: {
|
||||
pageXOffset: 12,
|
||||
pageYOffset: 34
|
||||
}
|
||||
}
|
||||
} as HTMLElement;
|
||||
}
|
||||
|
||||
toIFrame(): Promise<HTMLIFrameElement> {
|
||||
|
@ -13,18 +13,24 @@ import {
|
||||
isTextareaElement,
|
||||
isTextNode
|
||||
} from './node-parser';
|
||||
import {Logger} from '../core/logger';
|
||||
import {isIdentToken, nonFunctionArgSeparator} from '../css/syntax/parser';
|
||||
import {TokenType} from '../css/syntax/tokenizer';
|
||||
import {CounterState, createCounterText} from '../css/types/functions/counter';
|
||||
import {LIST_STYLE_TYPE, listStyleType} from '../css/property-descriptors/list-style-type';
|
||||
import {CSSParsedCounterDeclaration, CSSParsedPseudoDeclaration} from '../css/index';
|
||||
import {getQuote} from '../css/property-descriptors/quotes';
|
||||
import {Context} from '../core/context';
|
||||
|
||||
export interface CloneOptions {
|
||||
id: string;
|
||||
ignoreElements?: (element: Element) => boolean;
|
||||
onclone?: (document: Document) => void;
|
||||
onclone?: (document: Document, element: HTMLElement) => void;
|
||||
}
|
||||
|
||||
export interface WindowOptions {
|
||||
scrollX: number;
|
||||
scrollY: number;
|
||||
windowWidth: number;
|
||||
windowHeight: number;
|
||||
}
|
||||
|
||||
export type CloneConfigurations = CloneOptions & {
|
||||
@ -36,15 +42,17 @@ const IGNORE_ATTRIBUTE = 'data-html2canvas-ignore';
|
||||
|
||||
export class DocumentCloner {
|
||||
private readonly scrolledElements: [Element, number, number][];
|
||||
private readonly options: CloneConfigurations;
|
||||
private readonly referenceElement: HTMLElement;
|
||||
clonedReferenceElement?: HTMLElement;
|
||||
private readonly documentElement: HTMLElement;
|
||||
private readonly counters: CounterState;
|
||||
private quoteDepth: number;
|
||||
|
||||
constructor(element: HTMLElement, options: CloneConfigurations) {
|
||||
this.options = options;
|
||||
constructor(
|
||||
private readonly context: Context,
|
||||
element: HTMLElement,
|
||||
private readonly options: CloneConfigurations
|
||||
) {
|
||||
this.scrolledElements = [];
|
||||
this.referenceElement = element;
|
||||
this.counters = new CounterState();
|
||||
@ -81,15 +89,21 @@ export class DocumentCloner {
|
||||
/(iPad|iPhone|iPod)/g.test(navigator.userAgent) &&
|
||||
(cloneWindow.scrollY !== windowSize.top || cloneWindow.scrollX !== windowSize.left)
|
||||
) {
|
||||
documentClone.documentElement.style.top = -windowSize.top + 'px';
|
||||
documentClone.documentElement.style.left = -windowSize.left + 'px';
|
||||
documentClone.documentElement.style.position = 'absolute';
|
||||
this.context.logger.warn('Unable to restore scroll position for cloned document');
|
||||
this.context.windowBounds = this.context.windowBounds.add(
|
||||
cloneWindow.scrollX - windowSize.left,
|
||||
cloneWindow.scrollY - windowSize.top,
|
||||
0,
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const onclone = this.options.onclone;
|
||||
|
||||
if (typeof this.clonedReferenceElement === 'undefined') {
|
||||
const referenceElement = this.clonedReferenceElement;
|
||||
|
||||
if (typeof referenceElement === 'undefined') {
|
||||
return Promise.reject(`Error finding the ${this.referenceElement.nodeName} in the cloned document`);
|
||||
}
|
||||
|
||||
@ -97,9 +111,13 @@ export class DocumentCloner {
|
||||
await documentClone.fonts.ready;
|
||||
}
|
||||
|
||||
if (/(AppleWebKit)/g.test(navigator.userAgent)) {
|
||||
await imagesReady(documentClone);
|
||||
}
|
||||
|
||||
if (typeof onclone === 'function') {
|
||||
return Promise.resolve()
|
||||
.then(() => onclone(documentClone))
|
||||
.then(() => onclone(documentClone, referenceElement))
|
||||
.then(() => iframe);
|
||||
}
|
||||
|
||||
@ -120,18 +138,21 @@ export class DocumentCloner {
|
||||
if (isCanvasElement(node)) {
|
||||
return this.createCanvasClone(node);
|
||||
}
|
||||
/*
|
||||
if (isIFrameElement(node)) {
|
||||
return this.createIFrameClone(node);
|
||||
}
|
||||
*/
|
||||
|
||||
if (isStyleElement(node)) {
|
||||
return this.createStyleClone(node);
|
||||
}
|
||||
|
||||
const clone = node.cloneNode(false) as T;
|
||||
if (isImageElement(clone) && clone.loading === 'lazy') {
|
||||
clone.loading = 'eager';
|
||||
if (isImageElement(clone)) {
|
||||
if (isImageElement(node) && node.currentSrc && node.currentSrc !== node.src) {
|
||||
clone.src = node.currentSrc;
|
||||
clone.srcset = '';
|
||||
}
|
||||
|
||||
if (clone.loading === 'lazy') {
|
||||
clone.loading = 'eager';
|
||||
}
|
||||
}
|
||||
|
||||
return clone;
|
||||
@ -153,7 +174,7 @@ export class DocumentCloner {
|
||||
}
|
||||
} catch (e) {
|
||||
// accessing node.sheet.cssRules throws a DOMException
|
||||
Logger.getInstance(this.options.id).error('Unable to access cssRules property', e);
|
||||
this.context.logger.error('Unable to access cssRules property', e);
|
||||
if (e.name !== 'SecurityError') {
|
||||
throw e;
|
||||
}
|
||||
@ -168,7 +189,7 @@ export class DocumentCloner {
|
||||
img.src = canvas.toDataURL();
|
||||
return img;
|
||||
} catch (e) {
|
||||
Logger.getInstance(this.options.id).info(`Unable to clone canvas contents, canvas is tainted`);
|
||||
this.context.logger.info(`Unable to clone canvas contents, canvas is tainted`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -191,67 +212,7 @@ export class DocumentCloner {
|
||||
|
||||
return clonedCanvas;
|
||||
}
|
||||
/*
|
||||
createIFrameClone(iframe: HTMLIFrameElement) {
|
||||
const tempIframe = <HTMLIFrameElement>iframe.cloneNode(false);
|
||||
const iframeKey = generateIframeKey();
|
||||
tempIframe.setAttribute('data-html2canvas-internal-iframe-key', iframeKey);
|
||||
|
||||
const {width, height} = parseBounds(iframe);
|
||||
|
||||
this.resourceLoader.cache[iframeKey] = getIframeDocumentElement(iframe, 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
|
||||
},
|
||||
);
|
||||
})
|
||||
.then(
|
||||
(canvas: HTMLCanvasElement) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const iframeCanvas = document.createElement('img');
|
||||
iframeCanvas.onload = () => resolve(canvas);
|
||||
iframeCanvas.onerror = (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 && iframe.ownerDocument && iframe.ownerDocument.defaultView) {
|
||||
tempIframe.parentNode.replaceChild(
|
||||
copyCSSStyles(
|
||||
iframe.ownerDocument.defaultView.getComputedStyle(iframe),
|
||||
iframeCanvas
|
||||
),
|
||||
tempIframe
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
return tempIframe;
|
||||
}
|
||||
*/
|
||||
cloneNode(node: Node): Node {
|
||||
if (isTextNode(node)) {
|
||||
return document.createTextNode(node.data);
|
||||
@ -277,7 +238,7 @@ export class DocumentCloner {
|
||||
createPseudoHideStyles(clone);
|
||||
}
|
||||
|
||||
const counters = this.counters.parse(new CSSParsedCounterDeclaration(style));
|
||||
const counters = this.counters.parse(new CSSParsedCounterDeclaration(this.context, style));
|
||||
const before = this.resolvePseudoContent(node, clone, styleBefore, PseudoElementType.BEFORE);
|
||||
|
||||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||
@ -308,8 +269,6 @@ export class DocumentCloner {
|
||||
copyCSSStyles(style, clone);
|
||||
}
|
||||
|
||||
//this.inlineAllImages(clone);
|
||||
|
||||
if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
|
||||
this.scrolledElements.push([clone, node.scrollLeft, node.scrollTop]);
|
||||
}
|
||||
@ -343,8 +302,8 @@ export class DocumentCloner {
|
||||
return;
|
||||
}
|
||||
|
||||
this.counters.parse(new CSSParsedCounterDeclaration(style));
|
||||
const declaration = new CSSParsedPseudoDeclaration(style);
|
||||
this.counters.parse(new CSSParsedCounterDeclaration(this.context, style));
|
||||
const declaration = new CSSParsedPseudoDeclaration(this.context, style);
|
||||
|
||||
const anonymousReplacedElement = document.createElement('html2canvaspseudoelement');
|
||||
copyCSSStyles(style, anonymousReplacedElement);
|
||||
@ -371,7 +330,7 @@ export class DocumentCloner {
|
||||
const counterState = this.counters.getCounterValue(counter.value);
|
||||
const counterType =
|
||||
counterStyle && isIdentToken(counterStyle)
|
||||
? listStyleType.parse(counterStyle.value)
|
||||
? listStyleType.parse(this.context, counterStyle.value)
|
||||
: LIST_STYLE_TYPE.DECIMAL;
|
||||
|
||||
anonymousReplacedElement.appendChild(
|
||||
@ -384,7 +343,7 @@ export class DocumentCloner {
|
||||
const counterStates = this.counters.getCounterValues(counter.value);
|
||||
const counterType =
|
||||
counterStyle && isIdentToken(counterStyle)
|
||||
? listStyleType.parse(counterStyle.value)
|
||||
? listStyleType.parse(this.context, counterStyle.value)
|
||||
: LIST_STYLE_TYPE.DECIMAL;
|
||||
const separator = delim && delim.type === TokenType.STRING_TOKEN ? delim.value : '';
|
||||
const text = counterStates
|
||||
@ -462,6 +421,25 @@ const createIFrameContainer = (ownerDocument: Document, bounds: Bounds): HTMLIFr
|
||||
return cloneIframeContainer;
|
||||
};
|
||||
|
||||
const imageReady = (img: HTMLImageElement): Promise<Event | void | string> => {
|
||||
return new Promise((resolve) => {
|
||||
if (img.complete) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
if (!img.src) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
img.onload = resolve;
|
||||
img.onerror = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
const imagesReady = (document: HTMLDocument): Promise<unknown[]> => {
|
||||
return Promise.all([].slice.call(document.images, 0).map(imageReady));
|
||||
};
|
||||
|
||||
const iframeLoader = (iframe: HTMLIFrameElement): Promise<HTMLIFrameElement> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cloneWindow = iframe.contentWindow;
|
||||
@ -472,27 +450,29 @@ const iframeLoader = (iframe: HTMLIFrameElement): Promise<HTMLIFrameElement> =>
|
||||
|
||||
const documentClone = cloneWindow.document;
|
||||
|
||||
cloneWindow.onload =
|
||||
iframe.onload =
|
||||
documentClone.onreadystatechange =
|
||||
() => {
|
||||
cloneWindow.onload = iframe.onload = documentClone.onreadystatechange = null;
|
||||
const interval = setInterval(() => {
|
||||
if (documentClone.body.childNodes.length > 0 && documentClone.readyState === 'complete') {
|
||||
clearInterval(interval);
|
||||
resolve(iframe);
|
||||
}
|
||||
}, 50);
|
||||
};
|
||||
cloneWindow.onload = iframe.onload = () => {
|
||||
cloneWindow.onload = iframe.onload = null;
|
||||
const interval = setInterval(() => {
|
||||
if (documentClone.body.childNodes.length > 0 && documentClone.readyState === 'complete') {
|
||||
clearInterval(interval);
|
||||
resolve(iframe);
|
||||
}
|
||||
}, 50);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const ignoredStyleProperties = [
|
||||
'all', // #2476
|
||||
'd', // #2483
|
||||
'content' // Safari shows pseudoelements if content is set
|
||||
];
|
||||
|
||||
export const copyCSSStyles = <T extends HTMLElement | SVGElement>(style: CSSStyleDeclaration, target: T): T => {
|
||||
// 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') {
|
||||
if (ignoredStyleProperties.indexOf(property) === -1) {
|
||||
target.style.setProperty(property, style.getPropertyValue(property));
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import {CSSParsedDeclaration} from '../css/index';
|
||||
import {TextContainer} from './text-container';
|
||||
import {Bounds, parseBounds} from '../css/layout/bounds';
|
||||
import {isHTMLElementNode} from './node-parser';
|
||||
import {Context} from '../core/context';
|
||||
|
||||
export const enum FLAGS {
|
||||
CREATES_STACKING_CONTEXT = 1 << 1,
|
||||
@ -16,15 +17,15 @@ export class ElementContainer {
|
||||
bounds: Bounds;
|
||||
flags: number;
|
||||
|
||||
constructor(element: Element) {
|
||||
this.styles = new CSSParsedDeclaration(window.getComputedStyle(element, null));
|
||||
constructor(protected readonly context: Context, element: Element) {
|
||||
this.styles = new CSSParsedDeclaration(context, window.getComputedStyle(element, null));
|
||||
this.textNodes = [];
|
||||
this.elements = [];
|
||||
if (this.styles.transform !== null && isHTMLElementNode(element)) {
|
||||
// getBoundingClientRect takes transforms into account
|
||||
element.style.transform = 'none';
|
||||
}
|
||||
this.bounds = parseBounds(element);
|
||||
this.bounds = parseBounds(this.context, element);
|
||||
this.flags = 0;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import {ElementContainer} from '../element-container';
|
||||
import {Context} from '../../core/context';
|
||||
export class LIElementContainer extends ElementContainer {
|
||||
readonly value: number;
|
||||
|
||||
constructor(element: HTMLLIElement) {
|
||||
super(element);
|
||||
constructor(context: Context, element: HTMLLIElement) {
|
||||
super(context, element);
|
||||
this.value = element.value;
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import {ElementContainer} from '../element-container';
|
||||
import {Context} from '../../core/context';
|
||||
export class OLElementContainer extends ElementContainer {
|
||||
readonly start: number;
|
||||
readonly reversed: boolean;
|
||||
|
||||
constructor(element: HTMLOListElement) {
|
||||
super(element);
|
||||
constructor(context: Context, element: HTMLOListElement) {
|
||||
super(context, element);
|
||||
this.start = element.start;
|
||||
this.reversed = typeof element.reversed === 'boolean' && element.reversed === true;
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import {ElementContainer} from '../element-container';
|
||||
import {Context} from '../../core/context';
|
||||
export class SelectElementContainer extends ElementContainer {
|
||||
readonly value: string;
|
||||
constructor(element: HTMLSelectElement) {
|
||||
super(element);
|
||||
constructor(context: Context, element: HTMLSelectElement) {
|
||||
super(context, element);
|
||||
const option = element.options[element.selectedIndex || 0];
|
||||
this.value = option ? option.text || '' : '';
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import {ElementContainer} from '../element-container';
|
||||
import {Context} from '../../core/context';
|
||||
export class TextareaElementContainer extends ElementContainer {
|
||||
readonly value: string;
|
||||
constructor(element: HTMLTextAreaElement) {
|
||||
super(element);
|
||||
constructor(context: Context, element: HTMLTextAreaElement) {
|
||||
super(context, element);
|
||||
this.value = element.value;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {CSSParsedDeclaration} from '../css/index';
|
||||
import {CSSParsedDeclaration} from '../css';
|
||||
import {ElementContainer, FLAGS} from './element-container';
|
||||
import {TextContainer} from './text-container';
|
||||
import {ImageElementContainer} from './replaced-elements/image-element-container';
|
||||
@ -10,81 +10,93 @@ import {InputElementContainer} from './replaced-elements/input-element-container
|
||||
import {SelectElementContainer} from './elements/select-element-container';
|
||||
import {TextareaElementContainer} from './elements/textarea-element-container';
|
||||
import {IFrameElementContainer} from './replaced-elements/iframe-element-container';
|
||||
import {Context} from '../core/context';
|
||||
|
||||
const LIST_OWNERS = ['OL', 'UL', 'MENU'];
|
||||
|
||||
const parseNodeTree = (node: Node, parent: ElementContainer, root: ElementContainer) => {
|
||||
const parseNodeTree = (context: Context, node: Node, parent: ElementContainer, root: ElementContainer) => {
|
||||
for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) {
|
||||
nextNode = childNode.nextSibling;
|
||||
|
||||
if (isTextNode(childNode) && childNode.data.trim().length > 0) {
|
||||
parent.textNodes.push(new TextContainer(childNode, parent.styles));
|
||||
parent.textNodes.push(new TextContainer(context, childNode, parent.styles));
|
||||
} else if (isElementNode(childNode)) {
|
||||
const container = createContainer(childNode);
|
||||
if (container.styles.isVisible()) {
|
||||
if (createsRealStackingContext(childNode, container, root)) {
|
||||
container.flags |= FLAGS.CREATES_REAL_STACKING_CONTEXT;
|
||||
} else if (createsStackingContext(container.styles)) {
|
||||
container.flags |= FLAGS.CREATES_STACKING_CONTEXT;
|
||||
}
|
||||
if (isSlotElement(childNode) && childNode.assignedNodes) {
|
||||
childNode.assignedNodes().forEach((childNode) => parseNodeTree(context, childNode, parent, root));
|
||||
} else {
|
||||
const container = createContainer(context, childNode);
|
||||
if (container.styles.isVisible()) {
|
||||
if (createsRealStackingContext(childNode, container, root)) {
|
||||
container.flags |= FLAGS.CREATES_REAL_STACKING_CONTEXT;
|
||||
} else if (createsStackingContext(container.styles)) {
|
||||
container.flags |= FLAGS.CREATES_STACKING_CONTEXT;
|
||||
}
|
||||
|
||||
if (LIST_OWNERS.indexOf(childNode.tagName) !== -1) {
|
||||
container.flags |= FLAGS.IS_LIST_OWNER;
|
||||
}
|
||||
if (LIST_OWNERS.indexOf(childNode.tagName) !== -1) {
|
||||
container.flags |= FLAGS.IS_LIST_OWNER;
|
||||
}
|
||||
|
||||
parent.elements.push(container);
|
||||
if (!isTextareaElement(childNode) && !isSVGElement(childNode) && !isSelectElement(childNode)) {
|
||||
parseNodeTree(childNode, container, root);
|
||||
parent.elements.push(container);
|
||||
childNode.slot;
|
||||
if (childNode.shadowRoot) {
|
||||
parseNodeTree(context, childNode.shadowRoot, container, root);
|
||||
} else if (
|
||||
!isTextareaElement(childNode) &&
|
||||
!isSVGElement(childNode) &&
|
||||
!isSelectElement(childNode)
|
||||
) {
|
||||
parseNodeTree(context, childNode, container, root);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createContainer = (element: Element): ElementContainer => {
|
||||
const createContainer = (context: Context, element: Element): ElementContainer => {
|
||||
if (isImageElement(element)) {
|
||||
return new ImageElementContainer(element);
|
||||
return new ImageElementContainer(context, element);
|
||||
}
|
||||
|
||||
if (isCanvasElement(element)) {
|
||||
return new CanvasElementContainer(element);
|
||||
return new CanvasElementContainer(context, element);
|
||||
}
|
||||
|
||||
if (isSVGElement(element)) {
|
||||
return new SVGElementContainer(element);
|
||||
return new SVGElementContainer(context, element);
|
||||
}
|
||||
|
||||
if (isLIElement(element)) {
|
||||
return new LIElementContainer(element);
|
||||
return new LIElementContainer(context, element);
|
||||
}
|
||||
|
||||
if (isOLElement(element)) {
|
||||
return new OLElementContainer(element);
|
||||
return new OLElementContainer(context, element);
|
||||
}
|
||||
|
||||
if (isInputElement(element)) {
|
||||
return new InputElementContainer(element);
|
||||
return new InputElementContainer(context, element);
|
||||
}
|
||||
|
||||
if (isSelectElement(element)) {
|
||||
return new SelectElementContainer(element);
|
||||
return new SelectElementContainer(context, element);
|
||||
}
|
||||
|
||||
if (isTextareaElement(element)) {
|
||||
return new TextareaElementContainer(element);
|
||||
return new TextareaElementContainer(context, element);
|
||||
}
|
||||
|
||||
if (isIFrameElement(element)) {
|
||||
return new IFrameElementContainer(element);
|
||||
return new IFrameElementContainer(context, element);
|
||||
}
|
||||
|
||||
return new ElementContainer(element);
|
||||
return new ElementContainer(context, element);
|
||||
};
|
||||
|
||||
export const parseTree = (element: HTMLElement): ElementContainer => {
|
||||
const container = createContainer(element);
|
||||
export const parseTree = (context: Context, element: HTMLElement): ElementContainer => {
|
||||
const container = createContainer(context, element);
|
||||
container.flags |= FLAGS.CREATES_REAL_STACKING_CONTEXT;
|
||||
parseNodeTree(element, container, container);
|
||||
parseNodeTree(context, element, container, container);
|
||||
return container;
|
||||
};
|
||||
|
||||
@ -118,3 +130,4 @@ export const isStyleElement = (node: Element): node is HTMLStyleElement => node.
|
||||
export const isScriptElement = (node: Element): node is HTMLScriptElement => node.tagName === 'SCRIPT';
|
||||
export const isTextareaElement = (node: Element): node is HTMLTextAreaElement => node.tagName === 'TEXTAREA';
|
||||
export const isSelectElement = (node: Element): node is HTMLSelectElement => node.tagName === 'SELECT';
|
||||
export const isSlotElement = (node: Element): node is HTMLSlotElement => node.tagName === 'SLOT';
|
||||
|
@ -1,12 +1,13 @@
|
||||
import {ElementContainer} from '../element-container';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export class CanvasElementContainer extends ElementContainer {
|
||||
canvas: HTMLCanvasElement;
|
||||
intrinsicWidth: number;
|
||||
intrinsicHeight: number;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
super(canvas);
|
||||
constructor(context: Context, canvas: HTMLCanvasElement) {
|
||||
super(context, canvas);
|
||||
this.canvas = canvas;
|
||||
this.intrinsicWidth = canvas.width;
|
||||
this.intrinsicHeight = canvas.height;
|
||||
|
@ -1,9 +1,7 @@
|
||||
import {ElementContainer} from '../element-container';
|
||||
import {parseTree} from '../node-parser';
|
||||
import {Color, color, COLORS, isTransparent} from '../../css/types/color';
|
||||
import {Parser} from '../../css/syntax/parser';
|
||||
|
||||
const parseColor = (value: string): Color => color.parse(Parser.create(value).parseComponentValue());
|
||||
import {Color, parseColor, COLORS, isTransparent} from '../../css/types/color';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export class IFrameElementContainer extends ElementContainer {
|
||||
src: string;
|
||||
@ -12,8 +10,8 @@ export class IFrameElementContainer extends ElementContainer {
|
||||
tree?: ElementContainer;
|
||||
backgroundColor: Color;
|
||||
|
||||
constructor(iframe: HTMLIFrameElement) {
|
||||
super(iframe);
|
||||
constructor(context: Context, iframe: HTMLIFrameElement) {
|
||||
super(context, iframe);
|
||||
this.src = iframe.src;
|
||||
this.width = parseInt(iframe.width, 10) || 0;
|
||||
this.height = parseInt(iframe.height, 10) || 0;
|
||||
@ -24,16 +22,20 @@ export class IFrameElementContainer extends ElementContainer {
|
||||
iframe.contentWindow.document &&
|
||||
iframe.contentWindow.document.documentElement
|
||||
) {
|
||||
this.tree = parseTree(iframe.contentWindow.document.documentElement);
|
||||
this.tree = parseTree(context, iframe.contentWindow.document.documentElement);
|
||||
|
||||
// http://www.w3.org/TR/css3-background/#special-backgrounds
|
||||
const documentBackgroundColor = iframe.contentWindow.document.documentElement
|
||||
? parseColor(
|
||||
context,
|
||||
getComputedStyle(iframe.contentWindow.document.documentElement).backgroundColor as string
|
||||
)
|
||||
: COLORS.TRANSPARENT;
|
||||
const bodyBackgroundColor = iframe.contentWindow.document.body
|
||||
? parseColor(getComputedStyle(iframe.contentWindow.document.body).backgroundColor as string)
|
||||
? parseColor(
|
||||
context,
|
||||
getComputedStyle(iframe.contentWindow.document.body).backgroundColor as string
|
||||
)
|
||||
: COLORS.TRANSPARENT;
|
||||
|
||||
this.backgroundColor = isTransparent(documentBackgroundColor)
|
||||
|
@ -1,16 +1,16 @@
|
||||
import {ElementContainer} from '../element-container';
|
||||
import {CacheStorage} from '../../core/cache-storage';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export class ImageElementContainer extends ElementContainer {
|
||||
src: string;
|
||||
intrinsicWidth: number;
|
||||
intrinsicHeight: number;
|
||||
|
||||
constructor(img: HTMLImageElement) {
|
||||
super(img);
|
||||
constructor(context: Context, img: HTMLImageElement) {
|
||||
super(context, img);
|
||||
this.src = img.currentSrc || img.src;
|
||||
this.intrinsicWidth = img.naturalWidth;
|
||||
this.intrinsicHeight = img.naturalHeight;
|
||||
CacheStorage.getInstance().addImage(this.src);
|
||||
this.context.cache.addImage(this.src);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import {BACKGROUND_ORIGIN} from '../../css/property-descriptors/background-origi
|
||||
import {TokenType} from '../../css/syntax/tokenizer';
|
||||
import {LengthPercentageTuple} from '../../css/types/length-percentage';
|
||||
import {Bounds} from '../../css/layout/bounds';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
const CHECKBOX_BORDER_RADIUS: LengthPercentageTuple = [
|
||||
{
|
||||
@ -48,8 +49,8 @@ export class InputElementContainer extends ElementContainer {
|
||||
readonly checked: boolean;
|
||||
readonly value: string;
|
||||
|
||||
constructor(input: HTMLInputElement) {
|
||||
super(input);
|
||||
constructor(context: Context, input: HTMLInputElement) {
|
||||
super(context, input);
|
||||
this.type = input.type.toLowerCase();
|
||||
this.checked = input.checked;
|
||||
this.value = getInputValue(input);
|
||||
|
@ -1,18 +1,23 @@
|
||||
import {ElementContainer} from '../element-container';
|
||||
import {CacheStorage} from '../../core/cache-storage';
|
||||
import {parseBounds} from '../../css/layout/bounds';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export class SVGElementContainer extends ElementContainer {
|
||||
svg: string;
|
||||
intrinsicWidth: number;
|
||||
intrinsicHeight: number;
|
||||
|
||||
constructor(img: SVGSVGElement) {
|
||||
super(img);
|
||||
constructor(context: Context, img: SVGSVGElement) {
|
||||
super(context, img);
|
||||
const s = new XMLSerializer();
|
||||
const bounds = parseBounds(context, img);
|
||||
img.setAttribute('width', `${bounds.width}px`);
|
||||
img.setAttribute('height', `${bounds.height}px`);
|
||||
|
||||
this.svg = `data:image/svg+xml,${encodeURIComponent(s.serializeToString(img))}`;
|
||||
this.intrinsicWidth = img.width.baseVal.value;
|
||||
this.intrinsicHeight = img.height.baseVal.value;
|
||||
|
||||
CacheStorage.getInstance().addImage(this.svg);
|
||||
this.context.cache.addImage(this.svg);
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,15 @@
|
||||
import {CSSParsedDeclaration} from '../css/index';
|
||||
import {TEXT_TRANSFORM} from '../css/property-descriptors/text-transform';
|
||||
import {parseTextBounds, TextBounds} from '../css/layout/text';
|
||||
import {Context} from '../core/context';
|
||||
|
||||
export class TextContainer {
|
||||
text: string;
|
||||
textBounds: TextBounds[];
|
||||
|
||||
constructor(node: Text, styles: CSSParsedDeclaration) {
|
||||
constructor(context: Context, node: Text, styles: CSSParsedDeclaration) {
|
||||
this.text = transform(node.data, styles.textTransform);
|
||||
this.textBounds = parseTextBounds(this.text, styles, node);
|
||||
this.textBounds = parseTextBounds(context, this.text, styles, node);
|
||||
}
|
||||
}
|
||||
|
||||
|
199
src/index.ts
199
src/index.ts
@ -1,24 +1,21 @@
|
||||
import {Bounds, parseBounds, parseDocumentSize} from './css/layout/bounds';
|
||||
import {color, Color, COLORS, isTransparent} from './css/types/color';
|
||||
import {Parser} from './css/syntax/parser';
|
||||
import {CloneOptions, DocumentCloner} from './dom/document-cloner';
|
||||
import {COLORS, isTransparent, parseColor} from './css/types/color';
|
||||
import {CloneConfigurations, CloneOptions, DocumentCloner, WindowOptions} from './dom/document-cloner';
|
||||
import {isBodyElement, isHTMLElement, parseTree} from './dom/node-parser';
|
||||
import {Logger} from './core/logger';
|
||||
import {CacheStorage, ResourceOptions} from './core/cache-storage';
|
||||
import {CanvasRenderer, RenderOptions} from './render/canvas/canvas-renderer';
|
||||
import {CacheStorage} from './core/cache-storage';
|
||||
import {CanvasRenderer, RenderConfigurations, RenderOptions} from './render/canvas/canvas-renderer';
|
||||
import {ForeignObjectRenderer} from './render/canvas/foreignobject-renderer';
|
||||
import {Context, ContextOptions} from './core/context';
|
||||
|
||||
export type Options = CloneOptions &
|
||||
WindowOptions &
|
||||
RenderOptions &
|
||||
ResourceOptions & {
|
||||
ContextOptions & {
|
||||
backgroundColor: string | null;
|
||||
foreignObjectRendering: boolean;
|
||||
logging: boolean;
|
||||
removeContainer?: boolean;
|
||||
};
|
||||
|
||||
const parseColor = (value: string): Color => color.parse(Parser.create(value).parseComponentValue());
|
||||
|
||||
const html2canvas = (element: HTMLElement, options: Partial<Options> = {}): Promise<HTMLCanvasElement> => {
|
||||
return renderElement(element, options);
|
||||
};
|
||||
@ -29,10 +26,8 @@ if (typeof window !== 'undefined') {
|
||||
CacheStorage.setContext(window);
|
||||
}
|
||||
|
||||
let instanceCount = 1;
|
||||
|
||||
const renderElement = async (element: HTMLElement, opts: Partial<Options>): Promise<HTMLCanvasElement> => {
|
||||
if (typeof element !== 'object') {
|
||||
if (!element || typeof element !== 'object') {
|
||||
return Promise.reject('Invalid element provided as first argument');
|
||||
}
|
||||
const ownerDocument = element.ownerDocument;
|
||||
@ -47,51 +42,51 @@ const renderElement = async (element: HTMLElement, opts: Partial<Options>): Prom
|
||||
throw new Error(`Document is not attached to a Window`);
|
||||
}
|
||||
|
||||
const instanceName = `#${instanceCount++}`;
|
||||
|
||||
const {width, height, left, top} =
|
||||
isBodyElement(element) || isHTMLElement(element) ? parseDocumentSize(ownerDocument) : parseBounds(element);
|
||||
|
||||
const defaultResourceOptions = {
|
||||
allowTaint: false,
|
||||
imageTimeout: 15000,
|
||||
proxy: undefined,
|
||||
useCORS: false
|
||||
const resourceOptions = {
|
||||
allowTaint: opts.allowTaint ?? false,
|
||||
imageTimeout: opts.imageTimeout ?? 15000,
|
||||
proxy: opts.proxy,
|
||||
useCORS: opts.useCORS ?? false
|
||||
};
|
||||
|
||||
const resourceOptions: ResourceOptions = {...defaultResourceOptions, ...opts};
|
||||
|
||||
const defaultOptions = {
|
||||
backgroundColor: '#ffffff',
|
||||
cache: opts.cache ? opts.cache : CacheStorage.create(instanceName, resourceOptions),
|
||||
logging: true,
|
||||
removeContainer: true,
|
||||
foreignObjectRendering: false,
|
||||
scale: defaultView.devicePixelRatio || 1,
|
||||
windowWidth: defaultView.innerWidth,
|
||||
windowHeight: defaultView.innerHeight,
|
||||
scrollX: defaultView.pageXOffset,
|
||||
scrollY: defaultView.pageYOffset,
|
||||
x: left,
|
||||
y: top,
|
||||
width: Math.ceil(width),
|
||||
height: Math.ceil(height),
|
||||
id: instanceName
|
||||
const contextOptions = {
|
||||
logging: opts.logging ?? true,
|
||||
cache: opts.cache,
|
||||
...resourceOptions
|
||||
};
|
||||
|
||||
const options: Options = {...defaultOptions, ...resourceOptions, ...opts};
|
||||
const windowOptions = {
|
||||
windowWidth: opts.windowWidth ?? defaultView.innerWidth,
|
||||
windowHeight: opts.windowHeight ?? defaultView.innerHeight,
|
||||
scrollX: opts.scrollX ?? defaultView.pageXOffset,
|
||||
scrollY: opts.scrollY ?? defaultView.pageYOffset
|
||||
};
|
||||
|
||||
const windowBounds = new Bounds(options.scrollX, options.scrollY, options.windowWidth, options.windowHeight);
|
||||
const windowBounds = new Bounds(
|
||||
windowOptions.scrollX,
|
||||
windowOptions.scrollY,
|
||||
windowOptions.windowWidth,
|
||||
windowOptions.windowHeight
|
||||
);
|
||||
|
||||
Logger.create({id: instanceName, enabled: options.logging});
|
||||
Logger.getInstance(instanceName).debug(`Starting document clone`);
|
||||
const documentCloner = new DocumentCloner(element, {
|
||||
id: instanceName,
|
||||
onclone: options.onclone,
|
||||
ignoreElements: options.ignoreElements,
|
||||
inlineImages: options.foreignObjectRendering,
|
||||
copyStyles: options.foreignObjectRendering
|
||||
});
|
||||
const context = new Context(contextOptions, windowBounds);
|
||||
|
||||
const foreignObjectRendering = opts.foreignObjectRendering ?? false;
|
||||
|
||||
const cloneOptions: CloneConfigurations = {
|
||||
onclone: opts.onclone,
|
||||
ignoreElements: opts.ignoreElements,
|
||||
inlineImages: foreignObjectRendering,
|
||||
copyStyles: foreignObjectRendering
|
||||
};
|
||||
|
||||
context.logger.debug(
|
||||
`Starting document clone with size ${windowBounds.width}x${
|
||||
windowBounds.height
|
||||
} scrolled to ${-windowBounds.left},${-windowBounds.top}`
|
||||
);
|
||||
|
||||
const documentCloner = new DocumentCloner(context, element, cloneOptions);
|
||||
const clonedElement = documentCloner.clonedReferenceElement;
|
||||
if (!clonedElement) {
|
||||
return Promise.reject(`Unable to find element in cloned iframe`);
|
||||
@ -99,75 +94,81 @@ const renderElement = async (element: HTMLElement, opts: Partial<Options>): Prom
|
||||
|
||||
const container = await documentCloner.toIFrame(ownerDocument, windowBounds);
|
||||
|
||||
// http://www.w3.org/TR/css3-background/#special-backgrounds
|
||||
const documentBackgroundColor = ownerDocument.documentElement
|
||||
? parseColor(getComputedStyle(ownerDocument.documentElement).backgroundColor as string)
|
||||
: COLORS.TRANSPARENT;
|
||||
const bodyBackgroundColor = ownerDocument.body
|
||||
? parseColor(getComputedStyle(ownerDocument.body).backgroundColor as string)
|
||||
: COLORS.TRANSPARENT;
|
||||
const {width, height, left, top} =
|
||||
isBodyElement(clonedElement) || isHTMLElement(clonedElement)
|
||||
? parseDocumentSize(clonedElement.ownerDocument)
|
||||
: parseBounds(context, clonedElement);
|
||||
|
||||
const bgColor = opts.backgroundColor;
|
||||
const defaultBackgroundColor =
|
||||
typeof bgColor === 'string' ? parseColor(bgColor) : bgColor === null ? COLORS.TRANSPARENT : 0xffffffff;
|
||||
const backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor);
|
||||
|
||||
const backgroundColor =
|
||||
element === ownerDocument.documentElement
|
||||
? isTransparent(documentBackgroundColor)
|
||||
? isTransparent(bodyBackgroundColor)
|
||||
? defaultBackgroundColor
|
||||
: bodyBackgroundColor
|
||||
: documentBackgroundColor
|
||||
: defaultBackgroundColor;
|
||||
|
||||
const renderOptions = {
|
||||
id: instanceName,
|
||||
cache: options.cache,
|
||||
canvas: options.canvas,
|
||||
const renderOptions: RenderConfigurations = {
|
||||
canvas: opts.canvas,
|
||||
backgroundColor,
|
||||
scale: options.scale,
|
||||
x: options.x,
|
||||
y: options.y,
|
||||
scrollX: options.scrollX,
|
||||
scrollY: options.scrollY,
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
windowWidth: options.windowWidth,
|
||||
windowHeight: options.windowHeight
|
||||
scale: opts.scale ?? defaultView.devicePixelRatio ?? 1,
|
||||
x: (opts.x ?? 0) + left,
|
||||
y: (opts.y ?? 0) + top,
|
||||
width: opts.width ?? Math.ceil(width),
|
||||
height: opts.height ?? Math.ceil(height)
|
||||
};
|
||||
|
||||
let canvas;
|
||||
|
||||
if (options.foreignObjectRendering) {
|
||||
Logger.getInstance(instanceName).debug(`Document cloned, using foreign object rendering`);
|
||||
const renderer = new ForeignObjectRenderer(renderOptions);
|
||||
if (foreignObjectRendering) {
|
||||
context.logger.debug(`Document cloned, using foreign object rendering`);
|
||||
const renderer = new ForeignObjectRenderer(context, renderOptions);
|
||||
canvas = await renderer.render(clonedElement);
|
||||
} else {
|
||||
Logger.getInstance(instanceName).debug(`Document cloned, using computed rendering`);
|
||||
context.logger.debug(
|
||||
`Document cloned, element located at ${left},${top} with size ${width}x${height} using computed rendering`
|
||||
);
|
||||
|
||||
CacheStorage.attachInstance(options.cache);
|
||||
Logger.getInstance(instanceName).debug(`Starting DOM parsing`);
|
||||
const root = parseTree(clonedElement);
|
||||
CacheStorage.detachInstance();
|
||||
context.logger.debug(`Starting DOM parsing`);
|
||||
const root = parseTree(context, clonedElement);
|
||||
|
||||
if (backgroundColor === root.styles.backgroundColor) {
|
||||
root.styles.backgroundColor = COLORS.TRANSPARENT;
|
||||
}
|
||||
|
||||
Logger.getInstance(instanceName).debug(`Starting renderer`);
|
||||
context.logger.debug(
|
||||
`Starting renderer for element at ${renderOptions.x},${renderOptions.y} with size ${renderOptions.width}x${renderOptions.height}`
|
||||
);
|
||||
|
||||
const renderer = new CanvasRenderer(renderOptions);
|
||||
const renderer = new CanvasRenderer(context, renderOptions);
|
||||
canvas = await renderer.render(root);
|
||||
}
|
||||
|
||||
if (options.removeContainer === true) {
|
||||
if (opts.removeContainer ?? true) {
|
||||
if (!DocumentCloner.destroy(container)) {
|
||||
Logger.getInstance(instanceName).error(`Cannot detach cloned iframe as it is not in the DOM anymore`);
|
||||
context.logger.error(`Cannot detach cloned iframe as it is not in the DOM anymore`);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.getInstance(instanceName).debug(`Finished rendering`);
|
||||
Logger.destroy(instanceName);
|
||||
CacheStorage.destroy(instanceName);
|
||||
context.logger.debug(`Finished rendering`);
|
||||
return canvas;
|
||||
};
|
||||
|
||||
const parseBackgroundColor = (context: Context, element: HTMLElement, backgroundColorOverride?: string | null) => {
|
||||
const ownerDocument = element.ownerDocument;
|
||||
// http://www.w3.org/TR/css3-background/#special-backgrounds
|
||||
const documentBackgroundColor = ownerDocument.documentElement
|
||||
? parseColor(context, getComputedStyle(ownerDocument.documentElement).backgroundColor as string)
|
||||
: COLORS.TRANSPARENT;
|
||||
const bodyBackgroundColor = ownerDocument.body
|
||||
? parseColor(context, getComputedStyle(ownerDocument.body).backgroundColor as string)
|
||||
: COLORS.TRANSPARENT;
|
||||
|
||||
const defaultBackgroundColor =
|
||||
typeof backgroundColorOverride === 'string'
|
||||
? parseColor(context, backgroundColorOverride)
|
||||
: backgroundColorOverride === null
|
||||
? COLORS.TRANSPARENT
|
||||
: 0xffffffff;
|
||||
|
||||
return element === ownerDocument.documentElement
|
||||
? isTransparent(documentBackgroundColor)
|
||||
? isTransparent(bodyBackgroundColor)
|
||||
? defaultBackgroundColor
|
||||
: bodyBackgroundColor
|
||||
: documentBackgroundColor
|
||||
: defaultBackgroundColor;
|
||||
};
|
||||
|
@ -1,6 +1,5 @@
|
||||
import {ElementPaint, parseStackingContexts, StackingContext} from '../stacking-context';
|
||||
import {asString, Color, isTransparent} from '../../css/types/color';
|
||||
import {Logger} from '../../core/logger';
|
||||
import {ElementContainer} from '../../dom/element-container';
|
||||
import {BORDER_STYLE} from '../../css/property-descriptors/border-style';
|
||||
import {CSSParsedDeclaration} from '../../css/index';
|
||||
@ -17,11 +16,10 @@ import {
|
||||
parsePathForBorderDoubleOuter,
|
||||
parsePathForBorderStroke
|
||||
} from '../border';
|
||||
import {Cache} from '../../core/cache-storage';
|
||||
import {calculateBackgroundRendering, getBackgroundValueForIndex} from '../background';
|
||||
import {isDimensionToken} from '../../css/syntax/parser';
|
||||
import {TextBounds} from '../../css/layout/text';
|
||||
import {fromCodePoint, toCodePoints} from 'css-line-break';
|
||||
import {fromCodePoint, toCodePoints} from 'text-segmentation';
|
||||
import {ImageElementContainer} from '../../dom/replaced-elements/image-element-container';
|
||||
import {contentBox} from '../box-sizing';
|
||||
import {CanvasElementContainer} from '../../dom/replaced-elements/canvas-element-container';
|
||||
@ -43,39 +41,35 @@ import {TextareaElementContainer} from '../../dom/elements/textarea-element-cont
|
||||
import {SelectElementContainer} from '../../dom/elements/select-element-container';
|
||||
import {IFrameElementContainer} from '../../dom/replaced-elements/iframe-element-container';
|
||||
import {TextShadow} from '../../css/property-descriptors/text-shadow';
|
||||
import {PAINT_ORDER_LAYER} from '../../css/property-descriptors/paint-order';
|
||||
import {Renderer} from '../renderer';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export type RenderConfigurations = RenderOptions & {
|
||||
backgroundColor: Color | null;
|
||||
};
|
||||
|
||||
export interface RenderOptions {
|
||||
id: string;
|
||||
scale: number;
|
||||
canvas?: HTMLCanvasElement;
|
||||
x: number;
|
||||
y: number;
|
||||
scrollX: number;
|
||||
scrollY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
windowWidth: number;
|
||||
windowHeight: number;
|
||||
cache: Cache;
|
||||
}
|
||||
|
||||
const MASK_OFFSET = 10000;
|
||||
|
||||
export class CanvasRenderer {
|
||||
export class CanvasRenderer extends Renderer {
|
||||
canvas: HTMLCanvasElement;
|
||||
ctx: CanvasRenderingContext2D;
|
||||
options: RenderConfigurations;
|
||||
private readonly _activeEffects: IElementEffect[] = [];
|
||||
private readonly fontMetrics: FontMetrics;
|
||||
|
||||
constructor(options: RenderConfigurations) {
|
||||
constructor(context: Context, options: RenderConfigurations) {
|
||||
super(context, options);
|
||||
this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
|
||||
this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||
this.options = options;
|
||||
if (!options.canvas) {
|
||||
this.canvas.width = Math.floor(options.width * options.scale);
|
||||
this.canvas.height = Math.floor(options.height * options.scale);
|
||||
@ -84,11 +78,11 @@ export class CanvasRenderer {
|
||||
}
|
||||
this.fontMetrics = new FontMetrics(document);
|
||||
this.ctx.scale(this.options.scale, this.options.scale);
|
||||
this.ctx.translate(-options.x + options.scrollX, -options.y + options.scrollY);
|
||||
this.ctx.translate(-options.x, -options.y);
|
||||
this.ctx.textBaseline = 'bottom';
|
||||
this._activeEffects = [];
|
||||
Logger.getInstance(options.id).debug(
|
||||
`Canvas renderer initialized (${options.width}x${options.height} at ${options.x},${options.y}) with scale ${options.scale}`
|
||||
this.context.logger.debug(
|
||||
`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`
|
||||
);
|
||||
}
|
||||
|
||||
@ -179,65 +173,89 @@ export class CanvasRenderer {
|
||||
const [font, fontFamily, fontSize] = this.createFontStyle(styles);
|
||||
|
||||
this.ctx.font = font;
|
||||
this.ctx.textBaseline = 'alphabetic';
|
||||
|
||||
this.ctx.textBaseline = 'alphabetic';
|
||||
const {baseline, middle} = this.fontMetrics.getMetrics(fontFamily, fontSize);
|
||||
const paintOrder = styles.paintOrder;
|
||||
|
||||
text.textBounds.forEach((text) => {
|
||||
this.ctx.fillStyle = asString(styles.color);
|
||||
this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline);
|
||||
const textShadows: TextShadow = styles.textShadow;
|
||||
|
||||
if (textShadows.length && text.text.trim().length) {
|
||||
textShadows
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.forEach((textShadow) => {
|
||||
this.ctx.shadowColor = asString(textShadow.color);
|
||||
this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
|
||||
this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
|
||||
this.ctx.shadowBlur = textShadow.blur.number;
|
||||
|
||||
paintOrder.forEach((paintOrderLayer) => {
|
||||
switch (paintOrderLayer) {
|
||||
case PAINT_ORDER_LAYER.FILL:
|
||||
this.ctx.fillStyle = asString(styles.color);
|
||||
this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline);
|
||||
});
|
||||
const textShadows: TextShadow = styles.textShadow;
|
||||
|
||||
this.ctx.shadowColor = '';
|
||||
this.ctx.shadowOffsetX = 0;
|
||||
this.ctx.shadowOffsetY = 0;
|
||||
this.ctx.shadowBlur = 0;
|
||||
}
|
||||
if (textShadows.length && text.text.trim().length) {
|
||||
textShadows
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.forEach((textShadow) => {
|
||||
this.ctx.shadowColor = asString(textShadow.color);
|
||||
this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
|
||||
this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
|
||||
this.ctx.shadowBlur = textShadow.blur.number;
|
||||
|
||||
if (styles.textDecorationLine.length) {
|
||||
this.ctx.fillStyle = asString(styles.textDecorationColor || styles.color);
|
||||
styles.textDecorationLine.forEach((textDecorationLine) => {
|
||||
switch (textDecorationLine) {
|
||||
case TEXT_DECORATION_LINE.UNDERLINE:
|
||||
// Draws a line at the baseline of the font
|
||||
// TODO As some browsers display the line as more than 1px if the font-size is big,
|
||||
// need to take that into account both in position and size
|
||||
this.ctx.fillRect(
|
||||
text.bounds.left,
|
||||
Math.round(text.bounds.top + baseline),
|
||||
text.bounds.width,
|
||||
1
|
||||
);
|
||||
this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline);
|
||||
});
|
||||
|
||||
break;
|
||||
case TEXT_DECORATION_LINE.OVERLINE:
|
||||
this.ctx.fillRect(text.bounds.left, Math.round(text.bounds.top), text.bounds.width, 1);
|
||||
break;
|
||||
case TEXT_DECORATION_LINE.LINE_THROUGH:
|
||||
// TODO try and find exact position for line-through
|
||||
this.ctx.fillRect(
|
||||
text.bounds.left,
|
||||
Math.ceil(text.bounds.top + middle),
|
||||
text.bounds.width,
|
||||
1
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.ctx.shadowColor = '';
|
||||
this.ctx.shadowOffsetX = 0;
|
||||
this.ctx.shadowOffsetY = 0;
|
||||
this.ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
if (styles.textDecorationLine.length) {
|
||||
this.ctx.fillStyle = asString(styles.textDecorationColor || styles.color);
|
||||
styles.textDecorationLine.forEach((textDecorationLine) => {
|
||||
switch (textDecorationLine) {
|
||||
case TEXT_DECORATION_LINE.UNDERLINE:
|
||||
// Draws a line at the baseline of the font
|
||||
// TODO As some browsers display the line as more than 1px if the font-size is big,
|
||||
// need to take that into account both in position and size
|
||||
this.ctx.fillRect(
|
||||
text.bounds.left,
|
||||
Math.round(text.bounds.top + baseline),
|
||||
text.bounds.width,
|
||||
1
|
||||
);
|
||||
|
||||
break;
|
||||
case TEXT_DECORATION_LINE.OVERLINE:
|
||||
this.ctx.fillRect(
|
||||
text.bounds.left,
|
||||
Math.round(text.bounds.top),
|
||||
text.bounds.width,
|
||||
1
|
||||
);
|
||||
break;
|
||||
case TEXT_DECORATION_LINE.LINE_THROUGH:
|
||||
// TODO try and find exact position for line-through
|
||||
this.ctx.fillRect(
|
||||
text.bounds.left,
|
||||
Math.ceil(text.bounds.top + middle),
|
||||
text.bounds.width,
|
||||
1
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case PAINT_ORDER_LAYER.STROKE:
|
||||
if (styles.webkitTextStrokeWidth && text.text.trim().length) {
|
||||
this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
|
||||
this.ctx.lineWidth = styles.webkitTextStrokeWidth;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.ctx.lineJoin = !!(window as any).chrome ? 'miter' : 'round';
|
||||
this.ctx.strokeText(text.text, text.bounds.left, text.bounds.top + baseline);
|
||||
}
|
||||
this.ctx.strokeStyle = '';
|
||||
this.ctx.lineWidth = 0;
|
||||
this.ctx.lineJoin = 'miter';
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -278,10 +296,10 @@ export class CanvasRenderer {
|
||||
|
||||
if (container instanceof ImageElementContainer) {
|
||||
try {
|
||||
const image = await this.options.cache.match(container.src);
|
||||
const image = await this.context.cache.match(container.src);
|
||||
this.renderReplacedElement(container, curves, image);
|
||||
} catch (e) {
|
||||
Logger.getInstance(this.options.id).error(`Error loading image ${container.src}`);
|
||||
this.context.logger.error(`Error loading image ${container.src}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,27 +309,21 @@ export class CanvasRenderer {
|
||||
|
||||
if (container instanceof SVGElementContainer) {
|
||||
try {
|
||||
const image = await this.options.cache.match(container.svg);
|
||||
const image = await this.context.cache.match(container.svg);
|
||||
this.renderReplacedElement(container, curves, image);
|
||||
} catch (e) {
|
||||
Logger.getInstance(this.options.id).error(`Error loading svg ${container.svg.substring(0, 255)}`);
|
||||
this.context.logger.error(`Error loading svg ${container.svg.substring(0, 255)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (container instanceof IFrameElementContainer && container.tree) {
|
||||
const iframeRenderer = new CanvasRenderer({
|
||||
id: this.options.id,
|
||||
const iframeRenderer = new CanvasRenderer(this.context, {
|
||||
scale: this.options.scale,
|
||||
backgroundColor: container.backgroundColor,
|
||||
x: 0,
|
||||
y: 0,
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
width: container.width,
|
||||
height: container.height,
|
||||
cache: this.options.cache,
|
||||
windowWidth: container.width,
|
||||
windowHeight: container.height
|
||||
height: container.height
|
||||
});
|
||||
|
||||
const canvas = await iframeRenderer.render(container.tree);
|
||||
@ -376,7 +388,7 @@ export class CanvasRenderer {
|
||||
this.ctx.font = fontFamily;
|
||||
this.ctx.fillStyle = asString(styles.color);
|
||||
|
||||
this.ctx.textBaseline = 'middle';
|
||||
this.ctx.textBaseline = 'alphabetic';
|
||||
this.ctx.textAlign = canvasTextAlign(container.styles.textAlign);
|
||||
|
||||
const bounds = contentBox(container);
|
||||
@ -409,7 +421,7 @@ export class CanvasRenderer {
|
||||
baseline
|
||||
);
|
||||
this.ctx.restore();
|
||||
this.ctx.textBaseline = 'bottom';
|
||||
this.ctx.textBaseline = 'alphabetic';
|
||||
this.ctx.textAlign = 'left';
|
||||
}
|
||||
|
||||
@ -420,14 +432,14 @@ export class CanvasRenderer {
|
||||
let image;
|
||||
const url = (img as CSSURLImage).url;
|
||||
try {
|
||||
image = await this.options.cache.match(url);
|
||||
image = await this.context.cache.match(url);
|
||||
this.ctx.drawImage(image, container.bounds.left - (image.width + 10), container.bounds.top);
|
||||
} catch (e) {
|
||||
Logger.getInstance(this.options.id).error(`Error loading list-style-image ${url}`);
|
||||
this.context.logger.error(`Error loading list-style-image ${url}`);
|
||||
}
|
||||
}
|
||||
} else if (paint.listValue && container.styles.listStyleType !== LIST_STYLE_TYPE.NONE) {
|
||||
const [fontFamily, fontSize] = this.createFontStyle(styles);
|
||||
const [fontFamily] = this.createFontStyle(styles);
|
||||
|
||||
this.ctx.font = fontFamily;
|
||||
this.ctx.fillStyle = asString(styles.color);
|
||||
@ -441,12 +453,10 @@ export class CanvasRenderer {
|
||||
computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 1
|
||||
);
|
||||
|
||||
const {baseline} = this.fontMetrics.getMetrics(fontFamily, fontSize);
|
||||
|
||||
this.renderTextWithLetterSpacing(
|
||||
new TextBounds(paint.listValue, bounds),
|
||||
styles.letterSpacing,
|
||||
baseline
|
||||
computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 2
|
||||
);
|
||||
this.ctx.textBaseline = 'bottom';
|
||||
this.ctx.textAlign = 'left';
|
||||
@ -554,7 +564,8 @@ export class CanvasRenderer {
|
||||
return image;
|
||||
}
|
||||
|
||||
const canvas = (this.canvas.ownerDocument as Document).createElement('canvas');
|
||||
const ownerDocument = this.canvas.ownerDocument ?? document;
|
||||
const canvas = ownerDocument.createElement('canvas');
|
||||
canvas.width = Math.max(1, width);
|
||||
canvas.height = Math.max(1, height);
|
||||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||
@ -569,9 +580,9 @@ export class CanvasRenderer {
|
||||
let image;
|
||||
const url = (backgroundImage as CSSURLImage).url;
|
||||
try {
|
||||
image = await this.options.cache.match(url);
|
||||
image = await this.context.cache.match(url);
|
||||
} catch (e) {
|
||||
Logger.getInstance(this.options.id).error(`Error loading background-image ${url}`);
|
||||
this.context.logger.error(`Error loading background-image ${url}`);
|
||||
}
|
||||
|
||||
if (image) {
|
||||
@ -617,7 +628,7 @@ export class CanvasRenderer {
|
||||
const y = getAbsoluteValue(position[position.length - 1], height);
|
||||
|
||||
const [rx, ry] = calculateRadius(backgroundImage, x, y, width, height);
|
||||
if (rx > 0 && rx > 0) {
|
||||
if (rx > 0 && ry > 0) {
|
||||
const radialGradient = this.ctx.createRadialGradient(left + x, top + y, 0, left + x, top + y, rx);
|
||||
|
||||
processColorStops(backgroundImage.stops, rx * 2).forEach((colorStop) =>
|
||||
@ -879,12 +890,7 @@ export class CanvasRenderer {
|
||||
async render(element: ElementContainer): Promise<HTMLCanvasElement> {
|
||||
if (this.options.backgroundColor) {
|
||||
this.ctx.fillStyle = asString(this.options.backgroundColor);
|
||||
this.ctx.fillRect(
|
||||
this.options.x - this.options.scrollX,
|
||||
this.options.y - this.options.scrollY,
|
||||
this.options.width,
|
||||
this.options.height
|
||||
);
|
||||
this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
|
||||
}
|
||||
|
||||
const stack = parseStackingContexts(element);
|
||||
|
@ -1,14 +1,16 @@
|
||||
import {RenderConfigurations} from './canvas-renderer';
|
||||
import {Logger} from '../../core/logger';
|
||||
import {createForeignObjectSVG} from '../../core/features';
|
||||
import {asString} from '../../css/types/color';
|
||||
import {Renderer} from '../renderer';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export class ForeignObjectRenderer {
|
||||
export class ForeignObjectRenderer extends Renderer {
|
||||
canvas: HTMLCanvasElement;
|
||||
ctx: CanvasRenderingContext2D;
|
||||
options: RenderConfigurations;
|
||||
|
||||
constructor(options: RenderConfigurations) {
|
||||
constructor(context: Context, options: RenderConfigurations) {
|
||||
super(context, options);
|
||||
this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
|
||||
this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||
this.options = options;
|
||||
@ -18,18 +20,18 @@ export class ForeignObjectRenderer {
|
||||
this.canvas.style.height = `${options.height}px`;
|
||||
|
||||
this.ctx.scale(this.options.scale, this.options.scale);
|
||||
this.ctx.translate(-options.x + options.scrollX, -options.y + options.scrollY);
|
||||
Logger.getInstance(options.id).debug(
|
||||
this.ctx.translate(-options.x, -options.y);
|
||||
this.context.logger.debug(
|
||||
`EXPERIMENTAL ForeignObject renderer initialized (${options.width}x${options.height} at ${options.x},${options.y}) with scale ${options.scale}`
|
||||
);
|
||||
}
|
||||
|
||||
async render(element: HTMLElement): Promise<HTMLCanvasElement> {
|
||||
const svg = createForeignObjectSVG(
|
||||
Math.max(this.options.windowWidth, this.options.width) * this.options.scale,
|
||||
Math.max(this.options.windowHeight, this.options.height) * this.options.scale,
|
||||
this.options.scrollX * this.options.scale,
|
||||
this.options.scrollY * this.options.scale,
|
||||
this.options.width * this.options.scale,
|
||||
this.options.height * this.options.scale,
|
||||
this.options.scale,
|
||||
this.options.scale,
|
||||
element
|
||||
);
|
||||
|
||||
|
6
src/render/renderer.ts
Normal file
6
src/render/renderer.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import {Context} from '../core/context';
|
||||
import {RenderConfigurations} from './canvas/canvas-renderer';
|
||||
|
||||
export class Renderer {
|
||||
constructor(protected readonly context: Context, protected readonly options: RenderConfigurations) {}
|
||||
}
|
3
tests/reftests/background/base64.css
Normal file
3
tests/reftests/background/base64.css
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user