Implement unicode line-breaking

This commit is contained in:
Niklas von Hertzen 2017-12-31 00:12:28 +08:00
parent 3a5ed43e97
commit 1870433307
13 changed files with 263 additions and 85 deletions

View File

@ -1,5 +1,6 @@
[ignore] [ignore]
.*/www/.* .*/www/.*
.*/node_modules/css-line-break/scripts.*
[include] [include]
[libs] [libs]
./flow-typed ./flow-typed

View File

@ -36,6 +36,7 @@ Below is a list of all the supported CSS properties and values.
- height - height
- left - left
- letter-spacing - letter-spacing
- line-break
- list-style - list-style
- list-style-image - list-style-image
- list-style-position - list-style-position
@ -47,6 +48,7 @@ Below is a list of all the supported CSS properties and values.
- min-width - min-width
- opacity - opacity
- overflow - overflow
- overflow-wrap
- padding - padding
- position - position
- right - right
@ -62,7 +64,9 @@ Below is a list of all the supported CSS properties and values.
- visibility - visibility
- white-space - white-space
- width - width
- word-break
- word-spacing - word-spacing
- word-wrap
- z-index - z-index
## Unsupported CSS properties ## Unsupported CSS properties
@ -76,8 +80,6 @@ These CSS properties are **NOT** currently supported
- [mix-blend-mode](https://github.com/niklasvh/html2canvas/issues/580) - [mix-blend-mode](https://github.com/niklasvh/html2canvas/issues/580)
- [object-fit](https://github.com/niklasvh/html2canvas/issues/1064) - [object-fit](https://github.com/niklasvh/html2canvas/issues/1064)
- [repeating-linear-gradient()](https://github.com/niklasvh/html2canvas/issues/1162) - [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) - [writing-mode](https://github.com/niklasvh/html2canvas/issues/1258)
- [zoom](https://github.com/niklasvh/html2canvas/issues/732) - [zoom](https://github.com/niklasvh/html2canvas/issues/732)

16
package-lock.json generated
View File

@ -1056,8 +1056,7 @@
"base64-arraybuffer": { "base64-arraybuffer": {
"version": "0.1.5", "version": "0.1.5",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
"integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
"dev": true
}, },
"base64-js": { "base64-js": {
"version": "1.2.1", "version": "1.2.1",
@ -1901,6 +1900,14 @@
"integrity": "sha1-zFRJaF37hesRyYKKzHy4erW7/MA=", "integrity": "sha1-zFRJaF37hesRyYKKzHy4erW7/MA=",
"dev": true "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": { "cuid": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/cuid/-/cuid-1.3.8.tgz", "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": { "q": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",

View File

@ -77,6 +77,6 @@
"homepage": "https://html2canvas.hertzen.com", "homepage": "https://html2canvas.hertzen.com",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"punycode": "2.1.0" "css-line-break": "1.0.0"
} }
} }

View File

@ -7,9 +7,11 @@ import type {BorderRadius} from './parsing/borderRadius';
import type {DisplayBit} from './parsing/display'; import type {DisplayBit} from './parsing/display';
import type {Float} from './parsing/float'; import type {Float} from './parsing/float';
import type {Font} from './parsing/font'; import type {Font} from './parsing/font';
import type {LineBreak} from './parsing/lineBreak';
import type {ListStyle} from './parsing/listStyle'; import type {ListStyle} from './parsing/listStyle';
import type {Margin} from './parsing/margin'; import type {Margin} from './parsing/margin';
import type {Overflow} from './parsing/overflow'; import type {Overflow} from './parsing/overflow';
import type {OverflowWrap} from './parsing/overflowWrap';
import type {Padding} from './parsing/padding'; import type {Padding} from './parsing/padding';
import type {Position} from './parsing/position'; import type {Position} from './parsing/position';
import type {TextShadow} from './parsing/textShadow'; import type {TextShadow} from './parsing/textShadow';
@ -17,6 +19,7 @@ import type {TextTransform} from './parsing/textTransform';
import type {TextDecoration} from './parsing/textDecoration'; import type {TextDecoration} from './parsing/textDecoration';
import type {Transform} from './parsing/transform'; import type {Transform} from './parsing/transform';
import type {Visibility} from './parsing/visibility'; import type {Visibility} from './parsing/visibility';
import type {WordBreak} from './parsing/word-break';
import type {zIndex} from './parsing/zIndex'; import type {zIndex} from './parsing/zIndex';
import type {Bounds, BoundCurves} from './Bounds'; import type {Bounds, BoundCurves} from './Bounds';
@ -34,9 +37,11 @@ import {parseDisplay, DISPLAY} from './parsing/display';
import {parseCSSFloat, FLOAT} from './parsing/float'; import {parseCSSFloat, FLOAT} from './parsing/float';
import {parseFont} from './parsing/font'; import {parseFont} from './parsing/font';
import {parseLetterSpacing} from './parsing/letterSpacing'; import {parseLetterSpacing} from './parsing/letterSpacing';
import {parseLineBreak} from './parsing/lineBreak';
import {parseListStyle} from './parsing/listStyle'; import {parseListStyle} from './parsing/listStyle';
import {parseMargin} from './parsing/margin'; import {parseMargin} from './parsing/margin';
import {parseOverflow, OVERFLOW} from './parsing/overflow'; import {parseOverflow, OVERFLOW} from './parsing/overflow';
import {parseOverflowWrap} from './parsing/overflowWrap';
import {parsePadding} from './parsing/padding'; import {parsePadding} from './parsing/padding';
import {parsePosition, POSITION} from './parsing/position'; import {parsePosition, POSITION} from './parsing/position';
import {parseTextDecoration} from './parsing/textDecoration'; import {parseTextDecoration} from './parsing/textDecoration';
@ -44,6 +49,7 @@ import {parseTextShadow} from './parsing/textShadow';
import {parseTextTransform} from './parsing/textTransform'; import {parseTextTransform} from './parsing/textTransform';
import {parseTransform} from './parsing/transform'; import {parseTransform} from './parsing/transform';
import {parseVisibility, VISIBILITY} from './parsing/visibility'; import {parseVisibility, VISIBILITY} from './parsing/visibility';
import {parseWordBreak} from './parsing/word-break';
import {parseZIndex} from './parsing/zIndex'; import {parseZIndex} from './parsing/zIndex';
import {parseBounds, parseBoundCurves, calculatePaddingBoxPath} from './Bounds'; import {parseBounds, parseBoundCurves, calculatePaddingBoxPath} from './Bounds';
@ -65,10 +71,12 @@ type StyleDeclaration = {
float: Float, float: Float,
font: Font, font: Font,
letterSpacing: number, letterSpacing: number,
lineBreak: LineBreak,
listStyle: ListStyle | null, listStyle: ListStyle | null,
margin: Margin, margin: Margin,
opacity: number, opacity: number,
overflow: Overflow, overflow: Overflow,
overflowWrap: OverflowWrap,
padding: Padding, padding: Padding,
position: Position, position: Position,
textDecoration: TextDecoration | null, textDecoration: TextDecoration | null,
@ -76,6 +84,7 @@ type StyleDeclaration = {
textTransform: TextTransform, textTransform: TextTransform,
transform: Transform, transform: Transform,
visibility: Visibility, visibility: Visibility,
wordBreak: WordBreak,
zIndex: zIndex zIndex: zIndex
}; };
@ -134,12 +143,16 @@ export default class NodeContainer {
font: parseFont(style), font: parseFont(style),
letterSpacing: parseLetterSpacing(style.letterSpacing), letterSpacing: parseLetterSpacing(style.letterSpacing),
listStyle: display === DISPLAY.LIST_ITEM ? parseListStyle(style) : null, listStyle: display === DISPLAY.LIST_ITEM ? parseListStyle(style) : null,
lineBreak: parseLineBreak(style.lineBreak),
margin: parseMargin(style), margin: parseMargin(style),
opacity: parseFloat(style.opacity), opacity: parseFloat(style.opacity),
overflow: overflow:
INPUT_TAGS.indexOf(node.tagName) === -1 INPUT_TAGS.indexOf(node.tagName) === -1
? parseOverflow(style.overflow) ? parseOverflow(style.overflow)
: OVERFLOW.HIDDEN, : OVERFLOW.HIDDEN,
overflowWrap: parseOverflowWrap(
style.overflowWrap ? style.overflowWrap : style.wordWrap
),
padding: parsePadding(style), padding: parsePadding(style),
position: position, position: position,
textDecoration: parseTextDecoration(style), textDecoration: parseTextDecoration(style),
@ -147,6 +160,7 @@ export default class NodeContainer {
textTransform: parseTextTransform(style.textTransform), textTransform: parseTextTransform(style.textTransform),
transform: parseTransform(style), transform: parseTransform(style),
visibility: parseVisibility(style.visibility), visibility: parseVisibility(style.visibility),
wordBreak: parseWordBreak(style.wordBreak),
zIndex: parseZIndex(position !== POSITION.STATIC ? style.zIndex : 'auto') zIndex: parseZIndex(position !== POSITION.STATIC ? style.zIndex : 'auto')
}; };

View File

@ -1,18 +1,12 @@
/* @flow */ /* @flow */
'use strict'; 'use strict';
import {ucs2} from 'punycode';
import type NodeContainer from './NodeContainer'; import type NodeContainer from './NodeContainer';
import {Bounds, parseBounds} from './Bounds'; import {Bounds, parseBounds} from './Bounds';
import {TEXT_DECORATION} from './parsing/textDecoration'; import {TEXT_DECORATION} from './parsing/textDecoration';
import FEATURES from './Feature'; import FEATURES from './Feature';
import {breakWords, toCodePoints, fromCodePoint} from './Unicode';
const UNICODE = /[^\u0000-\u00ff]/;
const hasUnicodeCharacters = (text: string): boolean => UNICODE.test(text);
const encodeCodePoint = (codePoint: number): string => ucs2.encode([codePoint]);
export class TextBounds { export class TextBounds {
text: string; text: string;
@ -29,9 +23,10 @@ export const parseTextBounds = (
parent: NodeContainer, parent: NodeContainer,
node: Text node: Text
): Array<TextBounds> => { ): Array<TextBounds> => {
const codePoints = ucs2.decode(value); const letterRendering = parent.style.letterSpacing !== 0;
const letterRendering = parent.style.letterSpacing !== 0 || hasUnicodeCharacters(value); const textList = letterRendering
const textList = letterRendering ? codePoints.map(encodeCodePoint) : splitWords(codePoints); ? toCodePoints(value).map(i => fromCodePoint(i))
: breakWords(value, parent);
const length = textList.length; const length = textList.length;
const defaultView = node.parentNode ? node.parentNode.ownerDocument.defaultView : null; const defaultView = node.parentNode ? node.parentNode.ownerDocument.defaultView : null;
const scrollX = defaultView ? defaultView.pageXOffset : 0; const scrollX = defaultView ? defaultView.pageXOffset : 0;
@ -88,42 +83,3 @@ const getRangeBounds = (
range.setEnd(node, offset + length); range.setEnd(node, offset + length);
return Bounds.fromClientRect(range.getBoundingClientRect(), scrollX, scrollY); return Bounds.fromClientRect(range.getBoundingClientRect(), scrollX, scrollY);
}; };
const splitWords = (codePoints: Array<number>): Array<string> => {
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, // <space>
13, // \r
10, // \n
9, // \t
45 // -
].indexOf(characterCode) !== -1
);
};

View File

@ -1,32 +1,27 @@
/* @flow */ /* @flow */
'use strict'; 'use strict';
export const fromCodePoint = (...codePoints: Array<number>): string => { import NodeContainer from './NodeContainer';
if (String.fromCodePoint) { import {LineBreaker, fromCodePoint, toCodePoints} from 'css-line-break';
return String.fromCodePoint(...codePoints); import {OVERFLOW_WRAP} from './parsing/overflowWrap';
export {toCodePoints, fromCodePoint} from 'css-line-break';
export const breakWords = (str: string, parent: NodeContainer): Array<string> => {
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; return words;
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;
}; };

19
src/parsing/lineBreak.js Normal file
View File

@ -0,0 +1,19 @@
/* @flow */
'use strict';
export const LINE_BREAK = {
NORMAL: 'normal',
STRICT: 'strict'
};
export type LineBreak = $Values<typeof LINE_BREAK>;
export const parseLineBreak = (wordBreak: string): LineBreak => {
switch (wordBreak) {
case 'strict':
return LINE_BREAK.STRICT;
case 'normal':
default:
return LINE_BREAK.NORMAL;
}
};

View File

@ -0,0 +1,19 @@
/* @flow */
'use strict';
export const OVERFLOW_WRAP = {
NORMAL: 0,
BREAK_WORD: 1
};
export type OverflowWrap = $Values<typeof OVERFLOW_WRAP>;
export const parseOverflowWrap = (overflow: string): OverflowWrap => {
switch (overflow) {
case 'break-word':
return OVERFLOW_WRAP.BREAK_WORD;
case 'normal':
default:
return OVERFLOW_WRAP.NORMAL;
}
};

22
src/parsing/word-break.js Normal file
View File

@ -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<typeof WORD_BREAK>;
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;
}
};

View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<title>word-break</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script>
<style>
body {
font-family: Arial;
}
.test span {
line-break: normal;
}
.strict span {
line-break: strict;
}
p.test{
border: 1px solid gray;
color: blue;
width: 6em;
}
</style>
</head>
<body>
<!-- iteration marks -->
<p class="test" lang="ja">
<span>サンプルぁルぁルぁルぁルぁルぁルぁぁぁぁ文ンプル–文々サンプル文</span>
</p>
<p class="test strict" lang="ja">
<span>サンプルぁルぁルぁルぁルぁルぁルぁぁぁぁ文文文文文‐–〜゠サンプル文々サンプル文</span>
</p>
<hr />
</body>
</html>

View File

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html>
<head>
<title>word-break</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script>
<style>
body {
font-family: Arial;
}
.normal {
width: 13em;
background: gold;
overflow-wrap: normal;
}
.break-word {
width: 13em;
background: lime;
overflow-wrap: break-word;
}
.word-normal {
width: 13em;
background: gold;
word-wrap: normal;
}
.word-break-word {
width: 13em;
background: lime;
word-wrap: break-word;
}
</style>
</head>
<body>
<div>
<p>1. <code>overflow-wrap: normal</code></p>
<p class="normal">FStrPrivFinÄndG (Gesetz zur Änderung des
Fernstraßenbauprivatfinanzierungsgesetzes
und straßenverkehrsrechtlicher Vorschriften)</p>
<p>2. <code>overflow-wrap: break-word</code></p>
<p class="break-word">FStrPrivFinÄndG (Gesetz zur Änderung des
Fernstraßenbauprivatfinanzierungsgesetzes
und straßenverkehrsrechtlicher Vorschriften)</p>
<p>3. <code>word-wrap: normal</code></p>
<p class="word-normal">FStrPrivFinÄndG (Gesetz zur Änderung des
Fernstraßenbauprivatfinanzierungsgesetzes
und straßenverkehrsrechtlicher Vorschriften)</p>
<p>4. <code>word-wrap: break-word</code></p>
<p class="word-break-word">FStrPrivFinÄndG (Gesetz zur Änderung des
Fernstraßenbauprivatfinanzierungsgesetzes
und straßenverkehrsrechtlicher Vorschriften)</p>
</div>
</body>
</html>

View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<title>word-break</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script>
<style>
body {
font-family: Arial;
}
.narrow {
padding: 5px;
border: 1px solid;
width: 8em;
}
.normal {
word-break: normal;
}
.breakAll {
word-break: break-all;
}
.keep {
word-break: keep-all;
}
</style>
</head>
<body>
<div>
<p>1. <code>word-break: normal</code></p>
<p class="normal narrow">This is a long and
Supercalifragilisticexpialidocious sentence.
次の単語グレートブリテンおよび北アイルランド連合王国で本当に大きな言葉</p>
<p>2. <code>word-break: break-all</code></p>
<p class="breakAll narrow">This is a long and
Supercalifragilisticexpialidocious sentence.
次の単語グレートブリテンおよび北アイルランド連合王国で本当に大きな言葉</p>
<p>3. <code>word-break: keep-all</code></p>
<p class="keep narrow">This is a long and
Supercalifragilisticexpialidocious sentence.
次の単語グレートブリテンおよび北アイルランド連合王国で本当に大きな言葉</p>
</div>
</body>
</html>