diff --git a/.flowconfig b/.flowconfig index c4c5750..5807f3c 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,5 +1,6 @@ [ignore] .*/www/.* +.*/node_modules/css-line-break/scripts.* [include] [libs] ./flow-typed diff --git a/docs/features.md b/docs/features.md index 9f6d098..a6fa802 100644 --- a/docs/features.md +++ b/docs/features.md @@ -36,6 +36,7 @@ Below is a list of all the supported CSS properties and values. - height - left - letter-spacing + - line-break - list-style - list-style-image - list-style-position @@ -47,6 +48,7 @@ Below is a list of all the supported CSS properties and values. - min-width - opacity - overflow + - overflow-wrap - padding - position - right @@ -62,7 +64,9 @@ Below is a list of all the supported CSS properties and values. - visibility - white-space - width + - word-break - word-spacing + - word-wrap - z-index ## Unsupported CSS properties @@ -76,8 +80,6 @@ These CSS properties are **NOT** currently supported - [mix-blend-mode](https://github.com/niklasvh/html2canvas/issues/580) - [object-fit](https://github.com/niklasvh/html2canvas/issues/1064) - [repeating-linear-gradient()](https://github.com/niklasvh/html2canvas/issues/1162) - - word-break - - [word-wrap](https://github.com/niklasvh/html2canvas/issues/664) - [writing-mode](https://github.com/niklasvh/html2canvas/issues/1258) - [zoom](https://github.com/niklasvh/html2canvas/issues/732) diff --git a/package-lock.json b/package-lock.json index 5e6fc30..92aa64b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1056,8 +1056,7 @@ "base64-arraybuffer": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", - "dev": true + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" }, "base64-js": { "version": "1.2.1", @@ -1901,6 +1900,14 @@ "integrity": "sha1-zFRJaF37hesRyYKKzHy4erW7/MA=", "dev": true }, + "css-line-break": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.0.0.tgz", + "integrity": "sha1-m60drCLyTSiYFxqm1JxQuOrD/R4=", + "requires": { + "base64-arraybuffer": "0.1.5" + } + }, "cuid": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/cuid/-/cuid-1.3.8.tgz", @@ -5352,11 +5359,6 @@ } } }, - "punycode": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", - "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=" - }, "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", diff --git a/package.json b/package.json index 9baf006..e327de7 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,6 @@ "homepage": "https://html2canvas.hertzen.com", "license": "MIT", "dependencies": { - "punycode": "2.1.0" + "css-line-break": "1.0.0" } } diff --git a/src/NodeContainer.js b/src/NodeContainer.js index 76a7529..f233a38 100644 --- a/src/NodeContainer.js +++ b/src/NodeContainer.js @@ -7,9 +7,11 @@ import type {BorderRadius} from './parsing/borderRadius'; import type {DisplayBit} from './parsing/display'; import type {Float} from './parsing/float'; import type {Font} from './parsing/font'; +import type {LineBreak} from './parsing/lineBreak'; import type {ListStyle} from './parsing/listStyle'; import type {Margin} from './parsing/margin'; import type {Overflow} from './parsing/overflow'; +import type {OverflowWrap} from './parsing/overflowWrap'; import type {Padding} from './parsing/padding'; import type {Position} from './parsing/position'; import type {TextShadow} from './parsing/textShadow'; @@ -17,6 +19,7 @@ import type {TextTransform} from './parsing/textTransform'; import type {TextDecoration} from './parsing/textDecoration'; import type {Transform} from './parsing/transform'; import type {Visibility} from './parsing/visibility'; +import type {WordBreak} from './parsing/word-break'; import type {zIndex} from './parsing/zIndex'; import type {Bounds, BoundCurves} from './Bounds'; @@ -34,9 +37,11 @@ import {parseDisplay, DISPLAY} from './parsing/display'; import {parseCSSFloat, FLOAT} from './parsing/float'; import {parseFont} from './parsing/font'; import {parseLetterSpacing} from './parsing/letterSpacing'; +import {parseLineBreak} from './parsing/lineBreak'; import {parseListStyle} from './parsing/listStyle'; import {parseMargin} from './parsing/margin'; import {parseOverflow, OVERFLOW} from './parsing/overflow'; +import {parseOverflowWrap} from './parsing/overflowWrap'; import {parsePadding} from './parsing/padding'; import {parsePosition, POSITION} from './parsing/position'; import {parseTextDecoration} from './parsing/textDecoration'; @@ -44,6 +49,7 @@ import {parseTextShadow} from './parsing/textShadow'; import {parseTextTransform} from './parsing/textTransform'; import {parseTransform} from './parsing/transform'; import {parseVisibility, VISIBILITY} from './parsing/visibility'; +import {parseWordBreak} from './parsing/word-break'; import {parseZIndex} from './parsing/zIndex'; import {parseBounds, parseBoundCurves, calculatePaddingBoxPath} from './Bounds'; @@ -65,10 +71,12 @@ type StyleDeclaration = { float: Float, font: Font, letterSpacing: number, + lineBreak: LineBreak, listStyle: ListStyle | null, margin: Margin, opacity: number, overflow: Overflow, + overflowWrap: OverflowWrap, padding: Padding, position: Position, textDecoration: TextDecoration | null, @@ -76,6 +84,7 @@ type StyleDeclaration = { textTransform: TextTransform, transform: Transform, visibility: Visibility, + wordBreak: WordBreak, zIndex: zIndex }; @@ -134,12 +143,16 @@ export default class NodeContainer { font: parseFont(style), letterSpacing: parseLetterSpacing(style.letterSpacing), listStyle: display === DISPLAY.LIST_ITEM ? parseListStyle(style) : null, + lineBreak: parseLineBreak(style.lineBreak), margin: parseMargin(style), opacity: parseFloat(style.opacity), overflow: INPUT_TAGS.indexOf(node.tagName) === -1 ? parseOverflow(style.overflow) : OVERFLOW.HIDDEN, + overflowWrap: parseOverflowWrap( + style.overflowWrap ? style.overflowWrap : style.wordWrap + ), padding: parsePadding(style), position: position, textDecoration: parseTextDecoration(style), @@ -147,6 +160,7 @@ export default class NodeContainer { textTransform: parseTextTransform(style.textTransform), transform: parseTransform(style), visibility: parseVisibility(style.visibility), + wordBreak: parseWordBreak(style.wordBreak), zIndex: parseZIndex(position !== POSITION.STATIC ? style.zIndex : 'auto') }; diff --git a/src/TextBounds.js b/src/TextBounds.js index 1b7f2af..85a5e20 100644 --- a/src/TextBounds.js +++ b/src/TextBounds.js @@ -1,18 +1,12 @@ /* @flow */ 'use strict'; -import {ucs2} from 'punycode'; import type NodeContainer from './NodeContainer'; import {Bounds, parseBounds} from './Bounds'; import {TEXT_DECORATION} from './parsing/textDecoration'; import FEATURES from './Feature'; - -const UNICODE = /[^\u0000-\u00ff]/; - -const hasUnicodeCharacters = (text: string): boolean => UNICODE.test(text); - -const encodeCodePoint = (codePoint: number): string => ucs2.encode([codePoint]); +import {breakWords, toCodePoints, fromCodePoint} from './Unicode'; export class TextBounds { text: string; @@ -29,9 +23,10 @@ export const parseTextBounds = ( parent: NodeContainer, node: Text ): Array => { - const codePoints = ucs2.decode(value); - const letterRendering = parent.style.letterSpacing !== 0 || hasUnicodeCharacters(value); - const textList = letterRendering ? codePoints.map(encodeCodePoint) : splitWords(codePoints); + const letterRendering = parent.style.letterSpacing !== 0; + const textList = letterRendering + ? toCodePoints(value).map(i => fromCodePoint(i)) + : breakWords(value, parent); const length = textList.length; const defaultView = node.parentNode ? node.parentNode.ownerDocument.defaultView : null; const scrollX = defaultView ? defaultView.pageXOffset : 0; @@ -88,42 +83,3 @@ const getRangeBounds = ( range.setEnd(node, offset + length); return Bounds.fromClientRect(range.getBoundingClientRect(), scrollX, scrollY); }; - -const splitWords = (codePoints: Array): Array => { - const words = []; - let i = 0; - let onWordBoundary = false; - let word; - while (codePoints.length) { - if (isWordBoundary(codePoints[i]) === onWordBoundary) { - word = codePoints.splice(0, i); - if (word.length) { - words.push(ucs2.encode(word)); - } - onWordBoundary = !onWordBoundary; - i = 0; - } else { - i++; - } - - if (i >= codePoints.length) { - word = codePoints.splice(0, i); - if (word.length) { - words.push(ucs2.encode(word)); - } - } - } - return words; -}; - -const isWordBoundary = (characterCode: number): boolean => { - return ( - [ - 32, // - 13, // \r - 10, // \n - 9, // \t - 45 // - - ].indexOf(characterCode) !== -1 - ); -}; diff --git a/src/Unicode.js b/src/Unicode.js index d2364ff..ec31805 100644 --- a/src/Unicode.js +++ b/src/Unicode.js @@ -1,32 +1,27 @@ /* @flow */ 'use strict'; -export const fromCodePoint = (...codePoints: Array): string => { - if (String.fromCodePoint) { - return String.fromCodePoint(...codePoints); +import NodeContainer from './NodeContainer'; +import {LineBreaker, fromCodePoint, toCodePoints} from 'css-line-break'; +import {OVERFLOW_WRAP} from './parsing/overflowWrap'; + +export {toCodePoints, fromCodePoint} from 'css-line-break'; + +export const breakWords = (str: string, parent: NodeContainer): Array => { + const breaker = LineBreaker(str, { + lineBreak: parent.style.lineBreak, + wordBreak: + parent.style.overflowWrap === OVERFLOW_WRAP.BREAK_WORD + ? 'break-word' + : parent.style.wordBreak + }); + + const words = []; + let bk; + + while (!(bk = breaker.next()).done) { + words.push(bk.value.slice()); } - const length = codePoints.length; - if (!length) { - return ''; - } - - const codeUnits = []; - - let index = -1; - let result = ''; - while (++index < length) { - let codePoint = codePoints[index]; - if (codePoint <= 0xffff) { - codeUnits.push(codePoint); - } else { - codePoint -= 0x10000; - codeUnits.push((codePoint >> 10) + 0xd800, codePoint % 0x400 + 0xdc00); - } - if (index + 1 === length || codeUnits.length > 0x4000) { - result += String.fromCharCode(...codeUnits); - codeUnits.length = 0; - } - } - return result; + return words; }; diff --git a/src/parsing/lineBreak.js b/src/parsing/lineBreak.js new file mode 100644 index 0000000..b5a2e28 --- /dev/null +++ b/src/parsing/lineBreak.js @@ -0,0 +1,19 @@ +/* @flow */ +'use strict'; + +export const LINE_BREAK = { + NORMAL: 'normal', + STRICT: 'strict' +}; + +export type LineBreak = $Values; + +export const parseLineBreak = (wordBreak: string): LineBreak => { + switch (wordBreak) { + case 'strict': + return LINE_BREAK.STRICT; + case 'normal': + default: + return LINE_BREAK.NORMAL; + } +}; diff --git a/src/parsing/overflowWrap.js b/src/parsing/overflowWrap.js new file mode 100644 index 0000000..a9466f5 --- /dev/null +++ b/src/parsing/overflowWrap.js @@ -0,0 +1,19 @@ +/* @flow */ +'use strict'; + +export const OVERFLOW_WRAP = { + NORMAL: 0, + BREAK_WORD: 1 +}; + +export type OverflowWrap = $Values; + +export const parseOverflowWrap = (overflow: string): OverflowWrap => { + switch (overflow) { + case 'break-word': + return OVERFLOW_WRAP.BREAK_WORD; + case 'normal': + default: + return OVERFLOW_WRAP.NORMAL; + } +}; diff --git a/src/parsing/word-break.js b/src/parsing/word-break.js new file mode 100644 index 0000000..5857925 --- /dev/null +++ b/src/parsing/word-break.js @@ -0,0 +1,22 @@ +/* @flow */ +'use strict'; + +export const WORD_BREAK = { + NORMAL: 'normal', + BREAK_ALL: 'break-all', + KEEP_ALL: 'keep-all' +}; + +export type WordBreak = $Values; + +export const parseWordBreak = (wordBreak: string): WordBreak => { + switch (wordBreak) { + case 'break-all': + return WORD_BREAK.BREAK_ALL; + case 'keep-all': + return WORD_BREAK.KEEP_ALL; + case 'normal': + default: + return WORD_BREAK.NORMAL; + } +}; diff --git a/tests/reftests/text/line-break.html b/tests/reftests/text/line-break.html new file mode 100644 index 0000000..91a60c7 --- /dev/null +++ b/tests/reftests/text/line-break.html @@ -0,0 +1,40 @@ + + + + word-break + + + + + + + +

+ サンプルぁルぁルぁルぁルぁルぁルぁぁぁぁ文ンプル–文々サンプル文 +

+ +

+ サンプルぁルぁルぁルぁルぁルぁルぁぁぁぁ文文文文文‐–〜゠サンプル文々サンプル文 +

+ + +
+ + + diff --git a/tests/reftests/text/overflow-wrap.html b/tests/reftests/text/overflow-wrap.html new file mode 100644 index 0000000..6a64df6 --- /dev/null +++ b/tests/reftests/text/overflow-wrap.html @@ -0,0 +1,58 @@ + + + + word-break + + + + + + +
+

1. overflow-wrap: normal

+

FStrPrivFinÄndG (Gesetz zur Änderung des + Fernstraßenbauprivatfinanzierungsgesetzes + und straßenverkehrsrechtlicher Vorschriften)

+

2. overflow-wrap: break-word

+

FStrPrivFinÄndG (Gesetz zur Änderung des + Fernstraßenbauprivatfinanzierungsgesetzes + und straßenverkehrsrechtlicher Vorschriften)

+

3. word-wrap: normal

+

FStrPrivFinÄndG (Gesetz zur Änderung des + Fernstraßenbauprivatfinanzierungsgesetzes + und straßenverkehrsrechtlicher Vorschriften)

+

4. word-wrap: break-word

+

FStrPrivFinÄndG (Gesetz zur Änderung des + Fernstraßenbauprivatfinanzierungsgesetzes + und straßenverkehrsrechtlicher Vorschriften)

+
+ + + diff --git a/tests/reftests/text/word-break.html b/tests/reftests/text/word-break.html new file mode 100644 index 0000000..fe19bee --- /dev/null +++ b/tests/reftests/text/word-break.html @@ -0,0 +1,50 @@ + + + + word-break + + + + + + +
+

1. word-break: normal

+

This is a long and + Supercalifragilisticexpialidocious sentence. + 次の単語グレートブリテンおよび北アイルランド連合王国で本当に大きな言葉

+ +

2. word-break: break-all

+

This is a long and + Supercalifragilisticexpialidocious sentence. + 次の単語グレートブリテンおよび北アイルランド連合王国で本当に大きな言葉

+ +

3. word-break: keep-all

+

This is a long and + Supercalifragilisticexpialidocious sentence. + 次の単語グレートブリテンおよび北アイルランド連合王国で本当に大きな言葉

+
+ + +