mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
Deploying to gh-pages from @ 7c3269bdbe 🚀
This commit is contained in:
93
src/__tests__/index.ts
Normal file
93
src/__tests__/index.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import html2canvas from '../index';
|
||||
|
||||
import {CanvasRenderer} from '../render/canvas/canvas-renderer';
|
||||
import {DocumentCloner} from '../dom/document-cloner';
|
||||
import {COLORS} from '../css/types/color';
|
||||
|
||||
jest.mock('../core/logger');
|
||||
jest.mock('../css/layout/bounds');
|
||||
jest.mock('../dom/document-cloner');
|
||||
jest.mock('../dom/node-parser', () => {
|
||||
return {
|
||||
isBodyElement: () => false,
|
||||
isHTMLElement: () => false,
|
||||
parseTree: jest.fn().mockImplementation(() => {
|
||||
return {styles: {}};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../render/stacking-context');
|
||||
jest.mock('../render/canvas/canvas-renderer');
|
||||
|
||||
describe('html2canvas', () => {
|
||||
const element = {
|
||||
ownerDocument: {
|
||||
defaultView: {
|
||||
pageXOffset: 12,
|
||||
pageYOffset: 34
|
||||
}
|
||||
}
|
||||
} as HTMLElement;
|
||||
|
||||
it('should render with an element', async () => {
|
||||
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,
|
||||
height: 50,
|
||||
width: 200,
|
||||
x: 0,
|
||||
y: 0,
|
||||
canvas: undefined
|
||||
})
|
||||
);
|
||||
expect(DocumentCloner.destroy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should have transparent background with backgroundColor: null', async () => {
|
||||
await html2canvas(element, {backgroundColor: null});
|
||||
expect(CanvasRenderer).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
backgroundColor: COLORS.TRANSPARENT
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should use existing canvas when given as option', async () => {
|
||||
const canvas = {} as HTMLCanvasElement;
|
||||
await html2canvas(element, {canvas});
|
||||
expect(CanvasRenderer).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
canvas
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should not remove cloned window when removeContainer: false', async () => {
|
||||
DocumentCloner.destroy = jest.fn();
|
||||
await html2canvas(element, {removeContainer: false});
|
||||
expect(CanvasRenderer).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
backgroundColor: 0xffffffff,
|
||||
scale: 1,
|
||||
height: 50,
|
||||
width: 200,
|
||||
x: 0,
|
||||
y: 0,
|
||||
canvas: undefined
|
||||
})
|
||||
);
|
||||
expect(DocumentCloner.destroy).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
1
src/core/bitwise.ts
Normal file
1
src/core/bitwise.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const contains = (bit: number, value: number): boolean => (bit & value) !== 0;
|
||||
177
src/core/cache-storage.ts
Normal file
177
src/core/cache-storage.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import {FEATURES} from './features';
|
||||
import {Context} from './context';
|
||||
|
||||
export class CacheStorage {
|
||||
private static _link?: HTMLAnchorElement;
|
||||
private static _origin = 'about:blank';
|
||||
|
||||
static getOrigin(url: string): string {
|
||||
const link = CacheStorage._link;
|
||||
if (!link) {
|
||||
return 'about:blank';
|
||||
}
|
||||
|
||||
link.href = url;
|
||||
link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/
|
||||
return link.protocol + link.hostname + link.port;
|
||||
}
|
||||
|
||||
static isSameOrigin(src: string): boolean {
|
||||
return CacheStorage.getOrigin(src) === CacheStorage._origin;
|
||||
}
|
||||
|
||||
static setContext(window: Window): void {
|
||||
CacheStorage._link = window.document.createElement('a');
|
||||
CacheStorage._origin = CacheStorage.getOrigin(window.location.href);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ResourceOptions {
|
||||
imageTimeout: number;
|
||||
useCORS: boolean;
|
||||
allowTaint: boolean;
|
||||
proxy?: string;
|
||||
}
|
||||
|
||||
export class Cache {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private readonly _cache: {[key: string]: Promise<any>} = {};
|
||||
|
||||
constructor(private readonly context: Context, private readonly _options: ResourceOptions) {}
|
||||
|
||||
addImage(src: string): Promise<void> {
|
||||
const result = Promise.resolve();
|
||||
if (this.has(src)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (isBlobImage(src) || isRenderable(src)) {
|
||||
(this._cache[src] = this.loadImage(src)).catch(() => {
|
||||
// prevent unhandled rejection
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
match(src: string): Promise<any> {
|
||||
return this._cache[src];
|
||||
}
|
||||
|
||||
private async loadImage(key: string) {
|
||||
const isSameOrigin = CacheStorage.isSameOrigin(key);
|
||||
const useCORS =
|
||||
!isInlineImage(key) && this._options.useCORS === true && FEATURES.SUPPORT_CORS_IMAGES && !isSameOrigin;
|
||||
const useProxy =
|
||||
!isInlineImage(key) &&
|
||||
!isSameOrigin &&
|
||||
!isBlobImage(key) &&
|
||||
typeof this._options.proxy === 'string' &&
|
||||
FEATURES.SUPPORT_CORS_XHR &&
|
||||
!useCORS;
|
||||
if (
|
||||
!isSameOrigin &&
|
||||
this._options.allowTaint === false &&
|
||||
!isInlineImage(key) &&
|
||||
!isBlobImage(key) &&
|
||||
!useProxy &&
|
||||
!useCORS
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let src = key;
|
||||
if (useProxy) {
|
||||
src = await this.proxy(src);
|
||||
}
|
||||
|
||||
this.context.logger.debug(`Added image ${key.substring(0, 256)}`);
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
//ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
|
||||
if (isInlineBase64Image(src) || useCORS) {
|
||||
img.crossOrigin = 'anonymous';
|
||||
}
|
||||
img.src = src;
|
||||
if (img.complete === true) {
|
||||
// Inline XML images may fail to parse, throwing an Error later on
|
||||
setTimeout(() => resolve(img), 500);
|
||||
}
|
||||
if (this._options.imageTimeout > 0) {
|
||||
setTimeout(
|
||||
() => reject(`Timed out (${this._options.imageTimeout}ms) loading image`),
|
||||
this._options.imageTimeout
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private has(key: string): boolean {
|
||||
return typeof this._cache[key] !== 'undefined';
|
||||
}
|
||||
|
||||
keys(): Promise<string[]> {
|
||||
return Promise.resolve(Object.keys(this._cache));
|
||||
}
|
||||
|
||||
private proxy(src: string): Promise<string> {
|
||||
const proxy = this._options.proxy;
|
||||
|
||||
if (!proxy) {
|
||||
throw new Error('No proxy defined');
|
||||
}
|
||||
|
||||
const key = src.substring(0, 256);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const responseType = FEATURES.SUPPORT_RESPONSE_TYPE ? 'blob' : 'text';
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
if (responseType === 'text') {
|
||||
resolve(xhr.response);
|
||||
} else {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => resolve(reader.result as string), false);
|
||||
reader.addEventListener('error', (e) => reject(e), false);
|
||||
reader.readAsDataURL(xhr.response);
|
||||
}
|
||||
} else {
|
||||
reject(`Failed to proxy resource ${key} with status code ${xhr.status}`);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = reject;
|
||||
const queryString = proxy.indexOf('?') > -1 ? '&' : '?';
|
||||
xhr.open('GET', `${proxy}${queryString}url=${encodeURIComponent(src)}&responseType=${responseType}`);
|
||||
|
||||
if (responseType !== 'text' && xhr instanceof XMLHttpRequest) {
|
||||
xhr.responseType = responseType;
|
||||
}
|
||||
|
||||
if (this._options.imageTimeout) {
|
||||
const timeout = this._options.imageTimeout;
|
||||
xhr.timeout = timeout;
|
||||
xhr.ontimeout = () => reject(`Timed out (${timeout}ms) proxying ${key}`);
|
||||
}
|
||||
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const INLINE_SVG = /^data:image\/svg\+xml/i;
|
||||
const INLINE_BASE64 = /^data:image\/.*;base64,/i;
|
||||
const INLINE_IMG = /^data:image\/.*/i;
|
||||
|
||||
const isRenderable = (src: string): boolean => FEATURES.SUPPORT_SVG_DRAWING || !isSVG(src);
|
||||
const isInlineImage = (src: string): boolean => INLINE_IMG.test(src);
|
||||
const isInlineBase64Image = (src: string): boolean => INLINE_BASE64.test(src);
|
||||
const isBlobImage = (src: string): boolean => src.substr(0, 4) === 'blob';
|
||||
|
||||
const isSVG = (src: string): boolean => src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src);
|
||||
21
src/core/context.ts
Normal file
21
src/core/context.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {Logger} from './logger';
|
||||
import {Cache, ResourceOptions} from './cache-storage';
|
||||
import {Bounds} from '../css/layout/bounds';
|
||||
|
||||
export type ContextOptions = {
|
||||
logging: boolean;
|
||||
cache?: Cache;
|
||||
} & ResourceOptions;
|
||||
|
||||
export class Context {
|
||||
private readonly instanceName = `#${Context.instanceCount++}`;
|
||||
readonly logger: Logger;
|
||||
readonly cache: Cache;
|
||||
|
||||
private static instanceCount = 1;
|
||||
|
||||
constructor(options: ContextOptions, public windowBounds: Bounds) {
|
||||
this.logger = new Logger({id: this.instanceName, enabled: options.logging});
|
||||
this.cache = options.cache ?? new Cache(this, options);
|
||||
}
|
||||
}
|
||||
29
src/core/debugger.ts
Normal file
29
src/core/debugger.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
const elementDebuggerAttribute = 'data-html2canvas-debug';
|
||||
export const enum DebuggerType {
|
||||
NONE,
|
||||
ALL,
|
||||
CLONE,
|
||||
PARSE,
|
||||
RENDER
|
||||
}
|
||||
|
||||
const getElementDebugType = (element: Element): DebuggerType => {
|
||||
const attribute = element.getAttribute(elementDebuggerAttribute);
|
||||
switch (attribute) {
|
||||
case 'all':
|
||||
return DebuggerType.ALL;
|
||||
case 'clone':
|
||||
return DebuggerType.CLONE;
|
||||
case 'parse':
|
||||
return DebuggerType.PARSE;
|
||||
case 'render':
|
||||
return DebuggerType.RENDER;
|
||||
default:
|
||||
return DebuggerType.NONE;
|
||||
}
|
||||
};
|
||||
|
||||
export const isDebugging = (element: Element, type: Omit<DebuggerType, DebuggerType.NONE>): boolean => {
|
||||
const elementType = getElementDebugType(element);
|
||||
return elementType === DebuggerType.ALL || type === elementType;
|
||||
};
|
||||
222
src/core/features.ts
Normal file
222
src/core/features.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import {fromCodePoint, toCodePoints} from 'css-line-break';
|
||||
|
||||
const testRangeBounds = (document: Document) => {
|
||||
const TEST_HEIGHT = 123;
|
||||
|
||||
if (document.createRange) {
|
||||
const range = document.createRange();
|
||||
if (range.getBoundingClientRect) {
|
||||
const testElement = document.createElement('boundtest');
|
||||
testElement.style.height = `${TEST_HEIGHT}px`;
|
||||
testElement.style.display = 'block';
|
||||
document.body.appendChild(testElement);
|
||||
|
||||
range.selectNode(testElement);
|
||||
const rangeBounds = range.getBoundingClientRect();
|
||||
const rangeHeight = Math.round(rangeBounds.height);
|
||||
document.body.removeChild(testElement);
|
||||
if (rangeHeight === TEST_HEIGHT) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const testIOSLineBreak = (document: Document) => {
|
||||
const testElement = document.createElement('boundtest');
|
||||
testElement.style.width = '50px';
|
||||
testElement.style.display = 'block';
|
||||
testElement.style.fontSize = '12px';
|
||||
testElement.style.letterSpacing = '0px';
|
||||
testElement.style.wordSpacing = '0px';
|
||||
document.body.appendChild(testElement);
|
||||
const range = document.createRange();
|
||||
|
||||
testElement.innerHTML = typeof ''.repeat === 'function' ? '👨'.repeat(10) : '';
|
||||
|
||||
const node = testElement.firstChild as Text;
|
||||
|
||||
const textList = toCodePoints(node.data).map((i) => fromCodePoint(i));
|
||||
let offset = 0;
|
||||
let prev: DOMRect = {} as DOMRect;
|
||||
|
||||
// ios 13 does not handle range getBoundingClientRect line changes correctly #2177
|
||||
const supports = textList.every((text, i) => {
|
||||
range.setStart(node, offset);
|
||||
range.setEnd(node, offset + text.length);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
offset += text.length;
|
||||
const boundAhead = rect.x > prev.x || rect.y > prev.y;
|
||||
|
||||
prev = rect;
|
||||
if (i === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return boundAhead;
|
||||
});
|
||||
|
||||
document.body.removeChild(testElement);
|
||||
return supports;
|
||||
};
|
||||
|
||||
const testCORS = (): boolean => typeof new Image().crossOrigin !== 'undefined';
|
||||
|
||||
const testResponseType = (): boolean => typeof new XMLHttpRequest().responseType === 'string';
|
||||
|
||||
const testSVG = (document: Document): boolean => {
|
||||
const img = new Image();
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
return false;
|
||||
}
|
||||
|
||||
img.src = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'></svg>`;
|
||||
|
||||
try {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
canvas.toDataURL();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isGreenPixel = (data: Uint8ClampedArray): boolean =>
|
||||
data[0] === 0 && data[1] === 255 && data[2] === 0 && data[3] === 255;
|
||||
|
||||
const testForeignObject = (document: Document): Promise<boolean> => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const size = 100;
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
return Promise.reject(false);
|
||||
}
|
||||
ctx.fillStyle = 'rgb(0, 255, 0)';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
const img = new Image();
|
||||
const greenImageSrc = canvas.toDataURL();
|
||||
img.src = greenImageSrc;
|
||||
const svg = createForeignObjectSVG(size, size, 0, 0, img);
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
return loadSerializedSVG(svg)
|
||||
.then((img: HTMLImageElement) => {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const data = ctx.getImageData(0, 0, size, size).data;
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
const node = document.createElement('div');
|
||||
node.style.backgroundImage = `url(${greenImageSrc})`;
|
||||
node.style.height = `${size}px`;
|
||||
// Firefox 55 does not render inline <img /> tags
|
||||
return isGreenPixel(data)
|
||||
? loadSerializedSVG(createForeignObjectSVG(size, size, 0, 0, node))
|
||||
: Promise.reject(false);
|
||||
})
|
||||
.then((img: HTMLImageElement) => {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
// Edge does not render background-images
|
||||
return isGreenPixel(ctx.getImageData(0, 0, size, size).data);
|
||||
})
|
||||
.catch(() => false);
|
||||
};
|
||||
|
||||
export const createForeignObjectSVG = (
|
||||
width: number,
|
||||
height: number,
|
||||
x: number,
|
||||
y: number,
|
||||
node: Node
|
||||
): SVGForeignObjectElement => {
|
||||
const xmlns = 'http://www.w3.org/2000/svg';
|
||||
const svg = document.createElementNS(xmlns, 'svg');
|
||||
const foreignObject = document.createElementNS(xmlns, 'foreignObject');
|
||||
svg.setAttributeNS(null, 'width', width.toString());
|
||||
svg.setAttributeNS(null, 'height', height.toString());
|
||||
|
||||
foreignObject.setAttributeNS(null, 'width', '100%');
|
||||
foreignObject.setAttributeNS(null, 'height', '100%');
|
||||
foreignObject.setAttributeNS(null, 'x', x.toString());
|
||||
foreignObject.setAttributeNS(null, 'y', y.toString());
|
||||
foreignObject.setAttributeNS(null, 'externalResourcesRequired', 'true');
|
||||
svg.appendChild(foreignObject);
|
||||
|
||||
foreignObject.appendChild(node);
|
||||
|
||||
return svg;
|
||||
};
|
||||
|
||||
export const loadSerializedSVG = (svg: Node): Promise<HTMLImageElement> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
|
||||
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
|
||||
});
|
||||
};
|
||||
|
||||
export const FEATURES = {
|
||||
get SUPPORT_RANGE_BOUNDS(): boolean {
|
||||
'use strict';
|
||||
const value = testRangeBounds(document);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_RANGE_BOUNDS', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_WORD_BREAKING(): boolean {
|
||||
'use strict';
|
||||
const value = FEATURES.SUPPORT_RANGE_BOUNDS && testIOSLineBreak(document);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_WORD_BREAKING', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_SVG_DRAWING(): boolean {
|
||||
'use strict';
|
||||
const value = testSVG(document);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_SVG_DRAWING', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_FOREIGNOBJECT_DRAWING(): Promise<boolean> {
|
||||
'use strict';
|
||||
const value =
|
||||
typeof Array.from === 'function' && typeof window.fetch === 'function'
|
||||
? testForeignObject(document)
|
||||
: Promise.resolve(false);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_FOREIGNOBJECT_DRAWING', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_CORS_IMAGES(): boolean {
|
||||
'use strict';
|
||||
const value = testCORS();
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_CORS_IMAGES', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_RESPONSE_TYPE(): boolean {
|
||||
'use strict';
|
||||
const value = testResponseType();
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_RESPONSE_TYPE', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_CORS_XHR(): boolean {
|
||||
'use strict';
|
||||
const value = 'withCredentials' in new XMLHttpRequest();
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_NATIVE_TEXT_SEGMENTATION(): boolean {
|
||||
'use strict';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const value = !!(typeof Intl !== 'undefined' && (Intl as any).Segmenter);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_NATIVE_TEXT_SEGMENTATION', {value});
|
||||
return value;
|
||||
}
|
||||
};
|
||||
72
src/core/logger.ts
Normal file
72
src/core/logger.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
export interface LoggerOptions {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
static instances: {[key: string]: Logger} = {};
|
||||
|
||||
private readonly id: string;
|
||||
private readonly enabled: boolean;
|
||||
private readonly start: number;
|
||||
|
||||
constructor({id, enabled}: LoggerOptions) {
|
||||
this.id = id;
|
||||
this.enabled = enabled;
|
||||
this.start = Date.now();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
debug(...args: unknown[]): void {
|
||||
if (this.enabled) {
|
||||
// eslint-disable-next-line no-console
|
||||
if (typeof window !== 'undefined' && window.console && typeof console.debug === 'function') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug(this.id, `${this.getTime()}ms`, ...args);
|
||||
} else {
|
||||
this.info(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getTime(): number {
|
||||
return Date.now() - this.start;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
info(...args: unknown[]): void {
|
||||
if (this.enabled) {
|
||||
// eslint-disable-next-line no-console
|
||||
if (typeof window !== 'undefined' && window.console && typeof console.info === 'function') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info(this.id, `${this.getTime()}ms`, ...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// eslint-disable-next-line no-console
|
||||
if (typeof window !== 'undefined' && window.console && typeof console.error === 'function') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(this.id, `${this.getTime()}ms`, ...args);
|
||||
} else {
|
||||
this.info(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/core/util.ts
Normal file
1
src/core/util.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const SMALL_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||
49
src/css/IPropertyDescriptor.ts
Normal file
49
src/css/IPropertyDescriptor.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {CSSValue} from './syntax/parser';
|
||||
import {CSSTypes} from './types';
|
||||
import {Context} from '../core/context';
|
||||
|
||||
export const enum PropertyDescriptorParsingType {
|
||||
VALUE,
|
||||
LIST,
|
||||
IDENT_VALUE,
|
||||
TYPE_VALUE,
|
||||
TOKEN_VALUE
|
||||
}
|
||||
|
||||
export interface IPropertyDescriptor {
|
||||
name: string;
|
||||
type: PropertyDescriptorParsingType;
|
||||
initialValue: string;
|
||||
prefix: boolean;
|
||||
}
|
||||
|
||||
export interface IPropertyIdentValueDescriptor<T> extends IPropertyDescriptor {
|
||||
type: PropertyDescriptorParsingType.IDENT_VALUE;
|
||||
parse: (context: Context, token: string) => T;
|
||||
}
|
||||
|
||||
export interface IPropertyTypeValueDescriptor extends IPropertyDescriptor {
|
||||
type: PropertyDescriptorParsingType.TYPE_VALUE;
|
||||
format: CSSTypes;
|
||||
}
|
||||
|
||||
export interface IPropertyValueDescriptor<T> extends IPropertyDescriptor {
|
||||
type: PropertyDescriptorParsingType.VALUE;
|
||||
parse: (context: Context, token: CSSValue) => T;
|
||||
}
|
||||
|
||||
export interface IPropertyListDescriptor<T> extends IPropertyDescriptor {
|
||||
type: PropertyDescriptorParsingType.LIST;
|
||||
parse: (context: Context, tokens: CSSValue[]) => T;
|
||||
}
|
||||
|
||||
export interface IPropertyTokenValueDescriptor extends IPropertyDescriptor {
|
||||
type: PropertyDescriptorParsingType.TOKEN_VALUE;
|
||||
}
|
||||
|
||||
export type CSSPropertyDescriptor<T> =
|
||||
| IPropertyValueDescriptor<T>
|
||||
| IPropertyListDescriptor<T>
|
||||
| IPropertyIdentValueDescriptor<T>
|
||||
| IPropertyTypeValueDescriptor
|
||||
| IPropertyTokenValueDescriptor;
|
||||
7
src/css/ITypeDescriptor.ts
Normal file
7
src/css/ITypeDescriptor.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {CSSValue} from './syntax/parser';
|
||||
import {Context} from '../core/context';
|
||||
|
||||
export interface ITypeDescriptor<T> {
|
||||
name: string;
|
||||
parse: (context: Context, value: CSSValue) => T;
|
||||
}
|
||||
321
src/css/index.ts
Normal file
321
src/css/index.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
import {CSSPropertyDescriptor, PropertyDescriptorParsingType} from './IPropertyDescriptor';
|
||||
import {backgroundClip} from './property-descriptors/background-clip';
|
||||
import {backgroundColor} from './property-descriptors/background-color';
|
||||
import {backgroundImage} from './property-descriptors/background-image';
|
||||
import {backgroundOrigin} from './property-descriptors/background-origin';
|
||||
import {backgroundPosition} from './property-descriptors/background-position';
|
||||
import {backgroundRepeat} from './property-descriptors/background-repeat';
|
||||
import {backgroundSize} from './property-descriptors/background-size';
|
||||
import {
|
||||
borderBottomColor,
|
||||
borderLeftColor,
|
||||
borderRightColor,
|
||||
borderTopColor
|
||||
} from './property-descriptors/border-color';
|
||||
import {
|
||||
borderBottomLeftRadius,
|
||||
borderBottomRightRadius,
|
||||
borderTopLeftRadius,
|
||||
borderTopRightRadius
|
||||
} from './property-descriptors/border-radius';
|
||||
import {
|
||||
borderBottomStyle,
|
||||
borderLeftStyle,
|
||||
borderRightStyle,
|
||||
borderTopStyle
|
||||
} from './property-descriptors/border-style';
|
||||
import {
|
||||
borderBottomWidth,
|
||||
borderLeftWidth,
|
||||
borderRightWidth,
|
||||
borderTopWidth
|
||||
} from './property-descriptors/border-width';
|
||||
import {color} from './property-descriptors/color';
|
||||
import {direction} from './property-descriptors/direction';
|
||||
import {display, DISPLAY} from './property-descriptors/display';
|
||||
import {float, FLOAT} from './property-descriptors/float';
|
||||
import {letterSpacing} from './property-descriptors/letter-spacing';
|
||||
import {lineBreak} from './property-descriptors/line-break';
|
||||
import {lineHeight} from './property-descriptors/line-height';
|
||||
import {listStyleImage} from './property-descriptors/list-style-image';
|
||||
import {listStylePosition} from './property-descriptors/list-style-position';
|
||||
import {listStyleType} from './property-descriptors/list-style-type';
|
||||
import {marginBottom, marginLeft, marginRight, marginTop} from './property-descriptors/margin';
|
||||
import {overflow, OVERFLOW} from './property-descriptors/overflow';
|
||||
import {overflowWrap} from './property-descriptors/overflow-wrap';
|
||||
import {paddingBottom, paddingLeft, paddingRight, paddingTop} from './property-descriptors/padding';
|
||||
import {textAlign} from './property-descriptors/text-align';
|
||||
import {position, POSITION} from './property-descriptors/position';
|
||||
import {textShadow} from './property-descriptors/text-shadow';
|
||||
import {textTransform} from './property-descriptors/text-transform';
|
||||
import {transform} from './property-descriptors/transform';
|
||||
import {transformOrigin} from './property-descriptors/transform-origin';
|
||||
import {visibility, VISIBILITY} from './property-descriptors/visibility';
|
||||
import {wordBreak} from './property-descriptors/word-break';
|
||||
import {zIndex} from './property-descriptors/z-index';
|
||||
import {CSSValue, isIdentToken, Parser} from './syntax/parser';
|
||||
import {Tokenizer} from './syntax/tokenizer';
|
||||
import {Color, color as colorType, isTransparent} from './types/color';
|
||||
import {angle} from './types/angle';
|
||||
import {image} from './types/image';
|
||||
import {time} from './types/time';
|
||||
import {opacity} from './property-descriptors/opacity';
|
||||
import {textDecorationColor} from './property-descriptors/text-decoration-color';
|
||||
import {textDecorationLine} from './property-descriptors/text-decoration-line';
|
||||
import {isLengthPercentage, LengthPercentage, ZERO_LENGTH} from './types/length-percentage';
|
||||
import {fontFamily} from './property-descriptors/font-family';
|
||||
import {fontSize} from './property-descriptors/font-size';
|
||||
import {isLength} from './types/length';
|
||||
import {fontWeight} from './property-descriptors/font-weight';
|
||||
import {fontVariant} from './property-descriptors/font-variant';
|
||||
import {fontStyle} from './property-descriptors/font-style';
|
||||
import {contains} from '../core/bitwise';
|
||||
import {content} from './property-descriptors/content';
|
||||
import {counterIncrement} from './property-descriptors/counter-increment';
|
||||
import {counterReset} from './property-descriptors/counter-reset';
|
||||
import {duration} from './property-descriptors/duration';
|
||||
import {quotes} from './property-descriptors/quotes';
|
||||
import {boxShadow} from './property-descriptors/box-shadow';
|
||||
import {paintOrder} from './property-descriptors/paint-order';
|
||||
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 {
|
||||
animationDuration: ReturnType<typeof duration.parse>;
|
||||
backgroundClip: ReturnType<typeof backgroundClip.parse>;
|
||||
backgroundColor: Color;
|
||||
backgroundImage: ReturnType<typeof backgroundImage.parse>;
|
||||
backgroundOrigin: ReturnType<typeof backgroundOrigin.parse>;
|
||||
backgroundPosition: ReturnType<typeof backgroundPosition.parse>;
|
||||
backgroundRepeat: ReturnType<typeof backgroundRepeat.parse>;
|
||||
backgroundSize: ReturnType<typeof backgroundSize.parse>;
|
||||
borderTopColor: Color;
|
||||
borderRightColor: Color;
|
||||
borderBottomColor: Color;
|
||||
borderLeftColor: Color;
|
||||
borderTopLeftRadius: ReturnType<typeof borderTopLeftRadius.parse>;
|
||||
borderTopRightRadius: ReturnType<typeof borderTopRightRadius.parse>;
|
||||
borderBottomRightRadius: ReturnType<typeof borderBottomRightRadius.parse>;
|
||||
borderBottomLeftRadius: ReturnType<typeof borderBottomLeftRadius.parse>;
|
||||
borderTopStyle: ReturnType<typeof borderTopStyle.parse>;
|
||||
borderRightStyle: ReturnType<typeof borderRightStyle.parse>;
|
||||
borderBottomStyle: ReturnType<typeof borderBottomStyle.parse>;
|
||||
borderLeftStyle: ReturnType<typeof borderLeftStyle.parse>;
|
||||
borderTopWidth: ReturnType<typeof borderTopWidth.parse>;
|
||||
borderRightWidth: ReturnType<typeof borderRightWidth.parse>;
|
||||
borderBottomWidth: ReturnType<typeof borderBottomWidth.parse>;
|
||||
borderLeftWidth: ReturnType<typeof borderLeftWidth.parse>;
|
||||
boxShadow: ReturnType<typeof boxShadow.parse>;
|
||||
color: Color;
|
||||
direction: ReturnType<typeof direction.parse>;
|
||||
display: ReturnType<typeof display.parse>;
|
||||
float: ReturnType<typeof float.parse>;
|
||||
fontFamily: ReturnType<typeof fontFamily.parse>;
|
||||
fontSize: LengthPercentage;
|
||||
fontStyle: ReturnType<typeof fontStyle.parse>;
|
||||
fontVariant: ReturnType<typeof fontVariant.parse>;
|
||||
fontWeight: ReturnType<typeof fontWeight.parse>;
|
||||
letterSpacing: ReturnType<typeof letterSpacing.parse>;
|
||||
lineBreak: ReturnType<typeof lineBreak.parse>;
|
||||
lineHeight: CSSValue;
|
||||
listStyleImage: ReturnType<typeof listStyleImage.parse>;
|
||||
listStylePosition: ReturnType<typeof listStylePosition.parse>;
|
||||
listStyleType: ReturnType<typeof listStyleType.parse>;
|
||||
marginTop: CSSValue;
|
||||
marginRight: CSSValue;
|
||||
marginBottom: CSSValue;
|
||||
marginLeft: CSSValue;
|
||||
opacity: ReturnType<typeof opacity.parse>;
|
||||
overflowX: OVERFLOW;
|
||||
overflowY: OVERFLOW;
|
||||
overflowWrap: ReturnType<typeof overflowWrap.parse>;
|
||||
paddingTop: LengthPercentage;
|
||||
paddingRight: LengthPercentage;
|
||||
paddingBottom: LengthPercentage;
|
||||
paddingLeft: LengthPercentage;
|
||||
paintOrder: ReturnType<typeof paintOrder.parse>;
|
||||
position: ReturnType<typeof position.parse>;
|
||||
textAlign: ReturnType<typeof textAlign.parse>;
|
||||
textDecorationColor: Color;
|
||||
textDecorationLine: ReturnType<typeof textDecorationLine.parse>;
|
||||
textShadow: ReturnType<typeof textShadow.parse>;
|
||||
textTransform: ReturnType<typeof textTransform.parse>;
|
||||
transform: ReturnType<typeof transform.parse>;
|
||||
transformOrigin: ReturnType<typeof transformOrigin.parse>;
|
||||
visibility: ReturnType<typeof visibility.parse>;
|
||||
webkitTextStrokeColor: Color;
|
||||
webkitTextStrokeWidth: ReturnType<typeof webkitTextStrokeWidth.parse>;
|
||||
wordBreak: ReturnType<typeof wordBreak.parse>;
|
||||
zIndex: ReturnType<typeof zIndex.parse>;
|
||||
|
||||
constructor(context: Context, declaration: CSSStyleDeclaration) {
|
||||
this.animationDuration = parse(context, duration, declaration.animationDuration);
|
||||
this.backgroundClip = parse(context, backgroundClip, declaration.backgroundClip);
|
||||
this.backgroundColor = parse(context, backgroundColor, declaration.backgroundColor);
|
||||
this.backgroundImage = parse(context, backgroundImage, declaration.backgroundImage);
|
||||
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.direction = parse(context, direction, declaration.direction);
|
||||
this.display = parse(context, display, declaration.display);
|
||||
this.float = parse(context, float, declaration.cssFloat);
|
||||
this.fontFamily = parse(context, fontFamily, declaration.fontFamily);
|
||||
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(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(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 {
|
||||
return this.display > 0 && this.opacity > 0 && this.visibility === VISIBILITY.VISIBLE;
|
||||
}
|
||||
|
||||
isTransparent(): boolean {
|
||||
return isTransparent(this.backgroundColor);
|
||||
}
|
||||
|
||||
isTransformed(): boolean {
|
||||
return this.transform !== null;
|
||||
}
|
||||
|
||||
isPositioned(): boolean {
|
||||
return this.position !== POSITION.STATIC;
|
||||
}
|
||||
|
||||
isPositionedWithZIndex(): boolean {
|
||||
return this.isPositioned() && !this.zIndex.auto;
|
||||
}
|
||||
|
||||
isFloating(): boolean {
|
||||
return this.float !== FLOAT.NONE;
|
||||
}
|
||||
|
||||
isInlineLevel(): boolean {
|
||||
return (
|
||||
contains(this.display, DISPLAY.INLINE) ||
|
||||
contains(this.display, DISPLAY.INLINE_BLOCK) ||
|
||||
contains(this.display, DISPLAY.INLINE_FLEX) ||
|
||||
contains(this.display, DISPLAY.INLINE_GRID) ||
|
||||
contains(this.display, DISPLAY.INLINE_LIST_ITEM) ||
|
||||
contains(this.display, DISPLAY.INLINE_TABLE)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class CSSParsedPseudoDeclaration {
|
||||
content: ReturnType<typeof content.parse>;
|
||||
quotes: ReturnType<typeof quotes.parse>;
|
||||
|
||||
constructor(context: Context, declaration: CSSStyleDeclaration) {
|
||||
this.content = parse(context, content, declaration.content);
|
||||
this.quotes = parse(context, quotes, declaration.quotes);
|
||||
}
|
||||
}
|
||||
|
||||
export class CSSParsedCounterDeclaration {
|
||||
counterIncrement: ReturnType<typeof counterIncrement.parse>;
|
||||
counterReset: ReturnType<typeof counterReset.parse>;
|
||||
|
||||
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 = (context: Context, descriptor: CSSPropertyDescriptor<any>, style?: string | null) => {
|
||||
const tokenizer = new Tokenizer();
|
||||
const value = style !== null && typeof style !== 'undefined' ? style.toString() : descriptor.initialValue;
|
||||
tokenizer.write(value);
|
||||
const parser = new Parser(tokenizer.read());
|
||||
switch (descriptor.type) {
|
||||
case PropertyDescriptorParsingType.IDENT_VALUE:
|
||||
const token = parser.parseComponentValue();
|
||||
return descriptor.parse(context, isIdentToken(token) ? token.value : descriptor.initialValue);
|
||||
case PropertyDescriptorParsingType.VALUE:
|
||||
return descriptor.parse(context, parser.parseComponentValue());
|
||||
case PropertyDescriptorParsingType.LIST:
|
||||
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(context, parser.parseComponentValue());
|
||||
case 'color':
|
||||
return colorType.parse(context, parser.parseComponentValue());
|
||||
case 'image':
|
||||
return image.parse(context, parser.parseComponentValue());
|
||||
case 'length':
|
||||
const length = parser.parseComponentValue();
|
||||
return isLength(length) ? length : ZERO_LENGTH;
|
||||
case 'length-percentage':
|
||||
const value = parser.parseComponentValue();
|
||||
return isLengthPercentage(value) ? value : ZERO_LENGTH;
|
||||
case 'time':
|
||||
return time.parse(context, parser.parseComponentValue());
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
637
src/dom/document-cloner.ts
Normal file
637
src/dom/document-cloner.ts
Normal file
@@ -0,0 +1,637 @@
|
||||
import {Bounds} from '../css/layout/bounds';
|
||||
import {
|
||||
isBodyElement,
|
||||
isCanvasElement,
|
||||
isCustomElement,
|
||||
isElementNode,
|
||||
isHTMLElementNode,
|
||||
isIFrameElement,
|
||||
isImageElement,
|
||||
isScriptElement,
|
||||
isSelectElement,
|
||||
isSlotElement,
|
||||
isStyleElement,
|
||||
isSVGElementNode,
|
||||
isTextareaElement,
|
||||
isTextNode,
|
||||
isVideoElement
|
||||
} from './node-parser';
|
||||
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';
|
||||
import {DebuggerType, isDebugging} from '../core/debugger';
|
||||
|
||||
export interface CloneOptions {
|
||||
ignoreElements?: (element: Element) => boolean;
|
||||
onclone?: (document: Document, element: HTMLElement) => void;
|
||||
allowTaint?: boolean;
|
||||
}
|
||||
|
||||
export interface WindowOptions {
|
||||
scrollX: number;
|
||||
scrollY: number;
|
||||
windowWidth: number;
|
||||
windowHeight: number;
|
||||
}
|
||||
|
||||
export type CloneConfigurations = CloneOptions & {
|
||||
inlineImages: boolean;
|
||||
copyStyles: boolean;
|
||||
};
|
||||
|
||||
const IGNORE_ATTRIBUTE = 'data-html2canvas-ignore';
|
||||
|
||||
export class DocumentCloner {
|
||||
private readonly scrolledElements: [Element, number, number][];
|
||||
private readonly referenceElement: HTMLElement;
|
||||
clonedReferenceElement?: HTMLElement;
|
||||
private readonly documentElement: HTMLElement;
|
||||
private readonly counters: CounterState;
|
||||
private quoteDepth: number;
|
||||
|
||||
constructor(
|
||||
private readonly context: Context,
|
||||
element: HTMLElement,
|
||||
private readonly options: CloneConfigurations
|
||||
) {
|
||||
this.scrolledElements = [];
|
||||
this.referenceElement = element;
|
||||
this.counters = new CounterState();
|
||||
this.quoteDepth = 0;
|
||||
if (!element.ownerDocument) {
|
||||
throw new Error('Cloned element does not have an owner document');
|
||||
}
|
||||
|
||||
this.documentElement = this.cloneNode(element.ownerDocument.documentElement, false) as HTMLElement;
|
||||
}
|
||||
|
||||
toIFrame(ownerDocument: Document, windowSize: Bounds): Promise<HTMLIFrameElement> {
|
||||
const iframe: HTMLIFrameElement = createIFrameContainer(ownerDocument, windowSize);
|
||||
|
||||
if (!iframe.contentWindow) {
|
||||
return Promise.reject(`Unable to find iframe window`);
|
||||
}
|
||||
|
||||
const scrollX = (ownerDocument.defaultView as Window).pageXOffset;
|
||||
const scrollY = (ownerDocument.defaultView as Window).pageYOffset;
|
||||
|
||||
const cloneWindow = iframe.contentWindow;
|
||||
const documentClone: Document = cloneWindow.document;
|
||||
|
||||
/* Chrome doesn't detect relative background-images assigned in inline <style> sheets when fetched through getComputedStyle
|
||||
if window url is about:blank, we can assign the url to current by writing onto the document
|
||||
*/
|
||||
|
||||
const iframeLoad = iframeLoader(iframe).then(async () => {
|
||||
this.scrolledElements.forEach(restoreNodeScroll);
|
||||
if (cloneWindow) {
|
||||
cloneWindow.scrollTo(windowSize.left, windowSize.top);
|
||||
if (
|
||||
/(iPad|iPhone|iPod)/g.test(navigator.userAgent) &&
|
||||
(cloneWindow.scrollY !== windowSize.top || cloneWindow.scrollX !== windowSize.left)
|
||||
) {
|
||||
this.context.logger.warn('Unable to restore scroll position for cloned document');
|
||||
this.context.windowBounds = this.context.windowBounds.add(
|
||||
cloneWindow.scrollX - windowSize.left,
|
||||
cloneWindow.scrollY - windowSize.top,
|
||||
0,
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const onclone = this.options.onclone;
|
||||
|
||||
const referenceElement = this.clonedReferenceElement;
|
||||
|
||||
if (typeof referenceElement === 'undefined') {
|
||||
return Promise.reject(`Error finding the ${this.referenceElement.nodeName} in the cloned document`);
|
||||
}
|
||||
|
||||
if (documentClone.fonts && documentClone.fonts.ready) {
|
||||
await documentClone.fonts.ready;
|
||||
}
|
||||
|
||||
if (/(AppleWebKit)/g.test(navigator.userAgent)) {
|
||||
await imagesReady(documentClone);
|
||||
}
|
||||
|
||||
if (typeof onclone === 'function') {
|
||||
return Promise.resolve()
|
||||
.then(() => onclone(documentClone, referenceElement))
|
||||
.then(() => iframe);
|
||||
}
|
||||
|
||||
return iframe;
|
||||
});
|
||||
|
||||
documentClone.open();
|
||||
documentClone.write(`${serializeDoctype(document.doctype)}<html></html>`);
|
||||
// Chrome scrolls the parent document for some reason after the write to the cloned window???
|
||||
restoreOwnerScroll(this.referenceElement.ownerDocument, scrollX, scrollY);
|
||||
documentClone.replaceChild(documentClone.adoptNode(this.documentElement), documentClone.documentElement);
|
||||
documentClone.close();
|
||||
|
||||
return iframeLoad;
|
||||
}
|
||||
|
||||
createElementClone<T extends HTMLElement | SVGElement>(node: T): HTMLElement | SVGElement {
|
||||
if (isDebugging(node, DebuggerType.CLONE)) {
|
||||
debugger;
|
||||
}
|
||||
if (isCanvasElement(node)) {
|
||||
return this.createCanvasClone(node);
|
||||
}
|
||||
if (isVideoElement(node)) {
|
||||
return this.createVideoClone(node);
|
||||
}
|
||||
if (isStyleElement(node)) {
|
||||
return this.createStyleClone(node);
|
||||
}
|
||||
|
||||
const clone = node.cloneNode(false) as T;
|
||||
if (isImageElement(clone)) {
|
||||
if (isImageElement(node) && node.currentSrc && node.currentSrc !== node.src) {
|
||||
clone.src = node.currentSrc;
|
||||
clone.srcset = '';
|
||||
}
|
||||
|
||||
if (clone.loading === 'lazy') {
|
||||
clone.loading = 'eager';
|
||||
}
|
||||
}
|
||||
|
||||
if (isCustomElement(clone)) {
|
||||
return this.createCustomElementClone(clone);
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
createCustomElementClone(node: HTMLElement): HTMLElement {
|
||||
const clone = document.createElement('html2canvascustomelement');
|
||||
copyCSSStyles(node.style, clone);
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
createStyleClone(node: HTMLStyleElement): HTMLStyleElement {
|
||||
try {
|
||||
const sheet = node.sheet as CSSStyleSheet | undefined;
|
||||
if (sheet && sheet.cssRules) {
|
||||
const css: string = [].slice.call(sheet.cssRules, 0).reduce((css: string, rule: CSSRule) => {
|
||||
if (rule && typeof rule.cssText === 'string') {
|
||||
return css + rule.cssText;
|
||||
}
|
||||
return css;
|
||||
}, '');
|
||||
const style = node.cloneNode(false) as HTMLStyleElement;
|
||||
style.textContent = css;
|
||||
return style;
|
||||
}
|
||||
} catch (e) {
|
||||
// accessing node.sheet.cssRules throws a DOMException
|
||||
this.context.logger.error('Unable to access cssRules property', e);
|
||||
if (e.name !== 'SecurityError') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return node.cloneNode(false) as HTMLStyleElement;
|
||||
}
|
||||
|
||||
createCanvasClone(canvas: HTMLCanvasElement): HTMLImageElement | HTMLCanvasElement {
|
||||
if (this.options.inlineImages && canvas.ownerDocument) {
|
||||
const img = canvas.ownerDocument.createElement('img');
|
||||
try {
|
||||
img.src = canvas.toDataURL();
|
||||
return img;
|
||||
} catch (e) {
|
||||
this.context.logger.info(`Unable to inline canvas contents, canvas is tainted`, canvas);
|
||||
}
|
||||
}
|
||||
|
||||
const clonedCanvas = canvas.cloneNode(false) as HTMLCanvasElement;
|
||||
|
||||
try {
|
||||
clonedCanvas.width = canvas.width;
|
||||
clonedCanvas.height = canvas.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const clonedCtx = clonedCanvas.getContext('2d');
|
||||
if (clonedCtx) {
|
||||
if (!this.options.allowTaint && ctx) {
|
||||
clonedCtx.putImageData(ctx.getImageData(0, 0, canvas.width, canvas.height), 0, 0);
|
||||
} else {
|
||||
const gl = canvas.getContext('webgl2') ?? canvas.getContext('webgl');
|
||||
if (gl) {
|
||||
const attribs = gl.getContextAttributes();
|
||||
if (attribs?.preserveDrawingBuffer === false) {
|
||||
this.context.logger.warn(
|
||||
'Unable to clone WebGL context as it has preserveDrawingBuffer=false',
|
||||
canvas
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
clonedCtx.drawImage(canvas, 0, 0);
|
||||
}
|
||||
}
|
||||
return clonedCanvas;
|
||||
} catch (e) {
|
||||
this.context.logger.info(`Unable to clone canvas as it is tainted`, canvas);
|
||||
}
|
||||
|
||||
return clonedCanvas;
|
||||
}
|
||||
|
||||
createVideoClone(video: HTMLVideoElement): HTMLCanvasElement {
|
||||
const canvas = video.ownerDocument.createElement('canvas');
|
||||
|
||||
canvas.width = video.offsetWidth;
|
||||
canvas.height = video.offsetHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
try {
|
||||
if (ctx) {
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
if (!this.options.allowTaint) {
|
||||
ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
}
|
||||
return canvas;
|
||||
} catch (e) {
|
||||
this.context.logger.info(`Unable to clone video as it is tainted`, video);
|
||||
}
|
||||
|
||||
const blankCanvas = video.ownerDocument.createElement('canvas');
|
||||
|
||||
blankCanvas.width = video.offsetWidth;
|
||||
blankCanvas.height = video.offsetHeight;
|
||||
return blankCanvas;
|
||||
}
|
||||
|
||||
appendChildNode(clone: HTMLElement | SVGElement, child: Node, copyStyles: boolean): void {
|
||||
if (
|
||||
!isElementNode(child) ||
|
||||
(!isScriptElement(child) &&
|
||||
!child.hasAttribute(IGNORE_ATTRIBUTE) &&
|
||||
(typeof this.options.ignoreElements !== 'function' || !this.options.ignoreElements(child)))
|
||||
) {
|
||||
if (!this.options.copyStyles || !isElementNode(child) || !isStyleElement(child)) {
|
||||
clone.appendChild(this.cloneNode(child, copyStyles));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cloneChildNodes(node: Element, clone: HTMLElement | SVGElement, copyStyles: boolean): void {
|
||||
for (
|
||||
let child = node.shadowRoot ? node.shadowRoot.firstChild : node.firstChild;
|
||||
child;
|
||||
child = child.nextSibling
|
||||
) {
|
||||
if (isElementNode(child) && isSlotElement(child) && typeof child.assignedNodes === 'function') {
|
||||
const assignedNodes = child.assignedNodes() as ChildNode[];
|
||||
if (assignedNodes.length) {
|
||||
assignedNodes.forEach((assignedNode) => this.appendChildNode(clone, assignedNode, copyStyles));
|
||||
}
|
||||
} else {
|
||||
this.appendChildNode(clone, child, copyStyles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cloneNode(node: Node, copyStyles: boolean): Node {
|
||||
if (isTextNode(node)) {
|
||||
return document.createTextNode(node.data);
|
||||
}
|
||||
|
||||
if (!node.ownerDocument) {
|
||||
return node.cloneNode(false);
|
||||
}
|
||||
|
||||
const window = node.ownerDocument.defaultView;
|
||||
|
||||
if (window && isElementNode(node) && (isHTMLElementNode(node) || isSVGElementNode(node))) {
|
||||
const clone = this.createElementClone(node);
|
||||
clone.style.transitionProperty = 'none';
|
||||
|
||||
const style = window.getComputedStyle(node);
|
||||
const styleBefore = window.getComputedStyle(node, ':before');
|
||||
const styleAfter = window.getComputedStyle(node, ':after');
|
||||
|
||||
if (this.referenceElement === node && isHTMLElementNode(clone)) {
|
||||
this.clonedReferenceElement = clone;
|
||||
}
|
||||
if (isBodyElement(clone)) {
|
||||
createPseudoHideStyles(clone);
|
||||
}
|
||||
|
||||
const counters = this.counters.parse(new CSSParsedCounterDeclaration(this.context, style));
|
||||
const before = this.resolvePseudoContent(node, clone, styleBefore, PseudoElementType.BEFORE);
|
||||
|
||||
if (isCustomElement(node)) {
|
||||
copyStyles = true;
|
||||
}
|
||||
|
||||
if (!isVideoElement(node)) {
|
||||
this.cloneChildNodes(node, clone, copyStyles);
|
||||
}
|
||||
|
||||
if (before) {
|
||||
clone.insertBefore(before, clone.firstChild);
|
||||
}
|
||||
|
||||
const after = this.resolvePseudoContent(node, clone, styleAfter, PseudoElementType.AFTER);
|
||||
if (after) {
|
||||
clone.appendChild(after);
|
||||
}
|
||||
|
||||
this.counters.pop(counters);
|
||||
|
||||
if (
|
||||
(style && (this.options.copyStyles || isSVGElementNode(node)) && !isIFrameElement(node)) ||
|
||||
copyStyles
|
||||
) {
|
||||
copyCSSStyles(style, clone);
|
||||
}
|
||||
|
||||
if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
|
||||
this.scrolledElements.push([clone, node.scrollLeft, node.scrollTop]);
|
||||
}
|
||||
|
||||
if (
|
||||
(isTextareaElement(node) || isSelectElement(node)) &&
|
||||
(isTextareaElement(clone) || isSelectElement(clone))
|
||||
) {
|
||||
clone.value = node.value;
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
return node.cloneNode(false);
|
||||
}
|
||||
|
||||
resolvePseudoContent(
|
||||
node: Element,
|
||||
clone: Element,
|
||||
style: CSSStyleDeclaration,
|
||||
pseudoElt: PseudoElementType
|
||||
): HTMLElement | void {
|
||||
if (!style) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = style.content;
|
||||
const document = clone.ownerDocument;
|
||||
if (!document || !value || value === 'none' || value === '-moz-alt-content' || style.display === 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.counters.parse(new CSSParsedCounterDeclaration(this.context, style));
|
||||
const declaration = new CSSParsedPseudoDeclaration(this.context, style);
|
||||
|
||||
const anonymousReplacedElement = document.createElement('html2canvaspseudoelement');
|
||||
copyCSSStyles(style, anonymousReplacedElement);
|
||||
|
||||
declaration.content.forEach((token) => {
|
||||
if (token.type === TokenType.STRING_TOKEN) {
|
||||
anonymousReplacedElement.appendChild(document.createTextNode(token.value));
|
||||
} else if (token.type === TokenType.URL_TOKEN) {
|
||||
const img = document.createElement('img');
|
||||
img.src = token.value;
|
||||
img.style.opacity = '1';
|
||||
anonymousReplacedElement.appendChild(img);
|
||||
} else if (token.type === TokenType.FUNCTION) {
|
||||
if (token.name === 'attr') {
|
||||
const attr = token.values.filter(isIdentToken);
|
||||
if (attr.length) {
|
||||
anonymousReplacedElement.appendChild(
|
||||
document.createTextNode(node.getAttribute(attr[0].value) || '')
|
||||
);
|
||||
}
|
||||
} else if (token.name === 'counter') {
|
||||
const [counter, counterStyle] = token.values.filter(nonFunctionArgSeparator);
|
||||
if (counter && isIdentToken(counter)) {
|
||||
const counterState = this.counters.getCounterValue(counter.value);
|
||||
const counterType =
|
||||
counterStyle && isIdentToken(counterStyle)
|
||||
? listStyleType.parse(this.context, counterStyle.value)
|
||||
: LIST_STYLE_TYPE.DECIMAL;
|
||||
|
||||
anonymousReplacedElement.appendChild(
|
||||
document.createTextNode(createCounterText(counterState, counterType, false))
|
||||
);
|
||||
}
|
||||
} else if (token.name === 'counters') {
|
||||
const [counter, delim, counterStyle] = token.values.filter(nonFunctionArgSeparator);
|
||||
if (counter && isIdentToken(counter)) {
|
||||
const counterStates = this.counters.getCounterValues(counter.value);
|
||||
const counterType =
|
||||
counterStyle && isIdentToken(counterStyle)
|
||||
? listStyleType.parse(this.context, counterStyle.value)
|
||||
: LIST_STYLE_TYPE.DECIMAL;
|
||||
const separator = delim && delim.type === TokenType.STRING_TOKEN ? delim.value : '';
|
||||
const text = counterStates
|
||||
.map((value) => createCounterText(value, counterType, false))
|
||||
.join(separator);
|
||||
|
||||
anonymousReplacedElement.appendChild(document.createTextNode(text));
|
||||
}
|
||||
} else {
|
||||
// console.log('FUNCTION_TOKEN', token);
|
||||
}
|
||||
} else if (token.type === TokenType.IDENT_TOKEN) {
|
||||
switch (token.value) {
|
||||
case 'open-quote':
|
||||
anonymousReplacedElement.appendChild(
|
||||
document.createTextNode(getQuote(declaration.quotes, this.quoteDepth++, true))
|
||||
);
|
||||
break;
|
||||
case 'close-quote':
|
||||
anonymousReplacedElement.appendChild(
|
||||
document.createTextNode(getQuote(declaration.quotes, --this.quoteDepth, false))
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// safari doesn't parse string tokens correctly because of lack of quotes
|
||||
anonymousReplacedElement.appendChild(document.createTextNode(token.value));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
anonymousReplacedElement.className = `${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE} ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
|
||||
const newClassName =
|
||||
pseudoElt === PseudoElementType.BEFORE
|
||||
? ` ${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}`
|
||||
: ` ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
|
||||
|
||||
if (isSVGElementNode(clone)) {
|
||||
clone.className.baseValue += newClassName;
|
||||
} else {
|
||||
clone.className += newClassName;
|
||||
}
|
||||
|
||||
return anonymousReplacedElement;
|
||||
}
|
||||
|
||||
static destroy(container: HTMLIFrameElement): boolean {
|
||||
if (container.parentNode) {
|
||||
container.parentNode.removeChild(container);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
enum PseudoElementType {
|
||||
BEFORE,
|
||||
AFTER
|
||||
}
|
||||
|
||||
const createIFrameContainer = (ownerDocument: Document, bounds: Bounds): HTMLIFrameElement => {
|
||||
const cloneIframeContainer = ownerDocument.createElement('iframe');
|
||||
|
||||
cloneIframeContainer.className = 'html2canvas-container';
|
||||
cloneIframeContainer.style.visibility = 'hidden';
|
||||
cloneIframeContainer.style.position = 'fixed';
|
||||
cloneIframeContainer.style.left = '-10000px';
|
||||
cloneIframeContainer.style.top = '0px';
|
||||
cloneIframeContainer.style.border = '0';
|
||||
cloneIframeContainer.width = bounds.width.toString();
|
||||
cloneIframeContainer.height = bounds.height.toString();
|
||||
cloneIframeContainer.scrolling = 'no'; // ios won't scroll without it
|
||||
cloneIframeContainer.setAttribute(IGNORE_ATTRIBUTE, 'true');
|
||||
ownerDocument.body.appendChild(cloneIframeContainer);
|
||||
|
||||
return cloneIframeContainer;
|
||||
};
|
||||
|
||||
const imageReady = (img: HTMLImageElement): Promise<Event | void | string> => {
|
||||
return new Promise((resolve) => {
|
||||
if (img.complete) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
if (!img.src) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
img.onload = resolve;
|
||||
img.onerror = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
const imagesReady = (document: HTMLDocument): Promise<unknown[]> => {
|
||||
return Promise.all([].slice.call(document.images, 0).map(imageReady));
|
||||
};
|
||||
|
||||
const iframeLoader = (iframe: HTMLIFrameElement): Promise<HTMLIFrameElement> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cloneWindow = iframe.contentWindow;
|
||||
|
||||
if (!cloneWindow) {
|
||||
return reject(`No window assigned for iframe`);
|
||||
}
|
||||
|
||||
const documentClone = cloneWindow.document;
|
||||
|
||||
cloneWindow.onload = iframe.onload = () => {
|
||||
cloneWindow.onload = iframe.onload = null;
|
||||
const interval = setInterval(() => {
|
||||
if (documentClone.body.childNodes.length > 0 && documentClone.readyState === 'complete') {
|
||||
clearInterval(interval);
|
||||
resolve(iframe);
|
||||
}
|
||||
}, 50);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const ignoredStyleProperties = [
|
||||
'all', // #2476
|
||||
'd', // #2483
|
||||
'content' // Safari shows pseudoelements if content is set
|
||||
];
|
||||
|
||||
export const copyCSSStyles = <T extends HTMLElement | SVGElement>(style: CSSStyleDeclaration, target: T): T => {
|
||||
// Edge does not provide value for cssText
|
||||
for (let i = style.length - 1; i >= 0; i--) {
|
||||
const property = style.item(i);
|
||||
if (ignoredStyleProperties.indexOf(property) === -1) {
|
||||
target.style.setProperty(property, style.getPropertyValue(property));
|
||||
}
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
const serializeDoctype = (doctype?: DocumentType | null): string => {
|
||||
let str = '';
|
||||
if (doctype) {
|
||||
str += '<!DOCTYPE ';
|
||||
if (doctype.name) {
|
||||
str += doctype.name;
|
||||
}
|
||||
|
||||
if (doctype.internalSubset) {
|
||||
str += doctype.internalSubset;
|
||||
}
|
||||
|
||||
if (doctype.publicId) {
|
||||
str += `"${doctype.publicId}"`;
|
||||
}
|
||||
|
||||
if (doctype.systemId) {
|
||||
str += `"${doctype.systemId}"`;
|
||||
}
|
||||
|
||||
str += '>';
|
||||
}
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
const restoreOwnerScroll = (ownerDocument: Document | null, x: number, y: number) => {
|
||||
if (
|
||||
ownerDocument &&
|
||||
ownerDocument.defaultView &&
|
||||
(x !== ownerDocument.defaultView.pageXOffset || y !== ownerDocument.defaultView.pageYOffset)
|
||||
) {
|
||||
ownerDocument.defaultView.scrollTo(x, y);
|
||||
}
|
||||
};
|
||||
|
||||
const restoreNodeScroll = ([element, x, y]: [HTMLElement, number, number]) => {
|
||||
element.scrollLeft = x;
|
||||
element.scrollTop = y;
|
||||
};
|
||||
|
||||
const PSEUDO_BEFORE = ':before';
|
||||
const PSEUDO_AFTER = ':after';
|
||||
const PSEUDO_HIDE_ELEMENT_CLASS_BEFORE = '___html2canvas___pseudoelement_before';
|
||||
const PSEUDO_HIDE_ELEMENT_CLASS_AFTER = '___html2canvas___pseudoelement_after';
|
||||
|
||||
const PSEUDO_HIDE_ELEMENT_STYLE = `{
|
||||
content: "" !important;
|
||||
display: none !important;
|
||||
}`;
|
||||
|
||||
const createPseudoHideStyles = (body: HTMLElement) => {
|
||||
createStyles(
|
||||
body,
|
||||
`.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE}
|
||||
.${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}`
|
||||
);
|
||||
};
|
||||
|
||||
const createStyles = (body: HTMLElement, styles: string) => {
|
||||
const document = body.ownerDocument;
|
||||
if (document) {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = styles;
|
||||
body.appendChild(style);
|
||||
}
|
||||
};
|
||||
46
src/dom/element-container.ts
Normal file
46
src/dom/element-container.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
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';
|
||||
import {DebuggerType, isDebugging} from '../core/debugger';
|
||||
|
||||
export const enum FLAGS {
|
||||
CREATES_STACKING_CONTEXT = 1 << 1,
|
||||
CREATES_REAL_STACKING_CONTEXT = 1 << 2,
|
||||
IS_LIST_OWNER = 1 << 3,
|
||||
DEBUG_RENDER = 1 << 4
|
||||
}
|
||||
|
||||
export class ElementContainer {
|
||||
readonly styles: CSSParsedDeclaration;
|
||||
readonly textNodes: TextContainer[] = [];
|
||||
readonly elements: ElementContainer[] = [];
|
||||
bounds: Bounds;
|
||||
flags = 0;
|
||||
|
||||
constructor(protected readonly context: Context, element: Element) {
|
||||
if (isDebugging(element, DebuggerType.PARSE)) {
|
||||
debugger;
|
||||
}
|
||||
|
||||
this.styles = new CSSParsedDeclaration(context, window.getComputedStyle(element, null));
|
||||
|
||||
if (isHTMLElementNode(element)) {
|
||||
if (this.styles.animationDuration.some((duration) => duration > 0)) {
|
||||
element.style.animationDuration = '0s';
|
||||
}
|
||||
|
||||
if (this.styles.transform !== null) {
|
||||
// getBoundingClientRect takes transforms into account
|
||||
element.style.transform = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
this.bounds = parseBounds(this.context, element);
|
||||
|
||||
if (isDebugging(element, DebuggerType.RENDER)) {
|
||||
this.flags |= FLAGS.DEBUG_RENDER;
|
||||
}
|
||||
}
|
||||
}
|
||||
136
src/dom/node-parser.ts
Normal file
136
src/dom/node-parser.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import {CSSParsedDeclaration} from '../css';
|
||||
import {ElementContainer, FLAGS} from './element-container';
|
||||
import {TextContainer} from './text-container';
|
||||
import {ImageElementContainer} from './replaced-elements/image-element-container';
|
||||
import {CanvasElementContainer} from './replaced-elements/canvas-element-container';
|
||||
import {SVGElementContainer} from './replaced-elements/svg-element-container';
|
||||
import {LIElementContainer} from './elements/li-element-container';
|
||||
import {OLElementContainer} from './elements/ol-element-container';
|
||||
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 = (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(context, childNode, parent.styles));
|
||||
} else if (isElementNode(childNode)) {
|
||||
if (isSlotElement(childNode) && childNode.assignedNodes) {
|
||||
childNode.assignedNodes().forEach((childNode) => parseNodeTree(context, childNode, parent, root));
|
||||
} else {
|
||||
const container = createContainer(context, childNode);
|
||||
if (container.styles.isVisible()) {
|
||||
if (createsRealStackingContext(childNode, container, root)) {
|
||||
container.flags |= FLAGS.CREATES_REAL_STACKING_CONTEXT;
|
||||
} else if (createsStackingContext(container.styles)) {
|
||||
container.flags |= FLAGS.CREATES_STACKING_CONTEXT;
|
||||
}
|
||||
|
||||
if (LIST_OWNERS.indexOf(childNode.tagName) !== -1) {
|
||||
container.flags |= FLAGS.IS_LIST_OWNER;
|
||||
}
|
||||
|
||||
parent.elements.push(container);
|
||||
childNode.slot;
|
||||
if (childNode.shadowRoot) {
|
||||
parseNodeTree(context, childNode.shadowRoot, container, root);
|
||||
} else if (
|
||||
!isTextareaElement(childNode) &&
|
||||
!isSVGElement(childNode) &&
|
||||
!isSelectElement(childNode)
|
||||
) {
|
||||
parseNodeTree(context, childNode, container, root);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createContainer = (context: Context, element: Element): ElementContainer => {
|
||||
if (isImageElement(element)) {
|
||||
return new ImageElementContainer(context, element);
|
||||
}
|
||||
|
||||
if (isCanvasElement(element)) {
|
||||
return new CanvasElementContainer(context, element);
|
||||
}
|
||||
|
||||
if (isSVGElement(element)) {
|
||||
return new SVGElementContainer(context, element);
|
||||
}
|
||||
|
||||
if (isLIElement(element)) {
|
||||
return new LIElementContainer(context, element);
|
||||
}
|
||||
|
||||
if (isOLElement(element)) {
|
||||
return new OLElementContainer(context, element);
|
||||
}
|
||||
|
||||
if (isInputElement(element)) {
|
||||
return new InputElementContainer(context, element);
|
||||
}
|
||||
|
||||
if (isSelectElement(element)) {
|
||||
return new SelectElementContainer(context, element);
|
||||
}
|
||||
|
||||
if (isTextareaElement(element)) {
|
||||
return new TextareaElementContainer(context, element);
|
||||
}
|
||||
|
||||
if (isIFrameElement(element)) {
|
||||
return new IFrameElementContainer(context, element);
|
||||
}
|
||||
|
||||
return new ElementContainer(context, element);
|
||||
};
|
||||
|
||||
export const parseTree = (context: Context, element: HTMLElement): ElementContainer => {
|
||||
const container = createContainer(context, element);
|
||||
container.flags |= FLAGS.CREATES_REAL_STACKING_CONTEXT;
|
||||
parseNodeTree(context, element, container, container);
|
||||
return container;
|
||||
};
|
||||
|
||||
const createsRealStackingContext = (node: Element, container: ElementContainer, root: ElementContainer): boolean => {
|
||||
return (
|
||||
container.styles.isPositionedWithZIndex() ||
|
||||
container.styles.opacity < 1 ||
|
||||
container.styles.isTransformed() ||
|
||||
(isBodyElement(node) && root.styles.isTransparent())
|
||||
);
|
||||
};
|
||||
|
||||
const createsStackingContext = (styles: CSSParsedDeclaration): boolean => styles.isPositioned() || styles.isFloating();
|
||||
|
||||
export const isTextNode = (node: Node): node is Text => node.nodeType === Node.TEXT_NODE;
|
||||
export const isElementNode = (node: Node): node is Element => node.nodeType === Node.ELEMENT_NODE;
|
||||
export const isHTMLElementNode = (node: Node): node is HTMLElement =>
|
||||
isElementNode(node) && typeof (node as HTMLElement).style !== 'undefined' && !isSVGElementNode(node);
|
||||
export const isSVGElementNode = (element: Element): element is SVGElement =>
|
||||
typeof (element as SVGElement).className === 'object';
|
||||
export const isLIElement = (node: Element): node is HTMLLIElement => node.tagName === 'LI';
|
||||
export const isOLElement = (node: Element): node is HTMLOListElement => node.tagName === 'OL';
|
||||
export const isInputElement = (node: Element): node is HTMLInputElement => node.tagName === 'INPUT';
|
||||
export const isHTMLElement = (node: Element): node is HTMLHtmlElement => node.tagName === 'HTML';
|
||||
export const isSVGElement = (node: Element): node is SVGSVGElement => node.tagName === 'svg';
|
||||
export const isBodyElement = (node: Element): node is HTMLBodyElement => node.tagName === 'BODY';
|
||||
export const isCanvasElement = (node: Element): node is HTMLCanvasElement => node.tagName === 'CANVAS';
|
||||
export const isVideoElement = (node: Element): node is HTMLVideoElement => node.tagName === 'VIDEO';
|
||||
export const isImageElement = (node: Element): node is HTMLImageElement => node.tagName === 'IMG';
|
||||
export const isIFrameElement = (node: Element): node is HTMLIFrameElement => node.tagName === 'IFRAME';
|
||||
export const isStyleElement = (node: Element): node is HTMLStyleElement => node.tagName === 'STYLE';
|
||||
export const isScriptElement = (node: Element): node is HTMLScriptElement => node.tagName === 'SCRIPT';
|
||||
export const isTextareaElement = (node: Element): node is HTMLTextAreaElement => node.tagName === 'TEXTAREA';
|
||||
export const isSelectElement = (node: Element): node is HTMLSelectElement => node.tagName === 'SELECT';
|
||||
export const isSlotElement = (node: Element): node is HTMLSlotElement => node.tagName === 'SLOT';
|
||||
// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
|
||||
export const isCustomElement = (node: Element): node is HTMLElement => node.tagName.indexOf('-') > 0;
|
||||
37
src/dom/text-container.ts
Normal file
37
src/dom/text-container.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
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(context: Context, node: Text, styles: CSSParsedDeclaration) {
|
||||
this.text = transform(node.data, styles.textTransform);
|
||||
this.textBounds = parseTextBounds(context, this.text, styles, node);
|
||||
}
|
||||
}
|
||||
|
||||
const transform = (text: string, transform: TEXT_TRANSFORM) => {
|
||||
switch (transform) {
|
||||
case TEXT_TRANSFORM.LOWERCASE:
|
||||
return text.toLowerCase();
|
||||
case TEXT_TRANSFORM.CAPITALIZE:
|
||||
return text.replace(CAPITALIZE, capitalize);
|
||||
case TEXT_TRANSFORM.UPPERCASE:
|
||||
return text.toUpperCase();
|
||||
default:
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
const CAPITALIZE = /(^|\s|:|-|\(|\))([a-z])/g;
|
||||
|
||||
const capitalize = (m: string, p1: string, p2: string) => {
|
||||
if (m.length > 0) {
|
||||
return p1 + p2.toUpperCase();
|
||||
}
|
||||
|
||||
return m;
|
||||
};
|
||||
295
src/render/background.ts
Normal file
295
src/render/background.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
import {Bounds} from '../css/layout/bounds';
|
||||
import {BACKGROUND_ORIGIN} from '../css/property-descriptors/background-origin';
|
||||
import {ElementContainer} from '../dom/element-container';
|
||||
import {BACKGROUND_SIZE, BackgroundSizeInfo} from '../css/property-descriptors/background-size';
|
||||
import {Vector} from './vector';
|
||||
import {BACKGROUND_REPEAT} from '../css/property-descriptors/background-repeat';
|
||||
import {getAbsoluteValue, getAbsoluteValueForTuple, isLengthPercentage} from '../css/types/length-percentage';
|
||||
import {CSSValue, isIdentToken} from '../css/syntax/parser';
|
||||
import {contentBox, paddingBox} from './box-sizing';
|
||||
import {Path} from './path';
|
||||
import {BACKGROUND_CLIP} from '../css/property-descriptors/background-clip';
|
||||
|
||||
export const calculateBackgroundPositioningArea = (
|
||||
backgroundOrigin: BACKGROUND_ORIGIN,
|
||||
element: ElementContainer
|
||||
): Bounds => {
|
||||
if (backgroundOrigin === BACKGROUND_ORIGIN.BORDER_BOX) {
|
||||
return element.bounds;
|
||||
}
|
||||
|
||||
if (backgroundOrigin === BACKGROUND_ORIGIN.CONTENT_BOX) {
|
||||
return contentBox(element);
|
||||
}
|
||||
|
||||
return paddingBox(element);
|
||||
};
|
||||
|
||||
export const calculateBackgroundPaintingArea = (backgroundClip: BACKGROUND_CLIP, element: ElementContainer): Bounds => {
|
||||
if (backgroundClip === BACKGROUND_CLIP.BORDER_BOX) {
|
||||
return element.bounds;
|
||||
}
|
||||
|
||||
if (backgroundClip === BACKGROUND_CLIP.CONTENT_BOX) {
|
||||
return contentBox(element);
|
||||
}
|
||||
|
||||
return paddingBox(element);
|
||||
};
|
||||
|
||||
export const calculateBackgroundRendering = (
|
||||
container: ElementContainer,
|
||||
index: number,
|
||||
intrinsicSize: [number | null, number | null, number | null]
|
||||
): [Path[], number, number, number, number] => {
|
||||
const backgroundPositioningArea = calculateBackgroundPositioningArea(
|
||||
getBackgroundValueForIndex(container.styles.backgroundOrigin, index),
|
||||
container
|
||||
);
|
||||
|
||||
const backgroundPaintingArea = calculateBackgroundPaintingArea(
|
||||
getBackgroundValueForIndex(container.styles.backgroundClip, index),
|
||||
container
|
||||
);
|
||||
|
||||
const backgroundImageSize = calculateBackgroundSize(
|
||||
getBackgroundValueForIndex(container.styles.backgroundSize, index),
|
||||
intrinsicSize,
|
||||
backgroundPositioningArea
|
||||
);
|
||||
|
||||
const [sizeWidth, sizeHeight] = backgroundImageSize;
|
||||
|
||||
const position = getAbsoluteValueForTuple(
|
||||
getBackgroundValueForIndex(container.styles.backgroundPosition, index),
|
||||
backgroundPositioningArea.width - sizeWidth,
|
||||
backgroundPositioningArea.height - sizeHeight
|
||||
);
|
||||
|
||||
const path = calculateBackgroundRepeatPath(
|
||||
getBackgroundValueForIndex(container.styles.backgroundRepeat, index),
|
||||
position,
|
||||
backgroundImageSize,
|
||||
backgroundPositioningArea,
|
||||
backgroundPaintingArea
|
||||
);
|
||||
|
||||
const offsetX = Math.round(backgroundPositioningArea.left + position[0]);
|
||||
const offsetY = Math.round(backgroundPositioningArea.top + position[1]);
|
||||
|
||||
return [path, offsetX, offsetY, sizeWidth, sizeHeight];
|
||||
};
|
||||
|
||||
export const isAuto = (token: CSSValue): boolean => isIdentToken(token) && token.value === BACKGROUND_SIZE.AUTO;
|
||||
|
||||
const hasIntrinsicValue = (value: number | null): value is number => typeof value === 'number';
|
||||
|
||||
export const calculateBackgroundSize = (
|
||||
size: BackgroundSizeInfo[],
|
||||
[intrinsicWidth, intrinsicHeight, intrinsicProportion]: [number | null, number | null, number | null],
|
||||
bounds: Bounds
|
||||
): [number, number] => {
|
||||
const [first, second] = size;
|
||||
|
||||
if (!first) {
|
||||
return [0, 0];
|
||||
}
|
||||
|
||||
if (isLengthPercentage(first) && second && isLengthPercentage(second)) {
|
||||
return [getAbsoluteValue(first, bounds.width), getAbsoluteValue(second, bounds.height)];
|
||||
}
|
||||
|
||||
const hasIntrinsicProportion = hasIntrinsicValue(intrinsicProportion);
|
||||
|
||||
if (isIdentToken(first) && (first.value === BACKGROUND_SIZE.CONTAIN || first.value === BACKGROUND_SIZE.COVER)) {
|
||||
if (hasIntrinsicValue(intrinsicProportion)) {
|
||||
const targetRatio = bounds.width / bounds.height;
|
||||
|
||||
return targetRatio < intrinsicProportion !== (first.value === BACKGROUND_SIZE.COVER)
|
||||
? [bounds.width, bounds.width / intrinsicProportion]
|
||||
: [bounds.height * intrinsicProportion, bounds.height];
|
||||
}
|
||||
|
||||
return [bounds.width, bounds.height];
|
||||
}
|
||||
|
||||
const hasIntrinsicWidth = hasIntrinsicValue(intrinsicWidth);
|
||||
const hasIntrinsicHeight = hasIntrinsicValue(intrinsicHeight);
|
||||
const hasIntrinsicDimensions = hasIntrinsicWidth || hasIntrinsicHeight;
|
||||
|
||||
// If the background-size is auto or auto auto:
|
||||
if (isAuto(first) && (!second || isAuto(second))) {
|
||||
// If the image has both horizontal and vertical intrinsic dimensions, it's rendered at that size.
|
||||
if (hasIntrinsicWidth && hasIntrinsicHeight) {
|
||||
return [intrinsicWidth as number, intrinsicHeight as number];
|
||||
}
|
||||
|
||||
// If the image has no intrinsic dimensions and has no intrinsic proportions,
|
||||
// it's rendered at the size of the background positioning area.
|
||||
|
||||
if (!hasIntrinsicProportion && !hasIntrinsicDimensions) {
|
||||
return [bounds.width, bounds.height];
|
||||
}
|
||||
|
||||
// TODO If the image has no intrinsic dimensions but has intrinsic proportions, it's rendered as if contain had been specified instead.
|
||||
|
||||
// If the image has only one intrinsic dimension and has intrinsic proportions, it's rendered at the size corresponding to that one dimension.
|
||||
// The other dimension is computed using the specified dimension and the intrinsic proportions.
|
||||
if (hasIntrinsicDimensions && hasIntrinsicProportion) {
|
||||
const width = hasIntrinsicWidth
|
||||
? (intrinsicWidth as number)
|
||||
: (intrinsicHeight as number) * (intrinsicProportion as number);
|
||||
const height = hasIntrinsicHeight
|
||||
? (intrinsicHeight as number)
|
||||
: (intrinsicWidth as number) / (intrinsicProportion as number);
|
||||
return [width, height];
|
||||
}
|
||||
|
||||
// If the image has only one intrinsic dimension but has no intrinsic proportions,
|
||||
// it's rendered using the specified dimension and the other dimension of the background positioning area.
|
||||
const width = hasIntrinsicWidth ? (intrinsicWidth as number) : bounds.width;
|
||||
const height = hasIntrinsicHeight ? (intrinsicHeight as number) : bounds.height;
|
||||
return [width, height];
|
||||
}
|
||||
|
||||
// If the image has intrinsic proportions, it's stretched to the specified dimension.
|
||||
// The unspecified dimension is computed using the specified dimension and the intrinsic proportions.
|
||||
if (hasIntrinsicProportion) {
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
if (isLengthPercentage(first)) {
|
||||
width = getAbsoluteValue(first, bounds.width);
|
||||
} else if (isLengthPercentage(second)) {
|
||||
height = getAbsoluteValue(second, bounds.height);
|
||||
}
|
||||
|
||||
if (isAuto(first)) {
|
||||
width = height * (intrinsicProportion as number);
|
||||
} else if (!second || isAuto(second)) {
|
||||
height = width / (intrinsicProportion as number);
|
||||
}
|
||||
|
||||
return [width, height];
|
||||
}
|
||||
|
||||
// If the image has no intrinsic proportions, it's stretched to the specified dimension.
|
||||
// The unspecified dimension is computed using the image's corresponding intrinsic dimension,
|
||||
// if there is one. If there is no such intrinsic dimension,
|
||||
// it becomes the corresponding dimension of the background positioning area.
|
||||
|
||||
let width = null;
|
||||
let height = null;
|
||||
|
||||
if (isLengthPercentage(first)) {
|
||||
width = getAbsoluteValue(first, bounds.width);
|
||||
} else if (second && isLengthPercentage(second)) {
|
||||
height = getAbsoluteValue(second, bounds.height);
|
||||
}
|
||||
|
||||
if (width !== null && (!second || isAuto(second))) {
|
||||
height =
|
||||
hasIntrinsicWidth && hasIntrinsicHeight
|
||||
? (width / (intrinsicWidth as number)) * (intrinsicHeight as number)
|
||||
: bounds.height;
|
||||
}
|
||||
|
||||
if (height !== null && isAuto(first)) {
|
||||
width =
|
||||
hasIntrinsicWidth && hasIntrinsicHeight
|
||||
? (height / (intrinsicHeight as number)) * (intrinsicWidth as number)
|
||||
: bounds.width;
|
||||
}
|
||||
|
||||
if (width !== null && height !== null) {
|
||||
return [width, height];
|
||||
}
|
||||
|
||||
throw new Error(`Unable to calculate background-size for element`);
|
||||
};
|
||||
|
||||
export const getBackgroundValueForIndex = <T>(values: T[], index: number): T => {
|
||||
const value = values[index];
|
||||
if (typeof value === 'undefined') {
|
||||
return values[0];
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export const calculateBackgroundRepeatPath = (
|
||||
repeat: BACKGROUND_REPEAT,
|
||||
[x, y]: [number, number],
|
||||
[width, height]: [number, number],
|
||||
backgroundPositioningArea: Bounds,
|
||||
backgroundPaintingArea: Bounds
|
||||
): [Vector, Vector, Vector, Vector] => {
|
||||
switch (repeat) {
|
||||
case BACKGROUND_REPEAT.REPEAT_X:
|
||||
return [
|
||||
new Vector(Math.round(backgroundPositioningArea.left), Math.round(backgroundPositioningArea.top + y)),
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left + backgroundPositioningArea.width),
|
||||
Math.round(backgroundPositioningArea.top + y)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left + backgroundPositioningArea.width),
|
||||
Math.round(height + backgroundPositioningArea.top + y)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left),
|
||||
Math.round(height + backgroundPositioningArea.top + y)
|
||||
)
|
||||
];
|
||||
case BACKGROUND_REPEAT.REPEAT_Y:
|
||||
return [
|
||||
new Vector(Math.round(backgroundPositioningArea.left + x), Math.round(backgroundPositioningArea.top)),
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left + x + width),
|
||||
Math.round(backgroundPositioningArea.top)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left + x + width),
|
||||
Math.round(backgroundPositioningArea.height + backgroundPositioningArea.top)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left + x),
|
||||
Math.round(backgroundPositioningArea.height + backgroundPositioningArea.top)
|
||||
)
|
||||
];
|
||||
case BACKGROUND_REPEAT.NO_REPEAT:
|
||||
return [
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left + x),
|
||||
Math.round(backgroundPositioningArea.top + y)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left + x + width),
|
||||
Math.round(backgroundPositioningArea.top + y)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left + x + width),
|
||||
Math.round(backgroundPositioningArea.top + y + height)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(backgroundPositioningArea.left + x),
|
||||
Math.round(backgroundPositioningArea.top + y + height)
|
||||
)
|
||||
];
|
||||
default:
|
||||
return [
|
||||
new Vector(Math.round(backgroundPaintingArea.left), Math.round(backgroundPaintingArea.top)),
|
||||
new Vector(
|
||||
Math.round(backgroundPaintingArea.left + backgroundPaintingArea.width),
|
||||
Math.round(backgroundPaintingArea.top)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(backgroundPaintingArea.left + backgroundPaintingArea.width),
|
||||
Math.round(backgroundPaintingArea.height + backgroundPaintingArea.top)
|
||||
),
|
||||
new Vector(
|
||||
Math.round(backgroundPaintingArea.left),
|
||||
Math.round(backgroundPaintingArea.height + backgroundPaintingArea.top)
|
||||
)
|
||||
];
|
||||
}
|
||||
};
|
||||
47
src/render/bezier-curve.ts
Normal file
47
src/render/bezier-curve.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import {Vector} from './vector';
|
||||
import {IPath, PathType, Path} from './path';
|
||||
|
||||
const lerp = (a: Vector, b: Vector, t: number): Vector => {
|
||||
return new Vector(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
|
||||
};
|
||||
|
||||
export class BezierCurve implements IPath {
|
||||
type: PathType;
|
||||
start: Vector;
|
||||
startControl: Vector;
|
||||
endControl: Vector;
|
||||
end: Vector;
|
||||
|
||||
constructor(start: Vector, startControl: Vector, endControl: Vector, end: Vector) {
|
||||
this.type = PathType.BEZIER_CURVE;
|
||||
this.start = start;
|
||||
this.startControl = startControl;
|
||||
this.endControl = endControl;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
subdivide(t: number, firstHalf: boolean): BezierCurve {
|
||||
const ab = lerp(this.start, this.startControl, t);
|
||||
const bc = lerp(this.startControl, this.endControl, t);
|
||||
const cd = lerp(this.endControl, this.end, t);
|
||||
const abbc = lerp(ab, bc, t);
|
||||
const bccd = lerp(bc, cd, t);
|
||||
const dest = lerp(abbc, bccd, t);
|
||||
return firstHalf ? new BezierCurve(this.start, ab, abbc, dest) : new BezierCurve(dest, bccd, cd, this.end);
|
||||
}
|
||||
|
||||
add(deltaX: number, deltaY: number): BezierCurve {
|
||||
return new BezierCurve(
|
||||
this.start.add(deltaX, deltaY),
|
||||
this.startControl.add(deltaX, deltaY),
|
||||
this.endControl.add(deltaX, deltaY),
|
||||
this.end.add(deltaX, deltaY)
|
||||
);
|
||||
}
|
||||
|
||||
reverse(): BezierCurve {
|
||||
return new BezierCurve(this.end, this.endControl, this.startControl, this.start);
|
||||
}
|
||||
}
|
||||
|
||||
export const isBezierCurve = (path: Path): path is BezierCurve => path.type === PathType.BEZIER_CURVE;
|
||||
165
src/render/border.ts
Normal file
165
src/render/border.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import {Path} from './path';
|
||||
import {BoundCurves} from './bound-curves';
|
||||
import {isBezierCurve} from './bezier-curve';
|
||||
|
||||
export const parsePathForBorder = (curves: BoundCurves, borderSide: number): Path[] => {
|
||||
switch (borderSide) {
|
||||
case 0:
|
||||
return createPathFromCurves(
|
||||
curves.topLeftBorderBox,
|
||||
curves.topLeftPaddingBox,
|
||||
curves.topRightBorderBox,
|
||||
curves.topRightPaddingBox
|
||||
);
|
||||
case 1:
|
||||
return createPathFromCurves(
|
||||
curves.topRightBorderBox,
|
||||
curves.topRightPaddingBox,
|
||||
curves.bottomRightBorderBox,
|
||||
curves.bottomRightPaddingBox
|
||||
);
|
||||
case 2:
|
||||
return createPathFromCurves(
|
||||
curves.bottomRightBorderBox,
|
||||
curves.bottomRightPaddingBox,
|
||||
curves.bottomLeftBorderBox,
|
||||
curves.bottomLeftPaddingBox
|
||||
);
|
||||
case 3:
|
||||
default:
|
||||
return createPathFromCurves(
|
||||
curves.bottomLeftBorderBox,
|
||||
curves.bottomLeftPaddingBox,
|
||||
curves.topLeftBorderBox,
|
||||
curves.topLeftPaddingBox
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const parsePathForBorderDoubleOuter = (curves: BoundCurves, borderSide: number): Path[] => {
|
||||
switch (borderSide) {
|
||||
case 0:
|
||||
return createPathFromCurves(
|
||||
curves.topLeftBorderBox,
|
||||
curves.topLeftBorderDoubleOuterBox,
|
||||
curves.topRightBorderBox,
|
||||
curves.topRightBorderDoubleOuterBox
|
||||
);
|
||||
case 1:
|
||||
return createPathFromCurves(
|
||||
curves.topRightBorderBox,
|
||||
curves.topRightBorderDoubleOuterBox,
|
||||
curves.bottomRightBorderBox,
|
||||
curves.bottomRightBorderDoubleOuterBox
|
||||
);
|
||||
case 2:
|
||||
return createPathFromCurves(
|
||||
curves.bottomRightBorderBox,
|
||||
curves.bottomRightBorderDoubleOuterBox,
|
||||
curves.bottomLeftBorderBox,
|
||||
curves.bottomLeftBorderDoubleOuterBox
|
||||
);
|
||||
case 3:
|
||||
default:
|
||||
return createPathFromCurves(
|
||||
curves.bottomLeftBorderBox,
|
||||
curves.bottomLeftBorderDoubleOuterBox,
|
||||
curves.topLeftBorderBox,
|
||||
curves.topLeftBorderDoubleOuterBox
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const parsePathForBorderDoubleInner = (curves: BoundCurves, borderSide: number): Path[] => {
|
||||
switch (borderSide) {
|
||||
case 0:
|
||||
return createPathFromCurves(
|
||||
curves.topLeftBorderDoubleInnerBox,
|
||||
curves.topLeftPaddingBox,
|
||||
curves.topRightBorderDoubleInnerBox,
|
||||
curves.topRightPaddingBox
|
||||
);
|
||||
case 1:
|
||||
return createPathFromCurves(
|
||||
curves.topRightBorderDoubleInnerBox,
|
||||
curves.topRightPaddingBox,
|
||||
curves.bottomRightBorderDoubleInnerBox,
|
||||
curves.bottomRightPaddingBox
|
||||
);
|
||||
case 2:
|
||||
return createPathFromCurves(
|
||||
curves.bottomRightBorderDoubleInnerBox,
|
||||
curves.bottomRightPaddingBox,
|
||||
curves.bottomLeftBorderDoubleInnerBox,
|
||||
curves.bottomLeftPaddingBox
|
||||
);
|
||||
case 3:
|
||||
default:
|
||||
return createPathFromCurves(
|
||||
curves.bottomLeftBorderDoubleInnerBox,
|
||||
curves.bottomLeftPaddingBox,
|
||||
curves.topLeftBorderDoubleInnerBox,
|
||||
curves.topLeftPaddingBox
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const parsePathForBorderStroke = (curves: BoundCurves, borderSide: number): Path[] => {
|
||||
switch (borderSide) {
|
||||
case 0:
|
||||
return createStrokePathFromCurves(curves.topLeftBorderStroke, curves.topRightBorderStroke);
|
||||
case 1:
|
||||
return createStrokePathFromCurves(curves.topRightBorderStroke, curves.bottomRightBorderStroke);
|
||||
case 2:
|
||||
return createStrokePathFromCurves(curves.bottomRightBorderStroke, curves.bottomLeftBorderStroke);
|
||||
case 3:
|
||||
default:
|
||||
return createStrokePathFromCurves(curves.bottomLeftBorderStroke, curves.topLeftBorderStroke);
|
||||
}
|
||||
};
|
||||
|
||||
const createStrokePathFromCurves = (outer1: Path, outer2: Path): Path[] => {
|
||||
const path = [];
|
||||
if (isBezierCurve(outer1)) {
|
||||
path.push(outer1.subdivide(0.5, false));
|
||||
} else {
|
||||
path.push(outer1);
|
||||
}
|
||||
|
||||
if (isBezierCurve(outer2)) {
|
||||
path.push(outer2.subdivide(0.5, true));
|
||||
} else {
|
||||
path.push(outer2);
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
const createPathFromCurves = (outer1: Path, inner1: Path, outer2: Path, inner2: Path): Path[] => {
|
||||
const path = [];
|
||||
if (isBezierCurve(outer1)) {
|
||||
path.push(outer1.subdivide(0.5, false));
|
||||
} else {
|
||||
path.push(outer1);
|
||||
}
|
||||
|
||||
if (isBezierCurve(outer2)) {
|
||||
path.push(outer2.subdivide(0.5, true));
|
||||
} else {
|
||||
path.push(outer2);
|
||||
}
|
||||
|
||||
if (isBezierCurve(inner2)) {
|
||||
path.push(inner2.subdivide(0.5, true).reverse());
|
||||
} else {
|
||||
path.push(inner2);
|
||||
}
|
||||
|
||||
if (isBezierCurve(inner1)) {
|
||||
path.push(inner1.subdivide(0.5, false).reverse());
|
||||
} else {
|
||||
path.push(inner1);
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
388
src/render/bound-curves.ts
Normal file
388
src/render/bound-curves.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
import {ElementContainer} from '../dom/element-container';
|
||||
import {getAbsoluteValue, getAbsoluteValueForTuple} from '../css/types/length-percentage';
|
||||
import {Vector} from './vector';
|
||||
import {BezierCurve} from './bezier-curve';
|
||||
import {Path} from './path';
|
||||
|
||||
export class BoundCurves {
|
||||
readonly topLeftBorderDoubleOuterBox: Path;
|
||||
readonly topRightBorderDoubleOuterBox: Path;
|
||||
readonly bottomRightBorderDoubleOuterBox: Path;
|
||||
readonly bottomLeftBorderDoubleOuterBox: Path;
|
||||
readonly topLeftBorderDoubleInnerBox: Path;
|
||||
readonly topRightBorderDoubleInnerBox: Path;
|
||||
readonly bottomRightBorderDoubleInnerBox: Path;
|
||||
readonly bottomLeftBorderDoubleInnerBox: Path;
|
||||
readonly topLeftBorderStroke: Path;
|
||||
readonly topRightBorderStroke: Path;
|
||||
readonly bottomRightBorderStroke: Path;
|
||||
readonly bottomLeftBorderStroke: Path;
|
||||
readonly topLeftBorderBox: Path;
|
||||
readonly topRightBorderBox: Path;
|
||||
readonly bottomRightBorderBox: Path;
|
||||
readonly bottomLeftBorderBox: Path;
|
||||
readonly topLeftPaddingBox: Path;
|
||||
readonly topRightPaddingBox: Path;
|
||||
readonly bottomRightPaddingBox: Path;
|
||||
readonly bottomLeftPaddingBox: Path;
|
||||
readonly topLeftContentBox: Path;
|
||||
readonly topRightContentBox: Path;
|
||||
readonly bottomRightContentBox: Path;
|
||||
readonly bottomLeftContentBox: Path;
|
||||
|
||||
constructor(element: ElementContainer) {
|
||||
const styles = element.styles;
|
||||
const bounds = element.bounds;
|
||||
|
||||
let [tlh, tlv] = getAbsoluteValueForTuple(styles.borderTopLeftRadius, bounds.width, bounds.height);
|
||||
let [trh, trv] = getAbsoluteValueForTuple(styles.borderTopRightRadius, bounds.width, bounds.height);
|
||||
let [brh, brv] = getAbsoluteValueForTuple(styles.borderBottomRightRadius, bounds.width, bounds.height);
|
||||
let [blh, blv] = getAbsoluteValueForTuple(styles.borderBottomLeftRadius, bounds.width, bounds.height);
|
||||
|
||||
const factors = [];
|
||||
factors.push((tlh + trh) / bounds.width);
|
||||
factors.push((blh + brh) / bounds.width);
|
||||
factors.push((tlv + blv) / bounds.height);
|
||||
factors.push((trv + brv) / bounds.height);
|
||||
const maxFactor = Math.max(...factors);
|
||||
|
||||
if (maxFactor > 1) {
|
||||
tlh /= maxFactor;
|
||||
tlv /= maxFactor;
|
||||
trh /= maxFactor;
|
||||
trv /= maxFactor;
|
||||
brh /= maxFactor;
|
||||
brv /= maxFactor;
|
||||
blh /= maxFactor;
|
||||
blv /= maxFactor;
|
||||
}
|
||||
|
||||
const topWidth = bounds.width - trh;
|
||||
const rightHeight = bounds.height - brv;
|
||||
const bottomWidth = bounds.width - brh;
|
||||
const leftHeight = bounds.height - blv;
|
||||
|
||||
const borderTopWidth = styles.borderTopWidth;
|
||||
const borderRightWidth = styles.borderRightWidth;
|
||||
const borderBottomWidth = styles.borderBottomWidth;
|
||||
const borderLeftWidth = styles.borderLeftWidth;
|
||||
|
||||
const paddingTop = getAbsoluteValue(styles.paddingTop, element.bounds.width);
|
||||
const paddingRight = getAbsoluteValue(styles.paddingRight, element.bounds.width);
|
||||
const paddingBottom = getAbsoluteValue(styles.paddingBottom, element.bounds.width);
|
||||
const paddingLeft = getAbsoluteValue(styles.paddingLeft, element.bounds.width);
|
||||
|
||||
this.topLeftBorderDoubleOuterBox =
|
||||
tlh > 0 || tlv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + borderLeftWidth / 3,
|
||||
bounds.top + borderTopWidth / 3,
|
||||
tlh - borderLeftWidth / 3,
|
||||
tlv - borderTopWidth / 3,
|
||||
CORNER.TOP_LEFT
|
||||
)
|
||||
: new Vector(bounds.left + borderLeftWidth / 3, bounds.top + borderTopWidth / 3);
|
||||
this.topRightBorderDoubleOuterBox =
|
||||
tlh > 0 || tlv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + topWidth,
|
||||
bounds.top + borderTopWidth / 3,
|
||||
trh - borderRightWidth / 3,
|
||||
trv - borderTopWidth / 3,
|
||||
CORNER.TOP_RIGHT
|
||||
)
|
||||
: new Vector(bounds.left + bounds.width - borderRightWidth / 3, bounds.top + borderTopWidth / 3);
|
||||
this.bottomRightBorderDoubleOuterBox =
|
||||
brh > 0 || brv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + bottomWidth,
|
||||
bounds.top + rightHeight,
|
||||
brh - borderRightWidth / 3,
|
||||
brv - borderBottomWidth / 3,
|
||||
CORNER.BOTTOM_RIGHT
|
||||
)
|
||||
: new Vector(
|
||||
bounds.left + bounds.width - borderRightWidth / 3,
|
||||
bounds.top + bounds.height - borderBottomWidth / 3
|
||||
);
|
||||
this.bottomLeftBorderDoubleOuterBox =
|
||||
blh > 0 || blv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + borderLeftWidth / 3,
|
||||
bounds.top + leftHeight,
|
||||
blh - borderLeftWidth / 3,
|
||||
blv - borderBottomWidth / 3,
|
||||
CORNER.BOTTOM_LEFT
|
||||
)
|
||||
: new Vector(bounds.left + borderLeftWidth / 3, bounds.top + bounds.height - borderBottomWidth / 3);
|
||||
this.topLeftBorderDoubleInnerBox =
|
||||
tlh > 0 || tlv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + (borderLeftWidth * 2) / 3,
|
||||
bounds.top + (borderTopWidth * 2) / 3,
|
||||
tlh - (borderLeftWidth * 2) / 3,
|
||||
tlv - (borderTopWidth * 2) / 3,
|
||||
CORNER.TOP_LEFT
|
||||
)
|
||||
: new Vector(bounds.left + (borderLeftWidth * 2) / 3, bounds.top + (borderTopWidth * 2) / 3);
|
||||
this.topRightBorderDoubleInnerBox =
|
||||
tlh > 0 || tlv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + topWidth,
|
||||
bounds.top + (borderTopWidth * 2) / 3,
|
||||
trh - (borderRightWidth * 2) / 3,
|
||||
trv - (borderTopWidth * 2) / 3,
|
||||
CORNER.TOP_RIGHT
|
||||
)
|
||||
: new Vector(
|
||||
bounds.left + bounds.width - (borderRightWidth * 2) / 3,
|
||||
bounds.top + (borderTopWidth * 2) / 3
|
||||
);
|
||||
this.bottomRightBorderDoubleInnerBox =
|
||||
brh > 0 || brv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + bottomWidth,
|
||||
bounds.top + rightHeight,
|
||||
brh - (borderRightWidth * 2) / 3,
|
||||
brv - (borderBottomWidth * 2) / 3,
|
||||
CORNER.BOTTOM_RIGHT
|
||||
)
|
||||
: new Vector(
|
||||
bounds.left + bounds.width - (borderRightWidth * 2) / 3,
|
||||
bounds.top + bounds.height - (borderBottomWidth * 2) / 3
|
||||
);
|
||||
this.bottomLeftBorderDoubleInnerBox =
|
||||
blh > 0 || blv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + (borderLeftWidth * 2) / 3,
|
||||
bounds.top + leftHeight,
|
||||
blh - (borderLeftWidth * 2) / 3,
|
||||
blv - (borderBottomWidth * 2) / 3,
|
||||
CORNER.BOTTOM_LEFT
|
||||
)
|
||||
: new Vector(
|
||||
bounds.left + (borderLeftWidth * 2) / 3,
|
||||
bounds.top + bounds.height - (borderBottomWidth * 2) / 3
|
||||
);
|
||||
this.topLeftBorderStroke =
|
||||
tlh > 0 || tlv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + borderLeftWidth / 2,
|
||||
bounds.top + borderTopWidth / 2,
|
||||
tlh - borderLeftWidth / 2,
|
||||
tlv - borderTopWidth / 2,
|
||||
CORNER.TOP_LEFT
|
||||
)
|
||||
: new Vector(bounds.left + borderLeftWidth / 2, bounds.top + borderTopWidth / 2);
|
||||
this.topRightBorderStroke =
|
||||
tlh > 0 || tlv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + topWidth,
|
||||
bounds.top + borderTopWidth / 2,
|
||||
trh - borderRightWidth / 2,
|
||||
trv - borderTopWidth / 2,
|
||||
CORNER.TOP_RIGHT
|
||||
)
|
||||
: new Vector(bounds.left + bounds.width - borderRightWidth / 2, bounds.top + borderTopWidth / 2);
|
||||
this.bottomRightBorderStroke =
|
||||
brh > 0 || brv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + bottomWidth,
|
||||
bounds.top + rightHeight,
|
||||
brh - borderRightWidth / 2,
|
||||
brv - borderBottomWidth / 2,
|
||||
CORNER.BOTTOM_RIGHT
|
||||
)
|
||||
: new Vector(
|
||||
bounds.left + bounds.width - borderRightWidth / 2,
|
||||
bounds.top + bounds.height - borderBottomWidth / 2
|
||||
);
|
||||
this.bottomLeftBorderStroke =
|
||||
blh > 0 || blv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + borderLeftWidth / 2,
|
||||
bounds.top + leftHeight,
|
||||
blh - borderLeftWidth / 2,
|
||||
blv - borderBottomWidth / 2,
|
||||
CORNER.BOTTOM_LEFT
|
||||
)
|
||||
: new Vector(bounds.left + borderLeftWidth / 2, bounds.top + bounds.height - borderBottomWidth / 2);
|
||||
this.topLeftBorderBox =
|
||||
tlh > 0 || tlv > 0
|
||||
? getCurvePoints(bounds.left, bounds.top, tlh, tlv, CORNER.TOP_LEFT)
|
||||
: new Vector(bounds.left, bounds.top);
|
||||
this.topRightBorderBox =
|
||||
trh > 0 || trv > 0
|
||||
? getCurvePoints(bounds.left + topWidth, bounds.top, trh, trv, CORNER.TOP_RIGHT)
|
||||
: new Vector(bounds.left + bounds.width, bounds.top);
|
||||
this.bottomRightBorderBox =
|
||||
brh > 0 || brv > 0
|
||||
? getCurvePoints(bounds.left + bottomWidth, bounds.top + rightHeight, brh, brv, CORNER.BOTTOM_RIGHT)
|
||||
: new Vector(bounds.left + bounds.width, bounds.top + bounds.height);
|
||||
this.bottomLeftBorderBox =
|
||||
blh > 0 || blv > 0
|
||||
? getCurvePoints(bounds.left, bounds.top + leftHeight, blh, blv, CORNER.BOTTOM_LEFT)
|
||||
: new Vector(bounds.left, bounds.top + bounds.height);
|
||||
this.topLeftPaddingBox =
|
||||
tlh > 0 || tlv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + borderLeftWidth,
|
||||
bounds.top + borderTopWidth,
|
||||
Math.max(0, tlh - borderLeftWidth),
|
||||
Math.max(0, tlv - borderTopWidth),
|
||||
CORNER.TOP_LEFT
|
||||
)
|
||||
: new Vector(bounds.left + borderLeftWidth, bounds.top + borderTopWidth);
|
||||
this.topRightPaddingBox =
|
||||
trh > 0 || trv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + Math.min(topWidth, bounds.width - borderRightWidth),
|
||||
bounds.top + borderTopWidth,
|
||||
topWidth > bounds.width + borderRightWidth ? 0 : Math.max(0, trh - borderRightWidth),
|
||||
Math.max(0, trv - borderTopWidth),
|
||||
CORNER.TOP_RIGHT
|
||||
)
|
||||
: new Vector(bounds.left + bounds.width - borderRightWidth, bounds.top + borderTopWidth);
|
||||
this.bottomRightPaddingBox =
|
||||
brh > 0 || brv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + Math.min(bottomWidth, bounds.width - borderLeftWidth),
|
||||
bounds.top + Math.min(rightHeight, bounds.height - borderBottomWidth),
|
||||
Math.max(0, brh - borderRightWidth),
|
||||
Math.max(0, brv - borderBottomWidth),
|
||||
CORNER.BOTTOM_RIGHT
|
||||
)
|
||||
: new Vector(
|
||||
bounds.left + bounds.width - borderRightWidth,
|
||||
bounds.top + bounds.height - borderBottomWidth
|
||||
);
|
||||
this.bottomLeftPaddingBox =
|
||||
blh > 0 || blv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + borderLeftWidth,
|
||||
bounds.top + Math.min(leftHeight, bounds.height - borderBottomWidth),
|
||||
Math.max(0, blh - borderLeftWidth),
|
||||
Math.max(0, blv - borderBottomWidth),
|
||||
CORNER.BOTTOM_LEFT
|
||||
)
|
||||
: new Vector(bounds.left + borderLeftWidth, bounds.top + bounds.height - borderBottomWidth);
|
||||
this.topLeftContentBox =
|
||||
tlh > 0 || tlv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + borderLeftWidth + paddingLeft,
|
||||
bounds.top + borderTopWidth + paddingTop,
|
||||
Math.max(0, tlh - (borderLeftWidth + paddingLeft)),
|
||||
Math.max(0, tlv - (borderTopWidth + paddingTop)),
|
||||
CORNER.TOP_LEFT
|
||||
)
|
||||
: new Vector(bounds.left + borderLeftWidth + paddingLeft, bounds.top + borderTopWidth + paddingTop);
|
||||
this.topRightContentBox =
|
||||
trh > 0 || trv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + Math.min(topWidth, bounds.width + borderLeftWidth + paddingLeft),
|
||||
bounds.top + borderTopWidth + paddingTop,
|
||||
topWidth > bounds.width + borderLeftWidth + paddingLeft ? 0 : trh - borderLeftWidth + paddingLeft,
|
||||
trv - (borderTopWidth + paddingTop),
|
||||
CORNER.TOP_RIGHT
|
||||
)
|
||||
: new Vector(
|
||||
bounds.left + bounds.width - (borderRightWidth + paddingRight),
|
||||
bounds.top + borderTopWidth + paddingTop
|
||||
);
|
||||
this.bottomRightContentBox =
|
||||
brh > 0 || brv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + Math.min(bottomWidth, bounds.width - (borderLeftWidth + paddingLeft)),
|
||||
bounds.top + Math.min(rightHeight, bounds.height + borderTopWidth + paddingTop),
|
||||
Math.max(0, brh - (borderRightWidth + paddingRight)),
|
||||
brv - (borderBottomWidth + paddingBottom),
|
||||
CORNER.BOTTOM_RIGHT
|
||||
)
|
||||
: new Vector(
|
||||
bounds.left + bounds.width - (borderRightWidth + paddingRight),
|
||||
bounds.top + bounds.height - (borderBottomWidth + paddingBottom)
|
||||
);
|
||||
this.bottomLeftContentBox =
|
||||
blh > 0 || blv > 0
|
||||
? getCurvePoints(
|
||||
bounds.left + borderLeftWidth + paddingLeft,
|
||||
bounds.top + leftHeight,
|
||||
Math.max(0, blh - (borderLeftWidth + paddingLeft)),
|
||||
blv - (borderBottomWidth + paddingBottom),
|
||||
CORNER.BOTTOM_LEFT
|
||||
)
|
||||
: new Vector(
|
||||
bounds.left + borderLeftWidth + paddingLeft,
|
||||
bounds.top + bounds.height - (borderBottomWidth + paddingBottom)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum CORNER {
|
||||
TOP_LEFT = 0,
|
||||
TOP_RIGHT = 1,
|
||||
BOTTOM_RIGHT = 2,
|
||||
BOTTOM_LEFT = 3
|
||||
}
|
||||
|
||||
const getCurvePoints = (x: number, y: number, r1: number, r2: number, position: CORNER): BezierCurve => {
|
||||
const kappa = 4 * ((Math.sqrt(2) - 1) / 3);
|
||||
const ox = r1 * kappa; // control point offset horizontal
|
||||
const oy = r2 * kappa; // control point offset vertical
|
||||
const xm = x + r1; // x-middle
|
||||
const ym = y + r2; // y-middle
|
||||
|
||||
switch (position) {
|
||||
case CORNER.TOP_LEFT:
|
||||
return new BezierCurve(
|
||||
new Vector(x, ym),
|
||||
new Vector(x, ym - oy),
|
||||
new Vector(xm - ox, y),
|
||||
new Vector(xm, y)
|
||||
);
|
||||
case CORNER.TOP_RIGHT:
|
||||
return new BezierCurve(
|
||||
new Vector(x, y),
|
||||
new Vector(x + ox, y),
|
||||
new Vector(xm, ym - oy),
|
||||
new Vector(xm, ym)
|
||||
);
|
||||
case CORNER.BOTTOM_RIGHT:
|
||||
return new BezierCurve(
|
||||
new Vector(xm, y),
|
||||
new Vector(xm, y + oy),
|
||||
new Vector(x + ox, ym),
|
||||
new Vector(x, ym)
|
||||
);
|
||||
case CORNER.BOTTOM_LEFT:
|
||||
default:
|
||||
return new BezierCurve(
|
||||
new Vector(xm, ym),
|
||||
new Vector(xm - ox, ym),
|
||||
new Vector(x, y + oy),
|
||||
new Vector(x, y)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateBorderBoxPath = (curves: BoundCurves): Path[] => {
|
||||
return [curves.topLeftBorderBox, curves.topRightBorderBox, curves.bottomRightBorderBox, curves.bottomLeftBorderBox];
|
||||
};
|
||||
|
||||
export const calculateContentBoxPath = (curves: BoundCurves): Path[] => {
|
||||
return [
|
||||
curves.topLeftContentBox,
|
||||
curves.topRightContentBox,
|
||||
curves.bottomRightContentBox,
|
||||
curves.bottomLeftContentBox
|
||||
];
|
||||
};
|
||||
|
||||
export const calculatePaddingBoxPath = (curves: BoundCurves): Path[] => {
|
||||
return [
|
||||
curves.topLeftPaddingBox,
|
||||
curves.topRightPaddingBox,
|
||||
curves.bottomRightPaddingBox,
|
||||
curves.bottomLeftPaddingBox
|
||||
];
|
||||
};
|
||||
31
src/render/box-sizing.ts
Normal file
31
src/render/box-sizing.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {getAbsoluteValue} from '../css/types/length-percentage';
|
||||
import {Bounds} from '../css/layout/bounds';
|
||||
import {ElementContainer} from '../dom/element-container';
|
||||
|
||||
export const paddingBox = (element: ElementContainer): Bounds => {
|
||||
const bounds = element.bounds;
|
||||
const styles = element.styles;
|
||||
return bounds.add(
|
||||
styles.borderLeftWidth,
|
||||
styles.borderTopWidth,
|
||||
-(styles.borderRightWidth + styles.borderLeftWidth),
|
||||
-(styles.borderTopWidth + styles.borderBottomWidth)
|
||||
);
|
||||
};
|
||||
|
||||
export const contentBox = (element: ElementContainer): Bounds => {
|
||||
const styles = element.styles;
|
||||
const bounds = element.bounds;
|
||||
|
||||
const paddingLeft = getAbsoluteValue(styles.paddingLeft, bounds.width);
|
||||
const paddingRight = getAbsoluteValue(styles.paddingRight, bounds.width);
|
||||
const paddingTop = getAbsoluteValue(styles.paddingTop, bounds.width);
|
||||
const paddingBottom = getAbsoluteValue(styles.paddingBottom, bounds.width);
|
||||
|
||||
return bounds.add(
|
||||
paddingLeft + styles.borderLeftWidth,
|
||||
paddingTop + styles.borderTopWidth,
|
||||
-(styles.borderRightWidth + styles.borderLeftWidth + paddingLeft + paddingRight),
|
||||
-(styles.borderTopWidth + styles.borderBottomWidth + paddingTop + paddingBottom)
|
||||
);
|
||||
};
|
||||
43
src/render/effects.ts
Normal file
43
src/render/effects.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {Matrix} from '../css/property-descriptors/transform';
|
||||
import {Path} from './path';
|
||||
|
||||
export const enum EffectType {
|
||||
TRANSFORM = 0,
|
||||
CLIP = 1,
|
||||
OPACITY = 2
|
||||
}
|
||||
|
||||
export const enum EffectTarget {
|
||||
BACKGROUND_BORDERS = 1 << 1,
|
||||
CONTENT = 1 << 2
|
||||
}
|
||||
|
||||
export interface IElementEffect {
|
||||
readonly type: EffectType;
|
||||
readonly target: number;
|
||||
}
|
||||
|
||||
export class TransformEffect implements IElementEffect {
|
||||
readonly type: EffectType = EffectType.TRANSFORM;
|
||||
readonly target: number = EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT;
|
||||
|
||||
constructor(readonly offsetX: number, readonly offsetY: number, readonly matrix: Matrix) {}
|
||||
}
|
||||
|
||||
export class ClipEffect implements IElementEffect {
|
||||
readonly type: EffectType = EffectType.CLIP;
|
||||
|
||||
constructor(readonly path: Path[], readonly target: EffectTarget) {}
|
||||
}
|
||||
|
||||
export class OpacityEffect implements IElementEffect {
|
||||
readonly type: EffectType = EffectType.OPACITY;
|
||||
readonly target: number = EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT;
|
||||
|
||||
constructor(readonly opacity: number) {}
|
||||
}
|
||||
|
||||
export const isTransformEffect = (effect: IElementEffect): effect is TransformEffect =>
|
||||
effect.type === EffectType.TRANSFORM;
|
||||
export const isClipEffect = (effect: IElementEffect): effect is ClipEffect => effect.type === EffectType.CLIP;
|
||||
export const isOpacityEffect = (effect: IElementEffect): effect is OpacityEffect => effect.type === EffectType.OPACITY;
|
||||
72
src/render/font-metrics.ts
Normal file
72
src/render/font-metrics.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import {SMALL_IMAGE} from '../core/util';
|
||||
export interface FontMetric {
|
||||
baseline: number;
|
||||
middle: number;
|
||||
}
|
||||
|
||||
const SAMPLE_TEXT = 'Hidden Text';
|
||||
|
||||
export class FontMetrics {
|
||||
private readonly _data: {[key: string]: FontMetric};
|
||||
private readonly _document: Document;
|
||||
|
||||
constructor(document: Document) {
|
||||
this._data = {};
|
||||
this._document = document;
|
||||
}
|
||||
|
||||
private parseMetrics(fontFamily: string, fontSize: string): FontMetric {
|
||||
const container = this._document.createElement('div');
|
||||
const img = this._document.createElement('img');
|
||||
const span = this._document.createElement('span');
|
||||
|
||||
const body = this._document.body as HTMLBodyElement;
|
||||
|
||||
container.style.visibility = 'hidden';
|
||||
container.style.fontFamily = fontFamily;
|
||||
container.style.fontSize = fontSize;
|
||||
container.style.margin = '0';
|
||||
container.style.padding = '0';
|
||||
container.style.whiteSpace = 'nowrap';
|
||||
|
||||
body.appendChild(container);
|
||||
|
||||
img.src = SMALL_IMAGE;
|
||||
img.width = 1;
|
||||
img.height = 1;
|
||||
|
||||
img.style.margin = '0';
|
||||
img.style.padding = '0';
|
||||
img.style.verticalAlign = 'baseline';
|
||||
|
||||
span.style.fontFamily = fontFamily;
|
||||
span.style.fontSize = fontSize;
|
||||
span.style.margin = '0';
|
||||
span.style.padding = '0';
|
||||
|
||||
span.appendChild(this._document.createTextNode(SAMPLE_TEXT));
|
||||
container.appendChild(span);
|
||||
container.appendChild(img);
|
||||
const baseline = img.offsetTop - span.offsetTop + 2;
|
||||
|
||||
container.removeChild(span);
|
||||
container.appendChild(this._document.createTextNode(SAMPLE_TEXT));
|
||||
|
||||
container.style.lineHeight = 'normal';
|
||||
img.style.verticalAlign = 'super';
|
||||
|
||||
const middle = img.offsetTop - container.offsetTop + 2;
|
||||
|
||||
body.removeChild(container);
|
||||
|
||||
return {baseline, middle};
|
||||
}
|
||||
getMetrics(fontFamily: string, fontSize: string): FontMetric {
|
||||
const key = `${fontFamily} ${fontSize}`;
|
||||
if (typeof this._data[key] === 'undefined') {
|
||||
this._data[key] = this.parseMetrics(fontFamily, fontSize);
|
||||
}
|
||||
|
||||
return this._data[key];
|
||||
}
|
||||
}
|
||||
37
src/render/path.ts
Normal file
37
src/render/path.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {BezierCurve} from './bezier-curve';
|
||||
import {Vector} from './vector';
|
||||
export const enum PathType {
|
||||
VECTOR = 0,
|
||||
BEZIER_CURVE = 1
|
||||
}
|
||||
|
||||
export interface IPath {
|
||||
type: PathType;
|
||||
add(deltaX: number, deltaY: number): IPath;
|
||||
}
|
||||
|
||||
export const equalPath = (a: Path[], b: Path[]): boolean => {
|
||||
if (a.length === b.length) {
|
||||
return a.some((v, i) => v === b[i]);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const transformPath = (path: Path[], deltaX: number, deltaY: number, deltaW: number, deltaH: number): Path[] => {
|
||||
return path.map((point, index) => {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return point.add(deltaX, deltaY);
|
||||
case 1:
|
||||
return point.add(deltaX + deltaW, deltaY);
|
||||
case 2:
|
||||
return point.add(deltaX + deltaW, deltaY + deltaH);
|
||||
case 3:
|
||||
return point.add(deltaX, deltaY + deltaH);
|
||||
}
|
||||
return point;
|
||||
});
|
||||
};
|
||||
|
||||
export type Path = Vector | BezierCurve;
|
||||
6
src/render/renderer.ts
Normal file
6
src/render/renderer.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import {Context} from '../core/context';
|
||||
import {RenderConfigurations} from './canvas/canvas-renderer';
|
||||
|
||||
export class Renderer {
|
||||
constructor(protected readonly context: Context, protected readonly options: RenderConfigurations) {}
|
||||
}
|
||||
205
src/render/stacking-context.ts
Normal file
205
src/render/stacking-context.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import {ElementContainer, FLAGS} from '../dom/element-container';
|
||||
import {contains} from '../core/bitwise';
|
||||
import {BoundCurves, calculateBorderBoxPath, calculatePaddingBoxPath} from './bound-curves';
|
||||
import {ClipEffect, EffectTarget, IElementEffect, isClipEffect, OpacityEffect, TransformEffect} from './effects';
|
||||
import {OVERFLOW} from '../css/property-descriptors/overflow';
|
||||
import {equalPath} from './path';
|
||||
import {DISPLAY} from '../css/property-descriptors/display';
|
||||
import {OLElementContainer} from '../dom/elements/ol-element-container';
|
||||
import {LIElementContainer} from '../dom/elements/li-element-container';
|
||||
import {createCounterText} from '../css/types/functions/counter';
|
||||
import {POSITION} from '../css/property-descriptors/position';
|
||||
|
||||
export class StackingContext {
|
||||
element: ElementPaint;
|
||||
negativeZIndex: StackingContext[];
|
||||
zeroOrAutoZIndexOrTransformedOrOpacity: StackingContext[];
|
||||
positiveZIndex: StackingContext[];
|
||||
nonPositionedFloats: StackingContext[];
|
||||
nonPositionedInlineLevel: StackingContext[];
|
||||
inlineLevel: ElementPaint[];
|
||||
nonInlineLevel: ElementPaint[];
|
||||
|
||||
constructor(container: ElementPaint) {
|
||||
this.element = container;
|
||||
this.inlineLevel = [];
|
||||
this.nonInlineLevel = [];
|
||||
this.negativeZIndex = [];
|
||||
this.zeroOrAutoZIndexOrTransformedOrOpacity = [];
|
||||
this.positiveZIndex = [];
|
||||
this.nonPositionedFloats = [];
|
||||
this.nonPositionedInlineLevel = [];
|
||||
}
|
||||
}
|
||||
|
||||
export class ElementPaint {
|
||||
readonly effects: IElementEffect[] = [];
|
||||
readonly curves: BoundCurves;
|
||||
listValue?: string;
|
||||
|
||||
constructor(readonly container: ElementContainer, readonly parent: ElementPaint | null) {
|
||||
this.curves = new BoundCurves(this.container);
|
||||
if (this.container.styles.opacity < 1) {
|
||||
this.effects.push(new OpacityEffect(this.container.styles.opacity));
|
||||
}
|
||||
|
||||
if (this.container.styles.transform !== null) {
|
||||
const offsetX = this.container.bounds.left + this.container.styles.transformOrigin[0].number;
|
||||
const offsetY = this.container.bounds.top + this.container.styles.transformOrigin[1].number;
|
||||
const matrix = this.container.styles.transform;
|
||||
this.effects.push(new TransformEffect(offsetX, offsetY, matrix));
|
||||
}
|
||||
|
||||
if (this.container.styles.overflowX !== OVERFLOW.VISIBLE) {
|
||||
const borderBox = calculateBorderBoxPath(this.curves);
|
||||
const paddingBox = calculatePaddingBoxPath(this.curves);
|
||||
|
||||
if (equalPath(borderBox, paddingBox)) {
|
||||
this.effects.push(new ClipEffect(borderBox, EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT));
|
||||
} else {
|
||||
this.effects.push(new ClipEffect(borderBox, EffectTarget.BACKGROUND_BORDERS));
|
||||
this.effects.push(new ClipEffect(paddingBox, EffectTarget.CONTENT));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getEffects(target: EffectTarget): IElementEffect[] {
|
||||
let inFlow = [POSITION.ABSOLUTE, POSITION.FIXED].indexOf(this.container.styles.position) === -1;
|
||||
let parent = this.parent;
|
||||
const effects = this.effects.slice(0);
|
||||
while (parent) {
|
||||
const croplessEffects = parent.effects.filter((effect) => !isClipEffect(effect));
|
||||
if (inFlow || parent.container.styles.position !== POSITION.STATIC || !parent.parent) {
|
||||
effects.unshift(...croplessEffects);
|
||||
inFlow = [POSITION.ABSOLUTE, POSITION.FIXED].indexOf(parent.container.styles.position) === -1;
|
||||
if (parent.container.styles.overflowX !== OVERFLOW.VISIBLE) {
|
||||
const borderBox = calculateBorderBoxPath(parent.curves);
|
||||
const paddingBox = calculatePaddingBoxPath(parent.curves);
|
||||
if (!equalPath(borderBox, paddingBox)) {
|
||||
effects.unshift(
|
||||
new ClipEffect(paddingBox, EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT)
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
effects.unshift(...croplessEffects);
|
||||
}
|
||||
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
return effects.filter((effect) => contains(effect.target, target));
|
||||
}
|
||||
}
|
||||
|
||||
const parseStackTree = (
|
||||
parent: ElementPaint,
|
||||
stackingContext: StackingContext,
|
||||
realStackingContext: StackingContext,
|
||||
listItems: ElementPaint[]
|
||||
) => {
|
||||
parent.container.elements.forEach((child) => {
|
||||
const treatAsRealStackingContext = contains(child.flags, FLAGS.CREATES_REAL_STACKING_CONTEXT);
|
||||
const createsStackingContext = contains(child.flags, FLAGS.CREATES_STACKING_CONTEXT);
|
||||
const paintContainer = new ElementPaint(child, parent);
|
||||
if (contains(child.styles.display, DISPLAY.LIST_ITEM)) {
|
||||
listItems.push(paintContainer);
|
||||
}
|
||||
|
||||
const listOwnerItems = contains(child.flags, FLAGS.IS_LIST_OWNER) ? [] : listItems;
|
||||
|
||||
if (treatAsRealStackingContext || createsStackingContext) {
|
||||
const parentStack =
|
||||
treatAsRealStackingContext || child.styles.isPositioned() ? realStackingContext : stackingContext;
|
||||
|
||||
const stack = new StackingContext(paintContainer);
|
||||
|
||||
if (child.styles.isPositioned() || child.styles.opacity < 1 || child.styles.isTransformed()) {
|
||||
const order = child.styles.zIndex.order;
|
||||
if (order < 0) {
|
||||
let index = 0;
|
||||
|
||||
parentStack.negativeZIndex.some((current, i) => {
|
||||
if (order > current.element.container.styles.zIndex.order) {
|
||||
index = i;
|
||||
return false;
|
||||
} else if (index > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
parentStack.negativeZIndex.splice(index, 0, stack);
|
||||
} else if (order > 0) {
|
||||
let index = 0;
|
||||
parentStack.positiveZIndex.some((current, i) => {
|
||||
if (order >= current.element.container.styles.zIndex.order) {
|
||||
index = i + 1;
|
||||
return false;
|
||||
} else if (index > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
parentStack.positiveZIndex.splice(index, 0, stack);
|
||||
} else {
|
||||
parentStack.zeroOrAutoZIndexOrTransformedOrOpacity.push(stack);
|
||||
}
|
||||
} else {
|
||||
if (child.styles.isFloating()) {
|
||||
parentStack.nonPositionedFloats.push(stack);
|
||||
} else {
|
||||
parentStack.nonPositionedInlineLevel.push(stack);
|
||||
}
|
||||
}
|
||||
|
||||
parseStackTree(
|
||||
paintContainer,
|
||||
stack,
|
||||
treatAsRealStackingContext ? stack : realStackingContext,
|
||||
listOwnerItems
|
||||
);
|
||||
} else {
|
||||
if (child.styles.isInlineLevel()) {
|
||||
stackingContext.inlineLevel.push(paintContainer);
|
||||
} else {
|
||||
stackingContext.nonInlineLevel.push(paintContainer);
|
||||
}
|
||||
|
||||
parseStackTree(paintContainer, stackingContext, realStackingContext, listOwnerItems);
|
||||
}
|
||||
|
||||
if (contains(child.flags, FLAGS.IS_LIST_OWNER)) {
|
||||
processListItems(child, listOwnerItems);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const processListItems = (owner: ElementContainer, elements: ElementPaint[]) => {
|
||||
let numbering = owner instanceof OLElementContainer ? owner.start : 1;
|
||||
const reversed = owner instanceof OLElementContainer ? owner.reversed : false;
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const item = elements[i];
|
||||
if (
|
||||
item.container instanceof LIElementContainer &&
|
||||
typeof item.container.value === 'number' &&
|
||||
item.container.value !== 0
|
||||
) {
|
||||
numbering = item.container.value;
|
||||
}
|
||||
|
||||
item.listValue = createCounterText(numbering, item.container.styles.listStyleType, true);
|
||||
|
||||
numbering += reversed ? -1 : 1;
|
||||
}
|
||||
};
|
||||
|
||||
export const parseStackingContexts = (container: ElementContainer): StackingContext => {
|
||||
const paintContainer = new ElementPaint(container, null);
|
||||
const root = new StackingContext(paintContainer);
|
||||
const listItems: ElementPaint[] = [];
|
||||
parseStackTree(paintContainer, root, root, listItems);
|
||||
processListItems(paintContainer.container, listItems);
|
||||
return root;
|
||||
};
|
||||
19
src/render/vector.ts
Normal file
19
src/render/vector.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {IPath, Path, PathType} from './path';
|
||||
|
||||
export class Vector implements IPath {
|
||||
type: PathType;
|
||||
x: number;
|
||||
y: number;
|
||||
|
||||
constructor(x: number, y: number) {
|
||||
this.type = PathType.VECTOR;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
add(deltaX: number, deltaY: number): Vector {
|
||||
return new Vector(this.x + deltaX, this.y + deltaY);
|
||||
}
|
||||
}
|
||||
|
||||
export const isVector = (path: Path): path is Vector => path.type === PathType.VECTOR;
|
||||
Reference in New Issue
Block a user