mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
Compare commits
198 Commits
v1.0.0-alp
...
master
Author | SHA1 | Date | |
---|---|---|---|
6020386bbe | |||
f7c2289db1 | |||
7c3269bdbe | |||
e587a82dca | |||
67c5e8dec4 | |||
181d1b1103 | |||
46db86755f | |||
9fda3e1ede | |||
6521a487d7 | |||
0476d06515 | |||
74696faf47 | |||
1cc853a318 | |||
04787ee88a | |||
ba2b1cd8e9 | |||
d9222075da | |||
101c32ed71 | |||
ddffaecf6d | |||
fd22a01a3c | |||
ed57781594 | |||
eeda86bd5e | |||
0b1f0a7e90 | |||
38c682955a | |||
01ed87907a | |||
58ff0003f7 | |||
f143166551 | |||
cd0d7258c3 | |||
b482725994 | |||
1b55ed5668 | |||
68377b3244 | |||
6947982848 | |||
437b367d3b | |||
969638fb94 | |||
f919204efa | |||
c378e22069 | |||
2b4de68e92 | |||
1941b9e0ac | |||
e429e0443a | |||
f43f942fcd | |||
7a06d0c2c2 | |||
e36408ad03 | |||
a0dd38a8be | |||
b988d9d657 | |||
c5c6fa00d7 | |||
6651fc6789 | |||
df223c3ff2 | |||
95a46b00c5 | |||
878e37a242 | |||
1338c7b203 | |||
f284752295 | |||
96e23d1851 | |||
7d788c6f3d | |||
5dea36bd69 | |||
11d16d2b77 | |||
e9f7f48d57 | |||
4c360fc1f0 | |||
578bb771bf | |||
522e5aac5f | |||
dd6d8856ec | |||
45efe54da8 | |||
cd99f11b1b | |||
fa60716d07 | |||
99b687c412 | |||
1d00bfe175 | |||
58b4591174 | |||
92fa448913 | |||
1acdc827a4 | |||
acb4cd24b8 | |||
eeb5a3ea1d | |||
52a03c76b6 | |||
439e365ea8 | |||
e29af58661 | |||
171585491d | |||
99b8182991 | |||
a4a3ce8a2e | |||
084017a673 | |||
4555940d0b | |||
382853cb33 | |||
44296e5293 | |||
b2902ec31c | |||
e7a021ab93 | |||
85f79c1a5e | |||
cf35a282b2 | |||
bb9237155c | |||
b09ee30dae | |||
72cd528429 | |||
2a013e20c8 | |||
ff35c7dbd3 | |||
ba172678f0 | |||
7222aba1b4 | |||
82b7da558c | |||
3982df1492 | |||
1514220812 | |||
d07b6811c6 | |||
bacfadff96 | |||
d7d17adf70 | |||
60f6b85083 | |||
e3237dc4ce | |||
659f820170 | |||
f7b39c0914 | |||
cd77e1cea1 | |||
f23e6f6f26 | |||
04e145b5eb | |||
003cfd9073 | |||
9749bb1fb1 | |||
3c2c826ad7 | |||
e496047888 | |||
51de34787a | |||
6c86f5f653 | |||
8f4ec8a0c0 | |||
51aadbb7f1 | |||
8af7745e80 | |||
610997923a | |||
d844328e0a | |||
e0d536777a | |||
cc19105c91 | |||
2d30554b2a | |||
401df79087 | |||
7cb0019594 | |||
ebe9eccbd2 | |||
df44c86f68 | |||
e083e229c9 | |||
b308cbd998 | |||
4dd4a69c7a | |||
c366e8790d | |||
ae5f866b37 | |||
f139b513c5 | |||
e4d52a1ac6 | |||
8c04f94bc9 | |||
3f599103fb | |||
076492042a | |||
34b06d6365 | |||
99f105cb66 | |||
7d3456b78c | |||
00555cf1ef | |||
eedb81ef9e | |||
d4b58960dc | |||
ee3ca35636 | |||
9a63797aa7 | |||
81dcf7b6be | |||
61f4819e02 | |||
86a650bfd5 | |||
cbaecdca28 | |||
cae44a6f0a | |||
28dc05c4a3 | |||
409674fba6 | |||
5f31b74177 | |||
522a443055 | |||
20a797cbeb | |||
43058241b4 | |||
cdc4ca8296 | |||
a7d881019b | |||
b89458611b | |||
4e4a231683 | |||
a63cb3c0f1 | |||
7775d3c0d6 | |||
397595afb5 | |||
7027900f49 | |||
49f87fb680 | |||
238de790a9 | |||
029235a652 | |||
44f3d79f68 | |||
7ebef72e92 | |||
2c018d1987 | |||
5cbe5db351 | |||
3212184146 | |||
349bbf137a | |||
c45ef099fe | |||
24823d0491 | |||
41eb8ab22f | |||
4e02a4c7a1 | |||
ce45c1bbdd | |||
078b388974 | |||
3ae7dd2ebb | |||
dab77acde4 | |||
83e7eaa795 | |||
cb7fbcf33e | |||
9a6e57aa00 | |||
a3e25d71cb | |||
9da0f60551 | |||
f347953042 | |||
48b4c20e24 | |||
6341788edf | |||
0e273418c7 | |||
e6bbc1abb5 | |||
13bbc90048 | |||
102b5a1282 | |||
01e4920876 | |||
da2794f7f7 | |||
9fb9898894 | |||
952eb4cf7c | |||
fad4f837c9 | |||
e6c44afca1 | |||
d023de0b99 | |||
0f01810005 | |||
bf03cf5237 | |||
a555dfc085 | |||
69fb48969e | |||
974c35c368 |
13
.babelrc
13
.babelrc
@ -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"
|
||||
]
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[{.travis.yml,package.json}]
|
||||
[{*.yml,package.json}]
|
||||
# The indent size used in the `package.json` file cannot be changed
|
||||
# https://github.com/npm/npm/pull/3180#issuecomment-16336516
|
||||
indent_size = 2
|
||||
|
33
.eslintrc
33
.eslintrc
@ -1,23 +1,26 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
[ignore]
|
||||
.*/www/.*
|
||||
[include]
|
||||
[libs]
|
||||
./flow-typed
|
||||
[options]
|
||||
[lints]
|
359
.github/workflows/ci.yml
vendored
Normal file
359
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,359 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
- name: Npm install
|
||||
run: npm ci
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Pack
|
||||
run: |
|
||||
npm pack
|
||||
mv html2canvas-*.tgz html2canvas.tgz
|
||||
tar --list --verbose --file=html2canvas.tgz
|
||||
- name: Upload npm pack
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: npm
|
||||
path: html2canvas.tgz
|
||||
if-no-files-found: error
|
||||
- name: Upload dist
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
if-no-files-found: error
|
||||
- name: Upload build
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
if-no-files-found: error
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
name: Test
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
- name: Npm install
|
||||
run: npm ci
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
- name: Unit tests
|
||||
run: npm run unittest
|
||||
browser-test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
config:
|
||||
- os: ubuntu-latest
|
||||
name: Linux Firefox Stable
|
||||
targetBrowser: Firefox_Stable
|
||||
xvfb: true
|
||||
- os: ubuntu-latest
|
||||
name: Linux Chrome Stable
|
||||
targetBrowser: Chrome_Stable
|
||||
xvfb: true
|
||||
- os: macos-latest
|
||||
name: OSX Safari Stable
|
||||
targetBrowser: Safari_Stable
|
||||
- os: macos-10.15
|
||||
name: iOS Simulator Safari 12
|
||||
targetBrowser: Safari_IOS_12
|
||||
xcode: /Applications/Xcode_10.3.app
|
||||
- os: macos-10.15
|
||||
name: iOS Simulator Safari 13
|
||||
targetBrowser: Safari_IOS_13
|
||||
xcode: /Applications/Xcode_11.7.app
|
||||
- os: macos-10.15
|
||||
name: iOS Simulator Safari 14
|
||||
targetBrowser: Safari_IOS_14
|
||||
xcode: /Applications/Xcode_12.4.app
|
||||
- os: macos-11
|
||||
name: iOS Simulator Safari 15.0
|
||||
targetBrowser: Safari_IOS_15_0
|
||||
xcode: /Applications/Xcode_13.0.app
|
||||
- os: macos-11
|
||||
name: iOS Simulator Safari 15
|
||||
targetBrowser: Safari_IOS_15
|
||||
xcode: /Applications/Xcode_13.2.app
|
||||
- os: windows-latest
|
||||
name: Windows Internet Explorer 9 (Emulated)
|
||||
targetBrowser: IE_9
|
||||
- os: windows-latest
|
||||
name: Windows Internet Explorer 10 (Emulated)
|
||||
targetBrowser: IE_10
|
||||
- os: windows-latest
|
||||
name: Windows Internet Explorer 11
|
||||
targetBrowser: IE_11
|
||||
runs-on: ${{ matrix.config.os }}
|
||||
name: ${{ matrix.config.name }}
|
||||
env:
|
||||
TARGET_BROWSER: ${{ matrix.config.targetBrowser }}
|
||||
needs: build
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
- name: Npm install
|
||||
run: npm ci
|
||||
- name: Download library
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
- name: Download test-runner
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
- name: xcode selection
|
||||
if: ${{ matrix.config.xcode != '' }}
|
||||
run: sudo xcode-select -s "${{ matrix.config.xcode }}"
|
||||
- name: Run browser tests
|
||||
if: ${{ matrix.config.xvfb != true }}
|
||||
run: npm run karma
|
||||
- name: Start Xvfb
|
||||
if: ${{ matrix.config.xvfb == true }}
|
||||
run: Xvfb :99 &
|
||||
- name: Run browser tests
|
||||
if: ${{ matrix.config.xvfb == true }}
|
||||
run: DISPLAY=:99 npm run karma
|
||||
- name: Upload screenshots
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: reftest-results
|
||||
path: tmp/reftests
|
||||
if-no-files-found: error
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
name: Publish
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: browser-test
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Download NPM package
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: npm
|
||||
path: npm
|
||||
- name: Download library
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
- name: Create Github Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
draft: false
|
||||
prerelease: ${{ contains(github.ref, '-rc.') || contains(github.ref, '-alpha.') }}
|
||||
- name: Upload html2canvas.js
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./dist/html2canvas.js
|
||||
asset_name: html2canvas.js
|
||||
asset_content_type: text/javascript
|
||||
- name: Upload html2canvas.min.js
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./dist/html2canvas.min.js
|
||||
asset_name: html2canvas.min.js
|
||||
asset_content_type: text/javascript
|
||||
- name: Upload html2canvas.esm.js
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./dist/html2canvas.esm.js
|
||||
asset_name: html2canvas.esm.js
|
||||
asset_content_type: text/javascript
|
||||
- name: Unpack npm
|
||||
run: cd npm && tar -xvzf "html2canvas.tgz"
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: NPM Publish
|
||||
run: cd npm/package && npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
docs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build docs
|
||||
needs: browser-test
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
- name: Npm install
|
||||
run: npm ci
|
||||
- name: Download library
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: www/static/dist
|
||||
- name: Download test results
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: reftest-results
|
||||
path: www/static/results
|
||||
- name: Copy reftests to docs website
|
||||
run: 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
|
||||
- name: Create reftest result index
|
||||
run: npm run build:reftest-result-list www/static/results/metadata www/src/results.json
|
||||
- name: Create reftest previewer
|
||||
run: npm run build:reftest-preview
|
||||
- name: Clean metadata folder
|
||||
run: rm -rf www/static/results/metadata
|
||||
- name: Build docs
|
||||
run: npm run build && cd www && npm install && npm run build && cd ..
|
||||
- name: Upload docs
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: docs
|
||||
path: www/public
|
||||
if-no-files-found: error
|
||||
publish-docs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Publish Docs
|
||||
if: ${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v') }}
|
||||
needs: docs
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Download docs
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: docs
|
||||
path: docs
|
||||
- name: Publish docs
|
||||
uses: JamesIves/github-pages-deploy-action@3.7.1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BRANCH: gh-pages
|
||||
FOLDER: docs
|
||||
SINGLE_COMMIT: true
|
||||
CLEAN: true
|
||||
diff-reftests:
|
||||
runs-on: ubuntu-latest
|
||||
name: Diff reftest screenshots
|
||||
needs: browser-test
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
- name: Npm install
|
||||
run: npm ci
|
||||
- name: Checkout current snapshots
|
||||
run: git checkout origin/gh-pages results
|
||||
- name: Download test results
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: reftest-results
|
||||
path: tmp/reftests
|
||||
- name: Run diff
|
||||
run: npm run reftests-diff
|
||||
continue-on-error: true
|
||||
- name: Upload diff
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: snapshot-diffs
|
||||
path: tmp/snapshot-diffs
|
37
.github/workflows/release.yml
vendored
Normal file
37
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
name: Create Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Semantic version (major | minor | patch | premajor | preminor | prepatch | prerelease)'
|
||||
default: 'patch'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
version:
|
||||
name: Create version
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- name: Npm install
|
||||
run: npm ci
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config user.name "CI"
|
||||
git config user.email "niklasvh@gmail.com"
|
||||
- name: Create release
|
||||
run: npm run release -- --preset eslint --release-as ${{ github.event.inputs.version }}
|
||||
- name: Print details
|
||||
run: |
|
||||
cat package.json
|
||||
cat CHANGELOG.md
|
||||
git tag
|
||||
- name: Push git version
|
||||
run: git push --follow-tags origin master
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
|
||||
|
16
.npmignore
16
.npmignore
@ -1,18 +1,22 @@
|
||||
.github/
|
||||
.idea/
|
||||
.rpt2_cache
|
||||
build/
|
||||
configs/
|
||||
docs/
|
||||
examples/
|
||||
scripts/
|
||||
src/
|
||||
tests/
|
||||
www/
|
||||
.github/
|
||||
tmp/
|
||||
*.iml
|
||||
.babelrc
|
||||
.idea/
|
||||
.editorconfig
|
||||
.npmignore
|
||||
.eslintrc
|
||||
.travis.yml
|
||||
karma.js
|
||||
.npmignore
|
||||
.prettierrc
|
||||
jest.config.js
|
||||
karma.conf.js
|
||||
webpack.config.js
|
||||
karma.js
|
||||
rollup.config.ts
|
||||
|
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 4,
|
||||
"bracketSpacing": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
57
.travis.yml
57
.travis.yml
@ -1,57 +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
|
||||
- cd www && npm install && npm run build && cd ..
|
||||
- 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
|
||||
- provider: pages
|
||||
skip_cleanup: true
|
||||
local_dir: www/public
|
||||
target_branch: gh-pages
|
||||
fqdn: html2canvas.hertzen.com
|
||||
github_token:
|
||||
secure: "PowO/Jat660k3gHcjgI6DlJz15RM7pLUu11UPsLCtYJ8ZwodppE6Keg0DfVkSFSIZttZor+UssDwP/WOEqfZNLqmXbcj3Gec4xolohet/GOe0KJKKuF/HgggbcxumopxMX6sMVePlMBpkLpHh7tgEAEHBWTlzC1c1a7Xa48fZ7k="
|
||||
on:
|
||||
branch: master
|
||||
repo: niklasvh/html2canvas
|
483
CHANGELOG.md
483
CHANGELOG.md
@ -1,51 +1,486 @@
|
||||
### Changelog ###
|
||||
# Changelog
|
||||
|
||||
#### v1.0.0-alpha.9 - 7.1.2018 ####
|
||||
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.4.1](https://github.com/niklasvh/html2canvas/compare/v1.4.0...v1.4.1) (2022-01-22)
|
||||
|
||||
|
||||
### deps
|
||||
|
||||
* fix source maps (#2812) ([67c5e8d](https://github.com/niklasvh/html2canvas/commit/67c5e8dec4b2af9260a2b5b75b3399495fd1fee9)), closes [#2812](https://github.com/niklasvh/html2canvas/issues/2812)
|
||||
|
||||
### feat
|
||||
|
||||
* add support for `<video>` elements (#2788) ([181d1b1](https://github.com/niklasvh/html2canvas/commit/181d1b1103910d6e1b5277d5c007fc5e3006c6bf)), closes [#2788](https://github.com/niklasvh/html2canvas/issues/2788)
|
||||
|
||||
### fix
|
||||
|
||||
* Properties x and y of BoundingRect is undefined in old browser (#2797) ([e587a82](https://github.com/niklasvh/html2canvas/commit/e587a82dca01d9ada78cae34fd1bdb934e547f9b)), closes [#2797](https://github.com/niklasvh/html2canvas/issues/2797)
|
||||
* source maps (#2787) ([46db867](https://github.com/niklasvh/html2canvas/commit/46db86755f064828559a4b0b37310f3ae94f5494)), closes [#2787](https://github.com/niklasvh/html2canvas/issues/2787)
|
||||
|
||||
|
||||
|
||||
# [1.4.0](https://github.com/niklasvh/html2canvas/compare/v1.3.4...v1.4.0) (2022-01-01)
|
||||
|
||||
|
||||
### feat
|
||||
|
||||
* use native text segmenter where available (#2782) ([6521a48](https://github.com/niklasvh/html2canvas/commit/6521a487d78172f7179f7c973c1a3af40eb92009)), closes [#2782](https://github.com/niklasvh/html2canvas/issues/2782)
|
||||
|
||||
### fix
|
||||
|
||||
* adopted stylesheets (#2785) ([74696fa](https://github.com/niklasvh/html2canvas/commit/74696faf47c07b48b9c9587db0b999da1c08a8be)), closes [#2785](https://github.com/niklasvh/html2canvas/issues/2785)
|
||||
* ios text wrapping with 0 width rect (#2786) ([0476d06](https://github.com/niklasvh/html2canvas/commit/0476d065158c33d2020a9f602b3043e5e2f90c75)), closes [#2786](https://github.com/niklasvh/html2canvas/issues/2786)
|
||||
* reduce SLICE_STACK_SIZE to 50k (#2784) ([1cc853a](https://github.com/niklasvh/html2canvas/commit/1cc853a3186853eaca00af060f651697dc3497a9)), closes [#2784](https://github.com/niklasvh/html2canvas/issues/2784)
|
||||
|
||||
|
||||
|
||||
## [1.3.4](https://github.com/niklasvh/html2canvas/compare/v1.3.3...v1.3.4) (2021-12-29)
|
||||
|
||||
|
||||
### ci
|
||||
|
||||
* add ios 15.0 testing (#2780) ([d922207](https://github.com/niklasvh/html2canvas/commit/d9222075daaed08884491b0563fc899ee0ced731)), closes [#2780](https://github.com/niklasvh/html2canvas/issues/2780)
|
||||
|
||||
### fix
|
||||
|
||||
* ios 15 font rendering crash (#2645) ([ba2b1cd](https://github.com/niklasvh/html2canvas/commit/ba2b1cd8e9a9d7932675d7abffce1526a609e769)), closes [#2645](https://github.com/niklasvh/html2canvas/issues/2645)
|
||||
|
||||
|
||||
|
||||
## [1.3.3](https://github.com/niklasvh/html2canvas/compare/v1.3.2...v1.3.3) (2021-11-23)
|
||||
|
||||
|
||||
### ci
|
||||
|
||||
* fix macos action runners (#2757) ([ed57781](https://github.com/niklasvh/html2canvas/commit/ed577815949b6a565df54f2101cf6f0fb731c290)), closes [#2757](https://github.com/niklasvh/html2canvas/issues/2757)
|
||||
|
||||
### fix
|
||||
|
||||
* "offsets" text when font is big ([fd22a01](https://github.com/niklasvh/html2canvas/commit/fd22a01a3c9e39293f47caaed0c0e13d2dc8170c))
|
||||
* const enums (#2651) ([eeda86b](https://github.com/niklasvh/html2canvas/commit/eeda86bd5e81fb4e97675fe9bee3d4d15899997f)), closes [#2651](https://github.com/niklasvh/html2canvas/issues/2651)
|
||||
|
||||
|
||||
|
||||
## [1.3.2](https://github.com/niklasvh/html2canvas/compare/v1.3.1...v1.3.2) (2021-08-15)
|
||||
|
||||
|
||||
### docs
|
||||
|
||||
* add warning for webgl cloning with preserveDrawingBuffer=false (#2661) ([01ed879](https://github.com/niklasvh/html2canvas/commit/01ed87907ad9c7688880e2c5cb8ebc22ef73a4d8)), closes [#2661](https://github.com/niklasvh/html2canvas/issues/2661)
|
||||
* include src files on www (#2660) ([58ff000](https://github.com/niklasvh/html2canvas/commit/58ff0003f77d825ac027eeec95fa80c0123eaf8f)), closes [#2660](https://github.com/niklasvh/html2canvas/issues/2660)
|
||||
|
||||
### feat
|
||||
|
||||
* add support for data-html2canvas-debug property for debugging (#2658) ([cd0d725](https://github.com/niklasvh/html2canvas/commit/cd0d7258c3a93f2989d5d9ec0244ba2763ea2d23)), closes [#2658](https://github.com/niklasvh/html2canvas/issues/2658)
|
||||
|
||||
### fix
|
||||
|
||||
* disable transition properties (#2659) ([f143166](https://github.com/niklasvh/html2canvas/commit/f1431665513e0a4636fb167a241f4a0571ba728a)), closes [#2659](https://github.com/niklasvh/html2canvas/issues/2659)
|
||||
* overflows with absolutely positioned content (#2663) ([38c6829](https://github.com/niklasvh/html2canvas/commit/38c682955a9299ca7785af71d8f251df799405b0)), closes [#2663](https://github.com/niklasvh/html2canvas/issues/2663)
|
||||
|
||||
|
||||
|
||||
## [1.3.1](https://github.com/niklasvh/html2canvas/compare/v1.3.0...v1.3.1) (2021-08-14)
|
||||
|
||||
|
||||
### fix
|
||||
|
||||
* multi arg transition/animation duration (#2657) ([1b55ed5](https://github.com/niklasvh/html2canvas/commit/1b55ed5668dcbbe1c6d8d7e94736d8f2da2d31c5)), closes [#2657](https://github.com/niklasvh/html2canvas/issues/2657)
|
||||
|
||||
|
||||
|
||||
# [1.3.0](https://github.com/niklasvh/html2canvas/compare/v1.2.2...v1.3.0) (2021-08-13)
|
||||
|
||||
|
||||
### feat
|
||||
|
||||
* add rtl render support (#2653) ([6947982](https://github.com/niklasvh/html2canvas/commit/694798284838b16882e648914da0905818aa366c)), closes [#2653](https://github.com/niklasvh/html2canvas/issues/2653)
|
||||
* correctly break graphemes (#2652) ([437b367](https://github.com/niklasvh/html2canvas/commit/437b367d3ba9dfd7f9a4c8042ac8d00208c09770)), closes [#2652](https://github.com/niklasvh/html2canvas/issues/2652)
|
||||
|
||||
### fix
|
||||
|
||||
* correctly handle allowTaint canvas cloning (#2649) ([c378e22](https://github.com/niklasvh/html2canvas/commit/c378e220694c14cb7b3b4b8650a7757f8fc23c7a)), closes [#2649](https://github.com/niklasvh/html2canvas/issues/2649)
|
||||
* finish animation/transitions for elements (#2632) ([969638f](https://github.com/niklasvh/html2canvas/commit/969638fb94a0a14c64a667fa6e5689f79d9f1044)), closes [#2632](https://github.com/niklasvh/html2canvas/issues/2632)
|
||||
|
||||
### test
|
||||
|
||||
* add letter-spacing test for zwj emoji (#2650) ([f919204](https://github.com/niklasvh/html2canvas/commit/f919204efa06af219f155ca279f96124bb92862b)), closes [#2650](https://github.com/niklasvh/html2canvas/issues/2650)
|
||||
|
||||
|
||||
|
||||
## [1.2.2](https://github.com/niklasvh/html2canvas/compare/v1.2.1...v1.2.2) (2021-08-10)
|
||||
|
||||
|
||||
### ci
|
||||
|
||||
* add ios15 target (#2564) ([e429e04](https://github.com/niklasvh/html2canvas/commit/e429e0443adf5c7ca3041b97a8157b8911302206)), closes [#2564](https://github.com/niklasvh/html2canvas/issues/2564)
|
||||
|
||||
### docs
|
||||
|
||||
* update test previewer (#2637) ([7a06d0c](https://github.com/niklasvh/html2canvas/commit/7a06d0c2c2f3b8a1d1a8a85c540f8288b782e8c6)), closes [#2637](https://github.com/niklasvh/html2canvas/issues/2637)
|
||||
|
||||
### fix
|
||||
|
||||
* parsing counter content in pseudo element (#2640) ([1941b9e](https://github.com/niklasvh/html2canvas/commit/1941b9e0acfd9243da0beaf70e1643cab1b4a963)), closes [#2640](https://github.com/niklasvh/html2canvas/issues/2640)
|
||||
* radial gradient ry check (#2631) ([a0dd38a](https://github.com/niklasvh/html2canvas/commit/a0dd38a8be4e540ae1c1f4b4e41f6c386f3e454f)), closes [#2631](https://github.com/niklasvh/html2canvas/issues/2631)
|
||||
* test for ios range line break error (#2635) ([f43f942](https://github.com/niklasvh/html2canvas/commit/f43f942fcd793dde9cdc6c0438f379ec3c05c405)), closes [#2635](https://github.com/niklasvh/html2canvas/issues/2635)
|
||||
|
||||
### test
|
||||
|
||||
* large base64 encoded background (#2636) ([e36408a](https://github.com/niklasvh/html2canvas/commit/e36408ad030fe31acd9969a37fe24c1621c0bd04)), closes [#2636](https://github.com/niklasvh/html2canvas/issues/2636)
|
||||
|
||||
|
||||
|
||||
## [1.2.1](https://github.com/niklasvh/html2canvas/compare/v1.2.0...v1.2.1) (2021-08-05)
|
||||
|
||||
|
||||
### fix
|
||||
|
||||
* none image (#2627) ([6651fc6](https://github.com/niklasvh/html2canvas/commit/6651fc6789d5902d171dc53b4094887870433018)), closes [#2627](https://github.com/niklasvh/html2canvas/issues/2627)
|
||||
* type import that is only available ts 3.8 or higher (#2629) ([c5c6fa0](https://github.com/niklasvh/html2canvas/commit/c5c6fa00d71f36ef963ba5170ebc7b668d39c407)), closes [#2629](https://github.com/niklasvh/html2canvas/issues/2629)
|
||||
|
||||
|
||||
|
||||
# [1.2.0](https://github.com/niklasvh/html2canvas/compare/v1.1.5...v1.2.0) (2021-08-04)
|
||||
|
||||
|
||||
### fix
|
||||
|
||||
* element cropping & scrolling (#2625) ([878e37a](https://github.com/niklasvh/html2canvas/commit/878e37a24272d0412fe589975ef8eed931c56e0b)), closes [#2625](https://github.com/niklasvh/html2canvas/issues/2625)
|
||||
* overflow-wrap break-word (#2626) ([95a46b0](https://github.com/niklasvh/html2canvas/commit/95a46b00c53563722c035a0e45fdf5fb507275e4)), closes [#2626](https://github.com/niklasvh/html2canvas/issues/2626)
|
||||
|
||||
### test
|
||||
|
||||
* element with scrolled window (#2624) ([1338c7b](https://github.com/niklasvh/html2canvas/commit/1338c7b203535d53509416358d74014200a994eb)), closes [#2624](https://github.com/niklasvh/html2canvas/issues/2624)
|
||||
|
||||
|
||||
|
||||
## [1.1.5](https://github.com/niklasvh/html2canvas/compare/v1.1.4...v1.1.5) (2021-08-02)
|
||||
|
||||
|
||||
### docs
|
||||
|
||||
* update README to github discussion Q/A ([5dea36b](https://github.com/niklasvh/html2canvas/commit/5dea36bd6964164e8ba3f8780309e792f5d16255))
|
||||
|
||||
### fix
|
||||
|
||||
* emoji line breaking (fix #1813) (#2621) ([7d788c6](https://github.com/niklasvh/html2canvas/commit/7d788c6f3d221b87f6b59fcda8517731340b2d1f)), closes [#1813](https://github.com/niklasvh/html2canvas/issues/1813) [#2621](https://github.com/niklasvh/html2canvas/issues/2621) [#1813](https://github.com/niklasvh/html2canvas/issues/1813)
|
||||
* natural sizes for images with srcset (#2622) ([96e23d1](https://github.com/niklasvh/html2canvas/commit/96e23d185198b7131cf0cfa31c14c165790464e9)), closes [#2622](https://github.com/niklasvh/html2canvas/issues/2622)
|
||||
|
||||
|
||||
|
||||
## [1.1.4](https://github.com/niklasvh/html2canvas/compare/v1.1.3...v1.1.4) (2021-07-15)
|
||||
|
||||
|
||||
### feat
|
||||
|
||||
* add support for webkit-text-stroke and paint-order (#2591) ([522e5aa](https://github.com/niklasvh/html2canvas/commit/522e5aac5fdad090953d095b5d558053a5e2d43d)), closes [#2591](https://github.com/niklasvh/html2canvas/issues/2591)
|
||||
|
||||
### fix
|
||||
|
||||
* don't copy 'all' css property (#2586) ([fa60716](https://github.com/niklasvh/html2canvas/commit/fa60716d07ed590ec64543a586a7960cbc8557df)), closes [#2586](https://github.com/niklasvh/html2canvas/issues/2586)
|
||||
* svg d path getting truncated on copy (#2589) ([dd6d885](https://github.com/niklasvh/html2canvas/commit/dd6d8856eca820a13a0990c467b9e531433fd4a9)), closes [#2589](https://github.com/niklasvh/html2canvas/issues/2589)
|
||||
* text position for form elements and list markers (#2588) ([cd99f11](https://github.com/niklasvh/html2canvas/commit/cd99f11b1b9eb1260a548a63e2a370a0a5ddafa0)), closes [#2588](https://github.com/niklasvh/html2canvas/issues/2588)
|
||||
* this.canvas.ownerDocument is undefined (#2590) ([45efe54](https://github.com/niklasvh/html2canvas/commit/45efe54da8145f97b9ee0463e686103280e3c8b1)), closes [#2590](https://github.com/niklasvh/html2canvas/issues/2590)
|
||||
* word-break seperators (#2593) ([e9f7f48](https://github.com/niklasvh/html2canvas/commit/e9f7f48d571304be14610a181feedca3c3b42864)), closes [#2593](https://github.com/niklasvh/html2canvas/issues/2593)
|
||||
|
||||
### test
|
||||
|
||||
* refactor language tests (#2594) ([4c360fc](https://github.com/niklasvh/html2canvas/commit/4c360fc1f059f4dcab71a79f9dc8a5b2e25411ea)), closes [#2594](https://github.com/niklasvh/html2canvas/issues/2594)
|
||||
* update box-shadow with radius ([578bb77](https://github.com/niklasvh/html2canvas/commit/578bb771bfeb7e81362e9e355d6cc9ae910e3920))
|
||||
|
||||
|
||||
|
||||
## [1.1.3](https://github.com/niklasvh/html2canvas/compare/v1.1.2...v1.1.3) (2021-07-14)
|
||||
|
||||
|
||||
### feat
|
||||
|
||||
* allow access to reference element in onclone (#2584) ([58b4591](https://github.com/niklasvh/html2canvas/commit/58b45911741c0dbbccd462b2976560bb3999eaef)), closes [#2584](https://github.com/niklasvh/html2canvas/issues/2584)
|
||||
* support for custom and slot elements (#2581) ([acb4cd2](https://github.com/niklasvh/html2canvas/commit/acb4cd24b85527908c02a60794768949578678f0)), closes [#2581](https://github.com/niklasvh/html2canvas/issues/2581)
|
||||
|
||||
### fix
|
||||
|
||||
* iframe load to ensure images are loaded (#2577) ([eeb5a3e](https://github.com/niklasvh/html2canvas/commit/eeb5a3ea1d6c94e0f6dcfd40695eb88ebb3e0041)), closes [#2577](https://github.com/niklasvh/html2canvas/issues/2577)
|
||||
* image blob rendering ([1acdc82](https://github.com/niklasvh/html2canvas/commit/1acdc827a4e05933c2f7c9558405c66b7cd82f58))
|
||||
* responsive svg images (#2583) ([92fa448](https://github.com/niklasvh/html2canvas/commit/92fa448913192d5e4e82bfe14f6644b669d4e6ef)), closes [#2583](https://github.com/niklasvh/html2canvas/issues/2583)
|
||||
|
||||
### test
|
||||
|
||||
* add test cases for text-stroke and textarea from (#1540 and #2132) (#2585) ([1d00bfe](https://github.com/niklasvh/html2canvas/commit/1d00bfe175d51e663d0bae88b6dbd10a266a71f1)), closes [#1540](https://github.com/niklasvh/html2canvas/issues/1540) [#2132](https://github.com/niklasvh/html2canvas/issues/2132) [#2585](https://github.com/niklasvh/html2canvas/issues/2585)
|
||||
|
||||
|
||||
|
||||
## [1.1.2](https://github.com/niklasvh/html2canvas/compare/v1.1.1...v1.1.2) (2021-07-13)
|
||||
|
||||
|
||||
### ci
|
||||
|
||||
* implement screenshot diffing (#2571) ([e29af58](https://github.com/niklasvh/html2canvas/commit/e29af586618125bbad10ad6bee3d69fddbc5d22a)), closes [#2571](https://github.com/niklasvh/html2canvas/issues/2571)
|
||||
|
||||
### fix
|
||||
|
||||
* logger unique names (#2575) ([1715854](https://github.com/niklasvh/html2canvas/commit/171585491dd1bee4f30691328bd22e978f3ac79e)), closes [#2575](https://github.com/niklasvh/html2canvas/issues/2575)
|
||||
* text-shadow position with baseline (#2576) ([439e365](https://github.com/niklasvh/html2canvas/commit/439e365ea8c703b528778a268dcfc3bf9ccad6a9)), closes [#2576](https://github.com/niklasvh/html2canvas/issues/2576)
|
||||
|
||||
|
||||
|
||||
## [1.1.1](https://github.com/niklasvh/html2canvas/compare/v1.1.0...v1.1.1) (2021-07-12)
|
||||
|
||||
|
||||
### fix
|
||||
|
||||
* allow proxy url with parameters (#2100) ([a4a3ce8](https://github.com/niklasvh/html2canvas/commit/a4a3ce8a2eb6a4f43f1bc9a7935eb16f2b98a3cd)), closes [#2100](https://github.com/niklasvh/html2canvas/issues/2100)
|
||||
* crash on background-size with calc() (fix #2469) (#2569) ([084017a](https://github.com/niklasvh/html2canvas/commit/084017a67319a993d73c6bdf612dd8532f1b8dbe)), closes [#2469](https://github.com/niklasvh/html2canvas/issues/2469) [#2569](https://github.com/niklasvh/html2canvas/issues/2569)
|
||||
* handle unhandled promise rejections (#2568) ([4555940](https://github.com/niklasvh/html2canvas/commit/4555940d0bc17b7fd9327dd0164c382a3dbf1858)), closes [#2568](https://github.com/niklasvh/html2canvas/issues/2568)
|
||||
|
||||
|
||||
|
||||
# [1.1.0](https://github.com/niklasvh/html2canvas/compare/v1.0.0...v1.1.0) (2021-07-11)
|
||||
|
||||
|
||||
### ci
|
||||
|
||||
* fail build if no artifacts uploaded (#2566) ([e7a021a](https://github.com/niklasvh/html2canvas/commit/e7a021ab931f3c0de060b4643e88fad85345c3d3)), closes [#2566](https://github.com/niklasvh/html2canvas/issues/2566)
|
||||
|
||||
### deps
|
||||
|
||||
* update dependencies with lint fixes (#2565) ([b2902ec](https://github.com/niklasvh/html2canvas/commit/b2902ec31c2a414ea26f864f8e00aa8846890ffc)), closes [#2565](https://github.com/niklasvh/html2canvas/issues/2565)
|
||||
|
||||
### docs
|
||||
|
||||
* update border-style support ([cf35a28](https://github.com/niklasvh/html2canvas/commit/cf35a282b2c9d41b601c3148e8c08fe7ba61a5f9))
|
||||
|
||||
### fix
|
||||
|
||||
* Ensure resizeImage's canvas has at least 1px of width and height (#2409) ([bb92371](https://github.com/niklasvh/html2canvas/commit/bb9237155cf0ec090432ee6c5d9c555eb6ffa81f)), closes [#2409](https://github.com/niklasvh/html2canvas/issues/2409)
|
||||
* text-decoration-line fallback (#2088) (#2567) ([44296e5](https://github.com/niklasvh/html2canvas/commit/44296e529368140ec06a937383e05f3074b19ee2)), closes [#2088](https://github.com/niklasvh/html2canvas/issues/2088) [#2567](https://github.com/niklasvh/html2canvas/issues/2567)
|
||||
* use baseline for text positioning (#2109) ([85f79c1](https://github.com/niklasvh/html2canvas/commit/85f79c1a5e4c0b422ace552c430dd389c8477a44)), closes [#2109](https://github.com/niklasvh/html2canvas/issues/2109)
|
||||
|
||||
|
||||
|
||||
# [1.0.0](https://github.com/niklasvh/html2canvas/compare/v1.0.0-rc.7...v1.0.0) (2021-07-04)
|
||||
|
||||
|
||||
### ci
|
||||
|
||||
* update docs publish action (#2451) ([7222aba](https://github.com/niklasvh/html2canvas/commit/7222aba1b42138c3d466525172411b3d9869095f)), closes [#2451](https://github.com/niklasvh/html2canvas/issues/2451)
|
||||
|
||||
### deps
|
||||
|
||||
* update www deps (#2525) ([2a013e2](https://github.com/niklasvh/html2canvas/commit/2a013e20c814b7dbaea98f54f0bde8f553eb79a2)), closes [#2525](https://github.com/niklasvh/html2canvas/issues/2525)
|
||||
|
||||
### feat
|
||||
|
||||
* add support for border-style dashed, dotted, double (#2531) ([72cd528](https://github.com/niklasvh/html2canvas/commit/72cd5284296e4cdb3fe88f2982ec7528604b6618))
|
||||
|
||||
### fix
|
||||
|
||||
* opacity with overflow hidden (#2450) ([82b7da5](https://github.com/niklasvh/html2canvas/commit/82b7da558c342e7f53d298bb1d843a5db86c3b21)), closes [#2450](https://github.com/niklasvh/html2canvas/issues/2450)
|
||||
* top right border radius (#2522) ([ba17267](https://github.com/niklasvh/html2canvas/commit/ba172678f07f962e9f54b398df087e86217d7a13))
|
||||
|
||||
### test
|
||||
|
||||
* update karma runner (#2524) ([ff35c7d](https://github.com/niklasvh/html2canvas/commit/ff35c7dbd33f863f5b614d778baf8cb1e8dded60)), closes [#2524](https://github.com/niklasvh/html2canvas/issues/2524)
|
||||
|
||||
|
||||
|
||||
# [1.0.0-rc.7](https://github.com/niklasvh/html2canvas/compare/v1.0.0-rc.6...v1.0.0-rc.7) (2020-08-09)
|
||||
|
||||
|
||||
### fix
|
||||
|
||||
* concatenate contiguous font-family tokens (#2219) ([bacfadf](https://github.com/niklasvh/html2canvas/commit/bacfadff96d907d9e8ab4ef515ca6487de9e51fc)), closes [#2219](https://github.com/niklasvh/html2canvas/issues/2219)
|
||||
* external styles on svg elements (#2320) ([1514220](https://github.com/niklasvh/html2canvas/commit/1514220812cfb22d64d0974558d9c14fe90a41d3)), closes [#2320](https://github.com/niklasvh/html2canvas/issues/2320)
|
||||
|
||||
|
||||
|
||||
# [1.0.0-rc.6](https://github.com/niklasvh/html2canvas/compare/v1.0.0-rc.5...v1.0.0-rc.6) (2020-08-08)
|
||||
|
||||
|
||||
### ci
|
||||
|
||||
* Azure Pipelines: upgrade from macOS 10.13 -> 10.14 (#2204) ([c366e87](https://github.com/niklasvh/html2canvas/commit/c366e8790d346ea981b24b7425aef6bf6d7ebcec)), closes [#2204](https://github.com/niklasvh/html2canvas/issues/2204)
|
||||
* build docs (#2305) ([51de347](https://github.com/niklasvh/html2canvas/commit/51de34787ad8aba3f213800be45e878cddb064e9)), closes [#2305](https://github.com/niklasvh/html2canvas/issues/2305)
|
||||
|
||||
### fix
|
||||
|
||||
* #1868 Clone node, Setting className for SVG element raises error (#2079) ([f139b51](https://github.com/niklasvh/html2canvas/commit/f139b513c5cf9673dc727fd47124e0d779891e3a)), closes [#1868](https://github.com/niklasvh/html2canvas/issues/1868) [#2079](https://github.com/niklasvh/html2canvas/issues/2079) [#1868](https://github.com/niklasvh/html2canvas/issues/1868)
|
||||
* image loading="lazy" fix #2312 (#2314) ([f23e6f6](https://github.com/niklasvh/html2canvas/commit/f23e6f6f2690dc0dbd02621c3bb81025904e6647)), closes [#2312](https://github.com/niklasvh/html2canvas/issues/2312) [#2314](https://github.com/niklasvh/html2canvas/issues/2314)
|
||||
|
||||
|
||||
|
||||
# [1.0.0-rc.5](https://github.com/niklasvh/html2canvas/compare/v1.0.0-rc.4...v1.0.0-rc.5) (2019-09-27)
|
||||
|
||||
|
||||
### fix
|
||||
|
||||
* correctly respect logging option (#2013) ([34b06d6365603c3b16664ab7804efe94c7945946](https://github.com/niklasvh/html2canvas/commit/34b06d6365603c3b16664ab7804efe94c7945946)), closes [#2013](https://github.com/niklasvh/html2canvas/issues/2013)
|
||||
* safari pseudo element content parsing (#2018) ([3f599103fb139f218ffe917800e74af2c7cc7ad5](https://github.com/niklasvh/html2canvas/commit/3f599103fb139f218ffe917800e74af2c7cc7ad5)), closes [#2018](https://github.com/niklasvh/html2canvas/issues/2018)
|
||||
* using existing canvas option (#2017) ([076492042a73d67b30e4562f2964200e07d25f5e](https://github.com/niklasvh/html2canvas/commit/076492042a73d67b30e4562f2964200e07d25f5e)), closes [#2017](https://github.com/niklasvh/html2canvas/issues/2017)
|
||||
|
||||
|
||||
|
||||
# [1.0.0-rc.4](https://github.com/niklasvh/html2canvas/compare/v1.0.0-rc.3...v1.0.0-rc.4) (2019-09-22)
|
||||
|
||||
|
||||
### docs
|
||||
|
||||
* fix typo (#1864) ([9a63797aa7fb81454008745d2a1c069ca24339a4](https://github.com/niklasvh/html2canvas/commit/9a63797aa7fb81454008745d2a1c069ca24339a4)), closes [#1864](https://github.com/niklasvh/html2canvas/issues/1864)
|
||||
|
||||
### feat
|
||||
|
||||
* ignore unsupported image functions (#1873) ([61f4819e02102b112513d57b16ec7d37e989af20](https://github.com/niklasvh/html2canvas/commit/61f4819e02102b112513d57b16ec7d37e989af20)), closes [#1873](https://github.com/niklasvh/html2canvas/issues/1873)
|
||||
|
||||
### fix
|
||||
|
||||
* correctly render partial borders (fix #1920) (#2010) ([eedb81ef9e114366a7e286e975659360cf9d0983](https://github.com/niklasvh/html2canvas/commit/eedb81ef9e114366a7e286e975659360cf9d0983)), closes [#1920](https://github.com/niklasvh/html2canvas/issues/1920) [#2010](https://github.com/niklasvh/html2canvas/issues/2010)
|
||||
* nested z-index ordering (#2011) ([00555cf1efddfed5877811d8a03a326f9943ab06](https://github.com/niklasvh/html2canvas/commit/00555cf1efddfed5877811d8a03a326f9943ab06)), closes [#2011](https://github.com/niklasvh/html2canvas/issues/2011) [#1978](https://github.com/niklasvh/html2canvas/issues/1978)
|
||||
* null backgroundColor option as transparent (#2012) ([7d3456b78c37e7333db087601805b32ec7ca0253](https://github.com/niklasvh/html2canvas/commit/7d3456b78c37e7333db087601805b32ec7ca0253)), closes [#2012](https://github.com/niklasvh/html2canvas/issues/2012)
|
||||
* zero size iframe rendering (#1863) ([81dcf7b6be66920260a60908aa4b86e7530f6e17](https://github.com/niklasvh/html2canvas/commit/81dcf7b6be66920260a60908aa4b86e7530f6e17)), closes [#1863](https://github.com/niklasvh/html2canvas/issues/1863)
|
||||
|
||||
|
||||
|
||||
# [1.0.0-rc.3](https://github.com/niklasvh/html2canvas/compare/v1.0.0-rc.2...v1.0.0-rc.3) (2019-05-30)
|
||||
|
||||
|
||||
### fix
|
||||
|
||||
* stack exceeding for css tokenizer (#1862) ([cbaecdca28cfaf9bd854e1b0c005cc8058208b36](https://github.com/niklasvh/html2canvas/commit/cbaecdca28cfaf9bd854e1b0c005cc8058208b36)), closes [#1862](https://github.com/niklasvh/html2canvas/issues/1862)
|
||||
* typescript options type definition (#1861) ([cae44a6f0a6649bd8a7c4250a20792bb5c2e5b42](https://github.com/niklasvh/html2canvas/commit/cae44a6f0a6649bd8a7c4250a20792bb5c2e5b42)), closes [#1861](https://github.com/niklasvh/html2canvas/issues/1861)
|
||||
|
||||
|
||||
|
||||
# [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 ####
|
||||
# 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 ####
|
||||
# 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 ####
|
||||
# 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 ####
|
||||
# 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 ####
|
||||
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 ####
|
||||
# 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 ####
|
||||
# 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 ####
|
||||
# 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
|
||||
@ -54,19 +489,19 @@
|
||||
* Correctly handle px and percentage values in linear-gradients
|
||||
* 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
|
||||
@ -74,11 +509,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.
|
||||
@ -89,14 +524,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
|
||||
@ -106,7 +541,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>)
|
||||
@ -114,7 +549,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>)
|
||||
@ -122,7 +557,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>)
|
||||
|
24
README.md
24
README.md
@ -1,10 +1,10 @@
|
||||
html2canvas
|
||||
===========
|
||||
|
||||
[Homepage](https://html2canvas.hertzen.com) | [Downloads](https://github.com/niklasvh/html2canvas/releases) | [Questions](http://stackoverflow.com/questions/tagged/html2canvas?sort=newest) | [Donate](https://www.gittip.com/niklasvh/)
|
||||
[Homepage](https://html2canvas.hertzen.com) | [Downloads](https://github.com/niklasvh/html2canvas/releases) | [Questions](https://github.com/niklasvh/html2canvas/discussions/categories/q-a)
|
||||
|
||||
[](https://gitter.im/niklasvh/html2canvas?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](https://travis-ci.org/niklasvh/html2canvas)
|
||||

|
||||
[](https://www.npmjs.org/package/html2canvas)
|
||||
[](https://www.npmjs.org/package/html2canvas)
|
||||
|
||||
@ -39,12 +39,10 @@ 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]);`
|
||||
|
||||
The function returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) containing the `<canvas>` element. Simply add a promise fullfillment handler to the promise using `then`:
|
||||
The function returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) containing the `<canvas>` element. Simply add a promise fulfillment handler to the promise using `then`:
|
||||
|
||||
html2canvas(document.body).then(function(canvas) {
|
||||
document.body.appendChild(canvas);
|
||||
@ -66,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 ###
|
||||
|
||||
|
11
configs/base.json
Normal file
11
configs/base.json
Normal 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
10
configs/preview.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./base",
|
||||
"include": [
|
||||
"../www/src/preview.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
10
configs/scripts.json
Normal file
10
configs/scripts.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./base",
|
||||
"include": [
|
||||
"scripts/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "Configuration"
|
||||
title: "Options"
|
||||
description: "Explore the different configuration options available for html2canvas"
|
||||
previousUrl: "/getting-started"
|
||||
previousTitle: "Getting Started"
|
||||
@ -11,13 +11,14 @@ These are all of the available configuration options.
|
||||
|
||||
| Name | Default | Description |
|
||||
| ------------- | :------: | ----------- |
|
||||
| async | `true` | Whether to parse and render the element asynchronously
|
||||
| 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.
|
||||
|
10
docs/faq.md
10
docs/faq.md
@ -13,8 +13,14 @@ methods to check whether an image would taint the canvas before applying it. If
|
||||
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.
|
||||
The limitations vary by browser, operating system and system hardware.
|
||||
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
|
||||
|
@ -12,13 +12,13 @@ Below is a list of all the supported CSS properties and values.
|
||||
- url()
|
||||
- linear-gradient()
|
||||
- radial-gradient()
|
||||
- background-origin
|
||||
- background-origin
|
||||
- background-position
|
||||
- background-size
|
||||
- background-size
|
||||
- border
|
||||
- border-color
|
||||
- border-radius
|
||||
- border-style (**Only supports `solid`**)
|
||||
- border-style
|
||||
- border-width
|
||||
- bottom
|
||||
- box-sizing
|
||||
@ -50,6 +50,7 @@ Below is a list of all the supported CSS properties and values.
|
||||
- overflow
|
||||
- overflow-wrap
|
||||
- padding
|
||||
- paint-order
|
||||
- position
|
||||
- right
|
||||
- text-align
|
||||
@ -58,17 +59,18 @@ Below is a list of all the supported CSS properties and values.
|
||||
- text-decoration-line
|
||||
- text-decoration-style (**Only supports `solid`**)
|
||||
- text-shadow
|
||||
- text-transform
|
||||
- text-transform
|
||||
- top
|
||||
- transform (**Limited support**)
|
||||
- visibility
|
||||
- white-space
|
||||
- width
|
||||
- webkit-text-stroke
|
||||
- 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)
|
||||
|
@ -29,8 +29,6 @@
|
||||
var canvas = document.querySelector("canvas");
|
||||
var ctx = canvas.getContext("2d");
|
||||
|
||||
var ctx = canvas.getContext('2d');
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(75,75,50,0,Math.PI*2,true); // Outer circle
|
||||
ctx.moveTo(110,75);
|
||||
@ -42,7 +40,7 @@
|
||||
ctx.stroke();
|
||||
|
||||
document.querySelector("button").addEventListener("click", function() {
|
||||
html2canvas(document.querySelector("#content"), {canvas: canvas}).then(function(canvas) {
|
||||
html2canvas(document.querySelector("#content"), {canvas: canvas, scale: 1}).then(function(canvas) {
|
||||
console.log('Drew on the existing canvas');
|
||||
});
|
||||
}, false);
|
||||
|
11
flow-typed/myLibDef.js
vendored
11
flow-typed/myLibDef.js
vendored
@ -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 {}
|
5
jest.config.js
Normal file
5
jest.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jsdom',
|
||||
roots: ['src']
|
||||
};
|
197
karma.conf.js
197
karma.conf.js
@ -1,89 +1,193 @@
|
||||
// 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 listenAddress = 'localhost';
|
||||
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'
|
||||
// https://github.com/actions/virtual-environments/blob/master/images/macos/macos-10.15-Readme.md
|
||||
const launchers = {
|
||||
Safari_IOS_9: {
|
||||
base: 'MobileSafari',
|
||||
name: 'iPhone 5s',
|
||||
platform: 'iOS',
|
||||
sdk: '9.0'
|
||||
},
|
||||
sl_ie9: {
|
||||
Safari_IOS_10: {
|
||||
base: 'MobileSafari',
|
||||
name: 'iPhone 5s',
|
||||
platform: 'iOS',
|
||||
sdk: '10.0'
|
||||
},
|
||||
Safari_IOS_12: {
|
||||
base: 'MobileSafari',
|
||||
name: 'iPhone 5s',
|
||||
platform: 'iOS',
|
||||
sdk: '12.4'
|
||||
},
|
||||
Safari_IOS_13: {
|
||||
base: 'MobileSafari',
|
||||
name: 'iPhone 8',
|
||||
platform: 'iOS',
|
||||
sdk: '13.7'
|
||||
},
|
||||
Safari_IOS_14: {
|
||||
base: 'MobileSafari',
|
||||
name: 'iPhone 8',
|
||||
platform: 'iOS',
|
||||
sdk: '14.4'
|
||||
},
|
||||
Safari_IOS_15_0: {
|
||||
base: 'MobileSafari',
|
||||
name: 'iPhone 13',
|
||||
platform: 'iOS',
|
||||
sdk: '15.0'
|
||||
},
|
||||
Safari_IOS_15: {
|
||||
base: 'MobileSafari',
|
||||
name: 'iPhone 13',
|
||||
platform: 'iOS',
|
||||
sdk: '15.2'
|
||||
},
|
||||
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: 'SafariNative'
|
||||
},
|
||||
Chrome_Stable: {
|
||||
base: 'ChromeHeadless'
|
||||
},
|
||||
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'
|
||||
base: 'ChromeHeadless'
|
||||
},
|
||||
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(args.sdk, args.platform).then(devices => {
|
||||
const d = devices.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 +197,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 +231,14 @@ 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 listen address,
|
||||
listenAddress,
|
||||
|
||||
// web server port
|
||||
port,
|
||||
|
112
karma.js
112
karma.js
@ -1,112 +0,0 @@
|
||||
const Server = require('karma').Server;
|
||||
const cfg = require('karma').config;
|
||||
const path = require('path');
|
||||
const proxy = require('html2canvas-proxy');
|
||||
const karmaConfig = cfg.parseConfig(path.resolve('./karma.conf.js'));
|
||||
const server = new Server(karmaConfig, (exitCode) => {
|
||||
console.log('Karma has exited with ' + exitCode);
|
||||
process.exit(exitCode)
|
||||
});
|
||||
|
||||
const fs = require('fs');
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const cors = require('cors');
|
||||
const filenamifyUrl = require('filenamify-url');
|
||||
|
||||
const CORS_PORT = 8081;
|
||||
const corsApp = express();
|
||||
corsApp.use('/proxy', proxy());
|
||||
corsApp.use('/cors', cors(), express.static(path.resolve(__dirname)));
|
||||
corsApp.use('/', express.static(path.resolve(__dirname, '/tests')));
|
||||
corsApp.use((error, req, res, next) => {
|
||||
console.error(error);
|
||||
next();
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
if(err.errno === 'EADDRINUSE') {
|
||||
console.warn(err);
|
||||
} else {
|
||||
console.log(err);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
corsApp.listen(CORS_PORT, () => {
|
||||
console.log(`CORS server running on port ${CORS_PORT}`);
|
||||
});
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use((req, res, next) => {
|
||||
// IE9 doesn't set headers for cross-domain ajax requests
|
||||
if(typeof(req.headers['content-type']) === 'undefined'){
|
||||
req.headers['content-type'] = "application/json";
|
||||
}
|
||||
next();
|
||||
});
|
||||
app.use(
|
||||
bodyParser.json({
|
||||
limit: '15mb',
|
||||
type: '*/*'
|
||||
})
|
||||
);
|
||||
|
||||
const prefix = 'data:image/png;base64,';
|
||||
|
||||
const writeScreenshot = (buffer, body) => {
|
||||
const filename = `${filenamifyUrl(
|
||||
body.test.replace(/^\/tests\/reftests\//, '').replace(/\.html$/, ''),
|
||||
{replacement: '-'}
|
||||
)}!${body.platform.name}-${body.platform.version}.png`;
|
||||
|
||||
fs.writeFileSync(path.resolve(__dirname, './tests/results/', filename), buffer);
|
||||
};
|
||||
|
||||
app.post('/screenshot', (req, res) => {
|
||||
if (!req.body || !req.body.screenshot) {
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return res.sendStatus(200);
|
||||
});
|
||||
|
||||
app.use((error, req, res, next) => {
|
||||
console.error(error);
|
||||
next();
|
||||
});
|
||||
|
||||
const listener = app.listen(8000, () => {
|
||||
server.start();
|
||||
});
|
||||
|
||||
|
45973
package-lock.json
generated
45973
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
144
package.json
144
package.json
@ -2,15 +2,18 @@
|
||||
"title": "html2canvas",
|
||||
"name": "html2canvas",
|
||||
"description": "Screenshots with JavaScript",
|
||||
"main": "dist/npm/index.js",
|
||||
"version": "1.0.0-alpha.9",
|
||||
"main": "dist/html2canvas.js",
|
||||
"module": "dist/html2canvas.esm.js",
|
||||
"typings": "dist/types/index.d.ts",
|
||||
"browser": "dist/html2canvas.js",
|
||||
"version": "1.4.1",
|
||||
"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,102 @@
|
||||
"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",
|
||||
"@rollup/plugin-commonjs": "^19.0.0",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^13.0.0",
|
||||
"@rollup/plugin-typescript": "^8.2.1",
|
||||
"@types/chai": "^4.1.7",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/jest-image-snapshot": "^4.3.1",
|
||||
"@types/karma": "^6.3.0",
|
||||
"@types/mocha": "^8.2.3",
|
||||
"@types/node": "^16.3.1",
|
||||
"@types/platform": "^1.3.4",
|
||||
"@types/promise-polyfill": "^6.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.2",
|
||||
"@typescript-eslint/parser": "^4.28.2",
|
||||
"appium-ios-simulator": "^3.10.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-loader": "^8.0.5",
|
||||
"babel-plugin-add-module-exports": "^1.0.2",
|
||||
"babel-plugin-dev-expression": "^0.2.1",
|
||||
"base64-arraybuffer": "1.0.1",
|
||||
"body-parser": "^1.19.0",
|
||||
"chai": "4.1.1",
|
||||
"chromeless": "1.2.0",
|
||||
"cors": "2.8.4",
|
||||
"eslint": "4.2.0",
|
||||
"eslint-plugin-flowtype": "2.35.0",
|
||||
"eslint-plugin-prettier": "2.1.2",
|
||||
"express": "4.15.4",
|
||||
"chromeless": "^1.5.2",
|
||||
"cors": "^2.8.5",
|
||||
"es6-promise": "^4.2.8",
|
||||
"eslint": "^7.30.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "3.4.0",
|
||||
"express": "^4.17.1",
|
||||
"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",
|
||||
"platform": "1.3.4",
|
||||
"prettier": "1.5.3",
|
||||
"promise-polyfill": "6.0.2",
|
||||
"glob": "7.1.3",
|
||||
"html2canvas-proxy": "1.0.1",
|
||||
"jest": "^27.0.6",
|
||||
"jest-image-snapshot": "^4.5.1",
|
||||
"jquery": "^3.5.1",
|
||||
"js-polyfills": "^0.1.42",
|
||||
"karma": "^6.3.2",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
"karma-edge-launcher": "^0.4.2",
|
||||
"karma-firefox-launcher": "^2.1.0",
|
||||
"karma-ie-launcher": "^1.0.0",
|
||||
"karma-junit-reporter": "^2.0.1",
|
||||
"karma-mocha": "^2.0.1",
|
||||
"karma-safarinative-launcher": "^1.1.0",
|
||||
"karma-sauce-launcher": "^2.0.2",
|
||||
"mocha": "^9.0.2",
|
||||
"node-simctl": "^5.3.0",
|
||||
"platform": "^1.3.6",
|
||||
"prettier": "^2.3.2",
|
||||
"replace-in-file": "^3.0.0",
|
||||
"rimraf": "2.6.1",
|
||||
"serve-index": "1.9.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "^2.53.1",
|
||||
"rollup-plugin-sourcemaps": "^0.6.3",
|
||||
"serve-index": "^1.9.1",
|
||||
"slash": "1.0.0",
|
||||
"uglifyjs-webpack-plugin": "^1.1.2",
|
||||
"webpack": "3.4.1"
|
||||
"standard-version": "^8.0.2",
|
||||
"ts-jest": "^27.0.3",
|
||||
"ts-loader": "^8.3.0",
|
||||
"ts-node": "^10.1.0",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript": "^4.3.5",
|
||||
"uglify-js": "^3.13.10",
|
||||
"uglifyjs-webpack-plugin": "^2.2.0",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"yargs": "^17.0.1"
|
||||
},
|
||||
"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,www,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",
|
||||
"karma": "node karma",
|
||||
"watch": "webpack --progress --colors --watch",
|
||||
"start": "node tests/server"
|
||||
"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 --max-warnings 0",
|
||||
"test": "npm run lint && npm run unittest && npm run karma",
|
||||
"unittest": "jest",
|
||||
"reftests-diff": "mkdirp tmp/snapshots && jest --roots=tests --testMatch=**/reftest-diff.ts",
|
||||
"karma": "ts-node tests/karma",
|
||||
"watch": "rollup -c rollup.config.ts -w",
|
||||
"watch:unittest": "mocha --require ts-node/register --watch-extensions ts -w src/**/__tests__/*.ts",
|
||||
"start": "ts-node tests/server --port=8080 --cors=8081"
|
||||
},
|
||||
"homepage": "https://html2canvas.hertzen.com",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-line-break": "1.0.1"
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
}
|
||||
}
|
||||
|
42
rollup.config.ts
Normal file
42
rollup.config.ts
Normal 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-typescript';
|
||||
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({ sourceMap: true, inlineSources: true }),
|
||||
// Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
|
||||
commonjs({
|
||||
include: 'node_modules/**'
|
||||
}),
|
||||
|
||||
// Resolve source maps to the original source
|
||||
sourceMaps(),
|
||||
],
|
||||
}
|
@ -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`);
|
||||
}
|
||||
);
|
47
scripts/create-reftest-list.ts
Normal file
47
scripts/create-reftest-list.ts
Normal 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`);
|
44
scripts/create-reftest-result-list.ts
Normal file
44
scripts/create-reftest-result-list.ts
Normal file
@ -0,0 +1,44 @@
|
||||
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 (json.test) {
|
||||
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}`);
|
24
src/Angle.js
24
src/Angle.js
@ -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;
|
||||
};
|
370
src/Bounds.js
370
src/Bounds.js
@ -1,370 +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 => {
|
||||
let tlh = borderRadius[CORNER.TOP_LEFT][H].getAbsoluteValue(bounds.width);
|
||||
let tlv = borderRadius[CORNER.TOP_LEFT][V].getAbsoluteValue(bounds.height);
|
||||
let trh = borderRadius[CORNER.TOP_RIGHT][H].getAbsoluteValue(bounds.width);
|
||||
let trv = borderRadius[CORNER.TOP_RIGHT][V].getAbsoluteValue(bounds.height);
|
||||
let brh = borderRadius[CORNER.BOTTOM_RIGHT][H].getAbsoluteValue(bounds.width);
|
||||
let brv = borderRadius[CORNER.BOTTOM_RIGHT][V].getAbsoluteValue(bounds.height);
|
||||
let blh = borderRadius[CORNER.BOTTOM_LEFT][H].getAbsoluteValue(bounds.width);
|
||||
let blv = borderRadius[CORNER.BOTTOM_LEFT][V].getAbsoluteValue(bounds.height);
|
||||
|
||||
const factors = [];
|
||||
factors.push((tlh + trh) / bounds.width);
|
||||
factors.push((blh + brh) / bounds.width);
|
||||
factors.push((tlv + blv) / bounds.height);
|
||||
factors.push((trv + brv) / bounds.height);
|
||||
const maxFactor = Math.max(...factors);
|
||||
|
||||
if (maxFactor > 1) {
|
||||
tlh /= maxFactor;
|
||||
tlv /= maxFactor;
|
||||
trh /= maxFactor;
|
||||
trv /= maxFactor;
|
||||
brh /= maxFactor;
|
||||
brv /= maxFactor;
|
||||
blh /= maxFactor;
|
||||
blv /= maxFactor;
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
};
|
663
src/Clone.js
663
src/Clone.js
@ -1,663 +0,0 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
import type {Bounds} from './Bounds';
|
||||
import type {Options} from './index';
|
||||
import type {PseudoContentData, PseudoContentItem} from './PseudoNodeContent';
|
||||
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';
|
||||
import {
|
||||
parseCounterReset,
|
||||
popCounters,
|
||||
resolvePseudoContent,
|
||||
PSEUDO_CONTENT_ITEM_TYPE
|
||||
} from './PseudoNodeContent';
|
||||
|
||||
const IGNORE_ATTRIBUTE = 'data-html2canvas-ignore';
|
||||
|
||||
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<*>;
|
||||
pseudoContentData: PseudoContentData;
|
||||
|
||||
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);
|
||||
this.pseudoContentData = {
|
||||
counters: {},
|
||||
quoteDepth: 0
|
||||
};
|
||||
// $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,
|
||||
logging: this.options.logging,
|
||||
proxy: this.options.proxy,
|
||||
removeContainer: this.options.removeContainer,
|
||||
scale: this.options.scale,
|
||||
foreignObjectRendering: this.options.foreignObjectRendering,
|
||||
useCORS: this.options.useCORS,
|
||||
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;
|
||||
}
|
||||
|
||||
if (node instanceof HTMLStyleElement && node.sheet && node.sheet.cssRules) {
|
||||
const css = [].slice
|
||||
.call(node.sheet.cssRules, 0)
|
||||
.reduce((css, rule) => css + rule.cssText, '');
|
||||
const style = node.cloneNode(false);
|
||||
style.textContent = css;
|
||||
return style;
|
||||
}
|
||||
|
||||
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;
|
||||
const style = node instanceof window.HTMLElement ? window.getComputedStyle(node) : null;
|
||||
const styleBefore =
|
||||
node instanceof window.HTMLElement ? window.getComputedStyle(node, ':before') : null;
|
||||
const styleAfter =
|
||||
node instanceof window.HTMLElement ? window.getComputedStyle(node, ':after') : null;
|
||||
|
||||
if (this.referenceElement === node && clone instanceof window.HTMLElement) {
|
||||
this.clonedReferenceElement = clone;
|
||||
}
|
||||
|
||||
if (clone instanceof window.HTMLBodyElement) {
|
||||
createPseudoHideStyles(clone);
|
||||
}
|
||||
|
||||
const counters = parseCounterReset(style, this.pseudoContentData);
|
||||
const contentBefore = resolvePseudoContent(node, styleBefore, this.pseudoContentData);
|
||||
|
||||
for (let child = node.firstChild; child; child = child.nextSibling) {
|
||||
if (
|
||||
child.nodeType !== Node.ELEMENT_NODE ||
|
||||
// $FlowFixMe
|
||||
(child.nodeName !== 'SCRIPT' && !child.hasAttribute(IGNORE_ATTRIBUTE))
|
||||
) {
|
||||
if (!this.copyStyles || child.nodeName !== 'STYLE') {
|
||||
clone.appendChild(this.cloneNode(child));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const contentAfter = resolvePseudoContent(node, styleAfter, this.pseudoContentData);
|
||||
popCounters(counters, this.pseudoContentData);
|
||||
|
||||
if (node instanceof window.HTMLElement && clone instanceof window.HTMLElement) {
|
||||
if (styleBefore) {
|
||||
this.inlineAllImages(
|
||||
inlinePseudoElement(node, clone, styleBefore, contentBefore, PSEUDO_BEFORE)
|
||||
);
|
||||
}
|
||||
if (styleAfter) {
|
||||
this.inlineAllImages(
|
||||
inlinePseudoElement(node, clone, styleAfter, contentAfter, PSEUDO_AFTER)
|
||||
);
|
||||
}
|
||||
if (style && this.copyStyles && !(node instanceof HTMLIFrameElement)) {
|
||||
copyCSSStyles(style, clone);
|
||||
}
|
||||
this.inlineAllImages(clone);
|
||||
if (node.scrollTop !== 0 || node.scrollLeft !== 0) {
|
||||
this.scrolledElements.push([clone, 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;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const clonedCtx = clonedCanvas.getContext('2d');
|
||||
if (ctx) {
|
||||
clonedCtx.putImageData(ctx.getImageData(0, 0, canvas.width, canvas.height), 0, 0);
|
||||
} else {
|
||||
clonedCtx.drawImage(canvas, 0, 0);
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const inlinePseudoElement = (
|
||||
node: HTMLElement,
|
||||
clone: HTMLElement,
|
||||
style: CSSStyleDeclaration,
|
||||
contentItems: ?Array<PseudoContentItem>,
|
||||
pseudoElt: ':before' | ':after'
|
||||
): ?HTMLElement => {
|
||||
if (
|
||||
!style ||
|
||||
!style.content ||
|
||||
style.content === 'none' ||
|
||||
style.content === '-moz-alt-content' ||
|
||||
style.display === 'none'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const anonymousReplacedElement = clone.ownerDocument.createElement('html2canvaspseudoelement');
|
||||
copyCSSStyles(style, anonymousReplacedElement);
|
||||
|
||||
if (contentItems) {
|
||||
const len = contentItems.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
const item = contentItems[i];
|
||||
switch (item.type) {
|
||||
case PSEUDO_CONTENT_ITEM_TYPE.IMAGE:
|
||||
const img = clone.ownerDocument.createElement('img');
|
||||
img.src = parseBackgroundImage(`url(${item.value})`)[0].args[0];
|
||||
img.style.opacity = '1';
|
||||
anonymousReplacedElement.appendChild(img);
|
||||
break;
|
||||
case PSEUDO_CONTENT_ITEM_TYPE.TEXT:
|
||||
anonymousReplacedElement.appendChild(
|
||||
clone.ownerDocument.createTextNode(item.value)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anonymousReplacedElement.className = `${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE} ${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}`;
|
||||
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 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
|
||||
cloneIframeContainer.setAttribute(IGNORE_ATTRIBUTE, 'true');
|
||||
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(`${serializeDoctype(document.doctype)}<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;
|
||||
});
|
||||
};
|
||||
|
||||
const serializeDoctype = (doctype: ?DocumentType): string => {
|
||||
let str = '';
|
||||
if (doctype) {
|
||||
str += '<!DOCTYPE ';
|
||||
if (doctype.name) {
|
||||
str += doctype.name;
|
||||
}
|
||||
|
||||
if (doctype.internalSubset) {
|
||||
str += doctype.internalSubset;
|
||||
}
|
||||
|
||||
if (doctype.publicId) {
|
||||
str += `"${doctype.publicId}"`;
|
||||
}
|
||||
|
||||
if (doctype.systemId) {
|
||||
str += `"${doctype.systemId}"`;
|
||||
}
|
||||
|
||||
str += '>';
|
||||
}
|
||||
|
||||
return str;
|
||||
};
|
251
src/Color.js
251
src/Color.js
@ -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]);
|
179
src/Feature.js
179
src/Feature.js
@ -1,179 +0,0 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import {createForeignObjectSVG, loadSerializedSVG} from './renderer/ForeignObjectRenderer';
|
||||
|
||||
const testRangeBounds = document => {
|
||||
const TEST_HEIGHT = 123;
|
||||
|
||||
if (document.createRange) {
|
||||
const range = document.createRange();
|
||||
if (range.getBoundingClientRect) {
|
||||
const testElement = document.createElement('boundtest');
|
||||
testElement.style.height = `${TEST_HEIGHT}px`;
|
||||
testElement.style.display = 'block';
|
||||
document.body.appendChild(testElement);
|
||||
|
||||
range.selectNode(testElement);
|
||||
const rangeBounds = range.getBoundingClientRect();
|
||||
const rangeHeight = Math.round(rangeBounds.height);
|
||||
document.body.removeChild(testElement);
|
||||
if (rangeHeight === TEST_HEIGHT) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// iOS 10.3 taints canvas with base64 images unless crossOrigin = 'anonymous'
|
||||
const testBase64 = (document: Document, src: string): Promise<boolean> => {
|
||||
const img = new Image();
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
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 {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
canvas.toDataURL();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isGreenPixel = data => data[0] === 0 && data[1] === 255 && data[2] === 0 && data[3] === 255;
|
||||
|
||||
const testForeignObject = document => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const size = 100;
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = 'rgb(0, 255, 0)';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
const img = new Image();
|
||||
const greenImageSrc = canvas.toDataURL();
|
||||
img.src = greenImageSrc;
|
||||
const svg = createForeignObjectSVG(size, size, 0, 0, img);
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
return loadSerializedSVG(svg)
|
||||
.then(img => {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const data = ctx.getImageData(0, 0, size, size).data;
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
const node = document.createElement('div');
|
||||
node.style.backgroundImage = `url(${greenImageSrc})`;
|
||||
node.style.height = `${size}px`;
|
||||
// Firefox 55 does not render inline <img /> tags
|
||||
return isGreenPixel(data)
|
||||
? loadSerializedSVG(createForeignObjectSVG(size, size, 0, 0, node))
|
||||
: Promise.reject(false);
|
||||
})
|
||||
.then(img => {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
// Edge does not render background-images
|
||||
return isGreenPixel(ctx.getImageData(0, 0, size, size).data);
|
||||
})
|
||||
.catch(e => false);
|
||||
};
|
||||
|
||||
const FEATURES = {
|
||||
// $FlowFixMe - get/set properties not yet supported
|
||||
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 =
|
||||
typeof Array.from === 'function' && typeof window.fetch === 'function'
|
||||
? testForeignObject(document)
|
||||
: Promise.resolve(false);
|
||||
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();
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', {value});
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
export default FEATURES;
|
558
src/Gradient.js
558
src/Gradient.js
@ -1,558 +0,0 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type {BackgroundSource} from './parsing/background';
|
||||
import type {Bounds} from './Bounds';
|
||||
import NodeContainer from './NodeContainer';
|
||||
import {parseAngle} from './Angle';
|
||||
import Color from './Color';
|
||||
import Length, {LENGTH_TYPE, calculateLengthFromValueWithUnit} from './Length';
|
||||
import {distance} from './Util';
|
||||
|
||||
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_COLORSTOP = /^(from|to|color-stop)\((?:([\d.]+)(%)?,\s*)?(.+?)\)$/i;
|
||||
const RADIAL_SHAPE_DEFINITION = /^\s*(circle|ellipse)?\s*((?:([\d.]+)(px|r?em|%)\s*(?:([\d.]+)(px|r?em|%))?)|closest-side|closest-corner|farthest-side|farthest-corner)?\s*(?:at\s*(?:(left|center|right)|([\d.]+)(px|r?em|%))\s+(?:(top|center|bottom)|([\d.]+)(px|r?em|%)))?(?:\s|$)/i;
|
||||
|
||||
export type Point = {
|
||||
x: number,
|
||||
y: number
|
||||
};
|
||||
|
||||
export type Direction = {
|
||||
x0: number,
|
||||
x1: number,
|
||||
y0: number,
|
||||
y1: number
|
||||
};
|
||||
|
||||
export type ColorStop = {
|
||||
color: Color,
|
||||
stop: number
|
||||
};
|
||||
|
||||
export interface Gradient {
|
||||
type: GradientType,
|
||||
colorStops: Array<ColorStop>
|
||||
}
|
||||
|
||||
export const GRADIENT_TYPE = {
|
||||
LINEAR_GRADIENT: 0,
|
||||
RADIAL_GRADIENT: 1
|
||||
};
|
||||
|
||||
export type GradientType = $Values<typeof GRADIENT_TYPE>;
|
||||
|
||||
export const RADIAL_GRADIENT_SHAPE = {
|
||||
CIRCLE: 0,
|
||||
ELLIPSE: 1
|
||||
};
|
||||
|
||||
export type RadialGradientShapeType = $Values<typeof RADIAL_GRADIENT_SHAPE>;
|
||||
|
||||
const LENGTH_FOR_POSITION = {
|
||||
left: new Length('0%'),
|
||||
top: new Length('0%'),
|
||||
center: new Length('50%'),
|
||||
right: new Length('100%'),
|
||||
bottom: new Length('100%')
|
||||
};
|
||||
|
||||
export class LinearGradient implements Gradient {
|
||||
type: GradientType;
|
||||
colorStops: Array<ColorStop>;
|
||||
direction: Direction;
|
||||
|
||||
constructor(colorStops: Array<ColorStop>, direction: Direction) {
|
||||
this.type = GRADIENT_TYPE.LINEAR_GRADIENT;
|
||||
this.colorStops = colorStops;
|
||||
this.direction = direction;
|
||||
}
|
||||
}
|
||||
|
||||
export class RadialGradient implements Gradient {
|
||||
type: GradientType;
|
||||
colorStops: Array<ColorStop>;
|
||||
shape: RadialGradientShapeType;
|
||||
center: Point;
|
||||
radius: Point;
|
||||
|
||||
constructor(
|
||||
colorStops: Array<ColorStop>,
|
||||
shape: RadialGradientShapeType,
|
||||
center: Point,
|
||||
radius: Point
|
||||
) {
|
||||
this.type = GRADIENT_TYPE.RADIAL_GRADIENT;
|
||||
this.colorStops = colorStops;
|
||||
this.shape = shape;
|
||||
this.center = center;
|
||||
this.radius = radius;
|
||||
}
|
||||
}
|
||||
|
||||
export const parseGradient = (
|
||||
container: NodeContainer,
|
||||
{args, method, prefix}: BackgroundSource,
|
||||
bounds: Bounds
|
||||
): ?Gradient => {
|
||||
if (method === 'linear-gradient') {
|
||||
return parseLinearGradient(args, bounds, !!prefix);
|
||||
} else if (method === 'gradient' && args[0] === 'linear') {
|
||||
// TODO handle correct angle
|
||||
return parseLinearGradient(
|
||||
['to bottom'].concat(transformObsoleteColorStops(args.slice(3))),
|
||||
bounds,
|
||||
!!prefix
|
||||
);
|
||||
} else if (method === 'radial-gradient') {
|
||||
return parseRadialGradient(
|
||||
container,
|
||||
prefix === '-webkit-' ? transformWebkitRadialGradientArgs(args) : args,
|
||||
bounds
|
||||
);
|
||||
} else if (method === 'gradient' && args[0] === 'radial') {
|
||||
return parseRadialGradient(
|
||||
container,
|
||||
transformObsoleteColorStops(transformWebkitRadialGradientArgs(args.slice(1))),
|
||||
bounds
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const parseColorStops = (args: Array<string>, firstColorStopIndex: number, lineLength: number) => {
|
||||
const colorStops = [];
|
||||
|
||||
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});
|
||||
}
|
||||
|
||||
const absoluteValuedColorStops = colorStops.map(({color, stop}) => {
|
||||
const absoluteStop =
|
||||
lineLength === 0 ? 0 : stop ? stop.getAbsoluteValue(lineLength) / lineLength : null;
|
||||
|
||||
return {
|
||||
color,
|
||||
// $FlowFixMe
|
||||
stop: absoluteStop
|
||||
};
|
||||
});
|
||||
|
||||
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 absoluteValuedColorStops;
|
||||
};
|
||||
|
||||
const parseLinearGradient = (
|
||||
args: Array<string>,
|
||||
bounds: Bounds,
|
||||
hasPrefix: boolean
|
||||
): LinearGradient => {
|
||||
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(
|
||||
// if there is a prefix, the 0° angle points due East (instead of North per W3C)
|
||||
hasPrefix ? angle - Math.PI * 0.5 : angle,
|
||||
bounds
|
||||
)
|
||||
: HAS_SIDE_OR_CORNER
|
||||
? parseSideOrCorner(args[0], bounds)
|
||||
: parsePercentageAngle(args[0], bounds)
|
||||
: calculateGradientDirection(Math.PI, bounds);
|
||||
const firstColorStopIndex = HAS_DIRECTION ? 1 : 0;
|
||||
|
||||
// TODO: Fix some inaccuracy with color stops with px values
|
||||
const lineLength = Math.min(
|
||||
distance(
|
||||
Math.abs(direction.x0) + Math.abs(direction.x1),
|
||||
Math.abs(direction.y0) + Math.abs(direction.y1)
|
||||
),
|
||||
bounds.width * 2,
|
||||
bounds.height * 2
|
||||
);
|
||||
|
||||
return new LinearGradient(parseColorStops(args, firstColorStopIndex, lineLength), direction);
|
||||
};
|
||||
|
||||
const parseRadialGradient = (
|
||||
container: NodeContainer,
|
||||
args: Array<string>,
|
||||
bounds: Bounds
|
||||
): RadialGradient => {
|
||||
const m = args[0].match(RADIAL_SHAPE_DEFINITION);
|
||||
const shape =
|
||||
m &&
|
||||
(m[1] === 'circle' || // explicit shape specification
|
||||
(m[3] !== undefined && m[5] === undefined)) // only one radius coordinate
|
||||
? RADIAL_GRADIENT_SHAPE.CIRCLE
|
||||
: RADIAL_GRADIENT_SHAPE.ELLIPSE;
|
||||
const radius = {};
|
||||
const center = {};
|
||||
|
||||
if (m) {
|
||||
// Radius
|
||||
if (m[3] !== undefined) {
|
||||
radius.x = calculateLengthFromValueWithUnit(container, m[3], m[4]).getAbsoluteValue(
|
||||
bounds.width
|
||||
);
|
||||
}
|
||||
|
||||
if (m[5] !== undefined) {
|
||||
radius.y = calculateLengthFromValueWithUnit(container, m[5], m[6]).getAbsoluteValue(
|
||||
bounds.height
|
||||
);
|
||||
}
|
||||
|
||||
// Position
|
||||
if (m[7]) {
|
||||
center.x = LENGTH_FOR_POSITION[m[7].toLowerCase()];
|
||||
} else if (m[8] !== undefined) {
|
||||
center.x = calculateLengthFromValueWithUnit(container, m[8], m[9]);
|
||||
}
|
||||
|
||||
if (m[10]) {
|
||||
center.y = LENGTH_FOR_POSITION[m[10].toLowerCase()];
|
||||
} else if (m[11] !== undefined) {
|
||||
center.y = calculateLengthFromValueWithUnit(container, m[11], m[12]);
|
||||
}
|
||||
}
|
||||
|
||||
const gradientCenter = {
|
||||
x: center.x === undefined ? bounds.width / 2 : center.x.getAbsoluteValue(bounds.width),
|
||||
y: center.y === undefined ? bounds.height / 2 : center.y.getAbsoluteValue(bounds.height)
|
||||
};
|
||||
const gradientRadius = calculateRadius(
|
||||
(m && m[2]) || 'farthest-corner',
|
||||
shape,
|
||||
gradientCenter,
|
||||
radius,
|
||||
bounds
|
||||
);
|
||||
|
||||
return new RadialGradient(
|
||||
parseColorStops(args, m ? 1 : 0, Math.min(gradientRadius.x, gradientRadius.y)),
|
||||
shape,
|
||||
gradientCenter,
|
||||
gradientRadius
|
||||
);
|
||||
};
|
||||
|
||||
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 / (distance(bounds.width, bounds.height) / 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);
|
||||
};
|
||||
|
||||
const findCorner = (bounds: Bounds, x: number, y: number, closest: boolean): Point => {
|
||||
var corners = [
|
||||
{x: 0, y: 0},
|
||||
{x: 0, y: bounds.height},
|
||||
{x: bounds.width, y: 0},
|
||||
{x: bounds.width, y: bounds.height}
|
||||
];
|
||||
|
||||
// $FlowFixMe
|
||||
return corners.reduce(
|
||||
(stat, corner) => {
|
||||
const d = distance(x - corner.x, y - corner.y);
|
||||
if (closest ? d < stat.optimumDistance : d > stat.optimumDistance) {
|
||||
return {
|
||||
optimumCorner: corner,
|
||||
optimumDistance: d
|
||||
};
|
||||
}
|
||||
|
||||
return stat;
|
||||
},
|
||||
{
|
||||
optimumDistance: closest ? Infinity : -Infinity,
|
||||
optimumCorner: null
|
||||
}
|
||||
).optimumCorner;
|
||||
};
|
||||
|
||||
const calculateRadius = (
|
||||
extent: string,
|
||||
shape: RadialGradientShapeType,
|
||||
center: Point,
|
||||
radius: Point,
|
||||
bounds: Bounds
|
||||
): Point => {
|
||||
const x = center.x;
|
||||
const y = center.y;
|
||||
let rx = 0;
|
||||
let ry = 0;
|
||||
|
||||
switch (extent) {
|
||||
case 'closest-side':
|
||||
// The ending shape is sized so that that it exactly meets the side of the gradient box closest to the gradient’s center.
|
||||
// If the shape is an ellipse, it exactly meets the closest side in each dimension.
|
||||
if (shape === RADIAL_GRADIENT_SHAPE.CIRCLE) {
|
||||
rx = ry = Math.min(
|
||||
Math.abs(x),
|
||||
Math.abs(x - bounds.width),
|
||||
Math.abs(y),
|
||||
Math.abs(y - bounds.height)
|
||||
);
|
||||
} else if (shape === RADIAL_GRADIENT_SHAPE.ELLIPSE) {
|
||||
rx = Math.min(Math.abs(x), Math.abs(x - bounds.width));
|
||||
ry = Math.min(Math.abs(y), Math.abs(y - bounds.height));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'closest-corner':
|
||||
// The ending shape is sized so that that it passes through the corner of the gradient box closest to the gradient’s center.
|
||||
// If the shape is an ellipse, the ending shape is given the same aspect-ratio it would have if closest-side were specified.
|
||||
if (shape === RADIAL_GRADIENT_SHAPE.CIRCLE) {
|
||||
rx = ry = Math.min(
|
||||
distance(x, y),
|
||||
distance(x, y - bounds.height),
|
||||
distance(x - bounds.width, y),
|
||||
distance(x - bounds.width, y - bounds.height)
|
||||
);
|
||||
} else if (shape === RADIAL_GRADIENT_SHAPE.ELLIPSE) {
|
||||
// Compute the ratio ry/rx (which is to be the same as for "closest-side")
|
||||
const c =
|
||||
Math.min(Math.abs(y), Math.abs(y - bounds.height)) /
|
||||
Math.min(Math.abs(x), Math.abs(x - bounds.width));
|
||||
const corner = findCorner(bounds, x, y, true);
|
||||
rx = distance(corner.x - x, (corner.y - y) / c);
|
||||
ry = c * rx;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'farthest-side':
|
||||
// Same as closest-side, except the ending shape is sized based on the farthest side(s)
|
||||
if (shape === RADIAL_GRADIENT_SHAPE.CIRCLE) {
|
||||
rx = ry = Math.max(
|
||||
Math.abs(x),
|
||||
Math.abs(x - bounds.width),
|
||||
Math.abs(y),
|
||||
Math.abs(y - bounds.height)
|
||||
);
|
||||
} else if (shape === RADIAL_GRADIENT_SHAPE.ELLIPSE) {
|
||||
rx = Math.max(Math.abs(x), Math.abs(x - bounds.width));
|
||||
ry = Math.max(Math.abs(y), Math.abs(y - bounds.height));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'farthest-corner':
|
||||
// Same as closest-corner, except the ending shape is sized based on the farthest corner.
|
||||
// If the shape is an ellipse, the ending shape is given the same aspect ratio it would have if farthest-side were specified.
|
||||
if (shape === RADIAL_GRADIENT_SHAPE.CIRCLE) {
|
||||
rx = ry = Math.max(
|
||||
distance(x, y),
|
||||
distance(x, y - bounds.height),
|
||||
distance(x - bounds.width, y),
|
||||
distance(x - bounds.width, y - bounds.height)
|
||||
);
|
||||
} else if (shape === RADIAL_GRADIENT_SHAPE.ELLIPSE) {
|
||||
// Compute the ratio ry/rx (which is to be the same as for "farthest-side")
|
||||
const c =
|
||||
Math.max(Math.abs(y), Math.abs(y - bounds.height)) /
|
||||
Math.max(Math.abs(x), Math.abs(x - bounds.width));
|
||||
const corner = findCorner(bounds, x, y, false);
|
||||
rx = distance(corner.x - x, (corner.y - y) / c);
|
||||
ry = c * rx;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// pixel or percentage values
|
||||
rx = radius.x || 0;
|
||||
ry = radius.y !== undefined ? radius.y : rx;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
x: rx,
|
||||
y: ry
|
||||
};
|
||||
};
|
||||
|
||||
export const transformWebkitRadialGradientArgs = (args: Array<string>): Array<string> => {
|
||||
let shape = '';
|
||||
let radius = '';
|
||||
let extent = '';
|
||||
let position = '';
|
||||
let idx = 0;
|
||||
|
||||
const POSITION = /^(left|center|right|\d+(?:px|r?em|%)?)(?:\s+(top|center|bottom|\d+(?:px|r?em|%)?))?$/i;
|
||||
const SHAPE_AND_EXTENT = /^(circle|ellipse)?\s*(closest-side|closest-corner|farthest-side|farthest-corner|contain|cover)?$/i;
|
||||
const RADIUS = /^\d+(px|r?em|%)?(?:\s+\d+(px|r?em|%)?)?$/i;
|
||||
|
||||
const matchStartPosition = args[idx].match(POSITION);
|
||||
if (matchStartPosition) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
const matchShapeExtent = args[idx].match(SHAPE_AND_EXTENT);
|
||||
if (matchShapeExtent) {
|
||||
shape = matchShapeExtent[1] || '';
|
||||
extent = matchShapeExtent[2] || '';
|
||||
if (extent === 'contain') {
|
||||
extent = 'closest-side';
|
||||
} else if (extent === 'cover') {
|
||||
extent = 'farthest-corner';
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
|
||||
const matchStartRadius = args[idx].match(RADIUS);
|
||||
if (matchStartRadius) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
const matchEndPosition = args[idx].match(POSITION);
|
||||
if (matchEndPosition) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
const matchEndRadius = args[idx].match(RADIUS);
|
||||
if (matchEndRadius) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
const matchPosition = matchEndPosition || matchStartPosition;
|
||||
if (matchPosition && matchPosition[1]) {
|
||||
position = matchPosition[1] + (/^\d+$/.test(matchPosition[1]) ? 'px' : '');
|
||||
if (matchPosition[2]) {
|
||||
position += ' ' + matchPosition[2] + (/^\d+$/.test(matchPosition[2]) ? 'px' : '');
|
||||
}
|
||||
}
|
||||
|
||||
const matchRadius = matchEndRadius || matchStartRadius;
|
||||
if (matchRadius) {
|
||||
radius = matchRadius[0];
|
||||
if (!matchRadius[1]) {
|
||||
radius += 'px';
|
||||
}
|
||||
}
|
||||
|
||||
if (position && !shape && !radius && !extent) {
|
||||
radius = position;
|
||||
position = '';
|
||||
}
|
||||
|
||||
if (position) {
|
||||
position = `at ${position}`;
|
||||
}
|
||||
|
||||
return [[shape, extent, radius, position].filter(s => !!s).join(' ')].concat(args.slice(idx));
|
||||
};
|
||||
|
||||
const transformObsoleteColorStops = (args: Array<string>): Array<string> => {
|
||||
return (
|
||||
args
|
||||
.map(color => color.match(FROM_TO_COLORSTOP))
|
||||
// $FlowFixMe
|
||||
.map((v: Array<string>, index: number) => {
|
||||
if (!v) {
|
||||
return args[index];
|
||||
}
|
||||
|
||||
switch (v[1]) {
|
||||
case 'from':
|
||||
return `${v[4]} 0%`;
|
||||
case 'to':
|
||||
return `${v[4]} 100%`;
|
||||
case 'color-stop':
|
||||
if (v[3] === '%') {
|
||||
return `${v[4]} ${v[2]}`;
|
||||
}
|
||||
return `${v[4]} ${parseFloat(v[2]) * 100}%`;
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
155
src/Input.js
155
src/Input.js
@ -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 = 'absolute';
|
||||
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;
|
||||
};
|
@ -1,68 +0,0 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import NodeContainer from './NodeContainer';
|
||||
|
||||
const LENGTH_WITH_UNIT = /([\d.]+)(px|r?em|%)/i;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const getRootFontSize = (container: NodeContainer): number => {
|
||||
const parent = container.parent;
|
||||
return parent ? getRootFontSize(parent) : parseFloat(container.style.font.fontSize);
|
||||
};
|
||||
|
||||
export const calculateLengthFromValueWithUnit = (
|
||||
container: NodeContainer,
|
||||
value: string,
|
||||
unit: string
|
||||
): Length => {
|
||||
switch (unit) {
|
||||
case 'px':
|
||||
case '%':
|
||||
return new Length(value + unit);
|
||||
case 'em':
|
||||
case 'rem':
|
||||
const length = new Length(value);
|
||||
length.value *=
|
||||
unit === 'em'
|
||||
? parseFloat(container.style.font.fontSize)
|
||||
: getRootFontSize(container);
|
||||
return length;
|
||||
default:
|
||||
// TODO: handle correctly if unknown unit is used
|
||||
return new Length('0');
|
||||
}
|
||||
};
|
@ -1,48 +0,0 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export default class Logger {
|
||||
enabled: boolean;
|
||||
start: number;
|
||||
id: ?string;
|
||||
|
||||
constructor(enabled: boolean, id: ?string, start: ?number) {
|
||||
this.enabled = enabled;
|
||||
this.start = start ? start : Date.now();
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
child(id: string) {
|
||||
return new Logger(this.enabled, id, this.start);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line flowtype/no-weak-types
|
||||
log(...args: any) {
|
||||
if (this.enabled && 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 (this.enabled && 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))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,299 +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 {LineBreak} from './parsing/lineBreak';
|
||||
import type {ListStyle} from './parsing/listStyle';
|
||||
import type {Margin} from './parsing/margin';
|
||||
import type {Overflow} from './parsing/overflow';
|
||||
import type {OverflowWrap} from './parsing/overflowWrap';
|
||||
import type {Padding} from './parsing/padding';
|
||||
import type {Position} from './parsing/position';
|
||||
import type {TextShadow} from './parsing/textShadow';
|
||||
import type {TextTransform} from './parsing/textTransform';
|
||||
import type {TextDecoration} from './parsing/textDecoration';
|
||||
import type {Transform} from './parsing/transform';
|
||||
import type {Visibility} from './parsing/visibility';
|
||||
import type {WordBreak} from './parsing/word-break';
|
||||
import type {zIndex} from './parsing/zIndex';
|
||||
|
||||
import type {Bounds, BoundCurves} from './Bounds';
|
||||
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 {parseLineBreak} from './parsing/lineBreak';
|
||||
import {parseListStyle} from './parsing/listStyle';
|
||||
import {parseMargin} from './parsing/margin';
|
||||
import {parseOverflow, OVERFLOW} from './parsing/overflow';
|
||||
import {parseOverflowWrap} from './parsing/overflowWrap';
|
||||
import {parsePadding} from './parsing/padding';
|
||||
import {parsePosition, POSITION} from './parsing/position';
|
||||
import {parseTextDecoration} from './parsing/textDecoration';
|
||||
import {parseTextShadow} from './parsing/textShadow';
|
||||
import {parseTextTransform} from './parsing/textTransform';
|
||||
import {parseTransform} from './parsing/transform';
|
||||
import {parseVisibility, VISIBILITY} from './parsing/visibility';
|
||||
import {parseWordBreak} from './parsing/word-break';
|
||||
import {parseZIndex} from './parsing/zIndex';
|
||||
|
||||
import {parseBounds, parseBoundCurves, calculatePaddingBoxPath} from './Bounds';
|
||||
import {
|
||||
INPUT_BACKGROUND,
|
||||
INPUT_BORDERS,
|
||||
INPUT_COLOR,
|
||||
getInputBorderRadius,
|
||||
reformatInputBounds
|
||||
} from './Input';
|
||||
import {getListOwner} from './ListItem';
|
||||
|
||||
type StyleDeclaration = {
|
||||
background: Background,
|
||||
border: Array<Border>,
|
||||
borderRadius: Array<BorderRadius>,
|
||||
color: Color,
|
||||
display: DisplayBit,
|
||||
float: Float,
|
||||
font: Font,
|
||||
letterSpacing: number,
|
||||
lineBreak: LineBreak,
|
||||
listStyle: ListStyle | null,
|
||||
margin: Margin,
|
||||
opacity: number,
|
||||
overflow: Overflow,
|
||||
overflowWrap: OverflowWrap,
|
||||
padding: Padding,
|
||||
position: Position,
|
||||
textDecoration: TextDecoration | null,
|
||||
textShadow: Array<TextShadow> | null,
|
||||
textTransform: TextTransform,
|
||||
transform: Transform,
|
||||
visibility: Visibility,
|
||||
wordBreak: WordBreak,
|
||||
zIndex: zIndex
|
||||
};
|
||||
|
||||
const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT'];
|
||||
|
||||
export default class NodeContainer {
|
||||
name: ?string;
|
||||
parent: ?NodeContainer;
|
||||
style: StyleDeclaration;
|
||||
childNodes: Array<TextContainer | Path>;
|
||||
listItems: Array<NodeContainer>;
|
||||
listIndex: ?number;
|
||||
listStart: ?number;
|
||||
bounds: Bounds;
|
||||
curvedBounds: BoundCurves;
|
||||
image: ?string;
|
||||
index: number;
|
||||
tagName: string;
|
||||
|
||||
constructor(
|
||||
node: HTMLElement | SVGSVGElement,
|
||||
parent: ?NodeContainer,
|
||||
resourceLoader: ResourceLoader,
|
||||
index: number
|
||||
) {
|
||||
this.parent = parent;
|
||||
this.tagName = node.tagName;
|
||||
this.index = index;
|
||||
this.childNodes = [];
|
||||
this.listItems = [];
|
||||
if (typeof node.start === 'number') {
|
||||
this.listStart = node.start;
|
||||
}
|
||||
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),
|
||||
listStyle: display === DISPLAY.LIST_ITEM ? parseListStyle(style) : null,
|
||||
lineBreak: parseLineBreak(style.lineBreak),
|
||||
margin: parseMargin(style),
|
||||
opacity: parseFloat(style.opacity),
|
||||
overflow:
|
||||
INPUT_TAGS.indexOf(node.tagName) === -1
|
||||
? parseOverflow(style.overflow)
|
||||
: OVERFLOW.HIDDEN,
|
||||
overflowWrap: parseOverflowWrap(
|
||||
style.overflowWrap ? style.overflowWrap : style.wordWrap
|
||||
),
|
||||
padding: parsePadding(style),
|
||||
position: position,
|
||||
textDecoration: parseTextDecoration(style),
|
||||
textShadow: parseTextShadow(style.textShadow),
|
||||
textTransform: parseTextTransform(style.textTransform),
|
||||
transform: parseTransform(style),
|
||||
visibility: parseVisibility(style.visibility),
|
||||
wordBreak: parseWordBreak(style.wordBreak),
|
||||
zIndex: parseZIndex(position !== POSITION.STATIC ? style.zIndex : 'auto')
|
||||
};
|
||||
|
||||
if (this.isTransformed()) {
|
||||
// getBoundingClientRect provides values post-transform, we want them without the transformation
|
||||
node.style.transform = 'matrix(1,0,0,1,0,0)';
|
||||
}
|
||||
|
||||
if (display === DISPLAY.LIST_ITEM) {
|
||||
const listOwner = getListOwner(this);
|
||||
if (listOwner) {
|
||||
const listIndex = listOwner.listItems.length;
|
||||
listOwner.listItems.push(this);
|
||||
this.listIndex =
|
||||
node.hasAttribute('value') && typeof node.value === 'number'
|
||||
? node.value
|
||||
: listIndex === 0
|
||||
? typeof listOwner.listStart === 'number' ? listOwner.listStart : 1
|
||||
: listOwner.listItems[listIndex - 1].listIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO move bound retrieval for all nodes to a later stage?
|
||||
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.VISIBLE;
|
||||
|
||||
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;
|
||||
};
|
@ -1,165 +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';
|
||||
import {inlineListItemElement} from './ListItem';
|
||||
import {LIST_STYLE_TYPE} from './parsing/listStyle';
|
||||
|
||||
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);
|
||||
} else if (
|
||||
container.style.listStyle &&
|
||||
container.style.listStyle.listStyleType !== LIST_STYLE_TYPE.NONE
|
||||
) {
|
||||
inlineListItemElement(childNode, container, resourceLoader);
|
||||
}
|
||||
|
||||
const SHOULD_TRAVERSE_CHILDREN = childNode.tagName !== 'TEXTAREA';
|
||||
const 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()
|
||||
);
|
||||
};
|
62
src/Proxy.js
62
src/Proxy.js
@ -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();
|
||||
});
|
||||
};
|
@ -1,341 +0,0 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import {createCounterText} from './ListItem';
|
||||
import {parseListStyleType} from './parsing/listStyle';
|
||||
|
||||
export const PSEUDO_CONTENT_ITEM_TYPE = {
|
||||
TEXT: 0,
|
||||
IMAGE: 1
|
||||
};
|
||||
|
||||
export const TOKEN_TYPE = {
|
||||
STRING: 0,
|
||||
ATTRIBUTE: 1,
|
||||
URL: 2,
|
||||
COUNTER: 3,
|
||||
COUNTERS: 4,
|
||||
OPENQUOTE: 5,
|
||||
CLOSEQUOTE: 6
|
||||
};
|
||||
|
||||
export type PseudoContentData = {
|
||||
counters: {[string]: Array<number>},
|
||||
quoteDepth: number
|
||||
};
|
||||
|
||||
export type PseudoContentItem = {
|
||||
type: $Values<typeof PSEUDO_CONTENT_ITEM_TYPE>,
|
||||
value: string
|
||||
};
|
||||
|
||||
export type Token = {
|
||||
type: $Values<typeof TOKEN_TYPE>,
|
||||
value?: string,
|
||||
name?: string,
|
||||
format?: string,
|
||||
glue?: string
|
||||
};
|
||||
|
||||
export const parseCounterReset = (
|
||||
style: ?CSSStyleDeclaration,
|
||||
data: PseudoContentData
|
||||
): Array<string> => {
|
||||
if (!style || !style.counterReset || style.counterReset === 'none') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const counterNames: Array<string> = [];
|
||||
const counterResets = style.counterReset.split(/\s*,\s*/);
|
||||
const lenCounterResets = counterResets.length;
|
||||
|
||||
for (let i = 0; i < lenCounterResets; i++) {
|
||||
const [counterName, initialValue] = counterResets[i].split(/\s+/);
|
||||
counterNames.push(counterName);
|
||||
let counter = data.counters[counterName];
|
||||
if (!counter) {
|
||||
counter = data.counters[counterName] = [];
|
||||
}
|
||||
counter.push(parseInt(initialValue || 0, 10));
|
||||
}
|
||||
|
||||
return counterNames;
|
||||
};
|
||||
|
||||
export const popCounters = (counterNames: Array<string>, data: PseudoContentData): void => {
|
||||
const lenCounters = counterNames.length;
|
||||
for (let i = 0; i < lenCounters; i++) {
|
||||
data.counters[counterNames[i]].pop();
|
||||
}
|
||||
};
|
||||
|
||||
export const resolvePseudoContent = (
|
||||
node: Node,
|
||||
style: ?CSSStyleDeclaration,
|
||||
data: PseudoContentData
|
||||
): ?Array<PseudoContentItem> => {
|
||||
if (
|
||||
!style ||
|
||||
!style.content ||
|
||||
style.content === 'none' ||
|
||||
style.content === '-moz-alt-content' ||
|
||||
style.display === 'none'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokens = parseContent(style.content);
|
||||
|
||||
const len = tokens.length;
|
||||
const contentItems: Array<PseudoContentItem> = [];
|
||||
let s = '';
|
||||
|
||||
// increment the counter (if there is a "counter-increment" declaration)
|
||||
const counterIncrement = style.counterIncrement;
|
||||
if (counterIncrement && counterIncrement !== 'none') {
|
||||
const [counterName, incrementValue] = counterIncrement.split(/\s+/);
|
||||
const counter = data.counters[counterName];
|
||||
if (counter) {
|
||||
counter[counter.length - 1] +=
|
||||
incrementValue === undefined ? 1 : parseInt(incrementValue, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// build the content string
|
||||
for (let i = 0; i < len; i++) {
|
||||
const token = tokens[i];
|
||||
switch (token.type) {
|
||||
case TOKEN_TYPE.STRING:
|
||||
s += token.value || '';
|
||||
break;
|
||||
|
||||
case TOKEN_TYPE.ATTRIBUTE:
|
||||
if (node instanceof HTMLElement && token.value) {
|
||||
s += node.getAttribute(token.value) || '';
|
||||
}
|
||||
break;
|
||||
|
||||
case TOKEN_TYPE.COUNTER:
|
||||
const counter = data.counters[token.name || ''];
|
||||
if (counter) {
|
||||
s += formatCounterValue([counter[counter.length - 1]], '', token.format);
|
||||
}
|
||||
break;
|
||||
|
||||
case TOKEN_TYPE.COUNTERS:
|
||||
const counters = data.counters[token.name || ''];
|
||||
if (counters) {
|
||||
s += formatCounterValue(counters, token.glue, token.format);
|
||||
}
|
||||
break;
|
||||
|
||||
case TOKEN_TYPE.OPENQUOTE:
|
||||
s += getQuote(style, true, data.quoteDepth);
|
||||
data.quoteDepth++;
|
||||
break;
|
||||
|
||||
case TOKEN_TYPE.CLOSEQUOTE:
|
||||
data.quoteDepth--;
|
||||
s += getQuote(style, false, data.quoteDepth);
|
||||
break;
|
||||
|
||||
case TOKEN_TYPE.URL:
|
||||
if (s) {
|
||||
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.TEXT, value: s});
|
||||
s = '';
|
||||
}
|
||||
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.IMAGE, value: token.value || ''});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (s) {
|
||||
contentItems.push({type: PSEUDO_CONTENT_ITEM_TYPE.TEXT, value: s});
|
||||
}
|
||||
|
||||
return contentItems;
|
||||
};
|
||||
|
||||
export const parseContent = (content: string, cache?: {[string]: Array<Token>}): Array<Token> => {
|
||||
if (cache && cache[content]) {
|
||||
return cache[content];
|
||||
}
|
||||
|
||||
const tokens: Array<Token> = [];
|
||||
const len = content.length;
|
||||
|
||||
let isString = false;
|
||||
let isEscaped = false;
|
||||
let isFunction = false;
|
||||
let str = '';
|
||||
let functionName = '';
|
||||
let args = [];
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const c = content.charAt(i);
|
||||
|
||||
switch (c) {
|
||||
case "'":
|
||||
case '"':
|
||||
if (isEscaped) {
|
||||
str += c;
|
||||
} else {
|
||||
isString = !isString;
|
||||
if (!isFunction && !isString) {
|
||||
tokens.push({type: TOKEN_TYPE.STRING, value: str});
|
||||
str = '';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case '\\':
|
||||
if (isEscaped) {
|
||||
str += c;
|
||||
isEscaped = false;
|
||||
} else {
|
||||
isEscaped = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case '(':
|
||||
if (isString) {
|
||||
str += c;
|
||||
} else {
|
||||
isFunction = true;
|
||||
functionName = str;
|
||||
str = '';
|
||||
args = [];
|
||||
}
|
||||
break;
|
||||
|
||||
case ')':
|
||||
if (isString) {
|
||||
str += c;
|
||||
} else if (isFunction) {
|
||||
if (str) {
|
||||
args.push(str);
|
||||
}
|
||||
|
||||
switch (functionName) {
|
||||
case 'attr':
|
||||
if (args.length > 0) {
|
||||
tokens.push({type: TOKEN_TYPE.ATTRIBUTE, value: args[0]});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'counter':
|
||||
if (args.length > 0) {
|
||||
const counter: Token = {
|
||||
type: TOKEN_TYPE.COUNTER,
|
||||
name: args[0]
|
||||
};
|
||||
if (args.length > 1) {
|
||||
counter.format = args[1];
|
||||
}
|
||||
tokens.push(counter);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'counters':
|
||||
if (args.length > 0) {
|
||||
const counters: Token = {
|
||||
type: TOKEN_TYPE.COUNTERS,
|
||||
name: args[0]
|
||||
};
|
||||
if (args.length > 1) {
|
||||
counters.glue = args[1];
|
||||
}
|
||||
if (args.length > 2) {
|
||||
counters.format = args[2];
|
||||
}
|
||||
tokens.push(counters);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'url':
|
||||
if (args.length > 0) {
|
||||
tokens.push({type: TOKEN_TYPE.URL, value: args[0]});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
isFunction = false;
|
||||
str = '';
|
||||
}
|
||||
break;
|
||||
|
||||
case ',':
|
||||
if (isString) {
|
||||
str += c;
|
||||
} else if (isFunction) {
|
||||
args.push(str);
|
||||
str = '';
|
||||
}
|
||||
break;
|
||||
|
||||
case ' ':
|
||||
case '\t':
|
||||
if (isString) {
|
||||
str += c;
|
||||
} else if (str) {
|
||||
addOtherToken(tokens, str);
|
||||
str = '';
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
str += c;
|
||||
}
|
||||
|
||||
if (c !== '\\') {
|
||||
isEscaped = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (str) {
|
||||
addOtherToken(tokens, str);
|
||||
}
|
||||
|
||||
if (cache) {
|
||||
cache[content] = tokens;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
};
|
||||
|
||||
const addOtherToken = (tokens: Array<Token>, identifier: string): void => {
|
||||
switch (identifier) {
|
||||
case 'open-quote':
|
||||
tokens.push({type: TOKEN_TYPE.OPENQUOTE});
|
||||
break;
|
||||
case 'close-quote':
|
||||
tokens.push({type: TOKEN_TYPE.CLOSEQUOTE});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const getQuote = (style: CSSStyleDeclaration, isOpening: boolean, quoteDepth: number): string => {
|
||||
const quotes = style.quotes ? style.quotes.split(/\s+/) : ["'\"'", "'\"'"];
|
||||
let idx = quoteDepth * 2;
|
||||
if (idx >= quotes.length) {
|
||||
idx = quotes.length - 2;
|
||||
}
|
||||
if (!isOpening) {
|
||||
++idx;
|
||||
}
|
||||
return quotes[idx].replace(/^["']|["']$/g, '');
|
||||
};
|
||||
|
||||
const formatCounterValue = (counter, glue: ?string, format: ?string): string => {
|
||||
const len = counter.length;
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (i > 0) {
|
||||
result += glue || '';
|
||||
}
|
||||
result += createCounterText(counter[i], parseListStyleType(format || 'decimal'), false);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
450
src/Renderer.js
450
src/Renderer.js
@ -1,450 +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 {LinearGradient, RadialGradient} 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, calculatePaddingBoxPath} from './Bounds';
|
||||
import {FontMetrics} from './Font';
|
||||
import {parseGradient, GRADIENT_TYPE} from './Gradient';
|
||||
import TextContainer from './TextContainer';
|
||||
|
||||
import {
|
||||
calculateBackgroungPositioningArea,
|
||||
calculateBackgroungPaintingArea,
|
||||
calculateBackgroundPosition,
|
||||
calculateBackgroundRepeatPath,
|
||||
calculateBackgroundSize,
|
||||
calculateGradientBackgroundSize
|
||||
} 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: LinearGradient): void,
|
||||
|
||||
renderRadialGradient(bounds: Bounds, gradient: RadialGradient): 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 hasRenderableBorders = container.style.border.some(
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
container.style.border.forEach((border, side) => {
|
||||
if (
|
||||
border.borderStyle !== BORDER_STYLE.NONE &&
|
||||
!border.borderColor.isTransparent()
|
||||
) {
|
||||
this.renderBorder(border, side, container.curvedBounds);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (HAS_BACKGROUND || hasRenderableBorders) {
|
||||
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 if (/gradient/i.test(backgroundImage.source.method)) {
|
||||
this.renderBackgroundGradient(container, backgroundImage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderBackgroundRepeat(container: NodeContainer, background: BackgroundImage) {
|
||||
const image = this.options.imageStore.get(background.source.args[0]);
|
||||
if (image) {
|
||||
const backgroundPositioningArea = calculateBackgroungPositioningArea(
|
||||
container.style.background.backgroundOrigin,
|
||||
container.bounds,
|
||||
container.style.padding,
|
||||
container.style.border
|
||||
);
|
||||
const backgroundImageSize = calculateBackgroundSize(
|
||||
background,
|
||||
image,
|
||||
backgroundPositioningArea
|
||||
);
|
||||
const position = calculateBackgroundPosition(
|
||||
background.position,
|
||||
backgroundImageSize,
|
||||
backgroundPositioningArea
|
||||
);
|
||||
const path = calculateBackgroundRepeatPath(
|
||||
background,
|
||||
position,
|
||||
backgroundImageSize,
|
||||
backgroundPositioningArea,
|
||||
container.bounds
|
||||
);
|
||||
|
||||
const offsetX = Math.round(backgroundPositioningArea.left + position.x);
|
||||
const offsetY = Math.round(backgroundPositioningArea.top + position.y);
|
||||
this.target.renderRepeat(path, image, backgroundImageSize, offsetX, offsetY);
|
||||
}
|
||||
}
|
||||
|
||||
renderBackgroundGradient(container: NodeContainer, background: BackgroundImage) {
|
||||
const backgroundPositioningArea = calculateBackgroungPositioningArea(
|
||||
container.style.background.backgroundOrigin,
|
||||
container.bounds,
|
||||
container.style.padding,
|
||||
container.style.border
|
||||
);
|
||||
const backgroundImageSize = calculateGradientBackgroundSize(
|
||||
background,
|
||||
backgroundPositioningArea
|
||||
);
|
||||
const position = calculateBackgroundPosition(
|
||||
background.position,
|
||||
backgroundImageSize,
|
||||
backgroundPositioningArea
|
||||
);
|
||||
const gradientBounds = new Bounds(
|
||||
Math.round(backgroundPositioningArea.left + position.x),
|
||||
Math.round(backgroundPositioningArea.top + position.y),
|
||||
backgroundImageSize.width,
|
||||
backgroundImageSize.height
|
||||
);
|
||||
|
||||
const gradient = parseGradient(container, background.source, gradientBounds);
|
||||
if (gradient) {
|
||||
switch (gradient.type) {
|
||||
case GRADIENT_TYPE.LINEAR_GRADIENT:
|
||||
// $FlowFixMe
|
||||
this.target.renderLinearGradient(gradientBounds, gradient);
|
||||
break;
|
||||
case GRADIENT_TYPE.RADIAL_GRADIENT:
|
||||
// $FlowFixMe
|
||||
this.target.renderRadialGradient(gradientBounds, gradient);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
this.options.x,
|
||||
this.options.y,
|
||||
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;
|
||||
};
|
@ -1,241 +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) || FEATURES.SUPPORT_SVG_DRAWING) {
|
||||
if (this.options.allowTaint === true || isInlineImage(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
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
@ -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();
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import type NodeContainer from './NodeContainer';
|
||||
import {Bounds, parseBounds} from './Bounds';
|
||||
import {TEXT_DECORATION} from './parsing/textDecoration';
|
||||
|
||||
import FEATURES from './Feature';
|
||||
import {breakWords, toCodePoints, fromCodePoint} from './Unicode';
|
||||
|
||||
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 letterRendering = parent.style.letterSpacing !== 0;
|
||||
const textList = letterRendering
|
||||
? toCodePoints(value).map(i => fromCodePoint(i))
|
||||
: breakWords(value, parent);
|
||||
const length = textList.length;
|
||||
const defaultView = node.parentNode ? node.parentNode.ownerDocument.defaultView : null;
|
||||
const scrollX = defaultView ? defaultView.pageXOffset : 0;
|
||||
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);
|
||||
};
|
@ -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;
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
import NodeContainer from './NodeContainer';
|
||||
import {LineBreaker, fromCodePoint, toCodePoints} from 'css-line-break';
|
||||
import {OVERFLOW_WRAP} from './parsing/overflowWrap';
|
||||
|
||||
export {toCodePoints, fromCodePoint} from 'css-line-break';
|
||||
|
||||
export const breakWords = (str: string, parent: NodeContainer): Array<string> => {
|
||||
const breaker = LineBreaker(str, {
|
||||
lineBreak: parent.style.lineBreak,
|
||||
wordBreak:
|
||||
parent.style.overflowWrap === OVERFLOW_WRAP.BREAK_WORD
|
||||
? 'break-word'
|
||||
: parent.style.wordBreak
|
||||
});
|
||||
|
||||
const words = [];
|
||||
let bk;
|
||||
|
||||
while (!(bk = breaker.next()).done) {
|
||||
words.push(bk.value.slice());
|
||||
}
|
||||
|
||||
return words;
|
||||
};
|
21
src/Util.js
21
src/Util.js
@ -1,21 +0,0 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
|
||||
export const contains = (bit: number, value: number): boolean => (bit & value) !== 0;
|
||||
|
||||
export const distance = (a: number, b: number): number => Math.sqrt(a * a + b * b);
|
||||
|
||||
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';
|
142
src/Window.js
142
src/Window.js
@ -1,142 +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 => {
|
||||
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);
|
||||
const canvas = renderer.render(stack);
|
||||
if (options.removeContainer === true) {
|
||||
if (container.parentNode) {
|
||||
container.parentNode.removeChild(container);
|
||||
} else if (__DEV__) {
|
||||
logger.log(
|
||||
`Cannot detach cloned iframe as it is not in the DOM anymore`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return canvas;
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
93
src/__tests__/index.ts
Normal file
93
src/__tests__/index.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import html2canvas from '../index';
|
||||
|
||||
import {CanvasRenderer} from '../render/canvas/canvas-renderer';
|
||||
import {DocumentCloner} from '../dom/document-cloner';
|
||||
import {COLORS} from '../css/types/color';
|
||||
|
||||
jest.mock('../core/logger');
|
||||
jest.mock('../css/layout/bounds');
|
||||
jest.mock('../dom/document-cloner');
|
||||
jest.mock('../dom/node-parser', () => {
|
||||
return {
|
||||
isBodyElement: () => false,
|
||||
isHTMLElement: () => false,
|
||||
parseTree: jest.fn().mockImplementation(() => {
|
||||
return {styles: {}};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../render/stacking-context');
|
||||
jest.mock('../render/canvas/canvas-renderer');
|
||||
|
||||
describe('html2canvas', () => {
|
||||
const element = {
|
||||
ownerDocument: {
|
||||
defaultView: {
|
||||
pageXOffset: 12,
|
||||
pageYOffset: 34
|
||||
}
|
||||
}
|
||||
} as HTMLElement;
|
||||
|
||||
it('should render with an element', async () => {
|
||||
DocumentCloner.destroy = jest.fn().mockReturnValue(true);
|
||||
await html2canvas(element);
|
||||
expect(CanvasRenderer).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
cache: expect.any(Object),
|
||||
logger: expect.any(Object),
|
||||
windowBounds: expect.objectContaining({left: 12, top: 34})
|
||||
}),
|
||||
expect.objectContaining({
|
||||
backgroundColor: 0xffffffff,
|
||||
scale: 1,
|
||||
height: 50,
|
||||
width: 200,
|
||||
x: 0,
|
||||
y: 0,
|
||||
canvas: undefined
|
||||
})
|
||||
);
|
||||
expect(DocumentCloner.destroy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should have transparent background with backgroundColor: null', async () => {
|
||||
await html2canvas(element, {backgroundColor: null});
|
||||
expect(CanvasRenderer).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
backgroundColor: COLORS.TRANSPARENT
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should use existing canvas when given as option', async () => {
|
||||
const canvas = {} as HTMLCanvasElement;
|
||||
await html2canvas(element, {canvas});
|
||||
expect(CanvasRenderer).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
canvas
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should not remove cloned window when removeContainer: false', async () => {
|
||||
DocumentCloner.destroy = jest.fn();
|
||||
await html2canvas(element, {removeContainer: false});
|
||||
expect(CanvasRenderer).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
backgroundColor: 0xffffffff,
|
||||
scale: 1,
|
||||
height: 50,
|
||||
width: 200,
|
||||
x: 0,
|
||||
y: 0,
|
||||
canvas: undefined
|
||||
})
|
||||
);
|
||||
expect(DocumentCloner.destroy).not.toBeCalled();
|
||||
});
|
||||
});
|
1
src/core/__mocks__/cache-storage.ts
Normal file
1
src/core/__mocks__/cache-storage.ts
Normal file
@ -0,0 +1 @@
|
||||
export class CacheStorage {}
|
19
src/core/__mocks__/context.ts
Normal file
19
src/core/__mocks__/context.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {logger, Logger} from './logger';
|
||||
|
||||
export class Context {
|
||||
readonly logger: Logger = logger;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
readonly _cache: {[key: string]: Promise<any>} = {};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
readonly cache: any;
|
||||
|
||||
constructor() {
|
||||
this.cache = {
|
||||
addImage: jest.fn().mockImplementation((src: string): Promise<void> => {
|
||||
const result = Promise.resolve();
|
||||
this._cache[src] = result;
|
||||
return result;
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
8
src/core/__mocks__/features.ts
Normal file
8
src/core/__mocks__/features.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const FEATURES = {
|
||||
SUPPORT_RANGE_BOUNDS: true,
|
||||
SUPPORT_SVG_DRAWING: true,
|
||||
SUPPORT_FOREIGNOBJECT_DRAWING: true,
|
||||
SUPPORT_CORS_IMAGES: true,
|
||||
SUPPORT_RESPONSE_TYPE: true,
|
||||
SUPPORT_CORS_XHR: true
|
||||
};
|
22
src/core/__mocks__/logger.ts
Normal file
22
src/core/__mocks__/logger.ts
Normal file
@ -0,0 +1,22 @@
|
||||
export class Logger {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
debug(): void {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
static create(): void {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
static destroy(): void {}
|
||||
|
||||
static getInstance(): Logger {
|
||||
return logger;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
info(): void {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
error(): void {}
|
||||
}
|
||||
|
||||
export const logger = new Logger();
|
273
src/core/__tests__/cache-storage.ts
Normal file
273
src/core/__tests__/cache-storage.ts
Normal file
@ -0,0 +1,273 @@
|
||||
import {deepStrictEqual, fail} from 'assert';
|
||||
import {FEATURES} from '../features';
|
||||
import {CacheStorage} from '../cache-storage';
|
||||
import {Context} from '../context';
|
||||
import {Bounds} from '../../css/layout/bounds';
|
||||
|
||||
const proxy = 'http://example.com/proxy';
|
||||
|
||||
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);
|
||||
|
||||
return new Context(
|
||||
{
|
||||
logging: false,
|
||||
imageTimeout: 0,
|
||||
useCORS: false,
|
||||
allowTaint: false,
|
||||
proxy,
|
||||
...opts
|
||||
},
|
||||
new Bounds(0, 0, 0, 0)
|
||||
);
|
||||
};
|
||||
|
||||
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?: () => void;
|
||||
constructor() {
|
||||
images.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
class XMLHttpRequestMock {
|
||||
sent: boolean;
|
||||
status: number;
|
||||
timeout: number;
|
||||
method?: string;
|
||||
url?: string;
|
||||
response?: string;
|
||||
onload?: () => void;
|
||||
ontimeout?: () => void;
|
||||
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) {}
|
||||
});
|
||||
});
|
30
src/core/__tests__/logger.ts
Normal file
30
src/core/__tests__/logger.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import {Logger} from '../logger';
|
||||
|
||||
describe('logger', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let infoSpy: any;
|
||||
|
||||
beforeEach(() => {
|
||||
infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {
|
||||
// do nothing
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
infoSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should call console.info when logger enabled', () => {
|
||||
const id = Math.random().toString();
|
||||
const logger = new Logger({id, enabled: true});
|
||||
logger.info('testing');
|
||||
expect(infoSpy).toHaveBeenLastCalledWith(id, expect.stringMatching(/\d+ms/), 'testing');
|
||||
});
|
||||
|
||||
it("shouldn't call console.info when logger disabled", () => {
|
||||
const id = Math.random().toString();
|
||||
const logger = new Logger({id, enabled: false});
|
||||
logger.info('testing');
|
||||
expect(infoSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
1
src/core/bitwise.ts
Normal file
1
src/core/bitwise.ts
Normal file
@ -0,0 +1 @@
|
||||
export const contains = (bit: number, value: number): boolean => (bit & value) !== 0;
|
177
src/core/cache-storage.ts
Normal file
177
src/core/cache-storage.ts
Normal file
@ -0,0 +1,177 @@
|
||||
import {FEATURES} from './features';
|
||||
import {Context} from './context';
|
||||
|
||||
export class CacheStorage {
|
||||
private static _link?: HTMLAnchorElement;
|
||||
private static _origin = 'about:blank';
|
||||
|
||||
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): void {
|
||||
CacheStorage._link = window.document.createElement('a');
|
||||
CacheStorage._origin = CacheStorage.getOrigin(window.location.href);
|
||||
}
|
||||
}
|
||||
|
||||
export 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>} = {};
|
||||
|
||||
constructor(private readonly context: Context, private readonly _options: ResourceOptions) {}
|
||||
|
||||
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)).catch(() => {
|
||||
// prevent unhandled rejection
|
||||
});
|
||||
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 &&
|
||||
!isBlobImage(key) &&
|
||||
typeof this._options.proxy === 'string' &&
|
||||
FEATURES.SUPPORT_CORS_XHR &&
|
||||
!useCORS;
|
||||
if (
|
||||
!isSameOrigin &&
|
||||
this._options.allowTaint === false &&
|
||||
!isInlineImage(key) &&
|
||||
!isBlobImage(key) &&
|
||||
!useProxy &&
|
||||
!useCORS
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let src = key;
|
||||
if (useProxy) {
|
||||
src = await this.proxy(src);
|
||||
}
|
||||
|
||||
this.context.logger.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;
|
||||
const queryString = proxy.indexOf('?') > -1 ? '&' : '?';
|
||||
xhr.open('GET', `${proxy}${queryString}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);
|
21
src/core/context.ts
Normal file
21
src/core/context.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import {Logger} from './logger';
|
||||
import {Cache, ResourceOptions} from './cache-storage';
|
||||
import {Bounds} from '../css/layout/bounds';
|
||||
|
||||
export type ContextOptions = {
|
||||
logging: boolean;
|
||||
cache?: Cache;
|
||||
} & ResourceOptions;
|
||||
|
||||
export class Context {
|
||||
private readonly instanceName = `#${Context.instanceCount++}`;
|
||||
readonly logger: Logger;
|
||||
readonly cache: Cache;
|
||||
|
||||
private static instanceCount = 1;
|
||||
|
||||
constructor(options: ContextOptions, public windowBounds: Bounds) {
|
||||
this.logger = new Logger({id: this.instanceName, enabled: options.logging});
|
||||
this.cache = options.cache ?? new Cache(this, options);
|
||||
}
|
||||
}
|
29
src/core/debugger.ts
Normal file
29
src/core/debugger.ts
Normal file
@ -0,0 +1,29 @@
|
||||
const elementDebuggerAttribute = 'data-html2canvas-debug';
|
||||
export const enum DebuggerType {
|
||||
NONE,
|
||||
ALL,
|
||||
CLONE,
|
||||
PARSE,
|
||||
RENDER
|
||||
}
|
||||
|
||||
const getElementDebugType = (element: Element): DebuggerType => {
|
||||
const attribute = element.getAttribute(elementDebuggerAttribute);
|
||||
switch (attribute) {
|
||||
case 'all':
|
||||
return DebuggerType.ALL;
|
||||
case 'clone':
|
||||
return DebuggerType.CLONE;
|
||||
case 'parse':
|
||||
return DebuggerType.PARSE;
|
||||
case 'render':
|
||||
return DebuggerType.RENDER;
|
||||
default:
|
||||
return DebuggerType.NONE;
|
||||
}
|
||||
};
|
||||
|
||||
export const isDebugging = (element: Element, type: Omit<DebuggerType, DebuggerType.NONE>): boolean => {
|
||||
const elementType = getElementDebugType(element);
|
||||
return elementType === DebuggerType.ALL || type === elementType;
|
||||
};
|
222
src/core/features.ts
Normal file
222
src/core/features.ts
Normal file
@ -0,0 +1,222 @@
|
||||
import {fromCodePoint, toCodePoints} from 'css-line-break';
|
||||
|
||||
const testRangeBounds = (document: Document) => {
|
||||
const TEST_HEIGHT = 123;
|
||||
|
||||
if (document.createRange) {
|
||||
const range = document.createRange();
|
||||
if (range.getBoundingClientRect) {
|
||||
const testElement = document.createElement('boundtest');
|
||||
testElement.style.height = `${TEST_HEIGHT}px`;
|
||||
testElement.style.display = 'block';
|
||||
document.body.appendChild(testElement);
|
||||
|
||||
range.selectNode(testElement);
|
||||
const rangeBounds = range.getBoundingClientRect();
|
||||
const rangeHeight = Math.round(rangeBounds.height);
|
||||
document.body.removeChild(testElement);
|
||||
if (rangeHeight === TEST_HEIGHT) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const testIOSLineBreak = (document: Document) => {
|
||||
const testElement = document.createElement('boundtest');
|
||||
testElement.style.width = '50px';
|
||||
testElement.style.display = 'block';
|
||||
testElement.style.fontSize = '12px';
|
||||
testElement.style.letterSpacing = '0px';
|
||||
testElement.style.wordSpacing = '0px';
|
||||
document.body.appendChild(testElement);
|
||||
const range = document.createRange();
|
||||
|
||||
testElement.innerHTML = typeof ''.repeat === 'function' ? '👨'.repeat(10) : '';
|
||||
|
||||
const node = testElement.firstChild as Text;
|
||||
|
||||
const textList = toCodePoints(node.data).map((i) => fromCodePoint(i));
|
||||
let offset = 0;
|
||||
let prev: DOMRect = {} as DOMRect;
|
||||
|
||||
// ios 13 does not handle range getBoundingClientRect line changes correctly #2177
|
||||
const supports = textList.every((text, i) => {
|
||||
range.setStart(node, offset);
|
||||
range.setEnd(node, offset + text.length);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
offset += text.length;
|
||||
const boundAhead = rect.x > prev.x || rect.y > prev.y;
|
||||
|
||||
prev = rect;
|
||||
if (i === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return boundAhead;
|
||||
});
|
||||
|
||||
document.body.removeChild(testElement);
|
||||
return supports;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
img.src = `data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'></svg>`;
|
||||
|
||||
try {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
canvas.toDataURL();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isGreenPixel = (data: Uint8ClampedArray): boolean =>
|
||||
data[0] === 0 && data[1] === 255 && data[2] === 0 && data[3] === 255;
|
||||
|
||||
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);
|
||||
|
||||
const img = new Image();
|
||||
const greenImageSrc = canvas.toDataURL();
|
||||
img.src = greenImageSrc;
|
||||
const svg = createForeignObjectSVG(size, size, 0, 0, img);
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
return loadSerializedSVG(svg)
|
||||
.then((img: HTMLImageElement) => {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const data = ctx.getImageData(0, 0, size, size).data;
|
||||
ctx.fillStyle = 'red';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
const node = document.createElement('div');
|
||||
node.style.backgroundImage = `url(${greenImageSrc})`;
|
||||
node.style.height = `${size}px`;
|
||||
// Firefox 55 does not render inline <img /> tags
|
||||
return isGreenPixel(data)
|
||||
? loadSerializedSVG(createForeignObjectSVG(size, size, 0, 0, node))
|
||||
: Promise.reject(false);
|
||||
})
|
||||
.then((img: HTMLImageElement) => {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
// Edge does not render background-images
|
||||
return isGreenPixel(ctx.getImageData(0, 0, size, size).data);
|
||||
})
|
||||
.catch(() => false);
|
||||
};
|
||||
|
||||
export const createForeignObjectSVG = (
|
||||
width: number,
|
||||
height: number,
|
||||
x: number,
|
||||
y: number,
|
||||
node: Node
|
||||
): SVGForeignObjectElement => {
|
||||
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(): boolean {
|
||||
'use strict';
|
||||
const value = testRangeBounds(document);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_RANGE_BOUNDS', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_WORD_BREAKING(): boolean {
|
||||
'use strict';
|
||||
const value = FEATURES.SUPPORT_RANGE_BOUNDS && testIOSLineBreak(document);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_WORD_BREAKING', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_SVG_DRAWING(): boolean {
|
||||
'use strict';
|
||||
const value = testSVG(document);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_SVG_DRAWING', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_FOREIGNOBJECT_DRAWING(): Promise<boolean> {
|
||||
'use strict';
|
||||
const value =
|
||||
typeof Array.from === 'function' && typeof window.fetch === 'function'
|
||||
? testForeignObject(document)
|
||||
: Promise.resolve(false);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_FOREIGNOBJECT_DRAWING', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_CORS_IMAGES(): boolean {
|
||||
'use strict';
|
||||
const value = testCORS();
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_CORS_IMAGES', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_RESPONSE_TYPE(): boolean {
|
||||
'use strict';
|
||||
const value = testResponseType();
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_RESPONSE_TYPE', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_CORS_XHR(): boolean {
|
||||
'use strict';
|
||||
const value = 'withCredentials' in new XMLHttpRequest();
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', {value});
|
||||
return value;
|
||||
},
|
||||
get SUPPORT_NATIVE_TEXT_SEGMENTATION(): boolean {
|
||||
'use strict';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const value = !!(typeof Intl !== 'undefined' && (Intl as any).Segmenter);
|
||||
Object.defineProperty(FEATURES, 'SUPPORT_NATIVE_TEXT_SEGMENTATION', {value});
|
||||
return value;
|
||||
}
|
||||
};
|
72
src/core/logger.ts
Normal file
72
src/core/logger.ts
Normal file
@ -0,0 +1,72 @@
|
||||
export interface LoggerOptions {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
static instances: {[key: string]: Logger} = {};
|
||||
|
||||
private readonly id: string;
|
||||
private readonly enabled: boolean;
|
||||
private readonly start: number;
|
||||
|
||||
constructor({id, enabled}: LoggerOptions) {
|
||||
this.id = id;
|
||||
this.enabled = enabled;
|
||||
this.start = Date.now();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
debug(...args: unknown[]): void {
|
||||
if (this.enabled) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
info(...args: unknown[]): void {
|
||||
if (this.enabled) {
|
||||
// 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
|
||||
warn(...args: unknown[]): void {
|
||||
if (this.enabled) {
|
||||
// eslint-disable-next-line no-console
|
||||
if (typeof window !== 'undefined' && window.console && typeof console.warn === 'function') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(this.id, `${this.getTime()}ms`, ...args);
|
||||
} else {
|
||||
this.info(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error(...args: unknown[]): void {
|
||||
if (this.enabled) {
|
||||
// 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
1
src/core/util.ts
Normal file
@ -0,0 +1 @@
|
||||
export const SMALL_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
49
src/css/IPropertyDescriptor.ts
Normal file
49
src/css/IPropertyDescriptor.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import {CSSValue} from './syntax/parser';
|
||||
import {CSSTypes} from './types';
|
||||
import {Context} from '../core/context';
|
||||
|
||||
export const 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: (context: Context, token: string) => T;
|
||||
}
|
||||
|
||||
export interface IPropertyTypeValueDescriptor extends IPropertyDescriptor {
|
||||
type: PropertyDescriptorParsingType.TYPE_VALUE;
|
||||
format: CSSTypes;
|
||||
}
|
||||
|
||||
export interface IPropertyValueDescriptor<T> extends IPropertyDescriptor {
|
||||
type: PropertyDescriptorParsingType.VALUE;
|
||||
parse: (context: Context, token: CSSValue) => T;
|
||||
}
|
||||
|
||||
export interface IPropertyListDescriptor<T> extends IPropertyDescriptor {
|
||||
type: PropertyDescriptorParsingType.LIST;
|
||||
parse: (context: Context, tokens: CSSValue[]) => T;
|
||||
}
|
||||
|
||||
export interface IPropertyTokenValueDescriptor extends IPropertyDescriptor {
|
||||
type: PropertyDescriptorParsingType.TOKEN_VALUE;
|
||||
}
|
||||
|
||||
export type CSSPropertyDescriptor<T> =
|
||||
| IPropertyValueDescriptor<T>
|
||||
| IPropertyListDescriptor<T>
|
||||
| IPropertyIdentValueDescriptor<T>
|
||||
| IPropertyTypeValueDescriptor
|
||||
| IPropertyTokenValueDescriptor;
|
7
src/css/ITypeDescriptor.ts
Normal file
7
src/css/ITypeDescriptor.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import {CSSValue} from './syntax/parser';
|
||||
import {Context} from '../core/context';
|
||||
|
||||
export interface ITypeDescriptor<T> {
|
||||
name: string;
|
||||
parse: (context: Context, value: CSSValue) => T;
|
||||
}
|
321
src/css/index.ts
Normal file
321
src/css/index.ts
Normal file
@ -0,0 +1,321 @@
|
||||
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 {direction} from './property-descriptors/direction';
|
||||
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 {time} from './types/time';
|
||||
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 {duration} from './property-descriptors/duration';
|
||||
import {quotes} from './property-descriptors/quotes';
|
||||
import {boxShadow} from './property-descriptors/box-shadow';
|
||||
import {paintOrder} from './property-descriptors/paint-order';
|
||||
import {webkitTextStrokeColor} from './property-descriptors/webkit-text-stroke-color';
|
||||
import {webkitTextStrokeWidth} from './property-descriptors/webkit-text-stroke-width';
|
||||
import {Context} from '../core/context';
|
||||
|
||||
export class CSSParsedDeclaration {
|
||||
animationDuration: ReturnType<typeof duration.parse>;
|
||||
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;
|
||||
direction: ReturnType<typeof direction.parse>;
|
||||
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;
|
||||
paintOrder: ReturnType<typeof paintOrder.parse>;
|
||||
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>;
|
||||
webkitTextStrokeColor: Color;
|
||||
webkitTextStrokeWidth: ReturnType<typeof webkitTextStrokeWidth.parse>;
|
||||
wordBreak: ReturnType<typeof wordBreak.parse>;
|
||||
zIndex: ReturnType<typeof zIndex.parse>;
|
||||
|
||||
constructor(context: Context, declaration: CSSStyleDeclaration) {
|
||||
this.animationDuration = parse(context, duration, declaration.animationDuration);
|
||||
this.backgroundClip = parse(context, backgroundClip, declaration.backgroundClip);
|
||||
this.backgroundColor = parse(context, backgroundColor, declaration.backgroundColor);
|
||||
this.backgroundImage = parse(context, backgroundImage, declaration.backgroundImage);
|
||||
this.backgroundOrigin = parse(context, backgroundOrigin, declaration.backgroundOrigin);
|
||||
this.backgroundPosition = parse(context, backgroundPosition, declaration.backgroundPosition);
|
||||
this.backgroundRepeat = parse(context, backgroundRepeat, declaration.backgroundRepeat);
|
||||
this.backgroundSize = parse(context, backgroundSize, declaration.backgroundSize);
|
||||
this.borderTopColor = parse(context, borderTopColor, declaration.borderTopColor);
|
||||
this.borderRightColor = parse(context, borderRightColor, declaration.borderRightColor);
|
||||
this.borderBottomColor = parse(context, borderBottomColor, declaration.borderBottomColor);
|
||||
this.borderLeftColor = parse(context, borderLeftColor, declaration.borderLeftColor);
|
||||
this.borderTopLeftRadius = parse(context, borderTopLeftRadius, declaration.borderTopLeftRadius);
|
||||
this.borderTopRightRadius = parse(context, borderTopRightRadius, declaration.borderTopRightRadius);
|
||||
this.borderBottomRightRadius = parse(context, borderBottomRightRadius, declaration.borderBottomRightRadius);
|
||||
this.borderBottomLeftRadius = parse(context, borderBottomLeftRadius, declaration.borderBottomLeftRadius);
|
||||
this.borderTopStyle = parse(context, borderTopStyle, declaration.borderTopStyle);
|
||||
this.borderRightStyle = parse(context, borderRightStyle, declaration.borderRightStyle);
|
||||
this.borderBottomStyle = parse(context, borderBottomStyle, declaration.borderBottomStyle);
|
||||
this.borderLeftStyle = parse(context, borderLeftStyle, declaration.borderLeftStyle);
|
||||
this.borderTopWidth = parse(context, borderTopWidth, declaration.borderTopWidth);
|
||||
this.borderRightWidth = parse(context, borderRightWidth, declaration.borderRightWidth);
|
||||
this.borderBottomWidth = parse(context, borderBottomWidth, declaration.borderBottomWidth);
|
||||
this.borderLeftWidth = parse(context, borderLeftWidth, declaration.borderLeftWidth);
|
||||
this.boxShadow = parse(context, boxShadow, declaration.boxShadow);
|
||||
this.color = parse(context, color, declaration.color);
|
||||
this.direction = parse(context, direction, declaration.direction);
|
||||
this.display = parse(context, display, declaration.display);
|
||||
this.float = parse(context, float, declaration.cssFloat);
|
||||
this.fontFamily = parse(context, fontFamily, declaration.fontFamily);
|
||||
this.fontSize = parse(context, fontSize, declaration.fontSize);
|
||||
this.fontStyle = parse(context, fontStyle, declaration.fontStyle);
|
||||
this.fontVariant = parse(context, fontVariant, declaration.fontVariant);
|
||||
this.fontWeight = parse(context, fontWeight, declaration.fontWeight);
|
||||
this.letterSpacing = parse(context, letterSpacing, declaration.letterSpacing);
|
||||
this.lineBreak = parse(context, lineBreak, declaration.lineBreak);
|
||||
this.lineHeight = parse(context, lineHeight, declaration.lineHeight);
|
||||
this.listStyleImage = parse(context, listStyleImage, declaration.listStyleImage);
|
||||
this.listStylePosition = parse(context, listStylePosition, declaration.listStylePosition);
|
||||
this.listStyleType = parse(context, listStyleType, declaration.listStyleType);
|
||||
this.marginTop = parse(context, marginTop, declaration.marginTop);
|
||||
this.marginRight = parse(context, marginRight, declaration.marginRight);
|
||||
this.marginBottom = parse(context, marginBottom, declaration.marginBottom);
|
||||
this.marginLeft = parse(context, marginLeft, declaration.marginLeft);
|
||||
this.opacity = parse(context, opacity, declaration.opacity);
|
||||
const overflowTuple = parse(context, overflow, declaration.overflow);
|
||||
this.overflowX = overflowTuple[0];
|
||||
this.overflowY = overflowTuple[overflowTuple.length > 1 ? 1 : 0];
|
||||
this.overflowWrap = parse(context, overflowWrap, declaration.overflowWrap);
|
||||
this.paddingTop = parse(context, paddingTop, declaration.paddingTop);
|
||||
this.paddingRight = parse(context, paddingRight, declaration.paddingRight);
|
||||
this.paddingBottom = parse(context, paddingBottom, declaration.paddingBottom);
|
||||
this.paddingLeft = parse(context, paddingLeft, declaration.paddingLeft);
|
||||
this.paintOrder = parse(context, paintOrder, declaration.paintOrder);
|
||||
this.position = parse(context, position, declaration.position);
|
||||
this.textAlign = parse(context, textAlign, declaration.textAlign);
|
||||
this.textDecorationColor = parse(
|
||||
context,
|
||||
textDecorationColor,
|
||||
declaration.textDecorationColor ?? declaration.color
|
||||
);
|
||||
this.textDecorationLine = parse(
|
||||
context,
|
||||
textDecorationLine,
|
||||
declaration.textDecorationLine ?? declaration.textDecoration
|
||||
);
|
||||
this.textShadow = parse(context, textShadow, declaration.textShadow);
|
||||
this.textTransform = parse(context, textTransform, declaration.textTransform);
|
||||
this.transform = parse(context, transform, declaration.transform);
|
||||
this.transformOrigin = parse(context, transformOrigin, declaration.transformOrigin);
|
||||
this.visibility = parse(context, visibility, declaration.visibility);
|
||||
this.webkitTextStrokeColor = parse(context, webkitTextStrokeColor, declaration.webkitTextStrokeColor);
|
||||
this.webkitTextStrokeWidth = parse(context, webkitTextStrokeWidth, declaration.webkitTextStrokeWidth);
|
||||
this.wordBreak = parse(context, wordBreak, declaration.wordBreak);
|
||||
this.zIndex = parse(context, 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(context: Context, declaration: CSSStyleDeclaration) {
|
||||
this.content = parse(context, content, declaration.content);
|
||||
this.quotes = parse(context, quotes, declaration.quotes);
|
||||
}
|
||||
}
|
||||
|
||||
export class CSSParsedCounterDeclaration {
|
||||
counterIncrement: ReturnType<typeof counterIncrement.parse>;
|
||||
counterReset: ReturnType<typeof counterReset.parse>;
|
||||
|
||||
constructor(context: Context, declaration: CSSStyleDeclaration) {
|
||||
this.counterIncrement = parse(context, counterIncrement, declaration.counterIncrement);
|
||||
this.counterReset = parse(context, counterReset, declaration.counterReset);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const parse = (context: Context, 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(context, isIdentToken(token) ? token.value : descriptor.initialValue);
|
||||
case PropertyDescriptorParsingType.VALUE:
|
||||
return descriptor.parse(context, parser.parseComponentValue());
|
||||
case PropertyDescriptorParsingType.LIST:
|
||||
return descriptor.parse(context, parser.parseComponentValues());
|
||||
case PropertyDescriptorParsingType.TOKEN_VALUE:
|
||||
return parser.parseComponentValue();
|
||||
case PropertyDescriptorParsingType.TYPE_VALUE:
|
||||
switch (descriptor.format) {
|
||||
case 'angle':
|
||||
return angle.parse(context, parser.parseComponentValue());
|
||||
case 'color':
|
||||
return colorType.parse(context, parser.parseComponentValue());
|
||||
case 'image':
|
||||
return image.parse(context, 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;
|
||||
case 'time':
|
||||
return time.parse(context, parser.parseComponentValue());
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
4
src/css/layout/__mocks__/bounds.ts
Normal file
4
src/css/layout/__mocks__/bounds.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const {Bounds} = jest.requireActual('../bounds');
|
||||
export const parseBounds = (): typeof Bounds => {
|
||||
return new Bounds(0, 0, 200, 50);
|
||||
};
|
58
src/css/layout/bounds.ts
Normal file
58
src/css/layout/bounds.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export class Bounds {
|
||||
constructor(readonly left: number, readonly top: number, readonly width: number, readonly height: number) {}
|
||||
|
||||
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(context: Context, clientRect: ClientRect): Bounds {
|
||||
return new Bounds(
|
||||
clientRect.left + context.windowBounds.left,
|
||||
clientRect.top + context.windowBounds.top,
|
||||
clientRect.width,
|
||||
clientRect.height
|
||||
);
|
||||
}
|
||||
|
||||
static fromDOMRectList(context: Context, domRectList: DOMRectList): Bounds {
|
||||
const domRect = Array.from(domRectList).find((rect) => rect.width !== 0);
|
||||
return domRect
|
||||
? new Bounds(
|
||||
domRect.left + context.windowBounds.left,
|
||||
domRect.top + context.windowBounds.top,
|
||||
domRect.width,
|
||||
domRect.height
|
||||
)
|
||||
: Bounds.EMPTY;
|
||||
}
|
||||
|
||||
static EMPTY = new Bounds(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
export const parseBounds = (context: Context, node: Element): Bounds => {
|
||||
return Bounds.fromClientRect(context, 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);
|
||||
};
|
158
src/css/layout/text.ts
Normal file
158
src/css/layout/text.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import {OVERFLOW_WRAP} from '../property-descriptors/overflow-wrap';
|
||||
import {CSSParsedDeclaration} from '../index';
|
||||
import {fromCodePoint, LineBreaker, toCodePoints} from 'css-line-break';
|
||||
import {splitGraphemes} from 'text-segmentation';
|
||||
import {Bounds, parseBounds} from './bounds';
|
||||
import {FEATURES} from '../../core/features';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export class TextBounds {
|
||||
readonly text: string;
|
||||
readonly bounds: Bounds;
|
||||
|
||||
constructor(text: string, bounds: Bounds) {
|
||||
this.text = text;
|
||||
this.bounds = bounds;
|
||||
}
|
||||
}
|
||||
|
||||
export const parseTextBounds = (
|
||||
context: Context,
|
||||
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) {
|
||||
const clientRects = createRange(node, offset, text.length).getClientRects();
|
||||
if (clientRects.length > 1) {
|
||||
const subSegments = segmentGraphemes(text);
|
||||
let subOffset = 0;
|
||||
subSegments.forEach((subSegment) => {
|
||||
textBounds.push(
|
||||
new TextBounds(
|
||||
subSegment,
|
||||
Bounds.fromDOMRectList(
|
||||
context,
|
||||
createRange(node, subOffset + offset, subSegment.length).getClientRects()
|
||||
)
|
||||
)
|
||||
);
|
||||
subOffset += subSegment.length;
|
||||
});
|
||||
} else {
|
||||
textBounds.push(new TextBounds(text, Bounds.fromDOMRectList(context, clientRects)));
|
||||
}
|
||||
} else {
|
||||
const replacementNode = node.splitText(text.length);
|
||||
textBounds.push(new TextBounds(text, getWrapperBounds(context, node)));
|
||||
node = replacementNode;
|
||||
}
|
||||
} else if (!FEATURES.SUPPORT_RANGE_BOUNDS) {
|
||||
node = node.splitText(text.length);
|
||||
}
|
||||
offset += text.length;
|
||||
});
|
||||
|
||||
return textBounds;
|
||||
};
|
||||
|
||||
const getWrapperBounds = (context: Context, 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(context, wrapper);
|
||||
if (wrapper.firstChild) {
|
||||
parentNode.replaceChild(wrapper.firstChild, wrapper);
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
|
||||
return Bounds.EMPTY;
|
||||
};
|
||||
|
||||
const createRange = (node: Text, offset: number, length: number): Range => {
|
||||
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 range;
|
||||
};
|
||||
|
||||
export const segmentGraphemes = (value: string): string[] => {
|
||||
if (FEATURES.SUPPORT_NATIVE_TEXT_SEGMENTATION) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const segmenter = new (Intl as any).Segmenter(void 0, {granularity: 'grapheme'});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return Array.from(segmenter.segment(value)).map((segment: any) => segment.segment);
|
||||
}
|
||||
|
||||
return splitGraphemes(value);
|
||||
};
|
||||
|
||||
const segmentWords = (value: string, styles: CSSParsedDeclaration): string[] => {
|
||||
if (FEATURES.SUPPORT_NATIVE_TEXT_SEGMENTATION) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const segmenter = new (Intl as any).Segmenter(void 0, {
|
||||
granularity: 'word'
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return Array.from(segmenter.segment(value)).map((segment: any) => segment.segment);
|
||||
}
|
||||
|
||||
return breakWords(value, styles);
|
||||
};
|
||||
|
||||
const breakText = (value: string, styles: CSSParsedDeclaration): string[] => {
|
||||
return styles.letterSpacing !== 0 ? segmentGraphemes(value) : segmentWords(value, styles);
|
||||
};
|
||||
|
||||
// https://drafts.csswg.org/css-text/#word-separator
|
||||
const wordSeparators = [0x0020, 0x00a0, 0x1361, 0x10100, 0x10101, 0x1039, 0x1091];
|
||||
|
||||
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) {
|
||||
const value = bk.value.slice();
|
||||
const codePoints = toCodePoints(value);
|
||||
let word = '';
|
||||
codePoints.forEach((codePoint) => {
|
||||
if (wordSeparators.indexOf(codePoint) === -1) {
|
||||
word += fromCodePoint(codePoint);
|
||||
} else {
|
||||
if (word.length) {
|
||||
words.push(word);
|
||||
}
|
||||
words.push(fromCodePoint(codePoint));
|
||||
word = '';
|
||||
}
|
||||
});
|
||||
|
||||
if (word.length) {
|
||||
words.push(word);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return words;
|
||||
};
|
59
src/css/property-descriptors/__tests__/background-tests.ts
Normal file
59
src/css/property-descriptors/__tests__/background-tests.ts
Normal file
@ -0,0 +1,59 @@
|
||||
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';
|
||||
|
||||
jest.mock('../../../core/context');
|
||||
import {Context} from '../../../core/context';
|
||||
|
||||
jest.mock('../../../core/features');
|
||||
|
||||
const backgroundImageParse = (context: Context, value: string) =>
|
||||
backgroundImage.parse(context, Parser.parseValues(value));
|
||||
|
||||
describe('property-descriptors', () => {
|
||||
let context: Context;
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
context = new Context({} as any, {} as any);
|
||||
});
|
||||
describe('background-image', () => {
|
||||
it('none', () => {
|
||||
deepStrictEqual(backgroundImageParse(context, 'none'), []);
|
||||
expect(context.cache.addImage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('url(test.jpg), url(test2.jpg)', () => {
|
||||
deepStrictEqual(
|
||||
backgroundImageParse(context, '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}
|
||||
]
|
||||
);
|
||||
expect(context.cache.addImage).toHaveBeenCalledWith('http://example.com/test.jpg');
|
||||
expect(context.cache.addImage).toHaveBeenCalledWith('http://example.com/test2.jpg');
|
||||
});
|
||||
|
||||
it(`linear-gradient(to bottom, rgba(255,255,0,0.5), rgba(0,0,255,0.5)), url('https://html2canvas.hertzen.com')`, () =>
|
||||
deepStrictEqual(
|
||||
backgroundImageParse(
|
||||
context,
|
||||
`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}
|
||||
]
|
||||
));
|
||||
});
|
||||
});
|
25
src/css/property-descriptors/__tests__/font-family.ts
Normal file
25
src/css/property-descriptors/__tests__/font-family.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import {deepEqual} from 'assert';
|
||||
import {Parser} from '../../syntax/parser';
|
||||
import {fontFamily} from '../font-family';
|
||||
import {Context} from '../../../core/context';
|
||||
|
||||
const fontFamilyParse = (value: string) => fontFamily.parse({} as Context, Parser.parseValues(value));
|
||||
|
||||
describe('property-descriptors', () => {
|
||||
describe('font-family', () => {
|
||||
it('sans-serif', () => deepEqual(fontFamilyParse('sans-serif'), ['sans-serif']));
|
||||
|
||||
it('great fonts 40 library', () =>
|
||||
deepEqual(fontFamilyParse('great fonts 40 library'), ["'great fonts 40 library'"]));
|
||||
|
||||
it('preferred font, "quoted fallback font", font', () =>
|
||||
deepEqual(fontFamilyParse('preferred font, "quoted fallback font", font'), [
|
||||
"'preferred font'",
|
||||
"'quoted fallback font'",
|
||||
'font'
|
||||
]));
|
||||
|
||||
it("'escaping test\\'s font'", () =>
|
||||
deepEqual(fontFamilyParse("'escaping test\\'s font'"), ["'escaping test's font'"]));
|
||||
});
|
||||
});
|
87
src/css/property-descriptors/__tests__/paint-order.ts
Normal file
87
src/css/property-descriptors/__tests__/paint-order.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import {deepStrictEqual} from 'assert';
|
||||
import {Parser} from '../../syntax/parser';
|
||||
import {paintOrder, PAINT_ORDER_LAYER} from '../paint-order';
|
||||
import {Context} from '../../../core/context';
|
||||
|
||||
const paintOrderParse = (value: string) => paintOrder.parse({} as Context, Parser.parseValues(value));
|
||||
|
||||
describe('property-descriptors', () => {
|
||||
describe('paint-order', () => {
|
||||
it('none', () =>
|
||||
deepStrictEqual(paintOrderParse('none'), [
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.MARKERS
|
||||
]));
|
||||
|
||||
it('EMPTY', () =>
|
||||
deepStrictEqual(paintOrderParse(''), [
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.MARKERS
|
||||
]));
|
||||
|
||||
it('other values', () =>
|
||||
deepStrictEqual(paintOrderParse('other values'), [
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.MARKERS
|
||||
]));
|
||||
|
||||
it('normal', () =>
|
||||
deepStrictEqual(paintOrderParse('normal'), [
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.MARKERS
|
||||
]));
|
||||
|
||||
it('stroke', () =>
|
||||
deepStrictEqual(paintOrderParse('stroke'), [
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.MARKERS
|
||||
]));
|
||||
|
||||
it('fill', () =>
|
||||
deepStrictEqual(paintOrderParse('fill'), [
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.MARKERS
|
||||
]));
|
||||
|
||||
it('markers', () =>
|
||||
deepStrictEqual(paintOrderParse('markers'), [
|
||||
PAINT_ORDER_LAYER.MARKERS,
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.STROKE
|
||||
]));
|
||||
|
||||
it('stroke fill', () =>
|
||||
deepStrictEqual(paintOrderParse('stroke fill'), [
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.MARKERS
|
||||
]));
|
||||
|
||||
it('markers stroke', () =>
|
||||
deepStrictEqual(paintOrderParse('markers stroke'), [
|
||||
PAINT_ORDER_LAYER.MARKERS,
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.FILL
|
||||
]));
|
||||
|
||||
it('markers stroke fill', () =>
|
||||
deepStrictEqual(paintOrderParse('markers stroke fill'), [
|
||||
PAINT_ORDER_LAYER.MARKERS,
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.FILL
|
||||
]));
|
||||
|
||||
it('stroke fill markers', () =>
|
||||
deepStrictEqual(paintOrderParse('stroke fill markers'), [
|
||||
PAINT_ORDER_LAYER.STROKE,
|
||||
PAINT_ORDER_LAYER.FILL,
|
||||
PAINT_ORDER_LAYER.MARKERS
|
||||
]));
|
||||
});
|
||||
});
|
94
src/css/property-descriptors/__tests__/text-shadow.ts
Normal file
94
src/css/property-descriptors/__tests__/text-shadow.ts
Normal file
@ -0,0 +1,94 @@
|
||||
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';
|
||||
import {Context} from '../../../core/context';
|
||||
|
||||
const textShadowParse = (value: string) => textShadow.parse({} as Context, Parser.parseValues(value));
|
||||
const colorParse = (value: string) => color.parse({} as Context, 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')
|
||||
}
|
||||
]));
|
||||
});
|
||||
});
|
18
src/css/property-descriptors/__tests__/transform-tests.ts
Normal file
18
src/css/property-descriptors/__tests__/transform-tests.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import {transform} from '../transform';
|
||||
import {Parser} from '../../syntax/parser';
|
||||
import {deepStrictEqual} from 'assert';
|
||||
import {Context} from '../../../core/context';
|
||||
const parseValue = (value: string) => transform.parse({} as Context, 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]
|
||||
));
|
||||
});
|
||||
});
|
30
src/css/property-descriptors/background-clip.ts
Normal file
30
src/css/property-descriptors/background-clip.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
export const 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: (_context: Context, 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;
|
||||
});
|
||||
}
|
||||
};
|
9
src/css/property-descriptors/background-color.ts
Normal file
9
src/css/property-descriptors/background-color.ts
Normal 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'
|
||||
};
|
27
src/css/property-descriptors/background-image.ts
Normal file
27
src/css/property-descriptors/background-image.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import {TokenType} from '../syntax/tokenizer';
|
||||
import {ICSSImage, image, isSupportedImage} from '../types/image';
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, nonFunctionArgSeparator} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export const backgroundImage: IPropertyListDescriptor<ICSSImage[]> = {
|
||||
name: 'background-image',
|
||||
initialValue: 'none',
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
prefix: false,
|
||||
parse: (context: Context, tokens: CSSValue[]) => {
|
||||
if (tokens.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const first = tokens[0];
|
||||
|
||||
if (first.type === TokenType.IDENT_TOKEN && first.value === 'none') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return tokens
|
||||
.filter((value) => nonFunctionArgSeparator(value) && isSupportedImage(value))
|
||||
.map((value) => image.parse(context, value));
|
||||
}
|
||||
};
|
31
src/css/property-descriptors/background-origin.ts
Normal file
31
src/css/property-descriptors/background-origin.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
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: (_context: Context, 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;
|
||||
});
|
||||
}
|
||||
};
|
19
src/css/property-descriptors/background-position.ts
Normal file
19
src/css/property-descriptors/background-position.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {PropertyDescriptorParsingType, IPropertyListDescriptor} from '../IPropertyDescriptor';
|
||||
import {CSSValue, parseFunctionArgs} from '../syntax/parser';
|
||||
import {isLengthPercentage, LengthPercentageTuple, parseLengthPercentageTuple} from '../types/length-percentage';
|
||||
import {Context} from '../../core/context';
|
||||
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: (_context: Context, tokens: CSSValue[]): BackgroundPosition => {
|
||||
return parseFunctionArgs(tokens)
|
||||
.map((values: CSSValue[]) => values.filter(isLengthPercentage))
|
||||
.map(parseLengthPercentageTuple);
|
||||
}
|
||||
};
|
44
src/css/property-descriptors/background-repeat.ts
Normal file
44
src/css/property-descriptors/background-repeat.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken, parseFunctionArgs} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
export type BackgroundRepeat = BACKGROUND_REPEAT[];
|
||||
|
||||
export const 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: (_context: Context, 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;
|
||||
}
|
||||
};
|
27
src/css/property-descriptors/background-size.ts
Normal file
27
src/css/property-descriptors/background-size.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken, parseFunctionArgs} from '../syntax/parser';
|
||||
import {isLengthPercentage, LengthPercentage} from '../types/length-percentage';
|
||||
import {StringValueToken} from '../syntax/tokenizer';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
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: (_context: Context, tokens: CSSValue[]): BackgroundSize => {
|
||||
return parseFunctionArgs(tokens).map((values) => values.filter(isBackgroundSizeInfoToken));
|
||||
}
|
||||
};
|
||||
|
||||
const isBackgroundSizeInfoToken = (value: CSSValue): value is BackgroundSizeInfo =>
|
||||
isIdentToken(value) || isLengthPercentage(value);
|
13
src/css/property-descriptors/border-color.ts
Normal file
13
src/css/property-descriptors/border-color.ts
Normal 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');
|
19
src/css/property-descriptors/border-radius.ts
Normal file
19
src/css/property-descriptors/border-radius.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue} from '../syntax/parser';
|
||||
import {isLengthPercentage, LengthPercentageTuple, parseLengthPercentageTuple} from '../types/length-percentage';
|
||||
import {Context} from '../../core/context';
|
||||
export type BorderRadius = LengthPercentageTuple;
|
||||
|
||||
const borderRadiusForSide = (side: string): IPropertyListDescriptor<BorderRadius> => ({
|
||||
name: `border-radius-${side}`,
|
||||
initialValue: '0 0',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
parse: (_context: Context, 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');
|
34
src/css/property-descriptors/border-style.ts
Normal file
34
src/css/property-descriptors/border-style.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
export const enum BORDER_STYLE {
|
||||
NONE = 0,
|
||||
SOLID = 1,
|
||||
DASHED = 2,
|
||||
DOTTED = 3,
|
||||
DOUBLE = 4
|
||||
}
|
||||
|
||||
const borderStyleForSide = (side: string): IPropertyIdentValueDescriptor<BORDER_STYLE> => ({
|
||||
name: `border-${side}-style`,
|
||||
initialValue: 'solid',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.IDENT_VALUE,
|
||||
parse: (_context: Context, style: string): BORDER_STYLE => {
|
||||
switch (style) {
|
||||
case 'none':
|
||||
return BORDER_STYLE.NONE;
|
||||
case 'dashed':
|
||||
return BORDER_STYLE.DASHED;
|
||||
case 'dotted':
|
||||
return BORDER_STYLE.DOTTED;
|
||||
case 'double':
|
||||
return BORDER_STYLE.DOUBLE;
|
||||
}
|
||||
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');
|
20
src/css/property-descriptors/border-width.ts
Normal file
20
src/css/property-descriptors/border-width.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isDimensionToken} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
const borderWidthForSide = (side: string): IPropertyValueDescriptor<number> => ({
|
||||
name: `border-${side}-width`,
|
||||
initialValue: '0',
|
||||
type: PropertyDescriptorParsingType.VALUE,
|
||||
prefix: false,
|
||||
parse: (_context: Context, 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');
|
60
src/css/property-descriptors/box-shadow.ts
Normal file
60
src/css/property-descriptors/box-shadow.ts
Normal file
@ -0,0 +1,60 @@
|
||||
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';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
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: (context: Context, 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(context, token);
|
||||
}
|
||||
}
|
||||
return shadow;
|
||||
});
|
||||
}
|
||||
};
|
9
src/css/property-descriptors/color.ts
Normal file
9
src/css/property-descriptors/color.ts
Normal 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'
|
||||
};
|
26
src/css/property-descriptors/content.ts
Normal file
26
src/css/property-descriptors/content.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {TokenType} from '../syntax/tokenizer';
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export type Content = CSSValue[];
|
||||
|
||||
export const content: IPropertyListDescriptor<Content> = {
|
||||
name: 'content',
|
||||
initialValue: 'none',
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
prefix: false,
|
||||
parse: (_context: Context, tokens: CSSValue[]) => {
|
||||
if (tokens.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const first = tokens[0];
|
||||
|
||||
if (first.type === TokenType.IDENT_TOKEN && first.value === 'none') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
};
|
43
src/css/property-descriptors/counter-increment.ts
Normal file
43
src/css/property-descriptors/counter-increment.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isNumberToken, nonWhiteSpace} from '../syntax/parser';
|
||||
import {TokenType} from '../syntax/tokenizer';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
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: (_context: Context, 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;
|
||||
}
|
||||
};
|
36
src/css/property-descriptors/counter-reset.ts
Normal file
36
src/css/property-descriptors/counter-reset.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken, isNumberToken, nonWhiteSpace} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
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: (_context: Context, 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;
|
||||
}
|
||||
};
|
23
src/css/property-descriptors/direction.ts
Normal file
23
src/css/property-descriptors/direction.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
export const enum DIRECTION {
|
||||
LTR = 0,
|
||||
RTL = 1
|
||||
}
|
||||
|
||||
export const direction: IPropertyIdentValueDescriptor<DIRECTION> = {
|
||||
name: 'direction',
|
||||
initialValue: 'ltr',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.IDENT_VALUE,
|
||||
parse: (_context: Context, direction: string) => {
|
||||
switch (direction) {
|
||||
case 'rtl':
|
||||
return DIRECTION.RTL;
|
||||
case 'ltr':
|
||||
default:
|
||||
return DIRECTION.LTR;
|
||||
}
|
||||
}
|
||||
};
|
@ -1,45 +1,57 @@
|
||||
/* @flow */
|
||||
'use strict';
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
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: (_context: Context, 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':
|
||||
case '-webkit-box':
|
||||
return DISPLAY.BLOCK;
|
||||
case 'inline':
|
||||
return DISPLAY.INLINE;
|
||||
@ -52,8 +64,10 @@ const parseDisplayValue = (display: string): Display => {
|
||||
case 'table':
|
||||
return DISPLAY.TABLE;
|
||||
case 'flex':
|
||||
case '-webkit-flex':
|
||||
return DISPLAY.FLEX;
|
||||
case 'grid':
|
||||
case '-ms-grid':
|
||||
return DISPLAY.GRID;
|
||||
case 'ruby':
|
||||
return DISPLAY.RUBY;
|
||||
@ -101,11 +115,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);
|
||||
};
|
14
src/css/property-descriptors/duration.ts
Normal file
14
src/css/property-descriptors/duration.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
import {CSSValue, isDimensionToken} from '../syntax/parser';
|
||||
import {time} from '../types/time';
|
||||
|
||||
export const duration: IPropertyListDescriptor<number[]> = {
|
||||
name: 'duration',
|
||||
initialValue: '0s',
|
||||
prefix: false,
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
parse: (context: Context, tokens: CSSValue[]) => {
|
||||
return tokens.filter(isDimensionToken).map((token) => time.parse(context, token));
|
||||
}
|
||||
};
|
29
src/css/property-descriptors/float.ts
Normal file
29
src/css/property-descriptors/float.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
export const 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: (_context: Context, 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;
|
||||
}
|
||||
};
|
38
src/css/property-descriptors/font-family.ts
Normal file
38
src/css/property-descriptors/font-family.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue} from '../syntax/parser';
|
||||
import {TokenType} from '../syntax/tokenizer';
|
||||
import {Context} from '../../core/context';
|
||||
|
||||
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: (_context: Context, tokens: CSSValue[]) => {
|
||||
const accumulator: string[] = [];
|
||||
const results: string[] = [];
|
||||
tokens.forEach((token) => {
|
||||
switch (token.type) {
|
||||
case TokenType.IDENT_TOKEN:
|
||||
case TokenType.STRING_TOKEN:
|
||||
accumulator.push(token.value);
|
||||
break;
|
||||
case TokenType.NUMBER_TOKEN:
|
||||
accumulator.push(token.number.toString());
|
||||
break;
|
||||
case TokenType.COMMA_TOKEN:
|
||||
results.push(accumulator.join(' '));
|
||||
accumulator.length = 0;
|
||||
break;
|
||||
}
|
||||
});
|
||||
if (accumulator.length) {
|
||||
results.push(accumulator.join(' '));
|
||||
}
|
||||
return results.map((result) => (result.indexOf(' ') === -1 ? result : `'${result}'`));
|
||||
}
|
||||
};
|
9
src/css/property-descriptors/font-size.ts
Normal file
9
src/css/property-descriptors/font-size.ts
Normal 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'
|
||||
};
|
25
src/css/property-descriptors/font-style.ts
Normal file
25
src/css/property-descriptors/font-style.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {Context} from '../../core/context';
|
||||
export const 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: (_context: Context, overflow: string) => {
|
||||
switch (overflow) {
|
||||
case 'oblique':
|
||||
return FONT_STYLE.OBLIQUE;
|
||||
case 'italic':
|
||||
return FONT_STYLE.ITALIC;
|
||||
case 'normal':
|
||||
default:
|
||||
return FONT_STYLE.NORMAL;
|
||||
}
|
||||
}
|
||||
};
|
12
src/css/property-descriptors/font-variant.ts
Normal file
12
src/css/property-descriptors/font-variant.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
export const fontVariant: IPropertyListDescriptor<string[]> = {
|
||||
name: 'font-variant',
|
||||
initialValue: 'none',
|
||||
type: PropertyDescriptorParsingType.LIST,
|
||||
prefix: false,
|
||||
parse: (_context: Context, tokens: CSSValue[]): string[] => {
|
||||
return tokens.filter(isIdentToken).map((token) => token.value);
|
||||
}
|
||||
};
|
26
src/css/property-descriptors/font-weight.ts
Normal file
26
src/css/property-descriptors/font-weight.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
|
||||
import {CSSValue, isIdentToken, isNumberToken} from '../syntax/parser';
|
||||
import {Context} from '../../core/context';
|
||||
export const fontWeight: IPropertyValueDescriptor<number> = {
|
||||
name: 'font-weight',
|
||||
initialValue: 'normal',
|
||||
type: PropertyDescriptorParsingType.VALUE,
|
||||
prefix: false,
|
||||
parse: (_context: Context, 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;
|
||||
}
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user