mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
Implement unicode line-breaking
This commit is contained in:
parent
3a5ed43e97
commit
1870433307
@ -1,5 +1,6 @@
|
||||
[ignore]
|
||||
.*/www/.*
|
||||
.*/node_modules/css-line-break/scripts.*
|
||||
[include]
|
||||
[libs]
|
||||
./flow-typed
|
||||
|
@ -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)
|
||||
|
||||
|
16
package-lock.json
generated
16
package-lock.json
generated
@ -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",
|
||||
|
@ -77,6 +77,6 @@
|
||||
"homepage": "https://html2canvas.hertzen.com",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "2.1.0"
|
||||
"css-line-break": "1.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
};
|
||||
|
||||
|
@ -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<TextBounds> => {
|
||||
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<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
|
||||
);
|
||||
};
|
||||
|
@ -1,32 +1,27 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export const fromCodePoint = (...codePoints: Array<number>): 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<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;
|
||||
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;
|
||||
};
|
||||
|
19
src/parsing/lineBreak.js
Normal file
19
src/parsing/lineBreak.js
Normal 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;
|
||||
}
|
||||
};
|
19
src/parsing/overflowWrap.js
Normal file
19
src/parsing/overflowWrap.js
Normal 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
22
src/parsing/word-break.js
Normal 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;
|
||||
}
|
||||
};
|
40
tests/reftests/text/line-break.html
Normal file
40
tests/reftests/text/line-break.html
Normal 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>
|
58
tests/reftests/text/overflow-wrap.html
Normal file
58
tests/reftests/text/overflow-wrap.html
Normal 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>
|
50
tests/reftests/text/word-break.html
Normal file
50
tests/reftests/text/word-break.html
Normal 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>
|
Loading…
Reference in New Issue
Block a user