Compare commits

..

16 Commits

Author SHA1 Message Date
CI
0b1f0a7e90 chore(release): 1.3.2 2021-08-15 12:54:26 +00:00
38c682955a fix: overflows with absolutely positioned content (#2663) 2021-08-15 19:41:57 +08:00
01ed87907a docs: add warning for webgl cloning with preserveDrawingBuffer=false (#2661) 2021-08-14 17:18:30 +08:00
58ff0003f7 docs: include src files on www (#2660) 2021-08-14 16:48:22 +08:00
f143166551 fix: disable transition properties (#2659) 2021-08-14 16:22:36 +08:00
cd0d7258c3 feat: add support for data-html2canvas-debug property for debugging (#2658) 2021-08-14 15:01:41 +08:00
CI
b482725994 chore(release): 1.3.1 2021-08-14 06:06:09 +00:00
1b55ed5668 fix: multi arg transition/animation duration (#2657) 2021-08-14 14:05:15 +08:00
CI
68377b3244 chore(release): 1.3.0 2021-08-13 11:52:05 +00:00
6947982848 feat: add rtl render support (#2653) 2021-08-13 19:50:59 +08:00
437b367d3b feat: correctly break graphemes (#2652) 2021-08-13 18:57:49 +08:00
969638fb94 fix: finish animation/transitions for elements (#2632) 2021-08-13 18:15:55 +08:00
f919204efa test: add letter-spacing test for zwj emoji (#2650) 2021-08-13 17:45:42 +08:00
c378e22069 fix: correctly handle allowTaint canvas cloning (#2649) 2021-08-13 17:32:55 +08:00
CI
2b4de68e92 chore(release): 1.2.2 2021-08-10 10:39:01 +00:00
1941b9e0ac fix: parsing counter content in pseudo element (#2640) 2021-08-09 18:43:42 +08:00
32 changed files with 1078 additions and 2062 deletions

View File

@ -117,6 +117,9 @@ jobs:
name: iOS Simulator Safari 15
targetBrowser: Safari_IOS_15
xcode: /Applications/Xcode_13.0.app
- os: windows-latest
name: Windows Internet Explorer 9 (Emulated)
targetBrowser: IE_9
- os: windows-latest
name: Windows Internet Explorer 10 (Emulated)
targetBrowser: IE_10

View File

@ -2,6 +2,76 @@
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.3.2](https://github.com/niklasvh/html2canvas/compare/v1.3.1...v1.3.2) (2021-08-15)
### docs
* add warning for webgl cloning with preserveDrawingBuffer=false (#2661) ([01ed879](https://github.com/niklasvh/html2canvas/commit/01ed87907ad9c7688880e2c5cb8ebc22ef73a4d8)), closes [#2661](https://github.com/niklasvh/html2canvas/issues/2661)
* include src files on www (#2660) ([58ff000](https://github.com/niklasvh/html2canvas/commit/58ff0003f77d825ac027eeec95fa80c0123eaf8f)), closes [#2660](https://github.com/niklasvh/html2canvas/issues/2660)
### feat
* add support for data-html2canvas-debug property for debugging (#2658) ([cd0d725](https://github.com/niklasvh/html2canvas/commit/cd0d7258c3a93f2989d5d9ec0244ba2763ea2d23)), closes [#2658](https://github.com/niklasvh/html2canvas/issues/2658)
### fix
* disable transition properties (#2659) ([f143166](https://github.com/niklasvh/html2canvas/commit/f1431665513e0a4636fb167a241f4a0571ba728a)), closes [#2659](https://github.com/niklasvh/html2canvas/issues/2659)
* overflows with absolutely positioned content (#2663) ([38c6829](https://github.com/niklasvh/html2canvas/commit/38c682955a9299ca7785af71d8f251df799405b0)), closes [#2663](https://github.com/niklasvh/html2canvas/issues/2663)
## [1.3.1](https://github.com/niklasvh/html2canvas/compare/v1.3.0...v1.3.1) (2021-08-14)
### fix
* multi arg transition/animation duration (#2657) ([1b55ed5](https://github.com/niklasvh/html2canvas/commit/1b55ed5668dcbbe1c6d8d7e94736d8f2da2d31c5)), closes [#2657](https://github.com/niklasvh/html2canvas/issues/2657)
# [1.3.0](https://github.com/niklasvh/html2canvas/compare/v1.2.2...v1.3.0) (2021-08-13)
### feat
* add rtl render support (#2653) ([6947982](https://github.com/niklasvh/html2canvas/commit/694798284838b16882e648914da0905818aa366c)), closes [#2653](https://github.com/niklasvh/html2canvas/issues/2653)
* correctly break graphemes (#2652) ([437b367](https://github.com/niklasvh/html2canvas/commit/437b367d3ba9dfd7f9a4c8042ac8d00208c09770)), closes [#2652](https://github.com/niklasvh/html2canvas/issues/2652)
### fix
* correctly handle allowTaint canvas cloning (#2649) ([c378e22](https://github.com/niklasvh/html2canvas/commit/c378e220694c14cb7b3b4b8650a7757f8fc23c7a)), closes [#2649](https://github.com/niklasvh/html2canvas/issues/2649)
* finish animation/transitions for elements (#2632) ([969638f](https://github.com/niklasvh/html2canvas/commit/969638fb94a0a14c64a667fa6e5689f79d9f1044)), closes [#2632](https://github.com/niklasvh/html2canvas/issues/2632)
### test
* add letter-spacing test for zwj emoji (#2650) ([f919204](https://github.com/niklasvh/html2canvas/commit/f919204efa06af219f155ca279f96124bb92862b)), closes [#2650](https://github.com/niklasvh/html2canvas/issues/2650)
## [1.2.2](https://github.com/niklasvh/html2canvas/compare/v1.2.1...v1.2.2) (2021-08-10)
### ci
* add ios15 target (#2564) ([e429e04](https://github.com/niklasvh/html2canvas/commit/e429e0443adf5c7ca3041b97a8157b8911302206)), closes [#2564](https://github.com/niklasvh/html2canvas/issues/2564)
### docs
* update test previewer (#2637) ([7a06d0c](https://github.com/niklasvh/html2canvas/commit/7a06d0c2c2f3b8a1d1a8a85c540f8288b782e8c6)), closes [#2637](https://github.com/niklasvh/html2canvas/issues/2637)
### fix
* parsing counter content in pseudo element (#2640) ([1941b9e](https://github.com/niklasvh/html2canvas/commit/1941b9e0acfd9243da0beaf70e1643cab1b4a963)), closes [#2640](https://github.com/niklasvh/html2canvas/issues/2640)
* radial gradient ry check (#2631) ([a0dd38a](https://github.com/niklasvh/html2canvas/commit/a0dd38a8be4e540ae1c1f4b4e41f6c386f3e454f)), closes [#2631](https://github.com/niklasvh/html2canvas/issues/2631)
* test for ios range line break error (#2635) ([f43f942](https://github.com/niklasvh/html2canvas/commit/f43f942fcd793dde9cdc6c0438f379ec3c05c405)), closes [#2635](https://github.com/niklasvh/html2canvas/issues/2635)
### test
* large base64 encoded background (#2636) ([e36408a](https://github.com/niklasvh/html2canvas/commit/e36408ad030fe31acd9969a37fe24c1621c0bd04)), closes [#2636](https://github.com/niklasvh/html2canvas/issues/2636)
## [1.2.1](https://github.com/niklasvh/html2canvas/compare/v1.2.0...v1.2.1) (2021-08-05)

View File

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

View File

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

View File

@ -48,6 +48,12 @@ module.exports = function(config) {
platform: 'iOS',
sdk: '15.0'
},
SauceLabs_IE9: {
base: 'SauceLabs',
browserName: 'internet explorer',
version: '9.0',
platform: 'Windows 7'
},
SauceLabs_IE10: {
base: 'SauceLabs',
browserName: 'internet explorer',
@ -87,6 +93,11 @@ module.exports = function(config) {
version: '9.3',
device: 'iPhone 6 Plus Simulator'
},
IE_9: {
base: 'IE',
'x-ua-compatible': 'IE=EmulateIE9',
flags: ['-extoff']
},
IE_10: {
base: 'IE',
'x-ua-compatible': 'IE=EmulateIE10',

2551
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
"module": "dist/html2canvas.esm.js",
"typings": "dist/types/index.d.ts",
"browser": "dist/html2canvas.js",
"version": "1.2.1",
"version": "1.3.2",
"author": {
"name": "Niklas von Hertzen",
"email": "niklasvh@gmail.com",
@ -48,7 +48,7 @@
"babel-loader": "^8.0.5",
"babel-plugin-add-module-exports": "^1.0.2",
"babel-plugin-dev-expression": "^0.2.1",
"base64-arraybuffer": "0.2.0",
"base64-arraybuffer": "1.0.1",
"body-parser": "^1.19.0",
"chai": "4.1.1",
"chromeless": "^1.5.2",
@ -118,6 +118,7 @@
"homepage": "https://html2canvas.hertzen.com",
"license": "MIT",
"dependencies": {
"css-line-break": "2.0.1"
"css-line-break": "2.0.1",
"text-segmentation": "^1.0.2"
}
}

View File

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

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

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import {OVERFLOW_WRAP} from '../property-descriptors/overflow-wrap';
import {CSSParsedDeclaration} from '../index';
import {fromCodePoint, LineBreaker, toCodePoints} from 'css-line-break';
import {splitGraphemes} from 'text-segmentation';
import {Bounds, parseBounds} from './bounds';
import {FEATURES} from '../../core/features';
import {Context} from '../../core/context';
@ -86,7 +87,7 @@ const getRangeBounds = (context: Context, node: Text, offset: number, length: nu
};
const breakText = (value: string, styles: CSSParsedDeclaration): string[] => {
return styles.letterSpacing !== 0 ? toCodePoints(value).map((i) => fromCodePoint(i)) : breakWords(value, styles);
return styles.letterSpacing !== 0 ? splitGraphemes(value) : breakWords(value, styles);
};
// https://drafts.csswg.org/css-text/#word-separator

View File

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

View File

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

View File

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

View File

@ -5,7 +5,8 @@ export enum OVERFLOW {
VISIBLE = 0,
HIDDEN = 1,
SCROLL = 2,
AUTO = 3
CLIP = 3,
AUTO = 4
}
export const overflow: IPropertyListDescriptor<OVERFLOW[]> = {
@ -20,6 +21,8 @@ export const overflow: IPropertyListDescriptor<OVERFLOW[]> = {
return OVERFLOW.HIDDEN;
case 'scroll':
return OVERFLOW.SCROLL;
case 'clip':
return OVERFLOW.CLIP;
case 'auto':
return OVERFLOW.AUTO;
case 'visible':

View File

@ -2,7 +2,7 @@
import {fromCodePoint, toCodePoints} from 'css-line-break';
export enum TokenType {
export const enum TokenType {
STRING_TOKEN,
BAD_STRING_TOKEN,
LEFT_PARENTHESIS_TOKEN,

View File

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

View File

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

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

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

View File

@ -20,10 +20,12 @@ import {LIST_STYLE_TYPE, listStyleType} from '../css/property-descriptors/list-s
import {CSSParsedCounterDeclaration, CSSParsedPseudoDeclaration} from '../css/index';
import {getQuote} from '../css/property-descriptors/quotes';
import {Context} from '../core/context';
import {DebuggerType, isDebugging} from '../core/debugger';
export interface CloneOptions {
ignoreElements?: (element: Element) => boolean;
onclone?: (document: Document, element: HTMLElement) => void;
allowTaint?: boolean;
}
export interface WindowOptions {
@ -135,6 +137,9 @@ export class DocumentCloner {
}
createElementClone<T extends HTMLElement | SVGElement>(node: T): HTMLElement | SVGElement {
if (isDebugging(node, DebuggerType.CLONE)) {
debugger;
}
if (isCanvasElement(node)) {
return this.createCanvasClone(node);
}
@ -189,7 +194,7 @@ export class DocumentCloner {
img.src = canvas.toDataURL();
return img;
} catch (e) {
this.context.logger.info(`Unable to clone canvas contents, canvas is tainted`);
this.context.logger.info(`Unable to inline canvas contents, canvas is tainted`, canvas);
}
}
@ -201,14 +206,27 @@ export class DocumentCloner {
const ctx = canvas.getContext('2d');
const clonedCtx = clonedCanvas.getContext('2d');
if (clonedCtx) {
if (ctx) {
if (!this.options.allowTaint && ctx) {
clonedCtx.putImageData(ctx.getImageData(0, 0, canvas.width, canvas.height), 0, 0);
} else {
const gl = canvas.getContext('webgl2') ?? canvas.getContext('webgl');
if (gl) {
const attribs = gl.getContextAttributes();
if (attribs?.preserveDrawingBuffer === false) {
this.context.logger.warn(
'Unable to clone WebGL context as it has preserveDrawingBuffer=false',
canvas
);
}
}
clonedCtx.drawImage(canvas, 0, 0);
}
}
return clonedCanvas;
} catch (e) {}
} catch (e) {
this.context.logger.info(`Unable to clone canvas as it is tainted`, canvas);
}
return clonedCanvas;
}
@ -226,6 +244,7 @@ export class DocumentCloner {
if (window && isElementNode(node) && (isHTMLElementNode(node) || isSVGElementNode(node))) {
const clone = this.createElementClone(node);
clone.style.transitionProperty = 'none';
const style = window.getComputedStyle(node);
const styleBefore = window.getComputedStyle(node, ':before');

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import {ElementPaint, parseStackingContexts, StackingContext} from '../stacking-context';
import {asString, Color, isTransparent} from '../../css/types/color';
import {ElementContainer} from '../../dom/element-container';
import {ElementContainer, FLAGS} from '../../dom/element-container';
import {BORDER_STYLE} from '../../css/property-descriptors/border-style';
import {CSSParsedDeclaration} from '../../css/index';
import {TextContainer} from '../../dom/text-container';
@ -19,7 +19,6 @@ import {
import {calculateBackgroundRendering, getBackgroundValueForIndex} from '../background';
import {isDimensionToken} from '../../css/syntax/parser';
import {TextBounds} from '../../css/layout/text';
import {fromCodePoint, toCodePoints} from 'css-line-break';
import {ImageElementContainer} from '../../dom/replaced-elements/image-element-container';
import {contentBox} from '../box-sizing';
import {CanvasElementContainer} from '../../dom/replaced-elements/canvas-element-container';
@ -44,6 +43,8 @@ import {TextShadow} from '../../css/property-descriptors/text-shadow';
import {PAINT_ORDER_LAYER} from '../../css/property-descriptors/paint-order';
import {Renderer} from '../renderer';
import {Context} from '../../core/context';
import {DIRECTION} from '../../css/property-descriptors/direction';
import {splitGraphemes} from 'text-segmentation';
export type RenderConfigurations = RenderOptions & {
backgroundColor: Color | null;
@ -86,12 +87,12 @@ export class CanvasRenderer extends Renderer {
);
}
applyEffects(effects: IElementEffect[], target: EffectTarget): void {
applyEffects(effects: IElementEffect[]): void {
while (this._activeEffects.length) {
this.popEffect();
}
effects.filter((effect) => contains(effect.target, target)).forEach((effect) => this.applyEffect(effect));
effects.forEach((effect) => this.applyEffect(effect));
}
applyEffect(effect: IElementEffect): void {
@ -134,6 +135,10 @@ export class CanvasRenderer extends Renderer {
}
async renderNode(paint: ElementPaint): Promise<void> {
if (contains(paint.container.flags, FLAGS.DEBUG_RENDER)) {
debugger;
}
if (paint.container.styles.isVisible()) {
await this.renderNodeBackgroundAndBorders(paint);
await this.renderNodeContent(paint);
@ -144,7 +149,7 @@ export class CanvasRenderer extends Renderer {
if (letterSpacing === 0) {
this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + baseline);
} else {
const letters = toCodePoints(text.text).map((i) => fromCodePoint(i));
const letters = splitGraphemes(text.text);
letters.reduce((left, letter) => {
this.ctx.fillText(letter, left, text.bounds.top + baseline);
@ -174,6 +179,8 @@ export class CanvasRenderer extends Renderer {
this.ctx.font = font;
this.ctx.direction = styles.direction === DIRECTION.RTL ? 'rtl' : 'ltr';
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'alphabetic';
const {baseline, middle} = this.fontMetrics.getMetrics(fontFamily, fontSize);
const paintOrder = styles.paintOrder;
@ -286,7 +293,7 @@ export class CanvasRenderer extends Renderer {
}
async renderNodeContent(paint: ElementPaint): Promise<void> {
this.applyEffects(paint.effects, EffectTarget.CONTENT);
this.applyEffects(paint.getEffects(EffectTarget.CONTENT));
const container = paint.container;
const curves = paint.curves;
const styles = container.styles;
@ -465,6 +472,9 @@ export class CanvasRenderer extends Renderer {
}
async renderStackContent(stack: StackingContext): Promise<void> {
if (contains(stack.element.container.flags, FLAGS.DEBUG_RENDER)) {
debugger;
}
// https://www.w3.org/TR/css-position-3/#painting-order
// 1. the background and borders of the element forming the stacking context.
await this.renderNodeBackgroundAndBorders(stack.element);
@ -682,7 +692,7 @@ export class CanvasRenderer extends Renderer {
}
async renderNodeBackgroundAndBorders(paint: ElementPaint): Promise<void> {
this.applyEffects(paint.effects, EffectTarget.BACKGROUND_BORDERS);
this.applyEffects(paint.getEffects(EffectTarget.BACKGROUND_BORDERS));
const styles = paint.container.styles;
const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
@ -896,7 +906,7 @@ export class CanvasRenderer extends Renderer {
const stack = parseStackingContexts(element);
await this.renderStack(stack);
this.applyEffects([], EffectTarget.BACKGROUND_BORDERS);
this.applyEffects([]);
return this.canvas;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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