From 78c3c7fc7159925c5a2ec93d0527879aa1e0020e Mon Sep 17 00:00:00 2001 From: Matthias Christen Date: Fri, 15 Dec 2017 12:40:04 +0100 Subject: [PATCH 1/3] improved support of 'content' for pseudo elements (multiple components, counters, attr, quotes) --- docs/features.md | 2 +- package.json | 3 +- src/Clone.js | 80 +++++-- src/PseudoNodeContent.js | 340 +++++++++++++++++++++++++++++ tests/node/pseudonodecontent.js | 111 ++++++++++ tests/reftests/pseudo-content.html | 104 +++++++++ 6 files changed, 615 insertions(+), 25 deletions(-) create mode 100644 src/PseudoNodeContent.js create mode 100644 tests/node/pseudonodecontent.js create mode 100644 tests/reftests/pseudo-content.html diff --git a/docs/features.md b/docs/features.md index 72ea435..834631f 100644 --- a/docs/features.md +++ b/docs/features.md @@ -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 diff --git a/package.json b/package.json index d6a464e..1f7e9d7 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "homepage": "https://html2canvas.hertzen.com", "license": "MIT", "dependencies": { - "punycode": "2.1.0" + "punycode": "2.1.0", + "liststyletype-formatter": "latest" } } diff --git a/src/Clone.js b/src/Clone.js index c60ca9d..58f8aae 100644 --- a/src/Clone.js +++ b/src/Clone.js @@ -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); } @@ -226,6 +238,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; @@ -235,6 +252,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 || @@ -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) { - 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) { @@ -361,9 +393,10 @@ const cloneCanvasContents = (canvas: HTMLCanvasElement, clonedCanvas: HTMLCanvas const inlinePseudoElement = ( node: HTMLElement, clone: HTMLElement, + style: CSSStyleDeclaration, + contentItems: Array, pseudoElt: ':before' | ':after' ): ?HTMLElement => { - const style = node.ownerDocument.defaultView.getComputedStyle(node, pseudoElt); if ( !style || !style.content || @@ -374,20 +407,28 @@ 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'); + const len = contentItems.length; 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}`; clone.className += pseudoElt === PSEUDO_BEFORE @@ -402,13 +443,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'; diff --git a/src/PseudoNodeContent.js b/src/PseudoNodeContent.js new file mode 100644 index 0000000..dcd6b03 --- /dev/null +++ b/src/PseudoNodeContent.js @@ -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}, + quoteDepth: number +}; + +export type PseudoContentItem = { + type: $Values, + value: string +}; + +export type Token = { + type: $Values, + value: ?string, + format: ?string, + glue: ?string +}; + +export const parseCounterReset = ( + style: CSSStyleDeclaration, + data: PseudoContentData +): Array => { + if (!style || !style.counterReset || style.counterReset === 'none') { + return []; + } + + const counterNames: Array = []; + 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, 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 => { + 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 = []; + 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 => { + if (cache && cache[content]) { + return cache[content]; + } + + const tokens: Array = []; + 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, 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; +}; diff --git a/tests/node/pseudonodecontent.js b/tests/node/pseudonodecontent.js new file mode 100644 index 0000000..b481c33 --- /dev/null +++ b/tests/node/pseudonodecontent.js @@ -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: '!'} + ]); + }); +}); diff --git a/tests/reftests/pseudo-content.html b/tests/reftests/pseudo-content.html new file mode 100644 index 0000000..bac5796 --- /dev/null +++ b/tests/reftests/pseudo-content.html @@ -0,0 +1,104 @@ + + + + + + + + +
+
A
+
B
+
C
+
D
+
+ +
+
A
+
B
+
+ C +
+
a
+
b
+
+ c +
+
Aa
+
Bb
+
Cc
+
+
+
+
+
D
+
+ +
+ Hello +
+ Quoted +
World
+
+
+ +
+
+
+ + \ No newline at end of file From 6d0cd2d226c6cc4a62067f846fe216767959f782 Mon Sep 17 00:00:00 2001 From: Matthias Christen Date: Fri, 15 Dec 2017 23:46:26 +0100 Subject: [PATCH 2/3] fixed flow problems in PseudoNodeContent.js --- src/Clone.js | 35 ++++++++++++++++---------------- src/PseudoNodeContent.js | 43 ++++++++++++++++++++-------------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/Clone.js b/src/Clone.js index 58f8aae..de68549 100644 --- a/src/Clone.js +++ b/src/Clone.js @@ -394,7 +394,7 @@ const inlinePseudoElement = ( node: HTMLElement, clone: HTMLElement, style: CSSStyleDeclaration, - contentItems: Array, + contentItems: ?Array, pseudoElt: ':before' | ':after' ): ?HTMLElement => { if ( @@ -408,24 +408,25 @@ const inlinePseudoElement = ( } const anonymousReplacedElement = clone.ownerDocument.createElement('html2canvaspseudoelement'); - const len = contentItems.length; - 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; + 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; + } } } diff --git a/src/PseudoNodeContent.js b/src/PseudoNodeContent.js index dcd6b03..74890e0 100644 --- a/src/PseudoNodeContent.js +++ b/src/PseudoNodeContent.js @@ -1,3 +1,6 @@ +/* @flow */ +'use strict'; + import ListStyleTypeFormatter from 'liststyletype-formatter'; export const PSEUDO_CONTENT_ITEM_TYPE = { @@ -27,13 +30,14 @@ export type PseudoContentItem = { export type Token = { type: $Values, - value: ?string, - format: ?string, - glue: ?string + value?: string, + name?: string, + format?: string, + glue?: string }; export const parseCounterReset = ( - style: CSSStyleDeclaration, + style: ?CSSStyleDeclaration, data: PseudoContentData ): Array => { if (!style || !style.counterReset || style.counterReset === 'none') { @@ -66,9 +70,9 @@ export const popCounters = (counterNames: Array, data: PseudoContentData export const resolvePseudoContent = ( node: Node, - style: CSSStyleDeclaration, + style: ?CSSStyleDeclaration, data: PseudoContentData -): Array => { +): ?Array => { if ( !style || !style.content || @@ -100,24 +104,24 @@ export const resolvePseudoContent = ( const token = tokens[i]; switch (token.type) { case TOKEN_TYPE.STRING: - s += token.value; + s += token.value || ''; break; case TOKEN_TYPE.ATTRIBUTE: - if (node instanceof HTMLElement) { - s += node.getAttribute(token.value); + if (node instanceof HTMLElement && token.value) { + s += node.getAttribute(token.value) || ''; } break; case TOKEN_TYPE.COUNTER: - const counter = data.counters[token.name]; + 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]; + const counters = data.counters[token.name || '']; if (counters) { s += formatCounterValue(counters, token.glue, token.format); } @@ -138,7 +142,7 @@ export const resolvePseudoContent = ( contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.TEXT, value: s}); s = ''; } - contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.IMAGE, value: token.value}); + contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.IMAGE, value: token.value || ''}); break; } } @@ -150,12 +154,7 @@ export const resolvePseudoContent = ( return contentItems; }; -type Token = { - type: string, - value: ?string -}; - -export const parseContent = (content: string, cache: ?{[string]: string}): Array => { +export const parseContent = (content: string, cache?: {[string]: Array}): Array => { if (cache && cache[content]) { return cache[content]; } @@ -302,7 +301,7 @@ export const parseContent = (content: string, cache: ?{[string]: string}): Array return tokens; }; -const addOtherToken = (tokens: Array, identifier: string): Token => { +const addOtherToken = (tokens: Array, identifier: string): void => { switch (identifier) { case 'open-quote': tokens.push({type: TOKEN_TYPE.OPENQUOTE}); @@ -325,15 +324,15 @@ const getQuote = (style: CSSStyleDeclaration, isOpening: boolean, quoteDepth: nu return quotes[idx].replace(/^["']|["']$/g, ''); }; -const formatCounterValue = (counter, glue: string, format: string): string => { +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 += glue || ''; } - result += ListStyleTypeFormatter.format(counter[i], format, false); + result += ListStyleTypeFormatter.format(counter[i], format || 'decimal', false); } return result; From 9046e0d55419c82ac38289b81f1c7280215ee76d Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Sun, 24 Dec 2017 17:08:54 +0800 Subject: [PATCH 3/3] Update to use list style parser from ListItem --- package-lock.json | 2 +- package.json | 3 +- src/ListItem.js | 131 +++++++++++++++++++++++++-------------- src/PseudoNodeContent.js | 8 ++- src/parsing/listStyle.js | 2 +- 5 files changed, 93 insertions(+), 53 deletions(-) diff --git a/package-lock.json b/package-lock.json index 535fe27..bd4d9d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "html2canvas", - "version": "1.0.0-alpha.4", + "version": "1.0.0-alpha.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a0bbbc1..732907d 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,6 @@ "homepage": "https://html2canvas.hertzen.com", "license": "MIT", "dependencies": { - "punycode": "2.1.0", - "liststyletype-formatter": "latest" + "punycode": "2.1.0" } } diff --git a/src/ListItem.js b/src/ListItem.js index 65ee9f8..e3c7e19 100644 --- a/src/ListItem.js +++ b/src/ListItem.js @@ -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); } }; diff --git a/src/PseudoNodeContent.js b/src/PseudoNodeContent.js index 74890e0..b2f36fd 100644 --- a/src/PseudoNodeContent.js +++ b/src/PseudoNodeContent.js @@ -1,7 +1,8 @@ /* @flow */ 'use strict'; -import ListStyleTypeFormatter from 'liststyletype-formatter'; +import {createCounterText} from './ListItem'; +import {parseListStyleType} from './parsing/listStyle'; export const PSEUDO_CONTENT_ITEM_TYPE = { TEXT: 0, @@ -84,6 +85,9 @@ export const resolvePseudoContent = ( } const tokens = parseContent(style.content); + console.log(style.content); + console.log(tokens); + const len = tokens.length; const contentItems: Array = []; let s = ''; @@ -332,7 +336,7 @@ const formatCounterValue = (counter, glue: ?string, format: ?string): string => if (i > 0) { result += glue || ''; } - result += ListStyleTypeFormatter.format(counter[i], format || 'decimal', false); + result += createCounterText(counter[i], parseListStyleType(format || 'decimal'), false); } return result; diff --git a/src/parsing/listStyle.js b/src/parsing/listStyle.js index f9d2c14..e66c91b 100644 --- a/src/parsing/listStyle.js +++ b/src/parsing/listStyle.js @@ -75,7 +75,7 @@ export const LIST_STYLE_TYPE = { export type ListStylePosition = $Values; export type ListStyleType = $Values; -const parseListStyleType = (type: string): ListStyleType => { +export const parseListStyleType = (type: string): ListStyleType => { switch (type) { case 'disc': return LIST_STYLE_TYPE.DISC;