diff --git a/package-lock.json b/package-lock.json index f972f7112..0ba01d097 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,8 @@ "requires": true, "packages": { "": { - "version": "1.4.0", + "name": "html2canvas", + "version": "1.4.1", "license": "MIT", "dependencies": { "css-line-break": "^2.1.0", diff --git a/src/css/layout/__mocks__/bounds.ts b/src/css/layout/__mocks__/bounds.ts index c9bd89ad6..15f91864d 100644 --- a/src/css/layout/__mocks__/bounds.ts +++ b/src/css/layout/__mocks__/bounds.ts @@ -1,4 +1,8 @@ export const {Bounds} = jest.requireActual('../bounds'); -export const parseBounds = (): typeof Bounds => { +export const parseBounds = (): typeof Bounds[] => { + return [new Bounds(0, 0, 200, 50)]; +}; + +export const parseBound = (): typeof Bounds => { return new Bounds(0, 0, 200, 50); }; diff --git a/src/css/layout/bounds.ts b/src/css/layout/bounds.ts index 4e265127f..fbe4086ee 100644 --- a/src/css/layout/bounds.ts +++ b/src/css/layout/bounds.ts @@ -7,17 +7,19 @@ export class Bounds { return new Bounds(this.left + x, this.top + y, this.width + w, this.height + h); } - static fromClientRect(context: Context, clientRect: ClientRect): Bounds { - return new Bounds( - clientRect.left + context.windowBounds.left, - clientRect.top + context.windowBounds.top, - clientRect.width, - clientRect.height - ); + static fromDomRect(context: Context, domRect: DOMRect): Bounds { + return domRect.width !== 0 && domRect.height !== 0 + ? new Bounds( + domRect.left + context.windowBounds.left, + domRect.top + context.windowBounds.top, + domRect.width, + domRect.height + ) + : Bounds.EMPTY; } static fromDOMRectList(context: Context, domRectList: DOMRectList): Bounds { - const domRect = Array.from(domRectList).find((rect) => rect.width !== 0); + const domRect = Array.from(domRectList).find((rect) => rect.width !== 0 && rect.height !== 0); return domRect ? new Bounds( domRect.left + context.windowBounds.left, @@ -30,9 +32,12 @@ export class Bounds { static EMPTY = new Bounds(0, 0, 0, 0); } +export const parseBound = (context: Context, node: Element): Bounds => { + return Bounds.fromDomRect(context, node.getBoundingClientRect()); +}; -export const parseBounds = (context: Context, node: Element): Bounds => { - return Bounds.fromClientRect(context, node.getBoundingClientRect()); +export const parseBounds = (context: Context, node: Element): Bounds[] => { + return Array.from(node.getClientRects()).map((b) => Bounds.fromDomRect(context, b)); }; export const parseDocumentSize = (document: Document): Bounds => { diff --git a/src/css/layout/text.ts b/src/css/layout/text.ts index 0c6801932..6f91aa33d 100644 --- a/src/css/layout/text.ts +++ b/src/css/layout/text.ts @@ -49,7 +49,9 @@ export const parseTextBounds = ( } } else { const replacementNode = node.splitText(text.length); - textBounds.push(new TextBounds(text, getWrapperBounds(context, node))); + getWrapperBounds(context, node).forEach((wrapperBound) => { + textBounds.push(new TextBounds(text, wrapperBound)); + }); node = replacementNode; } } else if (!FEATURES.SUPPORT_RANGE_BOUNDS) { @@ -61,7 +63,7 @@ export const parseTextBounds = ( return textBounds; }; -const getWrapperBounds = (context: Context, node: Text): Bounds => { +const getWrapperBounds = (context: Context, node: Text): Bounds[] => { const ownerDocument = node.ownerDocument; if (ownerDocument) { const wrapper = ownerDocument.createElement('html2canvaswrapper'); @@ -77,7 +79,7 @@ const getWrapperBounds = (context: Context, node: Text): Bounds => { } } - return Bounds.EMPTY; + return [Bounds.EMPTY]; }; const createRange = (node: Text, offset: number, length: number): Range => { diff --git a/src/dom/element-container.ts b/src/dom/element-container.ts index 151e0cd8f..05704cc1f 100644 --- a/src/dom/element-container.ts +++ b/src/dom/element-container.ts @@ -16,7 +16,7 @@ export class ElementContainer { readonly styles: CSSParsedDeclaration; readonly textNodes: TextContainer[] = []; readonly elements: ElementContainer[] = []; - bounds: Bounds; + bounds: Bounds[]; flags = 0; constructor(protected readonly context: Context, element: Element) { diff --git a/src/dom/replaced-elements/input-element-container.ts b/src/dom/replaced-elements/input-element-container.ts index 1a2c78a38..711dbba48 100644 --- a/src/dom/replaced-elements/input-element-container.ts +++ b/src/dom/replaced-elements/input-element-container.ts @@ -74,7 +74,7 @@ export class InputElementContainer extends ElementContainer { BORDER_STYLE.SOLID; this.styles.backgroundClip = [BACKGROUND_CLIP.BORDER_BOX]; this.styles.backgroundOrigin = [BACKGROUND_ORIGIN.BORDER_BOX]; - this.bounds = reformatInputBounds(this.bounds); + this.bounds = this.bounds.map(reformatInputBounds); } switch (this.type) { diff --git a/src/dom/replaced-elements/svg-element-container.ts b/src/dom/replaced-elements/svg-element-container.ts index ae9c5a4c4..621428646 100644 --- a/src/dom/replaced-elements/svg-element-container.ts +++ b/src/dom/replaced-elements/svg-element-container.ts @@ -1,5 +1,5 @@ import {ElementContainer} from '../element-container'; -import {parseBounds} from '../../css/layout/bounds'; +import {parseBound} from '../../css/layout/bounds'; import {Context} from '../../core/context'; export class SVGElementContainer extends ElementContainer { @@ -10,7 +10,7 @@ export class SVGElementContainer extends ElementContainer { constructor(context: Context, img: SVGSVGElement) { super(context, img); const s = new XMLSerializer(); - const bounds = parseBounds(context, img); + const bounds = parseBound(context, img); img.setAttribute('width', `${bounds.width}px`); img.setAttribute('height', `${bounds.height}px`); diff --git a/src/index.ts b/src/index.ts index 348b050fb..758175ac2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import {Bounds, parseBounds, parseDocumentSize} from './css/layout/bounds'; +import {Bounds, parseBound, parseDocumentSize} from './css/layout/bounds'; import {COLORS, isTransparent, parseColor} from './css/types/color'; import {CloneConfigurations, CloneOptions, DocumentCloner, WindowOptions} from './dom/document-cloner'; import {isBodyElement, isHTMLElement, parseTree} from './dom/node-parser'; @@ -98,7 +98,7 @@ const renderElement = async (element: HTMLElement, opts: Partial): Prom const {width, height, left, top} = isBodyElement(clonedElement) || isHTMLElement(clonedElement) ? parseDocumentSize(clonedElement.ownerDocument) - : parseBounds(context, clonedElement); + : parseBound(context, clonedElement); const backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor); diff --git a/src/render/background.ts b/src/render/background.ts index 6e0e877ea..a7a3aa7d2 100644 --- a/src/render/background.ts +++ b/src/render/background.ts @@ -13,7 +13,7 @@ import {BACKGROUND_CLIP} from '../css/property-descriptors/background-clip'; export const calculateBackgroundPositioningArea = ( backgroundOrigin: BACKGROUND_ORIGIN, element: ElementContainer -): Bounds => { +): Bounds[] => { if (backgroundOrigin === BACKGROUND_ORIGIN.BORDER_BOX) { return element.bounds; } @@ -25,7 +25,10 @@ export const calculateBackgroundPositioningArea = ( return paddingBox(element); }; -export const calculateBackgroundPaintingArea = (backgroundClip: BACKGROUND_CLIP, element: ElementContainer): Bounds => { +export const calculateBackgroundPaintingArea = ( + backgroundClip: BACKGROUND_CLIP, + element: ElementContainer +): Bounds[] => { if (backgroundClip === BACKGROUND_CLIP.BORDER_BOX) { return element.bounds; } @@ -41,43 +44,46 @@ export const calculateBackgroundRendering = ( container: ElementContainer, index: number, intrinsicSize: [number | null, number | null, number | null] -): [Path[], number, number, number, number] => { - const backgroundPositioningArea = calculateBackgroundPositioningArea( +): [Path[], number, number, number, number][] => { + const backgroundPositioningAreas = calculateBackgroundPositioningArea( getBackgroundValueForIndex(container.styles.backgroundOrigin, index), container ); - const backgroundPaintingArea = calculateBackgroundPaintingArea( + const backgroundPaintingAreas = calculateBackgroundPaintingArea( getBackgroundValueForIndex(container.styles.backgroundClip, index), container ); - const backgroundImageSize = calculateBackgroundSize( + const backgroundImageSizes = 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 + backgroundPositioningAreas ); - 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]; + return backgroundPositioningAreas.map((backgroundPositioningArea, positionIndex) => { + const backgroundImageSize = backgroundImageSizes[positionIndex]; + 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, + backgroundPaintingAreas[positionIndex] + ); + + 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; @@ -87,30 +93,34 @@ const hasIntrinsicValue = (value: number | null): value is number => typeof valu export const calculateBackgroundSize = ( size: BackgroundSizeInfo[], [intrinsicWidth, intrinsicHeight, intrinsicProportion]: [number | null, number | null, number | null], - bounds: Bounds -): [number, number] => { + bounds: Bounds[] +): [number, number][] => { const [first, second] = size; if (!first) { - return [0, 0]; + return bounds.map(() => [0, 0]); } if (isLengthPercentage(first) && second && isLengthPercentage(second)) { - return [getAbsoluteValue(first, bounds.width), getAbsoluteValue(second, bounds.height)]; + return bounds.map((bound) => { + return [getAbsoluteValue(first, bound.width), getAbsoluteValue(second, bound.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 bounds.map((bound) => { + const targetRatio = bound.width / bound.height; - return targetRatio < intrinsicProportion !== (first.value === BACKGROUND_SIZE.COVER) - ? [bounds.width, bounds.width / intrinsicProportion] - : [bounds.height * intrinsicProportion, bounds.height]; + return targetRatio < intrinsicProportion !== (first.value === BACKGROUND_SIZE.COVER) + ? [bound.width, bound.width / intrinsicProportion] + : [bound.height * intrinsicProportion, bound.height]; + }); } - return [bounds.width, bounds.height]; + return bounds.map((bound) => [bound.width, bound.height]); } const hasIntrinsicWidth = hasIntrinsicValue(intrinsicWidth); @@ -121,14 +131,14 @@ export const calculateBackgroundSize = ( 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]; + return bounds.map(() => [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]; + return bounds.map((bound) => [bound.width, bound.height]); } // TODO If the image has no intrinsic dimensions but has intrinsic proportions, it's rendered as if contain had been specified instead. @@ -142,69 +152,73 @@ export const calculateBackgroundSize = ( const height = hasIntrinsicHeight ? (intrinsicHeight as number) : (intrinsicWidth as number) / (intrinsicProportion as number); - return [width, height]; + return bounds.map(() => [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]; + return bounds.map((bound) => { + const width = hasIntrinsicWidth ? (intrinsicWidth as number) : bound.width; + const height = hasIntrinsicHeight ? (intrinsicHeight as number) : bound.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 bounds.map((bound) => { + let width = 0; + let height = 0; + if (isLengthPercentage(first)) { + width = getAbsoluteValue(first, bound.width); + } else if (isLengthPercentage(second)) { + height = getAbsoluteValue(second, bound.height); + } + + if (isAuto(first)) { + width = height * (intrinsicProportion as number); + } else if (!second || isAuto(second)) { + height = width / (intrinsicProportion as number); + } - return [width, height]; + 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. + return bounds.map((bound) => { + let width = null; + let height = null; - 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 (isLengthPercentage(first)) { + width = getAbsoluteValue(first, bound.width); + } else if (second && isLengthPercentage(second)) { + height = getAbsoluteValue(second, bound.height); + } - if (width !== null && (!second || isAuto(second))) { - height = - hasIntrinsicWidth && hasIntrinsicHeight - ? (width / (intrinsicWidth as number)) * (intrinsicHeight as number) - : bounds.height; - } + if (width !== null && (!second || isAuto(second))) { + height = + hasIntrinsicWidth && hasIntrinsicHeight + ? (width / (intrinsicWidth as number)) * (intrinsicHeight as number) + : bound.height; + } - if (height !== null && isAuto(first)) { - width = - hasIntrinsicWidth && hasIntrinsicHeight - ? (height / (intrinsicHeight as number)) * (intrinsicWidth as number) - : bounds.width; - } + if (height !== null && isAuto(first)) { + width = + hasIntrinsicWidth && hasIntrinsicHeight + ? (height / (intrinsicHeight as number)) * (intrinsicWidth as number) + : bound.width; + } - if (width !== null && height !== null) { + if (width === null || height === null) { + throw new Error(`Unable to calculate background-size for element`); + } return [width, height]; - } - - throw new Error(`Unable to calculate background-size for element`); + }); }; export const getBackgroundValueForIndex = (values: T[], index: number): T => { diff --git a/src/render/border.ts b/src/render/border.ts index 20882c7d0..00bb9da13 100644 --- a/src/render/border.ts +++ b/src/render/border.ts @@ -2,30 +2,36 @@ import {Path} from './path'; import {BoundCurves} from './bound-curves'; import {isBezierCurve} from './bezier-curve'; -export const parsePathForBorder = (curves: BoundCurves, borderSide: number): Path[] => { +export enum BORDER_SIDE { + TOP = 0, + RIGHT = 1, + BOTTOM = 2, + LEFT = 3 +} +export const parsePathForBorder = (curves: BoundCurves, borderSide: BORDER_SIDE): Path[] => { switch (borderSide) { - case 0: + case BORDER_SIDE.TOP: return createPathFromCurves( curves.topLeftBorderBox, curves.topLeftPaddingBox, curves.topRightBorderBox, curves.topRightPaddingBox ); - case 1: + case BORDER_SIDE.RIGHT: return createPathFromCurves( curves.topRightBorderBox, curves.topRightPaddingBox, curves.bottomRightBorderBox, curves.bottomRightPaddingBox ); - case 2: + case BORDER_SIDE.BOTTOM: return createPathFromCurves( curves.bottomRightBorderBox, curves.bottomRightPaddingBox, curves.bottomLeftBorderBox, curves.bottomLeftPaddingBox ); - case 3: + case BORDER_SIDE.LEFT: default: return createPathFromCurves( curves.bottomLeftBorderBox, @@ -36,30 +42,30 @@ export const parsePathForBorder = (curves: BoundCurves, borderSide: number): Pat } }; -export const parsePathForBorderDoubleOuter = (curves: BoundCurves, borderSide: number): Path[] => { +export const parsePathForBorderDoubleOuter = (curves: BoundCurves, borderSide: BORDER_SIDE): Path[] => { switch (borderSide) { - case 0: + case BORDER_SIDE.TOP: return createPathFromCurves( curves.topLeftBorderBox, curves.topLeftBorderDoubleOuterBox, curves.topRightBorderBox, curves.topRightBorderDoubleOuterBox ); - case 1: + case BORDER_SIDE.RIGHT: return createPathFromCurves( curves.topRightBorderBox, curves.topRightBorderDoubleOuterBox, curves.bottomRightBorderBox, curves.bottomRightBorderDoubleOuterBox ); - case 2: + case BORDER_SIDE.BOTTOM: return createPathFromCurves( curves.bottomRightBorderBox, curves.bottomRightBorderDoubleOuterBox, curves.bottomLeftBorderBox, curves.bottomLeftBorderDoubleOuterBox ); - case 3: + case BORDER_SIDE.LEFT: default: return createPathFromCurves( curves.bottomLeftBorderBox, @@ -70,30 +76,30 @@ export const parsePathForBorderDoubleOuter = (curves: BoundCurves, borderSide: n } }; -export const parsePathForBorderDoubleInner = (curves: BoundCurves, borderSide: number): Path[] => { +export const parsePathForBorderDoubleInner = (curves: BoundCurves, borderSide: BORDER_SIDE): Path[] => { switch (borderSide) { - case 0: + case BORDER_SIDE.TOP: return createPathFromCurves( curves.topLeftBorderDoubleInnerBox, curves.topLeftPaddingBox, curves.topRightBorderDoubleInnerBox, curves.topRightPaddingBox ); - case 1: + case BORDER_SIDE.RIGHT: return createPathFromCurves( curves.topRightBorderDoubleInnerBox, curves.topRightPaddingBox, curves.bottomRightBorderDoubleInnerBox, curves.bottomRightPaddingBox ); - case 2: + case BORDER_SIDE.BOTTOM: return createPathFromCurves( curves.bottomRightBorderDoubleInnerBox, curves.bottomRightPaddingBox, curves.bottomLeftBorderDoubleInnerBox, curves.bottomLeftPaddingBox ); - case 3: + case BORDER_SIDE.LEFT: default: return createPathFromCurves( curves.bottomLeftBorderDoubleInnerBox, @@ -104,15 +110,15 @@ export const parsePathForBorderDoubleInner = (curves: BoundCurves, borderSide: n } }; -export const parsePathForBorderStroke = (curves: BoundCurves, borderSide: number): Path[] => { +export const parsePathForBorderStroke = (curves: BoundCurves, borderSide: BORDER_SIDE): Path[] => { switch (borderSide) { - case 0: + case BORDER_SIDE.TOP: return createStrokePathFromCurves(curves.topLeftBorderStroke, curves.topRightBorderStroke); - case 1: + case BORDER_SIDE.RIGHT: return createStrokePathFromCurves(curves.topRightBorderStroke, curves.bottomRightBorderStroke); - case 2: + case BORDER_SIDE.BOTTOM: return createStrokePathFromCurves(curves.bottomRightBorderStroke, curves.bottomLeftBorderStroke); - case 3: + case BORDER_SIDE.LEFT: default: return createStrokePathFromCurves(curves.bottomLeftBorderStroke, curves.topLeftBorderStroke); } diff --git a/src/render/bound-curves.ts b/src/render/bound-curves.ts index f6d19d4b1..c990be2ca 100644 --- a/src/render/bound-curves.ts +++ b/src/render/bound-curves.ts @@ -1,8 +1,11 @@ import {ElementContainer} from '../dom/element-container'; -import {getAbsoluteValue, getAbsoluteValueForTuple} from '../css/types/length-percentage'; +import {getAbsoluteValue, getAbsoluteValueForTuple, parseLengthPercentageTuple} from '../css/types/length-percentage'; import {Vector} from './vector'; import {BezierCurve} from './bezier-curve'; import {Path} from './path'; +import {Bounds} from '../css/layout/bounds'; +import {CSSParsedDeclaration} from '../css'; +import {TokenType} from '../css/syntax/tokenizer'; export class BoundCurves { readonly topLeftBorderDoubleOuterBox: Path; @@ -29,15 +32,53 @@ export class BoundCurves { readonly topRightContentBox: Path; readonly bottomRightContentBox: Path; readonly bottomLeftContentBox: Path; + readonly isFirstBoundOfElement: boolean; + readonly isLastBoundOfElement: boolean; - constructor(element: ElementContainer) { - const styles = element.styles; - const bounds = element.bounds; + static fromElementContainer(element: ElementContainer): BoundCurves[] { + const lastIndex = element.bounds.length - 1; + return element.bounds.map((bound, index) => { + return new BoundCurves(bound, element.styles, index === 0, index === lastIndex); + }); + } + + private static NoRadiusPercentage = parseLengthPercentageTuple([ + { + type: TokenType.DIMENSION_TOKEN, + flags: 0, + unit: 'px', + number: 0 + } + ]); - 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); + constructor( + bounds: Bounds, + styles: CSSParsedDeclaration, + isFirstBoundOfElement: boolean, + isLastBoundOfElement: boolean + ) { + this.isFirstBoundOfElement = isFirstBoundOfElement; + this.isLastBoundOfElement = isLastBoundOfElement; + let [tlh, tlv] = getAbsoluteValueForTuple( + this.isFirstBoundOfElement ? styles.borderTopLeftRadius : BoundCurves.NoRadiusPercentage, + bounds.width, + bounds.height + ); + let [trh, trv] = getAbsoluteValueForTuple( + this.isLastBoundOfElement ? styles.borderTopRightRadius : BoundCurves.NoRadiusPercentage, + bounds.width, + bounds.height + ); + let [brh, brv] = getAbsoluteValueForTuple( + this.isLastBoundOfElement ? styles.borderBottomRightRadius : BoundCurves.NoRadiusPercentage, + bounds.width, + bounds.height + ); + let [blh, blv] = getAbsoluteValueForTuple( + this.isFirstBoundOfElement ? styles.borderBottomLeftRadius : BoundCurves.NoRadiusPercentage, + bounds.width, + bounds.height + ); const factors = []; factors.push((tlh + trh) / bounds.width); @@ -67,10 +108,10 @@ export class BoundCurves { 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); + const paddingTop = getAbsoluteValue(styles.paddingTop, bounds.width); + const paddingRight = getAbsoluteValue(styles.paddingRight, bounds.width); + const paddingBottom = getAbsoluteValue(styles.paddingBottom, bounds.width); + const paddingLeft = getAbsoluteValue(styles.paddingLeft, bounds.width); this.topLeftBorderDoubleOuterBox = tlh > 0 || tlv > 0 diff --git a/src/render/box-sizing.ts b/src/render/box-sizing.ts index 01887d370..79d907343 100644 --- a/src/render/box-sizing.ts +++ b/src/render/box-sizing.ts @@ -2,30 +2,30 @@ 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; +export const paddingBox = (element: ElementContainer): Bounds[] => { const styles = element.styles; - return bounds.add( - styles.borderLeftWidth, - styles.borderTopWidth, - -(styles.borderRightWidth + styles.borderLeftWidth), - -(styles.borderTopWidth + styles.borderBottomWidth) - ); + return element.bounds.map((bound) => { + return bound.add( + styles.borderLeftWidth, + styles.borderTopWidth, + -(styles.borderRightWidth + styles.borderLeftWidth), + -(styles.borderTopWidth + styles.borderBottomWidth) + ); + }); }; -export const contentBox = (element: ElementContainer): Bounds => { +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) - ); + return element.bounds.map((bound) => { + const paddingLeft = getAbsoluteValue(styles.paddingLeft, bound.width); + const paddingRight = getAbsoluteValue(styles.paddingRight, bound.width); + const paddingTop = getAbsoluteValue(styles.paddingTop, bound.width); + const paddingBottom = getAbsoluteValue(styles.paddingBottom, bound.width); + return bound.add( + paddingLeft + styles.borderLeftWidth, + paddingTop + styles.borderTopWidth, + -(styles.borderRightWidth + styles.borderLeftWidth + paddingLeft + paddingRight), + -(styles.borderTopWidth + styles.borderBottomWidth + paddingTop + paddingBottom) + ); + }); }; diff --git a/src/render/canvas/canvas-renderer.ts b/src/render/canvas/canvas-renderer.ts index 6efb648bf..c10057402 100644 --- a/src/render/canvas/canvas-renderer.ts +++ b/src/render/canvas/canvas-renderer.ts @@ -14,7 +14,8 @@ import { parsePathForBorder, parsePathForBorderDoubleInner, parsePathForBorderDoubleOuter, - parsePathForBorderStroke + parsePathForBorderStroke, + BORDER_SIDE } from '../border'; import {calculateBackgroundRendering, getBackgroundValueForIndex} from '../background'; import {isDimensionToken} from '../../css/syntax/parser'; @@ -267,12 +268,19 @@ export class CanvasRenderer extends Renderer { renderReplacedElement( container: ReplacedElementContainer, - curves: BoundCurves, + curves: BoundCurves[], image: HTMLImageElement | HTMLCanvasElement ): void { if (image && container.intrinsicWidth > 0 && container.intrinsicHeight > 0) { - const box = contentBox(container); - const path = calculatePaddingBoxPath(curves); + const boxes = contentBox(container); + if (boxes.length !== 1) { + throw new Error(`Expecting a single bounding box but got ${boxes.length} for image replacement.`); + } + if (curves.length !== 1) { + throw new Error(`Expecting a single bounding box but got ${boxes.length} for image replacement.`); + } + const box = boxes[0]; + const path = calculatePaddingBoxPath(curves[0]); this.path(path); this.ctx.save(); this.ctx.clip(); @@ -334,55 +342,56 @@ export class CanvasRenderer extends Renderer { const canvas = await iframeRenderer.render(container.tree); if (container.width && container.height) { + const bound = container.bounds[0]; this.ctx.drawImage( canvas, 0, 0, container.width, container.height, - container.bounds.left, - container.bounds.top, - container.bounds.width, - container.bounds.height + bound.left, + bound.top, + bound.width, + bound.height ); } } if (container instanceof InputElementContainer) { - const size = Math.min(container.bounds.width, container.bounds.height); + //Should use flat map if target is updated. + const size = Math.min( + ...container.bounds.map((b) => b.width).concat(container.bounds.map((b) => b.height)) + ); if (container.type === CHECKBOX) { if (container.checked) { - this.ctx.save(); - this.path([ - new Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79), - new Vector(container.bounds.left + size * 0.16, container.bounds.top + size * 0.5549), - new Vector(container.bounds.left + size * 0.27347, container.bounds.top + size * 0.44071), - new Vector(container.bounds.left + size * 0.39694, container.bounds.top + size * 0.5649), - new Vector(container.bounds.left + size * 0.72983, container.bounds.top + size * 0.23), - new Vector(container.bounds.left + size * 0.84, container.bounds.top + size * 0.34085), - new Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79) - ]); - - this.ctx.fillStyle = asString(INPUT_COLOR); - this.ctx.fill(); - this.ctx.restore(); + container.bounds.forEach((bound) => { + this.ctx.save(); + this.path([ + new Vector(bound.left + size * 0.39363, bound.top + size * 0.79), + new Vector(bound.left + size * 0.16, bound.top + size * 0.5549), + new Vector(bound.left + size * 0.27347, bound.top + size * 0.44071), + new Vector(bound.left + size * 0.39694, bound.top + size * 0.5649), + new Vector(bound.left + size * 0.72983, bound.top + size * 0.23), + new Vector(bound.left + size * 0.84, bound.top + size * 0.34085), + new Vector(bound.left + size * 0.39363, bound.top + size * 0.79) + ]); + + this.ctx.fillStyle = asString(INPUT_COLOR); + this.ctx.fill(); + this.ctx.restore(); + }); } } else if (container.type === RADIO) { if (container.checked) { - this.ctx.save(); - this.ctx.beginPath(); - this.ctx.arc( - container.bounds.left + size / 2, - container.bounds.top + size / 2, - size / 4, - 0, - Math.PI * 2, - true - ); - this.ctx.fillStyle = asString(INPUT_COLOR); - this.ctx.fill(); - this.ctx.restore(); + container.bounds.forEach((bound) => { + this.ctx.save(); + this.ctx.beginPath(); + this.ctx.arc(bound.left + size / 2, bound.top + size / 2, size / 4, 0, Math.PI * 2, true); + this.ctx.fillStyle = asString(INPUT_COLOR); + this.ctx.fill(); + this.ctx.restore(); + }); } } } @@ -397,7 +406,7 @@ export class CanvasRenderer extends Renderer { this.ctx.textBaseline = 'alphabetic'; this.ctx.textAlign = canvasTextAlign(container.styles.textAlign); - const bounds = contentBox(container); + const bounds = contentBox(container)[0]; let x = 0; @@ -439,7 +448,11 @@ export class CanvasRenderer extends Renderer { const url = (img as CSSURLImage).url; try { image = await this.context.cache.match(url); - this.ctx.drawImage(image, container.bounds.left - (image.width + 10), container.bounds.top); + this.ctx.drawImage( + image, + Math.min(...container.bounds.map((b) => b.left)) - (image.width + 10), + Math.min(...container.bounds.map((b) => b.top)) + ); } catch (e) { this.context.logger.error(`Error loading list-style-image ${url}`); } @@ -452,10 +465,12 @@ export class CanvasRenderer extends Renderer { this.ctx.textBaseline = 'middle'; this.ctx.textAlign = 'right'; + const width = Math.max(...container.bounds.map((b) => b.width)); const bounds = new Bounds( - container.bounds.left, - container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width), - container.bounds.width, + Math.min(...container.bounds.map((b) => b.left)), + Math.min(...container.bounds.map((b) => b.top)) + + getAbsoluteValue(container.styles.paddingTop, width), + width, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 1 ); @@ -586,7 +601,7 @@ export class CanvasRenderer extends Renderer { let index = container.styles.backgroundImage.length - 1; for (const backgroundImage of container.styles.backgroundImage.slice(0).reverse()) { if (backgroundImage.type === CSSImageType.URL) { - let image; + let image: HTMLImageElement | null = null; const url = (backgroundImage as CSSURLImage).url; try { image = await this.context.cache.match(url); @@ -595,75 +610,93 @@ export class CanvasRenderer extends Renderer { } if (image) { - const [path, x, y, width, height] = calculateBackgroundRendering(container, index, [ + const areas = calculateBackgroundRendering(container, index, [ image.width, image.height, image.width / image.height ]); - const pattern = this.ctx.createPattern( - this.resizeImage(image, width, height), - 'repeat' - ) as CanvasPattern; - this.renderRepeat(path, pattern, x, y); + areas.forEach((area) => { + const [path, x, y, width, height] = area; + if (image) { + const pattern = this.ctx.createPattern( + this.resizeImage(image, width, height), + 'repeat' + ) as CanvasPattern; + this.renderRepeat(path, pattern, x, y); + } + }); } } else if (isLinearGradient(backgroundImage)) { - const [path, x, y, width, height] = calculateBackgroundRendering(container, index, [null, null, null]); - const [lineLength, x0, x1, y0, y1] = calculateGradientDirection(backgroundImage.angle, width, height); + const areas = calculateBackgroundRendering(container, index, [null, null, null]); + areas.forEach((area) => { + const [path, x, y, width, height] = area; + const [lineLength, x0, x1, y0, y1] = calculateGradientDirection( + backgroundImage.angle, + width, + height + ); - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; - const gradient = ctx.createLinearGradient(x0, y0, x1, y1); + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; + const gradient = ctx.createLinearGradient(x0, y0, x1, y1); - processColorStops(backgroundImage.stops, lineLength).forEach((colorStop) => - gradient.addColorStop(colorStop.stop, asString(colorStop.color)) - ); + processColorStops(backgroundImage.stops, lineLength).forEach((colorStop) => + gradient.addColorStop(colorStop.stop, asString(colorStop.color)) + ); - ctx.fillStyle = gradient; - ctx.fillRect(0, 0, width, height); - if (width > 0 && height > 0) { - const pattern = this.ctx.createPattern(canvas, 'repeat') as CanvasPattern; - this.renderRepeat(path, pattern, x, y); - } + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, width, height); + if (width > 0 && height > 0) { + const pattern = this.ctx.createPattern(canvas, 'repeat') as CanvasPattern; + this.renderRepeat(path, pattern, x, y); + } + }); } else if (isRadialGradient(backgroundImage)) { - const [path, left, top, width, height] = calculateBackgroundRendering(container, index, [ - null, - null, - null - ]); + const areas = calculateBackgroundRendering(container, index, [null, null, null]); const position = backgroundImage.position.length === 0 ? [FIFTY_PERCENT] : backgroundImage.position; - const x = getAbsoluteValue(position[0], width); - const y = getAbsoluteValue(position[position.length - 1], height); - - const [rx, ry] = calculateRadius(backgroundImage, x, y, width, height); - if (rx > 0 && ry > 0) { - const radialGradient = this.ctx.createRadialGradient(left + x, top + y, 0, left + x, top + y, rx); - - processColorStops(backgroundImage.stops, rx * 2).forEach((colorStop) => - radialGradient.addColorStop(colorStop.stop, asString(colorStop.color)) - ); - - this.path(path); - this.ctx.fillStyle = radialGradient; - if (rx !== ry) { - // transforms for elliptical radial gradient - const midX = container.bounds.left + 0.5 * container.bounds.width; - const midY = container.bounds.top + 0.5 * container.bounds.height; - const f = ry / rx; - const invF = 1 / f; - - this.ctx.save(); - this.ctx.translate(midX, midY); - this.ctx.transform(1, 0, 0, f, 0, 0); - this.ctx.translate(-midX, -midY); - - this.ctx.fillRect(left, invF * (top - midY) + midY, width, height * invF); - this.ctx.restore(); - } else { - this.ctx.fill(); + areas.forEach((area, areaindex) => { + const [path, left, top, width, height] = area; + const x = getAbsoluteValue(position[0], width); + const y = getAbsoluteValue(position[position.length - 1], height); + + const [rx, ry] = calculateRadius(backgroundImage, x, y, width, height); + if (rx > 0 && ry > 0) { + const radialGradient = this.ctx.createRadialGradient( + left + x, + top + y, + 0, + left + x, + top + y, + rx + ); + + processColorStops(backgroundImage.stops, rx * 2).forEach((colorStop) => + radialGradient.addColorStop(colorStop.stop, asString(colorStop.color)) + ); + + this.path(path); + this.ctx.fillStyle = radialGradient; + if (rx !== ry) { + // transforms for elliptical radial gradient + const midX = container.bounds[areaindex].left + 0.5 * container.bounds[areaindex].width; + const midY = container.bounds[areaindex].top + 0.5 * container.bounds[areaindex].height; + const f = ry / rx; + const invF = 1 / f; + + this.ctx.save(); + this.ctx.translate(midX, midY); + this.ctx.transform(1, 0, 0, f, 0, 0); + this.ctx.translate(-midX, -midY); + + this.ctx.fillRect(left, invF * (top - midY) + midY, width, height * invF); + this.ctx.restore(); + } else { + this.ctx.fill(); + } } - } + }); } index--; } @@ -701,88 +734,95 @@ export class CanvasRenderer extends Renderer { {style: styles.borderBottomStyle, color: styles.borderBottomColor, width: styles.borderBottomWidth}, {style: styles.borderLeftStyle, color: styles.borderLeftColor, width: styles.borderLeftWidth} ]; + for (const curve of paint.curves) { + const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea( + getBackgroundValueForIndex(styles.backgroundClip, 0), + curve + ); - const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea( - getBackgroundValueForIndex(styles.backgroundClip, 0), - paint.curves - ); + if (hasBackground || styles.boxShadow.length) { + this.ctx.save(); + this.path(backgroundPaintingArea); + this.ctx.clip(); - if (hasBackground || styles.boxShadow.length) { - this.ctx.save(); - this.path(backgroundPaintingArea); - this.ctx.clip(); + if (!isTransparent(styles.backgroundColor)) { + this.ctx.fillStyle = asString(styles.backgroundColor); + this.ctx.fill(); + } - if (!isTransparent(styles.backgroundColor)) { - this.ctx.fillStyle = asString(styles.backgroundColor); - this.ctx.fill(); - } + await this.renderBackgroundImage(paint.container); - await this.renderBackgroundImage(paint.container); + this.ctx.restore(); - this.ctx.restore(); + styles.boxShadow + .slice(0) + .reverse() + .forEach((shadow) => { + this.ctx.save(); + const borderBoxArea = calculateBorderBoxPath(curve); + const maskOffset = shadow.inset ? 0 : MASK_OFFSET; + const shadowPaintingArea = transformPath( + borderBoxArea, + -maskOffset + (shadow.inset ? 1 : -1) * shadow.spread.number, + (shadow.inset ? 1 : -1) * shadow.spread.number, + shadow.spread.number * (shadow.inset ? -2 : 2), + shadow.spread.number * (shadow.inset ? -2 : 2) + ); + + if (shadow.inset) { + this.path(borderBoxArea); + this.ctx.clip(); + this.mask(shadowPaintingArea); + } else { + this.mask(borderBoxArea); + this.ctx.clip(); + this.path(shadowPaintingArea); + } - styles.boxShadow - .slice(0) - .reverse() - .forEach((shadow) => { - this.ctx.save(); - const borderBoxArea = calculateBorderBoxPath(paint.curves); - const maskOffset = shadow.inset ? 0 : MASK_OFFSET; - const shadowPaintingArea = transformPath( - borderBoxArea, - -maskOffset + (shadow.inset ? 1 : -1) * shadow.spread.number, - (shadow.inset ? 1 : -1) * shadow.spread.number, - shadow.spread.number * (shadow.inset ? -2 : 2), - shadow.spread.number * (shadow.inset ? -2 : 2) - ); + this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset; + this.ctx.shadowOffsetY = shadow.offsetY.number; + this.ctx.shadowColor = asString(shadow.color); + this.ctx.shadowBlur = shadow.blur.number; + this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : 'rgba(0,0,0,1)'; - if (shadow.inset) { - this.path(borderBoxArea); - this.ctx.clip(); - this.mask(shadowPaintingArea); + this.ctx.fill(); + this.ctx.restore(); + }); + } + + let side = 0; + for (const border of borders) { + if ( + border.style !== BORDER_STYLE.NONE && + !isTransparent(border.color) && + border.width > 0 && + (curve.isFirstBoundOfElement || side !== BORDER_SIDE.LEFT) && + (curve.isLastBoundOfElement || side !== BORDER_SIDE.RIGHT) + ) { + if (border.style === BORDER_STYLE.DASHED) { + await this.renderDashedDottedBorder( + border.color, + border.width, + side, + curve, + BORDER_STYLE.DASHED + ); + } else if (border.style === BORDER_STYLE.DOTTED) { + await this.renderDashedDottedBorder( + border.color, + border.width, + side, + curve, + BORDER_STYLE.DOTTED + ); + } else if (border.style === BORDER_STYLE.DOUBLE) { + await this.renderDoubleBorder(border.color, border.width, side, curve); } else { - this.mask(borderBoxArea); - this.ctx.clip(); - this.path(shadowPaintingArea); + await this.renderSolidBorder(border.color, side, curve); } - - this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset; - this.ctx.shadowOffsetY = shadow.offsetY.number; - this.ctx.shadowColor = asString(shadow.color); - this.ctx.shadowBlur = shadow.blur.number; - this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : 'rgba(0,0,0,1)'; - - this.ctx.fill(); - this.ctx.restore(); - }); - } - - let side = 0; - for (const border of borders) { - if (border.style !== BORDER_STYLE.NONE && !isTransparent(border.color) && border.width > 0) { - if (border.style === BORDER_STYLE.DASHED) { - await this.renderDashedDottedBorder( - border.color, - border.width, - side, - paint.curves, - BORDER_STYLE.DASHED - ); - } else if (border.style === BORDER_STYLE.DOTTED) { - await this.renderDashedDottedBorder( - border.color, - border.width, - side, - paint.curves, - BORDER_STYLE.DOTTED - ); - } else if (border.style === BORDER_STYLE.DOUBLE) { - await this.renderDoubleBorder(border.color, border.width, side, paint.curves); - } else { - await this.renderSolidBorder(border.color, side, paint.curves); } + side++; } - side++; } } diff --git a/src/render/stacking-context.ts b/src/render/stacking-context.ts index c5ac088b0..9592fcfe1 100644 --- a/src/render/stacking-context.ts +++ b/src/render/stacking-context.ts @@ -34,32 +34,38 @@ export class StackingContext { export class ElementPaint { readonly effects: IElementEffect[] = []; - readonly curves: BoundCurves; + readonly curves: BoundCurves[]; listValue?: string; constructor(readonly container: ElementContainer, readonly parent: ElementPaint | null) { - this.curves = new BoundCurves(this.container); + this.curves = BoundCurves.fromElementContainer(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; + const matrix = this.container.styles.transform, + originZero = this.container.styles.transformOrigin[0].number, + originOne = this.container.styles.transformOrigin[1].number, + offsetX = Math.min(...this.container.bounds.map((b) => b.left)) + originZero, + offsetY = Math.min(...this.container.bounds.map((b) => b.top)) + originOne; 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)); - } + this.curves.forEach((curve) => { + const borderBox = calculateBorderBoxPath(curve); + const paddingBox = calculatePaddingBoxPath(curve); + + 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)); + } + }); } } @@ -73,13 +79,15 @@ export class ElementPaint { 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) - ); - } + this.curves.forEach((curve) => { + const borderBox = calculateBorderBoxPath(curve); + const paddingBox = calculatePaddingBoxPath(curve); + if (!equalPath(borderBox, paddingBox)) { + effects.unshift( + new ClipEffect(paddingBox, EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT) + ); + } + }); } } else { effects.unshift(...croplessEffects); diff --git a/tests/reftests/text/background-on-multiline-span.html b/tests/reftests/text/background-on-multiline-span.html new file mode 100644 index 000000000..ba0925cac --- /dev/null +++ b/tests/reftests/text/background-on-multiline-span.html @@ -0,0 +1,30 @@ + + + + + Background on multiline span + + + + + + +
+

bold 1 no + highlighting bold 2 still no + highlighting test inline stuff with background + that is long enough to go onto a + second line. this is just some extra stuff to get it to flow over to a new line.  stuff that is + being blanked out. More bold stuff that shouldn't be highlighted +

+
+ + + \ No newline at end of file