mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
improved support of 'content' for pseudo elements (multiple components, counters, attr, quotes)
This commit is contained in:
parent
54c4002df7
commit
78c3c7fc71
@ -22,7 +22,7 @@ Below is a list of all the supported CSS properties and values.
|
|||||||
- border-width
|
- border-width
|
||||||
- bottom
|
- bottom
|
||||||
- box-sizing
|
- box-sizing
|
||||||
- content (**Does not support `attr()`**)
|
- content
|
||||||
- color
|
- color
|
||||||
- display
|
- display
|
||||||
- flex
|
- flex
|
||||||
|
@ -77,6 +77,7 @@
|
|||||||
"homepage": "https://html2canvas.hertzen.com",
|
"homepage": "https://html2canvas.hertzen.com",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"punycode": "2.1.0"
|
"punycode": "2.1.0",
|
||||||
|
"liststyletype-formatter": "latest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
80
src/Clone.js
80
src/Clone.js
@ -2,6 +2,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
import type {Bounds} from './Bounds';
|
import type {Bounds} from './Bounds';
|
||||||
import type {Options} from './index';
|
import type {Options} from './index';
|
||||||
|
import type {PseudoContentData, PseudoContentItem} from './PseudoNodeContent';
|
||||||
import type Logger from './Logger';
|
import type Logger from './Logger';
|
||||||
|
|
||||||
import {parseBounds} from './Bounds';
|
import {parseBounds} from './Bounds';
|
||||||
@ -10,6 +11,12 @@ import ResourceLoader from './ResourceLoader';
|
|||||||
import {copyCSSStyles} from './Util';
|
import {copyCSSStyles} from './Util';
|
||||||
import {parseBackgroundImage} from './parsing/background';
|
import {parseBackgroundImage} from './parsing/background';
|
||||||
import CanvasRenderer from './renderer/CanvasRenderer';
|
import CanvasRenderer from './renderer/CanvasRenderer';
|
||||||
|
import {
|
||||||
|
parseCounterReset,
|
||||||
|
popCounters,
|
||||||
|
resolvePseudoContent,
|
||||||
|
PSEUDO_CONTENT_ITEM_TYPE
|
||||||
|
} from './PseudoNodeContent';
|
||||||
|
|
||||||
const IGNORE_ATTRIBUTE = 'data-html2canvas-ignore';
|
const IGNORE_ATTRIBUTE = 'data-html2canvas-ignore';
|
||||||
|
|
||||||
@ -24,6 +31,7 @@ export class DocumentCloner {
|
|||||||
inlineImages: boolean;
|
inlineImages: boolean;
|
||||||
copyStyles: boolean;
|
copyStyles: boolean;
|
||||||
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>;
|
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>;
|
||||||
|
pseudoContentData: PseudoContentData;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
@ -40,6 +48,10 @@ export class DocumentCloner {
|
|||||||
this.options = options;
|
this.options = options;
|
||||||
this.renderer = renderer;
|
this.renderer = renderer;
|
||||||
this.resourceLoader = new ResourceLoader(options, logger, window);
|
this.resourceLoader = new ResourceLoader(options, logger, window);
|
||||||
|
this.pseudoContentData = {
|
||||||
|
counters: {},
|
||||||
|
quoteDepth: 0
|
||||||
|
};
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
this.documentElement = this.cloneNode(element.ownerDocument.documentElement);
|
this.documentElement = this.cloneNode(element.ownerDocument.documentElement);
|
||||||
}
|
}
|
||||||
@ -226,6 +238,11 @@ export class DocumentCloner {
|
|||||||
: this.createElementClone(node);
|
: this.createElementClone(node);
|
||||||
|
|
||||||
const window = node.ownerDocument.defaultView;
|
const window = node.ownerDocument.defaultView;
|
||||||
|
const style = node instanceof window.HTMLElement ? window.getComputedStyle(node) : null;
|
||||||
|
const styleBefore =
|
||||||
|
node instanceof window.HTMLElement ? window.getComputedStyle(node, ':before') : null;
|
||||||
|
const styleAfter =
|
||||||
|
node instanceof window.HTMLElement ? window.getComputedStyle(node, ':after') : null;
|
||||||
|
|
||||||
if (this.referenceElement === node && clone instanceof window.HTMLElement) {
|
if (this.referenceElement === node && clone instanceof window.HTMLElement) {
|
||||||
this.clonedReferenceElement = clone;
|
this.clonedReferenceElement = clone;
|
||||||
@ -235,6 +252,9 @@ export class DocumentCloner {
|
|||||||
createPseudoHideStyles(clone);
|
createPseudoHideStyles(clone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const counters = parseCounterReset(style, this.pseudoContentData);
|
||||||
|
const contentBefore = resolvePseudoContent(node, styleBefore, this.pseudoContentData);
|
||||||
|
|
||||||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||||
if (
|
if (
|
||||||
child.nodeType !== Node.ELEMENT_NODE ||
|
child.nodeType !== Node.ELEMENT_NODE ||
|
||||||
@ -246,11 +266,23 @@ export class DocumentCloner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contentAfter = resolvePseudoContent(node, styleAfter, this.pseudoContentData);
|
||||||
|
popCounters(counters, this.pseudoContentData);
|
||||||
|
|
||||||
if (node instanceof window.HTMLElement && clone instanceof window.HTMLElement) {
|
if (node instanceof window.HTMLElement && clone instanceof window.HTMLElement) {
|
||||||
this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_BEFORE));
|
if (styleBefore) {
|
||||||
this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_AFTER));
|
this.inlineAllImages(
|
||||||
if (this.copyStyles && !(node instanceof HTMLIFrameElement)) {
|
inlinePseudoElement(node, clone, styleBefore, contentBefore, PSEUDO_BEFORE)
|
||||||
copyCSSStyles(node.ownerDocument.defaultView.getComputedStyle(node), clone);
|
);
|
||||||
|
}
|
||||||
|
if (styleAfter) {
|
||||||
|
this.inlineAllImages(
|
||||||
|
inlinePseudoElement(node, clone, styleAfter, contentAfter, PSEUDO_AFTER)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (style && this.copyStyles && !(node instanceof HTMLIFrameElement)) {
|
||||||
|
copyCSSStyles(style, clone);
|
||||||
}
|
}
|
||||||
this.inlineAllImages(clone);
|
this.inlineAllImages(clone);
|
||||||
if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
|
if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
|
||||||
@ -361,9 +393,10 @@ const cloneCanvasContents = (canvas: HTMLCanvasElement, clonedCanvas: HTMLCanvas
|
|||||||
const inlinePseudoElement = (
|
const inlinePseudoElement = (
|
||||||
node: HTMLElement,
|
node: HTMLElement,
|
||||||
clone: HTMLElement,
|
clone: HTMLElement,
|
||||||
|
style: CSSStyleDeclaration,
|
||||||
|
contentItems: Array<PseudoContentItem>,
|
||||||
pseudoElt: ':before' | ':after'
|
pseudoElt: ':before' | ':after'
|
||||||
): ?HTMLElement => {
|
): ?HTMLElement => {
|
||||||
const style = node.ownerDocument.defaultView.getComputedStyle(node, pseudoElt);
|
|
||||||
if (
|
if (
|
||||||
!style ||
|
!style ||
|
||||||
!style.content ||
|
!style.content ||
|
||||||
@ -374,20 +407,28 @@ const inlinePseudoElement = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = stripQuotes(style.content);
|
const anonymousReplacedElement = clone.ownerDocument.createElement('html2canvaspseudoelement');
|
||||||
const image = content.match(URL_REGEXP);
|
const len = contentItems.length;
|
||||||
const anonymousReplacedElement = clone.ownerDocument.createElement(
|
|
||||||
image ? 'img' : 'html2canvaspseudoelement'
|
|
||||||
);
|
|
||||||
if (image) {
|
|
||||||
// $FlowFixMe
|
|
||||||
anonymousReplacedElement.src = stripQuotes(image[1]);
|
|
||||||
} else {
|
|
||||||
anonymousReplacedElement.textContent = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
copyCSSStyles(style, anonymousReplacedElement);
|
copyCSSStyles(style, anonymousReplacedElement);
|
||||||
|
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
const item = contentItems[i];
|
||||||
|
switch (item.type) {
|
||||||
|
case PSEUDO_CONTENT_ITEM_TYPE.IMAGE:
|
||||||
|
const img = clone.ownerDocument.createElement('img');
|
||||||
|
img.src = parseBackgroundImage(`url(${item.value})`)[0].args[0];
|
||||||
|
img.style.opacity = '1';
|
||||||
|
anonymousReplacedElement.appendChild(img);
|
||||||
|
break;
|
||||||
|
case PSEUDO_CONTENT_ITEM_TYPE.TEXT:
|
||||||
|
anonymousReplacedElement.appendChild(
|
||||||
|
clone.ownerDocument.createTextNode(item.value)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
anonymousReplacedElement.className = `${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE} ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
|
anonymousReplacedElement.className = `${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE} ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
|
||||||
clone.className +=
|
clone.className +=
|
||||||
pseudoElt === PSEUDO_BEFORE
|
pseudoElt === PSEUDO_BEFORE
|
||||||
@ -402,13 +443,6 @@ const inlinePseudoElement = (
|
|||||||
return anonymousReplacedElement;
|
return anonymousReplacedElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const stripQuotes = (content: string): string => {
|
|
||||||
const first = content.substr(0, 1);
|
|
||||||
return first === content.substr(content.length - 1) && first.match(/['"]/)
|
|
||||||
? content.substr(1, content.length - 2)
|
|
||||||
: content;
|
|
||||||
};
|
|
||||||
|
|
||||||
const URL_REGEXP = /^url\((.+)\)$/i;
|
const URL_REGEXP = /^url\((.+)\)$/i;
|
||||||
const PSEUDO_BEFORE = ':before';
|
const PSEUDO_BEFORE = ':before';
|
||||||
const PSEUDO_AFTER = ':after';
|
const PSEUDO_AFTER = ':after';
|
||||||
|
340
src/PseudoNodeContent.js
Normal file
340
src/PseudoNodeContent.js
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
import ListStyleTypeFormatter from 'liststyletype-formatter';
|
||||||
|
|
||||||
|
export const PSEUDO_CONTENT_ITEM_TYPE = {
|
||||||
|
TEXT: 0,
|
||||||
|
IMAGE: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TOKEN_TYPE = {
|
||||||
|
STRING: 0,
|
||||||
|
ATTRIBUTE: 1,
|
||||||
|
URL: 2,
|
||||||
|
COUNTER: 3,
|
||||||
|
COUNTERS: 4,
|
||||||
|
OPENQUOTE: 5,
|
||||||
|
CLOSEQUOTE: 6
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PseudoContentData = {
|
||||||
|
counters: {[string]: Array<number>},
|
||||||
|
quoteDepth: number
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PseudoContentItem = {
|
||||||
|
type: $Values<typeof PSEUDO_CONTENT_ITEM_TYPE>,
|
||||||
|
value: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Token = {
|
||||||
|
type: $Values<typeof TOKEN_TYPE>,
|
||||||
|
value: ?string,
|
||||||
|
format: ?string,
|
||||||
|
glue: ?string
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseCounterReset = (
|
||||||
|
style: CSSStyleDeclaration,
|
||||||
|
data: PseudoContentData
|
||||||
|
): Array<string> => {
|
||||||
|
if (!style || !style.counterReset || style.counterReset === 'none') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const counterNames: Array<string> = [];
|
||||||
|
const counterResets = style.counterReset.split(/\s*,\s*/);
|
||||||
|
const lenCounterResets = counterResets.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < lenCounterResets; i++) {
|
||||||
|
const [counterName, initialValue] = counterResets[i].split(/\s+/);
|
||||||
|
counterNames.push(counterName);
|
||||||
|
let counter = data.counters[counterName];
|
||||||
|
if (!counter) {
|
||||||
|
counter = data.counters[counterName] = [];
|
||||||
|
}
|
||||||
|
counter.push(parseInt(initialValue || 0, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
return counterNames;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const popCounters = (counterNames: Array<string>, data: PseudoContentData): void => {
|
||||||
|
const lenCounters = counterNames.length;
|
||||||
|
for (let i = 0; i < lenCounters; i++) {
|
||||||
|
data.counters[counterNames[i]].pop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolvePseudoContent = (
|
||||||
|
node: Node,
|
||||||
|
style: CSSStyleDeclaration,
|
||||||
|
data: PseudoContentData
|
||||||
|
): Array<PseudoContentItem> => {
|
||||||
|
if (
|
||||||
|
!style ||
|
||||||
|
!style.content ||
|
||||||
|
style.content === 'none' ||
|
||||||
|
style.content === '-moz-alt-content' ||
|
||||||
|
style.display === 'none'
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = parseContent(style.content);
|
||||||
|
const len = tokens.length;
|
||||||
|
const contentItems: Array<PseudoContentItem> = [];
|
||||||
|
let s = '';
|
||||||
|
|
||||||
|
// increment the counter (if there is a "counter-increment" declaration)
|
||||||
|
const counterIncrement = style.counterIncrement;
|
||||||
|
if (counterIncrement && counterIncrement !== 'none') {
|
||||||
|
const [counterName, incrementValue] = counterIncrement.split(/\s+/);
|
||||||
|
const counter = data.counters[counterName];
|
||||||
|
if (counter) {
|
||||||
|
counter[counter.length - 1] +=
|
||||||
|
incrementValue === undefined ? 1 : parseInt(incrementValue, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the content string
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const token = tokens[i];
|
||||||
|
switch (token.type) {
|
||||||
|
case TOKEN_TYPE.STRING:
|
||||||
|
s += token.value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TOKEN_TYPE.ATTRIBUTE:
|
||||||
|
if (node instanceof HTMLElement) {
|
||||||
|
s += node.getAttribute(token.value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TOKEN_TYPE.COUNTER:
|
||||||
|
const counter = data.counters[token.name];
|
||||||
|
if (counter) {
|
||||||
|
s += formatCounterValue([counter[counter.length - 1]], '', token.format);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TOKEN_TYPE.COUNTERS:
|
||||||
|
const counters = data.counters[token.name];
|
||||||
|
if (counters) {
|
||||||
|
s += formatCounterValue(counters, token.glue, token.format);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TOKEN_TYPE.OPENQUOTE:
|
||||||
|
s += getQuote(style, true, data.quoteDepth);
|
||||||
|
data.quoteDepth++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TOKEN_TYPE.CLOSEQUOTE:
|
||||||
|
data.quoteDepth--;
|
||||||
|
s += getQuote(style, false, data.quoteDepth);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TOKEN_TYPE.URL:
|
||||||
|
if (s) {
|
||||||
|
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.TEXT, value: s});
|
||||||
|
s = '';
|
||||||
|
}
|
||||||
|
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.IMAGE, value: token.value});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s) {
|
||||||
|
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.TEXT, value: s});
|
||||||
|
}
|
||||||
|
|
||||||
|
return contentItems;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Token = {
|
||||||
|
type: string,
|
||||||
|
value: ?string
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseContent = (content: string, cache: ?{[string]: string}): Array<Token> => {
|
||||||
|
if (cache && cache[content]) {
|
||||||
|
return cache[content];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens: Array<Token> = [];
|
||||||
|
const len = content.length;
|
||||||
|
|
||||||
|
let isString = false;
|
||||||
|
let isEscaped = false;
|
||||||
|
let isFunction = false;
|
||||||
|
let str = '';
|
||||||
|
let functionName = '';
|
||||||
|
let args = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const c = content.charAt(i);
|
||||||
|
|
||||||
|
switch (c) {
|
||||||
|
case "'":
|
||||||
|
case '"':
|
||||||
|
if (isEscaped) {
|
||||||
|
str += c;
|
||||||
|
} else {
|
||||||
|
isString = !isString;
|
||||||
|
if (!isFunction && !isString) {
|
||||||
|
tokens.push({type: TOKEN_TYPE.STRING, value: str});
|
||||||
|
str = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '\\':
|
||||||
|
if (isEscaped) {
|
||||||
|
str += c;
|
||||||
|
isEscaped = false;
|
||||||
|
} else {
|
||||||
|
isEscaped = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '(':
|
||||||
|
if (isString) {
|
||||||
|
str += c;
|
||||||
|
} else {
|
||||||
|
isFunction = true;
|
||||||
|
functionName = str;
|
||||||
|
str = '';
|
||||||
|
args = [];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ')':
|
||||||
|
if (isString) {
|
||||||
|
str += c;
|
||||||
|
} else if (isFunction) {
|
||||||
|
if (str) {
|
||||||
|
args.push(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (functionName) {
|
||||||
|
case 'attr':
|
||||||
|
if (args.length > 0) {
|
||||||
|
tokens.push({type: TOKEN_TYPE.ATTRIBUTE, value: args[0]});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'counter':
|
||||||
|
if (args.length > 0) {
|
||||||
|
const counter: Token = {
|
||||||
|
type: TOKEN_TYPE.COUNTER,
|
||||||
|
name: args[0]
|
||||||
|
};
|
||||||
|
if (args.length > 1) {
|
||||||
|
counter.format = args[1];
|
||||||
|
}
|
||||||
|
tokens.push(counter);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'counters':
|
||||||
|
if (args.length > 0) {
|
||||||
|
const counters: Token = {
|
||||||
|
type: TOKEN_TYPE.COUNTERS,
|
||||||
|
name: args[0]
|
||||||
|
};
|
||||||
|
if (args.length > 1) {
|
||||||
|
counters.glue = args[1];
|
||||||
|
}
|
||||||
|
if (args.length > 2) {
|
||||||
|
counters.format = args[2];
|
||||||
|
}
|
||||||
|
tokens.push(counters);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'url':
|
||||||
|
if (args.length > 0) {
|
||||||
|
tokens.push({type: TOKEN_TYPE.URL, value: args[0]});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
isFunction = false;
|
||||||
|
str = '';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ',':
|
||||||
|
if (isString) {
|
||||||
|
str += c;
|
||||||
|
} else if (isFunction) {
|
||||||
|
args.push(str);
|
||||||
|
str = '';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ' ':
|
||||||
|
case '\t':
|
||||||
|
if (isString) {
|
||||||
|
str += c;
|
||||||
|
} else if (str) {
|
||||||
|
addOtherToken(tokens, str);
|
||||||
|
str = '';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
str += c;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c !== '\\') {
|
||||||
|
isEscaped = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str) {
|
||||||
|
addOtherToken(tokens, str);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cache) {
|
||||||
|
cache[content] = tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addOtherToken = (tokens: Array<Token>, identifier: string): Token => {
|
||||||
|
switch (identifier) {
|
||||||
|
case 'open-quote':
|
||||||
|
tokens.push({type: TOKEN_TYPE.OPENQUOTE});
|
||||||
|
break;
|
||||||
|
case 'close-quote':
|
||||||
|
tokens.push({type: TOKEN_TYPE.CLOSEQUOTE});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getQuote = (style: CSSStyleDeclaration, isOpening: boolean, quoteDepth: number): string => {
|
||||||
|
const quotes = style.quotes ? style.quotes.split(/\s+/) : ["'\"'", "'\"'"];
|
||||||
|
let idx = quoteDepth * 2;
|
||||||
|
if (idx >= quotes.length) {
|
||||||
|
idx = quotes.length - 2;
|
||||||
|
}
|
||||||
|
if (!isOpening) {
|
||||||
|
++idx;
|
||||||
|
}
|
||||||
|
return quotes[idx].replace(/^["']|["']$/g, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCounterValue = (counter, glue: string, format: string): string => {
|
||||||
|
const len = counter.length;
|
||||||
|
let result = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
result += glue;
|
||||||
|
}
|
||||||
|
result += ListStyleTypeFormatter.format(counter[i], format, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
111
tests/node/pseudonodecontent.js
Normal file
111
tests/node/pseudonodecontent.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
const PseudoNodeContent = require('../../dist/npm/PseudoNodeContent');
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
describe('PseudoNodeContent', function() {
|
||||||
|
it('should parse string', function() {
|
||||||
|
assert.deepEqual(PseudoNodeContent.parseContent('"hello"'), [
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: 'hello'}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse string with (,)', function() {
|
||||||
|
assert.deepEqual(PseudoNodeContent.parseContent('"a,b (c) d"'), [
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: 'a,b (c) d'}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse string with escaped quotes', function() {
|
||||||
|
assert.deepEqual(PseudoNodeContent.parseContent('"3.14\\""'), [
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: '3.14"'}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse string with escape', function() {
|
||||||
|
assert.deepEqual(PseudoNodeContent.parseContent('"a\\) \\\\ b"'), [
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: 'a) \\ b'}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse two strings', function() {
|
||||||
|
assert.deepEqual(PseudoNodeContent.parseContent('"hello" \'world\''), [
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: 'hello'},
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: 'world'}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse counter', function() {
|
||||||
|
assert.deepEqual(PseudoNodeContent.parseContent('counter(x)'), [
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.COUNTER, name: 'x'}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse counters', function() {
|
||||||
|
assert.deepEqual(PseudoNodeContent.parseContent('counters(x, "-")'), [
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.COUNTERS, name: 'x', glue: '-'}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse strings and counters', function() {
|
||||||
|
assert.deepEqual(PseudoNodeContent.parseContent('"["counters(c2, " < ") \']\''), [
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: '['},
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.COUNTERS, name: 'c2', glue: ' < '},
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: ']'}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse counter with format', function() {
|
||||||
|
assert.deepEqual(PseudoNodeContent.parseContent('counter(x, lower-greek)'), [
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.COUNTER, name: 'x', format: 'lower-greek'}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse counters with format', function() {
|
||||||
|
assert.deepEqual(PseudoNodeContent.parseContent('counters(x, "-", upper-roman)'), [
|
||||||
|
{
|
||||||
|
type: PseudoNodeContent.TOKEN_TYPE.COUNTERS,
|
||||||
|
name: 'x',
|
||||||
|
glue: '-',
|
||||||
|
format: 'upper-roman'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse strings and counters with format', function() {
|
||||||
|
assert.deepEqual(PseudoNodeContent.parseContent("\"[\"counters(c2, ' < ', disc) ']'"), [
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: '['},
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.COUNTERS, name: 'c2', glue: ' < ', format: 'disc'},
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: ']'}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse attr', function() {
|
||||||
|
assert.deepEqual(PseudoNodeContent.parseContent('attr(id)'), [
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.ATTRIBUTE, value: 'id'}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse url', function() {
|
||||||
|
assert.deepEqual(PseudoNodeContent.parseContent('url(http://www.abc.de/f/g.png)'), [
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.URL, value: 'http://www.abc.de/f/g.png'}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse open-quote', function() {
|
||||||
|
assert.deepEqual(PseudoNodeContent.parseContent('open-quote'), [
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.OPENQUOTE}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse close-quote', function() {
|
||||||
|
assert.deepEqual(PseudoNodeContent.parseContent('close-quote'), [
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.CLOSEQUOTE}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse open-quote and string', function() {
|
||||||
|
assert.deepEqual(PseudoNodeContent.parseContent('open-quote "!"'), [
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.OPENQUOTE},
|
||||||
|
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: '!'}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
104
tests/reftests/pseudo-content.html
Normal file
104
tests/reftests/pseudo-content.html
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script type="text/javascript" src="../test.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
quotes: "<{" "}>" "->" "<-" "(" ")" "-:" ":-";
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter1,
|
||||||
|
.counter2,
|
||||||
|
.quotes1,
|
||||||
|
.attr-url {
|
||||||
|
border: 1px solid deepskyblue;
|
||||||
|
padding: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter1 {
|
||||||
|
counter-reset: c1 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter1 > div::before {
|
||||||
|
content: "«\"" counter(c1) "\»";
|
||||||
|
counter-increment: c1 -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter2 {
|
||||||
|
counter-reset: c2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter2 > div::before {
|
||||||
|
content: "["counters(c2, " < ", upper-roman) ']';
|
||||||
|
counter-increment: c2 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quotes1::before {
|
||||||
|
content: open-quote "!" open-quote close-quote open-quote;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quotes1::after {
|
||||||
|
content: "!" close-quote close-quote;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quotes2 {
|
||||||
|
quotes: "«" "»" "“" "”";
|
||||||
|
}
|
||||||
|
|
||||||
|
.quotes2::before {
|
||||||
|
content: open-quote;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quotes2::after {
|
||||||
|
content: close-quote;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attr-url > div::after {
|
||||||
|
content: url(../assets/image.jpg) "///" attr(data-text);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="counter1">
|
||||||
|
<div>A</div>
|
||||||
|
<div>B</div>
|
||||||
|
<div>C</div>
|
||||||
|
<div>D</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="counter2">
|
||||||
|
<div>A</div>
|
||||||
|
<div>B</div>
|
||||||
|
<div>
|
||||||
|
C
|
||||||
|
<div class="counter2">
|
||||||
|
<div>a</div>
|
||||||
|
<div>b</div>
|
||||||
|
<div>
|
||||||
|
c
|
||||||
|
<div class="counter2">
|
||||||
|
<div>Aa</div>
|
||||||
|
<div>Bb</div>
|
||||||
|
<div>Cc</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>D</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="quotes1">
|
||||||
|
Hello
|
||||||
|
<div class="quotes2">
|
||||||
|
Quoted
|
||||||
|
<div class="quotes2">World</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="attr-url">
|
||||||
|
<div data-text="Hello World"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user