Compare commits

..

26 Commits

Author SHA1 Message Date
eca5033705 feat: drop support for IE9 2021-08-09 18:31:10 +08:00
e429e0443a ci: add ios15 target (#2564) 2021-08-09 17:15:00 +08:00
f43f942fcd fix: test for ios range line break error (#2635) 2021-08-09 17:14:40 +08:00
7a06d0c2c2 docs: update test previewer (#2637) 2021-08-07 18:05:08 +08:00
e36408ad03 test: large base64 encoded background (#2636) 2021-08-07 15:47:15 +08:00
a0dd38a8be fix: radial gradient ry check (#2631) 2021-08-05 14:07:10 +08:00
CI
b988d9d657 chore(release): 1.2.1 2021-08-05 02:39:44 +00:00
c5c6fa00d7 fix: type import that is only available ts 3.8 or higher (#2629) 2021-08-05 10:17:30 +08:00
6651fc6789 fix: none image (#2627) 2021-08-05 09:30:22 +08:00
CI
df223c3ff2 chore(release): 1.2.0 2021-08-04 15:00:38 +00:00
95a46b00c5 fix: overflow-wrap break-word (#2626) 2021-08-04 22:53:48 +08:00
878e37a242 fix: element cropping & scrolling (#2625) 2021-08-04 20:58:17 +08:00
1338c7b203 test: element with scrolled window (#2624) 2021-08-04 11:28:05 +08:00
CI
f284752295 chore(release): 1.1.5 2021-08-02 10:38:51 +00:00
96e23d1851 fix: natural sizes for images with srcset (#2622) 2021-08-02 18:27:03 +08:00
7d788c6f3d fix: emoji line breaking (fix #1813) (#2621)
* fix: emoji line breaking (fix #1813)

* test: fix text.html reftest
2021-08-02 17:49:46 +08:00
5dea36bd69 docs: update README to github discussion Q/A 2021-07-17 17:24:15 +08:00
CI
11d16d2b77 chore(release): 1.1.4 2021-07-15 16:44:42 +00:00
e9f7f48d57 fix: word-break seperators (#2593) 2021-07-15 21:05:26 +08:00
4c360fc1f0 test: refactor language tests (#2594) 2021-07-15 20:58:04 +08:00
578bb771bf test: update box-shadow with radius 2021-07-15 18:24:39 +08:00
522e5aac5f feat: add support for webkit-text-stroke and paint-order (#2591) 2021-07-15 18:19:26 +08:00
dd6d8856ec fix: svg d path getting truncated on copy (#2589) 2021-07-15 17:15:47 +08:00
45efe54da8 fix: this.canvas.ownerDocument is undefined (#2590) 2021-07-15 17:08:43 +08:00
cd99f11b1b fix: text position for form elements and list markers (#2588) 2021-07-15 16:54:01 +08:00
fa60716d07 fix: don't copy 'all' css property (#2586) 2021-07-14 22:49:52 +08:00
113 changed files with 1482 additions and 803 deletions

View File

@ -113,9 +113,10 @@ jobs:
name: iOS Simulator Safari 14
targetBrowser: Safari_IOS_14
xcode: /Applications/Xcode_12_beta.app
- os: windows-latest
name: Windows Internet Explorer 9 (Emulated)
targetBrowser: IE_9
- 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 10 (Emulated)
targetBrowser: IE_10

View File

@ -2,6 +2,66 @@
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)

View File

@ -1,9 +1,9 @@
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)
[![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,7 +28,8 @@ The library should work fine on the following browsers (with `Promise` polyfill)
* Firefox 3.5+
* Google Chrome
* Opera 12+
* IE9+
* IE10+
* Edge
* 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+
- IE9+
- IE10+
- Edge
- Safari 6+

View File

@ -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)

View File

@ -42,11 +42,11 @@ module.exports = function(config) {
platform: 'iOS',
sdk: '14.0'
},
SauceLabs_IE9: {
base: 'SauceLabs',
browserName: 'internet explorer',
version: '9.0',
platform: 'Windows 7'
Safari_IOS_15: {
base: 'MobileSafari',
name: 'iPhone 8',
platform: 'iOS',
sdk: '15.0'
},
SauceLabs_IE10: {
base: 'SauceLabs',
@ -87,11 +87,6 @@ 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',

18
package-lock.json generated
View File

@ -1,14 +1,14 @@
{
"name": "html2canvas",
"version": "1.1.3",
"version": "1.2.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"version": "1.1.0",
"version": "1.1.5",
"license": "MIT",
"dependencies": {
"css-line-break": "1.1.1"
"css-line-break": "2.0.1"
},
"devDependencies": {
"@babel/cli": "^7.4.3",
@ -8630,9 +8630,9 @@
}
},
"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"
}
@ -32811,9 +32811,9 @@
}
},
"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"
}

View File

@ -6,7 +6,7 @@
"module": "dist/html2canvas.esm.js",
"typings": "dist/types/index.d.ts",
"browser": "dist/html2canvas.js",
"version": "1.1.3",
"version": "1.2.1",
"author": {
"name": "Niklas von Hertzen",
"email": "niklasvh@gmail.com",
@ -118,6 +118,6 @@
"homepage": "https://html2canvas.hertzen.com",
"license": "MIT",
"dependencies": {
"css-line-break": "1.1.1"
"css-line-break": "2.0.1"
}
}

View File

@ -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
})
);

View File

@ -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 {}

View 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;
})
};
}
}

View File

@ -19,4 +19,4 @@ export class Logger {
error(): void {}
}
const logger = new Logger();
export const logger = new Logger();

View File

@ -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 {

View File

@ -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;
@ -31,7 +12,6 @@ export class CacheStorage {
}
link.href = url;
link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/
return link.protocol + link.hostname + link.port;
}
@ -43,22 +23,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 +34,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();
@ -128,7 +86,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
View 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);
}
}

View File

@ -1,3 +1,5 @@
import {fromCodePoint, toCodePoints} from 'css-line-break';
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' ? '&#128104;'.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);

View File

@ -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) {

View File

@ -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 {

View File

@ -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;
}

View File

@ -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;

View File

@ -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 => {

View File

@ -3,6 +3,7 @@ import {CSSParsedDeclaration} from '../index';
import {fromCodePoint, LineBreaker, toCodePoints} from 'css-line-break';
import {Bounds, parseBounds} from './bounds';
import {FEATURES} from '../../core/features';
import {Context} from '../../core/context';
export class TextBounds {
readonly text: string;
@ -14,17 +15,31 @@ export class TextBounds {
}
}
export const parseTextBounds = (value: string, styles: CSSParsedDeclaration, node: Text): TextBounds[] => {
export const parseTextBounds = (
context: Context,
value: string,
styles: CSSParsedDeclaration,
node: Text
): TextBounds[] => {
const textList = breakText(value, styles);
const textBounds: TextBounds[] = [];
let offset = 0;
textList.forEach((text) => {
if (styles.textDecorationLine.length || text.trim().length > 0) {
if (FEATURES.SUPPORT_RANGE_BOUNDS) {
textBounds.push(new TextBounds(text, getRangeBounds(node, offset, text.length)));
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 +51,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 +59,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 +67,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,13 +78,20 @@ 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 getRangeBounds = (context: Context, node: Text, offset: number, length: number): Bounds => {
return Bounds.fromClientRect(context, createRange(node, offset, length).getBoundingClientRect());
};
const breakText = (value: string, styles: CSSParsedDeclaration): string[] => {
return styles.letterSpacing !== 0 ? toCodePoints(value).map((i) => fromCodePoint(i)) : breakWords(value, styles);
};
// https://drafts.csswg.org/css-text/#word-separator
const wordSeparators = [0x0020, 0x00a0, 0x1361, 0x10100, 0x10101, 0x1039, 0x1091];
const breakWords = (str: string, styles: CSSParsedDeclaration): string[] => {
const breaker = LineBreaker(str, {
lineBreak: styles.lineBreak,
@ -81,7 +103,24 @@ const breakWords = (str: string, styles: CSSParsedDeclaration): string[] => {
while (!(bk = breaker.next()).done) {
if (bk.value) {
words.push(bk.value.slice());
const value = bk.value.slice();
const codePoints = toCodePoints(value);
let word = '';
codePoints.forEach((codePoint) => {
if (wordSeparators.indexOf(codePoint) === -1) {
word += fromCodePoint(codePoint);
} else {
if (word.length) {
words.push(word);
}
words.push(fromCodePoint(codePoint));
word = '';
}
});
if (word.length) {
words.push(word);
}
}
}

View File

@ -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')`
),
[

View File

@ -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', () => {

View 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
]));
});
});

View File

@ -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,

View File

@ -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', () => {

View File

@ -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) {

View File

@ -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));
}
};

View File

@ -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) {

View File

@ -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);

View File

@ -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

View File

@ -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));
}
};

View File

@ -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');

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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 [];
}

View File

@ -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;
}

View File

@ -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 [];
}

View File

@ -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);

View File

@ -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;

View File

@ -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) => {

View File

@ -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;

View File

@ -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);
}
};

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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);
}
};

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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':

View 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;
}
};

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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) => {

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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;
}

View File

@ -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;

View 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'
};

View 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;
}
};

View File

@ -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;

View File

@ -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};
}

View File

@ -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>', () => {

View File

@ -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,

View File

@ -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:

View File

@ -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,

View File

@ -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);
});

View File

@ -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);
}
});

View File

@ -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({

View File

@ -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', () => {

View File

@ -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};
};

View File

@ -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);
});

View File

@ -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);
}
});

View File

@ -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,

View File

@ -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> {

View File

@ -13,20 +13,26 @@ 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, element: HTMLElement) => void;
}
export interface WindowOptions {
scrollX: number;
scrollY: number;
windowWidth: number;
windowHeight: number;
}
export type CloneConfigurations = CloneOptions & {
inlineImages: boolean;
copyStyles: boolean;
@ -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,9 +89,13 @@ 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
);
}
}
@ -132,8 +144,15 @@ export class DocumentCloner {
}
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;
@ -155,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;
}
@ -170,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`);
}
}
@ -219,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) {
@ -283,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);
@ -311,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(
@ -324,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
@ -443,12 +462,17 @@ const iframeLoader = (iframe: HTMLIFrameElement): Promise<HTMLIFrameElement> =>
});
};
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));
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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 || '' : '';
}

View File

@ -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;
}
}

View File

@ -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,20 +10,21 @@ 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)) {
if (isSlotElement(childNode) && childNode.assignedNodes) {
childNode.assignedNodes().forEach((childNode) => parseNodeTree(childNode, parent, root));
childNode.assignedNodes().forEach((childNode) => parseNodeTree(context, childNode, parent, root));
} else {
const container = createContainer(childNode);
const container = createContainer(context, childNode);
if (container.styles.isVisible()) {
if (createsRealStackingContext(childNode, container, root)) {
container.flags |= FLAGS.CREATES_REAL_STACKING_CONTEXT;
@ -38,13 +39,13 @@ const parseNodeTree = (node: Node, parent: ElementContainer, root: ElementContai
parent.elements.push(container);
childNode.slot;
if (childNode.shadowRoot) {
parseNodeTree(childNode.shadowRoot, container, root);
parseNodeTree(context, childNode.shadowRoot, container, root);
} else if (
!isTextareaElement(childNode) &&
!isSVGElement(childNode) &&
!isSelectElement(childNode)
) {
parseNodeTree(childNode, container, root);
parseNodeTree(context, childNode, container, root);
}
}
}
@ -52,50 +53,50 @@ const parseNodeTree = (node: Node, parent: ElementContainer, root: ElementContai
}
};
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;
};

View File

@ -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;

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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);

View File

@ -1,16 +1,16 @@
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(img);
const bounds = parseBounds(context, img);
img.setAttribute('width', `${bounds.width}px`);
img.setAttribute('height', `${bounds.height}px`);
@ -18,6 +18,6 @@ export class SVGElementContainer extends ElementContainer {
this.intrinsicWidth = img.width.baseVal.value;
this.intrinsicHeight = img.height.baseVal.value;
CacheStorage.getInstance().addImage(this.svg);
this.context.cache.addImage(this.svg);
}
}

View File

@ -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);
}
}

View File

@ -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,8 +26,6 @@ if (typeof window !== 'undefined') {
CacheStorage.setContext(window);
}
let instanceCount = 1;
const renderElement = async (element: HTMLElement, opts: Partial<Options>): Promise<HTMLCanvasElement> => {
if (!element || typeof element !== 'object') {
return Promise.reject('Invalid element provided as first argument');
@ -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;
};

View File

@ -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,7 +16,6 @@ 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';
@ -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);

View File

@ -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
View 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) {}
}

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@
margin: 10px;
display: inline-block;
min-height: 50px;
// border-radius: 10px;
border-radius: 10px;
}
body {

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