Compare commits

..

133 Commits

Author SHA1 Message Date
CI
28dc05c4a3 chore(release): 1.0.0-rc.2 2019-05-29 02:44:57 +00:00
409674fba6 fix: multi token overflow #1850 (#1851) 2019-05-26 14:08:56 -07:00
5f31b74177 feat: box-shadow rendering (#1848)
* feat: box-shadow rendering

* test: add box-shadow test
2019-05-25 21:53:50 -07:00
522a443055 Typescript conversion (#1828)
* initial typescript conversion

* test: update overflow+transform ref test

* fix: correctly render pseudo element content

* fix: testrunner build

* fix: karma test urls

* test: update underline tests with <u> elements

* test: update to es6-promise polyfill

* test: remove watch from server

* test: remove flow

* format: update prettier for typescript

* test: update eslint to use typescript parser

* test: update linear gradient reftest

* test: update test runner

* test: update testrunner promise polyfill

* fix: handle display: -webkit-flex correctly (fix #1817)

* fix: correctly render gradients with clip & repeat (fix #1773)

* fix: webkit-gradient function support

* fix: implement radial gradients

* fix: text-decoration rendering

* fix: missing scroll positions for elements

* ci: fix ios 11 tests

* fix: ie logging

* ci: improve device availability logging

* fix: lint errors

* ci: update to ios 12

* fix: check for console availability

* ci: fix build dependency

* test: update text reftests

* fix: window reference for unit tests

* feat: add hsl/hsla color support

* fix: render options

* fix: CSSKeyframesRule cssText Permission Denied on Internet Explorer 11 (#1830)

* fix: option lint

* fix: list type rendering

* test: fix platform import

* fix: ie css parsing for numbers

* ci: add minified build

* fix: form element rendering

* fix: iframe rendering

* fix: re-introduce experimental foreignobject renderer

* fix: text-shadow rendering

* feat: improve logging

* fix: unit test logging

* fix: cleanup resources

* test: update overflow scrolling to work with ie

* build: update build to include typings

* fix: do not parse select element children

* test: fix onclone test to work with older IEs

* test: reduce reftest canvas sizes

* test: remove dynamic setUp from list tests

* test: update linear-gradient tests

* build: remove old source files

* build: update docs dependencies

* build: fix typescript definition path

* ci: include test.js on docs website
2019-05-25 15:54:41 -07:00
20a797cbeb docs: fix README documentation 2019-04-12 23:37:01 -07:00
43058241b4 docs: remove dead donation link (fix #1802) 2019-04-12 23:28:10 -07:00
cdc4ca8296 test: include reftests previewer with docs website (#1799) 2019-04-12 23:17:23 -07:00
a7d881019b ci: refactor browser tests (#1804) 2019-04-10 21:09:08 -07:00
CI
b89458611b chore(release): 1.0.0-rc.1 2019-04-10 03:43:30 +00:00
4e4a231683 fix: safari data url taints (#1797) 2019-04-09 20:32:22 -07:00
a63cb3c0f1 ci: add ios simulator tests (#1794) 2019-04-08 21:56:22 -07:00
7775d3c0d6 docs: remove invalid async option from docs (fix #1769) (#1796) 2019-04-07 22:16:29 -07:00
397595afb5 fix: don't apply text shadows on elements (#1795)
* Fix for Issue-1638 (https://github.com/niklasvh/html2canvas/issues/1638)
Resolution: Clearing the Shadow Properties after they are consumed
2019-04-07 21:36:14 -07:00
7027900f49 fix: context scale for high resolution displays with foreignobjectrendering (#1782) 2019-04-07 16:49:29 -07:00
49f87fb680 test: fix RefTestRenderer.js inclusion with karma 2019-04-07 15:08:23 -07:00
238de790a9 docs: fix release date in changelog 2019-04-07 15:03:14 -07:00
CI
029235a652 chore(release): 1.0.0-rc.0 2019-04-07 22:01:14 +00:00
44f3d79f68 build: update webpack and babel (#1793) 2019-04-07 14:24:17 -07:00
7ebef72e92 ci: automate changelog generation (#1792) 2019-04-07 12:49:10 -07:00
2c018d1987 fix: wrap .sheet.cssRules access in try...catch. (#1693) 2019-04-07 12:17:43 -07:00
5cbe5db351 fix: prevent unhandled promise rejections for hidden frames (#1762) 2019-04-06 23:44:01 -07:00
3212184146 docs: improve canvas size limit documentation (#1576) 2019-04-06 23:36:29 -07:00
349bbf137a fix: enforce colorstop min 0 (#1743) 2019-04-06 23:24:07 -07:00
c45ef099fe ci: Improve CI pipeline (#1790) 2019-04-06 23:07:52 -07:00
24823d0491 Upgrade gatsbyjs to v2 2019-04-05 21:28:45 -07:00
41eb8ab22f Add support for ES and browser bundlers (#1534) 2018-07-06 23:03:14 +08:00
4e02a4c7a1 Remove unintended space (#1501) 2018-07-06 23:02:50 +08:00
ce45c1bbdd Update proxy version 2018-07-06 22:55:59 +08:00
078b388974 Update mocha 2018-07-06 22:55:11 +08:00
3ae7dd2ebb Flow ignore rollup config 2018-07-06 22:39:35 +08:00
dab77acde4 Update package-lock.json 2018-07-06 22:31:13 +08:00
83e7eaa795 Fix type import 2018-07-06 22:28:48 +08:00
cb7fbcf33e Begin implementing rollup 2018-07-06 22:28:30 +08:00
9a6e57aa00 v1.0.0-alpha.12 2018-04-05 20:49:54 +08:00
a3e25d71cb Fix white space appearing on element rendering (Fix #1438) 2018-04-05 20:41:16 +08:00
9da0f60551 Reset canvas transform on finish (Fix #1494) 2018-04-05 19:42:32 +08:00
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
c4ba6f795c Update CHANGELOG 2017-12-12 22:55:32 +08:00
0b9f34a5bf Fix formatting 2017-12-12 22:39:27 +08:00
757c32f6c4 Update unsupported features 2017-12-12 22:32:20 +08:00
09bab18b48 Fix flow error 2017-12-12 22:32:12 +08:00
7e53b195ea Add no-response bot config 2017-12-12 22:28:27 +08:00
ab966ff311 Fix formatting 2017-12-12 22:23:48 +08:00
7bb4a6f08f Fix compiled code using symbols 2017-12-12 22:18:15 +08:00
f50da9718f Fix NaN color stop in IE 2017-12-12 22:08:46 +08:00
3965a0fd40 Fix backgroundColor option documentation (Fix #1164) 2017-12-12 21:23:53 +08:00
77d258f1d8 Fix rendering with multiple fonts defined (Fix #796) 2017-12-12 21:08:19 +08:00
261702a693 Merge branch 'vnmc-feature/HTC-0009_RadialGradients' 2017-12-12 20:58:07 +08:00
cacb9f64e4 Radial gradient support 2017-12-12 20:57:48 +08:00
8ef3861a5c added support for radial gradients 2017-12-12 20:16:04 +08:00
0b74e69611 Update website demo 2017-12-12 19:43:59 +08:00
c272b2e122 Remove reftest results 2017-12-11 21:38:16 +08:00
2d132b85c6 Add gzipped package size to website (Fix #992) 2017-12-11 21:20:14 +08:00
50608e9cd4 Fix external SVG loading with proxies (#802) 2017-12-11 20:51:39 +08:00
d87fef11a4 Fix logging option (#1302) 2017-12-11 20:23:43 +08:00
250208dc99 Add support for rendering webgl canvas content (#646) 2017-12-11 20:17:20 +08:00
2237e8e230 Update wip website 2017-12-10 22:28:34 +08:00
d3c640088c Merge branch 'feature/HTC-0008_LinearGradients' of git://github.com/vnmc/html2canvas into vnmc-feature/HTC-0008_LinearGradients 2017-12-10 16:46:31 +08:00
8fd616aed2 Update wip website 2017-12-10 16:43:34 +08:00
d1e870de88 added support for gradient background size and fixed linear gradient angle when vendor prefix is used 2017-12-09 23:07:27 +01:00
9bc0fb0bd1 Fix __DEV__ value for minified build 2017-12-09 18:00:45 +08:00
d83bc0247a Disable foreignObjectRendering by default (#1295) 2017-12-09 17:51:28 +08:00
13e80cc635 Begin implementing new website 2017-12-09 17:47:25 +08:00
b239937e00 Refactor Font.js 2017-12-09 17:46:32 +08:00
e8a4d775e8 Update docs 2017-12-09 17:46:07 +08:00
a6a3c1bd0f Fix tests and refactor background calculations out from Renderer 2017-12-09 17:45:58 +08:00
850338a76a added support for background-origin: content-box, fixed background-origin related background sizes 2017-12-09 00:12:29 +01:00
63377d47a4 Begin adding documentation 2017-12-07 23:50:30 +08:00
1d1c74a74e Add Github issue templates 2017-12-07 17:30:13 +08:00
ef5c59e26d Fix scroll positions for CanvasRenderer (Fix #1259) 2017-12-07 16:36:09 +08:00
8b653f89bc Prevent generated iframe from getting re-rendered 2017-12-07 16:16:22 +08:00
9db8580b97 Fix data-html2canvas-ignore attribute (Fix #1253) 2017-12-07 16:12:39 +08:00
4a09264103 Update CHANGELOG 2017-12-07 15:47:53 +08:00
b8178e92b4 Add deprecation warning for onrendered (Fix #1290) 2017-12-07 15:47:42 +08:00
166cbba7c2 Fix decimal letter spacing values (#1293) 2017-12-07 15:20:24 +08:00
0c8d38d9c0 Fix underlines, relative to 'bottom' baseline 2017-12-06 23:49:11 +11:00
350 changed files with 41919 additions and 22547 deletions

View File

@ -1,4 +1,13 @@
{
"plugins": ["transform-object-rest-spread"],
"presets": ["es2015", "flow"]
"presets": [[
"@babel/preset-env",
{
"targets": {
"ie": "9"
}
}
], "@babel/preset-flow"],
"plugins": [
"add-module-exports"
]
}

View File

@ -11,7 +11,7 @@ insert_final_newline = true
indent_style = space
indent_size = 4
[{.travis.yml,package.json}]
[{azure-pipelines.yml,package.json, ci/*.yml}]
# The indent size used in the `package.json` file cannot be changed
# https://github.com/npm/npm/pull/3180#issuecomment-16336516
indent_size = 2

View File

@ -1,23 +1,27 @@
{
"parser": "babel-eslint",
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended",
],
"parserOptions": {
"project": "./tsconfig.json",
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"flowtype",
"@typescript-eslint",
"prettier"
],
"rules": {
"no-console": ["error", { "allow": ["warn", "error"] }],
"flowtype/boolean-style": [
2,
"boolean"
],
"flowtype/no-weak-types": 2,
"flowtype/delimiter-dangle": 2,
"prettier/prettier": ["error", {
"singleQuote": true,
"bracketSpacing": false,
"parser": "flow",
"tabWidth": 4,
"printWidth": 100
}]
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/class-name-casing": "off",
"prettier/prettier": "error"
}
}

View File

@ -1,6 +0,0 @@
[ignore]
[include]
[libs]
./flow-typed
[options]
[lints]

19
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,19 @@
Please make sure you are testing with the latest [release of html2canvas](https://github.com/niklasvh/html2canvas/releases).
Old versions are not supported and issues reported for them will be closed.
# Please follow the general troubleshooting steps first:
- [ ] You are using the latest [version](https://github.com/niklasvh/html2canvas/releases)
- [ ] You are testing using the non-minified version of html2canvas and checked any potential issues reported in the console
<!-- You can erase any parts of this template not applicable to your Issue. -->
### Bug reports:
Please replace this line with a brief summary of your issue **AND** if possible an example on [jsfiddle](https://jsfiddle.net/).
### Specifications:
* html2canvas version tested with:
* Browser & version:
* Operating system:

37
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,37 @@
A similar PR may already be submitted!
Please search among the [Pull request](https://github.com/niklasvh/html2canvas/pulls) before creating one.
Thanks for submitting a pull request! Please provide enough information so that others can review your pull request:
Before opening a pull request, please make sure all the tests pass locally by running `npm test`.
**Summary**
<!-- Summary of the PR -->
This PR fixes/implements the following **bugs/features**
* [ ] Bug 1
* [ ] Bug 2
* [ ] Feature 1
* [ ] Feature 2
* [ ] Breaking changes
<!-- You can skip this if you're fixing a typo or adding an app to the Showcase. -->
Explain the **motivation** for making this change. What existing problem does the pull request solve?
<!-- Example: When "Adding a function to do X", explain why it is necessary to have a way to do X. -->
**Test plan (required)**
Demonstrate how the issue/feature can be replicated. For most cases, simply adding an appropriate html/css template into the [reftests](https://github.com/niklasvh/html2canvas/tree/master/tests/reftests) should be sufficient. Please see other tests there for reference.
**Code formatting**
Please make sure that code adheres to the project code formatting. Running `npm run format` will automatically format your code correctly.
**Closing issues**
<!-- Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if such). -->
Fixes #

13
.github/no-response.yml vendored Normal file
View File

@ -0,0 +1,13 @@
# Configuration for probot-no-response - https://github.com/probot/no-response
# Number of days of inactivity before an Issue is closed for lack of response
daysUntilClose: 14
# Label requiring a response
responseRequiredLabel: Needs More Information
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
closeComment: >
This issue has been automatically closed because there has been no response
to our request for more information from the original author. With only the
information that is currently in the issue, we don't have enough information
to take action. Please reach out if you have or find the answers we need so
that we can investigate further.

2
.gitignore vendored
View File

@ -1,4 +1,5 @@
/dist
/tmp
/build
/nbproject/
image.jpg
@ -15,3 +16,4 @@ npm-debug.log
debug.log
tests/reftests.js
*.log
.rpt2_cache

View File

@ -1,14 +1,22 @@
build/
docs/
examples/
scripts/
src/
tests/
www/
tmp/
.github/
*.iml
.babelrc
.idea/
.editorconfig
.npmignore
.jshintrc
.eslintrc
.travis.yml
azure-pipelines.yml
karma.js
karma.config.js
karma.conf.js
rollup.config.js
webpack.config.js
.rpt2_cache

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"trailingComma": "none",
"tabWidth": 4,
"bracketSpacing": false,
"singleQuote": true,
"printWidth": 120
}

View File

@ -1,46 +0,0 @@
language: node_js
node_js:
- '7'
env:
global:
- secure: eW41gIqOizwO4pTgWnAAbW75AP7F+CK9qfSed/fSh4sJ9HWMIY1YRIaY8gjr+6jV/f7XVHcXuym6ZxgINYSkVKbF1JKxBJNLOXtSgNbVHSic58pYFvUjwxIBI9aPig9uux1+DbnpWqXFDTcACJSevQZE0xwmjdrSkDLgB0G34v8=
- secure: Y2Av+Gd3z9uQEB36GwdOOuGka0hx0/HeitASEo59z934O8RxnmN9eNTXS7dDT3XtKtwxIyLTOEpS7qlRdWahH28hr/dS4xJj6ao58C+1xMcDs6NAPGmDxUlcJWpcGEsnjmXjQCc3fBioSTdpIBrK/gdvgpNh77UKG74Sk7Z+YGk=
addons:
chrome: stable
firefox: latest
dist: trusty
sudo: false
before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/2b007d4f86de89588804
on_success: always
on_failure: always
on_start: false
script:
- npm run build
- npm test
deploy:
- provider: npm
email: niklasvh@gmail.com
api_key:
secure: G/Szpr8q4/D6hp+H/Z9yyluUXtHAwf7LLa1Y07X59/Enlj1h7V5fQ7AW4/iAVM3XbIsrCPWR3dJU9g/ZxpxFg4OovIHVpS2Jr/mahtPYWdHR3pWuSmMW8QD+Twnq2VAFwSgg5Oumq3QxhX3YbCOnZox6+6Uviqk8FO7Z5B0RwW4=
skip_cleanup: true
on:
tags: true
branch: master
repo: niklasvh/html2canvas
- provider: releases
api_key:
secure: "PowO/Jat660k3gHcjgI6DlJz15RM7pLUu11UPsLCtYJ8ZwodppE6Keg0DfVkSFSIZttZor+UssDwP/WOEqfZNLqmXbcj3Gec4xolohet/GOe0KJKKuF/HgggbcxumopxMX6sMVePlMBpkLpHh7tgEAEHBWTlzC1c1a7Xa48fZ7k="
file:
- "dist/html2canvas.js"
- "dist/html2canvas.min.js"
skip_cleanup: true
on:
tags: true
branch: master
repo: niklasvh/html2canvas

View File

@ -1,12 +1,136 @@
### Changelog ###
# Change Log
#### v1.0.0-alpha1 - TBD ####
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
# [1.0.0-rc.2](https://github.com/niklasvh/html2canvas/compare/v1.0.0-rc.1...v1.0.0-rc.2) (2019-05-29)
### ci
* refactor browser tests (#1804) ([a7d881019bfe1fd6404c341ca1c6fa69e0274ef5](https://github.com/niklasvh/html2canvas/commit/a7d881019bfe1fd6404c341ca1c6fa69e0274ef5)), closes [#1804](https://github.com/niklasvh/html2canvas/issues/1804)
### docs
* fix README documentation ([20a797cbeb21baca4ce5b9a0642a5959cdf94a51](https://github.com/niklasvh/html2canvas/commit/20a797cbeb21baca4ce5b9a0642a5959cdf94a51))
* remove dead donation link (fix #1802) ([43058241b420a5dabe94b0a4e4f6534d16a75ec0](https://github.com/niklasvh/html2canvas/commit/43058241b420a5dabe94b0a4e4f6534d16a75ec0)), closes [#1802](https://github.com/niklasvh/html2canvas/issues/1802)
### fix
* multi token overflow #1850 (#1851) ([409674fba6f8038eb174b9c89360ef8b342971e9](https://github.com/niklasvh/html2canvas/commit/409674fba6f8038eb174b9c89360ef8b342971e9)), closes [#1850](https://github.com/niklasvh/html2canvas/issues/1850) [#1851](https://github.com/niklasvh/html2canvas/issues/1851)
### test
* include reftests previewer with docs website (#1799) ([cdc4ca8296570bf842e937c6fb7cc32a1ce2bc09](https://github.com/niklasvh/html2canvas/commit/cdc4ca8296570bf842e937c6fb7cc32a1ce2bc09)), closes [#1799](https://github.com/niklasvh/html2canvas/issues/1799)
# [1.0.0-rc.1](https://github.com/niklasvh/html2canvas/compare/v1.0.0-rc.0...v1.0.0-rc.1) (2019-04-10)
### ci
* add ios simulator tests (#1794) ([a63cb3c0f132b1af915d9ef55a4c174f6e5502ce](https://github.com/niklasvh/html2canvas/commit/a63cb3c0f132b1af915d9ef55a4c174f6e5502ce)), closes [#1794](https://github.com/niklasvh/html2canvas/issues/1794)
### docs
* fix release date in changelog ([238de790a9f223becbc8726633c0f2a2dabf2cb7](https://github.com/niklasvh/html2canvas/commit/238de790a9f223becbc8726633c0f2a2dabf2cb7))
* remove invalid `async` option from docs (fix #1769) (#1796) ([7775d3c0d6f3efca00611bedd5fc9200689a9f7a](https://github.com/niklasvh/html2canvas/commit/7775d3c0d6f3efca00611bedd5fc9200689a9f7a)), closes [#1769](https://github.com/niklasvh/html2canvas/issues/1769) [#1796](https://github.com/niklasvh/html2canvas/issues/1796)
### fix
* context scale for high resolution displays with foreignobjectrendering (#1782) ([7027900f4993dcd00745a4db045ed1c0e3255f8a](https://github.com/niklasvh/html2canvas/commit/7027900f4993dcd00745a4db045ed1c0e3255f8a)), closes [#1782](https://github.com/niklasvh/html2canvas/issues/1782)
* don't apply text shadows on elements (#1795) ([397595afb59ee50f0d128abb5945b5b9ddc6650d](https://github.com/niklasvh/html2canvas/commit/397595afb59ee50f0d128abb5945b5b9ddc6650d)), closes [#1795](https://github.com/niklasvh/html2canvas/issues/1795)
* safari data url taints (#1797) ([4e4a231683904dfdc1f82472ece5a160a158dbb8](https://github.com/niklasvh/html2canvas/commit/4e4a231683904dfdc1f82472ece5a160a158dbb8)), closes [#1797](https://github.com/niklasvh/html2canvas/issues/1797)
### test
* fix RefTestRenderer.js inclusion with karma ([49f87fb680dbfe1898b3aeb60f2f5c3a93bfbe6d](https://github.com/niklasvh/html2canvas/commit/49f87fb680dbfe1898b3aeb60f2f5c3a93bfbe6d))
# [1.0.0-rc.0](https://github.com/niklasvh/html2canvas/compare/v1.0.0-alpha.12...v1.0.0-rc.0) (2019-04-07)
### build
* update webpack and babel (#1793) ([44f3d79f68836624c2673a86f9ad47c17ef843c3](https://github.com/niklasvh/html2canvas/commit/44f3d79f68836624c2673a86f9ad47c17ef843c3)), closes [#1793](https://github.com/niklasvh/html2canvas/issues/1793)
### ci
* automate changelog generation (#1792) ([7ebef72e927eaafd34a1792ece431d2a73109230](https://github.com/niklasvh/html2canvas/commit/7ebef72e927eaafd34a1792ece431d2a73109230)), closes [#1792](https://github.com/niklasvh/html2canvas/issues/1792)
* Improve CI pipeline (#1790) ([c45ef099fe8f7142e174f4fce39448a370a987d5](https://github.com/niklasvh/html2canvas/commit/c45ef099fe8f7142e174f4fce39448a370a987d5)), closes [#1790](https://github.com/niklasvh/html2canvas/issues/1790)
### docs
* improve canvas size limit documentation (#1576) ([3212184146b33c3564c2f416e1bfda911737c38b](https://github.com/niklasvh/html2canvas/commit/3212184146b33c3564c2f416e1bfda911737c38b)), closes [#1576](https://github.com/niklasvh/html2canvas/issues/1576)
### fix
* enforce colorstop min 0 (#1743) ([349bbf137abd83464e074db3948fc79a541c2ef3](https://github.com/niklasvh/html2canvas/commit/349bbf137abd83464e074db3948fc79a541c2ef3)), closes [#1743](https://github.com/niklasvh/html2canvas/issues/1743)
* prevent unhandled promise rejections for hidden frames (#1762) ([5cbe5db35155e3a9790a30de09feb17843053b7a](https://github.com/niklasvh/html2canvas/commit/5cbe5db35155e3a9790a30de09feb17843053b7a)), closes [#1762](https://github.com/niklasvh/html2canvas/issues/1762)
* wrap .sheet.cssRules access in try...catch. (#1693) ([2c018d19875ced30caafdc40f84ca531de6e6f91](https://github.com/niklasvh/html2canvas/commit/2c018d19875ced30caafdc40f84ca531de6e6f91)), closes [#1693](https://github.com/niklasvh/html2canvas/issues/1693)
# [1.0.0-alpha.12](https://github.com/niklasvh/html2canvas/compare/v1.0.0-alpha.12...v1.0.0-alpha.13) (2018-04-05)
* Fix white space appearing on element rendering (Fix #1438)
* Reset canvas transform on finish (Fix #1494)
# 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)
* Add support for radial-gradients
* Fix logging option (#1302)
* Add support for rendering webgl canvas content (#646)
* Fix external SVG loading with proxies (#802)
# v1.0.0-alpha.3 - 9.12.2017
* Disable `foreignObjectRendering` by default (#1295)
* Fix background-size when using background-origin and background-size: cover/contain (#1299)
* Added support for background-origin: content-box (#1299)
# v1.0.0-alpha.2 - 7.12.2017
* Fix scroll positions for CanvasRenderer (#1259)
* Fix `data-html2canvas-ignore` attribute (#1253)
* Fix decimal `letter-spacing` values (#1293)
# v1.0.0-alpha.1 - 5.12.2017
* Complete rewrite of library
##### Breaking Changes #####
* Remove deprecated onrendered callback, calling `html2canvas` returns a `Promise<HTMLCanvasElement>`
* Removed option `type`, same results can be achieved by assigning `x`, `y`, `scrollX`, `scrollY`, `width` and `height` properties.
##### New featues / fixes #####
## New featues / fixes
* Add support for scaling canvas (defaults to device pixel ratio)
* Add support for multiple text-shadows
* Add support for multiple text-decorations
@ -16,18 +140,18 @@
* Correctly support all angle types for linear-gradients
* Add support for multiple values for background-repeat, background-position and background-size
#### v0.5.0-beta4 - 23.1.2016 ####
# v0.5.0-beta4 - 23.1.2016
* Fix logger requiring access to window object
* Derequire browserify build
* Fix rendering of specific elements when window is scrolled and `type` isn't set to `view`
#### v0.5.0-beta3 - 6.12.2015 ####
# v0.5.0-beta3 - 6.12.2015
* Handle color names in linear gradients
#### v0.5.0-beta2 - 20.10.2015 ####
# v0.5.0-beta2 - 20.10.2015
* Remove Promise polyfill (use native or provide it yourself)
#### v0.5.0-beta1 - 19.10.2015 ####
# v0.5.0-beta1 - 19.10.2015
* Fix bug with unmatched color stops in gradients
* Fix scrolling issues with iOS
* Correctly handle named colors in gradients
@ -35,11 +159,11 @@
* Fix transparent colors breaking gradients
* Preserve scrolling positions on render
#### v0.5.0-alpha2 - 3.2.2015 ####
# v0.5.0-alpha2 - 3.2.2015
* Switch to using browserify for building
* Fix (#517) Chrome stretches background images with 'auto' or single attributes
#### v0.5.0-alpha - 19.1.2015####
# v0.5.0-alpha - 19.1.2015
* Complete rewrite of library
* Switched interface to return Promise
* Uses hidden iframe window to perform rendering, allowing async rendering and doesn't force the viewport to be scrolled to the top anymore.
@ -50,14 +174,14 @@
* Changed format for proxy requests, permitting binary responses with CORS headers as well
* Fixed many layering issues (see z-index tests)
#### v0.4.1 - 7.9.2013 ####
# v0.4.1 - 7.9.2013
* Added support for bower
* Improved z-index ordering
* Basic implementation for CSS transformations
* Fixed inline text in top element
* Basic implementation for text-shadow
#### v0.4.0 - 30.1.2013 ####
# v0.4.0 - 30.1.2013
* Added rendering tests with <a href="https://github.com/niklasvh/webdriver.js">webdriver</a>
* Switched to using grunt for building
* Removed support for IE<9, including any FlashCanvas bits
@ -67,7 +191,7 @@
* Support for placeholder rendering
* Reformatted all tests to small units to test specific features
#### v0.3.4 - 26.6.2012 ####
# v0.3.4 - 26.6.2012
* Removed (last?) jQuery dependencies (<a href="https://github.com/niklasvh/html2canvas/commit/343b86705fe163766fcf735eb0217130e4bd5b17">niklasvh</a>)
* SVG-powered rendering (<a href="https://github.com/niklasvh/html2canvas/commit/67d3e0d0f59a5a654caf71a2e3be6494ff146c75">niklasvh</a>)
@ -75,7 +199,7 @@
* Split renderers to their own objects (<a href="https://github.com/niklasvh/html2canvas/commit/94f2f799a457cd29a21cc56ef8c06f1697866739">niklasvh</a>)
* Simplified API, cleaned up code (<a href="https://github.com/niklasvh/html2canvas/commit/c7d526c9eaa6a4abf4754d205fe1dee360c7660e">niklasvh</a>)
#### v0.3.3 - 2.3.2012 ####
# v0.3.3 - 2.3.2012
* SVG taint fix, and additional taint testing options for rendering (<a href="https://github.com/niklasvh/html2canvas/commit/2dc8b9385e656696cb019d615bdfa1d98b17d5d4">niklasvh</a>)
* Added support for CORS images and option to create canvas as tainted (<a href="https://github.com/niklasvh/html2canvas/commit/3ad49efa0032cde25c6ed32a39e35d1505d3b2ef">niklasvh</a>)
@ -83,7 +207,7 @@
* Added integrated support for Flashcanvas (<a href="https://github.com/niklasvh/html2canvas/commit/e9257191519f67d74fd5e364d8dee3c0963ba5fc">niklasvh</a>)
* Fixed a variety of legacy IE bugs (<a href="https://github.com/niklasvh/html2canvas/commit/b65357c55d0701017bafcd357bc654b54d458f8f">niklasvh</a>)
#### v0.3.2 - 20.2.2012 ####
# v0.3.2 - 20.2.2012
* Added changelog!
* Added bookmarklet (<a href="https://github.com/niklasvh/html2canvas/commit/b320dd306e1a2d32a3bc5a71b6ebf6d8c060cde5">cobexer</a>)

View File

@ -1,9 +1,12 @@
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)
[![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://dev.azure.com/niklasvh/html2canvas/_apis/build/status/niklasvh.html2canvas?branchName=master)](https://dev.azure.com/niklasvh/html2canvas/_build/latest?definitionId=1&branchName=master)
[![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 ####
@ -36,8 +39,6 @@ The html2canvas library utilizes `Promise`s and expects them to be available in
support [older browsers](http://caniuse.com/#search=promise) that do not natively support `Promise`s, please include a polyfill such as
[es6-promise](https://github.com/jakearchibald/es6-promise) before including `html2canvas`.
**Note!** These instructions are for using the current dev version of 0.5, for the latest release version (0.4.1), checkout the [old readme](https://github.com/niklasvh/html2canvas/blob/v0.4/readme.md).
To render an `element` with html2canvas, simply call:
` html2canvas(element[, options]);`
@ -63,23 +64,9 @@ Build browser bundle
$ npm run build
### Running tests ###
The library has two sets of tests. The first set is a number of qunit tests that check that different values parsed by browsers are correctly converted in html2canvas. To run these tests with grunt you'll need [phantomjs](http://phantomjs.org/).
The other set of tests run Firefox, Chrome and Internet Explorer with [webdriver](https://github.com/niklasvh/webdriver.js). The selenium standalone server (runs on Java) is required for these tests and can be downloaded from [here](http://code.google.com/p/selenium/downloads/list). They capture an actual screenshot from the test pages and compare the image to the screenshot created by html2canvas and calculate the percentage differences. These tests generally aren't expected to provide 100% matches, but while committing changes, these should generally not go decrease from the baseline values.
Start by downloading the dependencies:
$ npm install
Run tests:
$ npm test
### Examples ###
For more information and examples, please visit the [homepage](https://html2canvas.hertzen.com) or try the [test console](http://html2canvas.hertzen.com/screenshots.html).
For more information and examples, please visit the [homepage](https://html2canvas.hertzen.com) or try the [test console](https://html2canvas.hertzen.com/tests/).
### Contributing ###

170
azure-pipelines.yml Normal file
View File

@ -0,0 +1,170 @@
trigger:
- master
jobs:
- job: Build
displayName: Build
pool:
vmImage: 'Ubuntu-16.04'
steps:
- task: NodeTool@0
inputs:
versionSpec: '10.x'
displayName: 'Install Node.js'
- task: Npm@0
inputs:
command: install
- script: npm run build
displayName: Build
- script: |
npm pack
mv html2canvas-*.tgz html2canvas.tgz
tar --list --verbose --file=html2canvas.tgz
displayName: Pack
name: pack
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: html2canvas.tgz
artifactName: npm
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: 'dist'
artifactName: dist
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: 'build'
artifactName: build
- job: Test
displayName: Tests
pool:
vmImage: 'Ubuntu-16.04'
steps:
- task: NodeTool@0
inputs:
versionSpec: '10.x'
displayName: 'Install Node.js'
- task: Npm@0
inputs:
command: install
- script: npm run build
displayName: Build
- script: npm run lint
displayName: Lint
- script: npm run unittest
displayName: Unit tests
- template: ci/browser-tests.yml
parameters:
name: Browser_Tests_Linux_Firefox_Stable
displayName: Linux Firefox Stable
vmImage: 'ubuntu-16.04'
targetBrowser: Firefox_Stable
xvfb: true
- template: ci/browser-tests.yml
parameters:
name: Browser_Tests_Linux_Chrome_Stable
displayName: Linux Chrome Stable
vmImage: 'ubuntu-16.04'
targetBrowser: Chrome_Stable
xvfb: true
- template: ci/browser-tests.yml
parameters:
name: Browser_Tests_OSX_Safari_IOS_9
displayName: iOS Simulator Safari 9
vmImage: 'macOS-10.13'
targetBrowser: Safari_IOS_9
- template: ci/browser-tests.yml
parameters:
name: Browser_Tests_OSX_Safari_IOS_10
displayName: iOS Simulator Safari 10
vmImage: 'macOS-10.13'
targetBrowser: Safari_IOS_10
- template: ci/browser-tests.yml
parameters:
name: Browser_Tests_OSX_Safari_IOS_12
displayName: iOS Simulator Safari 12
vmImage: 'macOS-10.13'
targetBrowser: Safari_IOS_12
- template: ci/browser-tests.yml
parameters:
name: Browser_Tests_OSX_Safari_Stable
displayName: OSX Safari Stable
vmImage: 'macOS-10.13'
targetBrowser: Safari_Stable
- template: ci/browser-tests.yml
parameters:
name: Browser_Tests_Windows_IE9
displayName: Windows Internet Explorer 9 (Emulated)
vmImage: 'vs2017-win2016'
targetBrowser: IE_9
- template: ci/browser-tests.yml
parameters:
name: Browser_Tests_Windows_IE10
displayName: Windows Internet Explorer 10 (Emulated)
vmImage: 'vs2017-win2016'
targetBrowser: IE_10
- template: ci/browser-tests.yml
parameters:
name: Browser_Tests_Windows_IE11
displayName: Windows Internet Explorer 11
vmImage: 'vs2017-win2016'
targetBrowser: IE_11
- job: Build_docs
displayName: Build docs
pool:
vmImage: 'Ubuntu-16.04'
dependsOn:
- Browser_Tests_Linux_Firefox_Stable
- Browser_Tests_Linux_Chrome_Stable
- Browser_Tests_OSX_Safari_IOS_9
- Browser_Tests_OSX_Safari_IOS_10
- Browser_Tests_OSX_Safari_IOS_12
- Browser_Tests_OSX_Safari_Stable
- Browser_Tests_Windows_IE9
- Browser_Tests_Windows_IE10
- Browser_Tests_Windows_IE11
steps:
- task: NodeTool@0
inputs:
versionSpec: '10.x'
displayName: 'Install Node.js'
- task: Npm@0
inputs:
command: install
- task: DownloadBuildArtifacts@0
displayName: 'Download test results'
inputs:
artifactName: ReftestResults
downloadPath: $(System.DefaultWorkingDirectory)
- task: DownloadBuildArtifacts@0
displayName: 'Download dist'
inputs:
artifactName: dist
downloadPath: $(System.DefaultWorkingDirectory)
- script: cp -R tests/reftests www/static/tests/reftests && cp -R tests/assets www/static/tests/assets && cp tests/test.js www/static/tests/test.js && cp -R ReftestResults ./www/static/results
displayName: Copy reftests to docs website
- script: cp -R dist ./www/static/dist
displayName: Copy dist to docs website
- script: npm run build:reftest-result-list www/static/results/metadata www/src/results.json
displayName: Create reftest result index
- script: npm run build:reftest-preview
displayName: Create reftest previewer
- script: rm -rf www/static/results/metadata
displayName: Clean metadata folder
- script: npm run build && cd www && npm install && npm run build && cd ..
displayName: Build docs
- task: PublishBuildArtifacts@1
displayName: Upload docs website artifact
inputs:
PathtoPublish: 'www/public'
artifactName: docs

View File

@ -1,9 +0,0 @@
{
"name": "html2canvas",
"description": "Screenshots with JavaScript",
"main": "dist/html2canvas.js",
"ignore": [
"tests",
".travis.yml"
]
}

52
ci/browser-tests.yml Normal file
View File

@ -0,0 +1,52 @@
parameters:
name: ''
vmImage: ''
targetBrowser: ''
xvfb: false
jobs:
- job: ${{ parameters.name }}
displayName: ${{ parameters.displayName }}
pool:
vmImage: ${{ parameters.vmImage }}
variables:
TARGET_BROWSER: ${{ parameters.targetBrowser }}
dependsOn: Build
condition: succeeded()
steps:
- task: NodeTool@0
inputs:
versionSpec: '10.x'
displayName: 'Install Node.js'
- task: Npm@0
inputs:
command: install
- task: DownloadBuildArtifacts@0
displayName: 'Download library'
inputs:
artifactName: dist
downloadPath: $(System.DefaultWorkingDirectory)
- task: DownloadBuildArtifacts@0
displayName: 'Download testrunner'
inputs:
artifactName: build
downloadPath: $(System.DefaultWorkingDirectory)
- ${{ if not(eq(parameters.xvfb, 'true')) }}:
- script: npm run karma
displayName: 'Run browser tests'
- ${{ if eq(parameters.xvfb, 'true') }}:
- script: Xvfb :99 &
displayName: 'Start Xvfb'
- script: DISPLAY=:99 npm run karma
displayName: 'Run browser tests'
- task: PublishTestResults@2
condition: succeededOrFailed()
inputs:
testRunner: JUnit
testResultsFiles: 'tmp/junit/*.xml'
- task: PublishBuildArtifacts@1
displayName: Upload Screenshots
condition: succeededOrFailed()
inputs:
PathtoPublish: 'tmp/reftests'
artifactName: ReftestResults

11
configs/base.json Normal file
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"noImplicitAny": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"resolveJsonModule": true
}
}

10
configs/preview.json Normal file
View File

@ -0,0 +1,10 @@
{
"extends": "./base",
"include": [
"../www/src/preview.ts"
],
"exclude": [
"node_modules"
]
}

10
configs/scripts.json Normal file
View File

@ -0,0 +1,10 @@
{
"extends": "./base",
"include": [
"scripts/**/*.ts"
],
"exclude": [
"node_modules"
]
}

35
docs/configuration.md Normal file
View File

@ -0,0 +1,35 @@
---
title: "Options"
description: "Explore the different configuration options available for html2canvas"
previousUrl: "/getting-started"
previousTitle: "Getting Started"
nextUrl: "/features"
nextTitle: "Features"
---
These are all of the available configuration options.
| Name | Default | Description |
| ------------- | :------: | ----------- |
| allowTaint | `false` | Whether to allow cross-origin images to taint the canvas
| backgroundColor | `#ffffff` | Canvas background color, if none is specified in DOM. Set `null` for transparent
| 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
| 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
| 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.
| 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.
| useCORS | `false` | Whether to attempt to load images from a server using CORS
| width | `Element` width | The width of the `canvas`
| height | `Element` height | The height of the `canvas`
| x | `Element` x-offset | Crop canvas x-coordinate
| y | `Element` y-offset| Crop canvas y-coordinate
| scrollX | `Element` scrollX | The x-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
| 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.

42
docs/documentation.md Normal file
View File

@ -0,0 +1,42 @@
---
title: "About"
description: "Learn about html2canvas, how it works and some of its limitations"
nextUrl: "/getting-started"
nextTitle: "Getting Started"
---
Before you get started with the script, there are a few things that are good to know regarding the
script and some of its limitations.
## Introduction
The script allows you to take "screenshots" of webpages or parts of it, directly on the users browser.
The screenshot is based on the DOM and as such may not be 100% accurate to the real representation
as it does not make an actual screenshot, but builds the screenshot based on the information
available on the page.
## How it works
The script traverses through the DOM of the page it is loaded on. It gathers information on all the elements
there, which it then uses to build a representation of the page. In other words, it does not actually take a
screenshot of the page, but builds a representation of it based on the properties it reads from the DOM.
As a result, it is only able to render correctly properties that it understands, meaning there are many
CSS properties which do not work. For a full list of supported CSS properties, check out the
[supported features](/features/) page.
## Limitations
All the images that the script uses need to reside under the [same origin](http://en.wikipedia.org/wiki/Same_origin_policy)
for it to be able to read them without the assistance of a [proxy](/proxy/). Similarly, if you have other `canvas`
elements on the page, which have been tainted with cross-origin content, they will become dirty and no longer readable by html2canvas.
The script doesn't render plugin content such as Flash or Java applets.
## Browser compatibility
The library should work fine on the following browsers (with `Promise` polyfill):
- Firefox 3.5+
- Google Chrome
- Opera 12+
- IE9+
- Edge
- Safari 6+

48
docs/faq.md Normal file
View File

@ -0,0 +1,48 @@
---
title: "FAQ"
description: "Explore Frequently Asked Questions regarding html2canvas"
---
## Why aren't my images rendered?
html2canvas does not get around content policy restrictions set by your browser. Drawing images that reside outside of
the [origin](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) of the current page [taint the
canvas](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image#What_is_a_tainted_canvas) that they are drawn upon. If the canvas gets tainted, it cannot be read anymore. As such, html2canvas implements
methods to check whether an image would taint the canvas before applying it. If you have set the `allowTaint`
[option](/configuration) to `false`, it will not draw the image.
If you wish to load images that reside outside of your pages origin, you can use a [proxy](/proxy) to load the images.
## Why is the produced canvas empty or cuts off half way through?
Make sure that `canvas` element doesn't hit [browser limitations](https://stackoverflow.com/questions/6081483/maximum-size-of-a-canvas-element) for the `canvas` size or use the window configuration options to set a custom window size based on the `canvas` element:
```
await html2canvas(element, {
windowWidth: element.scrollWidth,
windowHeight: element.scrollHeight
});
```
The window limitations vary by browser, operating system and system hardware.
### Chrome
> Maximum height/width: 32,767 pixels
> Maximum area: 268,435,456 pixels (e.g., 16,384 x 16,384)
### Firefox
> Maximum height/width: 32,767 pixels
> Maximum area: 472,907,776 pixels (e.g., 22,528 x 20,992)
### Internet Explorer
> Maximum height/width: 8,192 pixels
> Maximum area: N/A
### iOS
> The maximum size for a canvas element is 3 megapixels for devices with less than 256 MB RAM and 5 megapixels for devices with greater or equal than 256 MB RAM
## Why doesn't CSS property X render correctly or only partially?
As each CSS property needs to be manually coded to render correctly, html2canvas will *never* have full CSS support.
The library tries to support the most [commonly used CSS properties](/features) to the extent that it can. If some CSS property
is missing or incomplete and you feel that it should be part of the library, create test cases for it and a new issue for it.
## How do I get html2canvas to work in a browser extension?
You shouldn't use html2canvas in a browser extension. Most browsers have native support for capturing screenshots from
tabs within extensions. Relevant information for [Chrome](https://developer.chrome.com/extensions/tabs#method-captureVisibleTab) and
[Firefox](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D#drawWindow()).

85
docs/features.md Normal file
View File

@ -0,0 +1,85 @@
---
title: "Features"
description: "Discover the different features supported by html2canvas"
---
Below is a list of all the supported CSS properties and values.
- background
- background-clip (**Does not support `text`**)
- background-color
- background-image
- url()
- linear-gradient()
- radial-gradient()
- background-origin
- background-position
- background-size
- border
- border-color
- border-radius
- border-style (**Only supports `solid`**)
- border-width
- bottom
- box-sizing
- content
- color
- display
- flex
- float
- font
- font-family
- font-size
- font-style
- font-variant
- font-weight
- height
- left
- letter-spacing
- line-break
- list-style
- list-style-image
- list-style-position
- list-style-type
- margin
- max-height
- max-width
- min-height
- min-width
- opacity
- overflow
- overflow-wrap
- padding
- position
- right
- text-align
- text-decoration
- text-decoration-color
- text-decoration-line
- text-decoration-style (**Only supports `solid`**)
- text-shadow
- text-transform
- top
- transform (**Limited support**)
- visibility
- white-space
- width
- word-break
- word-spacing
- word-wrap
- z-index
## Unsupported CSS properties
These CSS properties are **NOT** currently supported
- [background-blend-mode](https://github.com/niklasvh/html2canvas/issues/966)
- [border-image](https://github.com/niklasvh/html2canvas/issues/1287)
- [box-decoration-break](https://github.com/niklasvh/html2canvas/issues/552)
- [box-shadow](https://github.com/niklasvh/html2canvas/pull/1086)
- [filter](https://github.com/niklasvh/html2canvas/issues/493)
- [font-variant-ligatures](https://github.com/niklasvh/html2canvas/pull/1085)
- [mix-blend-mode](https://github.com/niklasvh/html2canvas/issues/580)
- [object-fit](https://github.com/niklasvh/html2canvas/issues/1064)
- [repeating-linear-gradient()](https://github.com/niklasvh/html2canvas/issues/1162)
- [writing-mode](https://github.com/niklasvh/html2canvas/issues/1258)
- [zoom](https://github.com/niklasvh/html2canvas/issues/732)

30
docs/getting-started.md Normal file
View File

@ -0,0 +1,30 @@
---
title: "Getting Started"
description: "Learn how to start using html2canvas"
previousUrl: "/documentation"
previousTitle: "About"
nextUrl: "/configuration"
nextTitle: "Configuration"
---
## Installing
You can install `html2canvas` through npm or [download a built release](https://github.com/niklasvh/html2canvas/releases).
### npm
npm install html2canvas
```javascript
import html2canvas from 'html2canvas';
```
## Usage
To render an `element` with html2canvas with some (optional) [options](/configuration/), simply call `html2canvas(element, options);`
```javascript
html2canvas(document.body).then(function(canvas) {
document.body.appendChild(canvas);
});
```

12
docs/proxy.md Normal file
View File

@ -0,0 +1,12 @@
---
title: "Proxy"
description: "Browse different proxies available for supporting CORS content"
---
html2canvas does not get around content policy restrictions set by your browser. Drawing images that reside outside of
the origin of the current page taint the canvas that they are drawn upon. If the canvas gets tainted,
it cannot be read anymore. If you wish to load images that reside outside of your pages origin, you can use a proxy to load the images.
## Available proxies
- [node.js](https://github.com/niklasvh/html2canvas-proxy-nodejs)

View File

@ -1,11 +0,0 @@
declare var __DEV__: boolean;
declare var __VERSION__: string;
declare class SVGSVGElement extends Element {
className: string;
style: CSSStyleDeclaration;
getPresentationAttribute(name: string): any;
}
declare class HTMLBodyElement extends HTMLElement {}

View File

@ -1,89 +1,164 @@
// Karma configuration
// Generated on Sat Aug 05 2017 23:42:26 GMT+0800 (Malay Peninsula Standard Time)
const path = require('path');
const simctl = require('node-simctl');
const iosSimulator = require('appium-ios-simulator');
const port = 9876;
const log = require('karma/lib/logger').create('launcher:MobileSafari');
module.exports = function(config) {
const slLaunchers = (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) ? {} : {
sl_beta_chrome: {
base: 'SauceLabs',
browserName: 'chrome',
platform: 'Windows 10',
version: 'beta'
const launchers = {
Safari_IOS_9: {
base: 'MobileSafari',
name: 'iPhone 5s',
sdk: '9.0'
},
sl_ie9: {
Safari_IOS_10: {
base: 'MobileSafari',
name: 'iPhone 5s',
sdk: '10.0'
},
Safari_IOS_12: {
base: 'MobileSafari',
name: 'iPhone 5s',
sdk: '12.1'
},
SauceLabs_IE9: {
base: 'SauceLabs',
browserName: 'internet explorer',
version: '9.0',
platform: 'Windows 7'
},
sl_ie10: {
SauceLabs_IE10: {
base: 'SauceLabs',
browserName: 'internet explorer',
version: '10.0',
platform: 'Windows 7'
},
sl_ie11: {
SauceLabs_IE11: {
base: 'SauceLabs',
browserName: 'internet explorer',
version: '11.0',
platform: 'Windows 7'
},
sl_edge_15: {
SauceLabs_Edge18: {
base: 'SauceLabs',
browserName: 'MicrosoftEdge',
version: '15.15063',
version: '18.17763',
platform: 'Windows 10'
},
sl_edge_14: {
base: 'SauceLabs',
browserName: 'MicrosoftEdge',
version: '14.14393',
platform: 'Windows 10'
},
sl_safari: {
base: 'SauceLabs',
browserName: 'safari',
version: '10.1',
platform: 'macOS 10.12'
},
'sl_android_4.4': {
SauceLabs_Android4: {
base: 'SauceLabs',
browserName: 'Browser',
platform: 'Android',
version: '4.4',
device: 'Android Emulator',
},
'sl_ios_10.3_safari': {
SauceLabs_iOS10_3: {
base: 'SauceLabs',
browserName: 'Safari',
platform: 'iOS',
version: '10.3',
device: 'iPhone 7 Plus Simulator'
},
'sl_ios_9.3_safari': {
SauceLabs_iOS9_3: {
base: 'SauceLabs',
browserName: 'Safari',
platform: 'iOS',
version: '9.3',
device: 'iPhone 6 Plus Simulator'
},
'sl_ios_8.4_safari': {
base: 'SauceLabs',
browserName: 'Safari',
platform: 'iOS',
version: '8.4',
device: 'iPhone 5s Simulator'
IE_9: {
base: 'IE',
'x-ua-compatible': 'IE=EmulateIE9',
flags: ['-extoff']
},
IE_10: {
base: 'IE',
'x-ua-compatible': 'IE=EmulateIE10',
flags: ['-extoff']
},
IE_11: {
base: 'IE',
flags: ['-extoff']
},
Safari_Stable: {
base: 'Safari'
},
Chrome_Stable: {
base: 'Chrome'
},
Firefox_Stable: {
base: 'Firefox'
}
};
const customLaunchers = Object.assign({}, slLaunchers, {
const ciLauncher = launchers[process.env.TARGET_BROWSER];
const customLaunchers = ciLauncher ? {target_browser: ciLauncher} : {
stable_chrome: {
base: 'Chrome'
},
stable_firefox: {
base: 'Firefox'
}
});
};
const injectTypedArrayPolyfills = function(files) {
files.unshift({
pattern: path.resolve(__dirname, './node_modules/js-polyfills/typedarray.js'),
included: true,
served: true,
watched: false
});
};
injectTypedArrayPolyfills.$inject = ['config.files'];
const MobileSafari = function(baseBrowserDecorator, args) {
if(process.platform !== "darwin"){
log.error("This launcher only works in MacOS.");
this._process.kill();
return;
}
baseBrowserDecorator(this);
this.on('start', url => {
simctl.getDevices().then(devices => {
const d = devices[args.sdk].find(d => {
return d.name === args.name;
});
if (!d) {
log.error(`No device found for sdk ${args.sdk} with name ${args.name}`);
log.info(`Available devices:`, devices);
this._process.kill();
return;
}
return iosSimulator.getSimulator(d.udid).then(device => {
return simctl.bootDevice(d.udid).then(() => device);
}).then(device => {
return device.waitForBoot(60 * 5 * 1000).then(() => {
return device.openUrl(url);
});
});
}).catch(e => {
console.log('err,', e);
});
});
};
MobileSafari.prototype = {
name: 'MobileSafari',
DEFAULT_CMD: {
darwin: '/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/Contents/MacOS/Simulator',
},
ENV_CMD: null,
};
MobileSafari.$inject = ['baseBrowserDecorator', 'args'];
config.set({
@ -93,17 +168,25 @@ module.exports = function(config) {
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha'],
frameworks: ['mocha', 'inline-mocha-fix'],
// list of files / patterns to load in the browser
files: [
'build/testrunner.js',
{ pattern: './tests/**/*', 'watched': true, 'included': false, 'served': true},
{ pattern: './dist/**/*', 'watched': true, 'included': false, 'served': true},
{ pattern: './node_modules/**/*', 'watched': true, 'included': false, 'served': true}
{ pattern: './node_modules/**/*', 'watched': true, 'included': false, 'served': true},
],
plugins: [
'karma-*',
{
'framework:inline-mocha-fix': ['factory', injectTypedArrayPolyfills]
},
{
'launcher:MobileSafari': ['type', MobileSafari]
}
],
// list of files to exclude
exclude: [
@ -119,7 +202,11 @@ module.exports = function(config) {
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress', 'saucelabs'],
reporters: ['dots', 'junit'],
junitReporter: {
outputDir: 'tmp/junit/'
},
// web server port
port,

View File

@ -14,6 +14,13 @@ const bodyParser = require('body-parser');
const cors = require('cors');
const filenamifyUrl = require('filenamify-url');
const mkdirp = require('mkdirp');
const screenshotFolder = './tmp/reftests';
const metadataFolder = './tmp/reftests/metadata';
mkdirp.sync(path.resolve(__dirname, screenshotFolder));
mkdirp.sync(path.resolve(__dirname, metadataFolder));
const CORS_PORT = 8081;
const corsApp = express();
corsApp.use('/proxy', proxy());
@ -59,9 +66,10 @@ const writeScreenshot = (buffer, body) => {
const filename = `${filenamifyUrl(
body.test.replace(/^\/tests\/reftests\//, '').replace(/\.html$/, ''),
{replacement: '-'}
)}!${body.platform.name}-${body.platform.version}.png`;
)}!${[process.env.TARGET_BROWSER, body.platform.name, body.platform.version].join('-')}`;
fs.writeFileSync(path.resolve(__dirname, './tests/results/', filename), buffer);
fs.writeFileSync(path.resolve(__dirname, screenshotFolder, `${filename}.png`), buffer);
return filename;
};
app.post('/screenshot', (req, res) => {
@ -70,33 +78,16 @@ app.post('/screenshot', (req, res) => {
}
const buffer = new Buffer(req.body.screenshot.substring(prefix.length), 'base64');
writeScreenshot(buffer, req.body);
return res.sendStatus(200);
});
const chunks = {};
app.post('/screenshot/chunk', (req, res) => {
if (!req.body || !req.body.screenshot) {
return res.sendStatus(400);
}
const key = `${req.body.platform.name}-${req.body.platform.version}-${req.body.test
.replace(/^\/tests\/reftests\//, '')
.replace(/\.html$/, '')}`;
if (!Array.isArray(chunks[key])) {
chunks[key] = Array.from(Array(req.body.totalCount));
}
chunks[key][req.body.part] = req.body.screenshot;
if (chunks[key].every(s => typeof s === 'string')) {
const str = chunks[key].reduce((acc, s) => acc + s, '');
const buffer = new Buffer(str.substring(prefix.length), 'base64');
delete chunks[key];
writeScreenshot(buffer, req.body);
}
const filename = writeScreenshot(buffer, req.body);
fs.writeFileSync(path.resolve(__dirname, metadataFolder, `${filename}.json`), JSON.stringify({
windowWidth: req.body.windowWidth,
windowHeight: req.body.windowHeight,
platform: req.body.platform,
devicePixelRatio: req.body.devicePixelRatio,
test: req.body.test,
id: process.env.TARGET_BROWSER,
screenshot: filename
}));
return res.sendStatus(200);
});

14836
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,15 +2,18 @@
"title": "html2canvas",
"name": "html2canvas",
"description": "Screenshots with JavaScript",
"main": "dist/npm/index.js",
"version": "1.0.0-alpha.1",
"main": "dist/html2canvas.js",
"module": "dist/html2canvas.esm.js",
"typings": "dist/types/index.d.ts",
"browser": "dist/html2canvas.js",
"version": "1.0.0-rc.2",
"author": {
"name": "Niklas von Hertzen",
"email": "niklasvh@gmail.com",
"url": "https://hertzen.com"
},
"engines": {
"node": ">=4.0.0"
"node": ">=8.0.0"
},
"repository": {
"type": "git",
@ -20,63 +23,91 @@
"url": "https://github.com/niklasvh/html2canvas/issues"
},
"devDependencies": {
"babel-cli": "6.24.1",
"babel-core": "6.25.0",
"babel-eslint": "7.2.3",
"babel-loader": "7.1.1",
"babel-plugin-dev-expression": "0.2.1",
"babel-plugin-transform-es2015-modules-commonjs": "6.26.0",
"babel-plugin-transform-object-rest-spread": "6.23.0",
"babel-preset-es2015": "6.24.1",
"babel-preset-flow": "6.23.0",
"base64-arraybuffer": "0.1.5",
"body-parser": "1.17.2",
"@babel/cli": "^7.4.3",
"@babel/core": "^7.4.3",
"@babel/preset-env": "^7.4.3",
"@babel/preset-flow": "^7.0.0",
"@types/chai": "^4.1.7",
"@types/glob": "^7.1.1",
"@types/mocha": "^5.2.6",
"@types/node": "^11.13.2",
"@types/platform": "^1.3.2",
"@types/promise-polyfill": "^6.0.3",
"@typescript-eslint/eslint-plugin": "^1.7.0",
"@typescript-eslint/parser": "^1.7.0",
"appium-ios-simulator": "^3.10.0",
"babel-eslint": "^10.0.1",
"babel-loader": "^8.0.5",
"babel-plugin-add-module-exports": "^1.0.0",
"babel-plugin-dev-expression": "^0.2.1",
"base64-arraybuffer": "0.2.0",
"body-parser": "^1.18.3",
"chai": "4.1.1",
"chromeless": "1.2.0",
"chromeless": "^1.5.2",
"cors": "2.8.4",
"eslint": "4.2.0",
"eslint-plugin-flowtype": "2.35.0",
"eslint-plugin-prettier": "2.1.2",
"express": "4.15.4",
"es6-promise": "^4.2.6",
"eslint": "^5.16.0",
"eslint-config-prettier": "^4.2.0",
"eslint-plugin-prettier": "3.0.1",
"express": "^4.16.4",
"filenamify-url": "1.0.0",
"flow-bin": "0.56.0",
"glob": "7.1.2",
"html2canvas-proxy": "1.0.0",
"jquery": "3.2.1",
"karma": "1.7.0",
"karma-chrome-launcher": "2.2.0",
"karma-edge-launcher": "0.4.1",
"karma-firefox-launcher": "1.0.1",
"karma-ie-launcher": "1.0.0",
"karma-mocha": "1.3.0",
"karma-sauce-launcher": "1.1.0",
"mocha": "3.5.0",
"glob": "7.1.3",
"html2canvas-proxy": "1.0.1",
"jquery": "^3.4.0",
"js-polyfills": "^0.1.42",
"karma": "^4.0.1",
"karma-chrome-launcher": "^2.2.0",
"karma-edge-launcher": "^0.4.2",
"karma-firefox-launcher": "^1.1.0",
"karma-ie-launcher": "^1.0.0",
"karma-junit-reporter": "^1.2.0",
"karma-mocha": "^1.3.0",
"karma-safari-launcher": "^1.0.0",
"karma-sauce-launcher": "^2.0.2",
"mocha": "^6.1.4",
"node-simctl": "^5.0.0",
"platform": "1.3.4",
"prettier": "1.5.3",
"promise-polyfill": "6.0.2",
"prettier": "1.17.0",
"replace-in-file": "^3.0.0",
"rimraf": "2.6.1",
"serve-index": "1.9.0",
"rollup": "^1.10.1",
"rollup-plugin-commonjs": "^9.3.4",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-resolve": "^4.2.3",
"rollup-plugin-sourcemaps": "^0.4.2",
"rollup-plugin-typescript2": "^0.21.0",
"serve-index": "^1.9.1",
"slash": "1.0.0",
"standard-version": "^5.0.2",
"ts-loader": "^5.3.3",
"ts-node": "^8.0.3",
"typescript": "^3.4.3",
"uglify-js": "^3.5.11",
"uglifyjs-webpack-plugin": "^1.1.2",
"webpack": "3.4.1"
"webpack": "^4.29.6",
"webpack-cli": "^3.3.0"
},
"scripts": {
"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:browser": "webpack",
"format": "prettier --single-quote --no-bracket-spacing --tab-width 4 --print-width 100 --write \"{src,tests,scripts}/**/*.js\"",
"flow": "flow",
"lint": "eslint src/**",
"test": "npm run flow && npm run lint && npm run test:node && npm run karma",
"test:node": "mocha tests/node/*.js",
"prebuild": "rimraf dist/ && rimraf build/ && mkdirp dist && mkdirp build",
"build": "tsc --module commonjs && rollup -c rollup.config.ts && npm run build:create-reftest-list && npm run build:testrunner && npm run build:minify",
"build:testrunner": "rollup -c tests/rollup.config.ts",
"build:minify": "uglifyjs --compress --comments /^!/ -o dist/html2canvas.min.js --mangle -- dist/html2canvas.js",
"build:reftest-result-list": "ts-node scripts/create-reftest-result-list.ts",
"build:create-reftest-list": "ts-node scripts/create-reftest-list.ts tests/reftests/ignore.txt build/reftests.ts",
"build:reftest-preview": "webpack --config www/webpack.config.js",
"release": "standard-version",
"format": "prettier --write \"{src,www/src,tests,scripts}/**/*.ts\"",
"lint": "eslint src/**/*.ts",
"test": "npm run lint && npm run unittest && npm run karma",
"unittest": "mocha --require ts-node/register src/**/__tests__/*.ts",
"karma": "node karma",
"watch": "webpack --progress --colors --watch",
"watch": "rollup -c rollup.config.ts -w",
"watch:unittest": "mocha --require ts-node/register --watch-extensions ts -w src/**/__tests__/*.ts",
"start": "node tests/server"
},
"homepage": "https://html2canvas.hertzen.com",
"license": "MIT",
"dependencies": {
"punycode": "2.1.0"
"css-line-break": "1.1.1"
}
}

42
rollup.config.ts Normal file
View File

@ -0,0 +1,42 @@
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import sourceMaps from 'rollup-plugin-sourcemaps';
import typescript from 'rollup-plugin-typescript2';
import json from 'rollup-plugin-json';
const pkg = require('./package.json');
const banner = `/*!
* ${pkg.title} ${pkg.version} <${pkg.homepage}>
* Copyright (c) ${(new Date()).getFullYear()} ${pkg.author.name} <${pkg.author.url}>
* Released under ${pkg.license} License
*/`;
export default {
input: `src/index.ts`,
output: [
{ file: pkg.main, name: pkg.name, format: 'umd', banner, sourcemap: true },
{ file: pkg.module, format: 'esm', banner, sourcemap: true },
],
external: [],
watch: {
include: 'src/**',
},
plugins: [
// Allow node_modules resolution, so you can use 'external' to control
// which external modules to include in the bundle
// https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve(),
// Allow json resolution
json(),
// Compile TypeScript files
typescript({ useTsconfigDeclarationDir: true }),
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
commonjs({
include: 'node_modules/**'
}),
// Resolve source maps to the original source
sourceMaps(),
],
}

View File

@ -1,53 +0,0 @@
'use strict';
const path = require('path');
const glob = require('glob');
const fs = require('fs');
const slash = require('slash');
const parseRefTest = require('./parse-reftest');
const outputPath = 'tests/reftests.js';
const ignoredTests = fs
.readFileSync(path.resolve(__dirname, `../tests/reftests/ignore.txt`))
.toString()
.split('\n')
.filter(l => l.length)
.reduce((acc, l) => {
const m = l.match(/^(\[(.+)\])?(.+)$/i);
acc[m[3]] = m[2] ? m[2].split(',') : [];
return acc;
}, {});
glob(
'../tests/reftests/**/*.html',
{
cwd: __dirname,
root: path.resolve(__dirname, '../../')
},
(err, files) => {
if (err) {
console.error(err);
process.exit(1);
}
const testList = files.reduce((acc, filename) => {
const refTestFilename = path.resolve(__dirname, filename.replace(/\.html$/, '.txt'));
const name = `/${slash(path.relative('../', filename))}`;
if (!Array.isArray(ignoredTests[name]) || ignoredTests[name].length) {
acc[name] = fs.existsSync(refTestFilename)
? parseRefTest(fs.readFileSync(refTestFilename).toString())
: null;
} else {
console.log(`IGNORED: ${name}`);
}
return acc;
}, {});
fs.writeFileSync(
path.resolve(__dirname, `../${outputPath}`),
`module.exports = ${JSON.stringify({testList, ignoredTests}, null, 4)};`
);
console.log(`${outputPath} updated`);
}
);

View File

@ -0,0 +1,47 @@
'use strict';
import {readFileSync, writeFileSync} from 'fs';
import {resolve, relative} from 'path';
import {sync} from 'glob';
const slash = require('slash');
if (process.argv.length <= 2) {
console.log('No ignore.txt file provided');
process.exit(1);
}
if (process.argv.length <= 3) {
console.log('No output file provided');
process.exit(1);
}
const path = resolve(__dirname, '../', process.argv[2]);
const outputPath = resolve(__dirname, '../', process.argv[3]);
const ignoredTests = readFileSync(path)
.toString()
.split(/\r\n|\r|\n/)
.filter(l => l.length)
.reduce((acc: {[key: string]: string[]}, l) => {
const m = l.match(/^(\[(.+)\])?(.+)$/i);
if (m) {
acc[m[3]] = m[2] ? m[2].split(',') : [];
}
return acc;
}, {});
const files: string[] = sync('../tests/reftests/**/*.html', {
cwd: __dirname,
root: resolve(__dirname, '../../')
});
const testList = files.map((filename: string) => `/${slash(relative('../', filename))}`);
writeFileSync(
outputPath,
[
`export const testList: string[] = ${JSON.stringify(testList, null, 4)};`,
`export const ignoredTests: {[key: string]: string[]} = ${JSON.stringify(ignoredTests, null, 4)};`
].join('\n')
);
console.log(`${outputPath} updated`);

View File

@ -0,0 +1,42 @@
import {readdirSync, readFileSync, writeFileSync} from 'fs';
import {resolve} from 'path';
if (process.argv.length <= 2) {
console.log('No metadata path provided');
process.exit(1);
}
if (process.argv.length <= 3) {
console.log('No output file given');
process.exit(1);
}
const path = resolve(__dirname, '../', process.argv[2]);
const files = readdirSync(path);
interface RefTestMetadata {}
interface RefTestSingleMetadata extends RefTestMetadata {
test: string;
}
interface RefTestResults {
[key: string]: Array<RefTestMetadata>;
}
const result: RefTestResults = files.reduce((result: RefTestResults, file) => {
const json: RefTestSingleMetadata = JSON.parse(readFileSync(resolve(__dirname, path, file)).toString());
if (!result[json.test]) {
result[json.test] = [];
}
result[json.test].push(json);
delete json.test;
return result;
}, {});
const output = resolve(__dirname, '../', process.argv[3]);
writeFileSync(output, JSON.stringify(result));
console.log(`Wrote file ${output}`);

View File

@ -1,24 +0,0 @@
/* @flow */
'use strict';
const ANGLE = /([+-]?\d*\.?\d+)(deg|grad|rad|turn)/i;
export const parseAngle = (angle: string): number | null => {
const match = angle.match(ANGLE);
if (match) {
const value = parseFloat(match[1]);
switch (match[2].toLowerCase()) {
case 'deg':
return Math.PI * value / 180;
case 'grad':
return Math.PI / 200 * value;
case 'rad':
return value;
case 'turn':
return Math.PI * 2 * value;
}
}
return null;
};

View File

@ -1,378 +0,0 @@
/* @flow */
'use strict';
import type {Border, BorderSide} from './parsing/border';
import type {BorderRadius} from './parsing/borderRadius';
import type {Padding} from './parsing/padding';
import type {Path} from './drawing/Path';
import Vector from './drawing/Vector';
import BezierCurve from './drawing/BezierCurve';
const TOP = 0;
const RIGHT = 1;
const BOTTOM = 2;
const LEFT = 3;
const H = 0;
const V = 1;
export type BoundCurves = {
topLeftOuter: BezierCurve | Vector,
topLeftInner: BezierCurve | Vector,
topRightOuter: BezierCurve | Vector,
topRightInner: BezierCurve | Vector,
bottomRightOuter: BezierCurve | Vector,
bottomRightInner: BezierCurve | Vector,
bottomLeftOuter: BezierCurve | Vector,
bottomLeftInner: BezierCurve | Vector
};
export class Bounds {
top: number;
left: number;
width: number;
height: number;
constructor(x: number, y: number, w: number, h: number) {
this.left = x;
this.top = y;
this.width = w;
this.height = h;
}
static fromClientRect(clientRect: ClientRect, scrollX: number, scrollY: number): Bounds {
return new Bounds(
clientRect.left + scrollX,
clientRect.top + scrollY,
clientRect.width,
clientRect.height
);
}
}
export const parseBounds = (
node: HTMLElement | SVGSVGElement,
scrollX: number,
scrollY: number
): Bounds => {
return Bounds.fromClientRect(node.getBoundingClientRect(), scrollX, scrollY);
};
export const calculatePaddingBox = (bounds: Bounds, borders: Array<Border>): Bounds => {
return new Bounds(
bounds.left + borders[LEFT].borderWidth,
bounds.top + borders[TOP].borderWidth,
bounds.width - (borders[RIGHT].borderWidth + borders[LEFT].borderWidth),
bounds.height - (borders[TOP].borderWidth + borders[BOTTOM].borderWidth)
);
};
export const calculateContentBox = (
bounds: Bounds,
padding: Padding,
borders: Array<Border>
): Bounds => {
// TODO support percentage paddings
const paddingTop = padding[TOP].value;
const paddingRight = padding[RIGHT].value;
const paddingBottom = padding[BOTTOM].value;
const paddingLeft = padding[LEFT].value;
return new Bounds(
bounds.left + paddingLeft + borders[LEFT].borderWidth,
bounds.top + paddingTop + borders[TOP].borderWidth,
bounds.width -
(borders[RIGHT].borderWidth + borders[LEFT].borderWidth + paddingLeft + paddingRight),
bounds.height -
(borders[TOP].borderWidth + borders[BOTTOM].borderWidth + paddingTop + paddingBottom)
);
};
export const parseDocumentSize = (document: Document): Bounds => {
const body = document.body;
const documentElement = document.documentElement;
if (!body || !documentElement) {
throw new Error(__DEV__ ? `Unable to get document size` : '');
}
const width = Math.max(
Math.max(body.scrollWidth, documentElement.scrollWidth),
Math.max(body.offsetWidth, documentElement.offsetWidth),
Math.max(body.clientWidth, documentElement.clientWidth)
);
const height = Math.max(
Math.max(body.scrollHeight, documentElement.scrollHeight),
Math.max(body.offsetHeight, documentElement.offsetHeight),
Math.max(body.clientHeight, documentElement.clientHeight)
);
return new Bounds(0, 0, width, height);
};
export const parsePathForBorder = (curves: BoundCurves, borderSide: BorderSide): Path => {
switch (borderSide) {
case TOP:
return createPathFromCurves(
curves.topLeftOuter,
curves.topLeftInner,
curves.topRightOuter,
curves.topRightInner
);
case RIGHT:
return createPathFromCurves(
curves.topRightOuter,
curves.topRightInner,
curves.bottomRightOuter,
curves.bottomRightInner
);
case BOTTOM:
return createPathFromCurves(
curves.bottomRightOuter,
curves.bottomRightInner,
curves.bottomLeftOuter,
curves.bottomLeftInner
);
case LEFT:
default:
return createPathFromCurves(
curves.bottomLeftOuter,
curves.bottomLeftInner,
curves.topLeftOuter,
curves.topLeftInner
);
}
};
const createPathFromCurves = (
outer1: BezierCurve | Vector,
inner1: BezierCurve | Vector,
outer2: BezierCurve | Vector,
inner2: BezierCurve | Vector
): Path => {
const path = [];
if (outer1 instanceof BezierCurve) {
path.push(outer1.subdivide(0.5, false));
} else {
path.push(outer1);
}
if (outer2 instanceof BezierCurve) {
path.push(outer2.subdivide(0.5, true));
} else {
path.push(outer2);
}
if (inner2 instanceof BezierCurve) {
path.push(inner2.subdivide(0.5, true).reverse());
} else {
path.push(inner2);
}
if (inner1 instanceof BezierCurve) {
path.push(inner1.subdivide(0.5, false).reverse());
} else {
path.push(inner1);
}
return path;
};
export const calculateBorderBoxPath = (curves: BoundCurves): Path => {
return [
curves.topLeftOuter,
curves.topRightOuter,
curves.bottomRightOuter,
curves.bottomLeftOuter
];
};
export const calculatePaddingBoxPath = (curves: BoundCurves): Path => {
return [
curves.topLeftInner,
curves.topRightInner,
curves.bottomRightInner,
curves.bottomLeftInner
];
};
export const parseBoundCurves = (
bounds: Bounds,
borders: Array<Border>,
borderRadius: Array<BorderRadius>
): BoundCurves => {
const HALF_WIDTH = bounds.width / 2;
const HALF_HEIGHT = bounds.height / 2;
const tlh =
borderRadius[CORNER.TOP_LEFT][H].getAbsoluteValue(bounds.width) < HALF_WIDTH
? borderRadius[CORNER.TOP_LEFT][H].getAbsoluteValue(bounds.width)
: HALF_WIDTH;
const tlv =
borderRadius[CORNER.TOP_LEFT][V].getAbsoluteValue(bounds.height) < HALF_HEIGHT
? borderRadius[CORNER.TOP_LEFT][V].getAbsoluteValue(bounds.height)
: HALF_HEIGHT;
const trh =
borderRadius[CORNER.TOP_RIGHT][H].getAbsoluteValue(bounds.width) < HALF_WIDTH
? borderRadius[CORNER.TOP_RIGHT][H].getAbsoluteValue(bounds.width)
: HALF_WIDTH;
const trv =
borderRadius[CORNER.TOP_RIGHT][V].getAbsoluteValue(bounds.height) < HALF_HEIGHT
? borderRadius[CORNER.TOP_RIGHT][V].getAbsoluteValue(bounds.height)
: HALF_HEIGHT;
const brh =
borderRadius[CORNER.BOTTOM_RIGHT][H].getAbsoluteValue(bounds.width) < HALF_WIDTH
? borderRadius[CORNER.BOTTOM_RIGHT][H].getAbsoluteValue(bounds.width)
: HALF_WIDTH;
const brv =
borderRadius[CORNER.BOTTOM_RIGHT][V].getAbsoluteValue(bounds.height) < HALF_HEIGHT
? borderRadius[CORNER.BOTTOM_RIGHT][V].getAbsoluteValue(bounds.height)
: 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 rightHeight = bounds.height - brv;
const bottomWidth = bounds.width - brh;
const leftHeight = bounds.height - blv;
return {
topLeftOuter:
tlh > 0 || tlv > 0
? getCurvePoints(bounds.left, bounds.top, tlh, tlv, CORNER.TOP_LEFT)
: new Vector(bounds.left, bounds.top),
topLeftInner:
tlh > 0 || tlv > 0
? getCurvePoints(
bounds.left + borders[LEFT].borderWidth,
bounds.top + borders[TOP].borderWidth,
Math.max(0, tlh - borders[LEFT].borderWidth),
Math.max(0, tlv - borders[TOP].borderWidth),
CORNER.TOP_LEFT
)
: new Vector(
bounds.left + borders[LEFT].borderWidth,
bounds.top + borders[TOP].borderWidth
),
topRightOuter:
trh > 0 || trv > 0
? getCurvePoints(bounds.left + topWidth, bounds.top, trh, trv, CORNER.TOP_RIGHT)
: new Vector(bounds.left + bounds.width, bounds.top),
topRightInner:
trh > 0 || trv > 0
? getCurvePoints(
bounds.left + Math.min(topWidth, bounds.width + borders[LEFT].borderWidth),
bounds.top + borders[TOP].borderWidth,
topWidth > bounds.width + borders[LEFT].borderWidth
? 0
: trh - borders[LEFT].borderWidth,
trv - borders[TOP].borderWidth,
CORNER.TOP_RIGHT
)
: new Vector(
bounds.left + bounds.width - borders[RIGHT].borderWidth,
bounds.top + borders[TOP].borderWidth
),
bottomRightOuter:
brh > 0 || brv > 0
? getCurvePoints(
bounds.left + bottomWidth,
bounds.top + rightHeight,
brh,
brv,
CORNER.BOTTOM_RIGHT
)
: new Vector(bounds.left + bounds.width, bounds.top + bounds.height),
bottomRightInner:
brh > 0 || brv > 0
? getCurvePoints(
bounds.left + Math.min(bottomWidth, bounds.width - borders[LEFT].borderWidth),
bounds.top + Math.min(rightHeight, bounds.height + borders[TOP].borderWidth),
Math.max(0, brh - borders[RIGHT].borderWidth),
brv - borders[BOTTOM].borderWidth,
CORNER.BOTTOM_RIGHT
)
: new Vector(
bounds.left + bounds.width - borders[RIGHT].borderWidth,
bounds.top + bounds.height - borders[BOTTOM].borderWidth
),
bottomLeftOuter:
blh > 0 || blv > 0
? getCurvePoints(bounds.left, bounds.top + leftHeight, blh, blv, CORNER.BOTTOM_LEFT)
: new Vector(bounds.left, bounds.top + bounds.height),
bottomLeftInner:
blh > 0 || blv > 0
? getCurvePoints(
bounds.left + borders[LEFT].borderWidth,
bounds.top + leftHeight,
Math.max(0, blh - borders[LEFT].borderWidth),
blv - borders[BOTTOM].borderWidth,
CORNER.BOTTOM_LEFT
)
: new Vector(
bounds.left + borders[LEFT].borderWidth,
bounds.top + bounds.height - borders[BOTTOM].borderWidth
)
};
};
const CORNER = {
TOP_LEFT: 0,
TOP_RIGHT: 1,
BOTTOM_RIGHT: 2,
BOTTOM_LEFT: 3
};
type Corner = $Values<typeof CORNER>;
const getCurvePoints = (
x: number,
y: number,
r1: number,
r2: number,
position: Corner
): BezierCurve => {
const kappa = 4 * ((Math.sqrt(2) - 1) / 3);
const ox = r1 * kappa; // control point offset horizontal
const oy = r2 * kappa; // control point offset vertical
const xm = x + r1; // x-middle
const ym = y + r2; // y-middle
switch (position) {
case CORNER.TOP_LEFT:
return new BezierCurve(
new Vector(x, ym),
new Vector(x, ym - oy),
new Vector(xm - ox, y),
new Vector(xm, y)
);
case CORNER.TOP_RIGHT:
return new BezierCurve(
new Vector(x, y),
new Vector(x + ox, y),
new Vector(xm, ym - oy),
new Vector(xm, ym)
);
case CORNER.BOTTOM_RIGHT:
return new BezierCurve(
new Vector(xm, y),
new Vector(xm, y + oy),
new Vector(x + ox, ym),
new Vector(x, ym)
);
case CORNER.BOTTOM_LEFT:
default:
return new BezierCurve(
new Vector(xm, ym),
new Vector(xm - ox, ym),
new Vector(x, y + oy),
new Vector(x, y)
);
}
};

View File

@ -1,584 +0,0 @@
/* @flow */
'use strict';
import type {Bounds} from './Bounds';
import type {Options} from './index';
import type Logger from './Logger';
import {parseBounds} from './Bounds';
import {Proxy} from './Proxy';
import ResourceLoader from './ResourceLoader';
import {copyCSSStyles} from './Util';
import {parseBackgroundImage} from './parsing/background';
import CanvasRenderer from './renderer/CanvasRenderer';
export class DocumentCloner {
scrolledElements: Array<[HTMLElement, number, number]>;
referenceElement: HTMLElement;
clonedReferenceElement: HTMLElement;
documentElement: HTMLElement;
resourceLoader: ResourceLoader;
logger: Logger;
options: Options;
inlineImages: boolean;
copyStyles: boolean;
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>;
constructor(
element: HTMLElement,
options: Options,
logger: Logger,
copyInline: boolean,
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>
) {
this.referenceElement = element;
this.scrolledElements = [];
this.copyStyles = copyInline;
this.inlineImages = copyInline;
this.logger = logger;
this.options = options;
this.renderer = renderer;
this.resourceLoader = new ResourceLoader(options, logger, window);
// $FlowFixMe
this.documentElement = this.cloneNode(element.ownerDocument.documentElement);
}
inlineAllImages(node: ?HTMLElement) {
if (this.inlineImages && node) {
const style = node.style;
Promise.all(
parseBackgroundImage(style.backgroundImage).map(backgroundImage => {
if (backgroundImage.method === 'url') {
return this.resourceLoader
.inlineImage(backgroundImage.args[0])
.then(
img =>
img && typeof img.src === 'string'
? `url("${img.src}")`
: 'none'
)
.catch(e => {
if (__DEV__) {
this.logger.log(`Unable to load image`, e);
}
});
}
return Promise.resolve(
`${backgroundImage.prefix}${backgroundImage.method}(${backgroundImage.args.join(
','
)})`
);
})
).then(backgroundImages => {
if (backgroundImages.length > 1) {
// TODO Multiple backgrounds somehow broken in Chrome
style.backgroundColor = '';
}
style.backgroundImage = backgroundImages.join(',');
});
if (node instanceof HTMLImageElement) {
this.resourceLoader
.inlineImage(node.src)
.then(img => {
if (img && node instanceof HTMLImageElement && node.parentNode) {
const parentNode = node.parentNode;
const clonedChild = copyCSSStyles(node.style, img.cloneNode(false));
parentNode.replaceChild(clonedChild, node);
}
})
.catch(e => {
if (__DEV__) {
this.logger.log(`Unable to load image`, e);
}
});
}
}
}
inlineFonts(document: Document): Promise<void> {
return Promise.all(
Array.from(document.styleSheets).map(sheet => {
if (sheet.href) {
return fetch(sheet.href)
.then(res => res.text())
.then(text => createStyleSheetFontsFromText(text, sheet.href))
.catch(e => {
if (__DEV__) {
this.logger.log(`Unable to load stylesheet`, e);
}
return [];
});
}
return getSheetFonts(sheet, document);
})
)
.then(fonts => fonts.reduce((acc, font) => acc.concat(font), []))
.then(fonts =>
Promise.all(
fonts.map(font =>
fetch(font.formats[0].src)
.then(response => response.blob())
.then(
blob =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = reject;
reader.onload = () => {
// $FlowFixMe
const result: string = reader.result;
resolve(result);
};
reader.readAsDataURL(blob);
})
)
.then(dataUri => {
font.fontFace.setProperty('src', `url("${dataUri}")`);
return `@font-face {${font.fontFace.cssText} `;
})
)
)
)
.then(fontCss => {
const style = document.createElement('style');
style.textContent = fontCss.join('\n');
this.documentElement.appendChild(style);
});
}
createElementClone(node: Node) {
if (this.copyStyles && node instanceof HTMLCanvasElement) {
const img = node.ownerDocument.createElement('img');
try {
img.src = node.toDataURL();
return img;
} catch (e) {
if (__DEV__) {
this.logger.log(`Unable to clone canvas contents, canvas is tainted`);
}
}
}
if (node instanceof HTMLIFrameElement) {
const tempIframe = node.cloneNode(false);
const iframeKey = generateIframeKey();
tempIframe.setAttribute('data-html2canvas-internal-iframe-key', iframeKey);
const {width, height} = parseBounds(node, 0, 0);
this.resourceLoader.cache[iframeKey] = getIframeDocumentElement(node, this.options)
.then(documentElement => {
return this.renderer(
documentElement,
{
async: this.options.async,
allowTaint: this.options.allowTaint,
backgroundColor: '#ffffff',
canvas: null,
imageTimeout: this.options.imageTimeout,
proxy: this.options.proxy,
removeContainer: this.options.removeContainer,
scale: this.options.scale,
foreignObjectRendering: this.options.foreignObjectRendering,
target: new CanvasRenderer(),
width,
height,
x: 0,
y: 0,
windowWidth: documentElement.ownerDocument.defaultView.innerWidth,
windowHeight: documentElement.ownerDocument.defaultView.innerHeight,
scrollX: documentElement.ownerDocument.defaultView.pageXOffset,
scrollY: documentElement.ownerDocument.defaultView.pageYOffset
},
this.logger.child(iframeKey)
);
})
.then(
canvas =>
new Promise((resolve, reject) => {
const iframeCanvas = document.createElement('img');
iframeCanvas.onload = () => resolve(canvas);
iframeCanvas.onerror = reject;
iframeCanvas.src = canvas.toDataURL();
if (tempIframe.parentNode) {
tempIframe.parentNode.replaceChild(
copyCSSStyles(
node.ownerDocument.defaultView.getComputedStyle(node),
iframeCanvas
),
tempIframe
);
}
})
);
return tempIframe;
}
return node.cloneNode(false);
}
cloneNode(node: Node): Node {
const clone =
node.nodeType === Node.TEXT_NODE
? document.createTextNode(node.nodeValue)
: this.createElementClone(node);
const window = node.ownerDocument.defaultView;
if (this.referenceElement === node && clone instanceof window.HTMLElement) {
this.clonedReferenceElement = clone;
}
if (clone instanceof window.HTMLBodyElement) {
createPseudoHideStyles(clone);
}
for (let child = node.firstChild; child; child = child.nextSibling) {
if (child.nodeType !== Node.ELEMENT_NODE || child.nodeName !== 'SCRIPT') {
if (!this.copyStyles || child.nodeName !== 'STYLE') {
clone.appendChild(this.cloneNode(child));
}
}
}
if (node instanceof window.HTMLElement && clone instanceof window.HTMLElement) {
this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_BEFORE));
this.inlineAllImages(inlinePseudoElement(node, clone, PSEUDO_AFTER));
if (this.copyStyles && !(node instanceof HTMLIFrameElement)) {
copyCSSStyles(node.ownerDocument.defaultView.getComputedStyle(node), clone);
}
this.inlineAllImages(clone);
if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
this.scrolledElements.push([node, node.scrollLeft, node.scrollTop]);
}
switch (node.nodeName) {
case 'CANVAS':
if (!this.copyStyles) {
cloneCanvasContents(node, clone);
}
break;
case 'TEXTAREA':
case 'SELECT':
clone.value = node.value;
break;
}
}
return clone;
}
}
type Font = {
src: string,
format: string
};
type FontFamily = {
formats: Array<Font>,
fontFace: CSSStyleDeclaration
};
const getSheetFonts = (sheet: StyleSheet, document: Document): Array<FontFamily> => {
// $FlowFixMe
return (sheet.cssRules ? Array.from(sheet.cssRules) : [])
.filter(rule => rule.type === CSSRule.FONT_FACE_RULE)
.map(rule => {
const src = parseBackgroundImage(rule.style.getPropertyValue('src'));
const formats = [];
for (let i = 0; i < src.length; i++) {
if (src[i].method === 'url' && src[i + 1] && src[i + 1].method === 'format') {
const a = document.createElement('a');
a.href = src[i].args[0];
if (document.body) {
document.body.appendChild(a);
}
const font = {
src: a.href,
format: src[i + 1].args[0]
};
formats.push(font);
}
}
return {
// TODO select correct format for browser),
formats: formats.filter(font => /^woff/i.test(font.format)),
fontFace: rule.style
};
})
.filter(font => font.formats.length);
};
const createStyleSheetFontsFromText = (text: string, baseHref: string): Array<FontFamily> => {
const doc = document.implementation.createHTMLDocument('');
const base = document.createElement('base');
// $FlowFixMe
base.href = baseHref;
const style = document.createElement('style');
style.textContent = text;
if (doc.head) {
doc.head.appendChild(base);
}
if (doc.body) {
doc.body.appendChild(style);
}
return style.sheet ? getSheetFonts(style.sheet, doc) : [];
};
const restoreOwnerScroll = (ownerDocument: Document, x: number, y: number) => {
if (
ownerDocument.defaultView &&
(x !== ownerDocument.defaultView.pageXOffset || y !== ownerDocument.defaultView.pageYOffset)
) {
ownerDocument.defaultView.scrollTo(x, y);
}
};
const cloneCanvasContents = (canvas: HTMLCanvasElement, clonedCanvas: HTMLCanvasElement) => {
try {
if (clonedCanvas) {
clonedCanvas.width = canvas.width;
clonedCanvas.height = canvas.height;
clonedCanvas
.getContext('2d')
.putImageData(
canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height),
0,
0
);
}
} catch (e) {}
};
const inlinePseudoElement = (
node: HTMLElement,
clone: HTMLElement,
pseudoElt: ':before' | ':after'
): ?HTMLElement => {
const style = node.ownerDocument.defaultView.getComputedStyle(node, pseudoElt);
if (
!style ||
!style.content ||
style.content === 'none' ||
style.content === '-moz-alt-content' ||
style.display === 'none'
) {
return;
}
const content = stripQuotes(style.content);
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);
anonymousReplacedElement.className = `${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE} ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
clone.className +=
pseudoElt === PSEUDO_BEFORE
? ` ${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}`
: ` ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
if (pseudoElt === PSEUDO_BEFORE) {
clone.insertBefore(anonymousReplacedElement, clone.firstChild);
} else {
clone.appendChild(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 PSEUDO_BEFORE = ':before';
const PSEUDO_AFTER = ':after';
const PSEUDO_HIDE_ELEMENT_CLASS_BEFORE = '___html2canvas___pseudoelement_before';
const PSEUDO_HIDE_ELEMENT_CLASS_AFTER = '___html2canvas___pseudoelement_after';
const PSEUDO_HIDE_ELEMENT_STYLE = `{
content: "" !important;
display: none !important;
}`;
const createPseudoHideStyles = (body: HTMLElement) => {
createStyles(
body,
`.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE}
.${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}`
);
};
const createStyles = (body: HTMLElement, styles) => {
const style = body.ownerDocument.createElement('style');
style.innerHTML = styles;
body.appendChild(style);
};
const initNode = ([element, x, y]: [HTMLElement, number, number]) => {
element.scrollLeft = x;
element.scrollTop = y;
};
const generateIframeKey = (): string =>
Math.ceil(Date.now() + Math.random() * 10000000).toString(16);
const DATA_URI_REGEXP = /^data:text\/(.+);(base64)?,(.*)$/i;
const getIframeDocumentElement = (
node: HTMLIFrameElement,
options: Options
): Promise<HTMLElement> => {
try {
return Promise.resolve(node.contentWindow.document.documentElement);
} catch (e) {
return options.proxy
? Proxy(node.src, options)
.then(html => {
const match = html.match(DATA_URI_REGEXP);
if (!match) {
return Promise.reject();
}
return match[2] === 'base64'
? window.atob(decodeURIComponent(match[3]))
: decodeURIComponent(match[3]);
})
.then(html =>
createIframeContainer(
node.ownerDocument,
parseBounds(node, 0, 0)
).then(cloneIframeContainer => {
const cloneWindow = cloneIframeContainer.contentWindow;
const documentClone = cloneWindow.document;
documentClone.open();
documentClone.write(html);
const iframeLoad = iframeLoader(cloneIframeContainer).then(
() => documentClone.documentElement
);
documentClone.close();
return iframeLoad;
})
)
: Promise.reject();
}
};
const createIframeContainer = (
ownerDocument: Document,
bounds: Bounds
): Promise<HTMLIFrameElement> => {
const cloneIframeContainer = ownerDocument.createElement('iframe');
cloneIframeContainer.className = 'html2canvas-container';
cloneIframeContainer.style.visibility = 'hidden';
cloneIframeContainer.style.position = 'fixed';
cloneIframeContainer.style.left = '-10000px';
cloneIframeContainer.style.top = '0px';
cloneIframeContainer.style.border = '0';
cloneIframeContainer.width = bounds.width.toString();
cloneIframeContainer.height = bounds.height.toString();
cloneIframeContainer.scrolling = 'no'; // ios won't scroll without it
if (!ownerDocument.body) {
return Promise.reject(
__DEV__ ? `Body element not found in Document that is getting rendered` : ''
);
}
ownerDocument.body.appendChild(cloneIframeContainer);
return Promise.resolve(cloneIframeContainer);
};
const iframeLoader = (cloneIframeContainer: HTMLIFrameElement): Promise<HTMLIFrameElement> => {
const cloneWindow = cloneIframeContainer.contentWindow;
const documentClone = cloneWindow.document;
return new Promise((resolve, reject) => {
cloneWindow.onload = cloneIframeContainer.onload = documentClone.onreadystatechange = () => {
const interval = setInterval(() => {
if (
documentClone.body.childNodes.length > 0 &&
documentClone.readyState === 'complete'
) {
clearInterval(interval);
resolve(cloneIframeContainer);
}
}, 50);
};
});
};
export const cloneWindow = (
ownerDocument: Document,
bounds: Bounds,
referenceElement: HTMLElement,
options: Options,
logger: Logger,
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>
): Promise<[HTMLIFrameElement, HTMLElement, ResourceLoader]> => {
const cloner = new DocumentCloner(referenceElement, options, logger, false, renderer);
const scrollX = ownerDocument.defaultView.pageXOffset;
const scrollY = ownerDocument.defaultView.pageYOffset;
return createIframeContainer(ownerDocument, bounds).then(cloneIframeContainer => {
const cloneWindow = cloneIframeContainer.contentWindow;
const documentClone = cloneWindow.document;
/* Chrome doesn't detect relative background-images assigned in inline <style> sheets when fetched through getComputedStyle
if window url is about:blank, we can assign the url to current by writing onto the document
*/
const iframeLoad = iframeLoader(cloneIframeContainer).then(() => {
cloner.scrolledElements.forEach(initNode);
cloneWindow.scrollTo(bounds.left, bounds.top);
if (
/(iPad|iPhone|iPod)/g.test(navigator.userAgent) &&
(cloneWindow.scrollY !== bounds.top || cloneWindow.scrollX !== bounds.left)
) {
documentClone.documentElement.style.top = -bounds.top + 'px';
documentClone.documentElement.style.left = -bounds.left + 'px';
documentClone.documentElement.style.position = 'absolute';
}
return cloner.clonedReferenceElement instanceof cloneWindow.HTMLElement ||
cloner.clonedReferenceElement instanceof ownerDocument.defaultView.HTMLElement ||
cloner.clonedReferenceElement instanceof HTMLElement
? Promise.resolve([
cloneIframeContainer,
cloner.clonedReferenceElement,
cloner.resourceLoader
])
: Promise.reject(
__DEV__
? `Error finding the ${referenceElement.nodeName} in the cloned document`
: ''
);
});
documentClone.open();
documentClone.write('<!DOCTYPE html><html></html>');
// Chrome scrolls the parent document for some reason after the write to the cloned window???
restoreOwnerScroll(referenceElement.ownerDocument, scrollX, scrollY);
documentClone.replaceChild(
documentClone.adoptNode(cloner.documentElement),
documentClone.documentElement
);
documentClone.close();
return iframeLoad;
});
};

View File

@ -1,251 +0,0 @@
/* @flow */
'use strict';
// http://dev.w3.org/csswg/css-color/
type ColorArray = [number, number, number, number | null];
const HEX3 = /^#([a-f0-9]{3})$/i;
const hex3 = (value: string): ColorArray | false => {
const match = value.match(HEX3);
if (match) {
return [
parseInt(match[1][0] + match[1][0], 16),
parseInt(match[1][1] + match[1][1], 16),
parseInt(match[1][2] + match[1][2], 16),
null
];
}
return false;
};
const HEX6 = /^#([a-f0-9]{6})$/i;
const hex6 = (value: string): ColorArray | false => {
const match = value.match(HEX6);
if (match) {
return [
parseInt(match[1].substring(0, 2), 16),
parseInt(match[1].substring(2, 4), 16),
parseInt(match[1].substring(4, 6), 16),
null
];
}
return false;
};
const RGB = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/;
const rgb = (value: string): ColorArray | false => {
const match = value.match(RGB);
if (match) {
return [Number(match[1]), Number(match[2]), Number(match[3]), null];
}
return false;
};
const RGBA = /^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d?\.?\d+)\s*\)$/;
const rgba = (value: string): ColorArray | false => {
const match = value.match(RGBA);
if (match && match.length > 4) {
return [Number(match[1]), Number(match[2]), Number(match[3]), Number(match[4])];
}
return false;
};
const fromArray = (array: Array<number>): ColorArray => {
return [
Math.min(array[0], 255),
Math.min(array[1], 255),
Math.min(array[2], 255),
array.length > 3 ? array[3] : null
];
};
const namedColor = (name: string): ColorArray | false => {
const color: ColorArray | void = NAMED_COLORS[name.toLowerCase()];
return color ? color : false;
};
export default class Color {
r: number;
g: number;
b: number;
a: number | null;
constructor(value: string | Array<number>) {
const [r, g, b, a] = Array.isArray(value)
? fromArray(value)
: hex3(value) ||
rgb(value) ||
rgba(value) ||
namedColor(value) ||
hex6(value) || [0, 0, 0, null];
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
isTransparent(): boolean {
return this.a === 0;
}
toString(): string {
return this.a !== null && this.a !== 1
? `rgba(${this.r},${this.g},${this.b},${this.a})`
: `rgb(${this.r},${this.g},${this.b})`;
}
}
const NAMED_COLORS = {
transparent: [0, 0, 0, 0],
aliceblue: [240, 248, 255, null],
antiquewhite: [250, 235, 215, null],
aqua: [0, 255, 255, null],
aquamarine: [127, 255, 212, null],
azure: [240, 255, 255, null],
beige: [245, 245, 220, null],
bisque: [255, 228, 196, null],
black: [0, 0, 0, null],
blanchedalmond: [255, 235, 205, null],
blue: [0, 0, 255, null],
blueviolet: [138, 43, 226, null],
brown: [165, 42, 42, null],
burlywood: [222, 184, 135, null],
cadetblue: [95, 158, 160, null],
chartreuse: [127, 255, 0, null],
chocolate: [210, 105, 30, null],
coral: [255, 127, 80, null],
cornflowerblue: [100, 149, 237, null],
cornsilk: [255, 248, 220, null],
crimson: [220, 20, 60, null],
cyan: [0, 255, 255, null],
darkblue: [0, 0, 139, null],
darkcyan: [0, 139, 139, null],
darkgoldenrod: [184, 134, 11, null],
darkgray: [169, 169, 169, null],
darkgreen: [0, 100, 0, null],
darkgrey: [169, 169, 169, null],
darkkhaki: [189, 183, 107, null],
darkmagenta: [139, 0, 139, null],
darkolivegreen: [85, 107, 47, null],
darkorange: [255, 140, 0, null],
darkorchid: [153, 50, 204, null],
darkred: [139, 0, 0, null],
darksalmon: [233, 150, 122, null],
darkseagreen: [143, 188, 143, null],
darkslateblue: [72, 61, 139, null],
darkslategray: [47, 79, 79, null],
darkslategrey: [47, 79, 79, null],
darkturquoise: [0, 206, 209, null],
darkviolet: [148, 0, 211, null],
deeppink: [255, 20, 147, null],
deepskyblue: [0, 191, 255, null],
dimgray: [105, 105, 105, null],
dimgrey: [105, 105, 105, null],
dodgerblue: [30, 144, 255, null],
firebrick: [178, 34, 34, null],
floralwhite: [255, 250, 240, null],
forestgreen: [34, 139, 34, null],
fuchsia: [255, 0, 255, null],
gainsboro: [220, 220, 220, null],
ghostwhite: [248, 248, 255, null],
gold: [255, 215, 0, null],
goldenrod: [218, 165, 32, null],
gray: [128, 128, 128, null],
green: [0, 128, 0, null],
greenyellow: [173, 255, 47, null],
grey: [128, 128, 128, null],
honeydew: [240, 255, 240, null],
hotpink: [255, 105, 180, null],
indianred: [205, 92, 92, null],
indigo: [75, 0, 130, null],
ivory: [255, 255, 240, null],
khaki: [240, 230, 140, null],
lavender: [230, 230, 250, null],
lavenderblush: [255, 240, 245, null],
lawngreen: [124, 252, 0, null],
lemonchiffon: [255, 250, 205, null],
lightblue: [173, 216, 230, null],
lightcoral: [240, 128, 128, null],
lightcyan: [224, 255, 255, null],
lightgoldenrodyellow: [250, 250, 210, null],
lightgray: [211, 211, 211, null],
lightgreen: [144, 238, 144, null],
lightgrey: [211, 211, 211, null],
lightpink: [255, 182, 193, null],
lightsalmon: [255, 160, 122, null],
lightseagreen: [32, 178, 170, null],
lightskyblue: [135, 206, 250, null],
lightslategray: [119, 136, 153, null],
lightslategrey: [119, 136, 153, null],
lightsteelblue: [176, 196, 222, null],
lightyellow: [255, 255, 224, null],
lime: [0, 255, 0, null],
limegreen: [50, 205, 50, null],
linen: [250, 240, 230, null],
magenta: [255, 0, 255, null],
maroon: [128, 0, 0, null],
mediumaquamarine: [102, 205, 170, null],
mediumblue: [0, 0, 205, null],
mediumorchid: [186, 85, 211, null],
mediumpurple: [147, 112, 219, null],
mediumseagreen: [60, 179, 113, null],
mediumslateblue: [123, 104, 238, null],
mediumspringgreen: [0, 250, 154, null],
mediumturquoise: [72, 209, 204, null],
mediumvioletred: [199, 21, 133, null],
midnightblue: [25, 25, 112, null],
mintcream: [245, 255, 250, null],
mistyrose: [255, 228, 225, null],
moccasin: [255, 228, 181, null],
navajowhite: [255, 222, 173, null],
navy: [0, 0, 128, null],
oldlace: [253, 245, 230, null],
olive: [128, 128, 0, null],
olivedrab: [107, 142, 35, null],
orange: [255, 165, 0, null],
orangered: [255, 69, 0, null],
orchid: [218, 112, 214, null],
palegoldenrod: [238, 232, 170, null],
palegreen: [152, 251, 152, null],
paleturquoise: [175, 238, 238, null],
palevioletred: [219, 112, 147, null],
papayawhip: [255, 239, 213, null],
peachpuff: [255, 218, 185, null],
peru: [205, 133, 63, null],
pink: [255, 192, 203, null],
plum: [221, 160, 221, null],
powderblue: [176, 224, 230, null],
purple: [128, 0, 128, null],
rebeccapurple: [102, 51, 153, null],
red: [255, 0, 0, null],
rosybrown: [188, 143, 143, null],
royalblue: [65, 105, 225, null],
saddlebrown: [139, 69, 19, null],
salmon: [250, 128, 114, null],
sandybrown: [244, 164, 96, null],
seagreen: [46, 139, 87, null],
seashell: [255, 245, 238, null],
sienna: [160, 82, 45, null],
silver: [192, 192, 192, null],
skyblue: [135, 206, 235, null],
slateblue: [106, 90, 205, null],
slategray: [112, 128, 144, null],
slategrey: [112, 128, 144, null],
snow: [255, 250, 250, null],
springgreen: [0, 255, 127, null],
steelblue: [70, 130, 180, null],
tan: [210, 180, 140, null],
teal: [0, 128, 128, null],
thistle: [216, 191, 216, null],
tomato: [255, 99, 71, null],
turquoise: [64, 224, 208, null],
violet: [238, 130, 238, null],
wheat: [245, 222, 179, null],
white: [255, 255, 255, null],
whitesmoke: [245, 245, 245, null],
yellow: [255, 255, 0, null],
yellowgreen: [154, 205, 50, null]
};
export const TRANSPARENT = new Color([0, 0, 0, 0]);

View File

@ -1,191 +0,0 @@
/* @flow */
'use strict';
import type {BackgroundSource} from './parsing/background';
import type {Bounds} from './Bounds';
import {parseAngle} from './Angle';
import Color from './Color';
import Length, {LENGTH_TYPE} from './Length';
const SIDE_OR_CORNER = /^(to )?(left|top|right|bottom)( (left|top|right|bottom))?$/i;
const PERCENTAGE_ANGLES = /^([+-]?\d*\.?\d+)% ([+-]?\d*\.?\d+)%$/i;
const ENDS_WITH_LENGTH = /(px)|%|( 0)$/i;
const FROM_TO = /^(from|to)\((.+)\)$/i;
export type Direction = {
x0: number,
x1: number,
y0: number,
y1: number
};
export type ColorStop = {
color: Color,
stop: number
};
export type Gradient = {
direction: Direction,
colorStops: Array<ColorStop>
};
export const parseGradient = (
{args, method, prefix}: BackgroundSource,
bounds: Bounds
): ?Gradient => {
if (method === 'linear-gradient') {
return parseLinearGradient(args, bounds);
} else if (method === 'gradient' && args[0] === 'linear') {
// TODO handle correct angle
return parseLinearGradient(
['to bottom'].concat(
args
.slice(3)
.map(color => color.match(FROM_TO))
.filter(v => v !== null)
// $FlowFixMe
.map(v => v[2])
),
bounds
);
}
};
const parseLinearGradient = (args: Array<string>, bounds: Bounds): Gradient => {
const angle = parseAngle(args[0]);
const HAS_SIDE_OR_CORNER = SIDE_OR_CORNER.test(args[0]);
const HAS_DIRECTION = HAS_SIDE_OR_CORNER || angle !== null || PERCENTAGE_ANGLES.test(args[0]);
const direction = HAS_DIRECTION
? angle !== null
? calculateGradientDirection(angle, bounds)
: HAS_SIDE_OR_CORNER
? parseSideOrCorner(args[0], bounds)
: parsePercentageAngle(args[0], bounds)
: calculateGradientDirection(Math.PI, bounds);
const colorStops = [];
const firstColorStopIndex = HAS_DIRECTION ? 1 : 0;
for (let i = firstColorStopIndex; i < args.length; i++) {
const value = args[i];
const HAS_LENGTH = ENDS_WITH_LENGTH.test(value);
const lastSpaceIndex = value.lastIndexOf(' ');
const color = new Color(HAS_LENGTH ? value.substring(0, lastSpaceIndex) : value);
const stop = HAS_LENGTH
? new Length(value.substring(lastSpaceIndex + 1))
: i === firstColorStopIndex
? new Length('0%')
: i === args.length - 1 ? new Length('100%') : null;
colorStops.push({color, stop});
}
// TODO: Fix some inaccuracy with color stops with px values
const lineLength = Math.min(
Math.sqrt(
Math.pow(Math.abs(direction.x0) + Math.abs(direction.x1), 2) +
Math.pow(Math.abs(direction.y0) + Math.abs(direction.y1), 2)
),
bounds.width * 2,
bounds.height * 2
);
const absoluteValuedColorStops = colorStops.map(({color, stop}) => {
return {
color,
// $FlowFixMe
stop: stop ? stop.getAbsoluteValue(lineLength) / lineLength : null
};
});
let previousColorStop = absoluteValuedColorStops[0].stop;
for (let i = 0; i < absoluteValuedColorStops.length; i++) {
if (previousColorStop !== null) {
const stop = absoluteValuedColorStops[i].stop;
if (stop === null) {
let n = i;
while (absoluteValuedColorStops[n].stop === null) {
n++;
}
const steps = n - i + 1;
const nextColorStep = absoluteValuedColorStops[n].stop;
const stepSize = (nextColorStep - previousColorStop) / steps;
for (; i < n; i++) {
previousColorStop = absoluteValuedColorStops[i].stop =
previousColorStop + stepSize;
}
} else {
previousColorStop = stop;
}
}
}
return {
direction,
colorStops: absoluteValuedColorStops
};
};
const calculateGradientDirection = (radian: number, bounds: Bounds): Direction => {
const width = bounds.width;
const height = bounds.height;
const HALF_WIDTH = width * 0.5;
const HALF_HEIGHT = height * 0.5;
const lineLength = Math.abs(width * Math.sin(radian)) + Math.abs(height * Math.cos(radian));
const HALF_LINE_LENGTH = lineLength / 2;
const x0 = HALF_WIDTH + Math.sin(radian) * HALF_LINE_LENGTH;
const y0 = HALF_HEIGHT - Math.cos(radian) * HALF_LINE_LENGTH;
const x1 = width - x0;
const y1 = height - y0;
return {x0, x1, y0, y1};
};
const parseTopRight = (bounds: Bounds) =>
Math.acos(
bounds.width / 2 / (Math.sqrt(Math.pow(bounds.width, 2) + Math.pow(bounds.height, 2)) / 2)
);
const parseSideOrCorner = (side: string, bounds: Bounds): Direction => {
switch (side) {
case 'bottom':
case 'to top':
return calculateGradientDirection(0, bounds);
case 'left':
case 'to right':
return calculateGradientDirection(Math.PI / 2, bounds);
case 'right':
case 'to left':
return calculateGradientDirection(3 * Math.PI / 2, bounds);
case 'top right':
case 'right top':
case 'to bottom left':
case 'to left bottom':
return calculateGradientDirection(Math.PI + parseTopRight(bounds), bounds);
case 'top left':
case 'left top':
case 'to bottom right':
case 'to right bottom':
return calculateGradientDirection(Math.PI - parseTopRight(bounds), bounds);
case 'bottom left':
case 'left bottom':
case 'to top right':
case 'to right top':
return calculateGradientDirection(parseTopRight(bounds), bounds);
case 'bottom right':
case 'right bottom':
case 'to top left':
case 'to left top':
return calculateGradientDirection(2 * Math.PI - parseTopRight(bounds), bounds);
case 'top':
case 'to bottom':
default:
return calculateGradientDirection(Math.PI, bounds);
}
};
const parsePercentageAngle = (angle: string, bounds: Bounds): Direction => {
const [left, top] = angle.split(' ').map(parseFloat);
const ratio = left / 100 * bounds.width / (top / 100 * bounds.height);
return calculateGradientDirection(Math.atan(isNaN(ratio) ? 1 : ratio) + Math.PI / 2, bounds);
};

View File

@ -1,155 +0,0 @@
/* @flow */
'use strict';
import type NodeContainer from './NodeContainer';
import TextContainer from './TextContainer';
import {BACKGROUND_CLIP, BACKGROUND_ORIGIN} from './parsing/background';
import {BORDER_STYLE} from './parsing/border';
import Circle from './drawing/Circle';
import Vector from './drawing/Vector';
import Color from './Color';
import Length from './Length';
import {Bounds} from './Bounds';
import {TextBounds} from './TextBounds';
import {copyCSSStyles} from './Util';
export const INPUT_COLOR = new Color([42, 42, 42]);
const INPUT_BORDER_COLOR = new Color([165, 165, 165]);
const INPUT_BACKGROUND_COLOR = new Color([222, 222, 222]);
const INPUT_BORDER = {
borderWidth: 1,
borderColor: INPUT_BORDER_COLOR,
borderStyle: BORDER_STYLE.SOLID
};
export const INPUT_BORDERS = [INPUT_BORDER, INPUT_BORDER, INPUT_BORDER, INPUT_BORDER];
export const INPUT_BACKGROUND = {
backgroundColor: INPUT_BACKGROUND_COLOR,
backgroundImage: [],
backgroundClip: BACKGROUND_CLIP.PADDING_BOX,
backgroundOrigin: BACKGROUND_ORIGIN.PADDING_BOX
};
const RADIO_BORDER_RADIUS = new Length('50%');
const RADIO_BORDER_RADIUS_TUPLE = [RADIO_BORDER_RADIUS, RADIO_BORDER_RADIUS];
const INPUT_RADIO_BORDER_RADIUS = [
RADIO_BORDER_RADIUS_TUPLE,
RADIO_BORDER_RADIUS_TUPLE,
RADIO_BORDER_RADIUS_TUPLE,
RADIO_BORDER_RADIUS_TUPLE
];
const CHECKBOX_BORDER_RADIUS = new Length('3px');
const CHECKBOX_BORDER_RADIUS_TUPLE = [CHECKBOX_BORDER_RADIUS, CHECKBOX_BORDER_RADIUS];
const INPUT_CHECKBOX_BORDER_RADIUS = [
CHECKBOX_BORDER_RADIUS_TUPLE,
CHECKBOX_BORDER_RADIUS_TUPLE,
CHECKBOX_BORDER_RADIUS_TUPLE,
CHECKBOX_BORDER_RADIUS_TUPLE
];
export const getInputBorderRadius = (node: HTMLInputElement) => {
return node.type === 'radio' ? INPUT_RADIO_BORDER_RADIUS : INPUT_CHECKBOX_BORDER_RADIUS;
};
export const inlineInputElement = (node: HTMLInputElement, container: NodeContainer): void => {
if (node.type === 'radio' || node.type === 'checkbox') {
if (node.checked) {
const size = Math.min(container.bounds.width, container.bounds.height);
container.childNodes.push(
node.type === 'checkbox'
? [
new Vector(
container.bounds.left + size * 0.39363,
container.bounds.top + size * 0.79
),
new Vector(
container.bounds.left + size * 0.16,
container.bounds.top + size * 0.5549
),
new Vector(
container.bounds.left + size * 0.27347,
container.bounds.top + size * 0.44071
),
new Vector(
container.bounds.left + size * 0.39694,
container.bounds.top + size * 0.5649
),
new Vector(
container.bounds.left + size * 0.72983,
container.bounds.top + size * 0.23
),
new Vector(
container.bounds.left + size * 0.84,
container.bounds.top + size * 0.34085
),
new Vector(
container.bounds.left + size * 0.39363,
container.bounds.top + size * 0.79
)
]
: new Circle(
container.bounds.left + size / 4,
container.bounds.top + size / 4,
size / 4
)
);
}
} else {
inlineFormElement(getInputValue(node), node, container, false);
}
};
export const inlineTextAreaElement = (
node: HTMLTextAreaElement,
container: NodeContainer
): void => {
inlineFormElement(node.value, node, container, true);
};
export const inlineSelectElement = (node: HTMLSelectElement, container: NodeContainer): void => {
const option = node.options[node.selectedIndex || 0];
inlineFormElement(option ? option.text || '' : '', node, container, false);
};
export const reformatInputBounds = (bounds: Bounds): Bounds => {
if (bounds.width > bounds.height) {
bounds.left += (bounds.width - bounds.height) / 2;
bounds.width = bounds.height;
} else if (bounds.width < bounds.height) {
bounds.top += (bounds.height - bounds.width) / 2;
bounds.height = bounds.width;
}
return bounds;
};
const inlineFormElement = (
value: string,
node: HTMLElement,
container: NodeContainer,
allowLinebreak: boolean
): void => {
const body = node.ownerDocument.body;
if (value.length > 0 && body) {
const wrapper = node.ownerDocument.createElement('html2canvaswrapper');
copyCSSStyles(node.ownerDocument.defaultView.getComputedStyle(node, null), wrapper);
wrapper.style.position = 'fixed';
wrapper.style.left = `${container.bounds.left}px`;
wrapper.style.top = `${container.bounds.top}px`;
if (!allowLinebreak) {
wrapper.style.whiteSpace = 'nowrap';
}
const text = node.ownerDocument.createTextNode(value);
wrapper.appendChild(text);
body.appendChild(wrapper);
container.childNodes.push(TextContainer.fromTextNode(text, container));
body.removeChild(wrapper);
}
};
const getInputValue = (node: HTMLInputElement): string => {
const value =
node.type === 'password' ? new Array(node.value.length + 1).join('\u2022') : node.value;
return value.length === 0 ? node.placeholder || '' : value;
};

View File

@ -1,36 +0,0 @@
/* @flow */
'use strict';
export const LENGTH_TYPE = {
PX: 0,
PERCENTAGE: 1
};
export type LengthType = $Values<typeof LENGTH_TYPE>;
export default class Length {
type: LengthType;
value: number;
constructor(value: string) {
this.type =
value.substr(value.length - 1) === '%' ? LENGTH_TYPE.PERCENTAGE : LENGTH_TYPE.PX;
const parsedValue = parseFloat(value);
if (__DEV__ && isNaN(parsedValue)) {
console.error(`Invalid value given for Length: "${value}"`);
}
this.value = isNaN(parsedValue) ? 0 : parsedValue;
}
isPercentage(): boolean {
return this.type === LENGTH_TYPE.PERCENTAGE;
}
getAbsoluteValue(parentLength: number): number {
return this.isPercentage() ? parentLength * (this.value / 100) : this.value;
}
static create(v): Length {
return new Length(v);
}
}

View File

@ -1,46 +0,0 @@
/* @flow */
'use strict';
export default class Logger {
start: number;
id: ?string;
constructor(id: ?string, start: ?number) {
this.start = start ? start : Date.now();
this.id = id;
}
child(id: string) {
return new Logger(id, this.start);
}
// eslint-disable-next-line flowtype/no-weak-types
log(...args: any) {
if (window.console && window.console.log) {
Function.prototype.bind
.call(window.console.log, window.console)
.apply(
window.console,
[
Date.now() - this.start + 'ms',
this.id ? `html2canvas (${this.id}):` : 'html2canvas:'
].concat([].slice.call(args, 0))
);
}
}
// eslint-disable-next-line flowtype/no-weak-types
error(...args: any) {
if (window.console && window.console.error) {
Function.prototype.bind
.call(window.console.error, window.console)
.apply(
window.console,
[
Date.now() - this.start + 'ms',
this.id ? `html2canvas (${this.id}):` : 'html2canvas:'
].concat([].slice.call(args, 0))
);
}
}
}

View File

@ -1,254 +0,0 @@
/* @flow */
'use strict';
import type {Background} from './parsing/background';
import type {Border} from './parsing/border';
import type {BorderRadius} from './parsing/borderRadius';
import type {DisplayBit} from './parsing/display';
import type {Float} from './parsing/float';
import type {Font} from './parsing/font';
import type {Overflow} from './parsing/overflow';
import type {Padding} from './parsing/padding';
import type {Position} from './parsing/position';
import type {TextShadow} from './parsing/textShadow';
import type {TextTransform} from './parsing/textTransform';
import type {TextDecoration} from './parsing/textDecoration';
import type {Transform} from './parsing/transform';
import type {Visibility} from './parsing/visibility';
import type {zIndex} from './parsing/zIndex';
import type {Bounds, BoundCurves} from './Bounds';
import type ResourceLoader, {ImageElement} from './ResourceLoader';
import type {Path} from './drawing/Path';
import type TextContainer from './TextContainer';
import Color from './Color';
import {contains} from './Util';
import {parseBackground} from './parsing/background';
import {parseBorder} from './parsing/border';
import {parseBorderRadius} from './parsing/borderRadius';
import {parseDisplay, DISPLAY} from './parsing/display';
import {parseCSSFloat, FLOAT} from './parsing/float';
import {parseFont} from './parsing/font';
import {parseLetterSpacing} from './parsing/letterSpacing';
import {parseOverflow, OVERFLOW} from './parsing/overflow';
import {parsePadding} from './parsing/padding';
import {parsePosition, POSITION} from './parsing/position';
import {parseTextDecoration} from './parsing/textDecoration';
import {parseTextShadow} from './parsing/textShadow';
import {parseTextTransform} from './parsing/textTransform';
import {parseTransform} from './parsing/transform';
import {parseVisibility, VISIBILITY} from './parsing/visibility';
import {parseZIndex} from './parsing/zIndex';
import {parseBounds, parseBoundCurves, calculatePaddingBoxPath} from './Bounds';
import {
INPUT_BACKGROUND,
INPUT_BORDERS,
INPUT_COLOR,
getInputBorderRadius,
reformatInputBounds
} from './Input';
type StyleDeclaration = {
background: Background,
border: Array<Border>,
borderRadius: Array<BorderRadius>,
color: Color,
display: DisplayBit,
float: Float,
font: Font,
letterSpacing: number,
opacity: number,
overflow: Overflow,
padding: Padding,
position: Position,
textDecoration: TextDecoration | null,
textShadow: Array<TextShadow> | null,
textTransform: TextTransform,
transform: Transform,
visibility: Visibility,
zIndex: zIndex
};
const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT'];
export default class NodeContainer {
name: ?string;
parent: ?NodeContainer;
style: StyleDeclaration;
childNodes: Array<TextContainer | Path>;
bounds: Bounds;
curvedBounds: BoundCurves;
image: ?string;
index: number;
constructor(
node: HTMLElement | SVGSVGElement,
parent: ?NodeContainer,
resourceLoader: ResourceLoader,
index: number
) {
this.parent = parent;
this.index = index;
this.childNodes = [];
const defaultView = node.ownerDocument.defaultView;
const scrollX = defaultView.pageXOffset;
const scrollY = defaultView.pageYOffset;
const style = defaultView.getComputedStyle(node, null);
const display = parseDisplay(style.display);
const IS_INPUT = node.type === 'radio' || node.type === 'checkbox';
const position = parsePosition(style.position);
this.style = {
background: IS_INPUT ? INPUT_BACKGROUND : parseBackground(style, resourceLoader),
border: IS_INPUT ? INPUT_BORDERS : parseBorder(style),
borderRadius:
(node instanceof defaultView.HTMLInputElement ||
node instanceof HTMLInputElement) &&
IS_INPUT
? getInputBorderRadius(node)
: parseBorderRadius(style),
color: IS_INPUT ? INPUT_COLOR : new Color(style.color),
display: display,
float: parseCSSFloat(style.float),
font: parseFont(style),
letterSpacing: parseLetterSpacing(style.letterSpacing),
opacity: parseFloat(style.opacity),
overflow:
INPUT_TAGS.indexOf(node.tagName) === -1
? parseOverflow(style.overflow)
: OVERFLOW.HIDDEN,
padding: parsePadding(style),
position: position,
textDecoration: parseTextDecoration(style),
textShadow: parseTextShadow(style.textShadow),
textTransform: parseTextTransform(style.textTransform),
transform: parseTransform(style),
visibility: parseVisibility(style.visibility),
zIndex: parseZIndex(position !== POSITION.STATIC ? style.zIndex : 'auto')
};
if (this.isTransformed()) {
// getBoundingClientRect provides values post-transform, we want them without the transformation
node.style.transform = 'matrix(1,0,0,1,0,0)';
}
// TODO move bound retrieval for all nodes to a later stage?
if (node.tagName === 'IMG') {
node.addEventListener('load', () => {
this.bounds = parseBounds(node, scrollX, scrollY);
this.curvedBounds = parseBoundCurves(
this.bounds,
this.style.border,
this.style.borderRadius
);
});
}
this.image = getImage(node, resourceLoader);
this.bounds = IS_INPUT
? reformatInputBounds(parseBounds(node, scrollX, scrollY))
: parseBounds(node, scrollX, scrollY);
this.curvedBounds = parseBoundCurves(
this.bounds,
this.style.border,
this.style.borderRadius
);
if (__DEV__) {
this.name = `${node.tagName.toLowerCase()}${node.id
? `#${node.id}`
: ''}${node.className
.toString()
.split(' ')
.map(s => (s.length ? `.${s}` : ''))
.join('')}`;
}
}
getClipPaths(): Array<Path> {
const parentClips = this.parent ? this.parent.getClipPaths() : [];
const isClipped =
this.style.overflow === OVERFLOW.HIDDEN || this.style.overflow === OVERFLOW.SCROLL;
return isClipped
? parentClips.concat([calculatePaddingBoxPath(this.curvedBounds)])
: parentClips;
}
isInFlow(): boolean {
return this.isRootElement() && !this.isFloating() && !this.isAbsolutelyPositioned();
}
isVisible(): boolean {
return (
!contains(this.style.display, DISPLAY.NONE) &&
this.style.opacity > 0 &&
this.style.visibility === VISIBILITY.VISIBLE
);
}
isAbsolutelyPositioned(): boolean {
return this.style.position !== POSITION.STATIC && this.style.position !== POSITION.RELATIVE;
}
isPositioned(): boolean {
return this.style.position !== POSITION.STATIC;
}
isFloating(): boolean {
return this.style.float !== FLOAT.NONE;
}
isRootElement(): boolean {
return this.parent === null;
}
isTransformed(): boolean {
return this.style.transform !== null;
}
isPositionedWithZIndex(): boolean {
return this.isPositioned() && !this.style.zIndex.auto;
}
isInlineLevel(): boolean {
return (
contains(this.style.display, DISPLAY.INLINE) ||
contains(this.style.display, DISPLAY.INLINE_BLOCK) ||
contains(this.style.display, DISPLAY.INLINE_FLEX) ||
contains(this.style.display, DISPLAY.INLINE_GRID) ||
contains(this.style.display, DISPLAY.INLINE_LIST_ITEM) ||
contains(this.style.display, DISPLAY.INLINE_TABLE)
);
}
isInlineBlockOrInlineTable(): boolean {
return (
contains(this.style.display, DISPLAY.INLINE_BLOCK) ||
contains(this.style.display, DISPLAY.INLINE_TABLE)
);
}
}
const getImage = (node: HTMLElement | SVGSVGElement, resourceLoader: ResourceLoader): ?string => {
if (
node instanceof node.ownerDocument.defaultView.SVGSVGElement ||
node instanceof SVGSVGElement
) {
const s = new XMLSerializer();
return resourceLoader.loadImage(
`data:image/svg+xml,${encodeURIComponent(s.serializeToString(node))}`
);
}
switch (node.tagName) {
case 'IMG':
// $FlowFixMe
const img: HTMLImageElement = node;
return resourceLoader.loadImage(img.currentSrc || img.src);
case 'CANVAS':
// $FlowFixMe
const canvas: HTMLCanvasElement = node;
return resourceLoader.loadCanvas(canvas);
case 'IFRAME':
const iframeKey = node.getAttribute('data-html2canvas-internal-iframe-key');
if (iframeKey) {
return iframeKey;
}
break;
}
return null;
};

View File

@ -1,158 +0,0 @@
/* @flow */
'use strict';
import type ResourceLoader, {ImageElement} from './ResourceLoader';
import type Logger from './Logger';
import StackingContext from './StackingContext';
import NodeContainer from './NodeContainer';
import TextContainer from './TextContainer';
import {inlineInputElement, inlineTextAreaElement, inlineSelectElement} from './Input';
export const NodeParser = (
node: HTMLElement,
resourceLoader: ResourceLoader,
logger: Logger
): StackingContext => {
if (__DEV__) {
logger.log(`Starting node parsing`);
}
let index = 0;
const container = new NodeContainer(node, null, resourceLoader, index++);
const stack = new StackingContext(container, null, true);
parseNodeTree(node, container, stack, resourceLoader, index);
if (__DEV__) {
logger.log(`Finished parsing node tree`);
}
return stack;
};
const IGNORED_NODE_NAMES = ['SCRIPT', 'HEAD', 'TITLE', 'OBJECT', 'BR', 'OPTION'];
const parseNodeTree = (
node: HTMLElement,
parent: NodeContainer,
stack: StackingContext,
resourceLoader: ResourceLoader,
index: number
): void => {
if (__DEV__ && index > 50000) {
throw new Error(`Recursion error while parsing node tree`);
}
for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) {
nextNode = childNode.nextSibling;
const defaultView = childNode.ownerDocument.defaultView;
if (
childNode instanceof defaultView.Text ||
childNode instanceof Text ||
(defaultView.parent && childNode instanceof defaultView.parent.Text)
) {
if (childNode.data.trim().length > 0) {
parent.childNodes.push(TextContainer.fromTextNode(childNode, parent));
}
} else if (
childNode instanceof defaultView.HTMLElement ||
childNode instanceof HTMLElement ||
(defaultView.parent && childNode instanceof defaultView.parent.HTMLElement)
) {
if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) {
const container = new NodeContainer(childNode, parent, resourceLoader, index++);
if (container.isVisible()) {
if (childNode.tagName === 'INPUT') {
// $FlowFixMe
inlineInputElement(childNode, container);
} else if (childNode.tagName === 'TEXTAREA') {
// $FlowFixMe
inlineTextAreaElement(childNode, container);
} else if (childNode.tagName === 'SELECT') {
// $FlowFixMe
inlineSelectElement(childNode, container);
}
const SHOULD_TRAVERSE_CHILDREN = childNode.tagName !== 'TEXTAREA';
const treatAsRealStackingContext = createsRealStackingContext(
container,
childNode
);
if (treatAsRealStackingContext || createsStackingContext(container)) {
// for treatAsRealStackingContext:false, any positioned descendants and descendants
// which actually create a new stacking context should be considered part of the parent stacking context
const parentStack =
treatAsRealStackingContext || container.isPositioned()
? stack.getRealParentStackingContext()
: stack;
const childStack = new StackingContext(
container,
parentStack,
treatAsRealStackingContext
);
parentStack.contexts.push(childStack);
if (SHOULD_TRAVERSE_CHILDREN) {
parseNodeTree(childNode, container, childStack, resourceLoader, index);
}
} else {
stack.children.push(container);
if (SHOULD_TRAVERSE_CHILDREN) {
parseNodeTree(childNode, container, stack, resourceLoader, index);
}
}
}
}
} else if (
childNode instanceof defaultView.SVGSVGElement ||
childNode instanceof SVGSVGElement ||
(defaultView.parent && childNode instanceof defaultView.parent.SVGSVGElement)
) {
const container = new NodeContainer(childNode, parent, resourceLoader, index++);
const treatAsRealStackingContext = createsRealStackingContext(container, childNode);
if (treatAsRealStackingContext || createsStackingContext(container)) {
// for treatAsRealStackingContext:false, any positioned descendants and descendants
// which actually create a new stacking context should be considered part of the parent stacking context
const parentStack =
treatAsRealStackingContext || container.isPositioned()
? stack.getRealParentStackingContext()
: stack;
const childStack = new StackingContext(
container,
parentStack,
treatAsRealStackingContext
);
parentStack.contexts.push(childStack);
} else {
stack.children.push(container);
}
}
}
};
const createsRealStackingContext = (
container: NodeContainer,
node: HTMLElement | SVGSVGElement
): boolean => {
return (
container.isRootElement() ||
container.isPositionedWithZIndex() ||
container.style.opacity < 1 ||
container.isTransformed() ||
isBodyWithTransparentRoot(container, node)
);
};
const createsStackingContext = (container: NodeContainer): boolean => {
return container.isPositioned() || container.isFloating();
};
const isBodyWithTransparentRoot = (
container: NodeContainer,
node: HTMLElement | SVGSVGElement
): boolean => {
return (
node.nodeName === 'BODY' &&
container.parent instanceof NodeContainer &&
container.parent.style.background.backgroundColor.isTransparent()
);
};

View File

@ -1,62 +0,0 @@
/* @flow */
'use strict';
import type Options from './index';
import FEATURES from './Feature';
export const Proxy = (src: string, options: Options): Promise<string> => {
if (!options.proxy) {
return Promise.reject(__DEV__ ? 'No proxy defined' : null);
}
const proxy = options.proxy;
return new Promise((resolve, reject) => {
const responseType =
FEATURES.SUPPORT_CORS_XHR && FEATURES.SUPPORT_RESPONSE_TYPE ? 'blob' : 'text';
const xhr = FEATURES.SUPPORT_CORS_XHR ? new XMLHttpRequest() : new XDomainRequest();
xhr.onload = () => {
if (xhr instanceof XMLHttpRequest) {
if (xhr.status === 200) {
if (responseType === 'text') {
resolve(xhr.response);
} else {
const reader = new FileReader();
// $FlowFixMe
reader.addEventListener('load', () => resolve(reader.result), false);
// $FlowFixMe
reader.addEventListener('error', e => reject(e), false);
reader.readAsDataURL(xhr.response);
}
} else {
reject(
__DEV__
? `Failed to proxy resource ${src.substring(
0,
256
)} with status code ${xhr.status}`
: ''
);
}
} else {
resolve(xhr.responseText);
}
};
xhr.onerror = reject;
xhr.open('GET', `${proxy}?url=${encodeURIComponent(src)}&responseType=${responseType}`);
if (responseType !== 'text' && xhr instanceof XMLHttpRequest) {
xhr.responseType = responseType;
}
if (options.imageTimeout) {
const timeout = options.imageTimeout;
xhr.timeout = timeout;
xhr.ontimeout = () =>
reject(__DEV__ ? `Timed out (${timeout}ms) proxying ${src.substring(0, 256)}` : '');
}
xhr.send();
});
};

View File

@ -1,413 +0,0 @@
/* @flow */
'use strict';
import type Color from './Color';
import type {Path} from './drawing/Path';
import type Size from './drawing/Size';
import type Logger from './Logger';
import type {BackgroundImage} from './parsing/background';
import type {Border, BorderSide} from './parsing/border';
import type {Font} from './parsing/font';
import type {TextDecoration} from './parsing/textDecoration';
import type {TextShadow} from './parsing/textShadow';
import type {Matrix} from './parsing/transform';
import type {BoundCurves} from './Bounds';
import type {Gradient} from './Gradient';
import type {ResourceStore, ImageElement} from './ResourceLoader';
import type NodeContainer from './NodeContainer';
import type StackingContext from './StackingContext';
import type {TextBounds} from './TextBounds';
import {
Bounds,
parsePathForBorder,
calculateContentBox,
calculatePaddingBox,
calculatePaddingBoxPath
} from './Bounds';
import {FontMetrics} from './Font';
import {parseGradient} from './Gradient';
import TextContainer from './TextContainer';
import {
BACKGROUND_ORIGIN,
calculateBackgroungPaintingArea,
calculateBackgroundPosition,
calculateBackgroundRepeatPath,
calculateBackgroundSize
} from './parsing/background';
import {BORDER_STYLE} from './parsing/border';
export type RenderOptions = {
scale: number,
backgroundColor: ?Color,
imageStore: ResourceStore,
fontMetrics: FontMetrics,
logger: Logger,
x: number,
y: number,
width: number,
height: number
};
export interface RenderTarget<Output> {
clip(clipPaths: Array<Path>, callback: () => void): void,
drawImage(image: ImageElement, source: Bounds, destination: Bounds): void,
drawShape(path: Path, color: Color): void,
fill(color: Color): void,
getTarget(): Promise<Output>,
rectangle(x: number, y: number, width: number, height: number, color: Color): void,
render(options: RenderOptions): void,
renderLinearGradient(bounds: Bounds, gradient: Gradient): void,
renderRepeat(
path: Path,
image: ImageElement,
imageSize: Size,
offsetX: number,
offsetY: number
): void,
renderTextNode(
textBounds: Array<TextBounds>,
color: Color,
font: Font,
textDecoration: TextDecoration | null,
textShadows: Array<TextShadow> | null
): void,
setOpacity(opacity: number): void,
transform(offsetX: number, offsetY: number, matrix: Matrix, callback: () => void): void
}
export default class Renderer {
target: RenderTarget<*>;
options: RenderOptions;
_opacity: ?number;
constructor(target: RenderTarget<*>, options: RenderOptions) {
this.target = target;
this.options = options;
target.render(options);
}
renderNode(container: NodeContainer) {
if (container.isVisible()) {
this.renderNodeBackgroundAndBorders(container);
this.renderNodeContent(container);
}
}
renderNodeContent(container: NodeContainer) {
const callback = () => {
if (container.childNodes.length) {
container.childNodes.forEach(child => {
if (child instanceof TextContainer) {
const style = child.parent.style;
this.target.renderTextNode(
child.bounds,
style.color,
style.font,
style.textDecoration,
style.textShadow
);
} else {
this.target.drawShape(child, container.style.color);
}
});
}
if (container.image) {
const image = this.options.imageStore.get(container.image);
if (image) {
const contentBox = calculateContentBox(
container.bounds,
container.style.padding,
container.style.border
);
const width =
typeof image.width === 'number' && image.width > 0
? image.width
: contentBox.width;
const height =
typeof image.height === 'number' && image.height > 0
? image.height
: contentBox.height;
if (width > 0 && height > 0) {
this.target.clip([calculatePaddingBoxPath(container.curvedBounds)], () => {
this.target.drawImage(
image,
new Bounds(0, 0, width, height),
contentBox
);
});
}
}
}
};
const paths = container.getClipPaths();
if (paths.length) {
this.target.clip(paths, callback);
} else {
callback();
}
}
renderNodeBackgroundAndBorders(container: NodeContainer) {
const HAS_BACKGROUND =
!container.style.background.backgroundColor.isTransparent() ||
container.style.background.backgroundImage.length;
const renderableBorders = container.style.border.filter(
border =>
border.borderStyle !== BORDER_STYLE.NONE && !border.borderColor.isTransparent()
);
const callback = () => {
const backgroundPaintingArea = calculateBackgroungPaintingArea(
container.curvedBounds,
container.style.background.backgroundClip
);
if (HAS_BACKGROUND) {
this.target.clip([backgroundPaintingArea], () => {
if (!container.style.background.backgroundColor.isTransparent()) {
this.target.fill(container.style.background.backgroundColor);
}
this.renderBackgroundImage(container);
});
}
renderableBorders.forEach((border, side) => {
this.renderBorder(border, side, container.curvedBounds);
});
};
if (HAS_BACKGROUND || renderableBorders.length) {
const paths = container.parent ? container.parent.getClipPaths() : [];
if (paths.length) {
this.target.clip(paths, callback);
} else {
callback();
}
}
}
renderBackgroundImage(container: NodeContainer) {
container.style.background.backgroundImage.slice(0).reverse().forEach(backgroundImage => {
if (backgroundImage.source.method === 'url' && backgroundImage.source.args.length) {
this.renderBackgroundRepeat(container, backgroundImage);
} else {
const gradient = parseGradient(backgroundImage.source, container.bounds);
if (gradient) {
const bounds = container.bounds;
this.target.renderLinearGradient(bounds, gradient);
}
}
});
}
renderBackgroundRepeat(container: NodeContainer, background: BackgroundImage) {
const image = this.options.imageStore.get(background.source.args[0]);
if (image) {
const bounds = container.bounds;
const paddingBox = calculatePaddingBox(bounds, container.style.border);
const backgroundImageSize = calculateBackgroundSize(background, image, bounds);
// TODO support CONTENT_BOX
const backgroundPositioningArea =
container.style.background.backgroundOrigin === BACKGROUND_ORIGIN.BORDER_BOX
? bounds
: paddingBox;
const position = calculateBackgroundPosition(
background.position,
backgroundImageSize,
backgroundPositioningArea
);
const path = calculateBackgroundRepeatPath(
background,
position,
backgroundImageSize,
backgroundPositioningArea,
bounds
);
const offsetX = Math.round(paddingBox.left + position.x);
const offsetY = Math.round(paddingBox.top + position.y);
this.target.renderRepeat(path, image, backgroundImageSize, offsetX, offsetY);
}
}
renderBorder(border: Border, side: BorderSide, curvePoints: BoundCurves) {
this.target.drawShape(parsePathForBorder(curvePoints, side), border.borderColor);
}
renderStack(stack: StackingContext) {
if (stack.container.isVisible()) {
const opacity = stack.getOpacity();
if (opacity !== this._opacity) {
this.target.setOpacity(stack.getOpacity());
this._opacity = opacity;
}
const transform = stack.container.style.transform;
if (transform !== null) {
this.target.transform(
stack.container.bounds.left + transform.transformOrigin[0].value,
stack.container.bounds.top + transform.transformOrigin[1].value,
transform.transform,
() => this.renderStackContent(stack)
);
} else {
this.renderStackContent(stack);
}
}
}
renderStackContent(stack: StackingContext) {
const [
negativeZIndex,
zeroOrAutoZIndexOrTransformedOrOpacity,
positiveZIndex,
nonPositionedFloats,
nonPositionedInlineLevel
] = splitStackingContexts(stack);
const [inlineLevel, nonInlineLevel] = splitDescendants(stack);
// https://www.w3.org/TR/css-position-3/#painting-order
// 1. the background and borders of the element forming the stacking context.
this.renderNodeBackgroundAndBorders(stack.container);
// 2. the child stacking contexts with negative stack levels (most negative first).
negativeZIndex.sort(sortByZIndex).forEach(this.renderStack, this);
// 3. For all its in-flow, non-positioned, block-level descendants in tree order:
this.renderNodeContent(stack.container);
nonInlineLevel.forEach(this.renderNode, this);
// 4. All non-positioned floating descendants, in tree order. For each one of these,
// treat the element as if it created a new stacking context, but any positioned descendants and descendants
// which actually create a new stacking context should be considered part of the parent stacking context,
// not this new one.
nonPositionedFloats.forEach(this.renderStack, this);
// 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
nonPositionedInlineLevel.forEach(this.renderStack, this);
inlineLevel.forEach(this.renderNode, this);
// 6. All positioned, opacity or transform descendants, in tree order that fall into the following categories:
// All positioned descendants with 'z-index: auto' or 'z-index: 0', in tree order.
// For those with 'z-index: auto', treat the element as if it created a new stacking context,
// but any positioned descendants and descendants which actually create a new stacking context should be
// considered part of the parent stacking context, not this new one. For those with 'z-index: 0',
// treat the stacking context generated atomically.
//
// All opacity descendants with opacity less than 1
//
// All transform descendants with transform other than none
zeroOrAutoZIndexOrTransformedOrOpacity.forEach(this.renderStack, this);
// 7. Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index
// order (smallest first) then tree order.
positiveZIndex.sort(sortByZIndex).forEach(this.renderStack, this);
}
render(stack: StackingContext): Promise<*> {
if (this.options.backgroundColor) {
this.target.rectangle(
0,
0,
this.options.width,
this.options.height,
this.options.backgroundColor
);
}
this.renderStack(stack);
const target = this.target.getTarget();
if (__DEV__) {
return target.then(output => {
this.options.logger.log(`Render completed`);
return output;
});
}
return target;
}
}
const splitDescendants = (stack: StackingContext): [Array<NodeContainer>, Array<NodeContainer>] => {
const inlineLevel = [];
const nonInlineLevel = [];
const length = stack.children.length;
for (let i = 0; i < length; i++) {
let child = stack.children[i];
if (child.isInlineLevel()) {
inlineLevel.push(child);
} else {
nonInlineLevel.push(child);
}
}
return [inlineLevel, nonInlineLevel];
};
const splitStackingContexts = (
stack: StackingContext
): [
Array<StackingContext>,
Array<StackingContext>,
Array<StackingContext>,
Array<StackingContext>,
Array<StackingContext>
] => {
const negativeZIndex = [];
const zeroOrAutoZIndexOrTransformedOrOpacity = [];
const positiveZIndex = [];
const nonPositionedFloats = [];
const nonPositionedInlineLevel = [];
const length = stack.contexts.length;
for (let i = 0; i < length; i++) {
let child = stack.contexts[i];
if (
child.container.isPositioned() ||
child.container.style.opacity < 1 ||
child.container.isTransformed()
) {
if (child.container.style.zIndex.order < 0) {
negativeZIndex.push(child);
} else if (child.container.style.zIndex.order > 0) {
positiveZIndex.push(child);
} else {
zeroOrAutoZIndexOrTransformedOrOpacity.push(child);
}
} else {
if (child.container.isFloating()) {
nonPositionedFloats.push(child);
} else {
nonPositionedInlineLevel.push(child);
}
}
}
return [
negativeZIndex,
zeroOrAutoZIndexOrTransformedOrOpacity,
positiveZIndex,
nonPositionedFloats,
nonPositionedInlineLevel
];
};
const sortByZIndex = (a: StackingContext, b: StackingContext): number => {
if (a.container.style.zIndex.order > b.container.style.zIndex.order) {
return 1;
} else if (a.container.style.zIndex.order < b.container.style.zIndex.order) {
return -1;
}
return a.container.index > b.container.index ? 1 : -1;
};

View File

@ -1,249 +0,0 @@
/* @flow */
'use strict';
import type Options from './index';
import type Logger from './Logger';
export type ImageElement = Image | HTMLCanvasElement;
export type Resource = ImageElement;
type ResourceCache = {[string]: Promise<Resource>};
import FEATURES from './Feature';
import {Proxy} from './Proxy';
export default class ResourceLoader {
origin: string;
options: Options;
_link: HTMLAnchorElement;
cache: ResourceCache;
logger: Logger;
_index: number;
_window: WindowProxy;
constructor(options: Options, logger: Logger, window: WindowProxy) {
this.options = options;
this._window = window;
this.origin = this.getOrigin(window.location.href);
this.cache = {};
this.logger = logger;
this._index = 0;
}
loadImage(src: string): ?string {
if (this.hasResourceInCache(src)) {
return src;
}
if (isSVG(src)) {
if (this.options.allowTaint === true || FEATURES.SUPPORT_SVG_DRAWING) {
return this.addImage(src, src, false);
}
} else {
if (
this.options.allowTaint === true ||
isInlineBase64Image(src) ||
this.isSameOrigin(src)
) {
return this.addImage(src, src, false);
} else if (!this.isSameOrigin(src)) {
if (typeof this.options.proxy === 'string') {
this.cache[src] = Proxy(src, this.options).then(src =>
loadImage(src, this.options.imageTimeout || 0)
);
return src;
} else if (this.options.useCORS === true && FEATURES.SUPPORT_CORS_IMAGES) {
return this.addImage(src, src, true);
}
}
}
}
inlineImage(src: string): Promise<Resource> {
if (isInlineImage(src)) {
return loadImage(src, this.options.imageTimeout || 0);
}
if (this.hasResourceInCache(src)) {
return this.cache[src];
}
if (!this.isSameOrigin(src) && typeof this.options.proxy === 'string') {
return (this.cache[src] = Proxy(src, this.options).then(src =>
loadImage(src, this.options.imageTimeout || 0)
));
}
return this.xhrImage(src);
}
xhrImage(src: string): Promise<Resource> {
this.cache[src] = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status !== 200) {
reject(
`Failed to fetch image ${src.substring(
0,
256
)} with status code ${xhr.status}`
);
} else {
const reader = new FileReader();
reader.addEventListener(
'load',
() => {
// $FlowFixMe
const result: string = reader.result;
resolve(result);
},
false
);
reader.addEventListener('error', (e: Event) => reject(e), false);
reader.readAsDataURL(xhr.response);
}
}
};
xhr.responseType = 'blob';
if (this.options.imageTimeout) {
const timeout = this.options.imageTimeout;
xhr.timeout = timeout;
xhr.ontimeout = () =>
reject(
__DEV__ ? `Timed out (${timeout}ms) fetching ${src.substring(0, 256)}` : ''
);
}
xhr.open('GET', src, true);
xhr.send();
}).then(src => loadImage(src, this.options.imageTimeout || 0));
return this.cache[src];
}
loadCanvas(node: HTMLCanvasElement): string {
const key = String(this._index++);
this.cache[key] = Promise.resolve(node);
return key;
}
hasResourceInCache(key: string): boolean {
return typeof this.cache[key] !== 'undefined';
}
addImage(key: string, src: string, useCORS: boolean): string {
if (__DEV__) {
this.logger.log(`Added image ${key.substring(0, 256)}`);
}
const imageLoadHandler = (supportsDataImages: boolean): Promise<Image> =>
new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
//ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
if (!supportsDataImages || useCORS) {
img.crossOrigin = 'anonymous';
}
img.onerror = reject;
img.src = src;
if (img.complete === true) {
// Inline XML images may fail to parse, throwing an Error later on
setTimeout(() => {
resolve(img);
}, 500);
}
if (this.options.imageTimeout) {
const timeout = this.options.imageTimeout;
setTimeout(
() =>
reject(
__DEV__
? `Timed out (${timeout}ms) fetching ${src.substring(0, 256)}`
: ''
),
timeout
);
}
});
this.cache[key] =
isInlineBase64Image(src) && !isSVG(src)
? // $FlowFixMe
FEATURES.SUPPORT_BASE64_DRAWING(src).then(imageLoadHandler)
: imageLoadHandler(true);
return key;
}
isSameOrigin(url: string): boolean {
return this.getOrigin(url) === this.origin;
}
getOrigin(url: string): string {
const link = this._link || (this._link = this._window.document.createElement('a'));
link.href = url;
link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/
return link.protocol + link.hostname + link.port;
}
ready(): Promise<ResourceStore> {
const keys: Array<string> = Object.keys(this.cache);
const values: Array<Promise<?Resource>> = keys.map(str =>
this.cache[str].catch(e => {
if (__DEV__) {
this.logger.log(`Unable to load image`, e);
}
return null;
})
);
return Promise.all(values).then((images: Array<?Resource>) => {
if (__DEV__) {
this.logger.log(`Finished loading ${images.length} images`, images);
}
return new ResourceStore(keys, images);
});
}
}
export class ResourceStore {
_keys: Array<string>;
_resources: Array<?Resource>;
constructor(keys: Array<string>, resources: Array<?Resource>) {
this._keys = keys;
this._resources = resources;
}
get(key: string): ?Resource {
const index = this._keys.indexOf(key);
return index === -1 ? null : this._resources[index];
}
}
const INLINE_SVG = /^data:image\/svg\+xml/i;
const INLINE_BASE64 = /^data:image\/.*;base64,/i;
const INLINE_IMG = /^data:image\/.*/i;
const isInlineImage = (src: string): boolean => INLINE_IMG.test(src);
const isInlineBase64Image = (src: string): boolean => INLINE_BASE64.test(src);
const isSVG = (src: string): boolean =>
src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src);
const loadImage = (src: string, timeout: number): Promise<Image> => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
if (img.complete === true) {
// Inline XML images may fail to parse, throwing an Error later on
setTimeout(() => {
resolve(img);
}, 500);
}
if (timeout) {
setTimeout(
() => reject(__DEV__ ? `Timed out (${timeout}ms) loading image` : ''),
timeout
);
}
});
};

View File

@ -1,37 +0,0 @@
/* @flow */
'use strict';
import NodeContainer from './NodeContainer';
import {POSITION} from './parsing/position';
export default class StackingContext {
container: NodeContainer;
parent: ?StackingContext;
contexts: Array<StackingContext>;
children: Array<NodeContainer>;
treatAsRealStackingContext: boolean;
constructor(
container: NodeContainer,
parent: ?StackingContext,
treatAsRealStackingContext: boolean
) {
this.container = container;
this.parent = parent;
this.contexts = [];
this.children = [];
this.treatAsRealStackingContext = treatAsRealStackingContext;
}
getOpacity(): number {
return this.parent
? this.container.style.opacity * this.parent.getOpacity()
: this.container.style.opacity;
}
getRealParentStackingContext(): StackingContext {
return !this.parent || this.treatAsRealStackingContext
? this
: this.parent.getRealParentStackingContext();
}
}

View File

@ -1,129 +0,0 @@
/* @flow */
'use strict';
import {ucs2} from 'punycode';
import type NodeContainer from './NodeContainer';
import {Bounds, parseBounds} from './Bounds';
import {TEXT_DECORATION} from './parsing/textDecoration';
import FEATURES from './Feature';
const UNICODE = /[^\u0000-\u00ff]/;
const hasUnicodeCharacters = (text: string): boolean => UNICODE.test(text);
const encodeCodePoint = (codePoint: number): string => ucs2.encode([codePoint]);
export class TextBounds {
text: string;
bounds: Bounds;
constructor(text: string, bounds: Bounds) {
this.text = text;
this.bounds = bounds;
}
}
export const parseTextBounds = (
value: string,
parent: NodeContainer,
node: Text
): Array<TextBounds> => {
const codePoints = ucs2.decode(value);
const letterRendering = parent.style.letterSpacing !== 0 || hasUnicodeCharacters(value);
const textList = letterRendering ? codePoints.map(encodeCodePoint) : splitWords(codePoints);
const length = textList.length;
const defaultView = node.parentNode ? node.parentNode.ownerDocument.defaultView : null;
const scrollX = defaultView ? defaultView.pageXOffset : 0;
const scrollY = defaultView ? defaultView.pageYOffset : 0;
const textBounds = [];
let offset = 0;
for (let i = 0; i < length; i++) {
let text = textList[i];
if (parent.style.textDecoration !== TEXT_DECORATION.NONE || text.trim().length > 0) {
if (FEATURES.SUPPORT_RANGE_BOUNDS) {
textBounds.push(
new TextBounds(
text,
getRangeBounds(node, offset, text.length, scrollX, scrollY)
)
);
} else {
const replacementNode = node.splitText(text.length);
textBounds.push(new TextBounds(text, getWrapperBounds(node, scrollX, scrollY)));
node = replacementNode;
}
} else if (!FEATURES.SUPPORT_RANGE_BOUNDS) {
node = node.splitText(text.length);
}
offset += text.length;
}
return textBounds;
};
const getWrapperBounds = (node: Text, scrollX: number, scrollY: number): Bounds => {
const wrapper = node.ownerDocument.createElement('html2canvaswrapper');
wrapper.appendChild(node.cloneNode(true));
const parentNode = node.parentNode;
if (parentNode) {
parentNode.replaceChild(wrapper, node);
const bounds = parseBounds(wrapper, scrollX, scrollY);
if (wrapper.firstChild) {
parentNode.replaceChild(wrapper.firstChild, wrapper);
}
return bounds;
}
return new Bounds(0, 0, 0, 0);
};
const getRangeBounds = (
node: Text,
offset: number,
length: number,
scrollX: number,
scrollY: number
): Bounds => {
const range = node.ownerDocument.createRange();
range.setStart(node, offset);
range.setEnd(node, offset + length);
return Bounds.fromClientRect(range.getBoundingClientRect(), scrollX, scrollY);
};
const splitWords = (codePoints: Array<number>): Array<string> => {
const words = [];
let i = 0;
let onWordBoundary = false;
let word;
while (codePoints.length) {
if (isWordBoundary(codePoints[i]) === onWordBoundary) {
word = codePoints.splice(0, i);
if (word.length) {
words.push(ucs2.encode(word));
}
onWordBoundary = !onWordBoundary;
i = 0;
} else {
i++;
}
if (i >= codePoints.length) {
word = codePoints.splice(0, i);
if (word.length) {
words.push(ucs2.encode(word));
}
}
}
return words;
};
const isWordBoundary = (characterCode: number): boolean => {
return (
[
32, // <space>
13, // \r
10, // \n
9, // \t
45 // -
].indexOf(characterCode) !== -1
);
};

View File

@ -1,48 +0,0 @@
/* @flow */
'use strict';
import type NodeContainer from './NodeContainer';
import type {TextTransform} from './parsing/textTransform';
import type {TextBounds} from './TextBounds';
import {TEXT_TRANSFORM} from './parsing/textTransform';
import {parseTextBounds} from './TextBounds';
export default class TextContainer {
text: string;
parent: NodeContainer;
bounds: Array<TextBounds>;
constructor(text: string, parent: NodeContainer, bounds: Array<TextBounds>) {
this.text = text;
this.parent = parent;
this.bounds = bounds;
}
static fromTextNode(node: Text, parent: NodeContainer) {
const text = transform(node.data, parent.style.textTransform);
return new TextContainer(text, parent, parseTextBounds(text, parent, node));
}
}
const CAPITALIZE = /(^|\s|:|-|\(|\))([a-z])/g;
const transform = (text: string, transform: TextTransform) => {
switch (transform) {
case TEXT_TRANSFORM.LOWERCASE:
return text.toLowerCase();
case TEXT_TRANSFORM.CAPITALIZE:
return text.replace(CAPITALIZE, capitalize);
case TEXT_TRANSFORM.UPPERCASE:
return text.toUpperCase();
default:
return text;
}
};
function capitalize(m, p1, p2) {
if (m.length > 0) {
return p1 + p2.toUpperCase();
}
return m;
}

View File

@ -1,19 +0,0 @@
/* @flow */
'use strict';
export const contains = (bit: number, value: number): boolean => (bit & value) !== 0;
export const copyCSSStyles = (style: CSSStyleDeclaration, target: HTMLElement): HTMLElement => {
// Edge does not provide value for cssText
for (let i = style.length - 1; i >= 0; i--) {
const property = style.item(i);
// Safari shows pseudoelements if content is set
if (property !== 'content') {
target.style.setProperty(property, style.getPropertyValue(property));
}
}
return target;
};
export const SMALL_IMAGE =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';

View File

@ -1,141 +0,0 @@
/* @flow */
'use strict';
import type {Options} from './index';
import Logger from './Logger';
import {NodeParser} from './NodeParser';
import Renderer from './Renderer';
import ForeignObjectRenderer from './renderer/ForeignObjectRenderer';
import Feature from './Feature';
import {Bounds} from './Bounds';
import {cloneWindow, DocumentCloner} from './Clone';
import {FontMetrics} from './Font';
import Color, {TRANSPARENT} from './Color';
export const renderElement = (
element: HTMLElement,
options: Options,
logger: Logger
): Promise<*> => {
const ownerDocument = element.ownerDocument;
const windowBounds = new Bounds(
options.scrollX,
options.scrollY,
options.windowWidth,
options.windowHeight
);
// http://www.w3.org/TR/css3-background/#special-backgrounds
const documentBackgroundColor = ownerDocument.documentElement
? new Color(getComputedStyle(ownerDocument.documentElement).backgroundColor)
: TRANSPARENT;
const bodyBackgroundColor = ownerDocument.body
? new Color(getComputedStyle(ownerDocument.body).backgroundColor)
: TRANSPARENT;
const backgroundColor =
element === ownerDocument.documentElement
? documentBackgroundColor.isTransparent()
? bodyBackgroundColor.isTransparent()
? options.backgroundColor ? new Color(options.backgroundColor) : null
: bodyBackgroundColor
: documentBackgroundColor
: options.backgroundColor ? new Color(options.backgroundColor) : null;
return (options.foreignObjectRendering
? // $FlowFixMe
Feature.SUPPORT_FOREIGNOBJECT_DRAWING
: Promise.resolve(false)).then(
supportForeignObject =>
supportForeignObject
? (cloner => {
if (__DEV__) {
logger.log(`Document cloned, using foreignObject rendering`);
}
return cloner
.inlineFonts(ownerDocument)
.then(() => cloner.resourceLoader.ready())
.then(() => {
const renderer = new ForeignObjectRenderer(cloner.documentElement);
return renderer.render({
backgroundColor,
logger,
scale: options.scale,
x: options.x,
y: options.y,
width: options.width,
height: options.height,
windowWidth: options.windowWidth,
windowHeight: options.windowHeight,
scrollX: options.scrollX,
scrollY: options.scrollY
});
});
})(new DocumentCloner(element, options, logger, true, renderElement))
: cloneWindow(
ownerDocument,
windowBounds,
element,
options,
logger,
renderElement
).then(([container, clonedElement, resourceLoader]) => {
if (__DEV__) {
logger.log(`Document cloned, using computed rendering`);
}
const stack = NodeParser(clonedElement, resourceLoader, logger);
const clonedDocument = clonedElement.ownerDocument;
if (backgroundColor === stack.container.style.background.backgroundColor) {
stack.container.style.background.backgroundColor = TRANSPARENT;
}
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);
if (__DEV__) {
logger.log(`Starting renderer`);
}
const renderOptions = {
backgroundColor,
fontMetrics,
imageStore,
logger,
scale: options.scale,
x: options.x,
y: options.y,
width: options.width,
height: options.height
};
if (Array.isArray(options.target)) {
return Promise.all(
options.target.map(target => {
const renderer = new Renderer(target, renderOptions);
return renderer.render(stack);
})
);
} else {
const renderer = new Renderer(options.target, renderOptions);
return renderer.render(stack);
}
});
})
);
};

View File

@ -0,0 +1,225 @@
import {deepStrictEqual, fail} from 'assert';
import {FEATURES} from '../features';
import {createMockContext, proxy} from './mock-context';
const images: ImageMock[] = [];
const xhr: XMLHttpRequestMock[] = [];
const sleep = async (timeout: number) => await new Promise(resolve => setTimeout(resolve, timeout));
class ImageMock {
src?: string;
crossOrigin?: string;
onload?: () => {};
constructor() {
images.push(this);
}
}
class XMLHttpRequestMock {
sent: boolean;
status: number;
timeout: number;
method?: string;
url?: string;
response?: string;
onload?: () => {};
ontimeout?: () => {};
constructor() {
this.sent = false;
this.status = 500;
this.timeout = 5000;
xhr.push(this);
}
async load(status: number, response: string) {
this.response = response;
this.status = status;
if (this.onload) {
this.onload();
}
await sleep(0);
}
open(method: string, url: string) {
this.method = method;
this.url = url;
}
send() {
this.sent = true;
}
}
Object.defineProperty(global, 'Image', {value: ImageMock, writable: true});
Object.defineProperty(global, 'XMLHttpRequest', {
value: XMLHttpRequestMock,
writable: true
});
const setFeatures = (opts: {[key: string]: boolean} = {}) => {
const defaults: {[key: string]: boolean} = {
SUPPORT_SVG_DRAWING: true,
SUPPORT_CORS_IMAGES: true,
SUPPORT_CORS_XHR: true,
SUPPORT_RESPONSE_TYPE: false
};
Object.keys(defaults).forEach(key => {
Object.defineProperty(FEATURES, key, {
value: typeof opts[key] === 'boolean' ? opts[key] : defaults[key],
writable: true
});
});
};
describe('cache-storage', () => {
beforeEach(() => setFeatures());
afterEach(() => {
xhr.splice(0, xhr.length);
images.splice(0, images.length);
});
it('addImage adds images to cache', async () => {
const cache = createMockContext('http://example.com', {proxy: null});
await cache.addImage('http://example.com/test.jpg');
await cache.addImage('http://example.com/test2.jpg');
deepStrictEqual(images.length, 2);
deepStrictEqual(images[0].src, 'http://example.com/test.jpg');
deepStrictEqual(images[1].src, 'http://example.com/test2.jpg');
});
it('addImage should not add duplicate entries', async () => {
const cache = createMockContext('http://example.com');
await cache.addImage('http://example.com/test.jpg');
await cache.addImage('http://example.com/test.jpg');
deepStrictEqual(images.length, 1);
deepStrictEqual(images[0].src, 'http://example.com/test.jpg');
});
describe('svg', () => {
it('should add svg images correctly', async () => {
const cache = createMockContext('http://example.com');
await cache.addImage('http://example.com/test.svg');
await cache.addImage('http://example.com/test2.svg');
deepStrictEqual(images.length, 2);
deepStrictEqual(images[0].src, 'http://example.com/test.svg');
deepStrictEqual(images[1].src, 'http://example.com/test2.svg');
});
it('should omit svg images if not supported', async () => {
setFeatures({SUPPORT_SVG_DRAWING: false});
const cache = createMockContext('http://example.com');
await cache.addImage('http://example.com/test.svg');
await cache.addImage('http://example.com/test2.svg');
deepStrictEqual(images.length, 0);
});
});
describe('cross-origin', () => {
it('addImage should not add images it cannot load/render', async () => {
const cache = createMockContext('http://example.com', {
proxy: undefined
});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 0);
});
it('addImage should add images if tainting enabled', async () => {
const cache = createMockContext('http://example.com', {
allowTaint: true,
proxy: undefined
});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 1);
deepStrictEqual(images[0].src, 'http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images[0].crossOrigin, undefined);
});
it('addImage should add images if cors enabled', async () => {
const cache = createMockContext('http://example.com', {useCORS: true});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 1);
deepStrictEqual(images[0].src, 'http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images[0].crossOrigin, 'anonymous');
});
it('addImage should not add images if cors enabled but not supported', async () => {
setFeatures({SUPPORT_CORS_IMAGES: false});
const cache = createMockContext('http://example.com', {
useCORS: true,
proxy: undefined
});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 0);
});
it('addImage should not add images to proxy if cors enabled', async () => {
const cache = createMockContext('http://example.com', {useCORS: true});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 1);
deepStrictEqual(images[0].src, 'http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images[0].crossOrigin, 'anonymous');
});
it('addImage should use proxy ', async () => {
const cache = createMockContext('http://example.com');
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(xhr.length, 1);
deepStrictEqual(
xhr[0].url,
`${proxy}?url=${encodeURIComponent('http://html2canvas.hertzen.com/test.jpg')}&responseType=text`
);
await xhr[0].load(200, '<data response>');
deepStrictEqual(images.length, 1);
deepStrictEqual(images[0].src, '<data response>');
});
it('proxy should respect imageTimeout', async () => {
const cache = createMockContext('http://example.com', {
imageTimeout: 10
});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(xhr.length, 1);
deepStrictEqual(
xhr[0].url,
`${proxy}?url=${encodeURIComponent('http://html2canvas.hertzen.com/test.jpg')}&responseType=text`
);
deepStrictEqual(xhr[0].timeout, 10);
if (xhr[0].ontimeout) {
xhr[0].ontimeout();
}
try {
await cache.match('http://html2canvas.hertzen.com/test.jpg');
fail('Expected result to timeout');
} catch (e) {}
});
});
it('match should return cache entry', async () => {
const cache = createMockContext('http://example.com');
await cache.addImage('http://example.com/test.jpg');
if (images[0].onload) {
images[0].onload();
}
const response = await cache.match('http://example.com/test.jpg');
deepStrictEqual(response.src, 'http://example.com/test.jpg');
});
it('image should respect imageTimeout', async () => {
const cache = createMockContext('http://example.com', {imageTimeout: 10});
cache.addImage('http://example.com/test.jpg');
try {
await cache.match('http://example.com/test.jpg');
fail('Expected result to timeout');
} catch (e) {}
});
});

View File

@ -0,0 +1,45 @@
import {CacheStorage} from '../cache-storage';
import {URL} from 'url';
import {Logger} from '../logger';
export const proxy = 'http://example.com/proxy';
export const createMockContext = (origin: string, opts = {}) => {
const context = {
location: {
href: origin
},
document: {
createElement(_name: string) {
let _href = '';
return {
set href(value: string) {
_href = value;
},
get href() {
return _href;
},
get protocol() {
return new URL(_href).protocol;
},
get hostname() {
return new URL(_href).hostname;
},
get port() {
return new URL(_href).port;
}
};
}
}
};
CacheStorage.setContext(context as Window);
Logger.create('test');
return CacheStorage.create('test', {
imageTimeout: 0,
useCORS: false,
allowTaint: false,
proxy,
...opts
});
};

1
src/core/bitwise.ts Normal file
View File

@ -0,0 +1 @@
export const contains = (bit: number, value: number): boolean => (bit & value) !== 0;

207
src/core/cache-storage.ts Normal file
View File

@ -0,0 +1,207 @@
import {FEATURES} from './features';
import {Logger} from './logger';
export class CacheStorage {
private static _caches: {[key: string]: Cache} = {};
private static _link?: HTMLAnchorElement;
private static _origin: string = 'about:blank';
private static _current: Cache | null = null;
static create(name: string, options: ResourceOptions): Cache {
return (CacheStorage._caches[name] = new Cache(name, options));
}
static destroy(name: string): void {
delete CacheStorage._caches[name];
}
static open(name: string): Cache {
const cache = CacheStorage._caches[name];
if (typeof cache !== 'undefined') {
return cache;
}
throw new Error(`Cache with key "${name}" not found`);
}
static getOrigin(url: string): string {
const link = CacheStorage._link;
if (!link) {
return 'about:blank';
}
link.href = url;
link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/
return link.protocol + link.hostname + link.port;
}
static isSameOrigin(src: string): boolean {
return CacheStorage.getOrigin(src) === CacheStorage._origin;
}
static setContext(window: Window) {
CacheStorage._link = window.document.createElement('a');
CacheStorage._origin = CacheStorage.getOrigin(window.location.href);
}
static getInstance(): Cache {
const current = CacheStorage._current;
if (current === null) {
throw new Error(`No cache instance attached`);
}
return current;
}
static attachInstance(cache: Cache) {
CacheStorage._current = cache;
}
static detachInstance() {
CacheStorage._current = null;
}
}
interface ResourceOptions {
imageTimeout: number;
useCORS: boolean;
allowTaint: boolean;
proxy?: string;
}
export class Cache {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private readonly _cache: {[key: string]: Promise<any>};
private readonly _options: ResourceOptions;
private readonly id: string;
constructor(id: string, options: ResourceOptions) {
this.id = id;
this._options = options;
this._cache = {};
}
addImage(src: string): Promise<void> {
const result = Promise.resolve();
if (this.has(src)) {
return result;
}
if (isBlobImage(src) || isRenderable(src)) {
this._cache[src] = this.loadImage(src);
return result;
}
return result;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
match(src: string): Promise<any> {
return this._cache[src];
}
private async loadImage(key: string) {
const isSameOrigin = CacheStorage.isSameOrigin(key);
const useCORS =
!isInlineImage(key) && this._options.useCORS === true && FEATURES.SUPPORT_CORS_IMAGES && !isSameOrigin;
const useProxy =
!isInlineImage(key) &&
!isSameOrigin &&
typeof this._options.proxy === 'string' &&
FEATURES.SUPPORT_CORS_XHR &&
!useCORS;
if (!isSameOrigin && this._options.allowTaint === false && !isInlineImage(key) && !useProxy && !useCORS) {
return;
}
let src = key;
if (useProxy) {
src = await this.proxy(src);
}
Logger.getInstance(this.id).debug(`Added image ${key.substring(0, 256)}`);
return await new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
//ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
if (isInlineBase64Image(src) || useCORS) {
img.crossOrigin = 'anonymous';
}
img.src = src;
if (img.complete === true) {
// Inline XML images may fail to parse, throwing an Error later on
setTimeout(() => resolve(img), 500);
}
if (this._options.imageTimeout > 0) {
setTimeout(
() => reject(`Timed out (${this._options.imageTimeout}ms) loading image`),
this._options.imageTimeout
);
}
});
}
private has(key: string): boolean {
return typeof this._cache[key] !== 'undefined';
}
keys(): Promise<string[]> {
return Promise.resolve(Object.keys(this._cache));
}
private proxy(src: string): Promise<string> {
const proxy = this._options.proxy;
if (!proxy) {
throw new Error('No proxy defined');
}
const key = src.substring(0, 256);
return new Promise((resolve, reject) => {
const responseType = FEATURES.SUPPORT_RESPONSE_TYPE ? 'blob' : 'text';
const xhr = new XMLHttpRequest();
xhr.onload = () => {
if (xhr.status === 200) {
if (responseType === 'text') {
resolve(xhr.response);
} else {
const reader = new FileReader();
reader.addEventListener('load', () => resolve(reader.result as string), false);
reader.addEventListener('error', e => reject(e), false);
reader.readAsDataURL(xhr.response);
}
} else {
reject(`Failed to proxy resource ${key} with status code ${xhr.status}`);
}
};
xhr.onerror = reject;
xhr.open('GET', `${proxy}?url=${encodeURIComponent(src)}&responseType=${responseType}`);
if (responseType !== 'text' && xhr instanceof XMLHttpRequest) {
xhr.responseType = responseType;
}
if (this._options.imageTimeout) {
const timeout = this._options.imageTimeout;
xhr.timeout = timeout;
xhr.ontimeout = () => reject(`Timed out (${timeout}ms) proxying ${key}`);
}
xhr.send();
});
}
}
const INLINE_SVG = /^data:image\/svg\+xml/i;
const INLINE_BASE64 = /^data:image\/.*;base64,/i;
const INLINE_IMG = /^data:image\/.*/i;
const isRenderable = (src: string): boolean => FEATURES.SUPPORT_SVG_DRAWING || !isSVG(src);
const isInlineImage = (src: string): boolean => INLINE_IMG.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 => src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src);

View File

@ -1,9 +1,4 @@
/* @flow */
'use strict';
import {createForeignObjectSVG, loadSerializedSVG} from './renderer/ForeignObjectRenderer';
const testRangeBounds = document => {
const testRangeBounds = (document: Document) => {
const TEST_HEIGHT = 123;
if (document.createRange) {
@ -27,46 +22,18 @@ const testRangeBounds = document => {
return false;
};
// iOS 10.3 taints canvas with base64 images unless crossOrigin = 'anonymous'
const testBase64 = (document: Document, src: string): Promise<boolean> => {
const testCORS = (): boolean => typeof new Image().crossOrigin !== 'undefined';
const testResponseType = (): boolean => typeof new XMLHttpRequest().responseType === 'string';
const testSVG = (document: Document): boolean => {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
return false;
}
return new Promise(resolve => {
// Single pixel base64 image renders fine on iOS 10.3???
img.src = src;
const onload = () => {
try {
ctx.drawImage(img, 0, 0);
canvas.toDataURL();
} catch (e) {
return resolve(false);
}
return resolve(true);
};
img.onload = onload;
img.onerror = () => resolve(false);
if (img.complete === true) {
setTimeout(() => {
onload();
}, 500);
}
});
};
const testCORS = () => typeof new Image().crossOrigin !== 'undefined';
const testResponseType = () => typeof new XMLHttpRequest().responseType === 'string';
const testSVG = document => {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
img.src = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'></svg>`;
try {
@ -78,14 +45,18 @@ const testSVG = document => {
return true;
};
const isGreenPixel = data => data[0] === 0 && data[1] === 255 && data[2] === 0 && data[3] === 255;
const isGreenPixel = (data: Uint8ClampedArray): boolean =>
data[0] === 0 && data[1] === 255 && data[2] === 0 && data[3] === 255;
const testForeignObject = document => {
const testForeignObject = (document: Document): Promise<boolean> => {
const canvas = document.createElement('canvas');
const size = 100;
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
if (!ctx) {
return Promise.reject(false);
}
ctx.fillStyle = 'rgb(0, 255, 0)';
ctx.fillRect(0, 0, size, size);
@ -97,7 +68,7 @@ const testForeignObject = document => {
ctx.fillRect(0, 0, size, size);
return loadSerializedSVG(svg)
.then(img => {
.then((img: HTMLImageElement) => {
ctx.drawImage(img, 0, 0);
const data = ctx.getImageData(0, 0, size, size).data;
ctx.fillStyle = 'red';
@ -111,39 +82,56 @@ const testForeignObject = document => {
? loadSerializedSVG(createForeignObjectSVG(size, size, 0, 0, node))
: Promise.reject(false);
})
.then(img => {
.then((img: HTMLImageElement) => {
ctx.drawImage(img, 0, 0);
// Edge does not render background-images
return isGreenPixel(ctx.getImageData(0, 0, size, size).data);
})
.catch(e => false);
.catch(() => false);
};
const FEATURES = {
// $FlowFixMe - get/set properties not yet supported
export const createForeignObjectSVG = (width: number, height: number, x: number, y: number, node: Node) => {
const xmlns = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(xmlns, 'svg');
const foreignObject = document.createElementNS(xmlns, 'foreignObject');
svg.setAttributeNS(null, 'width', width.toString());
svg.setAttributeNS(null, 'height', height.toString());
foreignObject.setAttributeNS(null, 'width', '100%');
foreignObject.setAttributeNS(null, 'height', '100%');
foreignObject.setAttributeNS(null, 'x', x.toString());
foreignObject.setAttributeNS(null, 'y', y.toString());
foreignObject.setAttributeNS(null, 'externalResourcesRequired', 'true');
svg.appendChild(foreignObject);
foreignObject.appendChild(node);
return svg;
};
export const loadSerializedSVG = (svg: Node): Promise<HTMLImageElement> => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
});
};
export const FEATURES = {
get SUPPORT_RANGE_BOUNDS() {
'use strict';
const value = testRangeBounds(document);
Object.defineProperty(FEATURES, 'SUPPORT_RANGE_BOUNDS', {value});
return value;
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_SVG_DRAWING() {
'use strict';
const value = testSVG(document);
Object.defineProperty(FEATURES, 'SUPPORT_SVG_DRAWING', {value});
return value;
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_BASE64_DRAWING() {
'use strict';
return (src: string) => {
const value = testBase64(document, src);
Object.defineProperty(FEATURES, 'SUPPORT_BASE64_DRAWING', {value: () => value});
return value;
};
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_FOREIGNOBJECT_DRAWING() {
'use strict';
const value =
@ -153,21 +141,18 @@ const FEATURES = {
Object.defineProperty(FEATURES, 'SUPPORT_FOREIGNOBJECT_DRAWING', {value});
return value;
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_CORS_IMAGES() {
'use strict';
const value = testCORS();
Object.defineProperty(FEATURES, 'SUPPORT_CORS_IMAGES', {value});
return value;
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_RESPONSE_TYPE() {
'use strict';
const value = testResponseType();
Object.defineProperty(FEATURES, 'SUPPORT_RESPONSE_TYPE', {value});
return value;
},
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_CORS_XHR() {
'use strict';
const value = 'withCredentials' in new XMLHttpRequest();
@ -175,5 +160,3 @@ const FEATURES = {
return value;
}
};
export default FEATURES;

62
src/core/logger.ts Normal file
View File

@ -0,0 +1,62 @@
export class Logger {
static instances: {[key: string]: Logger} = {};
private readonly id: string;
private readonly start: number;
constructor(id: string) {
this.id = id;
this.start = Date.now();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
debug(...args: any) {
// eslint-disable-next-line no-console
if (typeof window !== 'undefined' && window.console && typeof console.debug === 'function') {
// eslint-disable-next-line no-console
console.debug(this.id, `${this.getTime()}ms`, ...args);
} else {
this.info(...args);
}
}
getTime(): number {
return Date.now() - this.start;
}
static create(id: string) {
Logger.instances[id] = new Logger(id);
}
static destroy(id: string) {
delete Logger.instances[id];
}
static getInstance(id: string): Logger {
const instance = Logger.instances[id];
if (typeof instance === 'undefined') {
throw new Error(`No logger instance found with id ${id}`);
}
return instance;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
info(...args: any) {
// eslint-disable-next-line no-console
if (typeof window !== 'undefined' && window.console && typeof console.info === 'function') {
// eslint-disable-next-line no-console
console.info(this.id, `${this.getTime()}ms`, ...args);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error(...args: any) {
// eslint-disable-next-line no-console
if (typeof window !== 'undefined' && window.console && typeof console.error === 'function') {
// eslint-disable-next-line no-console
console.error(this.id, `${this.getTime()}ms`, ...args);
} else {
this.info(...args);
}
}
}

1
src/core/util.ts Normal file
View File

@ -0,0 +1 @@
export const SMALL_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';

View File

@ -0,0 +1,48 @@
import {CSSValue} from './syntax/parser';
import {CSSTypes} from './types/index';
export enum PropertyDescriptorParsingType {
VALUE,
LIST,
IDENT_VALUE,
TYPE_VALUE,
TOKEN_VALUE
}
export interface IPropertyDescriptor {
name: string;
type: PropertyDescriptorParsingType;
initialValue: string;
prefix: boolean;
}
export interface IPropertyIdentValueDescriptor<T> extends IPropertyDescriptor {
type: PropertyDescriptorParsingType.IDENT_VALUE;
parse: (token: string) => T;
}
export interface IPropertyTypeValueDescriptor extends IPropertyDescriptor {
type: PropertyDescriptorParsingType.TYPE_VALUE;
format: CSSTypes;
}
export interface IPropertyValueDescriptor<T> extends IPropertyDescriptor {
type: PropertyDescriptorParsingType.VALUE;
parse: (token: CSSValue) => T;
}
export interface IPropertyListDescriptor<T> extends IPropertyDescriptor {
type: PropertyDescriptorParsingType.LIST;
parse: (tokens: CSSValue[]) => T;
}
export interface IPropertyTokenValueDescriptor extends IPropertyDescriptor {
type: PropertyDescriptorParsingType.TOKEN_VALUE;
}
export type CSSPropertyDescriptor<T> =
| IPropertyValueDescriptor<T>
| IPropertyListDescriptor<T>
| IPropertyIdentValueDescriptor<T>
| IPropertyTypeValueDescriptor
| IPropertyTokenValueDescriptor;

View File

@ -0,0 +1,6 @@
import {CSSValue} from './syntax/parser';
export interface ITypeDescriptor<T> {
name: string;
parse: (value: CSSValue) => T;
}

295
src/css/index.ts Normal file
View File

@ -0,0 +1,295 @@
import {CSSPropertyDescriptor, PropertyDescriptorParsingType} from './IPropertyDescriptor';
import {backgroundClip} from './property-descriptors/background-clip';
import {backgroundColor} from './property-descriptors/background-color';
import {backgroundImage} from './property-descriptors/background-image';
import {backgroundOrigin} from './property-descriptors/background-origin';
import {backgroundPosition} from './property-descriptors/background-position';
import {backgroundRepeat} from './property-descriptors/background-repeat';
import {backgroundSize} from './property-descriptors/background-size';
import {
borderBottomColor,
borderLeftColor,
borderRightColor,
borderTopColor
} from './property-descriptors/border-color';
import {
borderBottomLeftRadius,
borderBottomRightRadius,
borderTopLeftRadius,
borderTopRightRadius
} from './property-descriptors/border-radius';
import {
borderBottomStyle,
borderLeftStyle,
borderRightStyle,
borderTopStyle
} from './property-descriptors/border-style';
import {
borderBottomWidth,
borderLeftWidth,
borderRightWidth,
borderTopWidth
} from './property-descriptors/border-width';
import {color} from './property-descriptors/color';
import {display, DISPLAY} from './property-descriptors/display';
import {float, FLOAT} from './property-descriptors/float';
import {letterSpacing} from './property-descriptors/letter-spacing';
import {lineBreak} from './property-descriptors/line-break';
import {lineHeight} from './property-descriptors/line-height';
import {listStyleImage} from './property-descriptors/list-style-image';
import {listStylePosition} from './property-descriptors/list-style-position';
import {listStyleType} from './property-descriptors/list-style-type';
import {marginBottom, marginLeft, marginRight, marginTop} from './property-descriptors/margin';
import {overflow, OVERFLOW} from './property-descriptors/overflow';
import {overflowWrap} from './property-descriptors/overflow-wrap';
import {paddingBottom, paddingLeft, paddingRight, paddingTop} from './property-descriptors/padding';
import {textAlign} from './property-descriptors/text-align';
import {position, POSITION} from './property-descriptors/position';
import {textShadow} from './property-descriptors/text-shadow';
import {textTransform} from './property-descriptors/text-transform';
import {transform} from './property-descriptors/transform';
import {transformOrigin} from './property-descriptors/transform-origin';
import {visibility, VISIBILITY} from './property-descriptors/visibility';
import {wordBreak} from './property-descriptors/word-break';
import {zIndex} from './property-descriptors/z-index';
import {CSSValue, isIdentToken, Parser} from './syntax/parser';
import {Tokenizer} from './syntax/tokenizer';
import {Color, color as colorType, isTransparent} from './types/color';
import {angle} from './types/angle';
import {image} from './types/image';
import {opacity} from './property-descriptors/opacity';
import {textDecorationColor} from './property-descriptors/text-decoration-color';
import {textDecorationLine} from './property-descriptors/text-decoration-line';
import {isLengthPercentage, LengthPercentage, ZERO_LENGTH} from './types/length-percentage';
import {fontFamily} from './property-descriptors/font-family';
import {fontSize} from './property-descriptors/font-size';
import {isLength} from './types/length';
import {fontWeight} from './property-descriptors/font-weight';
import {fontVariant} from './property-descriptors/font-variant';
import {fontStyle} from './property-descriptors/font-style';
import {contains} from '../core/bitwise';
import {content} from './property-descriptors/content';
import {counterIncrement} from './property-descriptors/counter-increment';
import {counterReset} from './property-descriptors/counter-reset';
import {quotes} from './property-descriptors/quotes';
import {boxShadow} from './property-descriptors/box-shadow';
export class CSSParsedDeclaration {
backgroundClip: ReturnType<typeof backgroundClip.parse>;
backgroundColor: Color;
backgroundImage: ReturnType<typeof backgroundImage.parse>;
backgroundOrigin: ReturnType<typeof backgroundOrigin.parse>;
backgroundPosition: ReturnType<typeof backgroundPosition.parse>;
backgroundRepeat: ReturnType<typeof backgroundRepeat.parse>;
backgroundSize: ReturnType<typeof backgroundSize.parse>;
borderTopColor: Color;
borderRightColor: Color;
borderBottomColor: Color;
borderLeftColor: Color;
borderTopLeftRadius: ReturnType<typeof borderTopLeftRadius.parse>;
borderTopRightRadius: ReturnType<typeof borderTopRightRadius.parse>;
borderBottomRightRadius: ReturnType<typeof borderBottomRightRadius.parse>;
borderBottomLeftRadius: ReturnType<typeof borderBottomLeftRadius.parse>;
borderTopStyle: ReturnType<typeof borderTopStyle.parse>;
borderRightStyle: ReturnType<typeof borderRightStyle.parse>;
borderBottomStyle: ReturnType<typeof borderBottomStyle.parse>;
borderLeftStyle: ReturnType<typeof borderLeftStyle.parse>;
borderTopWidth: ReturnType<typeof borderTopWidth.parse>;
borderRightWidth: ReturnType<typeof borderRightWidth.parse>;
borderBottomWidth: ReturnType<typeof borderBottomWidth.parse>;
borderLeftWidth: ReturnType<typeof borderLeftWidth.parse>;
boxShadow: ReturnType<typeof boxShadow.parse>;
color: Color;
display: ReturnType<typeof display.parse>;
float: ReturnType<typeof float.parse>;
fontFamily: ReturnType<typeof fontFamily.parse>;
fontSize: LengthPercentage;
fontStyle: ReturnType<typeof fontStyle.parse>;
fontVariant: ReturnType<typeof fontVariant.parse>;
fontWeight: ReturnType<typeof fontWeight.parse>;
letterSpacing: ReturnType<typeof letterSpacing.parse>;
lineBreak: ReturnType<typeof lineBreak.parse>;
lineHeight: CSSValue;
listStyleImage: ReturnType<typeof listStyleImage.parse>;
listStylePosition: ReturnType<typeof listStylePosition.parse>;
listStyleType: ReturnType<typeof listStyleType.parse>;
marginTop: CSSValue;
marginRight: CSSValue;
marginBottom: CSSValue;
marginLeft: CSSValue;
opacity: ReturnType<typeof opacity.parse>;
overflowX: OVERFLOW;
overflowY: OVERFLOW;
overflowWrap: ReturnType<typeof overflowWrap.parse>;
paddingTop: LengthPercentage;
paddingRight: LengthPercentage;
paddingBottom: LengthPercentage;
paddingLeft: LengthPercentage;
position: ReturnType<typeof position.parse>;
textAlign: ReturnType<typeof textAlign.parse>;
textDecorationColor: Color;
textDecorationLine: ReturnType<typeof textDecorationLine.parse>;
textShadow: ReturnType<typeof textShadow.parse>;
textTransform: ReturnType<typeof textTransform.parse>;
transform: ReturnType<typeof transform.parse>;
transformOrigin: ReturnType<typeof transformOrigin.parse>;
visibility: ReturnType<typeof visibility.parse>;
wordBreak: ReturnType<typeof wordBreak.parse>;
zIndex: ReturnType<typeof zIndex.parse>;
constructor(declaration: CSSStyleDeclaration) {
this.backgroundClip = parse(backgroundClip, declaration.backgroundClip);
this.backgroundColor = parse(backgroundColor, declaration.backgroundColor);
this.backgroundImage = parse(backgroundImage, declaration.backgroundImage);
this.backgroundOrigin = parse(backgroundOrigin, declaration.backgroundOrigin);
this.backgroundPosition = parse(backgroundPosition, declaration.backgroundPosition);
this.backgroundRepeat = parse(backgroundRepeat, declaration.backgroundRepeat);
this.backgroundSize = parse(backgroundSize, declaration.backgroundSize);
this.borderTopColor = parse(borderTopColor, declaration.borderTopColor);
this.borderRightColor = parse(borderRightColor, declaration.borderRightColor);
this.borderBottomColor = parse(borderBottomColor, declaration.borderBottomColor);
this.borderLeftColor = parse(borderLeftColor, declaration.borderLeftColor);
this.borderTopLeftRadius = parse(borderTopLeftRadius, declaration.borderTopLeftRadius);
this.borderTopRightRadius = parse(borderTopRightRadius, declaration.borderTopRightRadius);
this.borderBottomRightRadius = parse(borderBottomRightRadius, declaration.borderBottomRightRadius);
this.borderBottomLeftRadius = parse(borderBottomLeftRadius, declaration.borderBottomLeftRadius);
this.borderTopStyle = parse(borderTopStyle, declaration.borderTopStyle);
this.borderRightStyle = parse(borderRightStyle, declaration.borderRightStyle);
this.borderBottomStyle = parse(borderBottomStyle, declaration.borderBottomStyle);
this.borderLeftStyle = parse(borderLeftStyle, declaration.borderLeftStyle);
this.borderTopWidth = parse(borderTopWidth, declaration.borderTopWidth);
this.borderRightWidth = parse(borderRightWidth, declaration.borderRightWidth);
this.borderBottomWidth = parse(borderBottomWidth, declaration.borderBottomWidth);
this.borderLeftWidth = parse(borderLeftWidth, declaration.borderLeftWidth);
this.boxShadow = parse(boxShadow, declaration.boxShadow);
this.color = parse(color, declaration.color);
this.display = parse(display, declaration.display);
this.float = parse(float, declaration.cssFloat);
this.fontFamily = parse(fontFamily, declaration.fontFamily);
this.fontSize = parse(fontSize, declaration.fontSize);
this.fontStyle = parse(fontStyle, declaration.fontStyle);
this.fontVariant = parse(fontVariant, declaration.fontVariant);
this.fontWeight = parse(fontWeight, declaration.fontWeight);
this.letterSpacing = parse(letterSpacing, declaration.letterSpacing);
this.lineBreak = parse(lineBreak, declaration.lineBreak);
this.lineHeight = parse(lineHeight, declaration.lineHeight);
this.listStyleImage = parse(listStyleImage, declaration.listStyleImage);
this.listStylePosition = parse(listStylePosition, declaration.listStylePosition);
this.listStyleType = parse(listStyleType, declaration.listStyleType);
this.marginTop = parse(marginTop, declaration.marginTop);
this.marginRight = parse(marginRight, declaration.marginRight);
this.marginBottom = parse(marginBottom, declaration.marginBottom);
this.marginLeft = parse(marginLeft, declaration.marginLeft);
this.opacity = parse(opacity, declaration.opacity);
const overflowTuple = parse(overflow, declaration.overflow);
this.overflowX = overflowTuple[0];
this.overflowY = overflowTuple[overflowTuple.length > 1 ? 1 : 0];
this.overflowWrap = parse(overflowWrap, declaration.overflowWrap);
this.paddingTop = parse(paddingTop, declaration.paddingTop);
this.paddingRight = parse(paddingRight, declaration.paddingRight);
this.paddingBottom = parse(paddingBottom, declaration.paddingBottom);
this.paddingLeft = parse(paddingLeft, declaration.paddingLeft);
this.position = parse(position, declaration.position);
this.textAlign = parse(textAlign, declaration.textAlign);
this.textDecorationColor = parse(textDecorationColor, declaration.textDecorationColor || declaration.color);
this.textDecorationLine = parse(textDecorationLine, declaration.textDecorationLine);
this.textShadow = parse(textShadow, declaration.textShadow);
this.textTransform = parse(textTransform, declaration.textTransform);
this.transform = parse(transform, declaration.transform);
this.transformOrigin = parse(transformOrigin, declaration.transformOrigin);
this.visibility = parse(visibility, declaration.visibility);
this.wordBreak = parse(wordBreak, declaration.wordBreak);
this.zIndex = parse(zIndex, declaration.zIndex);
}
isVisible(): boolean {
return this.display > 0 && this.opacity > 0 && this.visibility === VISIBILITY.VISIBLE;
}
isTransparent(): boolean {
return isTransparent(this.backgroundColor);
}
isTransformed(): boolean {
return this.transform !== null;
}
isPositioned(): boolean {
return this.position !== POSITION.STATIC;
}
isPositionedWithZIndex(): boolean {
return this.isPositioned() && !this.zIndex.auto;
}
isFloating(): boolean {
return this.float !== FLOAT.NONE;
}
isInlineLevel(): boolean {
return (
contains(this.display, DISPLAY.INLINE) ||
contains(this.display, DISPLAY.INLINE_BLOCK) ||
contains(this.display, DISPLAY.INLINE_FLEX) ||
contains(this.display, DISPLAY.INLINE_GRID) ||
contains(this.display, DISPLAY.INLINE_LIST_ITEM) ||
contains(this.display, DISPLAY.INLINE_TABLE)
);
}
}
export class CSSParsedPseudoDeclaration {
content: ReturnType<typeof content.parse>;
quotes: ReturnType<typeof quotes.parse>;
constructor(declaration: CSSStyleDeclaration) {
this.content = parse(content, declaration.content);
this.quotes = parse(quotes, declaration.quotes);
}
}
export class CSSParsedCounterDeclaration {
counterIncrement: ReturnType<typeof counterIncrement.parse>;
counterReset: ReturnType<typeof counterReset.parse>;
constructor(declaration: CSSStyleDeclaration) {
this.counterIncrement = parse(counterIncrement, declaration.counterIncrement);
this.counterReset = parse(counterReset, declaration.counterReset);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parse = (descriptor: CSSPropertyDescriptor<any>, style?: string | null) => {
const tokenizer = new Tokenizer();
const value = style !== null && typeof style !== 'undefined' ? style.toString() : descriptor.initialValue;
tokenizer.write(value);
const parser = new Parser(tokenizer.read());
switch (descriptor.type) {
case PropertyDescriptorParsingType.IDENT_VALUE:
const token = parser.parseComponentValue();
return descriptor.parse(isIdentToken(token) ? token.value : descriptor.initialValue);
case PropertyDescriptorParsingType.VALUE:
return descriptor.parse(parser.parseComponentValue());
case PropertyDescriptorParsingType.LIST:
return descriptor.parse(parser.parseComponentValues());
case PropertyDescriptorParsingType.TOKEN_VALUE:
return parser.parseComponentValue();
case PropertyDescriptorParsingType.TYPE_VALUE:
switch (descriptor.format) {
case 'angle':
return angle.parse(parser.parseComponentValue());
case 'color':
return colorType.parse(parser.parseComponentValue());
case 'image':
return image.parse(parser.parseComponentValue());
case 'length':
const length = parser.parseComponentValue();
return isLength(length) ? length : ZERO_LENGTH;
case 'length-percentage':
const value = parser.parseComponentValue();
return isLengthPercentage(value) ? value : ZERO_LENGTH;
}
}
throw new Error(`Attempting to parse unsupported css format type ${descriptor.format}`);
};

47
src/css/layout/bounds.ts Normal file
View File

@ -0,0 +1,47 @@
export class Bounds {
readonly top: number;
readonly left: number;
readonly width: number;
readonly height: number;
constructor(x: number, y: number, w: number, h: number) {
this.left = x;
this.top = y;
this.width = w;
this.height = h;
}
add(x: number, y: number, w: number, h: number): Bounds {
return new Bounds(this.left + x, this.top + y, this.width + w, this.height + h);
}
static fromClientRect(clientRect: ClientRect): Bounds {
return new Bounds(clientRect.left, clientRect.top, clientRect.width, clientRect.height);
}
}
export const parseBounds = (node: Element): Bounds => {
return Bounds.fromClientRect(node.getBoundingClientRect());
};
export const parseDocumentSize = (document: Document): Bounds => {
const body = document.body;
const documentElement = document.documentElement;
if (!body || !documentElement) {
throw new Error(`Unable to get document size`);
}
const width = Math.max(
Math.max(body.scrollWidth, documentElement.scrollWidth),
Math.max(body.offsetWidth, documentElement.offsetWidth),
Math.max(body.clientWidth, documentElement.clientWidth)
);
const height = Math.max(
Math.max(body.scrollHeight, documentElement.scrollHeight),
Math.max(body.offsetHeight, documentElement.offsetHeight),
Math.max(body.clientHeight, documentElement.clientHeight)
);
return new Bounds(0, 0, width, height);
};

89
src/css/layout/text.ts Normal file
View File

@ -0,0 +1,89 @@
import {OVERFLOW_WRAP} from '../property-descriptors/overflow-wrap';
import {CSSParsedDeclaration} from '../index';
import {fromCodePoint, LineBreaker, toCodePoints} from 'css-line-break';
import {Bounds, parseBounds} from './bounds';
import {FEATURES} from '../../core/features';
export class TextBounds {
readonly text: string;
readonly bounds: Bounds;
constructor(text: string, bounds: Bounds) {
this.text = text;
this.bounds = bounds;
}
}
export const parseTextBounds = (value: string, styles: CSSParsedDeclaration, node: Text): TextBounds[] => {
const textList = breakText(value, styles);
const textBounds: TextBounds[] = [];
let offset = 0;
textList.forEach(text => {
if (styles.textDecorationLine.length || text.trim().length > 0) {
if (FEATURES.SUPPORT_RANGE_BOUNDS) {
textBounds.push(new TextBounds(text, getRangeBounds(node, offset, text.length)));
} else {
const replacementNode = node.splitText(text.length);
textBounds.push(new TextBounds(text, getWrapperBounds(node)));
node = replacementNode;
}
} else if (!FEATURES.SUPPORT_RANGE_BOUNDS) {
node = node.splitText(text.length);
}
offset += text.length;
});
return textBounds;
};
const getWrapperBounds = (node: Text): Bounds => {
const ownerDocument = node.ownerDocument;
if (ownerDocument) {
const wrapper = ownerDocument.createElement('html2canvaswrapper');
wrapper.appendChild(node.cloneNode(true));
const parentNode = node.parentNode;
if (parentNode) {
parentNode.replaceChild(wrapper, node);
const bounds = parseBounds(wrapper);
if (wrapper.firstChild) {
parentNode.replaceChild(wrapper.firstChild, wrapper);
}
return bounds;
}
}
return new Bounds(0, 0, 0, 0);
};
const getRangeBounds = (node: Text, offset: number, length: number): Bounds => {
const ownerDocument = node.ownerDocument;
if (!ownerDocument) {
throw new Error('Node has no owner document');
}
const range = ownerDocument.createRange();
range.setStart(node, offset);
range.setEnd(node, offset + length);
return Bounds.fromClientRect(range.getBoundingClientRect());
};
const breakText = (value: string, styles: CSSParsedDeclaration): string[] => {
return styles.letterSpacing !== 0 ? toCodePoints(value).map(i => fromCodePoint(i)) : breakWords(value, styles);
};
const breakWords = (str: string, styles: CSSParsedDeclaration): string[] => {
const breaker = LineBreaker(str, {
lineBreak: styles.lineBreak,
wordBreak: styles.overflowWrap === OVERFLOW_WRAP.BREAK_WORD ? 'break-word' : styles.wordBreak
});
const words = [];
let bk;
while (!(bk = breaker.next()).done) {
if (bk.value) {
words.push(bk.value.slice());
}
}
return words;
};

View File

@ -0,0 +1,43 @@
import {deepStrictEqual} from 'assert';
import {Parser} from '../../syntax/parser';
import {backgroundImage} from '../background-image';
import {CSSImageType} from '../../types/image';
import {pack} from '../../types/color';
import {deg} from '../../types/angle';
import {createMockContext} from '../../../core/__tests__/mock-context';
import {CacheStorage} from '../../../core/cache-storage';
const backgroundImageParse = (value: string) => backgroundImage.parse(Parser.parseValues(value));
describe('property-descriptors', () => {
before(() => {
CacheStorage.attachInstance(createMockContext('http://example.com'));
});
describe('background-image', () => {
it('none', () => deepStrictEqual(backgroundImageParse('none'), []));
it('url(test.jpg), url(test2.jpg)', () =>
deepStrictEqual(
backgroundImageParse('url(http://example.com/test.jpg), url(http://example.com/test2.jpg)'),
[
{url: 'http://example.com/test.jpg', type: CSSImageType.URL},
{url: 'http://example.com/test2.jpg', type: CSSImageType.URL}
]
));
it(`linear-gradient(to bottom, rgba(255,255,0,0.5), rgba(0,0,255,0.5)), url('https://html2canvas.hertzen.com')`, () =>
deepStrictEqual(
backgroundImageParse(
`linear-gradient(to bottom, rgba(255,255,0,0.5), rgba(0,0,255,0.5)), url('https://html2canvas.hertzen.com')`
),
[
{
angle: deg(180),
type: CSSImageType.LINEAR_GRADIENT,
stops: [{color: pack(255, 255, 0, 0.5), stop: null}, {color: pack(0, 0, 255, 0.5), stop: null}]
},
{url: 'https://html2canvas.hertzen.com', type: CSSImageType.URL}
]
));
});
});

View File

@ -0,0 +1,93 @@
import {deepStrictEqual} from 'assert';
import {Parser} from '../../syntax/parser';
import {color, COLORS} from '../../types/color';
import {textShadow} from '../text-shadow';
import {FLAG_INTEGER, DimensionToken, TokenType} from '../../syntax/tokenizer';
import {ZERO_LENGTH} from '../../types/length-percentage';
const textShadowParse = (value: string) => textShadow.parse(Parser.parseValues(value));
const colorParse = (value: string) => color.parse(Parser.parseValue(value));
const dimension = (number: number, unit: string): DimensionToken => ({
flags: FLAG_INTEGER,
number,
unit,
type: TokenType.DIMENSION_TOKEN
});
describe('property-descriptors', () => {
describe('text-shadow', () => {
it('none', () => deepStrictEqual(textShadowParse('none'), []));
it('1px 1px 2px pink', () =>
deepStrictEqual(textShadowParse('1px 1px 2px pink'), [
{
color: colorParse('pink'),
offsetX: dimension(1, 'px'),
offsetY: dimension(1, 'px'),
blur: dimension(2, 'px')
}
]));
it('#fc0 1px 0 10px', () =>
deepStrictEqual(textShadowParse('#fc0 1px 0 10px'), [
{
color: colorParse('#fc0'),
offsetX: dimension(1, 'px'),
offsetY: ZERO_LENGTH,
blur: dimension(10, 'px')
}
]));
it('5px 5px #558abb', () =>
deepStrictEqual(textShadowParse('5px 5px #558abb'), [
{
color: colorParse('#558abb'),
offsetX: dimension(5, 'px'),
offsetY: dimension(5, 'px'),
blur: ZERO_LENGTH
}
]));
it('white 2px 5px', () =>
deepStrictEqual(textShadowParse('white 2px 5px'), [
{
color: colorParse('#fff'),
offsetX: dimension(2, 'px'),
offsetY: dimension(5, 'px'),
blur: ZERO_LENGTH
}
]));
it('white 2px 5px', () =>
deepStrictEqual(textShadowParse('5px 10px'), [
{
color: COLORS.TRANSPARENT,
offsetX: dimension(5, 'px'),
offsetY: dimension(10, 'px'),
blur: ZERO_LENGTH
}
]));
it('1px 1px 2px red, 0 0 1em blue, 0 0 2em blue', () =>
deepStrictEqual(textShadowParse('1px 1px 2px red, 0 0 1em blue, 0 0 2em blue'), [
{
color: colorParse('red'),
offsetX: dimension(1, 'px'),
offsetY: dimension(1, 'px'),
blur: dimension(2, 'px')
},
{
color: colorParse('blue'),
offsetX: ZERO_LENGTH,
offsetY: ZERO_LENGTH,
blur: dimension(1, 'em')
},
{
color: colorParse('blue'),
offsetX: ZERO_LENGTH,
offsetY: ZERO_LENGTH,
blur: dimension(2, 'em')
}
]));
});
});

View File

@ -0,0 +1,21 @@
import {transform} from '../transform';
import {Parser} from '../../syntax/parser';
import {deepStrictEqual} from 'assert';
const parseValue = (value: string) => transform.parse(Parser.parseValue(value));
describe('property-descriptors', () => {
describe('transform', () => {
it('none', () => deepStrictEqual(parseValue('none'), null));
it('matrix(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)', () =>
deepStrictEqual(parseValue('matrix(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)'), [1, 2, 3, 4, 5, 6]));
it('matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)', () =>
deepStrictEqual(parseValue('matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)'), [
1,
0,
0,
1,
0,
0
]));
});
});

View File

@ -0,0 +1,29 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken} from '../syntax/parser';
export enum BACKGROUND_CLIP {
BORDER_BOX = 0,
PADDING_BOX = 1,
CONTENT_BOX = 2
}
export type BackgroundClip = BACKGROUND_CLIP[];
export const backgroundClip: IPropertyListDescriptor<BackgroundClip> = {
name: 'background-clip',
initialValue: 'border-box',
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]): BackgroundClip => {
return tokens.map(token => {
if (isIdentToken(token)) {
switch (token.value) {
case 'padding-box':
return BACKGROUND_CLIP.PADDING_BOX;
case 'content-box':
return BACKGROUND_CLIP.CONTENT_BOX;
}
}
return BACKGROUND_CLIP.BORDER_BOX;
});
}
};

View File

@ -0,0 +1,9 @@
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export const backgroundColor: IPropertyTypeValueDescriptor = {
name: `background-color`,
initialValue: 'transparent',
prefix: false,
type: PropertyDescriptorParsingType.TYPE_VALUE,
format: 'color'
};

View File

@ -0,0 +1,24 @@
import {TokenType} from '../syntax/tokenizer';
import {ICSSImage, image} from '../types/image';
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, nonFunctionArgSeperator} from '../syntax/parser';
export const backgroundImage: IPropertyListDescriptor<ICSSImage[]> = {
name: 'background-image',
initialValue: 'none',
type: PropertyDescriptorParsingType.LIST,
prefix: false,
parse: (tokens: CSSValue[]) => {
if (tokens.length === 0) {
return [];
}
const first = tokens[0];
if (first.type === TokenType.IDENT_TOKEN && first.value === 'none') {
return [];
}
return tokens.filter(nonFunctionArgSeperator).map(image.parse);
}
};

View File

@ -0,0 +1,30 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken} from '../syntax/parser';
export const enum BACKGROUND_ORIGIN {
BORDER_BOX = 0,
PADDING_BOX = 1,
CONTENT_BOX = 2
}
export type BackgroundOrigin = BACKGROUND_ORIGIN[];
export const backgroundOrigin: IPropertyListDescriptor<BackgroundOrigin> = {
name: 'background-origin',
initialValue: 'border-box',
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]): BackgroundOrigin => {
return tokens.map(token => {
if (isIdentToken(token)) {
switch (token.value) {
case 'padding-box':
return BACKGROUND_ORIGIN.PADDING_BOX;
case 'content-box':
return BACKGROUND_ORIGIN.CONTENT_BOX;
}
}
return BACKGROUND_ORIGIN.BORDER_BOX;
});
}
};

View File

@ -0,0 +1,18 @@
import {PropertyDescriptorParsingType, IPropertyListDescriptor} from '../IPropertyDescriptor';
import {CSSValue, parseFunctionArgs} from '../syntax/parser';
import {isLengthPercentage, LengthPercentageTuple, parseLengthPercentageTuple} from '../types/length-percentage';
export type BackgroundPosition = BackgroundImagePosition[];
export type BackgroundImagePosition = LengthPercentageTuple;
export const backgroundPosition: IPropertyListDescriptor<BackgroundPosition> = {
name: 'background-position',
initialValue: '0% 0%',
type: PropertyDescriptorParsingType.LIST,
prefix: false,
parse: (tokens: CSSValue[]): BackgroundPosition => {
return parseFunctionArgs(tokens)
.map((values: CSSValue[]) => values.filter(isLengthPercentage))
.map(parseLengthPercentageTuple);
}
};

View File

@ -0,0 +1,43 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken, parseFunctionArgs} from '../syntax/parser';
export type BackgroundRepeat = BACKGROUND_REPEAT[];
export enum BACKGROUND_REPEAT {
REPEAT = 0,
NO_REPEAT = 1,
REPEAT_X = 2,
REPEAT_Y = 3
}
export const backgroundRepeat: IPropertyListDescriptor<BackgroundRepeat> = {
name: 'background-repeat',
initialValue: 'repeat',
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]): BackgroundRepeat => {
return parseFunctionArgs(tokens)
.map(values =>
values
.filter(isIdentToken)
.map(token => token.value)
.join(' ')
)
.map(parseBackgroundRepeat);
}
};
const parseBackgroundRepeat = (value: string): BACKGROUND_REPEAT => {
switch (value) {
case 'no-repeat':
return BACKGROUND_REPEAT.NO_REPEAT;
case 'repeat-x':
case 'repeat no-repeat':
return BACKGROUND_REPEAT.REPEAT_X;
case 'repeat-y':
case 'no-repeat repeat':
return BACKGROUND_REPEAT.REPEAT_Y;
case 'repeat':
default:
return BACKGROUND_REPEAT.REPEAT;
}
};

View File

@ -0,0 +1,26 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken, parseFunctionArgs} from '../syntax/parser';
import {isLengthPercentage, LengthPercentage} from '../types/length-percentage';
import {StringValueToken} from '../syntax/tokenizer';
export enum BACKGROUND_SIZE {
AUTO = 'auto',
CONTAIN = 'contain',
COVER = 'cover'
}
export type BackgroundSizeInfo = LengthPercentage | StringValueToken;
export type BackgroundSize = BackgroundSizeInfo[][];
export const backgroundSize: IPropertyListDescriptor<BackgroundSize> = {
name: 'background-size',
initialValue: '0',
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]): BackgroundSize => {
return parseFunctionArgs(tokens).map(values => values.filter(isBackgroundSizeInfoToken));
}
};
const isBackgroundSizeInfoToken = (value: CSSValue): value is BackgroundSizeInfo =>
isIdentToken(value) || isLengthPercentage(value);

View File

@ -0,0 +1,13 @@
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
const borderColorForSide = (side: string): IPropertyTypeValueDescriptor => ({
name: `border-${side}-color`,
initialValue: 'transparent',
prefix: false,
type: PropertyDescriptorParsingType.TYPE_VALUE,
format: 'color'
});
export const borderTopColor: IPropertyTypeValueDescriptor = borderColorForSide('top');
export const borderRightColor: IPropertyTypeValueDescriptor = borderColorForSide('right');
export const borderBottomColor: IPropertyTypeValueDescriptor = borderColorForSide('bottom');
export const borderLeftColor: IPropertyTypeValueDescriptor = borderColorForSide('left');

View File

@ -0,0 +1,17 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue} from '../syntax/parser';
import {isLengthPercentage, LengthPercentageTuple, parseLengthPercentageTuple} from '../types/length-percentage';
export type BorderRadius = LengthPercentageTuple;
const borderRadiusForSide = (side: string): IPropertyListDescriptor<BorderRadius> => ({
name: `border-radius-${side}`,
initialValue: '0 0',
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]): BorderRadius => parseLengthPercentageTuple(tokens.filter(isLengthPercentage))
});
export const borderTopLeftRadius: IPropertyListDescriptor<BorderRadius> = borderRadiusForSide('top-left');
export const borderTopRightRadius: IPropertyListDescriptor<BorderRadius> = borderRadiusForSide('top-right');
export const borderBottomRightRadius: IPropertyListDescriptor<BorderRadius> = borderRadiusForSide('bottom-right');
export const borderBottomLeftRadius: IPropertyListDescriptor<BorderRadius> = borderRadiusForSide('bottom-left');

View File

@ -0,0 +1,24 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum BORDER_STYLE {
NONE = 0,
SOLID = 1
}
const borderStyleForSide = (side: string): IPropertyIdentValueDescriptor<BORDER_STYLE> => ({
name: `border-${side}-style`,
initialValue: 'solid',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (style: string): BORDER_STYLE => {
switch (style) {
case 'none':
return BORDER_STYLE.NONE;
}
return BORDER_STYLE.SOLID;
}
});
export const borderTopStyle: IPropertyIdentValueDescriptor<BORDER_STYLE> = borderStyleForSide('top');
export const borderRightStyle: IPropertyIdentValueDescriptor<BORDER_STYLE> = borderStyleForSide('right');
export const borderBottomStyle: IPropertyIdentValueDescriptor<BORDER_STYLE> = borderStyleForSide('bottom');
export const borderLeftStyle: IPropertyIdentValueDescriptor<BORDER_STYLE> = borderStyleForSide('left');

View File

@ -0,0 +1,19 @@
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isDimensionToken} from '../syntax/parser';
const borderWidthForSide = (side: string): IPropertyValueDescriptor<number> => ({
name: `border-${side}-width`,
initialValue: '0',
type: PropertyDescriptorParsingType.VALUE,
prefix: false,
parse: (token: CSSValue): number => {
if (isDimensionToken(token)) {
return token.number;
}
return 0;
}
});
export const borderTopWidth: IPropertyValueDescriptor<number> = borderWidthForSide('top');
export const borderRightWidth: IPropertyValueDescriptor<number> = borderWidthForSide('right');
export const borderBottomWidth: IPropertyValueDescriptor<number> = borderWidthForSide('bottom');
export const borderLeftWidth: IPropertyValueDescriptor<number> = borderWidthForSide('left');

View File

@ -0,0 +1,59 @@
import {PropertyDescriptorParsingType, IPropertyListDescriptor} from '../IPropertyDescriptor';
import {CSSValue, isIdentWithValue, parseFunctionArgs} from '../syntax/parser';
import {ZERO_LENGTH} from '../types/length-percentage';
import {color, Color} from '../types/color';
import {isLength, Length} from '../types/length';
export type BoxShadow = BoxShadowItem[];
interface BoxShadowItem {
inset: boolean;
color: Color;
offsetX: Length;
offsetY: Length;
blur: Length;
spread: Length;
}
export const boxShadow: IPropertyListDescriptor<BoxShadow> = {
name: 'box-shadow',
initialValue: 'none',
type: PropertyDescriptorParsingType.LIST,
prefix: false,
parse: (tokens: CSSValue[]): BoxShadow => {
if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) {
return [];
}
return parseFunctionArgs(tokens).map((values: CSSValue[]) => {
const shadow: BoxShadowItem = {
color: 0x000000ff,
offsetX: ZERO_LENGTH,
offsetY: ZERO_LENGTH,
blur: ZERO_LENGTH,
spread: ZERO_LENGTH,
inset: false
};
let c = 0;
for (let i = 0; i < values.length; i++) {
const token = values[i];
if (isIdentWithValue(token, 'inset')) {
shadow.inset = true;
} else if (isLength(token)) {
if (c === 0) {
shadow.offsetX = token;
} else if (c === 1) {
shadow.offsetY = token;
} else if (c === 2) {
shadow.blur = token;
} else {
shadow.spread = token;
}
c++;
} else {
shadow.color = color.parse(token);
}
}
return shadow;
});
}
};

View File

@ -0,0 +1,9 @@
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export const color: IPropertyTypeValueDescriptor = {
name: `color`,
initialValue: 'transparent',
prefix: false,
type: PropertyDescriptorParsingType.TYPE_VALUE,
format: 'color'
};

View File

@ -0,0 +1,25 @@
import {TokenType} from '../syntax/tokenizer';
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue} from '../syntax/parser';
export type Content = CSSValue[];
export const content: IPropertyListDescriptor<Content> = {
name: 'content',
initialValue: 'none',
type: PropertyDescriptorParsingType.LIST,
prefix: false,
parse: (tokens: CSSValue[]) => {
if (tokens.length === 0) {
return [];
}
const first = tokens[0];
if (first.type === TokenType.IDENT_TOKEN && first.value === 'none') {
return [];
}
return tokens;
}
};

View File

@ -0,0 +1,42 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isNumberToken, nonWhiteSpace} from '../syntax/parser';
import {TokenType} from '../syntax/tokenizer';
export interface COUNTER_INCREMENT {
counter: string;
increment: number;
}
export type CounterIncrement = COUNTER_INCREMENT[] | null;
export const counterIncrement: IPropertyListDescriptor<CounterIncrement> = {
name: 'counter-increment',
initialValue: 'none',
prefix: true,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]) => {
if (tokens.length === 0) {
return null;
}
const first = tokens[0];
if (first.type === TokenType.IDENT_TOKEN && first.value === 'none') {
return null;
}
const increments = [];
const filtered = tokens.filter(nonWhiteSpace);
for (let i = 0; i < filtered.length; i++) {
const counter = filtered[i];
const next = filtered[i + 1];
if (counter.type === TokenType.IDENT_TOKEN) {
const increment = next && isNumberToken(next) ? next.number : 1;
increments.push({counter: counter.value, increment});
}
}
return increments;
}
};

View File

@ -0,0 +1,35 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken, isNumberToken, nonWhiteSpace} from '../syntax/parser';
export interface COUNTER_RESET {
counter: string;
reset: number;
}
export type CounterReset = COUNTER_RESET[];
export const counterReset: IPropertyListDescriptor<CounterReset> = {
name: 'counter-reset',
initialValue: 'none',
prefix: true,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]) => {
if (tokens.length === 0) {
return [];
}
const resets = [];
const filtered = tokens.filter(nonWhiteSpace);
for (let i = 0; i < filtered.length; i++) {
const counter = filtered[i];
const next = filtered[i + 1];
if (isIdentToken(counter) && counter.value !== 'none') {
const reset = next && isNumberToken(next) ? next.number : 0;
resets.push({counter: counter.value, reset});
}
}
return resets;
}
};

View File

@ -1,42 +1,52 @@
/* @flow */
'use strict';
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken} from '../syntax/parser';
export const enum DISPLAY {
NONE = 0,
BLOCK = 1 << 1,
INLINE = 1 << 2,
RUN_IN = 1 << 3,
FLOW = 1 << 4,
FLOW_ROOT = 1 << 5,
TABLE = 1 << 6,
FLEX = 1 << 7,
GRID = 1 << 8,
RUBY = 1 << 9,
SUBGRID = 1 << 10,
LIST_ITEM = 1 << 11,
TABLE_ROW_GROUP = 1 << 12,
TABLE_HEADER_GROUP = 1 << 13,
TABLE_FOOTER_GROUP = 1 << 14,
TABLE_ROW = 1 << 15,
TABLE_CELL = 1 << 16,
TABLE_COLUMN_GROUP = 1 << 17,
TABLE_COLUMN = 1 << 18,
TABLE_CAPTION = 1 << 19,
RUBY_BASE = 1 << 20,
RUBY_TEXT = 1 << 21,
RUBY_BASE_CONTAINER = 1 << 22,
RUBY_TEXT_CONTAINER = 1 << 23,
CONTENTS = 1 << 24,
INLINE_BLOCK = 1 << 25,
INLINE_LIST_ITEM = 1 << 26,
INLINE_TABLE = 1 << 27,
INLINE_FLEX = 1 << 28,
INLINE_GRID = 1 << 29
}
export const DISPLAY = {
NONE: 1 << 0,
BLOCK: 1 << 1,
INLINE: 1 << 2,
RUN_IN: 1 << 3,
FLOW: 1 << 4,
FLOW_ROOT: 1 << 5,
TABLE: 1 << 6,
FLEX: 1 << 7,
GRID: 1 << 8,
RUBY: 1 << 9,
SUBGRID: 1 << 10,
LIST_ITEM: 1 << 11,
TABLE_ROW_GROUP: 1 << 12,
TABLE_HEADER_GROUP: 1 << 13,
TABLE_FOOTER_GROUP: 1 << 14,
TABLE_ROW: 1 << 15,
TABLE_CELL: 1 << 16,
TABLE_COLUMN_GROUP: 1 << 17,
TABLE_COLUMN: 1 << 18,
TABLE_CAPTION: 1 << 19,
RUBY_BASE: 1 << 20,
RUBY_TEXT: 1 << 21,
RUBY_BASE_CONTAINER: 1 << 22,
RUBY_TEXT_CONTAINER: 1 << 23,
CONTENTS: 1 << 24,
INLINE_BLOCK: 1 << 25,
INLINE_LIST_ITEM: 1 << 26,
INLINE_TABLE: 1 << 27,
INLINE_FLEX: 1 << 28,
INLINE_GRID: 1 << 29
export type Display = number;
export const display: IPropertyListDescriptor<Display> = {
name: 'display',
initialValue: 'inline-block',
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]): Display => {
return tokens.filter(isIdentToken).reduce((bit, token) => {
return bit | parseDisplayValue(token.value);
}, DISPLAY.NONE);
}
};
export type Display = $Values<typeof DISPLAY>;
export type DisplayBit = number;
const parseDisplayValue = (display: string): Display => {
switch (display) {
case 'block':
@ -52,6 +62,7 @@ const parseDisplayValue = (display: string): Display => {
case 'table':
return DISPLAY.TABLE;
case 'flex':
case '-webkit-flex':
return DISPLAY.FLEX;
case 'grid':
return DISPLAY.GRID;
@ -101,11 +112,3 @@ const parseDisplayValue = (display: string): Display => {
return DISPLAY.NONE;
};
const setDisplayBit = (bit: DisplayBit, display: string): DisplayBit => {
return bit | parseDisplayValue(display);
};
export const parseDisplay = (display: string): Display => {
return display.split(' ').reduce(setDisplayBit, 0);
};

View File

@ -0,0 +1,28 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum FLOAT {
NONE = 0,
LEFT = 1,
RIGHT = 2,
INLINE_START = 3,
INLINE_END = 4
}
export const float: IPropertyIdentValueDescriptor<FLOAT> = {
name: 'float',
initialValue: 'none',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (float: string) => {
switch (float) {
case 'left':
return FLOAT.LEFT;
case 'right':
return FLOAT.RIGHT;
case 'inline-start':
return FLOAT.INLINE_START;
case 'inline-end':
return FLOAT.INLINE_END;
}
return FLOAT.NONE;
}
};

View File

@ -0,0 +1,20 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue} from '../syntax/parser';
import {StringValueToken, TokenType} from '../syntax/tokenizer';
export type FONT_FAMILY = string;
export type FontFamily = FONT_FAMILY[];
export const fontFamily: IPropertyListDescriptor<FontFamily> = {
name: `font-family`,
initialValue: '',
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]) => {
return tokens.filter(isStringToken).map(token => token.value);
}
};
const isStringToken = (token: CSSValue): token is StringValueToken =>
token.type === TokenType.STRING_TOKEN || token.type === TokenType.IDENT_TOKEN;

View File

@ -0,0 +1,9 @@
import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export const fontSize: IPropertyTypeValueDescriptor = {
name: `font-size`,
initialValue: '0',
prefix: false,
type: PropertyDescriptorParsingType.TYPE_VALUE,
format: 'length'
};

View File

@ -0,0 +1,24 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum FONT_STYLE {
NORMAL = 'normal',
ITALIC = 'italic',
OBLIQUE = 'oblique'
}
export const fontStyle: IPropertyIdentValueDescriptor<FONT_STYLE> = {
name: 'font-style',
initialValue: 'normal',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (overflow: string) => {
switch (overflow) {
case 'oblique':
return FONT_STYLE.OBLIQUE;
case 'italic':
return FONT_STYLE.ITALIC;
case 'normal':
default:
return FONT_STYLE.NORMAL;
}
}
};

View File

@ -0,0 +1,11 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken} from '../syntax/parser';
export const fontVariant: IPropertyListDescriptor<string[]> = {
name: 'font-variant',
initialValue: 'none',
type: PropertyDescriptorParsingType.LIST,
prefix: false,
parse: (tokens: CSSValue[]): string[] => {
return tokens.filter(isIdentToken).map(token => token.value);
}
};

View File

@ -0,0 +1,25 @@
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken, isNumberToken} from '../syntax/parser';
export const fontWeight: IPropertyValueDescriptor<number> = {
name: 'font-weight',
initialValue: 'normal',
type: PropertyDescriptorParsingType.VALUE,
prefix: false,
parse: (token: CSSValue): number => {
if (isNumberToken(token)) {
return token.number;
}
if (isIdentToken(token)) {
switch (token.value) {
case 'bold':
return 700;
case 'normal':
default:
return 400;
}
}
return 400;
}
};

View File

@ -0,0 +1,24 @@
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue} from '../syntax/parser';
import {TokenType} from '../syntax/tokenizer';
export const letterSpacing: IPropertyValueDescriptor<number> = {
name: 'letter-spacing',
initialValue: '0',
prefix: false,
type: PropertyDescriptorParsingType.VALUE,
parse: (token: CSSValue) => {
if (token.type === TokenType.IDENT_TOKEN && token.value === 'normal') {
return 0;
}
if (token.type === TokenType.NUMBER_TOKEN) {
return token.number;
}
if (token.type === TokenType.DIMENSION_TOKEN) {
return token.number;
}
return 0;
}
};

View File

@ -0,0 +1,21 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum LINE_BREAK {
NORMAL = 'normal',
STRICT = 'strict'
}
export const lineBreak: IPropertyIdentValueDescriptor<LINE_BREAK> = {
name: 'line-break',
initialValue: 'normal',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (lineBreak: string): LINE_BREAK => {
switch (lineBreak) {
case 'strict':
return LINE_BREAK.STRICT;
case 'normal':
default:
return LINE_BREAK.NORMAL;
}
}
};

View File

@ -0,0 +1,22 @@
import {IPropertyTokenValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken} from '../syntax/parser';
import {TokenType} from '../syntax/tokenizer';
import {getAbsoluteValue, isLengthPercentage} from '../types/length-percentage';
export const lineHeight: IPropertyTokenValueDescriptor = {
name: 'line-height',
initialValue: 'normal',
prefix: false,
type: PropertyDescriptorParsingType.TOKEN_VALUE
};
export const computeLineHeight = (token: CSSValue, fontSize: number): number => {
if (isIdentToken(token) && token.value === 'normal') {
return 1.2 * fontSize;
} else if (token.type === TokenType.NUMBER_TOKEN) {
return fontSize * token.number;
} else if (isLengthPercentage(token)) {
return getAbsoluteValue(token, fontSize);
}
return fontSize;
};

View File

@ -0,0 +1,18 @@
import {TokenType} from '../syntax/tokenizer';
import {ICSSImage, image} from '../types/image';
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue} from '../syntax/parser';
export const listStyleImage: IPropertyValueDescriptor<ICSSImage | null> = {
name: 'list-style-image',
initialValue: 'none',
type: PropertyDescriptorParsingType.VALUE,
prefix: false,
parse: (token: CSSValue) => {
if (token.type === TokenType.IDENT_TOKEN && token.value === 'none') {
return null;
}
return image.parse(token);
}
};

View File

@ -0,0 +1,21 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum LIST_STYLE_POSITION {
INSIDE = 0,
OUTSIDE = 1
}
export const listStylePosition: IPropertyIdentValueDescriptor<LIST_STYLE_POSITION> = {
name: 'list-style-position',
initialValue: 'outside',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (position: string) => {
switch (position) {
case 'inside':
return LIST_STYLE_POSITION.INSIDE;
case 'outside':
default:
return LIST_STYLE_POSITION.OUTSIDE;
}
}
};

View File

@ -0,0 +1,177 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum 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 const listStyleType: IPropertyIdentValueDescriptor<LIST_STYLE_TYPE> = {
name: 'list-style-type',
initialValue: 'none',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (type: string) => {
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;
}
}
};

View File

@ -0,0 +1,13 @@
import {IPropertyTokenValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
const marginForSide = (side: string): IPropertyTokenValueDescriptor => ({
name: `margin-${side}`,
initialValue: '0',
prefix: false,
type: PropertyDescriptorParsingType.TOKEN_VALUE
});
export const marginTop: IPropertyTokenValueDescriptor = marginForSide('top');
export const marginRight: IPropertyTokenValueDescriptor = marginForSide('right');
export const marginBottom: IPropertyTokenValueDescriptor = marginForSide('bottom');
export const marginLeft: IPropertyTokenValueDescriptor = marginForSide('left');

View File

@ -0,0 +1,14 @@
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isNumberToken} from '../syntax/parser';
export const opacity: IPropertyValueDescriptor<number> = {
name: 'opacity',
initialValue: '1',
type: PropertyDescriptorParsingType.VALUE,
prefix: false,
parse: (token: CSSValue): number => {
if (isNumberToken(token)) {
return token.number;
}
return 1;
}
};

View File

@ -0,0 +1,21 @@
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
export enum OVERFLOW_WRAP {
NORMAL = 'normal',
BREAK_WORD = 'break-word'
}
export const overflowWrap: IPropertyIdentValueDescriptor<OVERFLOW_WRAP> = {
name: 'overflow-wrap',
initialValue: 'normal',
prefix: false,
type: PropertyDescriptorParsingType.IDENT_VALUE,
parse: (overflow: string) => {
switch (overflow) {
case 'break-word':
return OVERFLOW_WRAP.BREAK_WORD;
case 'normal':
default:
return OVERFLOW_WRAP.NORMAL;
}
}
};

View File

@ -0,0 +1,30 @@
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
import {CSSValue, isIdentToken} from '../syntax/parser';
export enum OVERFLOW {
VISIBLE = 0,
HIDDEN = 1,
SCROLL = 2,
AUTO = 3
}
export const overflow: IPropertyListDescriptor<OVERFLOW[]> = {
name: 'overflow',
initialValue: 'visible',
prefix: false,
type: PropertyDescriptorParsingType.LIST,
parse: (tokens: CSSValue[]): OVERFLOW[] => {
return tokens.filter(isIdentToken).map(overflow => {
switch (overflow.value) {
case 'hidden':
return OVERFLOW.HIDDEN;
case 'scroll':
return OVERFLOW.SCROLL;
case 'auto':
return OVERFLOW.AUTO;
case 'visible':
default:
return OVERFLOW.VISIBLE;
}
});
}
};

Some files were not shown because too many files have changed in this diff Show More