Merge branch 'vnmc-feature/HTC-0010_PseudoContent'

This commit is contained in:
Niklas von Hertzen 2017-12-24 17:09:27 +08:00
commit 22f58d5d1c
8 changed files with 704 additions and 74 deletions

View File

@ -22,7 +22,7 @@ Below is a list of all the supported CSS properties and values.
- border-width
- bottom
- box-sizing
- content (**Does not support `attr()`**)
- content
- color
- display
- flex

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "html2canvas",
"version": "1.0.0-alpha.4",
"version": "1.0.0-alpha.5",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -2,6 +2,7 @@
'use strict';
import type {Bounds} from './Bounds';
import type {Options} from './index';
import type {PseudoContentData, PseudoContentItem} from './PseudoNodeContent';
import type Logger from './Logger';
import {parseBounds} from './Bounds';
@ -10,6 +11,12 @@ import ResourceLoader from './ResourceLoader';
import {copyCSSStyles} from './Util';
import {parseBackgroundImage} from './parsing/background';
import CanvasRenderer from './renderer/CanvasRenderer';
import {
parseCounterReset,
popCounters,
resolvePseudoContent,
PSEUDO_CONTENT_ITEM_TYPE
} from './PseudoNodeContent';
const IGNORE_ATTRIBUTE = 'data-html2canvas-ignore';
@ -24,6 +31,7 @@ export class DocumentCloner {
inlineImages: boolean;
copyStyles: boolean;
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>;
pseudoContentData: PseudoContentData;
constructor(
element: HTMLElement,
@ -40,6 +48,10 @@ export class DocumentCloner {
this.options = options;
this.renderer = renderer;
this.resourceLoader = new ResourceLoader(options, logger, window);
this.pseudoContentData = {
counters: {},
quoteDepth: 0
};
// $FlowFixMe
this.documentElement = this.cloneNode(element.ownerDocument.documentElement);
}
@ -227,6 +239,11 @@ export class DocumentCloner {
: this.createElementClone(node);
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) {
this.clonedReferenceElement = clone;
@ -236,6 +253,9 @@ export class DocumentCloner {
createPseudoHideStyles(clone);
}
const counters = parseCounterReset(style, this.pseudoContentData);
const contentBefore = resolvePseudoContent(node, styleBefore, this.pseudoContentData);
for (let child = node.firstChild; child; child = child.nextSibling) {
if (
child.nodeType !== Node.ELEMENT_NODE ||
@ -247,11 +267,23 @@ export class DocumentCloner {
}
}
}
const contentAfter = resolvePseudoContent(node, styleAfter, this.pseudoContentData);
popCounters(counters, this.pseudoContentData);
if (node instanceof window.HTMLElement && clone instanceof window.HTMLElement) {
this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_BEFORE));
this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_AFTER));
if (this.copyStyles && !(node instanceof HTMLIFrameElement)) {
copyCSSStyles(node.ownerDocument.defaultView.getComputedStyle(node), clone);
if (styleBefore) {
this.inlineAllImages(
inlinePseudoElement(node, clone, styleBefore, contentBefore, PSEUDO_BEFORE)
);
}
if (styleAfter) {
this.inlineAllImages(
inlinePseudoElement(node, clone, styleAfter, contentAfter, PSEUDO_AFTER)
);
}
if (style && this.copyStyles && !(node instanceof HTMLIFrameElement)) {
copyCSSStyles(style, clone);
}
this.inlineAllImages(clone);
if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
@ -362,9 +394,10 @@ const cloneCanvasContents = (canvas: HTMLCanvasElement, clonedCanvas: HTMLCanvas
const inlinePseudoElement = (
node: HTMLElement,
clone: HTMLElement,
style: CSSStyleDeclaration,
contentItems: ?Array<PseudoContentItem>,
pseudoElt: ':before' | ':after'
): ?HTMLElement => {
const style = node.ownerDocument.defaultView.getComputedStyle(node, pseudoElt);
if (
!style ||
!style.content ||
@ -375,20 +408,29 @@ const inlinePseudoElement = (
return;
}
const content = stripQuotes(style.content);
const image = content.match(URL_REGEXP);
const anonymousReplacedElement = clone.ownerDocument.createElement(
image ? 'img' : 'html2canvaspseudoelement'
);
if (image) {
// $FlowFixMe
anonymousReplacedElement.src = stripQuotes(image[1]);
} else {
anonymousReplacedElement.textContent = content;
}
const anonymousReplacedElement = clone.ownerDocument.createElement('html2canvaspseudoelement');
copyCSSStyles(style, anonymousReplacedElement);
if (contentItems) {
const len = contentItems.length;
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}`;
clone.className +=
pseudoElt === PSEUDO_BEFORE
@ -403,13 +445,6 @@ const inlinePseudoElement = (
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 PSEUDO_BEFORE = ':before';
const PSEUDO_AFTER = ':after';

View File

@ -92,7 +92,7 @@ export const inlineListItemElement = (
}
} else if (typeof container.listIndex === 'number') {
text = node.ownerDocument.createTextNode(
createCounterText(container.listIndex, listStyle.listStyleType)
createCounterText(container.listIndex, listStyle.listStyleType, true)
);
wrapper.appendChild(text);
wrapper.style.top = `${container.bounds.top - MARGIN_TOP}px`;
@ -363,10 +363,10 @@ const createAdditiveCounter = (
max: number,
symbols,
fallback: ListStyleType,
suffix: string = '. '
suffix: string
) => {
if (value < min || value > max) {
return createCounterText(value, fallback);
return createCounterText(value, fallback, suffix.length > 0);
}
return (
@ -404,7 +404,7 @@ const createCounterStyleFromRange = (
codePointRangeStart: number,
codePointRangeEnd: number,
isNumeric: boolean,
suffix: string = '. '
suffix: string
): string => {
const codePointRangeLength = codePointRangeEnd - codePointRangeStart + 1;
@ -451,7 +451,7 @@ const createCJKCounter = (
flags: number
): string => {
if (value < -9999 || value > 9999) {
return createCounterText(value, LIST_STYLE_TYPE.CJK_DECIMAL);
return createCounterText(value, LIST_STYLE_TYPE.CJK_DECIMAL, suffix.length > 0);
}
let tmp = Math.abs(value);
let string = suffix;
@ -490,7 +490,14 @@ const CHINESE_FORMAL_MULTIPLIERS = '拾佰仟萬';
const JAPANESE_NEGATIVE = 'マイナス';
const KOREAN_NEGATIVE = '마이너스 ';
const createCounterText = (value: number, type: ListStyleType): string => {
export const createCounterText = (
value: number,
type: ListStyleType,
appendSuffix: boolean
): string => {
const defaultSuffix = appendSuffix ? '. ' : '';
const cjkSuffix = appendSuffix ? '、' : '';
const koreanSuffix = appendSuffix ? ', ' : '';
switch (type) {
case LIST_STYLE_TYPE.DISC:
return '•';
@ -499,48 +506,64 @@ const createCounterText = (value: number, type: ListStyleType): string => {
case LIST_STYLE_TYPE.SQUARE:
return '◾';
case LIST_STYLE_TYPE.DECIMAL_LEADING_ZERO:
const string = createCounterStyleFromRange(value, 48, 57, true);
const string = createCounterStyleFromRange(value, 48, 57, true, defaultSuffix);
return string.length < 4 ? `0${string}` : string;
case LIST_STYLE_TYPE.CJK_DECIMAL:
return createCounterStyleFromSymbols(value, '〇一二三四五六七八九', '、');
return createCounterStyleFromSymbols(value, '〇一二三四五六七八九', cjkSuffix);
case LIST_STYLE_TYPE.LOWER_ROMAN:
return createAdditiveCounter(
value,
1,
3999,
ROMAN_UPPER,
LIST_STYLE_TYPE.DECIMAL
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
).toLowerCase();
case LIST_STYLE_TYPE.UPPER_ROMAN:
return createAdditiveCounter(value, 1, 3999, ROMAN_UPPER, LIST_STYLE_TYPE.DECIMAL);
return createAdditiveCounter(
value,
1,
3999,
ROMAN_UPPER,
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
);
case LIST_STYLE_TYPE.LOWER_GREEK:
return createCounterStyleFromRange(value, 945, 969, false);
return createCounterStyleFromRange(value, 945, 969, false, defaultSuffix);
case LIST_STYLE_TYPE.LOWER_ALPHA:
return createCounterStyleFromRange(value, 97, 122, false);
return createCounterStyleFromRange(value, 97, 122, false, defaultSuffix);
case LIST_STYLE_TYPE.UPPER_ALPHA:
return createCounterStyleFromRange(value, 65, 90, false);
return createCounterStyleFromRange(value, 65, 90, false, defaultSuffix);
case LIST_STYLE_TYPE.ARABIC_INDIC:
return createCounterStyleFromRange(value, 1632, 1641, true);
return createCounterStyleFromRange(value, 1632, 1641, true, defaultSuffix);
case LIST_STYLE_TYPE.ARMENIAN:
case LIST_STYLE_TYPE.UPPER_ARMENIAN:
return createAdditiveCounter(value, 1, 9999, ARMENIAN, LIST_STYLE_TYPE.DECIMAL);
return createAdditiveCounter(
value,
1,
9999,
ARMENIAN,
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
);
case LIST_STYLE_TYPE.LOWER_ARMENIAN:
return createAdditiveCounter(
value,
1,
9999,
ARMENIAN,
LIST_STYLE_TYPE.DECIMAL
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
).toLowerCase();
case LIST_STYLE_TYPE.BENGALI:
return createCounterStyleFromRange(value, 2534, 2543, true);
return createCounterStyleFromRange(value, 2534, 2543, true, defaultSuffix);
case LIST_STYLE_TYPE.CAMBODIAN:
case LIST_STYLE_TYPE.KHMER:
return createCounterStyleFromRange(value, 6112, 6121, true);
return createCounterStyleFromRange(value, 6112, 6121, true, defaultSuffix);
case LIST_STYLE_TYPE.CJK_EARTHLY_BRANCH:
return createCounterStyleFromSymbols(value, '子丑寅卯辰巳午未申酉戌亥', '、');
return createCounterStyleFromSymbols(value, '子丑寅卯辰巳午未申酉戌亥', cjkSuffix);
case LIST_STYLE_TYPE.CJK_HEAVENLY_STEM:
return createCounterStyleFromSymbols(value, '甲乙丙丁戊己庚辛壬癸', '、');
return createCounterStyleFromSymbols(value, '甲乙丙丁戊己庚辛壬癸', cjkSuffix);
case LIST_STYLE_TYPE.CJK_IDEOGRAPHIC:
case LIST_STYLE_TYPE.TRAD_CHINESE_INFORMAL:
return createCJKCounter(
@ -548,7 +571,7 @@ const createCounterText = (value: number, type: ListStyleType): string => {
'零一二三四五六七八九',
CHINESE_INFORMAL_MULTIPLIERS,
'負',
'、',
cjkSuffix,
CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS
);
case LIST_STYLE_TYPE.TRAD_CHINESE_FORMAL:
@ -557,7 +580,7 @@ const createCounterText = (value: number, type: ListStyleType): string => {
'零壹貳參肆伍陸柒捌玖',
CHINESE_FORMAL_MULTIPLIERS,
'負',
'、',
cjkSuffix,
CJK_ZEROS |
CJK_TEN_COEFFICIENTS |
CJK_TEN_HIGH_COEFFICIENTS |
@ -569,7 +592,7 @@ const createCounterText = (value: number, type: ListStyleType): string => {
'零一二三四五六七八九',
CHINESE_INFORMAL_MULTIPLIERS,
'负',
'、',
cjkSuffix,
CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS
);
case LIST_STYLE_TYPE.SIMP_CHINESE_FORMAL:
@ -578,21 +601,21 @@ const createCounterText = (value: number, type: ListStyleType): string => {
'零壹贰叁肆伍陆柒捌玖',
CHINESE_FORMAL_MULTIPLIERS,
'负',
'、',
cjkSuffix,
CJK_ZEROS |
CJK_TEN_COEFFICIENTS |
CJK_TEN_HIGH_COEFFICIENTS |
CJK_HUNDRED_COEFFICIENTS
);
case LIST_STYLE_TYPE.JAPANESE_INFORMAL:
return createCJKCounter(value, '〇一二三四五六七八九', '十百千万', JAPANESE_NEGATIVE, '、', 0);
return createCJKCounter(value, '〇一二三四五六七八九', '十百千万', JAPANESE_NEGATIVE, cjkSuffix, 0);
case LIST_STYLE_TYPE.JAPANESE_FORMAL:
return createCJKCounter(
value,
'零壱弐参四伍六七八九',
'拾百千万',
JAPANESE_NEGATIVE,
'、',
cjkSuffix,
CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS
);
case LIST_STYLE_TYPE.KOREAN_HANGUL_FORMAL:
@ -601,30 +624,44 @@ const createCounterText = (value: number, type: ListStyleType): string => {
'영일이삼사오육칠팔구',
'십백천만',
KOREAN_NEGATIVE,
', ',
koreanSuffix,
CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS
);
case LIST_STYLE_TYPE.KOREAN_HANJA_INFORMAL:
return createCJKCounter(value, '零一二三四五六七八九', '十百千萬', KOREAN_NEGATIVE, ', ', 0);
return createCJKCounter(value, '零一二三四五六七八九', '十百千萬', KOREAN_NEGATIVE, koreanSuffix, 0);
case LIST_STYLE_TYPE.KOREAN_HANJA_FORMAL:
return createCJKCounter(
value,
'零壹貳參四五六七八九',
'拾百千',
KOREAN_NEGATIVE,
', ',
koreanSuffix,
CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS
);
case LIST_STYLE_TYPE.DEVANAGARI:
return createCounterStyleFromRange(value, 0x966, 0x96f, true);
return createCounterStyleFromRange(value, 0x966, 0x96f, true, defaultSuffix);
case LIST_STYLE_TYPE.GEORGIAN:
return createAdditiveCounter(value, 1, 19999, GEORGIAN, LIST_STYLE_TYPE.DECIMAL);
return createAdditiveCounter(
value,
1,
19999,
GEORGIAN,
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
);
case LIST_STYLE_TYPE.GUJARATI:
return createCounterStyleFromRange(value, 0xae6, 0xaef, true);
return createCounterStyleFromRange(value, 0xae6, 0xaef, true, defaultSuffix);
case LIST_STYLE_TYPE.GURMUKHI:
return createCounterStyleFromRange(value, 0xa66, 0xa6f, true);
return createCounterStyleFromRange(value, 0xa66, 0xa6f, true, defaultSuffix);
case LIST_STYLE_TYPE.HEBREW:
return createAdditiveCounter(value, 1, 10999, HEBREW, LIST_STYLE_TYPE.DECIMAL);
return createAdditiveCounter(
value,
1,
10999,
HEBREW,
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
);
case LIST_STYLE_TYPE.HIRAGANA:
return createCounterStyleFromSymbols(
value,
@ -636,39 +673,39 @@ const createCounterText = (value: number, type: ListStyleType): string => {
'いろはにほへとちりぬるをわかよたれそつねならむうゐのおくやまけふこえてあさきゆめみしゑひもせす'
);
case LIST_STYLE_TYPE.KANNADA:
return createCounterStyleFromRange(value, 0xce6, 0xcef, true);
return createCounterStyleFromRange(value, 0xce6, 0xcef, true, defaultSuffix);
case LIST_STYLE_TYPE.KATAKANA:
return createCounterStyleFromSymbols(
value,
'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヰヱヲン',
'、'
cjkSuffix
);
case LIST_STYLE_TYPE.KATAKANA_IROHA:
return createCounterStyleFromSymbols(
value,
'イロハニホヘトチリヌルヲワカヨタレソツネナラムウヰノオクヤマケフコエテアサキユメミシヱヒモセス',
'、'
cjkSuffix
);
case LIST_STYLE_TYPE.LAO:
return createCounterStyleFromRange(value, 0xed0, 0xed9, true);
return createCounterStyleFromRange(value, 0xed0, 0xed9, true, defaultSuffix);
case LIST_STYLE_TYPE.MONGOLIAN:
return createCounterStyleFromRange(value, 0x1810, 0x1819, true);
return createCounterStyleFromRange(value, 0x1810, 0x1819, true, defaultSuffix);
case LIST_STYLE_TYPE.MYANMAR:
return createCounterStyleFromRange(value, 0x1040, 0x1049, true);
return createCounterStyleFromRange(value, 0x1040, 0x1049, true, defaultSuffix);
case LIST_STYLE_TYPE.ORIYA:
return createCounterStyleFromRange(value, 0xb66, 0xb6f, true);
return createCounterStyleFromRange(value, 0xb66, 0xb6f, true, defaultSuffix);
case LIST_STYLE_TYPE.PERSIAN:
return createCounterStyleFromRange(value, 0x6f0, 0x6f9, true);
return createCounterStyleFromRange(value, 0x6f0, 0x6f9, true, defaultSuffix);
case LIST_STYLE_TYPE.TAMIL:
return createCounterStyleFromRange(value, 0xbe6, 0xbef, true);
return createCounterStyleFromRange(value, 0xbe6, 0xbef, true, defaultSuffix);
case LIST_STYLE_TYPE.TELUGU:
return createCounterStyleFromRange(value, 0xc66, 0xc6f, true);
return createCounterStyleFromRange(value, 0xc66, 0xc6f, true, defaultSuffix);
case LIST_STYLE_TYPE.THAI:
return createCounterStyleFromRange(value, 0xe50, 0xe59, true);
return createCounterStyleFromRange(value, 0xe50, 0xe59, true, defaultSuffix);
case LIST_STYLE_TYPE.TIBETAN:
return createCounterStyleFromRange(value, 0xf20, 0xf29, true);
return createCounterStyleFromRange(value, 0xf20, 0xf29, true, defaultSuffix);
case LIST_STYLE_TYPE.DECIMAL:
default:
return createCounterStyleFromRange(value, 48, 57, true);
return createCounterStyleFromRange(value, 48, 57, true, defaultSuffix);
}
};

343
src/PseudoNodeContent.js Normal file
View File

@ -0,0 +1,343 @@
/* @flow */
'use strict';
import {createCounterText} from './ListItem';
import {parseListStyleType} from './parsing/listStyle';
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,
name?: 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);
console.log(style.content);
console.log(tokens);
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 && token.value) {
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;
};
export const parseContent = (content: string, cache?: {[string]: Array<Token>}): 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): void => {
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 += createCounterText(counter[i], parseListStyleType(format || 'decimal'), false);
}
return result;
};

View File

@ -75,7 +75,7 @@ export const LIST_STYLE_TYPE = {
export type ListStylePosition = $Values<typeof LIST_STYLE_POSITION>;
export type ListStyleType = $Values<typeof LIST_STYLE_TYPE>;
const parseListStyleType = (type: string): ListStyleType => {
export const parseListStyleType = (type: string): ListStyleType => {
switch (type) {
case 'disc':
return LIST_STYLE_TYPE.DISC;

View 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: '!'}
]);
});
});

View 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>