Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement unicode line-breaking #1348

Merged
merged 2 commits into from Dec 31, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/features.md
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 changes: 9 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -77,6 +77,6 @@
"homepage": "https://html2canvas.hertzen.com",
"license": "MIT",
"dependencies": {
"punycode": "2.1.0"
"css-line-break": "1.0.1"
}
}
14 changes: 14 additions & 0 deletions src/NodeContainer.js
Expand Up @@ -7,16 +7,19 @@ 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';
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';
Expand All @@ -34,16 +37,19 @@ 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';
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';
Expand All @@ -65,17 +71,20 @@ 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,
textShadow: Array<TextShadow> | null,
textTransform: TextTransform,
transform: Transform,
visibility: Visibility,
wordBreak: WordBreak,
zIndex: zIndex
};

Expand Down Expand Up @@ -134,19 +143,24 @@ 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),
textShadow: parseTextShadow(style.textShadow),
textTransform: parseTextTransform(style.textTransform),
transform: parseTransform(style),
visibility: parseVisibility(style.visibility),
wordBreak: parseWordBreak(style.wordBreak),
zIndex: parseZIndex(position !== POSITION.STATIC ? style.zIndex : 'auto')
};

Expand Down
54 changes: 5 additions & 49 deletions 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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
);
};
43 changes: 19 additions & 24 deletions src/Unicode.js
@@ -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';

const length = codePoints.length;
if (!length) {
return '';
}
export {toCodePoints, fromCodePoint} from 'css-line-break';

const codeUnits = [];
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
});

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;
}
const words = [];
let bk;

while (!(bk = breaker.next()).done) {
words.push(bk.value.slice());
}
return result;

return words;
};
19 changes: 19 additions & 0 deletions src/parsing/lineBreak.js
@@ -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 changes: 19 additions & 0 deletions src/parsing/overflowWrap.js
@@ -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 changes: 22 additions & 0 deletions 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<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 changes: 40 additions & 0 deletions tests/reftests/text/line-break.html
@@ -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>