Skip to content

Commit

Permalink
Implement unicode line-breaking
Browse files Browse the repository at this point in the history
  • Loading branch information
niklasvh committed Dec 30, 2017
1 parent 3a5ed43 commit 1870433
Show file tree
Hide file tree
Showing 13 changed files with 261 additions and 83 deletions.
1 change: 1 addition & 0 deletions .flowconfig
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[ignore]
.*/www/.*
.*/node_modules/css-line-break/scripts.*
[include]
[libs]
./flow-typed
Expand Down
6 changes: 4 additions & 2 deletions docs/features.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,6 @@
"homepage": "https://html2canvas.hertzen.com",
"license": "MIT",
"dependencies": {
"punycode": "2.1.0"
"css-line-break": "1.0.0"
}
}
14 changes: 14 additions & 0 deletions src/NodeContainer.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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;
}
};
Loading

0 comments on commit 1870433

Please sign in to comment.