Compare commits

..

34 Commits

Author SHA1 Message Date
CI
7c3269bdbe chore(release): 1.4.1 2022-01-22 16:19:57 +00:00
e587a82dca fix: Properties x and y of BoundingRect is undefined in old browser (#2797) 2022-01-23 00:18:49 +08:00
67c5e8dec4 deps: fix source maps (#2812) 2022-01-23 00:17:25 +08:00
181d1b1103 feat: add support for <video> elements (#2788) 2022-01-02 16:14:27 +08:00
46db86755f fix: source maps (#2787) 2022-01-02 16:08:10 +08:00
CI
9fda3e1ede chore(release): 1.4.0 2022-01-01 15:08:48 +00:00
6521a487d7 feat: use native text segmenter where available (#2782) 2022-01-01 21:22:31 +08:00
0476d06515 fix: ios text wrapping with 0 width rect (#2786) 2022-01-01 13:07:52 +08:00
74696faf47 fix: adopted stylesheets (#2785) 2022-01-01 01:57:17 +08:00
1cc853a318 fix: reduce SLICE_STACK_SIZE to 50k (#2784) 2021-12-31 15:30:18 +08:00
CI
04787ee88a chore(release): 1.3.4 2021-12-29 14:11:55 +00:00
ba2b1cd8e9 fix: ios 15 font rendering crash (#2645) 2021-12-29 22:09:32 +08:00
d9222075da ci: add ios 15.0 testing (#2780) 2021-12-29 21:02:16 +08:00
CI
101c32ed71 chore(release): 1.3.3 2021-11-23 10:09:40 +00:00
ddffaecf6d add missing changelog entries for 1.0.0 (#2758) 2021-11-23 17:47:45 +08:00
fd22a01a3c fix: "offsets" text when font is big 2021-11-23 17:32:41 +08:00
ed57781594 ci: fix macos action runners (#2757) 2021-11-23 17:17:51 +08:00
eeda86bd5e fix: const enums (#2651) 2021-08-16 19:30:33 +08:00
CI
0b1f0a7e90 chore(release): 1.3.2 2021-08-15 12:54:26 +00:00
38c682955a fix: overflows with absolutely positioned content (#2663) 2021-08-15 19:41:57 +08:00
01ed87907a docs: add warning for webgl cloning with preserveDrawingBuffer=false (#2661) 2021-08-14 17:18:30 +08:00
58ff0003f7 docs: include src files on www (#2660) 2021-08-14 16:48:22 +08:00
f143166551 fix: disable transition properties (#2659) 2021-08-14 16:22:36 +08:00
cd0d7258c3 feat: add support for data-html2canvas-debug property for debugging (#2658) 2021-08-14 15:01:41 +08:00
CI
b482725994 chore(release): 1.3.1 2021-08-14 06:06:09 +00:00
1b55ed5668 fix: multi arg transition/animation duration (#2657) 2021-08-14 14:05:15 +08:00
CI
68377b3244 chore(release): 1.3.0 2021-08-13 11:52:05 +00:00
6947982848 feat: add rtl render support (#2653) 2021-08-13 19:50:59 +08:00
437b367d3b feat: correctly break graphemes (#2652) 2021-08-13 18:57:49 +08:00
969638fb94 fix: finish animation/transitions for elements (#2632) 2021-08-13 18:15:55 +08:00
f919204efa test: add letter-spacing test for zwj emoji (#2650) 2021-08-13 17:45:42 +08:00
c378e22069 fix: correctly handle allowTaint canvas cloning (#2649) 2021-08-13 17:32:55 +08:00
CI
2b4de68e92 chore(release): 1.2.2 2021-08-10 10:39:01 +00:00
1941b9e0ac fix: parsing counter content in pseudo element (#2640) 2021-08-09 18:43:42 +08:00
58 changed files with 1397 additions and 2150 deletions

View File

@ -101,22 +101,29 @@ jobs:
- os: macos-latest
name: OSX Safari Stable
targetBrowser: Safari_Stable
- os: macos-latest
- os: macos-10.15
name: iOS Simulator Safari 12
targetBrowser: Safari_IOS_12
xcode: /Applications/Xcode_10.3.app
- os: macos-latest
- os: macos-10.15
name: iOS Simulator Safari 13
targetBrowser: Safari_IOS_13
xcode: /Applications/Xcode_11.6_beta.app
- os: macos-latest
xcode: /Applications/Xcode_11.7.app
- os: macos-10.15
name: iOS Simulator Safari 14
targetBrowser: Safari_IOS_14
xcode: /Applications/Xcode_12_beta.app
xcode: /Applications/Xcode_12.4.app
- os: macos-11
name: iOS Simulator Safari 15.0
targetBrowser: Safari_IOS_15_0
xcode: /Applications/Xcode_13.0.app
- os: macos-11
name: iOS Simulator Safari 15
targetBrowser: Safari_IOS_15
xcode: /Applications/Xcode_13.0.app
xcode: /Applications/Xcode_13.2.app
- os: windows-latest
name: Windows Internet Explorer 9 (Emulated)
targetBrowser: IE_9
- os: windows-latest
name: Windows Internet Explorer 10 (Emulated)
targetBrowser: IE_10

View File

@ -2,6 +2,136 @@
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.4.1](https://github.com/niklasvh/html2canvas/compare/v1.4.0...v1.4.1) (2022-01-22)
### deps
* fix source maps (#2812) ([67c5e8d](https://github.com/niklasvh/html2canvas/commit/67c5e8dec4b2af9260a2b5b75b3399495fd1fee9)), closes [#2812](https://github.com/niklasvh/html2canvas/issues/2812)
### feat
* add support for <video> elements (#2788) ([181d1b1](https://github.com/niklasvh/html2canvas/commit/181d1b1103910d6e1b5277d5c007fc5e3006c6bf)), closes [#2788](https://github.com/niklasvh/html2canvas/issues/2788)
### fix
* Properties x and y of BoundingRect is undefined in old browser (#2797) ([e587a82](https://github.com/niklasvh/html2canvas/commit/e587a82dca01d9ada78cae34fd1bdb934e547f9b)), closes [#2797](https://github.com/niklasvh/html2canvas/issues/2797)
* source maps (#2787) ([46db867](https://github.com/niklasvh/html2canvas/commit/46db86755f064828559a4b0b37310f3ae94f5494)), closes [#2787](https://github.com/niklasvh/html2canvas/issues/2787)
# [1.4.0](https://github.com/niklasvh/html2canvas/compare/v1.3.4...v1.4.0) (2022-01-01)
### feat
* use native text segmenter where available (#2782) ([6521a48](https://github.com/niklasvh/html2canvas/commit/6521a487d78172f7179f7c973c1a3af40eb92009)), closes [#2782](https://github.com/niklasvh/html2canvas/issues/2782)
### fix
* adopted stylesheets (#2785) ([74696fa](https://github.com/niklasvh/html2canvas/commit/74696faf47c07b48b9c9587db0b999da1c08a8be)), closes [#2785](https://github.com/niklasvh/html2canvas/issues/2785)
* ios text wrapping with 0 width rect (#2786) ([0476d06](https://github.com/niklasvh/html2canvas/commit/0476d065158c33d2020a9f602b3043e5e2f90c75)), closes [#2786](https://github.com/niklasvh/html2canvas/issues/2786)
* reduce SLICE_STACK_SIZE to 50k (#2784) ([1cc853a](https://github.com/niklasvh/html2canvas/commit/1cc853a3186853eaca00af060f651697dc3497a9)), closes [#2784](https://github.com/niklasvh/html2canvas/issues/2784)
## [1.3.4](https://github.com/niklasvh/html2canvas/compare/v1.3.3...v1.3.4) (2021-12-29)
### ci
* add ios 15.0 testing (#2780) ([d922207](https://github.com/niklasvh/html2canvas/commit/d9222075daaed08884491b0563fc899ee0ced731)), closes [#2780](https://github.com/niklasvh/html2canvas/issues/2780)
### fix
* ios 15 font rendering crash (#2645) ([ba2b1cd](https://github.com/niklasvh/html2canvas/commit/ba2b1cd8e9a9d7932675d7abffce1526a609e769)), closes [#2645](https://github.com/niklasvh/html2canvas/issues/2645)
## [1.3.3](https://github.com/niklasvh/html2canvas/compare/v1.3.2...v1.3.3) (2021-11-23)
### ci
* fix macos action runners (#2757) ([ed57781](https://github.com/niklasvh/html2canvas/commit/ed577815949b6a565df54f2101cf6f0fb731c290)), closes [#2757](https://github.com/niklasvh/html2canvas/issues/2757)
### fix
* "offsets" text when font is big ([fd22a01](https://github.com/niklasvh/html2canvas/commit/fd22a01a3c9e39293f47caaed0c0e13d2dc8170c))
* const enums (#2651) ([eeda86b](https://github.com/niklasvh/html2canvas/commit/eeda86bd5e81fb4e97675fe9bee3d4d15899997f)), closes [#2651](https://github.com/niklasvh/html2canvas/issues/2651)
## [1.3.2](https://github.com/niklasvh/html2canvas/compare/v1.3.1...v1.3.2) (2021-08-15)
### docs
* add warning for webgl cloning with preserveDrawingBuffer=false (#2661) ([01ed879](https://github.com/niklasvh/html2canvas/commit/01ed87907ad9c7688880e2c5cb8ebc22ef73a4d8)), closes [#2661](https://github.com/niklasvh/html2canvas/issues/2661)
* include src files on www (#2660) ([58ff000](https://github.com/niklasvh/html2canvas/commit/58ff0003f77d825ac027eeec95fa80c0123eaf8f)), closes [#2660](https://github.com/niklasvh/html2canvas/issues/2660)
### feat
* add support for data-html2canvas-debug property for debugging (#2658) ([cd0d725](https://github.com/niklasvh/html2canvas/commit/cd0d7258c3a93f2989d5d9ec0244ba2763ea2d23)), closes [#2658](https://github.com/niklasvh/html2canvas/issues/2658)
### fix
* disable transition properties (#2659) ([f143166](https://github.com/niklasvh/html2canvas/commit/f1431665513e0a4636fb167a241f4a0571ba728a)), closes [#2659](https://github.com/niklasvh/html2canvas/issues/2659)
* overflows with absolutely positioned content (#2663) ([38c6829](https://github.com/niklasvh/html2canvas/commit/38c682955a9299ca7785af71d8f251df799405b0)), closes [#2663](https://github.com/niklasvh/html2canvas/issues/2663)
## [1.3.1](https://github.com/niklasvh/html2canvas/compare/v1.3.0...v1.3.1) (2021-08-14)
### fix
* multi arg transition/animation duration (#2657) ([1b55ed5](https://github.com/niklasvh/html2canvas/commit/1b55ed5668dcbbe1c6d8d7e94736d8f2da2d31c5)), closes [#2657](https://github.com/niklasvh/html2canvas/issues/2657)
# [1.3.0](https://github.com/niklasvh/html2canvas/compare/v1.2.2...v1.3.0) (2021-08-13)
### feat
* add rtl render support (#2653) ([6947982](https://github.com/niklasvh/html2canvas/commit/694798284838b16882e648914da0905818aa366c)), closes [#2653](https://github.com/niklasvh/html2canvas/issues/2653)
* correctly break graphemes (#2652) ([437b367](https://github.com/niklasvh/html2canvas/commit/437b367d3ba9dfd7f9a4c8042ac8d00208c09770)), closes [#2652](https://github.com/niklasvh/html2canvas/issues/2652)
### fix
* correctly handle allowTaint canvas cloning (#2649) ([c378e22](https://github.com/niklasvh/html2canvas/commit/c378e220694c14cb7b3b4b8650a7757f8fc23c7a)), closes [#2649](https://github.com/niklasvh/html2canvas/issues/2649)
* finish animation/transitions for elements (#2632) ([969638f](https://github.com/niklasvh/html2canvas/commit/969638fb94a0a14c64a667fa6e5689f79d9f1044)), closes [#2632](https://github.com/niklasvh/html2canvas/issues/2632)
### test
* add letter-spacing test for zwj emoji (#2650) ([f919204](https://github.com/niklasvh/html2canvas/commit/f919204efa06af219f155ca279f96124bb92862b)), closes [#2650](https://github.com/niklasvh/html2canvas/issues/2650)
## [1.2.2](https://github.com/niklasvh/html2canvas/compare/v1.2.1...v1.2.2) (2021-08-10)
### ci
* add ios15 target (#2564) ([e429e04](https://github.com/niklasvh/html2canvas/commit/e429e0443adf5c7ca3041b97a8157b8911302206)), closes [#2564](https://github.com/niklasvh/html2canvas/issues/2564)
### docs
* update test previewer (#2637) ([7a06d0c](https://github.com/niklasvh/html2canvas/commit/7a06d0c2c2f3b8a1d1a8a85c540f8288b782e8c6)), closes [#2637](https://github.com/niklasvh/html2canvas/issues/2637)
### fix
* parsing counter content in pseudo element (#2640) ([1941b9e](https://github.com/niklasvh/html2canvas/commit/1941b9e0acfd9243da0beaf70e1643cab1b4a963)), closes [#2640](https://github.com/niklasvh/html2canvas/issues/2640)
* radial gradient ry check (#2631) ([a0dd38a](https://github.com/niklasvh/html2canvas/commit/a0dd38a8be4e540ae1c1f4b4e41f6c386f3e454f)), closes [#2631](https://github.com/niklasvh/html2canvas/issues/2631)
* test for ios range line break error (#2635) ([f43f942](https://github.com/niklasvh/html2canvas/commit/f43f942fcd793dde9cdc6c0438f379ec3c05c405)), closes [#2635](https://github.com/niklasvh/html2canvas/issues/2635)
### test
* large base64 encoded background (#2636) ([e36408a](https://github.com/niklasvh/html2canvas/commit/e36408ad030fe31acd9969a37fe24c1621c0bd04)), closes [#2636](https://github.com/niklasvh/html2canvas/issues/2636)
## [1.2.1](https://github.com/niklasvh/html2canvas/compare/v1.2.0...v1.2.1) (2021-08-05)
@ -141,9 +271,14 @@ All notable changes to this project will be documented in this file. See [standa
* update www deps (#2525) ([2a013e2](https://github.com/niklasvh/html2canvas/commit/2a013e20c814b7dbaea98f54f0bde8f553eb79a2)), closes [#2525](https://github.com/niklasvh/html2canvas/issues/2525)
### feat
* add support for border-style dashed, dotted, double (#2531) ([72cd528](https://github.com/niklasvh/html2canvas/commit/72cd5284296e4cdb3fe88f2982ec7528604b6618))
### fix
* opacity with overflow hidden (#2450) ([82b7da5](https://github.com/niklasvh/html2canvas/commit/82b7da558c342e7f53d298bb1d843a5db86c3b21)), closes [#2450](https://github.com/niklasvh/html2canvas/issues/2450)
* top right border radius (#2522) ([ba17267](https://github.com/niklasvh/html2canvas/commit/ba172678f07f962e9f54b398df087e86217d7a13))
### test
@ -291,16 +426,16 @@ All notable changes to this project will be documented in this file. See [standa
* Fix white space appearing on element rendering (Fix #1438)
* Reset canvas transform on finish (Fix #1494)
# v1.0.0-alpha.11 - 1.4.2018
* Fix IE11 member not found error
# v1.0.0-alpha.11 - 1.4.2018
* Fix IE11 member not found error
* Support blob image resources in non-foreignObjectRendering mode
# v1.0.0-alpha.10 - 15.2.2018
# v1.0.0-alpha.10 - 15.2.2018
* Re-introduce `onclone` option (Fix #1434)
* Add `ignoreElements` predicate function option
* Fix version console logging
# v1.0.0-alpha.9 - 7.1.2018
# v1.0.0-alpha.9 - 7.1.2018
* Fix dynamic style sheets
* Fix > 50% border-radius values
@ -312,7 +447,7 @@ All notable changes to this project will be documented in this file. See [standa
* Fix form input rendering (#1338)
* Improve word line breaking algorithm
# v1.0.0-alpha.6 - 28.12.2017
# v1.0.0-alpha.6 - 28.12.2017
* Fix list-style: none (#1340)
* Extend supported values for pseudo element content
@ -322,7 +457,7 @@ All notable changes to this project will be documented in this file. See [standa
* Fix overflow: auto
* Added support for rendering list-style
v1.0.0-alpha.4 - 12.12.2017
v1.0.0-alpha.4 - 12.12.2017
* Fix rendering with multiple fonts defined (Fix #796)
* Add support for radial-gradients
* Fix logging option (#1302)
@ -344,8 +479,8 @@ All notable changes to this project will be documented in this file. See [standa
##### Breaking Changes #####
* Remove deprecated onrendered callback, calling `html2canvas` returns a `Promise<HTMLCanvasElement>`
* Removed option `type`, same results can be achieved by assigning `x`, `y`, `scrollX`, `scrollY`, `width` and `height` properties.
## New featues / fixes
## New featues / fixes
* Add support for scaling canvas (defaults to device pixel ratio)
* Add support for multiple text-shadows
* Add support for multiple text-decorations
@ -354,7 +489,7 @@ All notable changes to this project will be documented in this file. See [standa
* Correctly handle px and percentage values in linear-gradients
* Correctly support all angle types for linear-gradients
* Add support for multiple values for background-repeat, background-position and background-size
# v0.5.0-beta4 - 23.1.2016
* Fix logger requiring access to window object
* Derequire browserify build
@ -374,11 +509,11 @@ All notable changes to this project will be documented in this file. See [standa
* Fix transparent colors breaking gradients
* Preserve scrolling positions on render
# v0.5.0-alpha2 - 3.2.2015
# v0.5.0-alpha2 - 3.2.2015
* Switch to using browserify for building
* Fix (#517) Chrome stretches background images with 'auto' or single attributes
# v0.5.0-alpha - 19.1.2015
# v0.5.0-alpha - 19.1.2015
* Complete rewrite of library
* Switched interface to return Promise
* Uses hidden iframe window to perform rendering, allowing async rendering and doesn't force the viewport to be scrolled to the top anymore.
@ -389,14 +524,14 @@ All notable changes to this project will be documented in this file. See [standa
* Changed format for proxy requests, permitting binary responses with CORS headers as well
* Fixed many layering issues (see z-index tests)
# v0.4.1 - 7.9.2013
# v0.4.1 - 7.9.2013
* Added support for bower
* Improved z-index ordering
* Basic implementation for CSS transformations
* Fixed inline text in top element
* Basic implementation for text-shadow
# v0.4.0 - 30.1.2013
# v0.4.0 - 30.1.2013
* Added rendering tests with <a href="https://github.com/niklasvh/webdriver.js">webdriver</a>
* Switched to using grunt for building
* Removed support for IE<9, including any FlashCanvas bits
@ -406,7 +541,7 @@ All notable changes to this project will be documented in this file. See [standa
* Support for placeholder rendering
* Reformatted all tests to small units to test specific features
# v0.3.4 - 26.6.2012
# v0.3.4 - 26.6.2012
* Removed (last?) jQuery dependencies (<a href="https://github.com/niklasvh/html2canvas/commit/343b86705fe163766fcf735eb0217130e4bd5b17">niklasvh</a>)
* SVG-powered rendering (<a href="https://github.com/niklasvh/html2canvas/commit/67d3e0d0f59a5a654caf71a2e3be6494ff146c75">niklasvh</a>)

View File

@ -3,7 +3,7 @@ html2canvas
[Homepage](https://html2canvas.hertzen.com) | [Downloads](https://github.com/niklasvh/html2canvas/releases) | [Questions](https://github.com/niklasvh/html2canvas/discussions/categories/q-a)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/niklasvh/html2canvas?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/niklasvh/html2canvas?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
![CI](https://github.com/niklasvh/html2canvas/workflows/CI/badge.svg?branch=master)
[![NPM Downloads](https://img.shields.io/npm/dm/html2canvas.svg)](https://www.npmjs.org/package/html2canvas)
[![NPM Version](https://img.shields.io/npm/v/html2canvas.svg)](https://www.npmjs.org/package/html2canvas)
@ -28,8 +28,7 @@ The library should work fine on the following browsers (with `Promise` polyfill)
* Firefox 3.5+
* Google Chrome
* Opera 12+
* IE10+
* Edge
* IE9+
* Safari 6+
As each CSS property needs to be manually built to be supported, there are a number of properties that are not yet supported.

View File

@ -5,28 +5,28 @@ nextUrl: "/getting-started"
nextTitle: "Getting Started"
---
Before you get started with the script, there are a few things that are good to know regarding the
Before you get started with the script, there are a few things that are good to know regarding the
script and some of its limitations.
## Introduction
The script allows you to take "screenshots" of webpages or parts of it, directly on the users browser.
The screenshot is based on the DOM and as such may not be 100% accurate to the real representation
as it does not make an actual screenshot, but builds the screenshot based on the information
The screenshot is based on the DOM and as such may not be 100% accurate to the real representation
as it does not make an actual screenshot, but builds the screenshot based on the information
available on the page.
## How it works
The script traverses through the DOM of the page it is loaded on. It gathers information on all the elements
there, which it then uses to build a representation of the page. In other words, it does not actually take a
screenshot of the page, but builds a representation of it based on the properties it reads from the DOM.
As a result, it is only able to render correctly properties that it understands, meaning there are many
CSS properties which do not work. For a full list of supported CSS properties, check out the
As a result, it is only able to render correctly properties that it understands, meaning there are many
CSS properties which do not work. For a full list of supported CSS properties, check out the
[supported features](/features/) page.
## Limitations
All the images that the script uses need to reside under the [same origin](http://en.wikipedia.org/wiki/Same_origin_policy)
for it to be able to read them without the assistance of a [proxy](/proxy/). Similarly, if you have other `canvas`
All the images that the script uses need to reside under the [same origin](http://en.wikipedia.org/wiki/Same_origin_policy)
for it to be able to read them without the assistance of a [proxy](/proxy/). Similarly, if you have other `canvas`
elements on the page, which have been tainted with cross-origin content, they will become dirty and no longer readable by html2canvas.
The script doesn't render plugin content such as Flash or Java applets.
@ -37,6 +37,6 @@ The library should work fine on the following browsers (with `Promise` polyfill)
- Firefox 3.5+
- Google Chrome
- Opera 12+
- IE10+
- IE9+
- Edge
- Safari 6+

View File

@ -34,19 +34,31 @@ module.exports = function(config) {
base: 'MobileSafari',
name: 'iPhone 8',
platform: 'iOS',
sdk: '13.6'
sdk: '13.7'
},
Safari_IOS_14: {
base: 'MobileSafari',
name: 'iPhone 8',
platform: 'iOS',
sdk: '14.0'
sdk: '14.4'
},
Safari_IOS_15_0: {
base: 'MobileSafari',
name: 'iPhone 13',
platform: 'iOS',
sdk: '15.0'
},
Safari_IOS_15: {
base: 'MobileSafari',
name: 'iPhone 8',
name: 'iPhone 13',
platform: 'iOS',
sdk: '15.0'
sdk: '15.2'
},
SauceLabs_IE9: {
base: 'SauceLabs',
browserName: 'internet explorer',
version: '9.0',
platform: 'Windows 7'
},
SauceLabs_IE10: {
base: 'SauceLabs',
@ -87,6 +99,11 @@ module.exports = function(config) {
version: '9.3',
device: 'iPhone 6 Plus Simulator'
},
IE_9: {
base: 'IE',
'x-ua-compatible': 'IE=EmulateIE9',
flags: ['-extoff']
},
IE_10: {
base: 'IE',
'x-ua-compatible': 'IE=EmulateIE10',

2569
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
"module": "dist/html2canvas.esm.js",
"typings": "dist/types/index.d.ts",
"browser": "dist/html2canvas.js",
"version": "1.2.1",
"version": "1.4.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": "2.0.1"
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
}
}

View File

@ -30,7 +30,7 @@ export default {
// Allow json resolution
json(),
// Compile TypeScript files
typescript(),
typescript({ sourceMap: true, inlineSources: true }),
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
commonjs({
include: 'node_modules/**'

View File

@ -12,6 +12,7 @@ export class CacheStorage {
}
link.href = url;
link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/
return link.protocol + link.hostname + link.port;
}

29
src/core/debugger.ts Normal file
View File

@ -0,0 +1,29 @@
const elementDebuggerAttribute = 'data-html2canvas-debug';
export const enum DebuggerType {
NONE,
ALL,
CLONE,
PARSE,
RENDER
}
const getElementDebugType = (element: Element): DebuggerType => {
const attribute = element.getAttribute(elementDebuggerAttribute);
switch (attribute) {
case 'all':
return DebuggerType.ALL;
case 'clone':
return DebuggerType.CLONE;
case 'parse':
return DebuggerType.PARSE;
case 'render':
return DebuggerType.RENDER;
default:
return DebuggerType.NONE;
}
};
export const isDebugging = (element: Element, type: Omit<DebuggerType, DebuggerType.NONE>): boolean => {
const elementType = getElementDebugType(element);
return elementType === DebuggerType.ALL || type === elementType;
};

View File

@ -211,5 +211,12 @@ export const FEATURES = {
const value = 'withCredentials' in new XMLHttpRequest();
Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', {value});
return value;
},
get SUPPORT_NATIVE_TEXT_SEGMENTATION(): boolean {
'use strict';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const value = !!(typeof Intl !== 'undefined' && (Intl as any).Segmenter);
Object.defineProperty(FEATURES, 'SUPPORT_NATIVE_TEXT_SEGMENTATION', {value});
return value;
}
};

View File

@ -1,8 +1,8 @@
import {CSSValue} from './syntax/parser';
import {CSSTypes} from './types/index';
import {CSSTypes} from './types';
import {Context} from '../core/context';
export enum PropertyDescriptorParsingType {
export const enum PropertyDescriptorParsingType {
VALUE,
LIST,
IDENT_VALUE,

View File

@ -31,6 +31,7 @@ import {
borderTopWidth
} from './property-descriptors/border-width';
import {color} from './property-descriptors/color';
import {direction} from './property-descriptors/direction';
import {display, DISPLAY} from './property-descriptors/display';
import {float, FLOAT} from './property-descriptors/float';
import {letterSpacing} from './property-descriptors/letter-spacing';
@ -57,6 +58,7 @@ import {Tokenizer} from './syntax/tokenizer';
import {Color, color as colorType, isTransparent} from './types/color';
import {angle} from './types/angle';
import {image} from './types/image';
import {time} from './types/time';
import {opacity} from './property-descriptors/opacity';
import {textDecorationColor} from './property-descriptors/text-decoration-color';
import {textDecorationLine} from './property-descriptors/text-decoration-line';
@ -71,6 +73,7 @@ import {contains} from '../core/bitwise';
import {content} from './property-descriptors/content';
import {counterIncrement} from './property-descriptors/counter-increment';
import {counterReset} from './property-descriptors/counter-reset';
import {duration} from './property-descriptors/duration';
import {quotes} from './property-descriptors/quotes';
import {boxShadow} from './property-descriptors/box-shadow';
import {paintOrder} from './property-descriptors/paint-order';
@ -79,6 +82,7 @@ import {webkitTextStrokeWidth} from './property-descriptors/webkit-text-stroke-w
import {Context} from '../core/context';
export class CSSParsedDeclaration {
animationDuration: ReturnType<typeof duration.parse>;
backgroundClip: ReturnType<typeof backgroundClip.parse>;
backgroundColor: Color;
backgroundImage: ReturnType<typeof backgroundImage.parse>;
@ -104,6 +108,7 @@ export class CSSParsedDeclaration {
borderLeftWidth: ReturnType<typeof borderLeftWidth.parse>;
boxShadow: ReturnType<typeof boxShadow.parse>;
color: Color;
direction: ReturnType<typeof direction.parse>;
display: ReturnType<typeof display.parse>;
float: ReturnType<typeof float.parse>;
fontFamily: ReturnType<typeof fontFamily.parse>;
@ -145,6 +150,7 @@ export class CSSParsedDeclaration {
zIndex: ReturnType<typeof zIndex.parse>;
constructor(context: Context, declaration: CSSStyleDeclaration) {
this.animationDuration = parse(context, duration, declaration.animationDuration);
this.backgroundClip = parse(context, backgroundClip, declaration.backgroundClip);
this.backgroundColor = parse(context, backgroundColor, declaration.backgroundColor);
this.backgroundImage = parse(context, backgroundImage, declaration.backgroundImage);
@ -170,6 +176,7 @@ export class CSSParsedDeclaration {
this.borderLeftWidth = parse(context, borderLeftWidth, declaration.borderLeftWidth);
this.boxShadow = parse(context, boxShadow, declaration.boxShadow);
this.color = parse(context, color, declaration.color);
this.direction = parse(context, direction, declaration.direction);
this.display = parse(context, display, declaration.display);
this.float = parse(context, float, declaration.cssFloat);
this.fontFamily = parse(context, fontFamily, declaration.fontFamily);
@ -306,6 +313,8 @@ const parse = (context: Context, descriptor: CSSPropertyDescriptor<any>, style?:
case 'length-percentage':
const value = parser.parseComponentValue();
return isLengthPercentage(value) ? value : ZERO_LENGTH;
case 'time':
return time.parse(context, parser.parseComponentValue());
}
break;
}

View File

@ -17,11 +17,11 @@ export class Bounds {
}
static fromDOMRectList(context: Context, domRectList: DOMRectList): Bounds {
const domRect = domRectList[0];
const domRect = Array.from(domRectList).find((rect) => rect.width !== 0);
return domRect
? new Bounds(
domRect.x + context.windowBounds.left,
domRect.y + context.windowBounds.top,
domRect.left + context.windowBounds.left,
domRect.top + context.windowBounds.top,
domRect.width,
domRect.height
)

View File

@ -1,6 +1,7 @@
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';
@ -27,15 +28,24 @@ export const parseTextBounds = (
textList.forEach((text) => {
if (styles.textDecorationLine.length || text.trim().length > 0) {
if (FEATURES.SUPPORT_RANGE_BOUNDS) {
if (!FEATURES.SUPPORT_WORD_BREAKING) {
textBounds.push(
new TextBounds(
text,
Bounds.fromDOMRectList(context, createRange(node, offset, text.length).getClientRects())
)
);
const clientRects = createRange(node, offset, text.length).getClientRects();
if (clientRects.length > 1) {
const subSegments = segmentGraphemes(text);
let subOffset = 0;
subSegments.forEach((subSegment) => {
textBounds.push(
new TextBounds(
subSegment,
Bounds.fromDOMRectList(
context,
createRange(node, subOffset + offset, subSegment.length).getClientRects()
)
)
);
subOffset += subSegment.length;
});
} else {
textBounds.push(new TextBounds(text, getRangeBounds(context, node, offset, text.length)));
textBounds.push(new TextBounds(text, Bounds.fromDOMRectList(context, clientRects)));
}
} else {
const replacementNode = node.splitText(text.length);
@ -81,12 +91,32 @@ const createRange = (node: Text, offset: number, length: number): Range => {
return range;
};
const getRangeBounds = (context: Context, node: Text, offset: number, length: number): Bounds => {
return Bounds.fromClientRect(context, createRange(node, offset, length).getBoundingClientRect());
export const segmentGraphemes = (value: string): string[] => {
if (FEATURES.SUPPORT_NATIVE_TEXT_SEGMENTATION) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const segmenter = new (Intl as any).Segmenter(void 0, {granularity: 'grapheme'});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return Array.from(segmenter.segment(value)).map((segment: any) => segment.segment);
}
return splitGraphemes(value);
};
const segmentWords = (value: string, styles: CSSParsedDeclaration): string[] => {
if (FEATURES.SUPPORT_NATIVE_TEXT_SEGMENTATION) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const segmenter = new (Intl as any).Segmenter(void 0, {
granularity: 'word'
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return Array.from(segmenter.segment(value)).map((segment: any) => segment.segment);
}
return breakWords(value, styles);
};
const breakText = (value: string, styles: CSSParsedDeclaration): string[] => {
return styles.letterSpacing !== 0 ? toCodePoints(value).map((i) => fromCodePoint(i)) : breakWords(value, styles);
return styles.letterSpacing !== 0 ? segmentGraphemes(value) : segmentWords(value, styles);
};
// https://drafts.csswg.org/css-text/#word-separator

View File

@ -1,7 +1,7 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken} from '../syntax/parser';
import {Context} from '../../core/context';
export enum BACKGROUND_CLIP {
export const enum BACKGROUND_CLIP {
BORDER_BOX = 0,
PADDING_BOX = 1,
CONTENT_BOX = 2

View File

@ -3,7 +3,7 @@ import {CSSValue, isIdentToken, parseFunctionArgs} from '../syntax/parser';
import {Context} from '../../core/context';
export type BackgroundRepeat = BACKGROUND_REPEAT[];
export enum BACKGROUND_REPEAT {
export const enum BACKGROUND_REPEAT {
REPEAT = 0,
NO_REPEAT = 1,
REPEAT_X = 2,

View File

@ -1,6 +1,6 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {Context} from '../../core/context';
export enum BORDER_STYLE {
export const enum BORDER_STYLE {
NONE = 0,
SOLID = 1,
DASHED = 2,

View File

@ -0,0 +1,23 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {Context} from '../../core/context';
export const enum DIRECTION {
LTR = 0,
RTL = 1
}
export const direction: IPropertyIdentValueDescriptor<DIRECTION> = {
name: 'direction',
initialValue: 'ltr',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (_context: Context, direction: string) => {
switch (direction) {
case 'rtl':
return DIRECTION.RTL;
case 'ltr':
default:
return DIRECTION.LTR;
}
}
};

View File

@ -0,0 +1,14 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {Context} from '../../core/context';
import {CSSValue, isDimensionToken} from '../syntax/parser';
import {time} from '../types/time';
export const duration: IPropertyListDescriptor<number[]> = {
name: 'duration',
initialValue: '0s',
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (context: Context, tokens: CSSValue[]) => {
return tokens.filter(isDimensionToken).map((token) => time.parse(context, token));
}
};

View File

@ -1,6 +1,6 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {Context} from '../../core/context';
export enum FLOAT {
export const enum FLOAT {
NONE = 0,
LEFT = 1,
RIGHT = 2,

View File

@ -1,6 +1,6 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {Context} from '../../core/context';
export enum FONT_STYLE {
export const enum FONT_STYLE {
NORMAL = 'normal',
ITALIC = 'italic',
OBLIQUE = 'oblique'

View File

@ -1,6 +1,6 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {Context} from '../../core/context';
export enum LIST_STYLE_POSITION {
export const enum LIST_STYLE_POSITION {
INSIDE = 0,
OUTSIDE = 1
}

View File

@ -1,6 +1,6 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {Context} from '../../core/context';
export enum LIST_STYLE_TYPE {
export const enum LIST_STYLE_TYPE {
NONE = -1,
DISC = 0,
CIRCLE = 1,

View File

@ -1,6 +1,6 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {Context} from '../../core/context';
export enum OVERFLOW_WRAP {
export const enum OVERFLOW_WRAP {
NORMAL = 'normal',
BREAK_WORD = 'break-word'
}

View File

@ -1,11 +1,12 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken} from '../syntax/parser';
import {Context} from '../../core/context';
export enum OVERFLOW {
export const enum OVERFLOW {
VISIBLE = 0,
HIDDEN = 1,
SCROLL = 2,
AUTO = 3
CLIP = 3,
AUTO = 4
}
export const overflow: IPropertyListDescriptor<OVERFLOW[]> = {
@ -20,6 +21,8 @@ export const overflow: IPropertyListDescriptor<OVERFLOW[]> = {
return OVERFLOW.HIDDEN;
case 'scroll':
return OVERFLOW.SCROLL;
case 'clip':
return OVERFLOW.CLIP;
case 'auto':
return OVERFLOW.AUTO;
case 'visible':

View File

@ -1,7 +1,7 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken} from '../syntax/parser';
import {Context} from '../../core/context';
export enum PAINT_ORDER_LAYER {
export const enum PAINT_ORDER_LAYER {
FILL,
STROKE,
MARKERS

View File

@ -1,6 +1,6 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {Context} from '../../core/context';
export enum POSITION {
export const enum POSITION {
STATIC = 0,
RELATIVE = 1,
ABSOLUTE = 2,

View File

@ -1,6 +1,6 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {Context} from '../../core/context';
export enum TEXT_ALIGN {
export const enum TEXT_ALIGN {
LEFT = 0,
CENTER = 1,
RIGHT = 2

View File

@ -1,6 +1,6 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {Context} from '../../core/context';
export enum TEXT_TRANSFORM {
export const enum TEXT_TRANSFORM {
NONE = 0,
LOWERCASE = 1,
UPPERCASE = 2,

View File

@ -1,6 +1,6 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {Context} from '../../core/context';
export enum VISIBILITY {
export const enum VISIBILITY {
VISIBLE = 0,
HIDDEN = 1,
COLLAPSE = 2

View File

@ -2,7 +2,7 @@
import {fromCodePoint, toCodePoints} from 'css-line-break';
export enum TokenType {
export const enum TokenType {
STRING_TOKEN,
BAD_STRING_TOKEN,
LEFT_PARENTHESIS_TOKEN,
@ -657,7 +657,7 @@ export class Tokenizer {
}
private consumeStringSlice(count: number): string {
const SLICE_STACK_SIZE = 60000;
const SLICE_STACK_SIZE = 50000;
let value = '';
while (count > 0) {
const amount = Math.min(SLICE_STACK_SIZE, count);

View File

@ -4,10 +4,7 @@ import {contains} from '../../../core/bitwise';
import {CSSParsedCounterDeclaration} from '../../index';
export class CounterState {
readonly counters: {[key: string]: number[]};
constructor() {
this.counters = {};
}
private readonly counters: {[key: string]: number[]} = {};
getCounterValue(name: string): number {
const counter = this.counters[name];
@ -18,7 +15,7 @@ export class CounterState {
return 1;
}
getCounterValues(name: string): number[] {
getCounterValues(name: string): readonly number[] {
const counter = this.counters[name];
return counter ? counter : [];
}
@ -37,6 +34,9 @@ export class CounterState {
const counter = this.counters[entry.counter];
if (counter && entry.increment !== 0) {
canReset = false;
if (!counter.length) {
counter.push(1);
}
counter[Math.max(0, counter.length - 1)] += entry.increment;
}
});

View File

@ -10,7 +10,7 @@ import {radialGradient} from './functions/radial-gradient';
import {prefixRadialGradient} from './functions/-prefix-radial-gradient';
import {Context} from '../../core/context';
export enum CSSImageType {
export const enum CSSImageType {
URL,
LINEAR_GRADIENT,
RADIAL_GRADIENT
@ -56,12 +56,12 @@ export interface CSSLinearGradientImage extends ICSSGradientImage {
type: CSSImageType.LINEAR_GRADIENT;
}
export enum CSSRadialShape {
export const enum CSSRadialShape {
CIRCLE,
ELLIPSE
}
export enum CSSRadialExtent {
export const enum CSSRadialExtent {
CLOSEST_SIDE,
FARTHEST_SIDE,
CLOSEST_CORNER,

View File

@ -1 +1 @@
export type CSSTypes = 'angle' | 'color' | 'image' | 'length' | 'length-percentage';
export type CSSTypes = 'angle' | 'color' | 'image' | 'length' | 'length-percentage' | 'time';

20
src/css/types/time.ts Normal file
View File

@ -0,0 +1,20 @@
import {CSSValue} from '../syntax/parser';
import {TokenType} from '../syntax/tokenizer';
import {ITypeDescriptor} from '../ITypeDescriptor';
import {Context} from '../../core/context';
export const time: ITypeDescriptor<number> = {
name: 'time',
parse: (_context: Context, value: CSSValue): number => {
if (value.type === TokenType.DIMENSION_TOKEN) {
switch (value.unit.toLowerCase()) {
case 's':
return 1000 * value.number;
case 'ms':
return value.number;
}
}
throw new Error(`Unsupported time type`);
}
};

View File

@ -2,16 +2,19 @@ import {Bounds} from '../css/layout/bounds';
import {
isBodyElement,
isCanvasElement,
isCustomElement,
isElementNode,
isHTMLElementNode,
isIFrameElement,
isImageElement,
isScriptElement,
isSelectElement,
isSlotElement,
isStyleElement,
isSVGElementNode,
isTextareaElement,
isTextNode
isTextNode,
isVideoElement
} from './node-parser';
import {isIdentToken, nonFunctionArgSeparator} from '../css/syntax/parser';
import {TokenType} from '../css/syntax/tokenizer';
@ -20,10 +23,12 @@ import {LIST_STYLE_TYPE, listStyleType} from '../css/property-descriptors/list-s
import {CSSParsedCounterDeclaration, CSSParsedPseudoDeclaration} from '../css/index';
import {getQuote} from '../css/property-descriptors/quotes';
import {Context} from '../core/context';
import {DebuggerType, isDebugging} from '../core/debugger';
export interface CloneOptions {
ignoreElements?: (element: Element) => boolean;
onclone?: (document: Document, element: HTMLElement) => void;
allowTaint?: boolean;
}
export interface WindowOptions {
@ -61,7 +66,7 @@ export class DocumentCloner {
throw new Error('Cloned element does not have an owner document');
}
this.documentElement = this.cloneNode(element.ownerDocument.documentElement) as HTMLElement;
this.documentElement = this.cloneNode(element.ownerDocument.documentElement, false) as HTMLElement;
}
toIFrame(ownerDocument: Document, windowSize: Bounds): Promise<HTMLIFrameElement> {
@ -135,10 +140,15 @@ export class DocumentCloner {
}
createElementClone<T extends HTMLElement | SVGElement>(node: T): HTMLElement | SVGElement {
if (isDebugging(node, DebuggerType.CLONE)) {
debugger;
}
if (isCanvasElement(node)) {
return this.createCanvasClone(node);
}
if (isVideoElement(node)) {
return this.createVideoClone(node);
}
if (isStyleElement(node)) {
return this.createStyleClone(node);
}
@ -155,6 +165,17 @@ export class DocumentCloner {
}
}
if (isCustomElement(clone)) {
return this.createCustomElementClone(clone);
}
return clone;
}
createCustomElementClone(node: HTMLElement): HTMLElement {
const clone = document.createElement('html2canvascustomelement');
copyCSSStyles(node.style, clone);
return clone;
}
@ -189,7 +210,7 @@ export class DocumentCloner {
img.src = canvas.toDataURL();
return img;
} catch (e) {
this.context.logger.info(`Unable to clone canvas contents, canvas is tainted`);
this.context.logger.info(`Unable to inline canvas contents, canvas is tainted`, canvas);
}
}
@ -201,19 +222,88 @@ export class DocumentCloner {
const ctx = canvas.getContext('2d');
const clonedCtx = clonedCanvas.getContext('2d');
if (clonedCtx) {
if (ctx) {
if (!this.options.allowTaint && ctx) {
clonedCtx.putImageData(ctx.getImageData(0, 0, canvas.width, canvas.height), 0, 0);
} else {
const gl = canvas.getContext('webgl2') ?? canvas.getContext('webgl');
if (gl) {
const attribs = gl.getContextAttributes();
if (attribs?.preserveDrawingBuffer === false) {
this.context.logger.warn(
'Unable to clone WebGL context as it has preserveDrawingBuffer=false',
canvas
);
}
}
clonedCtx.drawImage(canvas, 0, 0);
}
}
return clonedCanvas;
} catch (e) {}
} catch (e) {
this.context.logger.info(`Unable to clone canvas as it is tainted`, canvas);
}
return clonedCanvas;
}
cloneNode(node: Node): Node {
createVideoClone(video: HTMLVideoElement): HTMLCanvasElement {
const canvas = video.ownerDocument.createElement('canvas');
canvas.width = video.offsetWidth;
canvas.height = video.offsetHeight;
const ctx = canvas.getContext('2d');
try {
if (ctx) {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
if (!this.options.allowTaint) {
ctx.getImageData(0, 0, canvas.width, canvas.height);
}
}
return canvas;
} catch (e) {
this.context.logger.info(`Unable to clone video as it is tainted`, video);
}
const blankCanvas = video.ownerDocument.createElement('canvas');
blankCanvas.width = video.offsetWidth;
blankCanvas.height = video.offsetHeight;
return blankCanvas;
}
appendChildNode(clone: HTMLElement | SVGElement, child: Node, copyStyles: boolean): void {
if (
!isElementNode(child) ||
(!isScriptElement(child) &&
!child.hasAttribute(IGNORE_ATTRIBUTE) &&
(typeof this.options.ignoreElements !== 'function' || !this.options.ignoreElements(child)))
) {
if (!this.options.copyStyles || !isElementNode(child) || !isStyleElement(child)) {
clone.appendChild(this.cloneNode(child, copyStyles));
}
}
}
cloneChildNodes(node: Element, clone: HTMLElement | SVGElement, copyStyles: boolean): void {
for (
let child = node.shadowRoot ? node.shadowRoot.firstChild : node.firstChild;
child;
child = child.nextSibling
) {
if (isElementNode(child) && isSlotElement(child) && typeof child.assignedNodes === 'function') {
const assignedNodes = child.assignedNodes() as ChildNode[];
if (assignedNodes.length) {
assignedNodes.forEach((assignedNode) => this.appendChildNode(clone, assignedNode, copyStyles));
}
} else {
this.appendChildNode(clone, child, copyStyles);
}
}
}
cloneNode(node: Node, copyStyles: boolean): Node {
if (isTextNode(node)) {
return document.createTextNode(node.data);
}
@ -226,6 +316,7 @@ export class DocumentCloner {
if (window && isElementNode(node) && (isHTMLElementNode(node) || isSVGElementNode(node))) {
const clone = this.createElementClone(node);
clone.style.transitionProperty = 'none';
const style = window.getComputedStyle(node);
const styleBefore = window.getComputedStyle(node, ':before');
@ -241,17 +332,12 @@ export class DocumentCloner {
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) {
if (
!isElementNode(child) ||
(!isScriptElement(child) &&
!child.hasAttribute(IGNORE_ATTRIBUTE) &&
(typeof this.options.ignoreElements !== 'function' || !this.options.ignoreElements(child)))
) {
if (!this.options.copyStyles || !isElementNode(child) || !isStyleElement(child)) {
clone.appendChild(this.cloneNode(child));
}
}
if (isCustomElement(node)) {
copyStyles = true;
}
if (!isVideoElement(node)) {
this.cloneChildNodes(node, clone, copyStyles);
}
if (before) {
@ -265,7 +351,10 @@ export class DocumentCloner {
this.counters.pop(counters);
if (style && (this.options.copyStyles || isSVGElementNode(node)) && !isIFrameElement(node)) {
if (
(style && (this.options.copyStyles || isSVGElementNode(node)) && !isIFrameElement(node)) ||
copyStyles
) {
copyCSSStyles(style, clone);
}

View File

@ -3,29 +3,44 @@ import {TextContainer} from './text-container';
import {Bounds, parseBounds} from '../css/layout/bounds';
import {isHTMLElementNode} from './node-parser';
import {Context} from '../core/context';
import {DebuggerType, isDebugging} from '../core/debugger';
export const enum FLAGS {
CREATES_STACKING_CONTEXT = 1 << 1,
CREATES_REAL_STACKING_CONTEXT = 1 << 2,
IS_LIST_OWNER = 1 << 3
IS_LIST_OWNER = 1 << 3,
DEBUG_RENDER = 1 << 4
}
export class ElementContainer {
readonly styles: CSSParsedDeclaration;
readonly textNodes: TextContainer[];
readonly elements: ElementContainer[];
readonly textNodes: TextContainer[] = [];
readonly elements: ElementContainer[] = [];
bounds: Bounds;
flags: number;
flags = 0;
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';
if (isDebugging(element, DebuggerType.PARSE)) {
debugger;
}
this.styles = new CSSParsedDeclaration(context, window.getComputedStyle(element, null));
if (isHTMLElementNode(element)) {
if (this.styles.animationDuration.some((duration) => duration > 0)) {
element.style.animationDuration = '0s';
}
if (this.styles.transform !== null) {
// getBoundingClientRect takes transforms into account
element.style.transform = 'none';
}
}
this.bounds = parseBounds(this.context, element);
this.flags = 0;
if (isDebugging(element, DebuggerType.RENDER)) {
this.flags |= FLAGS.DEBUG_RENDER;
}
}
}

View File

@ -124,6 +124,7 @@ export const isHTMLElement = (node: Element): node is HTMLHtmlElement => node.ta
export const isSVGElement = (node: Element): node is SVGSVGElement => node.tagName === 'svg';
export const isBodyElement = (node: Element): node is HTMLBodyElement => node.tagName === 'BODY';
export const isCanvasElement = (node: Element): node is HTMLCanvasElement => node.tagName === 'CANVAS';
export const isVideoElement = (node: Element): node is HTMLVideoElement => node.tagName === 'VIDEO';
export const isImageElement = (node: Element): node is HTMLImageElement => node.tagName === 'IMG';
export const isIFrameElement = (node: Element): node is HTMLIFrameElement => node.tagName === 'IFRAME';
export const isStyleElement = (node: Element): node is HTMLStyleElement => node.tagName === 'STYLE';
@ -131,3 +132,5 @@ export const isScriptElement = (node: Element): node is HTMLScriptElement => nod
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';
// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
export const isCustomElement = (node: Element): node is HTMLElement => node.tagName.indexOf('-') > 0;

View File

@ -74,6 +74,7 @@ const renderElement = async (element: HTMLElement, opts: Partial<Options>): Prom
const foreignObjectRendering = opts.foreignObjectRendering ?? false;
const cloneOptions: CloneConfigurations = {
allowTaint: opts.allowTaint ?? false,
onclone: opts.onclone,
ignoreElements: opts.ignoreElements,
inlineImages: foreignObjectRendering,

View File

@ -1,8 +1,8 @@
import {ElementPaint, parseStackingContexts, StackingContext} from '../stacking-context';
import {asString, Color, isTransparent} from '../../css/types/color';
import {ElementContainer} from '../../dom/element-container';
import {ElementContainer, FLAGS} from '../../dom/element-container';
import {BORDER_STYLE} from '../../css/property-descriptors/border-style';
import {CSSParsedDeclaration} from '../../css/index';
import {CSSParsedDeclaration} from '../../css';
import {TextContainer} from '../../dom/text-container';
import {Path, transformPath} from '../path';
import {BACKGROUND_CLIP} from '../../css/property-descriptors/background-clip';
@ -18,13 +18,12 @@ import {
} from '../border';
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 {segmentGraphemes, TextBounds} from '../../css/layout/text';
import {ImageElementContainer} from '../../dom/replaced-elements/image-element-container';
import {contentBox} from '../box-sizing';
import {CanvasElementContainer} from '../../dom/replaced-elements/canvas-element-container';
import {SVGElementContainer} from '../../dom/replaced-elements/svg-element-container';
import {ReplacedElementContainer} from '../../dom/replaced-elements/index';
import {ReplacedElementContainer} from '../../dom/replaced-elements';
import {EffectTarget, IElementEffect, isClipEffect, isOpacityEffect, isTransformEffect} from '../effects';
import {contains} from '../../core/bitwise';
import {calculateGradientDirection, calculateRadius, processColorStops} from '../../css/types/functions/gradient';
@ -44,6 +43,7 @@ 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';
import {DIRECTION} from '../../css/property-descriptors/direction';
export type RenderConfigurations = RenderOptions & {
backgroundColor: Color | null;
@ -86,12 +86,12 @@ export class CanvasRenderer extends Renderer {
);
}
applyEffects(effects: IElementEffect[], target: EffectTarget): void {
applyEffects(effects: IElementEffect[]): void {
while (this._activeEffects.length) {
this.popEffect();
}
effects.filter((effect) => contains(effect.target, target)).forEach((effect) => this.applyEffect(effect));
effects.forEach((effect) => this.applyEffect(effect));
}
applyEffect(effect: IElementEffect): void {
@ -134,6 +134,10 @@ export class CanvasRenderer extends Renderer {
}
async renderNode(paint: ElementPaint): Promise<void> {
if (contains(paint.container.flags, FLAGS.DEBUG_RENDER)) {
debugger;
}
if (paint.container.styles.isVisible()) {
await this.renderNodeBackgroundAndBorders(paint);
await this.renderNodeContent(paint);
@ -144,7 +148,7 @@ export class CanvasRenderer extends Renderer {
if (letterSpacing === 0) {
this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + baseline);
} else {
const letters = toCodePoints(text.text).map((i) => fromCodePoint(i));
const letters = segmentGraphemes(text.text);
letters.reduce((left, letter) => {
this.ctx.fillText(letter, left, text.bounds.top + baseline);
@ -157,7 +161,7 @@ export class CanvasRenderer extends Renderer {
const fontVariant = styles.fontVariant
.filter((variant) => variant === 'normal' || variant === 'small-caps')
.join('');
const fontFamily = styles.fontFamily.join(', ');
const fontFamily = fixIOSSystemFonts(styles.fontFamily).join(', ');
const fontSize = isDimensionToken(styles.fontSize)
? `${styles.fontSize.number}${styles.fontSize.unit}`
: `${styles.fontSize.number}px`;
@ -174,6 +178,8 @@ export class CanvasRenderer extends Renderer {
this.ctx.font = font;
this.ctx.direction = styles.direction === DIRECTION.RTL ? 'rtl' : 'ltr';
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'alphabetic';
const {baseline, middle} = this.fontMetrics.getMetrics(fontFamily, fontSize);
const paintOrder = styles.paintOrder;
@ -286,7 +292,7 @@ export class CanvasRenderer extends Renderer {
}
async renderNodeContent(paint: ElementPaint): Promise<void> {
this.applyEffects(paint.effects, EffectTarget.CONTENT);
this.applyEffects(paint.getEffects(EffectTarget.CONTENT));
const container = paint.container;
const curves = paint.curves;
const styles = container.styles;
@ -465,6 +471,9 @@ export class CanvasRenderer extends Renderer {
}
async renderStackContent(stack: StackingContext): Promise<void> {
if (contains(stack.element.container.flags, FLAGS.DEBUG_RENDER)) {
debugger;
}
// https://www.w3.org/TR/css-position-3/#painting-order
// 1. the background and borders of the element forming the stacking context.
await this.renderNodeBackgroundAndBorders(stack.element);
@ -682,7 +691,7 @@ export class CanvasRenderer extends Renderer {
}
async renderNodeBackgroundAndBorders(paint: ElementPaint): Promise<void> {
this.applyEffects(paint.effects, EffectTarget.BACKGROUND_BORDERS);
this.applyEffects(paint.getEffects(EffectTarget.BACKGROUND_BORDERS));
const styles = paint.container.styles;
const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
@ -896,7 +905,7 @@ export class CanvasRenderer extends Renderer {
const stack = parseStackingContexts(element);
await this.renderStack(stack);
this.applyEffects([], EffectTarget.BACKGROUND_BORDERS);
this.applyEffects([]);
return this.canvas;
}
}
@ -937,3 +946,12 @@ const canvasTextAlign = (textAlign: TEXT_ALIGN): CanvasTextAlign => {
return 'left';
}
};
// see https://github.com/niklasvh/html2canvas/pull/2645
const iOSBrokenFonts = ['-apple-system', 'system-ui'];
const fixIOSSystemFonts = (fontFamilies: string[]): string[] => {
return /iPhone OS 15_(0|1)/.test(window.navigator.userAgent)
? fontFamilies.filter((fontFamily) => iOSBrokenFonts.indexOf(fontFamily) === -1)
: fontFamilies;
};

View File

@ -20,36 +20,21 @@ export interface IElementEffect {
export class TransformEffect implements IElementEffect {
readonly type: EffectType = EffectType.TRANSFORM;
readonly target: number = EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT;
readonly offsetX: number;
readonly offsetY: number;
readonly matrix: Matrix;
constructor(offsetX: number, offsetY: number, matrix: Matrix) {
this.offsetX = offsetX;
this.offsetY = offsetY;
this.matrix = matrix;
}
constructor(readonly offsetX: number, readonly offsetY: number, readonly matrix: Matrix) {}
}
export class ClipEffect implements IElementEffect {
readonly type: EffectType = EffectType.CLIP;
readonly target: number;
readonly path: Path[];
constructor(path: Path[], target: EffectTarget) {
this.target = target;
this.path = path;
}
constructor(readonly path: Path[], readonly target: EffectTarget) {}
}
export class OpacityEffect implements IElementEffect {
readonly type: EffectType = EffectType.OPACITY;
readonly target: number = EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT;
readonly opacity: number;
constructor(opacity: number) {
this.opacity = opacity;
}
constructor(readonly opacity: number) {}
}
export const isTransformEffect = (effect: IElementEffect): effect is TransformEffect =>

View File

@ -27,6 +27,7 @@ export class FontMetrics {
container.style.fontSize = fontSize;
container.style.margin = '0';
container.style.padding = '0';
container.style.whiteSpace = 'nowrap';
body.appendChild(container);

View File

@ -1,6 +1,6 @@
import {BezierCurve} from './bezier-curve';
import {Vector} from './vector';
export enum PathType {
export const enum PathType {
VECTOR = 0,
BEZIER_CURVE = 1
}

View File

@ -1,13 +1,14 @@
import {ElementContainer, FLAGS} from '../dom/element-container';
import {contains} from '../core/bitwise';
import {BoundCurves, calculateBorderBoxPath, calculatePaddingBoxPath} from './bound-curves';
import {ClipEffect, EffectTarget, IElementEffect, OpacityEffect, TransformEffect} from './effects';
import {ClipEffect, EffectTarget, IElementEffect, isClipEffect, OpacityEffect, TransformEffect} from './effects';
import {OVERFLOW} from '../css/property-descriptors/overflow';
import {equalPath} from './path';
import {DISPLAY} from '../css/property-descriptors/display';
import {OLElementContainer} from '../dom/elements/ol-element-container';
import {LIElementContainer} from '../dom/elements/li-element-container';
import {createCounterText} from '../css/types/functions/counter';
import {POSITION} from '../css/property-descriptors/position';
export class StackingContext {
element: ElementPaint;
@ -32,27 +33,24 @@ export class StackingContext {
}
export class ElementPaint {
container: ElementContainer;
effects: IElementEffect[];
curves: BoundCurves;
readonly effects: IElementEffect[] = [];
readonly curves: BoundCurves;
listValue?: string;
constructor(element: ElementContainer, parentStack: IElementEffect[]) {
this.container = element;
this.effects = parentStack.slice(0);
this.curves = new BoundCurves(element);
if (element.styles.opacity < 1) {
this.effects.push(new OpacityEffect(element.styles.opacity));
constructor(readonly container: ElementContainer, readonly parent: ElementPaint | null) {
this.curves = new BoundCurves(this.container);
if (this.container.styles.opacity < 1) {
this.effects.push(new OpacityEffect(this.container.styles.opacity));
}
if (element.styles.transform !== null) {
const offsetX = element.bounds.left + element.styles.transformOrigin[0].number;
const offsetY = element.bounds.top + element.styles.transformOrigin[1].number;
const matrix = element.styles.transform;
if (this.container.styles.transform !== null) {
const offsetX = this.container.bounds.left + this.container.styles.transformOrigin[0].number;
const offsetY = this.container.bounds.top + this.container.styles.transformOrigin[1].number;
const matrix = this.container.styles.transform;
this.effects.push(new TransformEffect(offsetX, offsetY, matrix));
}
if (element.styles.overflowX !== OVERFLOW.VISIBLE) {
if (this.container.styles.overflowX !== OVERFLOW.VISIBLE) {
const borderBox = calculateBorderBoxPath(this.curves);
const paddingBox = calculatePaddingBoxPath(this.curves);
@ -65,16 +63,32 @@ export class ElementPaint {
}
}
getParentEffects(): IElementEffect[] {
getEffects(target: EffectTarget): IElementEffect[] {
let inFlow = [POSITION.ABSOLUTE, POSITION.FIXED].indexOf(this.container.styles.position) === -1;
let parent = this.parent;
const effects = this.effects.slice(0);
if (this.container.styles.overflowX !== OVERFLOW.VISIBLE) {
const borderBox = calculateBorderBoxPath(this.curves);
const paddingBox = calculatePaddingBoxPath(this.curves);
if (!equalPath(borderBox, paddingBox)) {
effects.push(new ClipEffect(paddingBox, EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT));
while (parent) {
const croplessEffects = parent.effects.filter((effect) => !isClipEffect(effect));
if (inFlow || parent.container.styles.position !== POSITION.STATIC || !parent.parent) {
effects.unshift(...croplessEffects);
inFlow = [POSITION.ABSOLUTE, POSITION.FIXED].indexOf(parent.container.styles.position) === -1;
if (parent.container.styles.overflowX !== OVERFLOW.VISIBLE) {
const borderBox = calculateBorderBoxPath(parent.curves);
const paddingBox = calculatePaddingBoxPath(parent.curves);
if (!equalPath(borderBox, paddingBox)) {
effects.unshift(
new ClipEffect(paddingBox, EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT)
);
}
}
} else {
effects.unshift(...croplessEffects);
}
parent = parent.parent;
}
return effects;
return effects.filter((effect) => contains(effect.target, target));
}
}
@ -87,7 +101,7 @@ const parseStackTree = (
parent.container.elements.forEach((child) => {
const treatAsRealStackingContext = contains(child.flags, FLAGS.CREATES_REAL_STACKING_CONTEXT);
const createsStackingContext = contains(child.flags, FLAGS.CREATES_STACKING_CONTEXT);
const paintContainer = new ElementPaint(child, parent.getParentEffects());
const paintContainer = new ElementPaint(child, parent);
if (contains(child.styles.display, DISPLAY.LIST_ITEM)) {
listItems.push(paintContainer);
}
@ -182,7 +196,7 @@ const processListItems = (owner: ElementContainer, elements: ElementPaint[]) =>
};
export const parseStackingContexts = (container: ElementContainer): StackingContext => {
const paintContainer = new ElementPaint(container, []);
const paintContainer = new ElementPaint(container, null);
const root = new StackingContext(paintContainer);
const listItems: ElementPaint[] = [];
parseStackTree(paintContainer, root, root, listItems);

BIN
tests/assets/cc0-video.mp4 Normal file

Binary file not shown.

View File

@ -5,46 +5,93 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../test.js"></script>
<style>
span {
color:blue;
}
p {
background-color: green;
}
div {
background: red;
border: 5px solid blue;
animation: spin 3s linear 1s infinite;
}
body {
font-family: Arial;
}
@-webkit-keyframes spin {
@keyframes rotate0 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
/* Firefox 16+, IE 10+, Opera */ }
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
/* Firefox 16+, IE 10+, Opera */ } }
}
}
@keyframes spin {
@keyframes rotate45 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
/* Firefox 16+, IE 10+, Opera */ }
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
/* Firefox 16+, IE 10+, Opera */ } }
transform: rotate(45deg);
}
}
p {
font: 22px/1 Arial, sans-serif;
position: absolute;
top: 25%;
left: 25%;
width: 50%;
height: 50%;
color: #fff;
background-color: #666;
line-height: 90px;
text-align: center;
}
.transformed.working p {
transform: rotate(45deg);
}
.animated.working p {
animation-name: rotate0;
animation-duration: 1ms, 1ms;
animation-play-state: paused;
}
.animated.broken p {
animation-name: rotate45;
animation-duration: 1ms;
animation-play-state: paused;
}
.transitioned p {
transition: 1ms, 1ms;
transform: rotate(45deg)
}
.transition-delay {
transition: 1ms;
transition-delay: 50ms;
transform: rotate(45deg)
}
div {
float: left;
clear: left;
margin-right: 10px;
background-color: #ccc;
width: 180px;
height: 180px;
position: relative;
}
</style>
</head>
<body>
<div style="clip: rect(0px, 400px, 50px, 200px); ">Some inline text <span> followed by text in span </span> followed by more inline text.
<p>Then a block level element.</p>
Then more inline text.</div>
<div class="transformed working">
<p>Hello</p>
</div>
<div class="animated working">
<p>Hello</p>
</div>
<div class="animated broken">
<p>Hello</p>
</div>
<div class="transitioned broken">
<p>Hello</p>
</div>
<div class="transition-delay">
<p>Hello</p>
</div>
</body>
</html>

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Video tests</title>
<script type="text/javascript" src="../../test.js"></script>
</head>
<body>
<h2>Same origin</h2>
<video controls width="250">
<source src="../../assets/cc0-video.mp4" type="video/mp4">
Sorry, your browser doesn't support embedded videos.
</video>
<h2>Cross-origin (doesn't taint)</h2>
<video controls width="250">
<source src="http://localhost:8081/cors/tests/assets/cc0-video.mp4" type="video/mp4">
Sorry, your browser doesn't support embedded videos.
</video>
</body>
</html>

View File

@ -51,6 +51,9 @@
.scroll {
overflow: scroll;
}
.clip {
overflow: clip;
}
.auto {
overflow: auto;
}
@ -70,6 +73,10 @@
scroll
<p class="scroll">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec luctus pretium facilisis. Praesent rutrum eget nisl in tristique. Sed tincidunt nisl et tellus vulputate, nec rhoncus orci pretium.</p>
</div>
<div class="cell">
clip
<p class="clip">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec luctus pretium facilisis. Praesent rutrum eget nisl in tristique. Sed tincidunt nisl et tellus vulputate, nec rhoncus orci pretium.</p>
</div>
<div class="cell">
auto
<p class="auto">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec luctus pretium facilisis. Praesent rutrum eget nisl in tristique. Sed tincidunt nisl et tellus vulputate, nec rhoncus orci pretium.</p>
@ -129,5 +136,17 @@
</script>
</div>
<div class="hidden">Hidden<div style="opacity: 0.5">With opacity</div></div>
<div class="hidden" style="height: 0px; width: 50px;">
<div style="position: absolute; width: 50px; height: 50px; left:400px;">absolute on static parent</div>
</div>
<div class="hidden" style="height: 0px; width: 50px;">
<div style="position: relative; width: 10px; height: 10px; left:400px;">relative on static parent</div>
</div>
<div class="hidden" style="height: 0px; width: 50px;">
<div style="position: fixed; width: 50px; height: 50px; left:400px; top:0;">fixed on static parent</div>
</div>
<div class="hidden" style="height: 0px; width: 50px;position: relative;">
<div style="position: absolute; width: 10px; height: 10px; left:400px;">absolute on relative parent</div>
</div>
</body>
</html>

View File

@ -86,6 +86,20 @@
of the section counter, separated
by a period */
}
.issue-2639 {
display: flex;
}
.issue-2639::before {
content: counter(ol0) '. ';
counter-increment: ol0;
}
.issue-2639:first-child {
counter-reset: ol0;
}
</style>
</head>
<body>
@ -163,5 +177,10 @@
<li>item</li> <!-- 2 -->
</ol>
<ol>
<li class="issue-2639">one</li>
<li class="issue-2639">two</li>
</ol>
</body>
</html>

View File

@ -11,6 +11,13 @@
float: left;
}
.apple-system {
font-family: -apple-system, Arial;
}
.system-ui {
font-family: system-ui, Arial;
}
</style>
</head>
<body>
@ -32,5 +39,7 @@
</p><p>&nbsp;&nbsp;&nbsp;&nbsp;13 法捷耶夫(一九○一——一九五六),苏联名作家。他所作的小说《毁灭》于一九二七年出版,内容是描写苏联国内战争时期由苏联远东滨海边区工人、农民和革命知识分子所组成的一支游击队同国内反革命白卫军以及日本武装干涉军进行斗争的故事。这部小说曾由鲁迅译为汉文。
</p><p>&nbsp;&nbsp;&nbsp;&nbsp;14 见鲁迅《集外集·自嘲》《鲁迅全集》第7卷人民文学出版社1981年版第147页</p>
</div>
<div class="apple-system">中文</div>
<div class="system-ui">中文</div>
</body>
</html>

View File

@ -149,5 +149,6 @@
<span>[AB / CD]</span>
</div>
<div>Emojis 🤷🏾‍♂️👨‍👩‍👧‍👦 :)</div>
<div style="letter-spacing: 2px">Emojis with letter-spacing 🤷🏾‍♂️👨‍👩‍👧‍👦 :) ❤️❤️❤️👨‍❤️‍💋‍👨👨‍❤️‍👨</div>
</body>
</html>

View File

@ -40,6 +40,10 @@ class AutonomousCustomElement extends HTMLElement {
wrapper.appendChild(img);
wrapper.appendChild(info);
}
connectedCallback() {
this.shadowRoot.adoptedStyleSheets = [sheet];
}
}
customElements.define('autonomous-custom-element', AutonomousCustomElement);

View File

@ -3,10 +3,11 @@
<head>
<title>Web components tests</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script>
const sheet = new CSSStyleSheet();
sheet.replaceSync('* { color: red !important; }')
</script>
<script type="text/javascript" src="../../test.js"></script>
<style>
</style>
</head>
<body>
<div>

View File

@ -12,7 +12,7 @@ const mkdirp = require('mkdirp');
export const app = express();
app.use('/', serveIndex(path.resolve(__dirname, '../'), {icons: true}));
app.use('/', express.static(path.resolve(__dirname, '../')));
app.use([/^\/src($|\/)/, '/'], express.static(path.resolve(__dirname, '../')));
export const corsApp = express();
corsApp.use('/proxy', proxy());

View File

@ -8,14 +8,15 @@ import {ScreenshotRequest} from './types';
// @ts-ignore
window.Promise = Promise;
const testRunnerUrl = location.href;
const hasHistoryApi = typeof window.history !== 'undefined' && typeof window.history.replaceState !== 'undefined';
const uploadResults = (canvas: HTMLCanvasElement, url: string) => {
// @ts-ignore
return new Promise((resolve: () => void, reject: (error: any) => void) => {
const xhr = new XMLHttpRequest();
return new Promise((resolve: () => void, reject: (error: string) => void) => {
// @ts-ignore
const xhr = 'withCredentials' in new XMLHttpRequest() ? new XMLHttpRequest() : new XDomainRequest();
xhr.onload = () => {
if (xhr.status === 200) {
if (typeof xhr.status !== 'number' || xhr.status === 200) {
resolve();
} else {
reject(`Failed to send screenshot with status ${xhr.status}`);
@ -61,17 +62,21 @@ testList
testContainer.onload = () => done();
testContainer.src = url + '?selenium&run=false&reftest&' + Math.random();
// Chrome does not resolve relative background urls correctly inside of a nested iframe
try {
history.replaceState(null, '', url);
} catch (e) {}
if (hasHistoryApi) {
// Chrome does not resolve relative background urls correctly inside of a nested iframe
try {
history.replaceState(null, '', url);
} catch (e) {}
}
document.body.appendChild(testContainer);
});
after(() => {
try {
history.replaceState(null, '', testRunnerUrl);
} catch (e) {}
if (hasHistoryApi) {
try {
history.replaceState(null, '', testRunnerUrl);
} catch (e) {}
}
document.body.removeChild(testContainer);
});

2
www/.gitignore vendored
View File

@ -9,3 +9,5 @@ yarn-error.log
src/results.json
static/tests/preview.js
src/preview.js
.docusaurus
build/

View File

@ -32,8 +32,10 @@
"license": "MIT",
"main": "n/a",
"scripts": {
"copybuild": "mkdirp public/dist && cpy ../dist/*.js public/dist",
"build": "npm run copybuild && gatsby build",
"copy:build": "mkdirp public/dist && cpy ../dist/*.js public/dist",
"copy:src": "mkdirp public/src && cpy ../src/**/*.ts public/src --parents",
"copy": "npm run copy:build && npm run copy:src",
"build": "npm run copy && gatsby build",
"start": "gatsby develop",
"test": "echo \"Error: no test specified\" && exit 1"
}