From 878e37a24272d0412fe589975ef8eed931c56e0b Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Wed, 4 Aug 2021 20:58:17 +0800 Subject: [PATCH] fix: element cropping & scrolling (#2625) --- src/__tests__/index.ts | 12 +- src/core/__mocks__/cache-storage.ts | 23 +- src/core/__mocks__/context.ts | 19 ++ src/core/__mocks__/logger.ts | 2 +- src/core/__tests__/cache-storage.ts | 49 +++-- src/core/cache-storage.ts | 49 +---- src/core/context.ts | 21 ++ src/core/logger.ts | 29 ++- src/css/IPropertyDescriptor.ts | 7 +- src/css/ITypeDescriptor.ts | 3 +- src/css/index.ts | 158 +++++++------- src/css/layout/bounds.ts | 27 ++- src/css/layout/text.ts | 20 +- .../__tests__/background-tests.ts | 27 ++- .../__tests__/font-family.ts | 3 +- .../__tests__/paint-order.ts | 3 +- .../__tests__/text-shadow.ts | 5 +- .../__tests__/transform-tests.ts | 3 +- .../property-descriptors/background-clip.ts | 3 +- .../property-descriptors/background-image.ts | 7 +- .../property-descriptors/background-origin.ts | 3 +- .../background-position.ts | 3 +- .../property-descriptors/background-repeat.ts | 3 +- .../property-descriptors/background-size.ts | 3 +- src/css/property-descriptors/border-radius.ts | 4 +- src/css/property-descriptors/border-style.ts | 3 +- src/css/property-descriptors/border-width.ts | 3 +- src/css/property-descriptors/box-shadow.ts | 5 +- src/css/property-descriptors/content.ts | 3 +- .../property-descriptors/counter-increment.ts | 3 +- src/css/property-descriptors/counter-reset.ts | 3 +- src/css/property-descriptors/display.ts | 3 +- src/css/property-descriptors/float.ts | 3 +- src/css/property-descriptors/font-family.ts | 3 +- src/css/property-descriptors/font-style.ts | 3 +- src/css/property-descriptors/font-variant.ts | 3 +- src/css/property-descriptors/font-weight.ts | 3 +- .../property-descriptors/letter-spacing.ts | 3 +- src/css/property-descriptors/line-break.ts | 3 +- .../property-descriptors/list-style-image.ts | 5 +- .../list-style-position.ts | 3 +- .../property-descriptors/list-style-type.ts | 3 +- src/css/property-descriptors/opacity.ts | 3 +- src/css/property-descriptors/overflow-wrap.ts | 3 +- src/css/property-descriptors/overflow.ts | 3 +- src/css/property-descriptors/paint-order.ts | 5 +- src/css/property-descriptors/position.ts | 3 +- src/css/property-descriptors/quotes.ts | 3 +- src/css/property-descriptors/text-align.ts | 3 +- .../text-decoration-line.ts | 3 +- src/css/property-descriptors/text-shadow.ts | 5 +- .../property-descriptors/text-transform.ts | 3 +- .../property-descriptors/transform-origin.ts | 3 +- src/css/property-descriptors/transform.ts | 3 +- src/css/property-descriptors/visibility.ts | 3 +- .../webkit-text-stroke-width.ts | 3 +- src/css/property-descriptors/word-break.ts | 3 +- src/css/property-descriptors/z-index.ts | 3 +- src/css/types/__tests__/color-tests.ts | 3 +- src/css/types/__tests__/image-tests.ts | 85 ++++---- src/css/types/angle.ts | 3 +- src/css/types/color.ts | 18 +- .../functions/-prefix-linear-gradient.ts | 7 +- .../functions/-prefix-radial-gradient.ts | 5 +- src/css/types/functions/-webkit-gradient.ts | 12 +- .../functions/__tests__/radial-gradient.ts | 5 +- src/css/types/functions/gradient.ts | 5 +- src/css/types/functions/linear-gradient.ts | 7 +- src/css/types/functions/radial-gradient.ts | 5 +- src/css/types/image.ts | 10 +- src/dom/__mocks__/document-cloner.ts | 9 +- src/dom/document-cloner.ts | 42 ++-- src/dom/element-container.ts | 7 +- src/dom/elements/li-element-container.ts | 5 +- src/dom/elements/ol-element-container.ts | 5 +- src/dom/elements/select-element-container.ts | 5 +- .../elements/textarea-element-container.ts | 5 +- src/dom/node-parser.ts | 43 ++-- .../canvas-element-container.ts | 5 +- .../iframe-element-container.ts | 18 +- .../image-element-container.ts | 8 +- .../input-element-container.ts | 5 +- .../svg-element-container.ts | 10 +- src/dom/text-container.ts | 5 +- src/index.ts | 197 +++++++++--------- src/render/canvas/canvas-renderer.ts | 57 ++--- src/render/canvas/foreignobject-renderer.ts | 20 +- src/render/renderer.ts | 6 + tests/reftests/options/crop-2.html | 44 ++++ tests/reftests/options/ignore-2.html | 48 +++++ 90 files changed, 750 insertions(+), 552 deletions(-) create mode 100644 src/core/__mocks__/context.ts create mode 100644 src/core/context.ts create mode 100644 src/render/renderer.ts create mode 100644 tests/reftests/options/crop-2.html create mode 100644 tests/reftests/options/ignore-2.html diff --git a/src/__tests__/index.ts b/src/__tests__/index.ts index be3a11d..ba56e5d 100644 --- a/src/__tests__/index.ts +++ b/src/__tests__/index.ts @@ -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 }) ); diff --git a/src/core/__mocks__/cache-storage.ts b/src/core/__mocks__/cache-storage.ts index 864e002..1338dd8 100644 --- a/src/core/__mocks__/cache-storage.ts +++ b/src/core/__mocks__/cache-storage.ts @@ -1,22 +1 @@ -class MockCache { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private readonly _cache: {[key: string]: Promise}; - - constructor() { - this._cache = {}; - } - - addImage(src: string): Promise { - 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 {} diff --git a/src/core/__mocks__/context.ts b/src/core/__mocks__/context.ts new file mode 100644 index 0000000..3a03a8d --- /dev/null +++ b/src/core/__mocks__/context.ts @@ -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} = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly cache: any; + + constructor() { + this.cache = { + addImage: jest.fn().mockImplementation((src: string): Promise => { + const result = Promise.resolve(); + this._cache[src] = result; + return result; + }) + }; + } +} diff --git a/src/core/__mocks__/logger.ts b/src/core/__mocks__/logger.ts index cd9bc0d..a4c1d62 100644 --- a/src/core/__mocks__/logger.ts +++ b/src/core/__mocks__/logger.ts @@ -19,4 +19,4 @@ export class Logger { error(): void {} } -const logger = new Logger(); +export const logger = new Logger(); diff --git a/src/core/__tests__/cache-storage.ts b/src/core/__tests__/cache-storage.ts index 595bc93..7ea4dce 100644 --- a/src/core/__tests__/cache-storage.ts +++ b/src/core/__tests__/cache-storage.ts @@ -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 { diff --git a/src/core/cache-storage.ts b/src/core/cache-storage.ts index 82e1dc0..278acb9 100644 --- a/src/core/cache-storage.ts +++ b/src/core/cache-storage.ts @@ -1,28 +1,9 @@ import {FEATURES} from './features'; -import {Logger} from './logger'; +import type {Context} from './context'; export class CacheStorage { - private static _caches: {[key: string]: Cache} = {}; private static _link?: HTMLAnchorElement; private static _origin = 'about:blank'; - private static _current: Cache | null = null; - - static create(name: string, options: ResourceOptions): Cache { - return (CacheStorage._caches[name] = new Cache(name, options)); - } - - static destroy(name: string): void { - delete CacheStorage._caches[name]; - } - - static open(name: string): Cache { - const cache = CacheStorage._caches[name]; - if (typeof cache !== 'undefined') { - return cache; - } - - throw new Error(`Cache with key "${name}" not found`); - } static getOrigin(url: string): string { const link = CacheStorage._link; @@ -43,22 +24,6 @@ export class CacheStorage { CacheStorage._link = window.document.createElement('a'); CacheStorage._origin = CacheStorage.getOrigin(window.location.href); } - - static getInstance(): Cache { - const current = CacheStorage._current; - if (current === null) { - throw new Error(`No cache instance attached`); - } - return current; - } - - static attachInstance(cache: Cache): void { - CacheStorage._current = cache; - } - - static detachInstance(): void { - CacheStorage._current = null; - } } export interface ResourceOptions { @@ -70,15 +35,9 @@ export interface ResourceOptions { export class Cache { // eslint-disable-next-line @typescript-eslint/no-explicit-any - private readonly _cache: {[key: string]: Promise}; - private readonly _options: ResourceOptions; - private readonly id: string; + private readonly _cache: {[key: string]: Promise} = {}; - 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 { const result = Promise.resolve(); @@ -128,7 +87,7 @@ export class Cache { src = await this.proxy(src); } - Logger.getInstance(this.id).debug(`Added image ${key.substring(0, 256)}`); + this.context.logger.debug(`Added image ${key.substring(0, 256)}`); return await new Promise((resolve, reject) => { const img = new Image(); diff --git a/src/core/context.ts b/src/core/context.ts new file mode 100644 index 0000000..c47d627 --- /dev/null +++ b/src/core/context.ts @@ -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); + } +} diff --git a/src/core/logger.ts b/src/core/logger.ts index 0a248d8..bd70128 100644 --- a/src/core/logger.ts +++ b/src/core/logger.ts @@ -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) { diff --git a/src/css/IPropertyDescriptor.ts b/src/css/IPropertyDescriptor.ts index e3113dc..8fc9e92 100644 --- a/src/css/IPropertyDescriptor.ts +++ b/src/css/IPropertyDescriptor.ts @@ -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 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 extends IPropertyDescriptor { type: PropertyDescriptorParsingType.VALUE; - parse: (token: CSSValue) => T; + parse: (context: Context, token: CSSValue) => T; } export interface IPropertyListDescriptor extends IPropertyDescriptor { type: PropertyDescriptorParsingType.LIST; - parse: (tokens: CSSValue[]) => T; + parse: (context: Context, tokens: CSSValue[]) => T; } export interface IPropertyTokenValueDescriptor extends IPropertyDescriptor { diff --git a/src/css/ITypeDescriptor.ts b/src/css/ITypeDescriptor.ts index e448601..2f47081 100644 --- a/src/css/ITypeDescriptor.ts +++ b/src/css/ITypeDescriptor.ts @@ -1,6 +1,7 @@ import {CSSValue} from './syntax/parser'; +import {Context} from '../core/context'; export interface ITypeDescriptor { name: string; - parse: (value: CSSValue) => T; + parse: (context: Context, value: CSSValue) => T; } diff --git a/src/css/index.ts b/src/css/index.ts index 09d22d3..e1ab483 100644 --- a/src/css/index.ts +++ b/src/css/index.ts @@ -76,6 +76,7 @@ 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; @@ -143,75 +144,80 @@ export class CSSParsedDeclaration { wordBreak: ReturnType; zIndex: ReturnType; - 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.paintOrder = parse(paintOrder, declaration.paintOrder); - 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.webkitTextStrokeColor = parse(webkitTextStrokeColor, declaration.webkitTextStrokeColor); - this.webkitTextStrokeWidth = parse(webkitTextStrokeWidth, declaration.webkitTextStrokeWidth); - 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 { @@ -254,9 +260,9 @@ export class CSSParsedPseudoDeclaration { content: ReturnType; quotes: ReturnType; - 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); } } @@ -264,14 +270,14 @@ export class CSSParsedCounterDeclaration { counterIncrement: ReturnType; counterReset: ReturnType; - 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, style?: string | null) => { +const parse = (context: Context, descriptor: CSSPropertyDescriptor, style?: string | null) => { const tokenizer = new Tokenizer(); const value = style !== null && typeof style !== 'undefined' ? style.toString() : descriptor.initialValue; tokenizer.write(value); @@ -279,21 +285,21 @@ const parse = (descriptor: CSSPropertyDescriptor, 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; diff --git a/src/css/layout/bounds.ts b/src/css/layout/bounds.ts index 42ae1c8..04596e3 100644 --- a/src/css/layout/bounds.ts +++ b/src/css/layout/bounds.ts @@ -1,27 +1,24 @@ -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 + ); } } -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 => { diff --git a/src/css/layout/text.ts b/src/css/layout/text.ts index 2734967..2473926 100644 --- a/src/css/layout/text.ts +++ b/src/css/layout/text.ts @@ -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,22 @@ 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))); + 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 +42,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 +50,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); } @@ -55,7 +61,7 @@ const getWrapperBounds = (node: Text): Bounds => { return new Bounds(0, 0, 0, 0); }; -const getRangeBounds = (node: Text, offset: number, length: number): Bounds => { +const getRangeBounds = (context: Context, node: Text, offset: number, length: number): Bounds => { const ownerDocument = node.ownerDocument; if (!ownerDocument) { throw new Error('Node has no owner document'); @@ -63,7 +69,7 @@ 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 Bounds.fromClientRect(context, range.getBoundingClientRect()); }; const breakText = (value: string, styles: CSSParsedDeclaration): string[] => { diff --git a/src/css/property-descriptors/__tests__/background-tests.ts b/src/css/property-descriptors/__tests__/background-tests.ts index 2f61495..35f0923 100644 --- a/src/css/property-descriptors/__tests__/background-tests.ts +++ b/src/css/property-descriptors/__tests__/background-tests.ts @@ -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')` ), [ diff --git a/src/css/property-descriptors/__tests__/font-family.ts b/src/css/property-descriptors/__tests__/font-family.ts index f98a9ce..38c54b8 100644 --- a/src/css/property-descriptors/__tests__/font-family.ts +++ b/src/css/property-descriptors/__tests__/font-family.ts @@ -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', () => { diff --git a/src/css/property-descriptors/__tests__/paint-order.ts b/src/css/property-descriptors/__tests__/paint-order.ts index 19aa7e3..6b76f4f 100644 --- a/src/css/property-descriptors/__tests__/paint-order.ts +++ b/src/css/property-descriptors/__tests__/paint-order.ts @@ -1,8 +1,9 @@ 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(Parser.parseValues(value)); +const paintOrderParse = (value: string) => paintOrder.parse({} as Context, Parser.parseValues(value)); describe('property-descriptors', () => { describe('paint-order', () => { diff --git a/src/css/property-descriptors/__tests__/text-shadow.ts b/src/css/property-descriptors/__tests__/text-shadow.ts index 1eae5f8..ae0ad74 100644 --- a/src/css/property-descriptors/__tests__/text-shadow.ts +++ b/src/css/property-descriptors/__tests__/text-shadow.ts @@ -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, diff --git a/src/css/property-descriptors/__tests__/transform-tests.ts b/src/css/property-descriptors/__tests__/transform-tests.ts index 48c6bf1..9495342 100644 --- a/src/css/property-descriptors/__tests__/transform-tests.ts +++ b/src/css/property-descriptors/__tests__/transform-tests.ts @@ -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', () => { diff --git a/src/css/property-descriptors/background-clip.ts b/src/css/property-descriptors/background-clip.ts index f64c53d..022676d 100644 --- a/src/css/property-descriptors/background-clip.ts +++ b/src/css/property-descriptors/background-clip.ts @@ -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 = { 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) { diff --git a/src/css/property-descriptors/background-image.ts b/src/css/property-descriptors/background-image.ts index 9709946..d8553f3 100644 --- a/src/css/property-descriptors/background-image.ts +++ b/src/css/property-descriptors/background-image.ts @@ -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 = { 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 = { 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)); } }; diff --git a/src/css/property-descriptors/background-origin.ts b/src/css/property-descriptors/background-origin.ts index 86b4a75..85a1ed8 100644 --- a/src/css/property-descriptors/background-origin.ts +++ b/src/css/property-descriptors/background-origin.ts @@ -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 = { 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) { diff --git a/src/css/property-descriptors/background-position.ts b/src/css/property-descriptors/background-position.ts index 5f67d18..729cac1 100644 --- a/src/css/property-descriptors/background-position.ts +++ b/src/css/property-descriptors/background-position.ts @@ -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 = { 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); diff --git a/src/css/property-descriptors/background-repeat.ts b/src/css/property-descriptors/background-repeat.ts index d35220b..e02dcb1 100644 --- a/src/css/property-descriptors/background-repeat.ts +++ b/src/css/property-descriptors/background-repeat.ts @@ -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 = { initialValue: 'repeat', prefix: false, type: PropertyDescriptorParsingType.LIST, - parse: (tokens: CSSValue[]): BackgroundRepeat => { + parse: (_context: Context, tokens: CSSValue[]): BackgroundRepeat => { return parseFunctionArgs(tokens) .map((values) => values diff --git a/src/css/property-descriptors/background-size.ts b/src/css/property-descriptors/background-size.ts index a62e6f6..d9b3d29 100644 --- a/src/css/property-descriptors/background-size.ts +++ b/src/css/property-descriptors/background-size.ts @@ -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 = { 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)); } }; diff --git a/src/css/property-descriptors/border-radius.ts b/src/css/property-descriptors/border-radius.ts index 23a4a9a..29fd3d1 100644 --- a/src/css/property-descriptors/border-radius.ts +++ b/src/css/property-descriptors/border-radius.ts @@ -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 => ({ @@ -8,7 +9,8 @@ const borderRadiusForSide = (side: string): IPropertyListDescriptor parseLengthPercentageTuple(tokens.filter(isLengthPercentage)) + parse: (_context: Context, tokens: CSSValue[]): BorderRadius => + parseLengthPercentageTuple(tokens.filter(isLengthPercentage)) }); export const borderTopLeftRadius: IPropertyListDescriptor = borderRadiusForSide('top-left'); diff --git a/src/css/property-descriptors/border-style.ts b/src/css/property-descriptors/border-style.ts index b70ff07..4302f66 100644 --- a/src/css/property-descriptors/border-style.ts +++ b/src/css/property-descriptors/border-style.ts @@ -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 { + parse: (_context: Context, style: string): BORDER_STYLE => { switch (style) { case 'none': return BORDER_STYLE.NONE; diff --git a/src/css/property-descriptors/border-width.ts b/src/css/property-descriptors/border-width.ts index 853dde9..1fd9e97 100644 --- a/src/css/property-descriptors/border-width.ts +++ b/src/css/property-descriptors/border-width.ts @@ -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 => ({ 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; } diff --git a/src/css/property-descriptors/box-shadow.ts b/src/css/property-descriptors/box-shadow.ts index 8a42536..84df3a0 100644 --- a/src/css/property-descriptors/box-shadow.ts +++ b/src/css/property-descriptors/box-shadow.ts @@ -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 = { 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 = { } c++; } else { - shadow.color = color.parse(token); + shadow.color = color.parse(context, token); } } return shadow; diff --git a/src/css/property-descriptors/content.ts b/src/css/property-descriptors/content.ts index 956d511..90ec020 100644 --- a/src/css/property-descriptors/content.ts +++ b/src/css/property-descriptors/content.ts @@ -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 = { initialValue: 'none', type: PropertyDescriptorParsingType.LIST, prefix: false, - parse: (tokens: CSSValue[]) => { + parse: (_context: Context, tokens: CSSValue[]) => { if (tokens.length === 0) { return []; } diff --git a/src/css/property-descriptors/counter-increment.ts b/src/css/property-descriptors/counter-increment.ts index a43e97e..6af5894 100644 --- a/src/css/property-descriptors/counter-increment.ts +++ b/src/css/property-descriptors/counter-increment.ts @@ -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 = { initialValue: 'none', prefix: true, type: PropertyDescriptorParsingType.LIST, - parse: (tokens: CSSValue[]) => { + parse: (_context: Context, tokens: CSSValue[]) => { if (tokens.length === 0) { return null; } diff --git a/src/css/property-descriptors/counter-reset.ts b/src/css/property-descriptors/counter-reset.ts index aa958ed..b77dc03 100644 --- a/src/css/property-descriptors/counter-reset.ts +++ b/src/css/property-descriptors/counter-reset.ts @@ -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 = { initialValue: 'none', prefix: true, type: PropertyDescriptorParsingType.LIST, - parse: (tokens: CSSValue[]) => { + parse: (_context: Context, tokens: CSSValue[]) => { if (tokens.length === 0) { return []; } diff --git a/src/css/property-descriptors/display.ts b/src/css/property-descriptors/display.ts index 4243c4a..d36d7c3 100644 --- a/src/css/property-descriptors/display.ts +++ b/src/css/property-descriptors/display.ts @@ -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 = { 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); diff --git a/src/css/property-descriptors/float.ts b/src/css/property-descriptors/float.ts index ba0a98a..134785e 100644 --- a/src/css/property-descriptors/float.ts +++ b/src/css/property-descriptors/float.ts @@ -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 = { initialValue: 'none', prefix: false, type: PropertyDescriptorParsingType.IDENT_VALUE, - parse: (float: string) => { + parse: (_context: Context, float: string) => { switch (float) { case 'left': return FLOAT.LEFT; diff --git a/src/css/property-descriptors/font-family.ts b/src/css/property-descriptors/font-family.ts index e644759..ec1e282 100644 --- a/src/css/property-descriptors/font-family.ts +++ b/src/css/property-descriptors/font-family.ts @@ -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 = { initialValue: '', prefix: false, type: PropertyDescriptorParsingType.LIST, - parse: (tokens: CSSValue[]) => { + parse: (_context: Context, tokens: CSSValue[]) => { const accumulator: string[] = []; const results: string[] = []; tokens.forEach((token) => { diff --git a/src/css/property-descriptors/font-style.ts b/src/css/property-descriptors/font-style.ts index 84fe0cc..9a9c40e 100644 --- a/src/css/property-descriptors/font-style.ts +++ b/src/css/property-descriptors/font-style.ts @@ -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 = { initialValue: 'normal', prefix: false, type: PropertyDescriptorParsingType.IDENT_VALUE, - parse: (overflow: string) => { + parse: (_context: Context, overflow: string) => { switch (overflow) { case 'oblique': return FONT_STYLE.OBLIQUE; diff --git a/src/css/property-descriptors/font-variant.ts b/src/css/property-descriptors/font-variant.ts index 66e54ba..e416513 100644 --- a/src/css/property-descriptors/font-variant.ts +++ b/src/css/property-descriptors/font-variant.ts @@ -1,11 +1,12 @@ import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor'; import {CSSValue, isIdentToken} from '../syntax/parser'; +import {Context} from '../../core/context'; export const fontVariant: IPropertyListDescriptor = { 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); } }; diff --git a/src/css/property-descriptors/font-weight.ts b/src/css/property-descriptors/font-weight.ts index cf80d1a..ca418b6 100644 --- a/src/css/property-descriptors/font-weight.ts +++ b/src/css/property-descriptors/font-weight.ts @@ -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 = { 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; } diff --git a/src/css/property-descriptors/letter-spacing.ts b/src/css/property-descriptors/letter-spacing.ts index d4d1c2e..6f82333 100644 --- a/src/css/property-descriptors/letter-spacing.ts +++ b/src/css/property-descriptors/letter-spacing.ts @@ -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 = { 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; } diff --git a/src/css/property-descriptors/line-break.ts b/src/css/property-descriptors/line-break.ts index 1a4b02b..1fe3f13 100644 --- a/src/css/property-descriptors/line-break.ts +++ b/src/css/property-descriptors/line-break.ts @@ -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 = { 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; diff --git a/src/css/property-descriptors/list-style-image.ts b/src/css/property-descriptors/list-style-image.ts index 11143f1..4cb4fcc 100644 --- a/src/css/property-descriptors/list-style-image.ts +++ b/src/css/property-descriptors/list-style-image.ts @@ -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 = { 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); } }; diff --git a/src/css/property-descriptors/list-style-position.ts b/src/css/property-descriptors/list-style-position.ts index c636cce..d4fbf74 100644 --- a/src/css/property-descriptors/list-style-position.ts +++ b/src/css/property-descriptors/list-style-position.ts @@ -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 { + parse: (_context: Context, position: string) => { switch (position) { case 'inside': return LIST_STYLE_POSITION.INSIDE; diff --git a/src/css/property-descriptors/list-style-type.ts b/src/css/property-descriptors/list-style-type.ts index 8ddaad6..8655001 100644 --- a/src/css/property-descriptors/list-style-type.ts +++ b/src/css/property-descriptors/list-style-type.ts @@ -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 = { 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; diff --git a/src/css/property-descriptors/opacity.ts b/src/css/property-descriptors/opacity.ts index cbcc033..579e973 100644 --- a/src/css/property-descriptors/opacity.ts +++ b/src/css/property-descriptors/opacity.ts @@ -1,11 +1,12 @@ import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor'; import {CSSValue, isNumberToken} from '../syntax/parser'; +import {Context} from '../../core/context'; export const opacity: IPropertyValueDescriptor = { 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; } diff --git a/src/css/property-descriptors/overflow-wrap.ts b/src/css/property-descriptors/overflow-wrap.ts index d7d2dfc..e28887d 100644 --- a/src/css/property-descriptors/overflow-wrap.ts +++ b/src/css/property-descriptors/overflow-wrap.ts @@ -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 = { 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; diff --git a/src/css/property-descriptors/overflow.ts b/src/css/property-descriptors/overflow.ts index 8d9f8fa..19ac544 100644 --- a/src/css/property-descriptors/overflow.ts +++ b/src/css/property-descriptors/overflow.ts @@ -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 = { 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': diff --git a/src/css/property-descriptors/paint-order.ts b/src/css/property-descriptors/paint-order.ts index eb2a901..83fade4 100644 --- a/src/css/property-descriptors/paint-order.ts +++ b/src/css/property-descriptors/paint-order.ts @@ -1,5 +1,6 @@ import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor'; import {CSSValue, isIdentToken} from '../syntax/parser'; +import {Context} from '../../core/context'; export enum PAINT_ORDER_LAYER { FILL, STROKE, @@ -13,9 +14,9 @@ export const paintOrder: IPropertyListDescriptor = { initialValue: 'normal', prefix: false, type: PropertyDescriptorParsingType.LIST, - parse: (tokens: CSSValue[]): PaintOrder => { + parse: (_context: Context, tokens: CSSValue[]): PaintOrder => { const DEFAULT_VALUE = [PAINT_ORDER_LAYER.FILL, PAINT_ORDER_LAYER.STROKE, PAINT_ORDER_LAYER.MARKERS]; - let layers: PaintOrder = []; + const layers: PaintOrder = []; tokens.filter(isIdentToken).forEach((token) => { switch (token.value) { diff --git a/src/css/property-descriptors/position.ts b/src/css/property-descriptors/position.ts index ba1b381..8ff8944 100644 --- a/src/css/property-descriptors/position.ts +++ b/src/css/property-descriptors/position.ts @@ -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 = { initialValue: 'static', prefix: false, type: PropertyDescriptorParsingType.IDENT_VALUE, - parse: (position: string) => { + parse: (_context: Context, position: string) => { switch (position) { case 'relative': return POSITION.RELATIVE; diff --git a/src/css/property-descriptors/quotes.ts b/src/css/property-descriptors/quotes.ts index 29c3c79..db22beb 100644 --- a/src/css/property-descriptors/quotes.ts +++ b/src/css/property-descriptors/quotes.ts @@ -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 = { initialValue: 'none', prefix: true, type: PropertyDescriptorParsingType.LIST, - parse: (tokens: CSSValue[]) => { + parse: (_context: Context, tokens: CSSValue[]) => { if (tokens.length === 0) { return null; } diff --git a/src/css/property-descriptors/text-align.ts b/src/css/property-descriptors/text-align.ts index d0ef6c5..6fb4d17 100644 --- a/src/css/property-descriptors/text-align.ts +++ b/src/css/property-descriptors/text-align.ts @@ -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 = { initialValue: 'left', prefix: false, type: PropertyDescriptorParsingType.IDENT_VALUE, - parse: (textAlign: string) => { + parse: (_context: Context, textAlign: string) => { switch (textAlign) { case 'right': return TEXT_ALIGN.RIGHT; diff --git a/src/css/property-descriptors/text-decoration-line.ts b/src/css/property-descriptors/text-decoration-line.ts index ebeeaea..5028eb5 100644 --- a/src/css/property-descriptors/text-decoration-line.ts +++ b/src/css/property-descriptors/text-decoration-line.ts @@ -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 = { initialValue: 'none', prefix: false, type: PropertyDescriptorParsingType.LIST, - parse: (tokens: CSSValue[]): TextDecorationLine => { + parse: (_context: Context, tokens: CSSValue[]): TextDecorationLine => { return tokens .filter(isIdentToken) .map((token) => { diff --git a/src/css/property-descriptors/text-shadow.ts b/src/css/property-descriptors/text-shadow.ts index 44efeae..fd58023 100644 --- a/src/css/property-descriptors/text-shadow.ts +++ b/src/css/property-descriptors/text-shadow.ts @@ -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 = { 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 = { } c++; } else { - shadow.color = color.parse(token); + shadow.color = color.parse(context, token); } } return shadow; diff --git a/src/css/property-descriptors/text-transform.ts b/src/css/property-descriptors/text-transform.ts index 08d3c4a..fd879be 100644 --- a/src/css/property-descriptors/text-transform.ts +++ b/src/css/property-descriptors/text-transform.ts @@ -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 = { initialValue: 'none', prefix: false, type: PropertyDescriptorParsingType.IDENT_VALUE, - parse: (textTransform: string) => { + parse: (_context: Context, textTransform: string) => { switch (textTransform) { case 'uppercase': return TEXT_TRANSFORM.UPPERCASE; diff --git a/src/css/property-descriptors/transform-origin.ts b/src/css/property-descriptors/transform-origin.ts index 29a1819..bdad471 100644 --- a/src/css/property-descriptors/transform-origin.ts +++ b/src/css/property-descriptors/transform-origin.ts @@ -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 = { 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) { diff --git a/src/css/property-descriptors/transform.ts b/src/css/property-descriptors/transform.ts index ca5a0bd..a99a575 100644 --- a/src/css/property-descriptors/transform.ts +++ b/src/css/property-descriptors/transform.ts @@ -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 = { 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; } diff --git a/src/css/property-descriptors/visibility.ts b/src/css/property-descriptors/visibility.ts index 0410edb..ede4df1 100644 --- a/src/css/property-descriptors/visibility.ts +++ b/src/css/property-descriptors/visibility.ts @@ -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 = { initialValue: 'none', prefix: false, type: PropertyDescriptorParsingType.IDENT_VALUE, - parse: (visibility: string) => { + parse: (_context: Context, visibility: string) => { switch (visibility) { case 'hidden': return VISIBILITY.HIDDEN; diff --git a/src/css/property-descriptors/webkit-text-stroke-width.ts b/src/css/property-descriptors/webkit-text-stroke-width.ts index 4d0acef..ffe0483 100644 --- a/src/css/property-descriptors/webkit-text-stroke-width.ts +++ b/src/css/property-descriptors/webkit-text-stroke-width.ts @@ -1,11 +1,12 @@ import {CSSValue, isDimensionToken} from '../syntax/parser'; import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor'; +import {Context} from '../../core/context'; export const webkitTextStrokeWidth: IPropertyValueDescriptor = { name: `-webkit-text-stroke-width`, initialValue: '0', type: PropertyDescriptorParsingType.VALUE, prefix: false, - parse: (token: CSSValue): number => { + parse: (_context: Context, token: CSSValue): number => { if (isDimensionToken(token)) { return token.number; } diff --git a/src/css/property-descriptors/word-break.ts b/src/css/property-descriptors/word-break.ts index e374e8e..c03f980 100644 --- a/src/css/property-descriptors/word-break.ts +++ b/src/css/property-descriptors/word-break.ts @@ -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 = { 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; diff --git a/src/css/property-descriptors/z-index.ts b/src/css/property-descriptors/z-index.ts index ee13b35..b366766 100644 --- a/src/css/property-descriptors/z-index.ts +++ b/src/css/property-descriptors/z-index.ts @@ -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 = { 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}; } diff --git a/src/css/types/__tests__/color-tests.ts b/src/css/types/__tests__/color-tests.ts index 9c8677d..2976c3c 100644 --- a/src/css/types/__tests__/color-tests.ts +++ b/src/css/types/__tests__/color-tests.ts @@ -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('', () => { diff --git a/src/css/types/__tests__/image-tests.ts b/src/css/types/__tests__/image-tests.ts index 9e50c6b..dac9eea 100644 --- a/src/css/types/__tests__/image-tests.ts +++ b/src/css/types/__tests__/image-tests.ts @@ -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('', () => { 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, diff --git a/src/css/types/angle.ts b/src/css/types/angle.ts index ec66b7e..676f8e0 100644 --- a/src/css/types/angle.ts +++ b/src/css/types/angle.ts @@ -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 = { name: 'angle', - parse: (value: CSSValue): number => { + parse: (_context: Context, value: CSSValue): number => { if (value.type === TokenType.DIMENSION_TOKEN) { switch (value.unit) { case DEG: diff --git a/src/css/types/color.ts b/src/css/types/color.ts index b0e1afa..29af9ce 100644 --- a/src/css/types/color.ts +++ b/src/css/types/color.ts @@ -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 = { 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, diff --git a/src/css/types/functions/-prefix-linear-gradient.ts b/src/css/types/functions/-prefix-linear-gradient.ts index 8df8199..147dd9d 100644 --- a/src/css/types/functions/-prefix-linear-gradient.ts +++ b/src/css/types/functions/-prefix-linear-gradient.ts @@ -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); }); diff --git a/src/css/types/functions/-prefix-radial-gradient.ts b/src/css/types/functions/-prefix-radial-gradient.ts index 7bb8622..e4a391b 100644 --- a/src/css/types/functions/-prefix-radial-gradient.ts +++ b/src/css/types/functions/-prefix-radial-gradient.ts @@ -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); } }); diff --git a/src/css/types/functions/-webkit-gradient.ts b/src/css/types/functions/-webkit-gradient.ts index e9f6f47..9a687d6 100644 --- a/src/css/types/functions/-webkit-gradient.ts +++ b/src/css/types/functions/-webkit-gradient.ts @@ -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({ diff --git a/src/css/types/functions/__tests__/radial-gradient.ts b/src/css/types/functions/__tests__/radial-gradient.ts index 508598e..3578416 100644 --- a/src/css/types/functions/__tests__/radial-gradient.ts +++ b/src/css/types/functions/__tests__/radial-gradient.ts @@ -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', () => { diff --git a/src/css/types/functions/gradient.ts b/src/css/types/functions/gradient.ts index 0463b8b..c6b894f 100644 --- a/src/css/types/functions/gradient.ts +++ b/src/css/types/functions/gradient.ts @@ -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}; }; diff --git a/src/css/types/functions/linear-gradient.ts b/src/css/types/functions/linear-gradient.ts index 27e65ce..2d13844 100644 --- a/src/css/types/functions/linear-gradient.ts +++ b/src/css/types/functions/linear-gradient.ts @@ -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); }); diff --git a/src/css/types/functions/radial-gradient.ts b/src/css/types/functions/radial-gradient.ts index 7fe99e2..f88bbdc 100644 --- a/src/css/types/functions/radial-gradient.ts +++ b/src/css/types/functions/radial-gradient.ts @@ -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); } }); diff --git a/src/css/types/image.ts b/src/css/types/image.ts index 17be3fe..7e4ae8d 100644 --- a/src/css/types/image.ts +++ b/src/css/types/image.ts @@ -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 = { 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,7 +91,7 @@ export const image: ITypeDescriptor = { 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`); @@ -102,7 +102,7 @@ export function isSupportedImage(value: CSSValue): boolean { return value.type !== TokenType.FUNCTION || !!SUPPORTED_IMAGE_FUNCTIONS[value.name]; } -const SUPPORTED_IMAGE_FUNCTIONS: Record ICSSImage> = { +const SUPPORTED_IMAGE_FUNCTIONS: Record ICSSImage> = { 'linear-gradient': linearGradient, '-moz-linear-gradient': prefixLinearGradient, '-ms-linear-gradient': prefixLinearGradient, diff --git a/src/dom/__mocks__/document-cloner.ts b/src/dom/__mocks__/document-cloner.ts index 0311b06..3fa53f6 100644 --- a/src/dom/__mocks__/document-cloner.ts +++ b/src/dom/__mocks__/document-cloner.ts @@ -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 { diff --git a/src/dom/document-cloner.ts b/src/dom/document-cloner.ts index 4e644ce..cacfc02 100644 --- a/src/dom/document-cloner.ts +++ b/src/dom/document-cloner.ts @@ -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 + ); } } @@ -162,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; } @@ -177,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`); } } @@ -226,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) { @@ -290,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); @@ -318,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( @@ -331,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 diff --git a/src/dom/element-container.ts b/src/dom/element-container.ts index 3482e96..9928cc7 100644 --- a/src/dom/element-container.ts +++ b/src/dom/element-container.ts @@ -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; } } diff --git a/src/dom/elements/li-element-container.ts b/src/dom/elements/li-element-container.ts index 5a42757..f65f088 100644 --- a/src/dom/elements/li-element-container.ts +++ b/src/dom/elements/li-element-container.ts @@ -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; } } diff --git a/src/dom/elements/ol-element-container.ts b/src/dom/elements/ol-element-container.ts index 65f3d9e..47ebcdc 100644 --- a/src/dom/elements/ol-element-container.ts +++ b/src/dom/elements/ol-element-container.ts @@ -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; } diff --git a/src/dom/elements/select-element-container.ts b/src/dom/elements/select-element-container.ts index 84147a6..147d145 100644 --- a/src/dom/elements/select-element-container.ts +++ b/src/dom/elements/select-element-container.ts @@ -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 || '' : ''; } diff --git a/src/dom/elements/textarea-element-container.ts b/src/dom/elements/textarea-element-container.ts index e7c56df..5b7b421 100644 --- a/src/dom/elements/textarea-element-container.ts +++ b/src/dom/elements/textarea-element-container.ts @@ -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; } } diff --git a/src/dom/node-parser.ts b/src/dom/node-parser.ts index f00c02b..260974e 100644 --- a/src/dom/node-parser.ts +++ b/src/dom/node-parser.ts @@ -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; }; diff --git a/src/dom/replaced-elements/canvas-element-container.ts b/src/dom/replaced-elements/canvas-element-container.ts index 84747d1..0364ca7 100644 --- a/src/dom/replaced-elements/canvas-element-container.ts +++ b/src/dom/replaced-elements/canvas-element-container.ts @@ -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; diff --git a/src/dom/replaced-elements/iframe-element-container.ts b/src/dom/replaced-elements/iframe-element-container.ts index 54ba1e9..1ad8d85 100644 --- a/src/dom/replaced-elements/iframe-element-container.ts +++ b/src/dom/replaced-elements/iframe-element-container.ts @@ -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) diff --git a/src/dom/replaced-elements/image-element-container.ts b/src/dom/replaced-elements/image-element-container.ts index ca74cf7..51ef1b1 100644 --- a/src/dom/replaced-elements/image-element-container.ts +++ b/src/dom/replaced-elements/image-element-container.ts @@ -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); } } diff --git a/src/dom/replaced-elements/input-element-container.ts b/src/dom/replaced-elements/input-element-container.ts index c06e454..1a2c78a 100644 --- a/src/dom/replaced-elements/input-element-container.ts +++ b/src/dom/replaced-elements/input-element-container.ts @@ -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); diff --git a/src/dom/replaced-elements/svg-element-container.ts b/src/dom/replaced-elements/svg-element-container.ts index 62511be..ae9c5a4 100644 --- a/src/dom/replaced-elements/svg-element-container.ts +++ b/src/dom/replaced-elements/svg-element-container.ts @@ -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); } } diff --git a/src/dom/text-container.ts b/src/dom/text-container.ts index 9eb2c52..3923bb2 100644 --- a/src/dom/text-container.ts +++ b/src/dom/text-container.ts @@ -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); } } diff --git a/src/index.ts b/src/index.ts index c6eb7b0..a56a284 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,24 +1,21 @@ import {Bounds, parseBounds, parseDocumentSize} from './css/layout/bounds'; -import {color, Color, COLORS, isTransparent} from './css/types/color'; -import {Parser} from './css/syntax/parser'; -import {CloneOptions, DocumentCloner} from './dom/document-cloner'; +import {COLORS, isTransparent, parseColor} from './css/types/color'; +import {CloneConfigurations, CloneOptions, DocumentCloner, WindowOptions} from './dom/document-cloner'; import {isBodyElement, isHTMLElement, parseTree} from './dom/node-parser'; -import {Logger} from './core/logger'; -import {CacheStorage, ResourceOptions} from './core/cache-storage'; -import {CanvasRenderer, RenderOptions} from './render/canvas/canvas-renderer'; +import {CacheStorage} from './core/cache-storage'; +import {CanvasRenderer, RenderConfigurations, RenderOptions} from './render/canvas/canvas-renderer'; import {ForeignObjectRenderer} from './render/canvas/foreignobject-renderer'; +import {Context, ContextOptions} from './core/context'; export type Options = CloneOptions & + WindowOptions & RenderOptions & - ResourceOptions & { + ContextOptions & { backgroundColor: string | null; foreignObjectRendering: boolean; - logging: boolean; removeContainer?: boolean; }; -const parseColor = (value: string): Color => color.parse(Parser.create(value).parseComponentValue()); - const html2canvas = (element: HTMLElement, options: Partial = {}): Promise => { 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): Promise => { 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): 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): 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; +}; diff --git a/src/render/canvas/canvas-renderer.ts b/src/render/canvas/canvas-renderer.ts index 08625bd..12d2a60 100644 --- a/src/render/canvas/canvas-renderer.ts +++ b/src/render/canvas/canvas-renderer.ts @@ -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'; @@ -44,39 +42,34 @@ import {SelectElementContainer} from '../../dom/elements/select-element-containe 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); @@ -85,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}` ); } @@ -253,6 +246,7 @@ export class CanvasRenderer { 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); } @@ -302,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}`); } } @@ -315,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); @@ -444,10 +432,10 @@ 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) { @@ -592,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) { @@ -902,12 +890,7 @@ export class CanvasRenderer { async render(element: ElementContainer): Promise { 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); diff --git a/src/render/canvas/foreignobject-renderer.ts b/src/render/canvas/foreignobject-renderer.ts index cee5971..a6d1e3a 100644 --- a/src/render/canvas/foreignobject-renderer.ts +++ b/src/render/canvas/foreignobject-renderer.ts @@ -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 { 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 ); diff --git a/src/render/renderer.ts b/src/render/renderer.ts new file mode 100644 index 0000000..420db35 --- /dev/null +++ b/src/render/renderer.ts @@ -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) {} +} diff --git a/tests/reftests/options/crop-2.html b/tests/reftests/options/crop-2.html new file mode 100644 index 0000000..7184945 --- /dev/null +++ b/tests/reftests/options/crop-2.html @@ -0,0 +1,44 @@ + + + + crop test + + + + + + + + +
+ great success +
+ + + diff --git a/tests/reftests/options/ignore-2.html b/tests/reftests/options/ignore-2.html new file mode 100644 index 0000000..70f6aea --- /dev/null +++ b/tests/reftests/options/ignore-2.html @@ -0,0 +1,48 @@ + + + + element render test + + + + + + + +
+ great failure +
+
+ ignore predicate +
+
+ great success +
+ + +