Compare commits

...

59 Commits

Author SHA1 Message Date
f347953042 Remove iOS 8.4 tests 2018-04-01 17:42:27 +08:00
48b4c20e24 v1.0.0-alpha.11 2018-04-01 17:14:38 +08:00
6341788edf Update package-lock.json 2018-04-01 17:09:49 +08:00
0e273418c7 IE11 Member not found fix Wrap accesing the cssText property in a try...catch (#1415)
* https://github.com/niklasvh/html2canvas/issues/1374 - Wrap accesing the cssText property in a try...catch
2018-04-01 16:49:23 +08:00
e6bbc1abb5 Merge pull request #1487 from mapleeit/support-blob-image-resources
support blob image resources in non-foreignObjectRendering mode
2018-04-01 16:43:40 +08:00
13bbc90048 support blob image resources in non-foreignObjectRendering mode 2018-03-30 16:37:18 +08:00
102b5a1282 v1.0.0-alpha.10 2018-02-15 22:50:40 +08:00
01e4920876 Fix window reference for node tests 2018-02-15 22:19:25 +08:00
da2794f7f7 Fix version logging (Fix #1421) 2018-02-15 22:07:40 +08:00
9fb9898894 Re-introduce onclone option (Fix #1434) 2018-02-15 21:40:48 +08:00
952eb4cf7c Fix Travis chrome tests 2018-02-15 21:28:11 +08:00
fad4f837c9 Add ignoreElements predicate function option 2018-02-15 21:26:09 +08:00
e6c44afca1 Merge pull request #1417 from eKoopmans/bugfix/underlines
Revert "Fix underlines, relative to 'bottom' baseline"
2018-02-15 21:00:48 +08:00
d023de0b99 Revert "Fix underlines, relative to 'bottom' baseline"
This reverts commit 0c8d38d9c0.
2018-01-26 18:10:15 +11:00
0f01810005 Update website styles 2018-01-08 22:21:11 +08:00
bf03cf5237 Make website responsive 2018-01-08 21:38:54 +08:00
a555dfc085 Revert "Revert "Update html2canvas version""
This reverts commit 69fb48969e.
2018-01-08 20:42:48 +08:00
69fb48969e Revert "Update html2canvas version"
This reverts commit c9a60c4ff9.

# Conflicts:
#	www/package-lock.json
#	www/package.json
2018-01-07 23:19:47 +08:00
974c35c368 Update website html2canvas package 2018-01-07 22:14:16 +08:00
0fe9632a32 v1.0.0-alpha.9 2018-01-07 20:56:59 +08:00
c9a60c4ff9 Update html2canvas version 2018-01-07 20:54:22 +08:00
4c14894a0a Correctly clone dynami CSSStyleSheets (Fix #1370) 2018-01-07 20:13:26 +08:00
8788a9f458 Add npm badges 2018-01-07 19:22:36 +08:00
e198eae398 Merge branch 'jkrielaars-border-radius' 2018-01-07 19:20:24 +08:00
474b5e81a7 Refactor border-radius update 2018-01-07 19:19:55 +08:00
b97972eeb6 updated calculation of border-radius 2018-01-04 08:59:38 +01:00
b7c7464c5f v1.0.0-alpha.8 2018-01-02 20:24:12 +08:00
ae019f174c Use correct doctype in cloned Document (Fix #1298) 2018-01-02 20:06:24 +08:00
ea6062c85b Fix individual border rendering (Fix #1349) 2018-01-02 20:04:28 +08:00
9a4a506366 v1.0.0-alpha.7 2017-12-31 20:22:20 +08:00
cb93b80d0d Add thai text test 2017-12-31 20:19:27 +08:00
79e1c857e6 Fix form input text positions (Fix #1338 #1347) 2017-12-31 19:38:31 +08:00
cc9d1f89dc Merge pull request #1348 from niklasvh/line-breaking
Implement unicode line-breaking
2017-12-31 19:14:26 +08:00
d0f7ecfa9a Update css-line-breaking to 1.0.1 2017-12-31 00:24:38 +08:00
1870433307 Implement unicode line-breaking 2017-12-31 00:14:21 +08:00
3a5ed43e97 Update package-lock.json 2017-12-29 19:15:49 +08:00
8429761e8f Fix tag names 2017-12-28 14:25:29 +08:00
c4e670addf v1.0.0-alpha6 2017-12-28 14:19:52 +08:00
0aeb54ca2e Remove console.logs 2017-12-28 13:58:42 +08:00
eec84fa39e Fix list-style-type: none (Fix #1340) 2017-12-28 13:52:05 +08:00
22f58d5d1c Merge branch 'vnmc-feature/HTC-0010_PseudoContent' 2017-12-24 17:09:27 +08:00
9046e0d554 Update to use list style parser from ListItem 2017-12-24 17:08:54 +08:00
afa5d7cb8e Merge branch 'feature/HTC-0010_PseudoContent' of git://github.com/vnmc/html2canvas into vnmc-feature/HTC-0010_PseudoContent 2017-12-24 16:30:13 +08:00
3881e3cf96 Update support for list-style 2017-12-22 00:07:10 +08:00
0aa973ab0d v1.0.0-alpha.5 2017-12-21 23:54:27 +08:00
baaf9b0701 Merge branch 'bugfix/underlines' of git://github.com/eKoopmans/html2canvas into eKoopmans-bugfix/underlines 2017-12-21 23:45:06 +08:00
02de2ee829 Document data-html2canvas-ignore (Fix #1316) 2017-12-21 23:42:59 +08:00
a570f5df74 Update useCORS documentation (Fix #1323) 2017-12-21 23:41:01 +08:00
38749bc4b6 Fix canvas rendering on Chrome 2017-12-21 23:31:55 +08:00
e1d6b4c76f Fix overflow: auto 2017-12-21 23:29:59 +08:00
31f2c22477 Fix list style issues 2017-12-21 23:22:09 +08:00
6d0cd2d226 fixed flow problems in PseudoNodeContent.js 2017-12-15 23:46:26 +01:00
7335984ab7 added support for rendering ordered lists and list-style 2017-12-15 22:55:27 +01:00
78c3c7fc71 improved support of 'content' for pseudo elements (multiple components, counters, attr, quotes) 2017-12-15 12:40:04 +01:00
4551976246 Merge pull request #1312 from a0viedo/patch-1
change build badge to SVG
2017-12-14 11:01:32 +08:00
9e04772b42 change build badge to SVG
since it will be better for high-res screens
2017-12-13 13:01:35 -03:00
54c4002df7 Fix example button hitbox 2017-12-12 23:58:09 +08:00
91641a3746 Deploy new website 2017-12-12 23:26:28 +08:00
0c8d38d9c0 Fix underlines, relative to 'bottom' baseline 2017-12-06 23:49:11 +11:00
50 changed files with 5306 additions and 1423 deletions

View File

@ -9,7 +9,7 @@ addons:
chrome: stable chrome: stable
firefox: latest firefox: latest
dist: trusty dist: trusty
sudo: false sudo: true
before_script: before_script:
- export DISPLAY=:99.0 - export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start - sh -e /etc/init.d/xvfb start
@ -48,7 +48,7 @@ deploy:
- provider: pages - provider: pages
skip_cleanup: true skip_cleanup: true
local_dir: www/public local_dir: www/public
target_branch: gh-pages-test target_branch: gh-pages
fqdn: html2canvas.hertzen.com fqdn: html2canvas.hertzen.com
github_token: github_token:
secure: "PowO/Jat660k3gHcjgI6DlJz15RM7pLUu11UPsLCtYJ8ZwodppE6Keg0DfVkSFSIZttZor+UssDwP/WOEqfZNLqmXbcj3Gec4xolohet/GOe0KJKKuF/HgggbcxumopxMX6sMVePlMBpkLpHh7tgEAEHBWTlzC1c1a7Xa48fZ7k=" secure: "PowO/Jat660k3gHcjgI6DlJz15RM7pLUu11UPsLCtYJ8ZwodppE6Keg0DfVkSFSIZttZor+UssDwP/WOEqfZNLqmXbcj3Gec4xolohet/GOe0KJKKuF/HgggbcxumopxMX6sMVePlMBpkLpHh7tgEAEHBWTlzC1c1a7Xa48fZ7k="

View File

@ -1,23 +1,54 @@
### Changelog ### ### Changelog ###
#### v1.0.0-alpha4 - 12.12.2017 #### #### v1.0.0-alpha.11 - 1.4.2018 ####
* Fix IE11 member not found error
* Support blob image resources in non-foreignObjectRendering mode
#### v1.0.0-alpha.10 - 15.2.2018 ####
* Re-introduce `onclone` option (Fix #1434)
* Add `ignoreElements` predicate function option
* Fix version console logging
#### v1.0.0-alpha.9 - 7.1.2018 ####
* Fix dynamic style sheets
* Fix > 50% border-radius values
#### v1.0.0-alpha.8 - 2.1.2018 ####
* Use correct doctype in cloned Document (Fix #1298)
* Fix individual border rendering (Fix #1349)
#### v1.0.0-alpha.7 - 31.12.2017 ####
* Fix form input rendering (#1338)
* Improve word line breaking algorithm
#### v1.0.0-alpha.6 - 28.12.2017 ####
* Fix list-style: none (#1340)
* Extend supported values for pseudo element content
#### v1.0.0-alpha.5 - 21.12.2017 ####
* Fix underline positioning
* Fix canvas rendering on Chrome
* Fix overflow: auto
* Added support for rendering list-style
#### v1.0.0-alpha.4 - 12.12.2017 ####
* Fix rendering with multiple fonts defined (Fix #796) * Fix rendering with multiple fonts defined (Fix #796)
* Add support for radial-gradients * Add support for radial-gradients
* Fix logging option (#1302) * Fix logging option (#1302)
* Add support for rendering webgl canvas content (#646) * Add support for rendering webgl canvas content (#646)
* Fix external SVG loading with proxies (#802) * Fix external SVG loading with proxies (#802)
#### v1.0.0-alpha3 - 9.12.2017 #### #### v1.0.0-alpha.3 - 9.12.2017 ####
* Disable `foreignObjectRendering` by default (#1295) * Disable `foreignObjectRendering` by default (#1295)
* Fix background-size when using background-origin and background-size: cover/contain (#1299) * Fix background-size when using background-origin and background-size: cover/contain (#1299)
* Added support for background-origin: content-box (#1299) * Added support for background-origin: content-box (#1299)
#### v1.0.0-alpha2 - 7.12.2017 #### #### v1.0.0-alpha.2 - 7.12.2017 ####
* Fix scroll positions for CanvasRenderer (#1259) * Fix scroll positions for CanvasRenderer (#1259)
* Fix `data-html2canvas-ignore` attribute (#1253) * Fix `data-html2canvas-ignore` attribute (#1253)
* Fix decimal `letter-spacing` values (#1293) * Fix decimal `letter-spacing` values (#1293)
#### v1.0.0-alpha1 - 5.12.2017 #### #### v1.0.0-alpha.1 - 5.12.2017 ####
* Complete rewrite of library * Complete rewrite of library
##### Breaking Changes ##### ##### Breaking Changes #####
* Remove deprecated onrendered callback, calling `html2canvas` returns a `Promise<HTMLCanvasElement>` * Remove deprecated onrendered callback, calling `html2canvas` returns a `Promise<HTMLCanvasElement>`

View File

@ -3,7 +3,10 @@ html2canvas
[Homepage](https://html2canvas.hertzen.com) | [Downloads](https://github.com/niklasvh/html2canvas/releases) | [Questions](http://stackoverflow.com/questions/tagged/html2canvas?sort=newest) | [Donate](https://www.gittip.com/niklasvh/) [Homepage](https://html2canvas.hertzen.com) | [Downloads](https://github.com/niklasvh/html2canvas/releases) | [Questions](http://stackoverflow.com/questions/tagged/html2canvas?sort=newest) | [Donate](https://www.gittip.com/niklasvh/)
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/niklasvh/html2canvas?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Build Status](https://travis-ci.org/niklasvh/html2canvas.png)](https://travis-ci.org/niklasvh/html2canvas) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/niklasvh/html2canvas?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Build Status](https://travis-ci.org/niklasvh/html2canvas.svg)](https://travis-ci.org/niklasvh/html2canvas)
[![NPM Downloads](https://img.shields.io/npm/dm/html2canvas.svg)](https://www.npmjs.org/package/html2canvas)
[![NPM Version](https://img.shields.io/npm/v/html2canvas.svg)](https://www.npmjs.org/package/html2canvas)
#### JavaScript HTML renderer #### #### JavaScript HTML renderer ####

View File

@ -1,5 +1,5 @@
--- ---
title: "Configuration" title: "Options"
description: "Explore the different configuration options available for html2canvas" description: "Explore the different configuration options available for html2canvas"
previousUrl: "/getting-started" previousUrl: "/getting-started"
previousTitle: "Getting Started" previousTitle: "Getting Started"
@ -17,10 +17,13 @@ These are all of the available configuration options.
| canvas | `null` | Existing `canvas` element to use as a base for drawing on | canvas | `null` | Existing `canvas` element to use as a base for drawing on
| foreignObjectRendering | `false` | Whether to use ForeignObject rendering if the browser supports it | foreignObjectRendering | `false` | Whether to use ForeignObject rendering if the browser supports it
| imageTimeout | `15000` | Timeout for loading an image (in milliseconds). Set to `0` to disable timeout. | imageTimeout | `15000` | Timeout for loading an image (in milliseconds). Set to `0` to disable timeout.
| ignoreElements | `(element) => false` | Predicate function which removes the matching elements from the render.
| logging | `true` | Enable logging for debug purposes | logging | `true` | Enable logging for debug purposes
| onclone | `null` | Callback function which is called when the Document has been cloned for rendering, can be used to modify the contents that will be rendered without affecting the original source document.
| proxy | `null` | Url to the [proxy](/proxy/) which is to be used for loading cross-origin images. If left empty, cross-origin images won't be loaded. | proxy | `null` | Url to the [proxy](/proxy/) which is to be used for loading cross-origin images. If left empty, cross-origin images won't be loaded.
| removeContainer | `true` | Whether to cleanup the cloned DOM elements html2canvas creates temporarily | removeContainer | `true` | Whether to cleanup the cloned DOM elements html2canvas creates temporarily
| scale | `window.devicePixelRatio` | The scale to use for rendering. Defaults to the browsers device pixel ratio. | scale | `window.devicePixelRatio` | The scale to use for rendering. Defaults to the browsers device pixel ratio.
| useCORS | `false` | Whether to attempt to load images from a server using CORS
| width | `Element` width | The width of the `canvas` | width | `Element` width | The width of the `canvas`
| height | `Element` height | The height of the `canvas` | height | `Element` height | The height of the `canvas`
| x | `Element` x-offset | Crop canvas x-coordinate | x | `Element` x-offset | Crop canvas x-coordinate
@ -29,3 +32,5 @@ These are all of the available configuration options.
| scrollY | `Element` scrollY | The y-scroll position to used when rendering element, (for example if the Element uses `position: fixed`) | scrollY | `Element` scrollY | The y-scroll position to used when rendering element, (for example if the Element uses `position: fixed`)
| windowWidth | `Window.innerWidth` | Window width to use when rendering `Element`, which may affect things like Media queries | windowWidth | `Window.innerWidth` | Window width to use when rendering `Element`, which may affect things like Media queries
| windowHeight | `Window.innerHeight` | Window height to use when rendering `Element`, which may affect things like Media queries | windowHeight | `Window.innerHeight` | Window height to use when rendering `Element`, which may affect things like Media queries
If you wish to exclude certain `Element`s from getting rendered, you can add a `data-html2canvas-ignore` attribute to those elements and html2canvas will exclude them from the rendering.

View File

@ -22,7 +22,7 @@ Below is a list of all the supported CSS properties and values.
- border-width - border-width
- bottom - bottom
- box-sizing - box-sizing
- content (**Does not support `attr()`**) - content
- color - color
- display - display
- flex - flex
@ -36,6 +36,11 @@ 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-image
- list-style-position
- list-style-type
- margin - margin
- max-height - max-height
- max-width - max-width
@ -43,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
@ -58,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
@ -71,10 +79,7 @@ These CSS properties are **NOT** currently supported
- [font-variant-ligatures](https://github.com/niklasvh/html2canvas/pull/1085) - [font-variant-ligatures](https://github.com/niklasvh/html2canvas/pull/1085)
- [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)
- [list-style](https://github.com/niklasvh/html2canvas/issues/177)
- [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)

View File

@ -66,13 +66,6 @@ module.exports = function(config) {
platform: 'iOS', platform: 'iOS',
version: '9.3', version: '9.3',
device: 'iPhone 6 Plus Simulator' device: 'iPhone 6 Plus Simulator'
},
'sl_ios_8.4_safari': {
base: 'SauceLabs',
browserName: 'Safari',
platform: 'iOS',
version: '8.4',
device: 'iPhone 5s Simulator'
} }
}; };

3857
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
"name": "html2canvas", "name": "html2canvas",
"description": "Screenshots with JavaScript", "description": "Screenshots with JavaScript",
"main": "dist/npm/index.js", "main": "dist/npm/index.js",
"version": "1.0.0-alpha.4", "version": "1.0.0-alpha.11",
"author": { "author": {
"name": "Niklas von Hertzen", "name": "Niklas von Hertzen",
"email": "niklasvh@gmail.com", "email": "niklasvh@gmail.com",
@ -65,7 +65,7 @@
"build": "rimraf dist/ && node scripts/create-reftest-list && npm run build:npm && npm run build:browser", "build": "rimraf dist/ && node scripts/create-reftest-list && npm run build:npm && npm run build:browser",
"build:npm": "babel src/ -d dist/npm/ --plugins=dev-expression,transform-es2015-modules-commonjs && replace-in-file __VERSION__ '\"$npm_package_version\"' dist/npm/index.js", "build:npm": "babel src/ -d dist/npm/ --plugins=dev-expression,transform-es2015-modules-commonjs && replace-in-file __VERSION__ '\"$npm_package_version\"' dist/npm/index.js",
"build:browser": "webpack", "build:browser": "webpack",
"format": "prettier --single-quote --no-bracket-spacing --tab-width 4 --print-width 100 --write \"{src,www,tests,scripts}/**/*.js\"", "format": "prettier --single-quote --no-bracket-spacing --tab-width 4 --print-width 100 --write \"{src,www/src,tests,scripts}/**/*.js\"",
"flow": "flow", "flow": "flow",
"lint": "eslint src/**", "lint": "eslint src/**",
"test": "npm run flow && npm run lint && npm run test:node && npm run karma", "test": "npm run flow && npm run lint && npm run test:node && npm run karma",
@ -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.1"
} }
} }

View File

@ -202,40 +202,32 @@ export const parseBoundCurves = (
borders: Array<Border>, borders: Array<Border>,
borderRadius: Array<BorderRadius> borderRadius: Array<BorderRadius>
): BoundCurves => { ): BoundCurves => {
const HALF_WIDTH = bounds.width / 2; let tlh = borderRadius[CORNER.TOP_LEFT][H].getAbsoluteValue(bounds.width);
const HALF_HEIGHT = bounds.height / 2; let tlv = borderRadius[CORNER.TOP_LEFT][V].getAbsoluteValue(bounds.height);
const tlh = let trh = borderRadius[CORNER.TOP_RIGHT][H].getAbsoluteValue(bounds.width);
borderRadius[CORNER.TOP_LEFT][H].getAbsoluteValue(bounds.width) < HALF_WIDTH let trv = borderRadius[CORNER.TOP_RIGHT][V].getAbsoluteValue(bounds.height);
? borderRadius[CORNER.TOP_LEFT][H].getAbsoluteValue(bounds.width) let brh = borderRadius[CORNER.BOTTOM_RIGHT][H].getAbsoluteValue(bounds.width);
: HALF_WIDTH; let brv = borderRadius[CORNER.BOTTOM_RIGHT][V].getAbsoluteValue(bounds.height);
const tlv = let blh = borderRadius[CORNER.BOTTOM_LEFT][H].getAbsoluteValue(bounds.width);
borderRadius[CORNER.TOP_LEFT][V].getAbsoluteValue(bounds.height) < HALF_HEIGHT let blv = borderRadius[CORNER.BOTTOM_LEFT][V].getAbsoluteValue(bounds.height);
? borderRadius[CORNER.TOP_LEFT][V].getAbsoluteValue(bounds.height)
: HALF_HEIGHT; const factors = [];
const trh = factors.push((tlh + trh) / bounds.width);
borderRadius[CORNER.TOP_RIGHT][H].getAbsoluteValue(bounds.width) < HALF_WIDTH factors.push((blh + brh) / bounds.width);
? borderRadius[CORNER.TOP_RIGHT][H].getAbsoluteValue(bounds.width) factors.push((tlv + blv) / bounds.height);
: HALF_WIDTH; factors.push((trv + brv) / bounds.height);
const trv = const maxFactor = Math.max(...factors);
borderRadius[CORNER.TOP_RIGHT][V].getAbsoluteValue(bounds.height) < HALF_HEIGHT
? borderRadius[CORNER.TOP_RIGHT][V].getAbsoluteValue(bounds.height) if (maxFactor > 1) {
: HALF_HEIGHT; tlh /= maxFactor;
const brh = tlv /= maxFactor;
borderRadius[CORNER.BOTTOM_RIGHT][H].getAbsoluteValue(bounds.width) < HALF_WIDTH trh /= maxFactor;
? borderRadius[CORNER.BOTTOM_RIGHT][H].getAbsoluteValue(bounds.width) trv /= maxFactor;
: HALF_WIDTH; brh /= maxFactor;
const brv = brv /= maxFactor;
borderRadius[CORNER.BOTTOM_RIGHT][V].getAbsoluteValue(bounds.height) < HALF_HEIGHT blh /= maxFactor;
? borderRadius[CORNER.BOTTOM_RIGHT][V].getAbsoluteValue(bounds.height) blv /= maxFactor;
: HALF_HEIGHT; }
const blh =
borderRadius[CORNER.BOTTOM_LEFT][H].getAbsoluteValue(bounds.width) < HALF_WIDTH
? borderRadius[CORNER.BOTTOM_LEFT][H].getAbsoluteValue(bounds.width)
: HALF_WIDTH;
const blv =
borderRadius[CORNER.BOTTOM_LEFT][V].getAbsoluteValue(bounds.height) < HALF_HEIGHT
? borderRadius[CORNER.BOTTOM_LEFT][V].getAbsoluteValue(bounds.height)
: HALF_HEIGHT;
const topWidth = bounds.width - trh; const topWidth = bounds.width - trh;
const rightHeight = bounds.height - brv; const rightHeight = bounds.height - brv;

View File

@ -2,6 +2,7 @@
'use strict'; 'use strict';
import type {Bounds} from './Bounds'; import type {Bounds} from './Bounds';
import type {Options} from './index'; import type {Options} from './index';
import type {PseudoContentData, PseudoContentItem} from './PseudoNodeContent';
import type Logger from './Logger'; import type Logger from './Logger';
import {parseBounds} from './Bounds'; import {parseBounds} from './Bounds';
@ -10,6 +11,12 @@ import ResourceLoader from './ResourceLoader';
import {copyCSSStyles} from './Util'; import {copyCSSStyles} from './Util';
import {parseBackgroundImage} from './parsing/background'; import {parseBackgroundImage} from './parsing/background';
import CanvasRenderer from './renderer/CanvasRenderer'; import CanvasRenderer from './renderer/CanvasRenderer';
import {
parseCounterReset,
popCounters,
resolvePseudoContent,
PSEUDO_CONTENT_ITEM_TYPE
} from './PseudoNodeContent';
const IGNORE_ATTRIBUTE = 'data-html2canvas-ignore'; const IGNORE_ATTRIBUTE = 'data-html2canvas-ignore';
@ -24,6 +31,7 @@ export class DocumentCloner {
inlineImages: boolean; inlineImages: boolean;
copyStyles: boolean; copyStyles: boolean;
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>; renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>;
pseudoContentData: PseudoContentData;
constructor( constructor(
element: HTMLElement, element: HTMLElement,
@ -40,6 +48,10 @@ export class DocumentCloner {
this.options = options; this.options = options;
this.renderer = renderer; this.renderer = renderer;
this.resourceLoader = new ResourceLoader(options, logger, window); this.resourceLoader = new ResourceLoader(options, logger, window);
this.pseudoContentData = {
counters: {},
quoteDepth: 0
};
// $FlowFixMe // $FlowFixMe
this.documentElement = this.cloneNode(element.ownerDocument.documentElement); this.documentElement = this.cloneNode(element.ownerDocument.documentElement);
} }
@ -182,6 +194,7 @@ export class DocumentCloner {
removeContainer: this.options.removeContainer, removeContainer: this.options.removeContainer,
scale: this.options.scale, scale: this.options.scale,
foreignObjectRendering: this.options.foreignObjectRendering, foreignObjectRendering: this.options.foreignObjectRendering,
useCORS: this.options.useCORS,
target: new CanvasRenderer(), target: new CanvasRenderer(),
width, width,
height, height,
@ -216,6 +229,23 @@ export class DocumentCloner {
return tempIframe; return tempIframe;
} }
if (node instanceof HTMLStyleElement && node.sheet && node.sheet.cssRules) {
const css = [].slice.call(node.sheet.cssRules, 0).reduce((css, rule) => {
try {
if (rule && rule.cssText) {
return css + rule.cssText;
}
return css;
} catch (err) {
this.logger.log('Unable to access cssText property', rule.name);
return css;
}
}, '');
const style = node.cloneNode(false);
style.textContent = css;
return style;
}
return node.cloneNode(false); return node.cloneNode(false);
} }
@ -226,6 +256,11 @@ export class DocumentCloner {
: this.createElementClone(node); : this.createElementClone(node);
const window = node.ownerDocument.defaultView; const window = node.ownerDocument.defaultView;
const style = node instanceof window.HTMLElement ? window.getComputedStyle(node) : null;
const styleBefore =
node instanceof window.HTMLElement ? window.getComputedStyle(node, ':before') : null;
const styleAfter =
node instanceof window.HTMLElement ? window.getComputedStyle(node, ':after') : null;
if (this.referenceElement === node && clone instanceof window.HTMLElement) { if (this.referenceElement === node && clone instanceof window.HTMLElement) {
this.clonedReferenceElement = clone; this.clonedReferenceElement = clone;
@ -235,22 +270,41 @@ export class DocumentCloner {
createPseudoHideStyles(clone); createPseudoHideStyles(clone);
} }
const counters = parseCounterReset(style, this.pseudoContentData);
const contentBefore = resolvePseudoContent(node, styleBefore, this.pseudoContentData);
for (let child = node.firstChild; child; child = child.nextSibling) { for (let child = node.firstChild; child; child = child.nextSibling) {
if ( if (
child.nodeType !== Node.ELEMENT_NODE || child.nodeType !== Node.ELEMENT_NODE ||
// $FlowFixMe (child.nodeName !== 'SCRIPT' &&
(child.nodeName !== 'SCRIPT' && !child.hasAttribute(IGNORE_ATTRIBUTE)) // $FlowFixMe
!child.hasAttribute(IGNORE_ATTRIBUTE) &&
(typeof this.options.ignoreElements !== 'function' ||
// $FlowFixMe
!this.options.ignoreElements(child)))
) { ) {
if (!this.copyStyles || child.nodeName !== 'STYLE') { if (!this.copyStyles || child.nodeName !== 'STYLE') {
clone.appendChild(this.cloneNode(child)); clone.appendChild(this.cloneNode(child));
} }
} }
} }
const contentAfter = resolvePseudoContent(node, styleAfter, this.pseudoContentData);
popCounters(counters, this.pseudoContentData);
if (node instanceof window.HTMLElement && clone instanceof window.HTMLElement) { if (node instanceof window.HTMLElement && clone instanceof window.HTMLElement) {
this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_BEFORE)); if (styleBefore) {
this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_AFTER)); this.inlineAllImages(
if (this.copyStyles && !(node instanceof HTMLIFrameElement)) { inlinePseudoElement(node, clone, styleBefore, contentBefore, PSEUDO_BEFORE)
copyCSSStyles(node.ownerDocument.defaultView.getComputedStyle(node), clone); );
}
if (styleAfter) {
this.inlineAllImages(
inlinePseudoElement(node, clone, styleAfter, contentAfter, PSEUDO_AFTER)
);
}
if (style && this.copyStyles && !(node instanceof HTMLIFrameElement)) {
copyCSSStyles(style, clone);
} }
this.inlineAllImages(clone); this.inlineAllImages(clone);
if (node.scrollTop !== 0 || node.scrollLeft !== 0) { if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
@ -361,9 +415,10 @@ const cloneCanvasContents = (canvas: HTMLCanvasElement, clonedCanvas: HTMLCanvas
const inlinePseudoElement = ( const inlinePseudoElement = (
node: HTMLElement, node: HTMLElement,
clone: HTMLElement, clone: HTMLElement,
style: CSSStyleDeclaration,
contentItems: ?Array<PseudoContentItem>,
pseudoElt: ':before' | ':after' pseudoElt: ':before' | ':after'
): ?HTMLElement => { ): ?HTMLElement => {
const style = node.ownerDocument.defaultView.getComputedStyle(node, pseudoElt);
if ( if (
!style || !style ||
!style.content || !style.content ||
@ -374,20 +429,29 @@ const inlinePseudoElement = (
return; return;
} }
const content = stripQuotes(style.content); const anonymousReplacedElement = clone.ownerDocument.createElement('html2canvaspseudoelement');
const image = content.match(URL_REGEXP);
const anonymousReplacedElement = clone.ownerDocument.createElement(
image ? 'img' : 'html2canvaspseudoelement'
);
if (image) {
// $FlowFixMe
anonymousReplacedElement.src = stripQuotes(image[1]);
} else {
anonymousReplacedElement.textContent = content;
}
copyCSSStyles(style, anonymousReplacedElement); copyCSSStyles(style, anonymousReplacedElement);
if (contentItems) {
const len = contentItems.length;
for (var i = 0; i < len; i++) {
const item = contentItems[i];
switch (item.type) {
case PSEUDO_CONTENT_ITEM_TYPE.IMAGE:
const img = clone.ownerDocument.createElement('img');
img.src = parseBackgroundImage(`url(${item.value})`)[0].args[0];
img.style.opacity = '1';
anonymousReplacedElement.appendChild(img);
break;
case PSEUDO_CONTENT_ITEM_TYPE.TEXT:
anonymousReplacedElement.appendChild(
clone.ownerDocument.createTextNode(item.value)
);
break;
}
}
}
anonymousReplacedElement.className = `${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE} ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`; anonymousReplacedElement.className = `${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE} ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
clone.className += clone.className +=
pseudoElt === PSEUDO_BEFORE pseudoElt === PSEUDO_BEFORE
@ -402,13 +466,6 @@ const inlinePseudoElement = (
return anonymousReplacedElement; return anonymousReplacedElement;
}; };
const stripQuotes = (content: string): string => {
const first = content.substr(0, 1);
return first === content.substr(content.length - 1) && first.match(/['"]/)
? content.substr(1, content.length - 2)
: content;
};
const URL_REGEXP = /^url\((.+)\)$/i; const URL_REGEXP = /^url\((.+)\)$/i;
const PSEUDO_BEFORE = ':before'; const PSEUDO_BEFORE = ':before';
const PSEUDO_AFTER = ':after'; const PSEUDO_AFTER = ':after';
@ -562,14 +619,21 @@ export const cloneWindow = (
documentClone.documentElement.style.left = -bounds.left + 'px'; documentClone.documentElement.style.left = -bounds.left + 'px';
documentClone.documentElement.style.position = 'absolute'; documentClone.documentElement.style.position = 'absolute';
} }
const result = Promise.resolve([
cloneIframeContainer,
cloner.clonedReferenceElement,
cloner.resourceLoader
]);
const onclone = options.onclone;
return cloner.clonedReferenceElement instanceof cloneWindow.HTMLElement || return cloner.clonedReferenceElement instanceof cloneWindow.HTMLElement ||
cloner.clonedReferenceElement instanceof ownerDocument.defaultView.HTMLElement || cloner.clonedReferenceElement instanceof ownerDocument.defaultView.HTMLElement ||
cloner.clonedReferenceElement instanceof HTMLElement cloner.clonedReferenceElement instanceof HTMLElement
? Promise.resolve([ ? typeof onclone === 'function'
cloneIframeContainer, ? Promise.resolve().then(() => onclone(documentClone)).then(() => result)
cloner.clonedReferenceElement, : result
cloner.resourceLoader
])
: Promise.reject( : Promise.reject(
__DEV__ __DEV__
? `Error finding the ${referenceElement.nodeName} in the cloned document` ? `Error finding the ${referenceElement.nodeName} in the cloned document`
@ -578,7 +642,7 @@ export const cloneWindow = (
}); });
documentClone.open(); documentClone.open();
documentClone.write('<!DOCTYPE html><html></html>'); documentClone.write(`${serializeDoctype(document.doctype)}<html></html>`);
// Chrome scrolls the parent document for some reason after the write to the cloned window??? // Chrome scrolls the parent document for some reason after the write to the cloned window???
restoreOwnerScroll(referenceElement.ownerDocument, scrollX, scrollY); restoreOwnerScroll(referenceElement.ownerDocument, scrollX, scrollY);
documentClone.replaceChild( documentClone.replaceChild(
@ -590,3 +654,29 @@ export const cloneWindow = (
return iframeLoad; return iframeLoad;
}); });
}; };
const serializeDoctype = (doctype: ?DocumentType): string => {
let str = '';
if (doctype) {
str += '<!DOCTYPE ';
if (doctype.name) {
str += doctype.name;
}
if (doctype.internalSubset) {
str += doctype.internalSubset;
}
if (doctype.publicId) {
str += `"${doctype.publicId}"`;
}
if (doctype.systemId) {
str += `"${doctype.systemId}"`;
}
str += '>';
}
return str;
};

View File

@ -133,7 +133,7 @@ const inlineFormElement = (
if (value.length > 0 && body) { if (value.length > 0 && body) {
const wrapper = node.ownerDocument.createElement('html2canvaswrapper'); const wrapper = node.ownerDocument.createElement('html2canvaswrapper');
copyCSSStyles(node.ownerDocument.defaultView.getComputedStyle(node, null), wrapper); copyCSSStyles(node.ownerDocument.defaultView.getComputedStyle(node, null), wrapper);
wrapper.style.position = 'fixed'; wrapper.style.position = 'absolute';
wrapper.style.left = `${container.bounds.left}px`; wrapper.style.left = `${container.bounds.left}px`;
wrapper.style.top = `${container.bounds.top}px`; wrapper.style.top = `${container.bounds.top}px`;
if (!allowLinebreak) { if (!allowLinebreak) {

711
src/ListItem.js Normal file
View File

@ -0,0 +1,711 @@
/* @flow */
'use strict';
import type ResourceLoader from './ResourceLoader';
import type {ListStyleType} from './parsing/listStyle';
import {copyCSSStyles, contains} from './Util';
import NodeContainer from './NodeContainer';
import TextContainer from './TextContainer';
import {LIST_STYLE_POSITION, LIST_STYLE_TYPE} from './parsing/listStyle';
import {fromCodePoint} from './Unicode';
// Margin between the enumeration and the list item content
const MARGIN_RIGHT = 7;
const ancestorTypes = ['OL', 'UL', 'MENU'];
export const getListOwner = (container: NodeContainer): ?NodeContainer => {
let parent = container.parent;
if (!parent) {
return null;
}
do {
let isAncestor = ancestorTypes.indexOf(parent.tagName) !== -1;
if (isAncestor) {
return parent;
}
parent = parent.parent;
} while (parent);
return container.parent;
};
export const inlineListItemElement = (
node: HTMLElement,
container: NodeContainer,
resourceLoader: ResourceLoader
): void => {
const listStyle = container.style.listStyle;
if (!listStyle) {
return;
}
const style = node.ownerDocument.defaultView.getComputedStyle(node, null);
const wrapper = node.ownerDocument.createElement('html2canvaswrapper');
copyCSSStyles(style, wrapper);
wrapper.style.position = 'absolute';
wrapper.style.bottom = 'auto';
wrapper.style.display = 'block';
wrapper.style.letterSpacing = 'normal';
switch (listStyle.listStylePosition) {
case LIST_STYLE_POSITION.OUTSIDE:
wrapper.style.left = 'auto';
wrapper.style.right = `${node.ownerDocument.defaultView.innerWidth -
container.bounds.left -
container.style.margin[1].getAbsoluteValue(container.bounds.width) +
MARGIN_RIGHT}px`;
wrapper.style.textAlign = 'right';
break;
case LIST_STYLE_POSITION.INSIDE:
wrapper.style.left = `${container.bounds.left -
container.style.margin[3].getAbsoluteValue(container.bounds.width)}px`;
wrapper.style.right = 'auto';
wrapper.style.textAlign = 'left';
break;
}
let text;
const MARGIN_TOP = container.style.margin[0].getAbsoluteValue(container.bounds.width);
const styleImage = listStyle.listStyleImage;
if (styleImage) {
if (styleImage.method === 'url') {
const image = node.ownerDocument.createElement('img');
image.src = styleImage.args[0];
wrapper.style.top = `${container.bounds.top - MARGIN_TOP}px`;
wrapper.style.width = 'auto';
wrapper.style.height = 'auto';
wrapper.appendChild(image);
} else {
const size = parseFloat(container.style.font.fontSize) * 0.5;
wrapper.style.top = `${container.bounds.top -
MARGIN_TOP +
container.bounds.height -
1.5 * size}px`;
wrapper.style.width = `${size}px`;
wrapper.style.height = `${size}px`;
wrapper.style.backgroundImage = style.listStyleImage;
}
} else if (typeof container.listIndex === 'number') {
text = node.ownerDocument.createTextNode(
createCounterText(container.listIndex, listStyle.listStyleType, true)
);
wrapper.appendChild(text);
wrapper.style.top = `${container.bounds.top - MARGIN_TOP}px`;
}
// $FlowFixMe
const body: HTMLBodyElement = node.ownerDocument.body;
body.appendChild(wrapper);
if (text) {
container.childNodes.push(TextContainer.fromTextNode(text, container));
body.removeChild(wrapper);
} else {
// $FlowFixMe
container.childNodes.push(new NodeContainer(wrapper, container, resourceLoader, 0));
}
};
const ROMAN_UPPER = {
integers: [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1],
values: ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I']
};
const ARMENIAN = {
integers: [
9000,
8000,
7000,
6000,
5000,
4000,
3000,
2000,
1000,
900,
800,
700,
600,
500,
400,
300,
200,
100,
90,
80,
70,
60,
50,
40,
30,
20,
10,
9,
8,
7,
6,
5,
4,
3,
2,
1
],
values: [
'Ք',
'Փ',
'Ւ',
'Ց',
'Ր',
'Տ',
'Վ',
'Ս',
'Ռ',
'Ջ',
'Պ',
'Չ',
'Ո',
'Շ',
'Ն',
'Յ',
'Մ',
'Ճ',
'Ղ',
'Ձ',
'Հ',
'Կ',
'Ծ',
'Խ',
'Լ',
'Ի',
'Ժ',
'Թ',
'Ը',
'Է',
'Զ',
'Ե',
'Դ',
'Գ',
'Բ',
'Ա'
]
};
const HEBREW = {
integers: [
10000,
9000,
8000,
7000,
6000,
5000,
4000,
3000,
2000,
1000,
400,
300,
200,
100,
90,
80,
70,
60,
50,
40,
30,
20,
19,
18,
17,
16,
15,
10,
9,
8,
7,
6,
5,
4,
3,
2,
1
],
values: [
'י׳',
'ט׳',
'ח׳',
'ז׳',
'ו׳',
'ה׳',
'ד׳',
'ג׳',
'ב׳',
'א׳',
'ת',
'ש',
'ר',
'ק',
'צ',
'פ',
'ע',
'ס',
'נ',
'מ',
'ל',
'כ',
'יט',
'יח',
'יז',
'טז',
'טו',
'י',
'ט',
'ח',
'ז',
'ו',
'ה',
'ד',
'ג',
'ב',
'א'
]
};
const GEORGIAN = {
integers: [
10000,
9000,
8000,
7000,
6000,
5000,
4000,
3000,
2000,
1000,
900,
800,
700,
600,
500,
400,
300,
200,
100,
90,
80,
70,
60,
50,
40,
30,
20,
10,
9,
8,
7,
6,
5,
4,
3,
2,
1
],
values: [
'ჵ',
'ჰ',
'ჯ',
'ჴ',
'ხ',
'ჭ',
'წ',
'ძ',
'ც',
'ჩ',
'შ',
'',
'ღ',
'ქ',
'ფ',
'ჳ',
'ტ',
'ს',
'რ',
'ჟ',
'პ',
'ო',
'ჲ',
'ნ',
'მ',
'ლ',
'კ',
'ი',
'თ',
'ჱ',
'ზ',
'ვ',
'ე',
'დ',
'გ',
'ბ',
'ა'
]
};
const createAdditiveCounter = (
value: number,
min: number,
max: number,
symbols,
fallback: ListStyleType,
suffix: string
) => {
if (value < min || value > max) {
return createCounterText(value, fallback, suffix.length > 0);
}
return (
symbols.integers.reduce((string, integer, index) => {
while (value >= integer) {
value -= integer;
string += symbols.values[index];
}
return string;
}, '') + suffix
);
};
const createCounterStyleWithSymbolResolver = (
value: number,
codePointRangeLength: number,
isNumeric: boolean,
resolver
): string => {
let string = '';
do {
if (!isNumeric) {
value--;
}
string = resolver(value) + string;
value /= codePointRangeLength;
} while (value * codePointRangeLength >= codePointRangeLength);
return string;
};
const createCounterStyleFromRange = (
value: number,
codePointRangeStart: number,
codePointRangeEnd: number,
isNumeric: boolean,
suffix: string
): string => {
const codePointRangeLength = codePointRangeEnd - codePointRangeStart + 1;
return (
(value < 0 ? '-' : '') +
(createCounterStyleWithSymbolResolver(
Math.abs(value),
codePointRangeLength,
isNumeric,
codePoint =>
fromCodePoint(Math.floor(codePoint % codePointRangeLength) + codePointRangeStart)
) +
suffix)
);
};
const createCounterStyleFromSymbols = (
value: number,
symbols: string,
suffix: string = '. '
): string => {
const codePointRangeLength = symbols.length;
return (
createCounterStyleWithSymbolResolver(
Math.abs(value),
codePointRangeLength,
false,
codePoint => symbols[Math.floor(codePoint % codePointRangeLength)]
) + suffix
);
};
const CJK_ZEROS = 1 << 0;
const CJK_TEN_COEFFICIENTS = 1 << 1;
const CJK_TEN_HIGH_COEFFICIENTS = 1 << 2;
const CJK_HUNDRED_COEFFICIENTS = 1 << 3;
const createCJKCounter = (
value: number,
numbers: string,
multipliers: string,
negativeSign: string,
suffix: string,
flags: number
): string => {
if (value < -9999 || value > 9999) {
return createCounterText(value, LIST_STYLE_TYPE.CJK_DECIMAL, suffix.length > 0);
}
let tmp = Math.abs(value);
let string = suffix;
if (tmp === 0) {
return numbers[0] + string;
}
for (let digit = 0; tmp > 0 && digit <= 4; digit++) {
let coefficient = tmp % 10;
if (coefficient === 0 && contains(flags, CJK_ZEROS) && string !== '') {
string = numbers[coefficient] + string;
} else if (
coefficient > 1 ||
(coefficient === 1 && digit === 0) ||
(coefficient === 1 && digit === 1 && contains(flags, CJK_TEN_COEFFICIENTS)) ||
(coefficient === 1 &&
digit === 1 &&
contains(flags, CJK_TEN_HIGH_COEFFICIENTS) &&
value > 100) ||
(coefficient === 1 && digit > 1 && contains(flags, CJK_HUNDRED_COEFFICIENTS))
) {
string = numbers[coefficient] + (digit > 0 ? multipliers[digit - 1] : '') + string;
} else if (coefficient === 1 && digit > 0) {
string = multipliers[digit - 1] + string;
}
tmp = Math.floor(tmp / 10);
}
return (value < 0 ? negativeSign : '') + string;
};
const CHINESE_INFORMAL_MULTIPLIERS = '十百千萬';
const CHINESE_FORMAL_MULTIPLIERS = '拾佰仟萬';
const JAPANESE_NEGATIVE = 'マイナス';
const KOREAN_NEGATIVE = '마이너스 ';
export const createCounterText = (
value: number,
type: ListStyleType,
appendSuffix: boolean
): string => {
const defaultSuffix = appendSuffix ? '. ' : '';
const cjkSuffix = appendSuffix ? '、' : '';
const koreanSuffix = appendSuffix ? ', ' : '';
switch (type) {
case LIST_STYLE_TYPE.DISC:
return '•';
case LIST_STYLE_TYPE.CIRCLE:
return '◦';
case LIST_STYLE_TYPE.SQUARE:
return '◾';
case LIST_STYLE_TYPE.DECIMAL_LEADING_ZERO:
const string = createCounterStyleFromRange(value, 48, 57, true, defaultSuffix);
return string.length < 4 ? `0${string}` : string;
case LIST_STYLE_TYPE.CJK_DECIMAL:
return createCounterStyleFromSymbols(value, '〇一二三四五六七八九', cjkSuffix);
case LIST_STYLE_TYPE.LOWER_ROMAN:
return createAdditiveCounter(
value,
1,
3999,
ROMAN_UPPER,
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
).toLowerCase();
case LIST_STYLE_TYPE.UPPER_ROMAN:
return createAdditiveCounter(
value,
1,
3999,
ROMAN_UPPER,
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
);
case LIST_STYLE_TYPE.LOWER_GREEK:
return createCounterStyleFromRange(value, 945, 969, false, defaultSuffix);
case LIST_STYLE_TYPE.LOWER_ALPHA:
return createCounterStyleFromRange(value, 97, 122, false, defaultSuffix);
case LIST_STYLE_TYPE.UPPER_ALPHA:
return createCounterStyleFromRange(value, 65, 90, false, defaultSuffix);
case LIST_STYLE_TYPE.ARABIC_INDIC:
return createCounterStyleFromRange(value, 1632, 1641, true, defaultSuffix);
case LIST_STYLE_TYPE.ARMENIAN:
case LIST_STYLE_TYPE.UPPER_ARMENIAN:
return createAdditiveCounter(
value,
1,
9999,
ARMENIAN,
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
);
case LIST_STYLE_TYPE.LOWER_ARMENIAN:
return createAdditiveCounter(
value,
1,
9999,
ARMENIAN,
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
).toLowerCase();
case LIST_STYLE_TYPE.BENGALI:
return createCounterStyleFromRange(value, 2534, 2543, true, defaultSuffix);
case LIST_STYLE_TYPE.CAMBODIAN:
case LIST_STYLE_TYPE.KHMER:
return createCounterStyleFromRange(value, 6112, 6121, true, defaultSuffix);
case LIST_STYLE_TYPE.CJK_EARTHLY_BRANCH:
return createCounterStyleFromSymbols(value, '子丑寅卯辰巳午未申酉戌亥', cjkSuffix);
case LIST_STYLE_TYPE.CJK_HEAVENLY_STEM:
return createCounterStyleFromSymbols(value, '甲乙丙丁戊己庚辛壬癸', cjkSuffix);
case LIST_STYLE_TYPE.CJK_IDEOGRAPHIC:
case LIST_STYLE_TYPE.TRAD_CHINESE_INFORMAL:
return createCJKCounter(
value,
'零一二三四五六七八九',
CHINESE_INFORMAL_MULTIPLIERS,
'負',
cjkSuffix,
CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS
);
case LIST_STYLE_TYPE.TRAD_CHINESE_FORMAL:
return createCJKCounter(
value,
'零壹貳參肆伍陸柒捌玖',
CHINESE_FORMAL_MULTIPLIERS,
'負',
cjkSuffix,
CJK_ZEROS |
CJK_TEN_COEFFICIENTS |
CJK_TEN_HIGH_COEFFICIENTS |
CJK_HUNDRED_COEFFICIENTS
);
case LIST_STYLE_TYPE.SIMP_CHINESE_INFORMAL:
return createCJKCounter(
value,
'零一二三四五六七八九',
CHINESE_INFORMAL_MULTIPLIERS,
'负',
cjkSuffix,
CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS | CJK_HUNDRED_COEFFICIENTS
);
case LIST_STYLE_TYPE.SIMP_CHINESE_FORMAL:
return createCJKCounter(
value,
'零壹贰叁肆伍陆柒捌玖',
CHINESE_FORMAL_MULTIPLIERS,
'负',
cjkSuffix,
CJK_ZEROS |
CJK_TEN_COEFFICIENTS |
CJK_TEN_HIGH_COEFFICIENTS |
CJK_HUNDRED_COEFFICIENTS
);
case LIST_STYLE_TYPE.JAPANESE_INFORMAL:
return createCJKCounter(value, '〇一二三四五六七八九', '十百千万', JAPANESE_NEGATIVE, cjkSuffix, 0);
case LIST_STYLE_TYPE.JAPANESE_FORMAL:
return createCJKCounter(
value,
'零壱弐参四伍六七八九',
'拾百千万',
JAPANESE_NEGATIVE,
cjkSuffix,
CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS
);
case LIST_STYLE_TYPE.KOREAN_HANGUL_FORMAL:
return createCJKCounter(
value,
'영일이삼사오육칠팔구',
'십백천만',
KOREAN_NEGATIVE,
koreanSuffix,
CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS
);
case LIST_STYLE_TYPE.KOREAN_HANJA_INFORMAL:
return createCJKCounter(value, '零一二三四五六七八九', '十百千萬', KOREAN_NEGATIVE, koreanSuffix, 0);
case LIST_STYLE_TYPE.KOREAN_HANJA_FORMAL:
return createCJKCounter(
value,
'零壹貳參四五六七八九',
'拾百千',
KOREAN_NEGATIVE,
koreanSuffix,
CJK_ZEROS | CJK_TEN_COEFFICIENTS | CJK_TEN_HIGH_COEFFICIENTS
);
case LIST_STYLE_TYPE.DEVANAGARI:
return createCounterStyleFromRange(value, 0x966, 0x96f, true, defaultSuffix);
case LIST_STYLE_TYPE.GEORGIAN:
return createAdditiveCounter(
value,
1,
19999,
GEORGIAN,
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
);
case LIST_STYLE_TYPE.GUJARATI:
return createCounterStyleFromRange(value, 0xae6, 0xaef, true, defaultSuffix);
case LIST_STYLE_TYPE.GURMUKHI:
return createCounterStyleFromRange(value, 0xa66, 0xa6f, true, defaultSuffix);
case LIST_STYLE_TYPE.HEBREW:
return createAdditiveCounter(
value,
1,
10999,
HEBREW,
LIST_STYLE_TYPE.DECIMAL,
defaultSuffix
);
case LIST_STYLE_TYPE.HIRAGANA:
return createCounterStyleFromSymbols(
value,
'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわゐゑをん'
);
case LIST_STYLE_TYPE.HIRAGANA_IROHA:
return createCounterStyleFromSymbols(
value,
'いろはにほへとちりぬるをわかよたれそつねならむうゐのおくやまけふこえてあさきゆめみしゑひもせす'
);
case LIST_STYLE_TYPE.KANNADA:
return createCounterStyleFromRange(value, 0xce6, 0xcef, true, defaultSuffix);
case LIST_STYLE_TYPE.KATAKANA:
return createCounterStyleFromSymbols(
value,
'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヰヱヲン',
cjkSuffix
);
case LIST_STYLE_TYPE.KATAKANA_IROHA:
return createCounterStyleFromSymbols(
value,
'イロハニホヘトチリヌルヲワカヨタレソツネナラムウヰノオクヤマケフコエテアサキユメミシヱヒモセス',
cjkSuffix
);
case LIST_STYLE_TYPE.LAO:
return createCounterStyleFromRange(value, 0xed0, 0xed9, true, defaultSuffix);
case LIST_STYLE_TYPE.MONGOLIAN:
return createCounterStyleFromRange(value, 0x1810, 0x1819, true, defaultSuffix);
case LIST_STYLE_TYPE.MYANMAR:
return createCounterStyleFromRange(value, 0x1040, 0x1049, true, defaultSuffix);
case LIST_STYLE_TYPE.ORIYA:
return createCounterStyleFromRange(value, 0xb66, 0xb6f, true, defaultSuffix);
case LIST_STYLE_TYPE.PERSIAN:
return createCounterStyleFromRange(value, 0x6f0, 0x6f9, true, defaultSuffix);
case LIST_STYLE_TYPE.TAMIL:
return createCounterStyleFromRange(value, 0xbe6, 0xbef, true, defaultSuffix);
case LIST_STYLE_TYPE.TELUGU:
return createCounterStyleFromRange(value, 0xc66, 0xc6f, true, defaultSuffix);
case LIST_STYLE_TYPE.THAI:
return createCounterStyleFromRange(value, 0xe50, 0xe59, true, defaultSuffix);
case LIST_STYLE_TYPE.TIBETAN:
return createCounterStyleFromRange(value, 0xf20, 0xf29, true, defaultSuffix);
case LIST_STYLE_TYPE.DECIMAL:
default:
return createCounterStyleFromRange(value, 48, 57, true, defaultSuffix);
}
};

View File

@ -7,7 +7,7 @@ export default class Logger {
id: ?string; id: ?string;
constructor(enabled: boolean, id: ?string, start: ?number) { constructor(enabled: boolean, id: ?string, start: ?number) {
this.enabled = enabled; this.enabled = typeof window !== 'undefined' && enabled;
this.start = start ? start : Date.now(); this.start = start ? start : Date.now();
this.id = id; this.id = id;
} }

View File

@ -7,7 +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 {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';
@ -15,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';
@ -32,7 +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 {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';
@ -40,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';
@ -50,6 +60,7 @@ import {
getInputBorderRadius, getInputBorderRadius,
reformatInputBounds reformatInputBounds
} from './Input'; } from './Input';
import {getListOwner} from './ListItem';
type StyleDeclaration = { type StyleDeclaration = {
background: Background, background: Background,
@ -60,8 +71,12 @@ type StyleDeclaration = {
float: Float, float: Float,
font: Font, font: Font,
letterSpacing: number, letterSpacing: number,
lineBreak: LineBreak,
listStyle: ListStyle | null,
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,
@ -69,6 +84,7 @@ type StyleDeclaration = {
textTransform: TextTransform, textTransform: TextTransform,
transform: Transform, transform: Transform,
visibility: Visibility, visibility: Visibility,
wordBreak: WordBreak,
zIndex: zIndex zIndex: zIndex
}; };
@ -79,10 +95,14 @@ export default class NodeContainer {
parent: ?NodeContainer; parent: ?NodeContainer;
style: StyleDeclaration; style: StyleDeclaration;
childNodes: Array<TextContainer | Path>; childNodes: Array<TextContainer | Path>;
listItems: Array<NodeContainer>;
listIndex: ?number;
listStart: ?number;
bounds: Bounds; bounds: Bounds;
curvedBounds: BoundCurves; curvedBounds: BoundCurves;
image: ?string; image: ?string;
index: number; index: number;
tagName: string;
constructor( constructor(
node: HTMLElement | SVGSVGElement, node: HTMLElement | SVGSVGElement,
@ -91,8 +111,13 @@ export default class NodeContainer {
index: number index: number
) { ) {
this.parent = parent; this.parent = parent;
this.tagName = node.tagName;
this.index = index; this.index = index;
this.childNodes = []; this.childNodes = [];
this.listItems = [];
if (typeof node.start === 'number') {
this.listStart = node.start;
}
const defaultView = node.ownerDocument.defaultView; const defaultView = node.ownerDocument.defaultView;
const scrollX = defaultView.pageXOffset; const scrollX = defaultView.pageXOffset;
const scrollY = defaultView.pageYOffset; const scrollY = defaultView.pageYOffset;
@ -117,11 +142,17 @@ export default class NodeContainer {
float: parseCSSFloat(style.float), float: parseCSSFloat(style.float),
font: parseFont(style), font: parseFont(style),
letterSpacing: parseLetterSpacing(style.letterSpacing), letterSpacing: parseLetterSpacing(style.letterSpacing),
listStyle: display === DISPLAY.LIST_ITEM ? parseListStyle(style) : null,
lineBreak: parseLineBreak(style.lineBreak),
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),
@ -129,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')
}; };
@ -137,6 +169,20 @@ export default class NodeContainer {
node.style.transform = 'matrix(1,0,0,1,0,0)'; node.style.transform = 'matrix(1,0,0,1,0,0)';
} }
if (display === DISPLAY.LIST_ITEM) {
const listOwner = getListOwner(this);
if (listOwner) {
const listIndex = listOwner.listItems.length;
listOwner.listItems.push(this);
this.listIndex =
node.hasAttribute('value') && typeof node.value === 'number'
? node.value
: listIndex === 0
? typeof listOwner.listStart === 'number' ? listOwner.listStart : 1
: listOwner.listItems[listIndex - 1].listIndex + 1;
}
}
// TODO move bound retrieval for all nodes to a later stage? // TODO move bound retrieval for all nodes to a later stage?
if (node.tagName === 'IMG') { if (node.tagName === 'IMG') {
node.addEventListener('load', () => { node.addEventListener('load', () => {
@ -170,8 +216,7 @@ export default class NodeContainer {
} }
getClipPaths(): Array<Path> { getClipPaths(): Array<Path> {
const parentClips = this.parent ? this.parent.getClipPaths() : []; const parentClips = this.parent ? this.parent.getClipPaths() : [];
const isClipped = const isClipped = this.style.overflow !== OVERFLOW.VISIBLE;
this.style.overflow === OVERFLOW.HIDDEN || this.style.overflow === OVERFLOW.SCROLL;
return isClipped return isClipped
? parentClips.concat([calculatePaddingBoxPath(this.curvedBounds)]) ? parentClips.concat([calculatePaddingBoxPath(this.curvedBounds)])

View File

@ -6,6 +6,8 @@ import StackingContext from './StackingContext';
import NodeContainer from './NodeContainer'; import NodeContainer from './NodeContainer';
import TextContainer from './TextContainer'; import TextContainer from './TextContainer';
import {inlineInputElement, inlineTextAreaElement, inlineSelectElement} from './Input'; import {inlineInputElement, inlineTextAreaElement, inlineSelectElement} from './Input';
import {inlineListItemElement} from './ListItem';
import {LIST_STYLE_TYPE} from './parsing/listStyle';
export const NodeParser = ( export const NodeParser = (
node: HTMLElement, node: HTMLElement,
@ -71,6 +73,11 @@ const parseNodeTree = (
} else if (childNode.tagName === 'SELECT') { } else if (childNode.tagName === 'SELECT') {
// $FlowFixMe // $FlowFixMe
inlineSelectElement(childNode, container); inlineSelectElement(childNode, container);
} else if (
container.style.listStyle &&
container.style.listStyle.listStyleType !== LIST_STYLE_TYPE.NONE
) {
inlineListItemElement(childNode, container, resourceLoader);
} }
const SHOULD_TRAVERSE_CHILDREN = childNode.tagName !== 'TEXTAREA'; const SHOULD_TRAVERSE_CHILDREN = childNode.tagName !== 'TEXTAREA';

341
src/PseudoNodeContent.js Normal file
View File

@ -0,0 +1,341 @@
/* @flow */
'use strict';
import {createCounterText} from './ListItem';
import {parseListStyleType} from './parsing/listStyle';
export const PSEUDO_CONTENT_ITEM_TYPE = {
TEXT: 0,
IMAGE: 1
};
export const TOKEN_TYPE = {
STRING: 0,
ATTRIBUTE: 1,
URL: 2,
COUNTER: 3,
COUNTERS: 4,
OPENQUOTE: 5,
CLOSEQUOTE: 6
};
export type PseudoContentData = {
counters: {[string]: Array<number>},
quoteDepth: number
};
export type PseudoContentItem = {
type: $Values<typeof PSEUDO_CONTENT_ITEM_TYPE>,
value: string
};
export type Token = {
type: $Values<typeof TOKEN_TYPE>,
value?: string,
name?: string,
format?: string,
glue?: string
};
export const parseCounterReset = (
style: ?CSSStyleDeclaration,
data: PseudoContentData
): Array<string> => {
if (!style || !style.counterReset || style.counterReset === 'none') {
return [];
}
const counterNames: Array<string> = [];
const counterResets = style.counterReset.split(/\s*,\s*/);
const lenCounterResets = counterResets.length;
for (let i = 0; i < lenCounterResets; i++) {
const [counterName, initialValue] = counterResets[i].split(/\s+/);
counterNames.push(counterName);
let counter = data.counters[counterName];
if (!counter) {
counter = data.counters[counterName] = [];
}
counter.push(parseInt(initialValue || 0, 10));
}
return counterNames;
};
export const popCounters = (counterNames: Array<string>, data: PseudoContentData): void => {
const lenCounters = counterNames.length;
for (let i = 0; i < lenCounters; i++) {
data.counters[counterNames[i]].pop();
}
};
export const resolvePseudoContent = (
node: Node,
style: ?CSSStyleDeclaration,
data: PseudoContentData
): ?Array<PseudoContentItem> => {
if (
!style ||
!style.content ||
style.content === 'none' ||
style.content === '-moz-alt-content' ||
style.display === 'none'
) {
return null;
}
const tokens = parseContent(style.content);
const len = tokens.length;
const contentItems: Array<PseudoContentItem> = [];
let s = '';
// increment the counter (if there is a "counter-increment" declaration)
const counterIncrement = style.counterIncrement;
if (counterIncrement && counterIncrement !== 'none') {
const [counterName, incrementValue] = counterIncrement.split(/\s+/);
const counter = data.counters[counterName];
if (counter) {
counter[counter.length - 1] +=
incrementValue === undefined ? 1 : parseInt(incrementValue, 10);
}
}
// build the content string
for (let i = 0; i < len; i++) {
const token = tokens[i];
switch (token.type) {
case TOKEN_TYPE.STRING:
s += token.value || '';
break;
case TOKEN_TYPE.ATTRIBUTE:
if (node instanceof HTMLElement && token.value) {
s += node.getAttribute(token.value) || '';
}
break;
case TOKEN_TYPE.COUNTER:
const counter = data.counters[token.name || ''];
if (counter) {
s += formatCounterValue([counter[counter.length - 1]], '', token.format);
}
break;
case TOKEN_TYPE.COUNTERS:
const counters = data.counters[token.name || ''];
if (counters) {
s += formatCounterValue(counters, token.glue, token.format);
}
break;
case TOKEN_TYPE.OPENQUOTE:
s += getQuote(style, true, data.quoteDepth);
data.quoteDepth++;
break;
case TOKEN_TYPE.CLOSEQUOTE:
data.quoteDepth--;
s += getQuote(style, false, data.quoteDepth);
break;
case TOKEN_TYPE.URL:
if (s) {
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.TEXT, value: s});
s = '';
}
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.IMAGE, value: token.value || ''});
break;
}
}
if (s) {
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.TEXT, value: s});
}
return contentItems;
};
export const parseContent = (content: string, cache?: {[string]: Array<Token>}): Array<Token> => {
if (cache && cache[content]) {
return cache[content];
}
const tokens: Array<Token> = [];
const len = content.length;
let isString = false;
let isEscaped = false;
let isFunction = false;
let str = '';
let functionName = '';
let args = [];
for (let i = 0; i < len; i++) {
const c = content.charAt(i);
switch (c) {
case "'":
case '"':
if (isEscaped) {
str += c;
} else {
isString = !isString;
if (!isFunction && !isString) {
tokens.push({type: TOKEN_TYPE.STRING, value: str});
str = '';
}
}
break;
case '\\':
if (isEscaped) {
str += c;
isEscaped = false;
} else {
isEscaped = true;
}
break;
case '(':
if (isString) {
str += c;
} else {
isFunction = true;
functionName = str;
str = '';
args = [];
}
break;
case ')':
if (isString) {
str += c;
} else if (isFunction) {
if (str) {
args.push(str);
}
switch (functionName) {
case 'attr':
if (args.length > 0) {
tokens.push({type: TOKEN_TYPE.ATTRIBUTE, value: args[0]});
}
break;
case 'counter':
if (args.length > 0) {
const counter: Token = {
type: TOKEN_TYPE.COUNTER,
name: args[0]
};
if (args.length > 1) {
counter.format = args[1];
}
tokens.push(counter);
}
break;
case 'counters':
if (args.length > 0) {
const counters: Token = {
type: TOKEN_TYPE.COUNTERS,
name: args[0]
};
if (args.length > 1) {
counters.glue = args[1];
}
if (args.length > 2) {
counters.format = args[2];
}
tokens.push(counters);
}
break;
case 'url':
if (args.length > 0) {
tokens.push({type: TOKEN_TYPE.URL, value: args[0]});
}
break;
}
isFunction = false;
str = '';
}
break;
case ',':
if (isString) {
str += c;
} else if (isFunction) {
args.push(str);
str = '';
}
break;
case ' ':
case '\t':
if (isString) {
str += c;
} else if (str) {
addOtherToken(tokens, str);
str = '';
}
break;
default:
str += c;
}
if (c !== '\\') {
isEscaped = false;
}
}
if (str) {
addOtherToken(tokens, str);
}
if (cache) {
cache[content] = tokens;
}
return tokens;
};
const addOtherToken = (tokens: Array<Token>, identifier: string): void => {
switch (identifier) {
case 'open-quote':
tokens.push({type: TOKEN_TYPE.OPENQUOTE});
break;
case 'close-quote':
tokens.push({type: TOKEN_TYPE.CLOSEQUOTE});
break;
}
};
const getQuote = (style: CSSStyleDeclaration, isOpening: boolean, quoteDepth: number): string => {
const quotes = style.quotes ? style.quotes.split(/\s+/) : ["'\"'", "'\"'"];
let idx = quoteDepth * 2;
if (idx >= quotes.length) {
idx = quotes.length - 2;
}
if (!isOpening) {
++idx;
}
return quotes[idx].replace(/^["']|["']$/g, '');
};
const formatCounterValue = (counter, glue: ?string, format: ?string): string => {
const len = counter.length;
let result = '';
for (let i = 0; i < len; i++) {
if (i > 0) {
result += glue || '';
}
result += createCounterText(counter[i], parseListStyleType(format || 'decimal'), false);
}
return result;
};

View File

@ -165,7 +165,7 @@ export default class Renderer {
!container.style.background.backgroundColor.isTransparent() || !container.style.background.backgroundColor.isTransparent() ||
container.style.background.backgroundImage.length; container.style.background.backgroundImage.length;
const renderableBorders = container.style.border.filter( const hasRenderableBorders = container.style.border.some(
border => border =>
border.borderStyle !== BORDER_STYLE.NONE && !border.borderColor.isTransparent() border.borderStyle !== BORDER_STYLE.NONE && !border.borderColor.isTransparent()
); );
@ -186,12 +186,17 @@ export default class Renderer {
}); });
} }
renderableBorders.forEach((border, side) => { container.style.border.forEach((border, side) => {
this.renderBorder(border, side, container.curvedBounds); if (
border.borderStyle !== BORDER_STYLE.NONE &&
!border.borderColor.isTransparent()
) {
this.renderBorder(border, side, container.curvedBounds);
}
}); });
}; };
if (HAS_BACKGROUND || renderableBorders.length) { if (HAS_BACKGROUND || hasRenderableBorders) {
const paths = container.parent ? container.parent.getClipPaths() : []; const paths = container.parent ? container.parent.getClipPaths() : [];
if (paths.length) { if (paths.length) {
this.target.clip(paths, callback); this.target.clip(paths, callback);

View File

@ -33,6 +33,10 @@ export default class ResourceLoader {
if (this.hasResourceInCache(src)) { if (this.hasResourceInCache(src)) {
return src; return src;
} }
if (isBlobImage(src)) {
this.cache[src] = loadImage(src, this.options.imageTimeout || 0);
return src;
}
if (!isSVG(src) || FEATURES.SUPPORT_SVG_DRAWING) { if (!isSVG(src) || FEATURES.SUPPORT_SVG_DRAWING) {
if (this.options.allowTaint === true || isInlineImage(src) || this.isSameOrigin(src)) { if (this.options.allowTaint === true || isInlineImage(src) || this.isSameOrigin(src)) {
@ -215,6 +219,7 @@ const INLINE_IMG = /^data:image\/.*/i;
const isInlineImage = (src: string): boolean => INLINE_IMG.test(src); const isInlineImage = (src: string): boolean => INLINE_IMG.test(src);
const isInlineBase64Image = (src: string): boolean => INLINE_BASE64.test(src); const isInlineBase64Image = (src: string): boolean => INLINE_BASE64.test(src);
const isBlobImage = (src: string): boolean => src.substr(0, 4) === 'blob';
const isSVG = (src: string): boolean => const isSVG = (src: string): boolean =>
src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src); src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src);

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
);
};

27
src/Unicode.js Normal file
View File

@ -0,0 +1,27 @@
/* @flow */
'use strict';
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());
}
return words;
};

View File

@ -97,16 +97,6 @@ export const renderElement = (
} }
return resourceLoader.ready().then(imageStore => { return resourceLoader.ready().then(imageStore => {
if (options.removeContainer === true) {
if (container.parentNode) {
container.parentNode.removeChild(container);
} else if (__DEV__) {
logger.log(
`Cannot detach cloned iframe as it is not in the DOM anymore`
);
}
}
const fontMetrics = new FontMetrics(clonedDocument); const fontMetrics = new FontMetrics(clonedDocument);
if (__DEV__) { if (__DEV__) {
logger.log(`Starting renderer`); logger.log(`Starting renderer`);
@ -133,7 +123,18 @@ export const renderElement = (
); );
} else { } else {
const renderer = new Renderer(options.target, renderOptions); const renderer = new Renderer(options.target, renderOptions);
return renderer.render(stack); const canvas = renderer.render(stack);
if (options.removeContainer === true) {
if (container.parentNode) {
container.parentNode.removeChild(container);
} else if (__DEV__) {
logger.log(
`Cannot detach cloned iframe as it is not in the DOM anymore`
);
}
}
return canvas;
} }
}); });
}) })

View File

@ -14,12 +14,15 @@ export type Options = {
backgroundColor: string, backgroundColor: string,
canvas: ?HTMLCanvasElement, canvas: ?HTMLCanvasElement,
foreignObjectRendering: boolean, foreignObjectRendering: boolean,
ignoreElements?: HTMLElement => boolean,
imageTimeout: number, imageTimeout: number,
logging: boolean, logging: boolean,
onclone?: Document => void,
proxy: ?string, proxy: ?string,
removeContainer: ?boolean, removeContainer: ?boolean,
scale: number, scale: number,
target: RenderTarget<*>, target: RenderTarget<*>,
useCORS: boolean,
width: number, width: number,
height: number, height: number,
x: number, x: number,
@ -31,14 +34,9 @@ export type Options = {
}; };
const html2canvas = (element: HTMLElement, conf: ?Options): Promise<*> => { const html2canvas = (element: HTMLElement, conf: ?Options): Promise<*> => {
// eslint-disable-next-line no-console
if (typeof console === 'object' && typeof console.log === 'function') {
// eslint-disable-next-line no-console
console.log(`html2canvas ${__VERSION__}`);
}
const config = conf || {}; const config = conf || {};
const logger = new Logger(typeof config.logging === 'boolean' ? config.logging : true); const logger = new Logger(typeof config.logging === 'boolean' ? config.logging : true);
logger.log(`html2canvas ${__VERSION__}`);
if (__DEV__ && typeof config.onrendered === 'function') { if (__DEV__ && typeof config.onrendered === 'function') {
logger.error( logger.error(
@ -72,6 +70,7 @@ const html2canvas = (element: HTMLElement, conf: ?Options): Promise<*> => {
foreignObjectRendering: false, foreignObjectRendering: false,
scale: defaultView.devicePixelRatio || 1, scale: defaultView.devicePixelRatio || 1,
target: new CanvasRenderer(config.canvas), target: new CanvasRenderer(config.canvas),
useCORS: false,
x: left, x: left,
y: top, y: top,
width: Math.ceil(width), width: Math.ceil(width),

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;
}
};

209
src/parsing/listStyle.js Normal file
View File

@ -0,0 +1,209 @@
/* @flow */
'use strict';
import type {BackgroundSource} from './background';
import {parseBackgroundImage} from './background';
export type ListStyle = {
listStyleType: ListStyleType,
listStyleImage: ?BackgroundSource,
listStylePosition: ListStylePosition
};
export const LIST_STYLE_POSITION = {
INSIDE: 0,
OUTSIDE: 1
};
export const LIST_STYLE_TYPE = {
NONE: -1,
DISC: 0,
CIRCLE: 1,
SQUARE: 2,
DECIMAL: 3,
CJK_DECIMAL: 4,
DECIMAL_LEADING_ZERO: 5,
LOWER_ROMAN: 6,
UPPER_ROMAN: 7,
LOWER_GREEK: 8,
LOWER_ALPHA: 9,
UPPER_ALPHA: 10,
ARABIC_INDIC: 11,
ARMENIAN: 12,
BENGALI: 13,
CAMBODIAN: 14,
CJK_EARTHLY_BRANCH: 15,
CJK_HEAVENLY_STEM: 16,
CJK_IDEOGRAPHIC: 17,
DEVANAGARI: 18,
ETHIOPIC_NUMERIC: 19,
GEORGIAN: 20,
GUJARATI: 21,
GURMUKHI: 22,
HEBREW: 22,
HIRAGANA: 23,
HIRAGANA_IROHA: 24,
JAPANESE_FORMAL: 25,
JAPANESE_INFORMAL: 26,
KANNADA: 27,
KATAKANA: 28,
KATAKANA_IROHA: 29,
KHMER: 30,
KOREAN_HANGUL_FORMAL: 31,
KOREAN_HANJA_FORMAL: 32,
KOREAN_HANJA_INFORMAL: 33,
LAO: 34,
LOWER_ARMENIAN: 35,
MALAYALAM: 36,
MONGOLIAN: 37,
MYANMAR: 38,
ORIYA: 39,
PERSIAN: 40,
SIMP_CHINESE_FORMAL: 41,
SIMP_CHINESE_INFORMAL: 42,
TAMIL: 43,
TELUGU: 44,
THAI: 45,
TIBETAN: 46,
TRAD_CHINESE_FORMAL: 47,
TRAD_CHINESE_INFORMAL: 48,
UPPER_ARMENIAN: 49,
DISCLOSURE_OPEN: 50,
DISCLOSURE_CLOSED: 51
};
export type ListStylePosition = $Values<typeof LIST_STYLE_POSITION>;
export type ListStyleType = $Values<typeof LIST_STYLE_TYPE>;
export const parseListStyleType = (type: string): ListStyleType => {
switch (type) {
case 'disc':
return LIST_STYLE_TYPE.DISC;
case 'circle':
return LIST_STYLE_TYPE.CIRCLE;
case 'square':
return LIST_STYLE_TYPE.SQUARE;
case 'decimal':
return LIST_STYLE_TYPE.DECIMAL;
case 'cjk-decimal':
return LIST_STYLE_TYPE.CJK_DECIMAL;
case 'decimal-leading-zero':
return LIST_STYLE_TYPE.DECIMAL_LEADING_ZERO;
case 'lower-roman':
return LIST_STYLE_TYPE.LOWER_ROMAN;
case 'upper-roman':
return LIST_STYLE_TYPE.UPPER_ROMAN;
case 'lower-greek':
return LIST_STYLE_TYPE.LOWER_GREEK;
case 'lower-alpha':
return LIST_STYLE_TYPE.LOWER_ALPHA;
case 'upper-alpha':
return LIST_STYLE_TYPE.UPPER_ALPHA;
case 'arabic-indic':
return LIST_STYLE_TYPE.ARABIC_INDIC;
case 'armenian':
return LIST_STYLE_TYPE.ARMENIAN;
case 'bengali':
return LIST_STYLE_TYPE.BENGALI;
case 'cambodian':
return LIST_STYLE_TYPE.CAMBODIAN;
case 'cjk-earthly-branch':
return LIST_STYLE_TYPE.CJK_EARTHLY_BRANCH;
case 'cjk-heavenly-stem':
return LIST_STYLE_TYPE.CJK_HEAVENLY_STEM;
case 'cjk-ideographic':
return LIST_STYLE_TYPE.CJK_IDEOGRAPHIC;
case 'devanagari':
return LIST_STYLE_TYPE.DEVANAGARI;
case 'ethiopic-numeric':
return LIST_STYLE_TYPE.ETHIOPIC_NUMERIC;
case 'georgian':
return LIST_STYLE_TYPE.GEORGIAN;
case 'gujarati':
return LIST_STYLE_TYPE.GUJARATI;
case 'gurmukhi':
return LIST_STYLE_TYPE.GURMUKHI;
case 'hebrew':
return LIST_STYLE_TYPE.HEBREW;
case 'hiragana':
return LIST_STYLE_TYPE.HIRAGANA;
case 'hiragana-iroha':
return LIST_STYLE_TYPE.HIRAGANA_IROHA;
case 'japanese-formal':
return LIST_STYLE_TYPE.JAPANESE_FORMAL;
case 'japanese-informal':
return LIST_STYLE_TYPE.JAPANESE_INFORMAL;
case 'kannada':
return LIST_STYLE_TYPE.KANNADA;
case 'katakana':
return LIST_STYLE_TYPE.KATAKANA;
case 'katakana-iroha':
return LIST_STYLE_TYPE.KATAKANA_IROHA;
case 'khmer':
return LIST_STYLE_TYPE.KHMER;
case 'korean-hangul-formal':
return LIST_STYLE_TYPE.KOREAN_HANGUL_FORMAL;
case 'korean-hanja-formal':
return LIST_STYLE_TYPE.KOREAN_HANJA_FORMAL;
case 'korean-hanja-informal':
return LIST_STYLE_TYPE.KOREAN_HANJA_INFORMAL;
case 'lao':
return LIST_STYLE_TYPE.LAO;
case 'lower-armenian':
return LIST_STYLE_TYPE.LOWER_ARMENIAN;
case 'malayalam':
return LIST_STYLE_TYPE.MALAYALAM;
case 'mongolian':
return LIST_STYLE_TYPE.MONGOLIAN;
case 'myanmar':
return LIST_STYLE_TYPE.MYANMAR;
case 'oriya':
return LIST_STYLE_TYPE.ORIYA;
case 'persian':
return LIST_STYLE_TYPE.PERSIAN;
case 'simp-chinese-formal':
return LIST_STYLE_TYPE.SIMP_CHINESE_FORMAL;
case 'simp-chinese-informal':
return LIST_STYLE_TYPE.SIMP_CHINESE_INFORMAL;
case 'tamil':
return LIST_STYLE_TYPE.TAMIL;
case 'telugu':
return LIST_STYLE_TYPE.TELUGU;
case 'thai':
return LIST_STYLE_TYPE.THAI;
case 'tibetan':
return LIST_STYLE_TYPE.TIBETAN;
case 'trad-chinese-formal':
return LIST_STYLE_TYPE.TRAD_CHINESE_FORMAL;
case 'trad-chinese-informal':
return LIST_STYLE_TYPE.TRAD_CHINESE_INFORMAL;
case 'upper-armenian':
return LIST_STYLE_TYPE.UPPER_ARMENIAN;
case 'disclosure-open':
return LIST_STYLE_TYPE.DISCLOSURE_OPEN;
case 'disclosure-closed':
return LIST_STYLE_TYPE.DISCLOSURE_CLOSED;
case 'none':
default:
return LIST_STYLE_TYPE.NONE;
}
};
export const parseListStyle = (style: CSSStyleDeclaration): ListStyle => {
const listStyleImage = parseBackgroundImage(style.getPropertyValue('list-style-image'));
return {
listStyleType: parseListStyleType(style.getPropertyValue('list-style-type')),
listStyleImage: listStyleImage.length ? listStyleImage[0] : null,
listStylePosition: parseListStylePosition(style.getPropertyValue('list-style-position'))
};
};
const parseListStylePosition = (position: string): ListStylePosition => {
switch (position) {
case 'inside':
return LIST_STYLE_POSITION.INSIDE;
case 'outside':
default:
return LIST_STYLE_POSITION.OUTSIDE;
}
};

11
src/parsing/margin.js Normal file
View File

@ -0,0 +1,11 @@
/* @flow */
'use strict';
import Length from '../Length';
const SIDES = ['top', 'right', 'bottom', 'left'];
export type Margin = Array<Length>;
export const parseMargin = (style: CSSStyleDeclaration): Margin => {
return SIDES.map(side => new Length(style.getPropertyValue(`margin-${side}`)));
};

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,111 @@
const PseudoNodeContent = require('../../dist/npm/PseudoNodeContent');
const assert = require('assert');
describe('PseudoNodeContent', function() {
it('should parse string', function() {
assert.deepEqual(PseudoNodeContent.parseContent('"hello"'), [
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: 'hello'}
]);
});
it('should parse string with (,)', function() {
assert.deepEqual(PseudoNodeContent.parseContent('"a,b (c) d"'), [
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: 'a,b (c) d'}
]);
});
it('should parse string with escaped quotes', function() {
assert.deepEqual(PseudoNodeContent.parseContent('"3.14\\""'), [
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: '3.14"'}
]);
});
it('should parse string with escape', function() {
assert.deepEqual(PseudoNodeContent.parseContent('"a\\) \\\\ b"'), [
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: 'a) \\ b'}
]);
});
it('should parse two strings', function() {
assert.deepEqual(PseudoNodeContent.parseContent('"hello" \'world\''), [
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: 'hello'},
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: 'world'}
]);
});
it('should parse counter', function() {
assert.deepEqual(PseudoNodeContent.parseContent('counter(x)'), [
{type: PseudoNodeContent.TOKEN_TYPE.COUNTER, name: 'x'}
]);
});
it('should parse counters', function() {
assert.deepEqual(PseudoNodeContent.parseContent('counters(x, "-")'), [
{type: PseudoNodeContent.TOKEN_TYPE.COUNTERS, name: 'x', glue: '-'}
]);
});
it('should parse strings and counters', function() {
assert.deepEqual(PseudoNodeContent.parseContent('"["counters(c2, " < ") \']\''), [
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: '['},
{type: PseudoNodeContent.TOKEN_TYPE.COUNTERS, name: 'c2', glue: ' < '},
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: ']'}
]);
});
it('should parse counter with format', function() {
assert.deepEqual(PseudoNodeContent.parseContent('counter(x, lower-greek)'), [
{type: PseudoNodeContent.TOKEN_TYPE.COUNTER, name: 'x', format: 'lower-greek'}
]);
});
it('should parse counters with format', function() {
assert.deepEqual(PseudoNodeContent.parseContent('counters(x, "-", upper-roman)'), [
{
type: PseudoNodeContent.TOKEN_TYPE.COUNTERS,
name: 'x',
glue: '-',
format: 'upper-roman'
}
]);
});
it('should parse strings and counters with format', function() {
assert.deepEqual(PseudoNodeContent.parseContent("\"[\"counters(c2, ' < ', disc) ']'"), [
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: '['},
{type: PseudoNodeContent.TOKEN_TYPE.COUNTERS, name: 'c2', glue: ' < ', format: 'disc'},
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: ']'}
]);
});
it('should parse attr', function() {
assert.deepEqual(PseudoNodeContent.parseContent('attr(id)'), [
{type: PseudoNodeContent.TOKEN_TYPE.ATTRIBUTE, value: 'id'}
]);
});
it('should parse url', function() {
assert.deepEqual(PseudoNodeContent.parseContent('url(http://www.abc.de/f/g.png)'), [
{type: PseudoNodeContent.TOKEN_TYPE.URL, value: 'http://www.abc.de/f/g.png'}
]);
});
it('should parse open-quote', function() {
assert.deepEqual(PseudoNodeContent.parseContent('open-quote'), [
{type: PseudoNodeContent.TOKEN_TYPE.OPENQUOTE}
]);
});
it('should parse close-quote', function() {
assert.deepEqual(PseudoNodeContent.parseContent('close-quote'), [
{type: PseudoNodeContent.TOKEN_TYPE.CLOSEQUOTE}
]);
});
it('should parse open-quote and string', function() {
assert.deepEqual(PseudoNodeContent.parseContent('open-quote "!"'), [
{type: PseudoNodeContent.TOKEN_TYPE.OPENQUOTE},
{type: PseudoNodeContent.TOKEN_TYPE.STRING, value: '!'}
]);
});
});

View File

@ -5,7 +5,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script> <script type="text/javascript" src="../../test.js"></script>
<style type="text/css"> <style type="text/css">
div { .box {
width: 200px; width: 200px;
height: 200px; height: 200px;
display: inline-block; display: inline-block;
@ -63,6 +63,21 @@
border-radius: 200px; border-radius: 200px;
} }
.gauge{
display: inline-block;
width: 100px;
height: 50px;
background: green;
margin-bottom: 20px;
}
.gauge1{ border-radius: 25px 25px 0 0 / 25px 25px 0 0; }
.gauge2{ border-radius: 100px 100px 0 0 / 50px 50px 0 0; }
.gauge3{ border-radius: 100px 100px 0 0 / 100px 100px 0 0; }
.gauge4{ border-radius: 300px 100px 0 0 / 100px 100px 0 0; }
.gauge5{ border-radius: 400px 400px 50px 50px / 50px 50px 50px 50px; }
html { html {
background: #3a84c3; background: #3a84c3;
} }
@ -70,11 +85,16 @@
</head> </head>
<body> <body>
<div class="box1">&nbsp;</div> <div class="box box1">&nbsp;</div>
<div class="box2">&nbsp;</div> <div class="box box2">&nbsp;</div>
<div class="box3">&nbsp;</div> <div class="box box3">&nbsp;</div>
<div class="box4">&nbsp;</div> <div class="box box4">&nbsp;</div>
<div class="box5">&nbsp;</div> <div class="box box5">&nbsp;</div>
<div class="box6">&nbsp;</div> <div class="box box6">&nbsp;</div>
<div class="gauge gauge1"></div>
<div class="gauge gauge2"></div>
<div class="gauge gauge3"></div>
<div class="gauge gauge4"></div>
<div class="gauge gauge5"></div>
</body> </body>
</html> </html>

View File

@ -12,6 +12,7 @@
margin: 10px; margin: 10px;
background:#6F428C; background:#6F428C;
border-style: solid; border-style: solid;
border-width: 0;
} }
.box1 { .box1 {
@ -33,6 +34,12 @@
border-color: green; border-color: green;
} }
.box5 {
border-style: none;
border-bottom: 50px solid #807d32;
border-bottom-width: 50px;
}
html { html {
background: #3a84c3; background: #3a84c3;
} }
@ -43,5 +50,6 @@
<div class="box2">&nbsp;</div> <div class="box2">&nbsp;</div>
<div class="box3">&nbsp;</div> <div class="box3">&nbsp;</div>
<div class="box4">&nbsp;</div> <div class="box4">&nbsp;</div>
<div class="box5">&nbsp;</div>
</body> </body>
</html> </html>

View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<title>Dynamic style</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../test.js"></script>
<style id="style">
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v18/CWB0XYA8bzo0kSThX0UTuA.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215;
}
body { font-family: "Roboto", serif }
div {
padding: 20px;
}
.div1 {
background: darkred;
color: white;
}
@media (min-width: 10px) {
.div1 {
background: darkgreen;
}
}
</style>
<script>
document.querySelector('#style').sheet.insertRule(
'.div2 { background: darkgreen; color:white; }',
document.querySelector('#style').sheet.cssRules.length
);
</script>
</head>
<body>
<div class="div1">Static styles</div>
<div class="div2">Dynamic styles</div>
</body>
</html>

View File

@ -0,0 +1,11 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<script type="text/javascript" src="../../test.js"></script>
</head>
<body>
<img src="../../assets/image.jpg" />
<img src="../../assets/image2.jpg" style="display:block;" />
<br>
</body>
</html>

View File

@ -0,0 +1,102 @@
<!doctype html>
<html>
<head>
<title>List tests</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script>
<style>
li {
margin: 10px 5%;
}
.list1 {
list-style-type: circle;
}
.list2 {
list-style-image: url(../../assets/image.jpg);
}
.list3 {
list-style-image: linear-gradient(60deg, deeppink, aquamarine);
list-style-position: inside;
}
.list4 {
}
.list5 {
list-style-type: lower-roman;
}
.list6 {
list-style-type: hiragana;
}
.list7 {
list-style-type: simp-chinese-informal;
}
.list8 {
list-style-type: lower-roman;
}
.list8 li {
display: block;
}
.list9 {
display: list-item;
list-style-type: lower-alpha;
margin: 10px;
position: relative;
left: 200px;
}
</style>
</head>
<body>
<ul class="list1">
<li>Alpha</li>
<li>Beta</li>
<li>Gamma</li>
</ul>
<ul class="list2">
<li>Alpha</li>
<li>Beta</li>
<li>Gamma</li>
</ul>
<ul class="list3">
<li>Alpha</li>
<li>Beta</li>
<li>Gamma</li>
</ul>
<ol class="list4" start="-1">
<li>Alpha</li>
<li>Beta</li>
<li>Gamma</li>
</ol>
<ol class="list5">
<li>Alpha</li>
<li value="5">Beta</li>
<li>Gamma</li>
</ol>
<ol class="list6">
<li>Alpha</li>
<li>Beta</li>
<li>Gamma</li>
</ol>
<ol class="list7">
<li>Alpha</li>
<li>Beta</li>
<li>Gamma</li>
</ol>
<ol class="list8">
<li>Alpha</li>
<li>Beta</li>
<li>Gamma</li>
</ol>
<div class="list9">Alpha</div>
<div class="list9">Beta</div>
<div class="list9">Gamma</div>
</body>
</html>

View File

@ -4,7 +4,9 @@
<title>element render test</title> <title>element render test</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script> <script>
h2cOptions = {ignoreElements: function(element) {
return element.className === 'ignored';
}};
</script> </script>
<script type="text/javascript" src="../../test.js"></script> <script type="text/javascript" src="../../test.js"></script>
<style> <style>
@ -15,7 +17,7 @@
background: green; background: green;
} }
#ignored { #ignored, .ignored {
background: red; background: red;
width: 100px; width: 100px;
height: 100px; height: 100px;
@ -32,10 +34,11 @@
<div id="ignored" data-html2canvas-ignore> <div id="ignored" data-html2canvas-ignore>
great failure great failure
</div> </div>
<div class="ignored">
ignore predicate
</div>
<div id="div1"> <div id="div1">
great success great success
</div> </div>
</body> </body>
</html> </html>

View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<title>element render test</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script>
h2cOptions = {onclone: function(document) {
const remove = document.querySelector('.ignored');
remove.parentNode.removeChild(remove);
}};
</script>
<script type="text/javascript" src="../../test.js"></script>
<style>
#div1 {
width: 100px;
height: 100px;
background: green;
}
#ignored, .ignored {
background: red;
width: 100px;
height: 100px;
}
body, html {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div class="ignored">
ignore during onclone
</div>
<div id="div1">
great success
</div>
</body>
</html>

View File

@ -1,84 +1,122 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Overflow tests</title> <title>Overflow tests</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script> <script type="text/javascript" src="../../test.js"></script>
<style> <style>
.small{ .small{
font-size:14px; font-size:14px;
} }
.medium{ .medium{
font-size:18px; font-size:18px;
} }
.large{ .large{
font-size:24px; font-size:24px;
} }
div{ div{
background: #ccc; background: #ccc;
border:6px solid black; border:6px solid black;
width:300px; width:300px;
height:200px; height:200px;
margin: 0 0 60px 0; margin: 0 0 60px 0;
} }
h1 { h1 {
margin:0; margin:0;
line-height: 40px; line-height: 40px;
} }
body { body {
font-family: Arial; font-family: Arial;
line-height: 17px; line-height: 17px;
font-size: 1em; font-size: 1em;
} }
</style> .cell {
display: inline-block;
width: 100px;
float: right;
height: 100px;
}
</head> .cell p {
<body> height: 60px;
}
<h1>Overflow: visible</h1> .visible {
<div> Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum. overflow: visible;
</div> }
.hidden {
overflow: hidden;
}
.scroll {
overflow: scroll;
}
.auto {
overflow: auto;
}
</style>
<h1>Overflow: hidden</h1> </head>
<div style="overflow:hidden;float: right; height: 100px; border-radius: 100%; border-color: transparent;"> <body>
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s <div class="cell">
visible
<p class="visible">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec luctus pretium facilisis. Praesent rutrum eget nisl in tristique. Sed tincidunt nisl et tellus vulputate, nec rhoncus orci pretium.</p>
</div>
<div class="cell">
hidden
<p class="hidden">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec luctus pretium facilisis. Praesent rutrum eget nisl in tristique. Sed tincidunt nisl et tellus vulputate, nec rhoncus orci pretium.</p>
</div>
<div class="cell">
scroll
<p class="scroll">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec luctus pretium facilisis. Praesent rutrum eget nisl in tristique. Sed tincidunt nisl et tellus vulputate, nec rhoncus orci pretium.</p>
</div>
<div class="cell">
auto
<p class="auto">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec luctus pretium facilisis. Praesent rutrum eget nisl in tristique. Sed tincidunt nisl et tellus vulputate, nec rhoncus orci pretium.</p>
</div>
<h1>Overflow: visible</h1>
<div> Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum.
</div>
with the release of <div style="border-width:10px 0 5px 0;background:green;">a</div>Letraset sheets containing Lorem Ipsum passages, <img src="../../assets/image.jpg" /> and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum. <h1>Overflow: hidden</h1>
<div style="overflow:hidden;float: right; height: 100px; border-radius: 100%; border-color: transparent;">
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s
with the release of <div style="border-width:10px 0 5px 0;background:green;">a</div>Letraset sheets containing Lorem Ipsum passages, <img src="../../assets/image.jpg" /> and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum.
<div style="overflow:visible;position:relative;"><u>position:relative within a overflow:hidden element</u><br /> <br /> <div style="overflow:visible;position:relative;"><u>position:relative within a overflow:hidden element</u><br /> <br />
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum. Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum.
</div> </div>
</div> </div>
<div style="overflow:hidden;"> <div style="overflow:hidden;">
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s
with the release of <div style="border-width:10px 0 5px 0;background:green;">a</div>Letraset sheets containing Lorem Ipsum passages, <img src="../../assets/image.jpg" /> and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum. with the release of <div style="border-width:10px 0 5px 0;background:green;">a</div>Letraset sheets containing Lorem Ipsum passages, <img src="../../assets/image.jpg" /> and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum.
<div style="overflow:visible;position:relative;"><u>position:relative within a overflow:hidden element</u><br /> <br /> <div style="overflow:visible;position:relative;"><u>position:relative within a overflow:hidden element</u><br /> <br />
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum. Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum.
</div> </div>
</div> </div>
<div style="overflow:scroll; height: 100px;" id="scroll"> <div style="overflow:scroll; height: 100px;" id="scroll">
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s
with the release of <div style="border-width:10px 0 5px 0;background:green;">a</div>Letraset sheets containing Lorem Ipsum passages, <img src="../../assets/image.jpg" /> and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum. with the release of <div style="border-width:10px 0 5px 0;background:green;">a</div>Letraset sheets containing Lorem Ipsum passages, <img src="../../assets/image.jpg" /> and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum.
<div style="overflow:visible;position:relative;"><u>position:relative within a overflow:hidden element</u><br /> <br /> <div style="overflow:visible;position:relative;"><u>position:relative within a overflow:hidden element</u><br /> <br />
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum. Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like <b>Aldus PageMaker</b> including versions of Lorem Ipsum.
</div> </div>
<script> <script>
document.querySelector('#scroll').scrollTo(0, 200); document.querySelector('#scroll').scrollTo(0, 200);
</script> </script>
</div> </div>
</body> </body>
</html> </html>

View File

@ -0,0 +1,104 @@
<!doctype html>
<html>
<head>
<script type="text/javascript" src="../test.js"></script>
<style>
body {
quotes: "<{" "}>" "->" "<-" "(" ")" "-:" ":-";
}
.counter1,
.counter2,
.quotes1,
.attr-url {
border: 1px solid deepskyblue;
padding: 5px;
margin-bottom: 10px;
}
.counter1 {
counter-reset: c1 3;
}
.counter1 > div::before {
content: "«\"" counter(c1) "\»";
counter-increment: c1 -1;
}
.counter2 {
counter-reset: c2;
}
.counter2 > div::before {
content: "["counters(c2, " < ", upper-roman) ']';
counter-increment: c2 2;
}
.quotes1::before {
content: open-quote "!" open-quote close-quote open-quote;
}
.quotes1::after {
content: "!" close-quote close-quote;
}
.quotes2 {
quotes: "«" "»" "“" "”";
}
.quotes2::before {
content: open-quote;
}
.quotes2::after {
content: close-quote;
}
.attr-url > div::after {
content: url(../assets/image.jpg) "///" attr(data-text);
}
</style>
</head>
<body>
<div class="counter1">
<div>A</div>
<div>B</div>
<div>C</div>
<div>D</div>
</div>
<div class="counter2">
<div>A</div>
<div>B</div>
<div>
C
<div class="counter2">
<div>a</div>
<div>b</div>
<div>
c
<div class="counter2">
<div>Aa</div>
<div>Bb</div>
<div>Cc</div>
</div>
</div>
</div>
</div>
<div>D</div>
</div>
<div class="quotes1">
Hello
<div class="quotes2">
Quoted
<div class="quotes2">World</div>
</div>
</div>
<div class="attr-url">
<div data-text="Hello World"></div>
</div>
</body>
</html>

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,28 @@
<!DOCTYPE html>
<html>
<head>
<title>Thai text</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="../../test.js"></script>
<style>
.text-block {
width: 500px;
font-family: serif;
float:left;
text-align:justify;
}
</style>
</head>
<body>
<div id="text" class="text-block">
<p>.....</p>
<p>ทดสอบ แบบกำหนดรูปแบบ ทำ ที นี่ นู่น นั่น นี้ มี หรือ ไม่ </p>
<p>ภาษาไทย เป็นภาษาราชการของประเทศไทย และภาษาแม่ของชาวไทย และชนเชื้อสายอื่นในประเทศไทย ภาษาไทยเป็นภาษาในกลุ่มภาษาไท ซึ่งเป็นกลุ่มย่อยของตระกูลภาษาไท-กะได สันนิษฐานว่า ภาษาในตระกูลนี้มีถิ่นกำเนิดจากทางตอนใต้ของประเทศจีน และนักภาษาศาสตร์บางส่วนเสนอว่า ภาษาไทยน่าจะมีความเชื่อมโยงกับตระกูลภาษาออสโตร-เอเชียติก ตระกูลภาษาออสโตรนีเซียน และตระกูลภาษาจีน-ทิเบต</p>
<p>ภาษาไทยเป็นภาษาที่มีระดับเสียงของคำแน่นอนหรือวรรณยุกต์เช่นเดียวกับภาษาจีน และออกเสียงแยกคำต่อคำ ทำให้เป็นที่ลำบากของชาวต่างชาติเนื่องจากการออกเสียงวรรณยุกต์ที่เป็นเอกลักษณ์ของแต่ละคำ และการสะกดคำที่ซับซ้อน</p>
<p>คำว่า ไทย หมายความว่า อิสรภาพ เสรีภาพ หรืออีกความหมายหนึ่งคือ ใหญ่ ยิ่งใหญ่ เพราะการจะเป็นอิสระได้จะต้องมีกำลังที่มากกว่า แข็งแกร่งกว่า เพื่อป้องกันการรุกรานจากข้าศึก คำนี้เป็นคำไทยแท้ที่เกิดจากการสร้างคำที่เรียก "การลากคำเข้าวัด" ซึ่งเป็นการลากความวิธีหนึ่ง ตามหลักคติชนวิทยา คนไทยเป็นชนชาติที่นับถือกันว่า ภาษาบาลี ซึ่งเป็นภาษาที่บันทึกพระธรรมคำสอนของพระพุทธเจ้าเป็นภาษาอันศักดิ์สิทธิ์และเป็นมงคล เมื่อคนไทยต้องการตั้งชื่อประเทศว่า ไท ซึ่งเป็นคำไทยแท้ จึงเติมตัว ย เข้าไปข้างท้าย เพื่อให้มีลักษณะคล้ายคำในภาษาบาลี - สันสกฤตเพื่อความเป็นมงคลตามความเชื่อของตน ภาษาไทยจึงหมายถึงภาษาของชนชาติไทยผู้เป็นไทนั่นเอง</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>

16
www/package-lock.json generated
View File

@ -2684,6 +2684,14 @@
"hyphenate-style-name": "1.0.2" "hyphenate-style-name": "1.0.2"
} }
}, },
"css-line-break": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.0.1.tgz",
"integrity": "sha1-GfIGOjPpX7KDG4ZEbAuAwYivRQo=",
"requires": {
"base64-arraybuffer": "0.1.5"
}
},
"css-loader": { "css-loader": {
"version": "0.26.4", "version": "0.26.4",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.26.4.tgz", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.26.4.tgz",
@ -4895,11 +4903,11 @@
"integrity": "sha1-nSLgyjKsyVs/RbjVtPb73AWv/VU=" "integrity": "sha1-nSLgyjKsyVs/RbjVtPb73AWv/VU="
}, },
"html2canvas": { "html2canvas": {
"version": "1.0.0-alpha.3", "version": "1.0.0-alpha.9",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-alpha.3.tgz", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-alpha.9.tgz",
"integrity": "sha1-0QeAa+W1kuRr0iI7B3KTgaBIGzo=", "integrity": "sha1-/2A9gINa/rbCdm6GEEdooc7xyAE=",
"requires": { "requires": {
"punycode": "2.1.0" "css-line-break": "1.0.1"
} }
}, },
"htmlparser2": { "htmlparser2": {

View File

@ -18,7 +18,7 @@
"gatsby-source-filesystem": "^1.5.9", "gatsby-source-filesystem": "^1.5.9",
"gatsby-transformer-remark": "^1.7.23", "gatsby-transformer-remark": "^1.7.23",
"gzip-size": "^4.1.0", "gzip-size": "^4.1.0",
"html2canvas": "1.0.0-alpha.3", "html2canvas": "^1.0.0-alpha.9",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"typography": "^0.16.6", "typography": "^0.16.6",
"typography-theme-github": "^0.15.10" "typography-theme-github": "^0.15.10"
@ -28,7 +28,7 @@
"scripts": { "scripts": {
"copybuild": "mkdirp public/dist && cpy ../dist/*.js public/dist", "copybuild": "mkdirp public/dist && cpy ../dist/*.js public/dist",
"build": "npm run copybuild && gatsby build", "build": "npm run copybuild && gatsby build",
"develop": "gatsby develop", "start": "gatsby develop",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
} }
} }

View File

@ -66,7 +66,9 @@ export default class Example extends Component {
position: 'fixed', position: 'fixed',
zIndex: '1000', zIndex: '1000',
right: '-348.4px', right: '-348.4px',
bottom: '-327.2px' bottom: '-327.2px',
visibility: this.state.open ? 'visible' : 'hidden',
transition: 'visibility 0.3s cubic-bezier(0.42, 0, 0.58, 1)'
}} }}
> >
<div <div
@ -107,6 +109,7 @@ export default class Example extends Component {
cursor: 'pointer', cursor: 'pointer',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
borderRadius: '50%', borderRadius: '50%',
visibility: 'visible',
boxShadow: boxShadow:
'0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2)' '0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2)'
}} }}

View File

@ -4,14 +4,25 @@ export default () =>
<footer <footer
css={{ css={{
backgroundColor: '#558b2f', backgroundColor: '#558b2f',
color: 'rgba(255,255,255,0.8)', color: 'rgba(255,255,255, 0.8)',
fontWeight: 300, fontWeight: 300,
minHeight: '50px', minHeight: '50px',
lineHeight: '50px', lineHeight: '50px',
padding: '10px 0px' padding: '10px 0px'
}} }}
> >
<div css={{margin: '0 auto', width: '85%'}}> <div
css={{
margin: '0 auto',
fontSize: '10.5px',
textAlign: 'center',
'@media(min-width: 1000px)': {
textAlign: 'left',
width: '85%',
fontSize: '14.5px'
}
}}
>
Created by{' '} Created by{' '}
<a href="https://hertzen.com" css={{color: '#fff', fontWeight: 'bold'}}> <a href="https://hertzen.com" css={{color: '#fff', fontWeight: 'bold'}}>
Niklas von Hertzen Niklas von Hertzen

View File

@ -1,6 +1,7 @@
import React from 'react'; import React, {Component} from 'react';
import Link from 'gatsby-link'; import Link from 'gatsby-link';
import logo from '../images/logo.svg'; import logo from '../images/logo.svg';
import menu from '../images/ic_menu_black_24px.svg';
const lineLinkStyle = { const lineLinkStyle = {
lineHeight: '44px', lineHeight: '44px',
@ -16,15 +17,17 @@ const lineLinkStyle = {
}; };
const navStyle = { const navStyle = {
height: '100%', '@media(min-width: 1000px)': {
width: '300px', position: 'fixed',
position: 'fixed', top: 0,
top: 0, left: 0,
left: 0, width: '300px',
boxShadow:
'0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2)',
height: '100%'
},
fontSize: '13px', fontSize: '13px',
backgroundColor: '#fff', backgroundColor: '#fff'
boxShadow:
'0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2)'
}; };
const links = [ const links = [
@ -36,31 +39,70 @@ const links = [
{href: '/faq', text: 'FAQ'} {href: '/faq', text: 'FAQ'}
]; ];
export default () => export default class Navigation extends Component {
<div css={navStyle}> constructor(props) {
<Link to="/" css={{background: '#558b2f', display: 'block', padding: '24px 30px 24px'}}> super(props);
<img src={logo} css={{margin: 0}} /> this.state = {open: false};
</Link> }
<ul render() {
style={{ return (
listStyle: 'none', <div css={navStyle}>
margin: 0, <div
padding: 0 css={{
}} background: '#558b2f',
> alignItems: 'center',
{links.map(({href, text}, i) => padding: '24px 30px 24px 0px',
<li style={{padding: 0, margin: 0}} key={i}> display: 'flex',
<Link '@media(min-width: 1000px)': {
to={href} padding: '24px 30px 24px 30px'
css={lineLinkStyle} }
activeStyle={{ }}
backgroundColor: '#7cb342', >
color: '#fff' <img
src={menu}
onClick={() => this.setState(s => ({open: !s.open}))}
css={{
width: '50px',
cursor: 'pointer',
margin: '0 20px 0',
display: 'block',
'@media(min-width: 1000px)': {
display: 'none'
}
}} }}
> />
{text} <Link to="/">
<img src={logo} css={{margin: 0}} />
</Link> </Link>
</li> </div>
)}
</ul> <ul
</div>; css={{
display: this.state.open ? 'block' : 'none',
listStyle: 'none',
margin: 0,
padding: 0,
'@media(min-width: 1000px)': {
display: 'block'
}
}}
>
{links.map(({href, text}, i) =>
<li style={{padding: 0, margin: 0}} key={i}>
<Link
to={href}
css={lineLinkStyle}
activeStyle={{
backgroundColor: '#7cb342',
color: '#fff'
}}
>
{text}
</Link>
</li>
)}
</ul>
</div>
);
}
}

View File

@ -0,0 +1,4 @@
<svg fill="#ffffff" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>

After

Width:  |  Height:  |  Size: 210 B

View File

@ -36,9 +36,22 @@ table {
border: 1px solid #ddd; border: 1px solid #ddd;
} }
table tr:nth-child(odd) {
background-color: #f9f9f9;
}
th { th {
padding: 8px; padding: 8px;
border: 1px solid #ddd; border: 1px solid #ddd;
border-bottom-width: 2px;
}
td {
padding: 8px;
}
th:first-child, td:first-child {
padding-left: 8px;
} }
:not(pre) > code { :not(pre) > code {

View File

@ -55,8 +55,24 @@ export default ({data}) => {
Screenshots with JavaScript Screenshots with JavaScript
</h4> </h4>
<div css={{display: 'flex', justifyContent: 'center'}}> <div
<div> css={{
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
'@media(min-width: 1000px)': {
flexDirection: 'row'
}
}}
>
<div
css={{
display: 'none',
'@media(min-width: 1000px)': {
display: 'block'
}
}}
>
<h4>HTML</h4> <h4>HTML</h4>
<div <div
css={{marginRight: '5px'}} css={{marginRight: '5px'}}
@ -70,7 +86,14 @@ export default ({data}) => {
}} }}
/> />
</div> </div>
<div> <div
css={{
display: 'none',
'@media(min-width: 1000px)': {
display: 'block'
}
}}
>
<h4>JavaScript</h4> <h4>JavaScript</h4>
<div <div
css={{marginLeft: '5px'}} css={{marginLeft: '5px'}}
@ -101,14 +124,25 @@ export default ({data}) => {
Documentation Documentation
</Link> </Link>
</div> </div>
<div css={{display: 'flex'}}> <div
css={{
display: 'flex',
flexDirection: 'column',
'@media(min-width: 1000px)': {
flexDirection: 'row'
}
}}
>
<div <div
css={{ css={{
flex: 1, flex: 1,
backgroundColor: '#558b2f', backgroundColor: '#558b2f',
padding: '10px 20px', padding: '10px 20px',
borderRadius: '10px', borderRadius: '10px',
marginRight: '5px' marginBottom: '10px',
'@media(min-width: 1000px)': {
marginRight: '5px'
}
}} }}
> >
<Carbon /> <Carbon />
@ -116,12 +150,16 @@ export default ({data}) => {
<div <div
css={{ css={{
flex: 1, flex: 1,
marginLeft: '5px',
backgroundColor: '#558b2f', backgroundColor: '#558b2f',
padding: '10px 20px', padding: '10px 20px',
borderRadius: '10px', borderRadius: '10px',
textAlign: 'left', textAlign: 'left',
marginRight: '5px' marginBottom: '10px',
'@media(min-width: 1000px)': {
marginLeft: '5px',
marginRight: '5px'
}
}} }}
> >
<h6>Install NPM</h6> <h6>Install NPM</h6>
@ -143,11 +181,14 @@ export default ({data}) => {
<div <div
css={{ css={{
flex: 1, flex: 1,
marginLeft: '5px',
backgroundColor: '#558b2f', backgroundColor: '#558b2f',
padding: '10px 20px', padding: '10px 20px',
borderRadius: '10px', borderRadius: '10px',
textAlign: 'left' textAlign: 'left',
marginBottom: '10px',
'@media(min-width: 1000px)': {
marginLeft: '5px'
}
}} }}
> >
<h5>Connect</h5> <h5>Connect</h5>

View File

@ -19,7 +19,9 @@ export default ({data}) => {
<Navigation /> <Navigation />
<div <div
css={{ css={{
marginLeft: '300px', '@media(min-width: 1000px)': {
marginLeft: '300px'
},
display: 'flex', display: 'flex',
minHeight: '100vh', minHeight: '100vh',
flexDirection: 'column' flexDirection: 'column'
@ -37,10 +39,23 @@ export default ({data}) => {
<div css={{maxWidth: '960px'}}> <div css={{maxWidth: '960px'}}>
<div css={{width: '85%', margin: '0 auto', display: 'flex'}}> <div css={{width: '85%', margin: '0 auto', display: 'flex'}}>
<div css={{flex: 1}}> <div css={{flex: 1}}>
<h1 css={{padding: '20px 0'}}> <h1
css={{
padding: '20px 0',
fontSize: '2.8rem',
'@media(min-width: 1000px)': {fontSize: '4.2rem'}
}}
>
{post.frontmatter.title} {post.frontmatter.title}
</h1> </h1>
<h4 css={{fontWeight: 300, color: 'rgba(255, 255, 255, 0.6)'}}> <h4
css={{
fontWeight: 300,
color: 'rgba(255, 255, 255, 0.6)',
fontSize: '1.8rem',
'@media(min-width: 1000px)': {fontSize: '2.28rem'}
}}
>
{post.frontmatter.description} {post.frontmatter.description}
</h4> </h4>
</div> </div>