├── .babelrc
├── .editorconfig
├── .eslintrc
├── .github
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
├── no-response.yml
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── .npmignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── configs
├── base.json
├── preview.json
└── scripts.json
├── docs
├── configuration.md
├── documentation.md
├── faq.md
├── features.md
├── getting-started.md
└── proxy.md
├── examples
├── demo.html
├── demo2.html
└── existing_canvas.html
├── jest.config.js
├── karma.conf.js
├── package-lock.json
├── package.json
├── rollup.config.ts
├── scripts
├── create-reftest-list.ts
├── create-reftest-result-list.ts
├── create-reftests.js
└── parse-reftest.js
├── src
├── __tests__
│ └── index.ts
├── core
│ ├── __mocks__
│ │ ├── cache-storage.ts
│ │ ├── context.ts
│ │ ├── features.ts
│ │ └── logger.ts
│ ├── __tests__
│ │ ├── cache-storage.ts
│ │ └── logger.ts
│ ├── bitwise.ts
│ ├── cache-storage.ts
│ ├── context.ts
│ ├── debugger.ts
│ ├── features.ts
│ ├── logger.ts
│ └── util.ts
├── css
│ ├── IPropertyDescriptor.ts
│ ├── ITypeDescriptor.ts
│ ├── index.ts
│ ├── layout
│ │ ├── __mocks__
│ │ │ └── bounds.ts
│ │ ├── bounds.ts
│ │ └── text.ts
│ ├── property-descriptors
│ │ ├── __tests__
│ │ │ ├── background-tests.ts
│ │ │ ├── font-family.ts
│ │ │ ├── paint-order.ts
│ │ │ ├── text-shadow.ts
│ │ │ └── transform-tests.ts
│ │ ├── background-clip.ts
│ │ ├── background-color.ts
│ │ ├── background-image.ts
│ │ ├── background-origin.ts
│ │ ├── background-position.ts
│ │ ├── background-repeat.ts
│ │ ├── background-size.ts
│ │ ├── border-color.ts
│ │ ├── border-radius.ts
│ │ ├── border-style.ts
│ │ ├── border-width.ts
│ │ ├── box-shadow.ts
│ │ ├── color.ts
│ │ ├── content.ts
│ │ ├── counter-increment.ts
│ │ ├── counter-reset.ts
│ │ ├── direction.ts
│ │ ├── display.ts
│ │ ├── duration.ts
│ │ ├── float.ts
│ │ ├── font-family.ts
│ │ ├── font-size.ts
│ │ ├── font-style.ts
│ │ ├── font-variant.ts
│ │ ├── font-weight.ts
│ │ ├── letter-spacing.ts
│ │ ├── line-break.ts
│ │ ├── line-height.ts
│ │ ├── list-style-image.ts
│ │ ├── list-style-position.ts
│ │ ├── list-style-type.ts
│ │ ├── margin.ts
│ │ ├── opacity.ts
│ │ ├── overflow-wrap.ts
│ │ ├── overflow.ts
│ │ ├── padding.ts
│ │ ├── paint-order.ts
│ │ ├── position.ts
│ │ ├── quotes.ts
│ │ ├── text-align.ts
│ │ ├── text-decoration-color.ts
│ │ ├── text-decoration-line.ts
│ │ ├── text-shadow.ts
│ │ ├── text-transform.ts
│ │ ├── transform-origin.ts
│ │ ├── transform.ts
│ │ ├── visibility.ts
│ │ ├── webkit-text-stroke-color.ts
│ │ ├── webkit-text-stroke-width.ts
│ │ ├── word-break.ts
│ │ └── z-index.ts
│ ├── syntax
│ │ ├── __tests__
│ │ │ └── tokernizer-tests.ts
│ │ ├── parser.ts
│ │ └── tokenizer.ts
│ └── types
│ │ ├── __tests__
│ │ ├── color-tests.ts
│ │ └── image-tests.ts
│ │ ├── angle.ts
│ │ ├── color.ts
│ │ ├── functions
│ │ ├── -prefix-linear-gradient.ts
│ │ ├── -prefix-radial-gradient.ts
│ │ ├── -webkit-gradient.ts
│ │ ├── __tests__
│ │ │ └── radial-gradient.ts
│ │ ├── counter.ts
│ │ ├── gradient.ts
│ │ ├── linear-gradient.ts
│ │ └── radial-gradient.ts
│ │ ├── image.ts
│ │ ├── index.ts
│ │ ├── length-percentage.ts
│ │ ├── length.ts
│ │ └── time.ts
├── dom
│ ├── __mocks__
│ │ └── document-cloner.ts
│ ├── document-cloner.ts
│ ├── element-container.ts
│ ├── elements
│ │ ├── li-element-container.ts
│ │ ├── ol-element-container.ts
│ │ ├── select-element-container.ts
│ │ └── textarea-element-container.ts
│ ├── node-parser.ts
│ ├── replaced-elements
│ │ ├── canvas-element-container.ts
│ │ ├── iframe-element-container.ts
│ │ ├── image-element-container.ts
│ │ ├── index.ts
│ │ ├── input-element-container.ts
│ │ ├── pseudo-elements.ts
│ │ └── svg-element-container.ts
│ └── text-container.ts
├── global.d.ts
├── index.ts
├── invariant.ts
└── render
│ ├── background.ts
│ ├── bezier-curve.ts
│ ├── border.ts
│ ├── bound-curves.ts
│ ├── box-sizing.ts
│ ├── canvas
│ ├── canvas-renderer.ts
│ └── foreignobject-renderer.ts
│ ├── effects.ts
│ ├── font-metrics.ts
│ ├── path.ts
│ ├── renderer.ts
│ ├── stacking-context.ts
│ └── vector.ts
├── tests
├── assets
│ ├── bg-sliver.png
│ ├── cc0-video.mp4
│ ├── iframe
│ │ └── frame1.html
│ ├── image.jpg
│ ├── image.svg
│ ├── image2.jpg
│ ├── image2_1.jpg
│ └── image_1.jpg
├── karma.ts
├── node
│ ├── color.js
│ ├── gradient.js
│ ├── package.js
│ └── pseudonodecontent.js
├── rangetest.html
├── reftest-diff.ts
├── reftests
│ ├── acid2.html
│ ├── animation.html
│ ├── background
│ │ ├── base64.css
│ │ ├── box-shadow.html
│ │ ├── clip.html
│ │ ├── encoded.html
│ │ ├── linear-gradient.html
│ │ ├── linear-gradient2.html
│ │ ├── multi.html
│ │ ├── origin.html
│ │ ├── position.html
│ │ ├── radial-gradient.html
│ │ ├── radial-gradient2.html
│ │ ├── repeat.html
│ │ └── size.html
│ ├── border
│ │ ├── dashed.html
│ │ ├── dotted.html
│ │ ├── double.html
│ │ ├── inset.html
│ │ ├── radius.html
│ │ └── solid.html
│ ├── clip.html
│ ├── crossorigin-iframe.html
│ ├── dynamicstyle.html
│ ├── forms.html
│ ├── iframe.html
│ ├── ignore.txt
│ ├── images
│ │ ├── base.html
│ │ ├── canvas.html
│ │ ├── cross-origin.html
│ │ ├── doctype.html
│ │ ├── empty.html
│ │ ├── images.html
│ │ ├── svg
│ │ │ ├── base64.html
│ │ │ ├── external.html
│ │ │ ├── inline.html
│ │ │ ├── native_only.html
│ │ │ └── node.html
│ │ └── video.html
│ ├── list
│ │ ├── decimal-leading-zero.html
│ │ ├── decimal.html
│ │ ├── liststyle.html
│ │ ├── lower-alpha.html
│ │ └── upper-roman.html
│ ├── options
│ │ ├── crop-2.html
│ │ ├── crop.html
│ │ ├── element.html
│ │ ├── ignore-2.html
│ │ ├── ignore.html
│ │ ├── onclone.html
│ │ ├── scroll-2.html
│ │ └── scroll.html
│ ├── overflow
│ │ ├── overflow-transform.html
│ │ └── overflow.html
│ ├── pseudo-content.html
│ ├── pseudoelements.html
│ ├── text
│ │ ├── child-textnodes.html
│ │ ├── fontawesome.html
│ │ ├── lang
│ │ │ ├── chinese.html
│ │ │ ├── persian.html
│ │ │ └── thai.html
│ │ ├── line-break.html
│ │ ├── linethrough.html
│ │ ├── multiple.html
│ │ ├── overflow-wrap.html
│ │ ├── shadow.html
│ │ ├── stroke.html
│ │ ├── text.html
│ │ ├── textarea.html
│ │ ├── underline-lineheight.html
│ │ ├── underline.html
│ │ └── word-break.html
│ ├── transform
│ │ ├── nested.html
│ │ ├── rotate.html
│ │ └── translate.html
│ ├── visibility.html
│ ├── webcomponents
│ │ ├── autonomous-custom-element.js
│ │ ├── slot-element.js
│ │ └── webcomponents.html
│ └── zindex
│ │ ├── z-index1.html
│ │ ├── z-index10.html
│ │ ├── z-index11.html
│ │ ├── z-index12.html
│ │ ├── z-index13.html
│ │ ├── z-index14.html
│ │ ├── z-index15.html
│ │ ├── z-index16.html
│ │ ├── z-index17.html
│ │ ├── z-index18.html
│ │ ├── z-index19.html
│ │ ├── z-index2.html
│ │ ├── z-index20.html
│ │ ├── z-index3.html
│ │ ├── z-index4.html
│ │ ├── z-index5.html
│ │ ├── z-index6.html
│ │ ├── z-index7.html
│ │ ├── z-index8.html
│ │ └── z-index9.html
├── results
│ └── .gitignore
├── rollup.config.ts
├── sauceconnect.js
├── server.ts
├── test.js
├── testrunner.html
├── testrunner.ts
├── tsconfig.json
└── types.ts
├── tsconfig.json
└── www
├── .gitignore
├── LICENSE
├── README.md
├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-node.js
├── gatsby-ssr.js
├── package-lock.json
├── package.json
├── src
├── components
│ ├── carbon.css
│ ├── carbon.js
│ ├── example.css
│ ├── example.js
│ ├── footer.js
│ ├── layout.css
│ ├── layout.js
│ └── navigation.js
├── images
│ ├── ic_arrow_back_black_24px.svg
│ ├── ic_arrow_forward_black_24px.svg
│ ├── ic_camera_alt_black_24px.svg
│ ├── ic_close_black_24px.svg
│ ├── ic_menu_black_24px.svg
│ ├── logo.svg
│ └── logo_icon.svg
├── pages
│ ├── 404.js
│ └── index.js
├── preview.ts
├── templates
│ └── docs.js
└── utils
│ └── typography.js
├── static
├── CNAME
└── tests
│ └── index.html
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [[
3 | "@babel/preset-env",
4 | {
5 | "targets": {
6 | "ie": "9"
7 | }
8 | }
9 | ], "@babel/preset-flow"],
10 | "plugins": [
11 | "add-module-exports"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | indent_style = space
12 | indent_size = 4
13 |
14 | [{*.yml,package.json}]
15 | # The indent size used in the `package.json` file cannot be changed
16 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516
17 | indent_size = 2
18 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "plugin:@typescript-eslint/recommended",
5 | "prettier"
6 | ],
7 | "parserOptions": {
8 | "project": "./tsconfig.json",
9 | "ecmaVersion": 2018,
10 | "sourceType": "module"
11 | },
12 | "plugins": [
13 | "@typescript-eslint",
14 | "prettier"
15 | ],
16 | "rules": {
17 | "no-console": ["error", { "allow": ["warn", "error"] }],
18 | "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
19 | "@typescript-eslint/interface-name-prefix": "off",
20 | "@typescript-eslint/explicit-function-return-type": "off",
21 | "@typescript-eslint/no-use-before-define": "off",
22 | "@typescript-eslint/no-unused-vars": "off",
23 | "@typescript-eslint/class-name-casing": "off",
24 | "prettier/prettier": "error"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Please make sure you are testing with the latest [release of html2canvas](https://github.com/niklasvh/html2canvas/releases).
2 | Old versions are not supported and issues reported for them will be closed.
3 |
4 | # Please follow the general troubleshooting steps first:
5 |
6 | - [ ] You are using the latest [version](https://github.com/niklasvh/html2canvas/releases)
7 | - [ ] You are testing using the non-minified version of html2canvas and checked any potential issues reported in the console
8 |
9 |
10 |
11 | ### Bug reports:
12 |
13 | Please replace this line with a brief summary of your issue **AND** if possible an example on [jsfiddle](https://jsfiddle.net/).
14 |
15 | ### Specifications:
16 |
17 | * html2canvas version tested with:
18 | * Browser & version:
19 | * Operating system:
20 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | A similar PR may already be submitted!
2 | Please search among the [Pull request](https://github.com/niklasvh/html2canvas/pulls) before creating one.
3 |
4 | Thanks for submitting a pull request! Please provide enough information so that others can review your pull request:
5 |
6 | Before opening a pull request, please make sure all the tests pass locally by running `npm test`.
7 |
8 | **Summary**
9 |
10 |
11 |
12 | This PR fixes/implements the following **bugs/features**
13 |
14 | * [ ] Bug 1
15 | * [ ] Bug 2
16 | * [ ] Feature 1
17 | * [ ] Feature 2
18 | * [ ] Breaking changes
19 |
20 |
21 |
22 | Explain the **motivation** for making this change. What existing problem does the pull request solve?
23 |
24 |
25 |
26 | **Test plan (required)**
27 |
28 | Demonstrate how the issue/feature can be replicated. For most cases, simply adding an appropriate html/css template into the [reftests](https://github.com/niklasvh/html2canvas/tree/master/tests/reftests) should be sufficient. Please see other tests there for reference.
29 |
30 | **Code formatting**
31 |
32 | Please make sure that code adheres to the project code formatting. Running `npm run format` will automatically format your code correctly.
33 |
34 | **Closing issues**
35 |
36 |
37 | Fixes #
38 |
--------------------------------------------------------------------------------
/.github/no-response.yml:
--------------------------------------------------------------------------------
1 | # Configuration for probot-no-response - https://github.com/probot/no-response
2 |
3 | # Number of days of inactivity before an Issue is closed for lack of response
4 | daysUntilClose: 14
5 | # Label requiring a response
6 | responseRequiredLabel: Needs More Information
7 | # Comment to post when closing an Issue for lack of response. Set to `false` to disable
8 | closeComment: >
9 | This issue has been automatically closed because there has been no response
10 | to our request for more information from the original author. With only the
11 | information that is currently in the issue, we don't have enough information
12 | to take action. Please reach out if you have or find the answers we need so
13 | that we can investigate further.
14 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Create Release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version:
7 | description: 'Semantic version (major | minor | patch | premajor | preminor | prepatch | prerelease)'
8 | default: 'patch'
9 | required: true
10 |
11 | jobs:
12 | version:
13 | name: Create version
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v2
17 | with:
18 | fetch-depth: 0
19 | token: ${{ secrets.PAT_TOKEN }}
20 | - uses: actions/setup-node@v1
21 | with:
22 | node-version: 12
23 | - name: Npm install
24 | run: npm ci
25 | - name: Configure git
26 | run: |
27 | git config user.name "CI"
28 | git config user.email "niklasvh@gmail.com"
29 | - name: Create release
30 | run: npm run release -- --preset eslint --release-as ${{ github.event.inputs.version }}
31 | - name: Print details
32 | run: |
33 | cat package.json
34 | cat CHANGELOG.md
35 | git tag
36 | - name: Push git version
37 | run: git push --follow-tags origin master
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /dist
2 | /tmp
3 | /build
4 | /nbproject/
5 | image.jpg
6 | /.project
7 | /.settings/
8 | node_modules/
9 | .envrc
10 | *.sublime-workspace
11 | *.baseline
12 | *.iml
13 | .idea/
14 | .DS_Store
15 | npm-debug.log
16 | debug.log
17 | tests/reftests.js
18 | *.log
19 | .rpt2_cache
20 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .github/
2 | .idea/
3 | .rpt2_cache
4 | build/
5 | configs/
6 | docs/
7 | examples/
8 | scripts/
9 | src/
10 | tests/
11 | www/
12 | tmp/
13 | *.iml
14 | .babelrc
15 | .editorconfig
16 | .eslintrc
17 | .npmignore
18 | .prettierrc
19 | jest.config.js
20 | karma.conf.js
21 | karma.js
22 | rollup.config.ts
23 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "tabWidth": 4,
4 | "bracketSpacing": false,
5 | "singleQuote": true,
6 | "printWidth": 120
7 | }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012 Niklas von Hertzen
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/configs/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "noImplicitAny": true,
4 | "noImplicitThis": true,
5 | "noUnusedLocals": true,
6 | "noUnusedParameters": true,
7 | "strictNullChecks": true,
8 | "strictPropertyInitialization": true,
9 | "resolveJsonModule": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/configs/preview.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./base",
3 | "include": [
4 | "../www/src/preview.ts"
5 | ],
6 | "exclude": [
7 | "node_modules"
8 | ]
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/configs/scripts.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./base",
3 | "include": [
4 | "scripts/**/*.ts"
5 | ],
6 | "exclude": [
7 | "node_modules"
8 | ]
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/docs/documentation.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "About"
3 | description: "Learn about html2canvas, how it works and some of its limitations"
4 | nextUrl: "/getting-started"
5 | nextTitle: "Getting Started"
6 | ---
7 |
8 | Before you get started with the script, there are a few things that are good to know regarding the
9 | script and some of its limitations.
10 |
11 | ## Introduction
12 | The script allows you to take "screenshots" of webpages or parts of it, directly on the users browser.
13 | The screenshot is based on the DOM and as such may not be 100% accurate to the real representation
14 | as it does not make an actual screenshot, but builds the screenshot based on the information
15 | available on the page.
16 |
17 | ## How it works
18 | The script traverses through the DOM of the page it is loaded on. It gathers information on all the elements
19 | there, which it then uses to build a representation of the page. In other words, it does not actually take a
20 | screenshot of the page, but builds a representation of it based on the properties it reads from the DOM.
21 |
22 |
23 | As a result, it is only able to render correctly properties that it understands, meaning there are many
24 | CSS properties which do not work. For a full list of supported CSS properties, check out the
25 | [supported features](/features/) page.
26 |
27 | ## Limitations
28 | All the images that the script uses need to reside under the [same origin](http://en.wikipedia.org/wiki/Same_origin_policy)
29 | for it to be able to read them without the assistance of a [proxy](/proxy/). Similarly, if you have other `canvas`
30 | elements on the page, which have been tainted with cross-origin content, they will become dirty and no longer readable by html2canvas.
31 |
32 | The script doesn't render plugin content such as Flash or Java applets.
33 |
34 | ## Browser compatibility
35 |
36 | The library should work fine on the following browsers (with `Promise` polyfill):
37 | - Firefox 3.5+
38 | - Google Chrome
39 | - Opera 12+
40 | - IE9+
41 | - Edge
42 | - Safari 6+
43 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Getting Started"
3 | description: "Learn how to start using html2canvas"
4 | previousUrl: "/documentation"
5 | previousTitle: "About"
6 | nextUrl: "/configuration"
7 | nextTitle: "Configuration"
8 | ---
9 |
10 | ## Installing
11 |
12 | You can install `html2canvas` through npm or [download a built release](https://github.com/niklasvh/html2canvas/releases).
13 |
14 | ### npm
15 |
16 | npm install html2canvas
17 |
18 | ```javascript
19 | import html2canvas from 'html2canvas';
20 | ```
21 |
22 | ## Usage
23 |
24 | To render an `element` with html2canvas with some (optional) [options](/configuration/), simply call `html2canvas(element, options);`
25 |
26 | ```javascript
27 | html2canvas(document.body).then(function(canvas) {
28 | document.body.appendChild(canvas);
29 | });
30 | ```
31 |
--------------------------------------------------------------------------------
/docs/proxy.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Proxy"
3 | description: "Browse different proxies available for supporting CORS content"
4 | ---
5 |
6 | html2canvas does not get around content policy restrictions set by your browser. Drawing images that reside outside of
7 | the origin of the current page taint the canvas that they are drawn upon. If the canvas gets tainted,
8 | it cannot be read anymore. If you wish to load images that reside outside of your pages origin, you can use a proxy to load the images.
9 |
10 | ## Available proxies
11 |
12 | - [node.js](https://github.com/niklasvh/html2canvas-proxy-nodejs)
13 |
--------------------------------------------------------------------------------
/examples/demo2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
19 |
20 |
38 |
39 |
40 |
41 |
42 |
43 |
Heading
44 | Text that isn't wrapped in anything.
45 |
Followed by some text wrapped in a <p> paragraph.
46 | Maybe add a
link or a different style of
link with a highlight.
47 |
48 |
More content
49 |
a
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/examples/existing_canvas.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Using an existing canvas to draw on
6 |
19 |
20 |
21 | HTML content to render:
22 |
Render the content in this element only onto the existing canvas element
23 |
24 | Existing canvas:
25 |
26 |
27 |
28 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'jsdom',
4 | roots: ['src']
5 | };
6 |
--------------------------------------------------------------------------------
/rollup.config.ts:
--------------------------------------------------------------------------------
1 | import resolve from '@rollup/plugin-node-resolve';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import sourceMaps from 'rollup-plugin-sourcemaps';
4 | import typescript from '@rollup/plugin-typescript';
5 | import json from '@rollup/plugin-json';
6 |
7 | const pkg = require('./package.json');
8 |
9 | const banner = `/*!
10 | * ${pkg.title} ${pkg.version} <${pkg.homepage}>
11 | * Copyright (c) ${(new Date()).getFullYear()} ${pkg.author.name} <${pkg.author.url}>
12 | * Released under ${pkg.license} License
13 | */`;
14 |
15 | export default {
16 | input: `src/index.ts`,
17 | output: [
18 | { file: pkg.main, name: pkg.name, format: 'umd', banner, sourcemap: true },
19 | { file: pkg.module, format: 'esm', banner, sourcemap: true },
20 | ],
21 | external: [],
22 | watch: {
23 | include: 'src/**',
24 | },
25 | plugins: [
26 | // Allow node_modules resolution, so you can use 'external' to control
27 | // which external modules to include in the bundle
28 | // https://github.com/rollup/rollup-plugin-node-resolve#usage
29 | resolve(),
30 | // Allow json resolution
31 | json(),
32 | // Compile TypeScript files
33 | typescript({ sourceMap: true, inlineSources: true }),
34 | // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs)
35 | commonjs({
36 | include: 'node_modules/**'
37 | }),
38 |
39 | // Resolve source maps to the original source
40 | sourceMaps(),
41 | ],
42 | }
43 |
--------------------------------------------------------------------------------
/scripts/create-reftest-list.ts:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import {readFileSync, writeFileSync} from 'fs';
4 | import {resolve, relative} from 'path';
5 | import {sync} from 'glob';
6 |
7 | const slash = require('slash');
8 |
9 | if (process.argv.length <= 2) {
10 | console.log('No ignore.txt file provided');
11 | process.exit(1);
12 | }
13 |
14 | if (process.argv.length <= 3) {
15 | console.log('No output file provided');
16 | process.exit(1);
17 | }
18 |
19 | const path = resolve(__dirname, '../', process.argv[2]);
20 | const outputPath = resolve(__dirname, '../', process.argv[3]);
21 | const ignoredTests = readFileSync(path)
22 | .toString()
23 | .split(/\r\n|\r|\n/)
24 | .filter((l) => l.length)
25 | .reduce((acc: {[key: string]: string[]}, l) => {
26 | const m = l.match(/^(\[(.+)\])?(.+)$/i);
27 | if (m) {
28 | acc[m[3]] = m[2] ? m[2].split(',') : [];
29 | }
30 | return acc;
31 | }, {});
32 |
33 | const files: string[] = sync('../tests/reftests/**/*.html', {
34 | cwd: __dirname,
35 | root: resolve(__dirname, '../../')
36 | });
37 |
38 | const testList = files.map((filename: string) => `/${slash(relative('../', filename))}`);
39 | writeFileSync(
40 | outputPath,
41 | [
42 | `export const testList: string[] = ${JSON.stringify(testList, null, 4)};`,
43 | `export const ignoredTests: {[key: string]: string[]} = ${JSON.stringify(ignoredTests, null, 4)};`
44 | ].join('\n')
45 | );
46 |
47 | console.log(`${outputPath} updated`);
48 |
--------------------------------------------------------------------------------
/scripts/create-reftest-result-list.ts:
--------------------------------------------------------------------------------
1 | import {readdirSync, readFileSync, writeFileSync} from 'fs';
2 | import {resolve} from 'path';
3 |
4 | if (process.argv.length <= 2) {
5 | console.log('No metadata path provided');
6 | process.exit(1);
7 | }
8 |
9 | if (process.argv.length <= 3) {
10 | console.log('No output file given');
11 | process.exit(1);
12 | }
13 |
14 | const path = resolve(__dirname, '../', process.argv[2]);
15 | const files = readdirSync(path);
16 |
17 | interface RefTestMetadata {}
18 |
19 | interface RefTestSingleMetadata extends RefTestMetadata {
20 | test?: string;
21 | }
22 |
23 | interface RefTestResults {
24 | [key: string]: Array;
25 | }
26 |
27 | const result: RefTestResults = files.reduce((result: RefTestResults, file) => {
28 | const json: RefTestSingleMetadata = JSON.parse(readFileSync(resolve(__dirname, path, file)).toString());
29 | if (json.test) {
30 | if (!result[json.test]) {
31 | result[json.test] = [];
32 | }
33 |
34 | result[json.test].push(json);
35 | delete json.test;
36 | }
37 |
38 | return result;
39 | }, {});
40 |
41 | const output = resolve(__dirname, '../', process.argv[3]);
42 | writeFileSync(output, JSON.stringify(result));
43 |
44 | console.log(`Wrote file ${output}`);
45 |
--------------------------------------------------------------------------------
/scripts/create-reftests.js:
--------------------------------------------------------------------------------
1 | const {Chromeless} = require('chromeless');
2 | const path = require('path');
3 | const fs = require('fs');
4 | const express = require('express');
5 | const reftests = require('../tests/reftests');
6 |
7 | const app = express();
8 | app.use('/', express.static(path.resolve(__dirname, '../')));
9 |
10 | const listener = app.listen(0, () => {
11 | async function run() {
12 | const chromeless = new Chromeless();
13 | const tests = Object.keys(reftests.testList);
14 | let i = 0;
15 | while (tests[i]) {
16 | const filename = tests[i];
17 | i++;
18 | const reftest = await chromeless
19 | .goto(`http://localhost:${listener.address().port}${filename}?reftest&run=false`)
20 | .evaluate(() =>
21 | html2canvas(document.documentElement, {
22 | windowWidth: 800,
23 | windowHeight: 600,
24 | target: new RefTestRenderer()
25 | })
26 | );
27 | fs.writeFileSync(
28 | path.resolve(__dirname, `..${filename.replace(/\.html$/i, '.txt')}`),
29 | reftest
30 | );
31 | }
32 |
33 | await chromeless.end();
34 | }
35 |
36 | run().catch(console.error.bind(console)).then(() => process.exit(0));
37 | });
38 |
--------------------------------------------------------------------------------
/src/core/__mocks__/cache-storage.ts:
--------------------------------------------------------------------------------
1 | export class CacheStorage {}
2 |
--------------------------------------------------------------------------------
/src/core/__mocks__/context.ts:
--------------------------------------------------------------------------------
1 | import {logger, Logger} from './logger';
2 |
3 | export class Context {
4 | readonly logger: Logger = logger;
5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
6 | readonly _cache: {[key: string]: Promise} = {};
7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
8 | readonly cache: any;
9 |
10 | constructor() {
11 | this.cache = {
12 | addImage: jest.fn().mockImplementation((src: string): Promise => {
13 | const result = Promise.resolve();
14 | this._cache[src] = result;
15 | return result;
16 | })
17 | };
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/core/__mocks__/features.ts:
--------------------------------------------------------------------------------
1 | export const FEATURES = {
2 | SUPPORT_RANGE_BOUNDS: true,
3 | SUPPORT_SVG_DRAWING: true,
4 | SUPPORT_FOREIGNOBJECT_DRAWING: true,
5 | SUPPORT_CORS_IMAGES: true,
6 | SUPPORT_RESPONSE_TYPE: true,
7 | SUPPORT_CORS_XHR: true
8 | };
9 |
--------------------------------------------------------------------------------
/src/core/__mocks__/logger.ts:
--------------------------------------------------------------------------------
1 | export class Logger {
2 | // eslint-disable-next-line @typescript-eslint/no-empty-function
3 | debug(): void {}
4 |
5 | // eslint-disable-next-line @typescript-eslint/no-empty-function
6 | static create(): void {}
7 |
8 | // eslint-disable-next-line @typescript-eslint/no-empty-function
9 | static destroy(): void {}
10 |
11 | static getInstance(): Logger {
12 | return logger;
13 | }
14 |
15 | // eslint-disable-next-line @typescript-eslint/no-empty-function
16 | info(): void {}
17 |
18 | // eslint-disable-next-line @typescript-eslint/no-empty-function
19 | error(): void {}
20 | }
21 |
22 | export const logger = new Logger();
23 |
--------------------------------------------------------------------------------
/src/core/__tests__/logger.ts:
--------------------------------------------------------------------------------
1 | import {Logger} from '../logger';
2 |
3 | describe('logger', () => {
4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
5 | let infoSpy: any;
6 |
7 | beforeEach(() => {
8 | infoSpy = jest.spyOn(console, 'info').mockImplementation(() => {
9 | // do nothing
10 | });
11 | });
12 |
13 | afterEach(() => {
14 | infoSpy.mockRestore();
15 | });
16 |
17 | it('should call console.info when logger enabled', () => {
18 | const id = Math.random().toString();
19 | const logger = new Logger({id, enabled: true});
20 | logger.info('testing');
21 | expect(infoSpy).toHaveBeenLastCalledWith(id, expect.stringMatching(/\d+ms/), 'testing');
22 | });
23 |
24 | it("shouldn't call console.info when logger disabled", () => {
25 | const id = Math.random().toString();
26 | const logger = new Logger({id, enabled: false});
27 | logger.info('testing');
28 | expect(infoSpy).not.toHaveBeenCalled();
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/src/core/bitwise.ts:
--------------------------------------------------------------------------------
1 | export const contains = (bit: number, value: number): boolean => (bit & value) !== 0;
2 |
--------------------------------------------------------------------------------
/src/core/context.ts:
--------------------------------------------------------------------------------
1 | import {Logger} from './logger';
2 | import {Cache, ResourceOptions} from './cache-storage';
3 | import {Bounds} from '../css/layout/bounds';
4 |
5 | export type ContextOptions = {
6 | logging: boolean;
7 | cache?: Cache;
8 | } & ResourceOptions;
9 |
10 | export class Context {
11 | private readonly instanceName = `#${Context.instanceCount++}`;
12 | readonly logger: Logger;
13 | readonly cache: Cache;
14 |
15 | private static instanceCount = 1;
16 |
17 | constructor(options: ContextOptions, public windowBounds: Bounds) {
18 | this.logger = new Logger({id: this.instanceName, enabled: options.logging});
19 | this.cache = options.cache ?? new Cache(this, options);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/core/debugger.ts:
--------------------------------------------------------------------------------
1 | const elementDebuggerAttribute = 'data-html2canvas-debug';
2 | export const enum DebuggerType {
3 | NONE,
4 | ALL,
5 | CLONE,
6 | PARSE,
7 | RENDER
8 | }
9 |
10 | const getElementDebugType = (element: Element): DebuggerType => {
11 | const attribute = element.getAttribute(elementDebuggerAttribute);
12 | switch (attribute) {
13 | case 'all':
14 | return DebuggerType.ALL;
15 | case 'clone':
16 | return DebuggerType.CLONE;
17 | case 'parse':
18 | return DebuggerType.PARSE;
19 | case 'render':
20 | return DebuggerType.RENDER;
21 | default:
22 | return DebuggerType.NONE;
23 | }
24 | };
25 |
26 | export const isDebugging = (element: Element, type: Omit): boolean => {
27 | const elementType = getElementDebugType(element);
28 | return elementType === DebuggerType.ALL || type === elementType;
29 | };
30 |
--------------------------------------------------------------------------------
/src/core/util.ts:
--------------------------------------------------------------------------------
1 | export const SMALL_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
2 |
--------------------------------------------------------------------------------
/src/css/IPropertyDescriptor.ts:
--------------------------------------------------------------------------------
1 | import {CSSValue} from './syntax/parser';
2 | import {CSSTypes} from './types';
3 | import {Context} from '../core/context';
4 |
5 | export const enum PropertyDescriptorParsingType {
6 | VALUE,
7 | LIST,
8 | IDENT_VALUE,
9 | TYPE_VALUE,
10 | TOKEN_VALUE
11 | }
12 |
13 | export interface IPropertyDescriptor {
14 | name: string;
15 | type: PropertyDescriptorParsingType;
16 | initialValue: string;
17 | prefix: boolean;
18 | }
19 |
20 | export interface IPropertyIdentValueDescriptor extends IPropertyDescriptor {
21 | type: PropertyDescriptorParsingType.IDENT_VALUE;
22 | parse: (context: Context, token: string) => T;
23 | }
24 |
25 | export interface IPropertyTypeValueDescriptor extends IPropertyDescriptor {
26 | type: PropertyDescriptorParsingType.TYPE_VALUE;
27 | format: CSSTypes;
28 | }
29 |
30 | export interface IPropertyValueDescriptor extends IPropertyDescriptor {
31 | type: PropertyDescriptorParsingType.VALUE;
32 | parse: (context: Context, token: CSSValue) => T;
33 | }
34 |
35 | export interface IPropertyListDescriptor extends IPropertyDescriptor {
36 | type: PropertyDescriptorParsingType.LIST;
37 | parse: (context: Context, tokens: CSSValue[]) => T;
38 | }
39 |
40 | export interface IPropertyTokenValueDescriptor extends IPropertyDescriptor {
41 | type: PropertyDescriptorParsingType.TOKEN_VALUE;
42 | }
43 |
44 | export type CSSPropertyDescriptor =
45 | | IPropertyValueDescriptor
46 | | IPropertyListDescriptor
47 | | IPropertyIdentValueDescriptor
48 | | IPropertyTypeValueDescriptor
49 | | IPropertyTokenValueDescriptor;
50 |
--------------------------------------------------------------------------------
/src/css/ITypeDescriptor.ts:
--------------------------------------------------------------------------------
1 | import {CSSValue} from './syntax/parser';
2 | import {Context} from '../core/context';
3 |
4 | export interface ITypeDescriptor {
5 | name: string;
6 | parse: (context: Context, value: CSSValue) => T;
7 | }
8 |
--------------------------------------------------------------------------------
/src/css/layout/__mocks__/bounds.ts:
--------------------------------------------------------------------------------
1 | export const {Bounds} = jest.requireActual('../bounds');
2 | export const parseBounds = (): typeof Bounds => {
3 | return new Bounds(0, 0, 200, 50);
4 | };
5 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/__tests__/font-family.ts:
--------------------------------------------------------------------------------
1 | import {deepEqual} from 'assert';
2 | import {Parser} from '../../syntax/parser';
3 | import {fontFamily} from '../font-family';
4 | import {Context} from '../../../core/context';
5 |
6 | const fontFamilyParse = (value: string) => fontFamily.parse({} as Context, Parser.parseValues(value));
7 |
8 | describe('property-descriptors', () => {
9 | describe('font-family', () => {
10 | it('sans-serif', () => deepEqual(fontFamilyParse('sans-serif'), ['sans-serif']));
11 |
12 | it('great fonts 40 library', () =>
13 | deepEqual(fontFamilyParse('great fonts 40 library'), ["'great fonts 40 library'"]));
14 |
15 | it('preferred font, "quoted fallback font", font', () =>
16 | deepEqual(fontFamilyParse('preferred font, "quoted fallback font", font'), [
17 | "'preferred font'",
18 | "'quoted fallback font'",
19 | 'font'
20 | ]));
21 |
22 | it("'escaping test\\'s font'", () =>
23 | deepEqual(fontFamilyParse("'escaping test\\'s font'"), ["'escaping test's font'"]));
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/__tests__/transform-tests.ts:
--------------------------------------------------------------------------------
1 | import {transform} from '../transform';
2 | import {Parser} from '../../syntax/parser';
3 | import {deepStrictEqual} from 'assert';
4 | import {Context} from '../../../core/context';
5 | const parseValue = (value: string) => transform.parse({} as Context, Parser.parseValue(value));
6 |
7 | describe('property-descriptors', () => {
8 | describe('transform', () => {
9 | it('none', () => deepStrictEqual(parseValue('none'), null));
10 | it('matrix(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)', () =>
11 | deepStrictEqual(parseValue('matrix(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)'), [1, 2, 3, 4, 5, 6]));
12 | it('matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)', () =>
13 | deepStrictEqual(
14 | parseValue('matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)'),
15 | [1, 0, 0, 1, 0, 0]
16 | ));
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/background-clip.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue, isIdentToken} from '../syntax/parser';
3 | import {Context} from '../../core/context';
4 | export const enum BACKGROUND_CLIP {
5 | BORDER_BOX = 0,
6 | PADDING_BOX = 1,
7 | CONTENT_BOX = 2
8 | }
9 |
10 | export type BackgroundClip = BACKGROUND_CLIP[];
11 |
12 | export const backgroundClip: IPropertyListDescriptor = {
13 | name: 'background-clip',
14 | initialValue: 'border-box',
15 | prefix: false,
16 | type: PropertyDescriptorParsingType.LIST,
17 | parse: (_context: Context, tokens: CSSValue[]): BackgroundClip => {
18 | return tokens.map((token) => {
19 | if (isIdentToken(token)) {
20 | switch (token.value) {
21 | case 'padding-box':
22 | return BACKGROUND_CLIP.PADDING_BOX;
23 | case 'content-box':
24 | return BACKGROUND_CLIP.CONTENT_BOX;
25 | }
26 | }
27 | return BACKGROUND_CLIP.BORDER_BOX;
28 | });
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/background-color.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 |
3 | export const backgroundColor: IPropertyTypeValueDescriptor = {
4 | name: `background-color`,
5 | initialValue: 'transparent',
6 | prefix: false,
7 | type: PropertyDescriptorParsingType.TYPE_VALUE,
8 | format: 'color'
9 | };
10 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/background-image.ts:
--------------------------------------------------------------------------------
1 | import {TokenType} from '../syntax/tokenizer';
2 | import {ICSSImage, image, isSupportedImage} from '../types/image';
3 | import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
4 | import {CSSValue, nonFunctionArgSeparator} from '../syntax/parser';
5 | import {Context} from '../../core/context';
6 |
7 | export const backgroundImage: IPropertyListDescriptor = {
8 | name: 'background-image',
9 | initialValue: 'none',
10 | type: PropertyDescriptorParsingType.LIST,
11 | prefix: false,
12 | parse: (context: Context, tokens: CSSValue[]) => {
13 | if (tokens.length === 0) {
14 | return [];
15 | }
16 |
17 | const first = tokens[0];
18 |
19 | if (first.type === TokenType.IDENT_TOKEN && first.value === 'none') {
20 | return [];
21 | }
22 |
23 | return tokens
24 | .filter((value) => nonFunctionArgSeparator(value) && isSupportedImage(value))
25 | .map((value) => image.parse(context, value));
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/background-origin.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue, isIdentToken} from '../syntax/parser';
3 | import {Context} from '../../core/context';
4 |
5 | export const enum BACKGROUND_ORIGIN {
6 | BORDER_BOX = 0,
7 | PADDING_BOX = 1,
8 | CONTENT_BOX = 2
9 | }
10 |
11 | export type BackgroundOrigin = BACKGROUND_ORIGIN[];
12 |
13 | export const backgroundOrigin: IPropertyListDescriptor = {
14 | name: 'background-origin',
15 | initialValue: 'border-box',
16 | prefix: false,
17 | type: PropertyDescriptorParsingType.LIST,
18 | parse: (_context: Context, tokens: CSSValue[]): BackgroundOrigin => {
19 | return tokens.map((token) => {
20 | if (isIdentToken(token)) {
21 | switch (token.value) {
22 | case 'padding-box':
23 | return BACKGROUND_ORIGIN.PADDING_BOX;
24 | case 'content-box':
25 | return BACKGROUND_ORIGIN.CONTENT_BOX;
26 | }
27 | }
28 | return BACKGROUND_ORIGIN.BORDER_BOX;
29 | });
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/background-position.ts:
--------------------------------------------------------------------------------
1 | import {PropertyDescriptorParsingType, IPropertyListDescriptor} from '../IPropertyDescriptor';
2 | import {CSSValue, parseFunctionArgs} from '../syntax/parser';
3 | import {isLengthPercentage, LengthPercentageTuple, parseLengthPercentageTuple} from '../types/length-percentage';
4 | import {Context} from '../../core/context';
5 | export type BackgroundPosition = BackgroundImagePosition[];
6 |
7 | export type BackgroundImagePosition = LengthPercentageTuple;
8 |
9 | export const backgroundPosition: IPropertyListDescriptor = {
10 | name: 'background-position',
11 | initialValue: '0% 0%',
12 | type: PropertyDescriptorParsingType.LIST,
13 | prefix: false,
14 | parse: (_context: Context, tokens: CSSValue[]): BackgroundPosition => {
15 | return parseFunctionArgs(tokens)
16 | .map((values: CSSValue[]) => values.filter(isLengthPercentage))
17 | .map(parseLengthPercentageTuple);
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/background-repeat.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue, isIdentToken, parseFunctionArgs} from '../syntax/parser';
3 | import {Context} from '../../core/context';
4 | export type BackgroundRepeat = BACKGROUND_REPEAT[];
5 |
6 | export const enum BACKGROUND_REPEAT {
7 | REPEAT = 0,
8 | NO_REPEAT = 1,
9 | REPEAT_X = 2,
10 | REPEAT_Y = 3
11 | }
12 |
13 | export const backgroundRepeat: IPropertyListDescriptor = {
14 | name: 'background-repeat',
15 | initialValue: 'repeat',
16 | prefix: false,
17 | type: PropertyDescriptorParsingType.LIST,
18 | parse: (_context: Context, tokens: CSSValue[]): BackgroundRepeat => {
19 | return parseFunctionArgs(tokens)
20 | .map((values) =>
21 | values
22 | .filter(isIdentToken)
23 | .map((token) => token.value)
24 | .join(' ')
25 | )
26 | .map(parseBackgroundRepeat);
27 | }
28 | };
29 |
30 | const parseBackgroundRepeat = (value: string): BACKGROUND_REPEAT => {
31 | switch (value) {
32 | case 'no-repeat':
33 | return BACKGROUND_REPEAT.NO_REPEAT;
34 | case 'repeat-x':
35 | case 'repeat no-repeat':
36 | return BACKGROUND_REPEAT.REPEAT_X;
37 | case 'repeat-y':
38 | case 'no-repeat repeat':
39 | return BACKGROUND_REPEAT.REPEAT_Y;
40 | case 'repeat':
41 | default:
42 | return BACKGROUND_REPEAT.REPEAT;
43 | }
44 | };
45 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/background-size.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue, isIdentToken, parseFunctionArgs} from '../syntax/parser';
3 | import {isLengthPercentage, LengthPercentage} from '../types/length-percentage';
4 | import {StringValueToken} from '../syntax/tokenizer';
5 | import {Context} from '../../core/context';
6 |
7 | export enum BACKGROUND_SIZE {
8 | AUTO = 'auto',
9 | CONTAIN = 'contain',
10 | COVER = 'cover'
11 | }
12 |
13 | export type BackgroundSizeInfo = LengthPercentage | StringValueToken;
14 | export type BackgroundSize = BackgroundSizeInfo[][];
15 |
16 | export const backgroundSize: IPropertyListDescriptor = {
17 | name: 'background-size',
18 | initialValue: '0',
19 | prefix: false,
20 | type: PropertyDescriptorParsingType.LIST,
21 | parse: (_context: Context, tokens: CSSValue[]): BackgroundSize => {
22 | return parseFunctionArgs(tokens).map((values) => values.filter(isBackgroundSizeInfoToken));
23 | }
24 | };
25 |
26 | const isBackgroundSizeInfoToken = (value: CSSValue): value is BackgroundSizeInfo =>
27 | isIdentToken(value) || isLengthPercentage(value);
28 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/border-color.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | const borderColorForSide = (side: string): IPropertyTypeValueDescriptor => ({
3 | name: `border-${side}-color`,
4 | initialValue: 'transparent',
5 | prefix: false,
6 | type: PropertyDescriptorParsingType.TYPE_VALUE,
7 | format: 'color'
8 | });
9 |
10 | export const borderTopColor: IPropertyTypeValueDescriptor = borderColorForSide('top');
11 | export const borderRightColor: IPropertyTypeValueDescriptor = borderColorForSide('right');
12 | export const borderBottomColor: IPropertyTypeValueDescriptor = borderColorForSide('bottom');
13 | export const borderLeftColor: IPropertyTypeValueDescriptor = borderColorForSide('left');
14 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/border-radius.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue} from '../syntax/parser';
3 | import {isLengthPercentage, LengthPercentageTuple, parseLengthPercentageTuple} from '../types/length-percentage';
4 | import {Context} from '../../core/context';
5 | export type BorderRadius = LengthPercentageTuple;
6 |
7 | const borderRadiusForSide = (side: string): IPropertyListDescriptor => ({
8 | name: `border-radius-${side}`,
9 | initialValue: '0 0',
10 | prefix: false,
11 | type: PropertyDescriptorParsingType.LIST,
12 | parse: (_context: Context, tokens: CSSValue[]): BorderRadius =>
13 | parseLengthPercentageTuple(tokens.filter(isLengthPercentage))
14 | });
15 |
16 | export const borderTopLeftRadius: IPropertyListDescriptor = borderRadiusForSide('top-left');
17 | export const borderTopRightRadius: IPropertyListDescriptor = borderRadiusForSide('top-right');
18 | export const borderBottomRightRadius: IPropertyListDescriptor = borderRadiusForSide('bottom-right');
19 | export const borderBottomLeftRadius: IPropertyListDescriptor = borderRadiusForSide('bottom-left');
20 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/border-style.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {Context} from '../../core/context';
3 | export const enum BORDER_STYLE {
4 | NONE = 0,
5 | SOLID = 1,
6 | DASHED = 2,
7 | DOTTED = 3,
8 | DOUBLE = 4
9 | }
10 |
11 | const borderStyleForSide = (side: string): IPropertyIdentValueDescriptor => ({
12 | name: `border-${side}-style`,
13 | initialValue: 'solid',
14 | prefix: false,
15 | type: PropertyDescriptorParsingType.IDENT_VALUE,
16 | parse: (_context: Context, style: string): BORDER_STYLE => {
17 | switch (style) {
18 | case 'none':
19 | return BORDER_STYLE.NONE;
20 | case 'dashed':
21 | return BORDER_STYLE.DASHED;
22 | case 'dotted':
23 | return BORDER_STYLE.DOTTED;
24 | case 'double':
25 | return BORDER_STYLE.DOUBLE;
26 | }
27 | return BORDER_STYLE.SOLID;
28 | }
29 | });
30 |
31 | export const borderTopStyle: IPropertyIdentValueDescriptor = borderStyleForSide('top');
32 | export const borderRightStyle: IPropertyIdentValueDescriptor = borderStyleForSide('right');
33 | export const borderBottomStyle: IPropertyIdentValueDescriptor = borderStyleForSide('bottom');
34 | export const borderLeftStyle: IPropertyIdentValueDescriptor = borderStyleForSide('left');
35 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/border-width.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue, isDimensionToken} from '../syntax/parser';
3 | import {Context} from '../../core/context';
4 | const borderWidthForSide = (side: string): IPropertyValueDescriptor => ({
5 | name: `border-${side}-width`,
6 | initialValue: '0',
7 | type: PropertyDescriptorParsingType.VALUE,
8 | prefix: false,
9 | parse: (_context: Context, token: CSSValue): number => {
10 | if (isDimensionToken(token)) {
11 | return token.number;
12 | }
13 | return 0;
14 | }
15 | });
16 |
17 | export const borderTopWidth: IPropertyValueDescriptor = borderWidthForSide('top');
18 | export const borderRightWidth: IPropertyValueDescriptor = borderWidthForSide('right');
19 | export const borderBottomWidth: IPropertyValueDescriptor = borderWidthForSide('bottom');
20 | export const borderLeftWidth: IPropertyValueDescriptor = borderWidthForSide('left');
21 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/color.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 |
3 | export const color: IPropertyTypeValueDescriptor = {
4 | name: `color`,
5 | initialValue: 'transparent',
6 | prefix: false,
7 | type: PropertyDescriptorParsingType.TYPE_VALUE,
8 | format: 'color'
9 | };
10 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/content.ts:
--------------------------------------------------------------------------------
1 | import {TokenType} from '../syntax/tokenizer';
2 | import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
3 | import {CSSValue} from '../syntax/parser';
4 | import {Context} from '../../core/context';
5 |
6 | export type Content = CSSValue[];
7 |
8 | export const content: IPropertyListDescriptor = {
9 | name: 'content',
10 | initialValue: 'none',
11 | type: PropertyDescriptorParsingType.LIST,
12 | prefix: false,
13 | parse: (_context: Context, tokens: CSSValue[]) => {
14 | if (tokens.length === 0) {
15 | return [];
16 | }
17 |
18 | const first = tokens[0];
19 |
20 | if (first.type === TokenType.IDENT_TOKEN && first.value === 'none') {
21 | return [];
22 | }
23 |
24 | return tokens;
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/counter-increment.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue, isNumberToken, nonWhiteSpace} from '../syntax/parser';
3 | import {TokenType} from '../syntax/tokenizer';
4 | import {Context} from '../../core/context';
5 |
6 | export interface COUNTER_INCREMENT {
7 | counter: string;
8 | increment: number;
9 | }
10 |
11 | export type CounterIncrement = COUNTER_INCREMENT[] | null;
12 |
13 | export const counterIncrement: IPropertyListDescriptor = {
14 | name: 'counter-increment',
15 | initialValue: 'none',
16 | prefix: true,
17 | type: PropertyDescriptorParsingType.LIST,
18 | parse: (_context: Context, tokens: CSSValue[]) => {
19 | if (tokens.length === 0) {
20 | return null;
21 | }
22 |
23 | const first = tokens[0];
24 |
25 | if (first.type === TokenType.IDENT_TOKEN && first.value === 'none') {
26 | return null;
27 | }
28 |
29 | const increments = [];
30 | const filtered = tokens.filter(nonWhiteSpace);
31 |
32 | for (let i = 0; i < filtered.length; i++) {
33 | const counter = filtered[i];
34 | const next = filtered[i + 1];
35 | if (counter.type === TokenType.IDENT_TOKEN) {
36 | const increment = next && isNumberToken(next) ? next.number : 1;
37 | increments.push({counter: counter.value, increment});
38 | }
39 | }
40 |
41 | return increments;
42 | }
43 | };
44 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/counter-reset.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue, isIdentToken, isNumberToken, nonWhiteSpace} from '../syntax/parser';
3 | import {Context} from '../../core/context';
4 |
5 | export interface COUNTER_RESET {
6 | counter: string;
7 | reset: number;
8 | }
9 |
10 | export type CounterReset = COUNTER_RESET[];
11 |
12 | export const counterReset: IPropertyListDescriptor = {
13 | name: 'counter-reset',
14 | initialValue: 'none',
15 | prefix: true,
16 | type: PropertyDescriptorParsingType.LIST,
17 | parse: (_context: Context, tokens: CSSValue[]) => {
18 | if (tokens.length === 0) {
19 | return [];
20 | }
21 |
22 | const resets = [];
23 | const filtered = tokens.filter(nonWhiteSpace);
24 |
25 | for (let i = 0; i < filtered.length; i++) {
26 | const counter = filtered[i];
27 | const next = filtered[i + 1];
28 | if (isIdentToken(counter) && counter.value !== 'none') {
29 | const reset = next && isNumberToken(next) ? next.number : 0;
30 | resets.push({counter: counter.value, reset});
31 | }
32 | }
33 |
34 | return resets;
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/direction.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {Context} from '../../core/context';
3 |
4 | export const enum DIRECTION {
5 | LTR = 0,
6 | RTL = 1
7 | }
8 |
9 | export const direction: IPropertyIdentValueDescriptor = {
10 | name: 'direction',
11 | initialValue: 'ltr',
12 | prefix: false,
13 | type: PropertyDescriptorParsingType.IDENT_VALUE,
14 | parse: (_context: Context, direction: string) => {
15 | switch (direction) {
16 | case 'rtl':
17 | return DIRECTION.RTL;
18 | case 'ltr':
19 | default:
20 | return DIRECTION.LTR;
21 | }
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/duration.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {Context} from '../../core/context';
3 | import {CSSValue, isDimensionToken} from '../syntax/parser';
4 | import {time} from '../types/time';
5 |
6 | export const duration: IPropertyListDescriptor = {
7 | name: 'duration',
8 | initialValue: '0s',
9 | prefix: false,
10 | type: PropertyDescriptorParsingType.LIST,
11 | parse: (context: Context, tokens: CSSValue[]) => {
12 | return tokens.filter(isDimensionToken).map((token) => time.parse(context, token));
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/float.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {Context} from '../../core/context';
3 | export const enum FLOAT {
4 | NONE = 0,
5 | LEFT = 1,
6 | RIGHT = 2,
7 | INLINE_START = 3,
8 | INLINE_END = 4
9 | }
10 |
11 | export const float: IPropertyIdentValueDescriptor = {
12 | name: 'float',
13 | initialValue: 'none',
14 | prefix: false,
15 | type: PropertyDescriptorParsingType.IDENT_VALUE,
16 | parse: (_context: Context, float: string) => {
17 | switch (float) {
18 | case 'left':
19 | return FLOAT.LEFT;
20 | case 'right':
21 | return FLOAT.RIGHT;
22 | case 'inline-start':
23 | return FLOAT.INLINE_START;
24 | case 'inline-end':
25 | return FLOAT.INLINE_END;
26 | }
27 | return FLOAT.NONE;
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/font-family.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue} from '../syntax/parser';
3 | import {TokenType} from '../syntax/tokenizer';
4 | import {Context} from '../../core/context';
5 |
6 | export type FONT_FAMILY = string;
7 |
8 | export type FontFamily = FONT_FAMILY[];
9 |
10 | export const fontFamily: IPropertyListDescriptor = {
11 | name: `font-family`,
12 | initialValue: '',
13 | prefix: false,
14 | type: PropertyDescriptorParsingType.LIST,
15 | parse: (_context: Context, tokens: CSSValue[]) => {
16 | const accumulator: string[] = [];
17 | const results: string[] = [];
18 | tokens.forEach((token) => {
19 | switch (token.type) {
20 | case TokenType.IDENT_TOKEN:
21 | case TokenType.STRING_TOKEN:
22 | accumulator.push(token.value);
23 | break;
24 | case TokenType.NUMBER_TOKEN:
25 | accumulator.push(token.number.toString());
26 | break;
27 | case TokenType.COMMA_TOKEN:
28 | results.push(accumulator.join(' '));
29 | accumulator.length = 0;
30 | break;
31 | }
32 | });
33 | if (accumulator.length) {
34 | results.push(accumulator.join(' '));
35 | }
36 | return results.map((result) => (result.indexOf(' ') === -1 ? result : `'${result}'`));
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/font-size.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 |
3 | export const fontSize: IPropertyTypeValueDescriptor = {
4 | name: `font-size`,
5 | initialValue: '0',
6 | prefix: false,
7 | type: PropertyDescriptorParsingType.TYPE_VALUE,
8 | format: 'length'
9 | };
10 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/font-style.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {Context} from '../../core/context';
3 | export const enum FONT_STYLE {
4 | NORMAL = 'normal',
5 | ITALIC = 'italic',
6 | OBLIQUE = 'oblique'
7 | }
8 |
9 | export const fontStyle: IPropertyIdentValueDescriptor = {
10 | name: 'font-style',
11 | initialValue: 'normal',
12 | prefix: false,
13 | type: PropertyDescriptorParsingType.IDENT_VALUE,
14 | parse: (_context: Context, overflow: string) => {
15 | switch (overflow) {
16 | case 'oblique':
17 | return FONT_STYLE.OBLIQUE;
18 | case 'italic':
19 | return FONT_STYLE.ITALIC;
20 | case 'normal':
21 | default:
22 | return FONT_STYLE.NORMAL;
23 | }
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/font-variant.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue, isIdentToken} from '../syntax/parser';
3 | import {Context} from '../../core/context';
4 | export const fontVariant: IPropertyListDescriptor = {
5 | name: 'font-variant',
6 | initialValue: 'none',
7 | type: PropertyDescriptorParsingType.LIST,
8 | prefix: false,
9 | parse: (_context: Context, tokens: CSSValue[]): string[] => {
10 | return tokens.filter(isIdentToken).map((token) => token.value);
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/font-weight.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue, isIdentToken, isNumberToken} from '../syntax/parser';
3 | import {Context} from '../../core/context';
4 | export const fontWeight: IPropertyValueDescriptor = {
5 | name: 'font-weight',
6 | initialValue: 'normal',
7 | type: PropertyDescriptorParsingType.VALUE,
8 | prefix: false,
9 | parse: (_context: Context, token: CSSValue): number => {
10 | if (isNumberToken(token)) {
11 | return token.number;
12 | }
13 |
14 | if (isIdentToken(token)) {
15 | switch (token.value) {
16 | case 'bold':
17 | return 700;
18 | case 'normal':
19 | default:
20 | return 400;
21 | }
22 | }
23 |
24 | return 400;
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/letter-spacing.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue} from '../syntax/parser';
3 | import {TokenType} from '../syntax/tokenizer';
4 | import {Context} from '../../core/context';
5 | export const letterSpacing: IPropertyValueDescriptor = {
6 | name: 'letter-spacing',
7 | initialValue: '0',
8 | prefix: false,
9 | type: PropertyDescriptorParsingType.VALUE,
10 | parse: (_context: Context, token: CSSValue) => {
11 | if (token.type === TokenType.IDENT_TOKEN && token.value === 'normal') {
12 | return 0;
13 | }
14 |
15 | if (token.type === TokenType.NUMBER_TOKEN) {
16 | return token.number;
17 | }
18 |
19 | if (token.type === TokenType.DIMENSION_TOKEN) {
20 | return token.number;
21 | }
22 |
23 | return 0;
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/line-break.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {Context} from '../../core/context';
3 | export enum LINE_BREAK {
4 | NORMAL = 'normal',
5 | STRICT = 'strict'
6 | }
7 |
8 | export const lineBreak: IPropertyIdentValueDescriptor = {
9 | name: 'line-break',
10 | initialValue: 'normal',
11 | prefix: false,
12 | type: PropertyDescriptorParsingType.IDENT_VALUE,
13 | parse: (_context: Context, lineBreak: string): LINE_BREAK => {
14 | switch (lineBreak) {
15 | case 'strict':
16 | return LINE_BREAK.STRICT;
17 | case 'normal':
18 | default:
19 | return LINE_BREAK.NORMAL;
20 | }
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/line-height.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyTokenValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue, isIdentToken} from '../syntax/parser';
3 | import {TokenType} from '../syntax/tokenizer';
4 | import {getAbsoluteValue, isLengthPercentage} from '../types/length-percentage';
5 | export const lineHeight: IPropertyTokenValueDescriptor = {
6 | name: 'line-height',
7 | initialValue: 'normal',
8 | prefix: false,
9 | type: PropertyDescriptorParsingType.TOKEN_VALUE
10 | };
11 |
12 | export const computeLineHeight = (token: CSSValue, fontSize: number): number => {
13 | if (isIdentToken(token) && token.value === 'normal') {
14 | return 1.2 * fontSize;
15 | } else if (token.type === TokenType.NUMBER_TOKEN) {
16 | return fontSize * token.number;
17 | } else if (isLengthPercentage(token)) {
18 | return getAbsoluteValue(token, fontSize);
19 | }
20 |
21 | return fontSize;
22 | };
23 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/list-style-image.ts:
--------------------------------------------------------------------------------
1 | import {TokenType} from '../syntax/tokenizer';
2 | import {ICSSImage, image} from '../types/image';
3 | import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
4 | import {CSSValue} from '../syntax/parser';
5 | import {Context} from '../../core/context';
6 |
7 | export const listStyleImage: IPropertyValueDescriptor = {
8 | name: 'list-style-image',
9 | initialValue: 'none',
10 | type: PropertyDescriptorParsingType.VALUE,
11 | prefix: false,
12 | parse: (context: Context, token: CSSValue) => {
13 | if (token.type === TokenType.IDENT_TOKEN && token.value === 'none') {
14 | return null;
15 | }
16 |
17 | return image.parse(context, token);
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/list-style-position.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {Context} from '../../core/context';
3 | export const enum LIST_STYLE_POSITION {
4 | INSIDE = 0,
5 | OUTSIDE = 1
6 | }
7 |
8 | export const listStylePosition: IPropertyIdentValueDescriptor = {
9 | name: 'list-style-position',
10 | initialValue: 'outside',
11 | prefix: false,
12 | type: PropertyDescriptorParsingType.IDENT_VALUE,
13 | parse: (_context: Context, position: string) => {
14 | switch (position) {
15 | case 'inside':
16 | return LIST_STYLE_POSITION.INSIDE;
17 | case 'outside':
18 | default:
19 | return LIST_STYLE_POSITION.OUTSIDE;
20 | }
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/margin.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyTokenValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 |
3 | const marginForSide = (side: string): IPropertyTokenValueDescriptor => ({
4 | name: `margin-${side}`,
5 | initialValue: '0',
6 | prefix: false,
7 | type: PropertyDescriptorParsingType.TOKEN_VALUE
8 | });
9 |
10 | export const marginTop: IPropertyTokenValueDescriptor = marginForSide('top');
11 | export const marginRight: IPropertyTokenValueDescriptor = marginForSide('right');
12 | export const marginBottom: IPropertyTokenValueDescriptor = marginForSide('bottom');
13 | export const marginLeft: IPropertyTokenValueDescriptor = marginForSide('left');
14 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/opacity.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue, isNumberToken} from '../syntax/parser';
3 | import {Context} from '../../core/context';
4 | export const opacity: IPropertyValueDescriptor = {
5 | name: 'opacity',
6 | initialValue: '1',
7 | type: PropertyDescriptorParsingType.VALUE,
8 | prefix: false,
9 | parse: (_context: Context, token: CSSValue): number => {
10 | if (isNumberToken(token)) {
11 | return token.number;
12 | }
13 | return 1;
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/overflow-wrap.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {Context} from '../../core/context';
3 | export const enum OVERFLOW_WRAP {
4 | NORMAL = 'normal',
5 | BREAK_WORD = 'break-word'
6 | }
7 |
8 | export const overflowWrap: IPropertyIdentValueDescriptor = {
9 | name: 'overflow-wrap',
10 | initialValue: 'normal',
11 | prefix: false,
12 | type: PropertyDescriptorParsingType.IDENT_VALUE,
13 | parse: (_context: Context, overflow: string) => {
14 | switch (overflow) {
15 | case 'break-word':
16 | return OVERFLOW_WRAP.BREAK_WORD;
17 | case 'normal':
18 | default:
19 | return OVERFLOW_WRAP.NORMAL;
20 | }
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/overflow.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue, isIdentToken} from '../syntax/parser';
3 | import {Context} from '../../core/context';
4 | export const enum OVERFLOW {
5 | VISIBLE = 0,
6 | HIDDEN = 1,
7 | SCROLL = 2,
8 | CLIP = 3,
9 | AUTO = 4
10 | }
11 |
12 | export const overflow: IPropertyListDescriptor = {
13 | name: 'overflow',
14 | initialValue: 'visible',
15 | prefix: false,
16 | type: PropertyDescriptorParsingType.LIST,
17 | parse: (_context: Context, tokens: CSSValue[]): OVERFLOW[] => {
18 | return tokens.filter(isIdentToken).map((overflow) => {
19 | switch (overflow.value) {
20 | case 'hidden':
21 | return OVERFLOW.HIDDEN;
22 | case 'scroll':
23 | return OVERFLOW.SCROLL;
24 | case 'clip':
25 | return OVERFLOW.CLIP;
26 | case 'auto':
27 | return OVERFLOW.AUTO;
28 | case 'visible':
29 | default:
30 | return OVERFLOW.VISIBLE;
31 | }
32 | });
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/padding.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 |
3 | const paddingForSide = (side: string): IPropertyTypeValueDescriptor => ({
4 | name: `padding-${side}`,
5 | initialValue: '0',
6 | prefix: false,
7 | type: PropertyDescriptorParsingType.TYPE_VALUE,
8 | format: 'length-percentage'
9 | });
10 |
11 | export const paddingTop: IPropertyTypeValueDescriptor = paddingForSide('top');
12 | export const paddingRight: IPropertyTypeValueDescriptor = paddingForSide('right');
13 | export const paddingBottom: IPropertyTypeValueDescriptor = paddingForSide('bottom');
14 | export const paddingLeft: IPropertyTypeValueDescriptor = paddingForSide('left');
15 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/paint-order.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue, isIdentToken} from '../syntax/parser';
3 | import {Context} from '../../core/context';
4 | export const enum PAINT_ORDER_LAYER {
5 | FILL,
6 | STROKE,
7 | MARKERS
8 | }
9 |
10 | export type PaintOrder = PAINT_ORDER_LAYER[];
11 |
12 | export const paintOrder: IPropertyListDescriptor = {
13 | name: 'paint-order',
14 | initialValue: 'normal',
15 | prefix: false,
16 | type: PropertyDescriptorParsingType.LIST,
17 | parse: (_context: Context, tokens: CSSValue[]): PaintOrder => {
18 | const DEFAULT_VALUE = [PAINT_ORDER_LAYER.FILL, PAINT_ORDER_LAYER.STROKE, PAINT_ORDER_LAYER.MARKERS];
19 | const layers: PaintOrder = [];
20 |
21 | tokens.filter(isIdentToken).forEach((token) => {
22 | switch (token.value) {
23 | case 'stroke':
24 | layers.push(PAINT_ORDER_LAYER.STROKE);
25 | break;
26 | case 'fill':
27 | layers.push(PAINT_ORDER_LAYER.FILL);
28 | break;
29 | case 'markers':
30 | layers.push(PAINT_ORDER_LAYER.MARKERS);
31 | break;
32 | }
33 | });
34 | DEFAULT_VALUE.forEach((value) => {
35 | if (layers.indexOf(value) === -1) {
36 | layers.push(value);
37 | }
38 | });
39 |
40 | return layers;
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/position.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {Context} from '../../core/context';
3 | export const enum POSITION {
4 | STATIC = 0,
5 | RELATIVE = 1,
6 | ABSOLUTE = 2,
7 | FIXED = 3,
8 | STICKY = 4
9 | }
10 |
11 | export const position: IPropertyIdentValueDescriptor = {
12 | name: 'position',
13 | initialValue: 'static',
14 | prefix: false,
15 | type: PropertyDescriptorParsingType.IDENT_VALUE,
16 | parse: (_context: Context, position: string) => {
17 | switch (position) {
18 | case 'relative':
19 | return POSITION.RELATIVE;
20 | case 'absolute':
21 | return POSITION.ABSOLUTE;
22 | case 'fixed':
23 | return POSITION.FIXED;
24 | case 'sticky':
25 | return POSITION.STICKY;
26 | }
27 |
28 | return POSITION.STATIC;
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/quotes.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue, isStringToken} from '../syntax/parser';
3 | import {TokenType} from '../syntax/tokenizer';
4 | import {Context} from '../../core/context';
5 |
6 | export interface QUOTE {
7 | open: string;
8 | close: string;
9 | }
10 |
11 | export type Quotes = QUOTE[] | null;
12 |
13 | export const quotes: IPropertyListDescriptor = {
14 | name: 'quotes',
15 | initialValue: 'none',
16 | prefix: true,
17 | type: PropertyDescriptorParsingType.LIST,
18 | parse: (_context: Context, tokens: CSSValue[]) => {
19 | if (tokens.length === 0) {
20 | return null;
21 | }
22 |
23 | const first = tokens[0];
24 |
25 | if (first.type === TokenType.IDENT_TOKEN && first.value === 'none') {
26 | return null;
27 | }
28 |
29 | const quotes = [];
30 | const filtered = tokens.filter(isStringToken);
31 |
32 | if (filtered.length % 2 !== 0) {
33 | return null;
34 | }
35 |
36 | for (let i = 0; i < filtered.length; i += 2) {
37 | const open = filtered[i].value;
38 | const close = filtered[i + 1].value;
39 | quotes.push({open, close});
40 | }
41 |
42 | return quotes;
43 | }
44 | };
45 |
46 | export const getQuote = (quotes: Quotes, depth: number, open: boolean): string => {
47 | if (!quotes) {
48 | return '';
49 | }
50 |
51 | const quote = quotes[Math.min(depth, quotes.length - 1)];
52 | if (!quote) {
53 | return '';
54 | }
55 |
56 | return open ? quote.open : quote.close;
57 | };
58 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/text-align.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {Context} from '../../core/context';
3 | export const enum TEXT_ALIGN {
4 | LEFT = 0,
5 | CENTER = 1,
6 | RIGHT = 2
7 | }
8 |
9 | export const textAlign: IPropertyIdentValueDescriptor = {
10 | name: 'text-align',
11 | initialValue: 'left',
12 | prefix: false,
13 | type: PropertyDescriptorParsingType.IDENT_VALUE,
14 | parse: (_context: Context, textAlign: string) => {
15 | switch (textAlign) {
16 | case 'right':
17 | return TEXT_ALIGN.RIGHT;
18 | case 'center':
19 | case 'justify':
20 | return TEXT_ALIGN.CENTER;
21 | case 'left':
22 | default:
23 | return TEXT_ALIGN.LEFT;
24 | }
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/text-decoration-color.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 |
3 | export const textDecorationColor: IPropertyTypeValueDescriptor = {
4 | name: `text-decoration-color`,
5 | initialValue: 'transparent',
6 | prefix: false,
7 | type: PropertyDescriptorParsingType.TYPE_VALUE,
8 | format: 'color'
9 | };
10 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/text-decoration-line.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue, isIdentToken} from '../syntax/parser';
3 | import {Context} from '../../core/context';
4 |
5 | export const enum TEXT_DECORATION_LINE {
6 | NONE = 0,
7 | UNDERLINE = 1,
8 | OVERLINE = 2,
9 | LINE_THROUGH = 3,
10 | BLINK = 4
11 | }
12 |
13 | export type TextDecorationLine = TEXT_DECORATION_LINE[];
14 |
15 | export const textDecorationLine: IPropertyListDescriptor = {
16 | name: 'text-decoration-line',
17 | initialValue: 'none',
18 | prefix: false,
19 | type: PropertyDescriptorParsingType.LIST,
20 | parse: (_context: Context, tokens: CSSValue[]): TextDecorationLine => {
21 | return tokens
22 | .filter(isIdentToken)
23 | .map((token) => {
24 | switch (token.value) {
25 | case 'underline':
26 | return TEXT_DECORATION_LINE.UNDERLINE;
27 | case 'overline':
28 | return TEXT_DECORATION_LINE.OVERLINE;
29 | case 'line-through':
30 | return TEXT_DECORATION_LINE.LINE_THROUGH;
31 | case 'none':
32 | return TEXT_DECORATION_LINE.BLINK;
33 | }
34 | return TEXT_DECORATION_LINE.NONE;
35 | })
36 | .filter((line) => line !== TEXT_DECORATION_LINE.NONE);
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/text-shadow.ts:
--------------------------------------------------------------------------------
1 | import {PropertyDescriptorParsingType, IPropertyListDescriptor} from '../IPropertyDescriptor';
2 | import {CSSValue, isIdentWithValue, parseFunctionArgs} from '../syntax/parser';
3 | import {ZERO_LENGTH} from '../types/length-percentage';
4 | import {color, Color, COLORS} from '../types/color';
5 | import {isLength, Length} from '../types/length';
6 | import {Context} from '../../core/context';
7 |
8 | export type TextShadow = TextShadowItem[];
9 | interface TextShadowItem {
10 | color: Color;
11 | offsetX: Length;
12 | offsetY: Length;
13 | blur: Length;
14 | }
15 |
16 | export const textShadow: IPropertyListDescriptor = {
17 | name: 'text-shadow',
18 | initialValue: 'none',
19 | type: PropertyDescriptorParsingType.LIST,
20 | prefix: false,
21 | parse: (context: Context, tokens: CSSValue[]): TextShadow => {
22 | if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) {
23 | return [];
24 | }
25 |
26 | return parseFunctionArgs(tokens).map((values: CSSValue[]) => {
27 | const shadow: TextShadowItem = {
28 | color: COLORS.TRANSPARENT,
29 | offsetX: ZERO_LENGTH,
30 | offsetY: ZERO_LENGTH,
31 | blur: ZERO_LENGTH
32 | };
33 | let c = 0;
34 | for (let i = 0; i < values.length; i++) {
35 | const token = values[i];
36 | if (isLength(token)) {
37 | if (c === 0) {
38 | shadow.offsetX = token;
39 | } else if (c === 1) {
40 | shadow.offsetY = token;
41 | } else {
42 | shadow.blur = token;
43 | }
44 | c++;
45 | } else {
46 | shadow.color = color.parse(context, token);
47 | }
48 | }
49 | return shadow;
50 | });
51 | }
52 | };
53 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/text-transform.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {Context} from '../../core/context';
3 | export const enum TEXT_TRANSFORM {
4 | NONE = 0,
5 | LOWERCASE = 1,
6 | UPPERCASE = 2,
7 | CAPITALIZE = 3
8 | }
9 |
10 | export const textTransform: IPropertyIdentValueDescriptor = {
11 | name: 'text-transform',
12 | initialValue: 'none',
13 | prefix: false,
14 | type: PropertyDescriptorParsingType.IDENT_VALUE,
15 | parse: (_context: Context, textTransform: string) => {
16 | switch (textTransform) {
17 | case 'uppercase':
18 | return TEXT_TRANSFORM.UPPERCASE;
19 | case 'lowercase':
20 | return TEXT_TRANSFORM.LOWERCASE;
21 | case 'capitalize':
22 | return TEXT_TRANSFORM.CAPITALIZE;
23 | }
24 |
25 | return TEXT_TRANSFORM.NONE;
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/transform-origin.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue} from '../syntax/parser';
3 | import {isLengthPercentage, LengthPercentage} from '../types/length-percentage';
4 | import {FLAG_INTEGER, TokenType} from '../syntax/tokenizer';
5 | import {Context} from '../../core/context';
6 | export type TransformOrigin = [LengthPercentage, LengthPercentage];
7 |
8 | const DEFAULT_VALUE: LengthPercentage = {
9 | type: TokenType.PERCENTAGE_TOKEN,
10 | number: 50,
11 | flags: FLAG_INTEGER
12 | };
13 | const DEFAULT: TransformOrigin = [DEFAULT_VALUE, DEFAULT_VALUE];
14 |
15 | export const transformOrigin: IPropertyListDescriptor = {
16 | name: 'transform-origin',
17 | initialValue: '50% 50%',
18 | prefix: true,
19 | type: PropertyDescriptorParsingType.LIST,
20 | parse: (_context: Context, tokens: CSSValue[]) => {
21 | const origins: LengthPercentage[] = tokens.filter(isLengthPercentage);
22 |
23 | if (origins.length !== 2) {
24 | return DEFAULT;
25 | }
26 |
27 | return [origins[0], origins[1]];
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/visibility.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {Context} from '../../core/context';
3 | export const enum VISIBILITY {
4 | VISIBLE = 0,
5 | HIDDEN = 1,
6 | COLLAPSE = 2
7 | }
8 |
9 | export const visibility: IPropertyIdentValueDescriptor = {
10 | name: 'visible',
11 | initialValue: 'none',
12 | prefix: false,
13 | type: PropertyDescriptorParsingType.IDENT_VALUE,
14 | parse: (_context: Context, visibility: string) => {
15 | switch (visibility) {
16 | case 'hidden':
17 | return VISIBILITY.HIDDEN;
18 | case 'collapse':
19 | return VISIBILITY.COLLAPSE;
20 | case 'visible':
21 | default:
22 | return VISIBILITY.VISIBLE;
23 | }
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/webkit-text-stroke-color.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyTypeValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | export const webkitTextStrokeColor: IPropertyTypeValueDescriptor = {
3 | name: `-webkit-text-stroke-color`,
4 | initialValue: 'currentcolor',
5 | prefix: false,
6 | type: PropertyDescriptorParsingType.TYPE_VALUE,
7 | format: 'color'
8 | };
9 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/webkit-text-stroke-width.ts:
--------------------------------------------------------------------------------
1 | import {CSSValue, isDimensionToken} from '../syntax/parser';
2 | import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
3 | import {Context} from '../../core/context';
4 | export const webkitTextStrokeWidth: IPropertyValueDescriptor = {
5 | name: `-webkit-text-stroke-width`,
6 | initialValue: '0',
7 | type: PropertyDescriptorParsingType.VALUE,
8 | prefix: false,
9 | parse: (_context: Context, token: CSSValue): number => {
10 | if (isDimensionToken(token)) {
11 | return token.number;
12 | }
13 | return 0;
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/word-break.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyIdentValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {Context} from '../../core/context';
3 | export enum WORD_BREAK {
4 | NORMAL = 'normal',
5 | BREAK_ALL = 'break-all',
6 | KEEP_ALL = 'keep-all'
7 | }
8 |
9 | export const wordBreak: IPropertyIdentValueDescriptor = {
10 | name: 'word-break',
11 | initialValue: 'normal',
12 | prefix: false,
13 | type: PropertyDescriptorParsingType.IDENT_VALUE,
14 | parse: (_context: Context, wordBreak: string): WORD_BREAK => {
15 | switch (wordBreak) {
16 | case 'break-all':
17 | return WORD_BREAK.BREAK_ALL;
18 | case 'keep-all':
19 | return WORD_BREAK.KEEP_ALL;
20 | case 'normal':
21 | default:
22 | return WORD_BREAK.NORMAL;
23 | }
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/src/css/property-descriptors/z-index.ts:
--------------------------------------------------------------------------------
1 | import {IPropertyValueDescriptor, PropertyDescriptorParsingType} from '../IPropertyDescriptor';
2 | import {CSSValue, isNumberToken} from '../syntax/parser';
3 | import {TokenType} from '../syntax/tokenizer';
4 | import {Context} from '../../core/context';
5 |
6 | interface zIndex {
7 | order: number;
8 | auto: boolean;
9 | }
10 |
11 | export const zIndex: IPropertyValueDescriptor = {
12 | name: 'z-index',
13 | initialValue: 'auto',
14 | prefix: false,
15 | type: PropertyDescriptorParsingType.VALUE,
16 | parse: (_context: Context, token: CSSValue): zIndex => {
17 | if (token.type === TokenType.IDENT_TOKEN) {
18 | return {auto: true, order: 0};
19 | }
20 |
21 | if (isNumberToken(token)) {
22 | return {auto: false, order: token.number};
23 | }
24 |
25 | throw new Error(`Invalid z-index number parsed`);
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/src/css/syntax/__tests__/tokernizer-tests.ts:
--------------------------------------------------------------------------------
1 | import {deepEqual} from 'assert';
2 | import {Tokenizer, TokenType} from '../tokenizer';
3 |
4 | const tokenize = (value: string) => {
5 | const tokenizer = new Tokenizer();
6 | tokenizer.write(value);
7 | return tokenizer.read();
8 | };
9 |
10 | describe('tokenizer', () => {
11 | describe('', () => {
12 | it('auto', () => deepEqual(tokenize('auto'), [{type: TokenType.IDENT_TOKEN, value: 'auto'}]));
13 | it('url', () => deepEqual(tokenize('url'), [{type: TokenType.IDENT_TOKEN, value: 'url'}]));
14 | it('auto test', () =>
15 | deepEqual(tokenize('auto test'), [
16 | {type: TokenType.IDENT_TOKEN, value: 'auto'},
17 | {type: TokenType.WHITESPACE_TOKEN},
18 | {type: TokenType.IDENT_TOKEN, value: 'test'}
19 | ]));
20 | });
21 | describe('', () => {
22 | it('url(test.jpg)', () =>
23 | deepEqual(tokenize('url(test.jpg)'), [{type: TokenType.URL_TOKEN, value: 'test.jpg'}]));
24 | it('url("test.jpg")', () =>
25 | deepEqual(tokenize('url("test.jpg")'), [{type: TokenType.URL_TOKEN, value: 'test.jpg'}]));
26 | it("url('test.jpg')", () =>
27 | deepEqual(tokenize("url('test.jpg')"), [{type: TokenType.URL_TOKEN, value: 'test.jpg'}]));
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/src/css/types/functions/-prefix-linear-gradient.ts:
--------------------------------------------------------------------------------
1 | import {CSSValue, parseFunctionArgs} from '../../syntax/parser';
2 | import {CSSImageType, CSSLinearGradientImage, GradientCorner, UnprocessedGradientColorStop} from '../image';
3 | import {TokenType} from '../../syntax/tokenizer';
4 | import {isAngle, angle as angleType, parseNamedSide, deg} from '../angle';
5 | import {parseColorStop} from './gradient';
6 | import {Context} from '../../../core/context';
7 |
8 | export const prefixLinearGradient = (context: Context, tokens: CSSValue[]): CSSLinearGradientImage => {
9 | let angle: number | GradientCorner = deg(180);
10 | const stops: UnprocessedGradientColorStop[] = [];
11 |
12 | parseFunctionArgs(tokens).forEach((arg, i) => {
13 | if (i === 0) {
14 | const firstToken = arg[0];
15 | if (
16 | firstToken.type === TokenType.IDENT_TOKEN &&
17 | ['top', 'left', 'right', 'bottom'].indexOf(firstToken.value) !== -1
18 | ) {
19 | angle = parseNamedSide(arg);
20 | return;
21 | } else if (isAngle(firstToken)) {
22 | angle = (angleType.parse(context, firstToken) + deg(270)) % deg(360);
23 | return;
24 | }
25 | }
26 | const colorStop = parseColorStop(context, arg);
27 | stops.push(colorStop);
28 | });
29 |
30 | return {
31 | angle,
32 | stops,
33 | type: CSSImageType.LINEAR_GRADIENT
34 | };
35 | };
36 |
--------------------------------------------------------------------------------
/src/css/types/functions/linear-gradient.ts:
--------------------------------------------------------------------------------
1 | import {CSSValue, parseFunctionArgs} from '../../syntax/parser';
2 | import {TokenType} from '../../syntax/tokenizer';
3 | import {isAngle, angle as angleType, parseNamedSide, deg} from '../angle';
4 | import {CSSImageType, CSSLinearGradientImage, GradientCorner, UnprocessedGradientColorStop} from '../image';
5 | import {parseColorStop} from './gradient';
6 | import {Context} from '../../../core/context';
7 |
8 | export const linearGradient = (context: Context, tokens: CSSValue[]): CSSLinearGradientImage => {
9 | let angle: number | GradientCorner = deg(180);
10 | const stops: UnprocessedGradientColorStop[] = [];
11 |
12 | parseFunctionArgs(tokens).forEach((arg, i) => {
13 | if (i === 0) {
14 | const firstToken = arg[0];
15 | if (firstToken.type === TokenType.IDENT_TOKEN && firstToken.value === 'to') {
16 | angle = parseNamedSide(arg);
17 | return;
18 | } else if (isAngle(firstToken)) {
19 | angle = angleType.parse(context, firstToken);
20 | return;
21 | }
22 | }
23 | const colorStop = parseColorStop(context, arg);
24 | stops.push(colorStop);
25 | });
26 |
27 | return {angle, stops, type: CSSImageType.LINEAR_GRADIENT};
28 | };
29 |
--------------------------------------------------------------------------------
/src/css/types/index.ts:
--------------------------------------------------------------------------------
1 | export type CSSTypes = 'angle' | 'color' | 'image' | 'length' | 'length-percentage' | 'time';
2 |
--------------------------------------------------------------------------------
/src/css/types/length-percentage.ts:
--------------------------------------------------------------------------------
1 | import {DimensionToken, FLAG_INTEGER, NumberValueToken, TokenType} from '../syntax/tokenizer';
2 | import {CSSValue, isDimensionToken} from '../syntax/parser';
3 | import {isLength} from './length';
4 | export type LengthPercentage = DimensionToken | NumberValueToken;
5 | export type LengthPercentageTuple = [LengthPercentage] | [LengthPercentage, LengthPercentage];
6 |
7 | export const isLengthPercentage = (token: CSSValue): token is LengthPercentage =>
8 | token.type === TokenType.PERCENTAGE_TOKEN || isLength(token);
9 | export const parseLengthPercentageTuple = (tokens: LengthPercentage[]): LengthPercentageTuple =>
10 | tokens.length > 1 ? [tokens[0], tokens[1]] : [tokens[0]];
11 | export const ZERO_LENGTH: NumberValueToken = {
12 | type: TokenType.NUMBER_TOKEN,
13 | number: 0,
14 | flags: FLAG_INTEGER
15 | };
16 |
17 | export const FIFTY_PERCENT: NumberValueToken = {
18 | type: TokenType.PERCENTAGE_TOKEN,
19 | number: 50,
20 | flags: FLAG_INTEGER
21 | };
22 |
23 | export const HUNDRED_PERCENT: NumberValueToken = {
24 | type: TokenType.PERCENTAGE_TOKEN,
25 | number: 100,
26 | flags: FLAG_INTEGER
27 | };
28 |
29 | export const getAbsoluteValueForTuple = (
30 | tuple: LengthPercentageTuple,
31 | width: number,
32 | height: number
33 | ): [number, number] => {
34 | const [x, y] = tuple;
35 | return [getAbsoluteValue(x, width), getAbsoluteValue(typeof y !== 'undefined' ? y : x, height)];
36 | };
37 | export const getAbsoluteValue = (token: LengthPercentage, parent: number): number => {
38 | if (token.type === TokenType.PERCENTAGE_TOKEN) {
39 | return (token.number / 100) * parent;
40 | }
41 |
42 | if (isDimensionToken(token)) {
43 | switch (token.unit) {
44 | case 'rem':
45 | case 'em':
46 | return 16 * token.number; // TODO use correct font-size
47 | case 'px':
48 | default:
49 | return token.number;
50 | }
51 | }
52 |
53 | return token.number;
54 | };
55 |
--------------------------------------------------------------------------------
/src/css/types/length.ts:
--------------------------------------------------------------------------------
1 | import {CSSValue} from '../syntax/parser';
2 | import {DimensionToken, NumberValueToken, TokenType} from '../syntax/tokenizer';
3 |
4 | export type Length = DimensionToken | NumberValueToken;
5 |
6 | export const isLength = (token: CSSValue): token is Length =>
7 | token.type === TokenType.NUMBER_TOKEN || token.type === TokenType.DIMENSION_TOKEN;
8 |
--------------------------------------------------------------------------------
/src/css/types/time.ts:
--------------------------------------------------------------------------------
1 | import {CSSValue} from '../syntax/parser';
2 | import {TokenType} from '../syntax/tokenizer';
3 | import {ITypeDescriptor} from '../ITypeDescriptor';
4 | import {Context} from '../../core/context';
5 |
6 | export const time: ITypeDescriptor = {
7 | name: 'time',
8 | parse: (_context: Context, value: CSSValue): number => {
9 | if (value.type === TokenType.DIMENSION_TOKEN) {
10 | switch (value.unit.toLowerCase()) {
11 | case 's':
12 | return 1000 * value.number;
13 | case 'ms':
14 | return value.number;
15 | }
16 | }
17 |
18 | throw new Error(`Unsupported time type`);
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/src/dom/__mocks__/document-cloner.ts:
--------------------------------------------------------------------------------
1 | export class DocumentCloner {
2 | clonedReferenceElement?: HTMLElement;
3 |
4 | constructor() {
5 | this.clonedReferenceElement = {
6 | ownerDocument: {
7 | defaultView: {
8 | pageXOffset: 12,
9 | pageYOffset: 34
10 | }
11 | }
12 | } as HTMLElement;
13 | }
14 |
15 | toIFrame(): Promise {
16 | return Promise.resolve({} as HTMLIFrameElement);
17 | }
18 |
19 | static destroy(): boolean {
20 | return true;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/dom/element-container.ts:
--------------------------------------------------------------------------------
1 | import {CSSParsedDeclaration} from '../css/index';
2 | import {TextContainer} from './text-container';
3 | import {Bounds, parseBounds} from '../css/layout/bounds';
4 | import {isHTMLElementNode} from './node-parser';
5 | import {Context} from '../core/context';
6 | import {DebuggerType, isDebugging} from '../core/debugger';
7 |
8 | export const enum FLAGS {
9 | CREATES_STACKING_CONTEXT = 1 << 1,
10 | CREATES_REAL_STACKING_CONTEXT = 1 << 2,
11 | IS_LIST_OWNER = 1 << 3,
12 | DEBUG_RENDER = 1 << 4
13 | }
14 |
15 | export class ElementContainer {
16 | readonly styles: CSSParsedDeclaration;
17 | readonly textNodes: TextContainer[] = [];
18 | readonly elements: ElementContainer[] = [];
19 | bounds: Bounds;
20 | flags = 0;
21 |
22 | constructor(protected readonly context: Context, element: Element) {
23 | if (isDebugging(element, DebuggerType.PARSE)) {
24 | debugger;
25 | }
26 |
27 | this.styles = new CSSParsedDeclaration(context, window.getComputedStyle(element, null));
28 |
29 | if (isHTMLElementNode(element)) {
30 | if (this.styles.animationDuration.some((duration) => duration > 0)) {
31 | element.style.animationDuration = '0s';
32 | }
33 |
34 | if (this.styles.transform !== null) {
35 | // getBoundingClientRect takes transforms into account
36 | element.style.transform = 'none';
37 | }
38 | }
39 |
40 | this.bounds = parseBounds(this.context, element);
41 |
42 | if (isDebugging(element, DebuggerType.RENDER)) {
43 | this.flags |= FLAGS.DEBUG_RENDER;
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/dom/elements/li-element-container.ts:
--------------------------------------------------------------------------------
1 | import {ElementContainer} from '../element-container';
2 | import {Context} from '../../core/context';
3 | export class LIElementContainer extends ElementContainer {
4 | readonly value: number;
5 |
6 | constructor(context: Context, element: HTMLLIElement) {
7 | super(context, element);
8 | this.value = element.value;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/dom/elements/ol-element-container.ts:
--------------------------------------------------------------------------------
1 | import {ElementContainer} from '../element-container';
2 | import {Context} from '../../core/context';
3 | export class OLElementContainer extends ElementContainer {
4 | readonly start: number;
5 | readonly reversed: boolean;
6 |
7 | constructor(context: Context, element: HTMLOListElement) {
8 | super(context, element);
9 | this.start = element.start;
10 | this.reversed = typeof element.reversed === 'boolean' && element.reversed === true;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/dom/elements/select-element-container.ts:
--------------------------------------------------------------------------------
1 | import {ElementContainer} from '../element-container';
2 | import {Context} from '../../core/context';
3 | export class SelectElementContainer extends ElementContainer {
4 | readonly value: string;
5 | constructor(context: Context, element: HTMLSelectElement) {
6 | super(context, element);
7 | const option = element.options[element.selectedIndex || 0];
8 | this.value = option ? option.text || '' : '';
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/dom/elements/textarea-element-container.ts:
--------------------------------------------------------------------------------
1 | import {ElementContainer} from '../element-container';
2 | import {Context} from '../../core/context';
3 | export class TextareaElementContainer extends ElementContainer {
4 | readonly value: string;
5 | constructor(context: Context, element: HTMLTextAreaElement) {
6 | super(context, element);
7 | this.value = element.value;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/dom/replaced-elements/canvas-element-container.ts:
--------------------------------------------------------------------------------
1 | import {ElementContainer} from '../element-container';
2 | import {Context} from '../../core/context';
3 |
4 | export class CanvasElementContainer extends ElementContainer {
5 | canvas: HTMLCanvasElement;
6 | intrinsicWidth: number;
7 | intrinsicHeight: number;
8 |
9 | constructor(context: Context, canvas: HTMLCanvasElement) {
10 | super(context, canvas);
11 | this.canvas = canvas;
12 | this.intrinsicWidth = canvas.width;
13 | this.intrinsicHeight = canvas.height;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/dom/replaced-elements/image-element-container.ts:
--------------------------------------------------------------------------------
1 | import {ElementContainer} from '../element-container';
2 | import {Context} from '../../core/context';
3 |
4 | export class ImageElementContainer extends ElementContainer {
5 | src: string;
6 | intrinsicWidth: number;
7 | intrinsicHeight: number;
8 |
9 | constructor(context: Context, img: HTMLImageElement) {
10 | super(context, img);
11 | this.src = img.currentSrc || img.src;
12 | this.intrinsicWidth = img.naturalWidth;
13 | this.intrinsicHeight = img.naturalHeight;
14 | this.context.cache.addImage(this.src);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/dom/replaced-elements/index.ts:
--------------------------------------------------------------------------------
1 | import {CanvasElementContainer} from './canvas-element-container';
2 | import {ImageElementContainer} from './image-element-container';
3 | import {SVGElementContainer} from './svg-element-container';
4 |
5 | export type ReplacedElementContainer = CanvasElementContainer | ImageElementContainer | SVGElementContainer;
6 |
--------------------------------------------------------------------------------
/src/dom/replaced-elements/pseudo-elements.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/niklasvh/html2canvas/6020386bbeed60ad68e675fdcaa6220e292fd35a/src/dom/replaced-elements/pseudo-elements.ts
--------------------------------------------------------------------------------
/src/dom/replaced-elements/svg-element-container.ts:
--------------------------------------------------------------------------------
1 | import {ElementContainer} from '../element-container';
2 | import {parseBounds} from '../../css/layout/bounds';
3 | import {Context} from '../../core/context';
4 |
5 | export class SVGElementContainer extends ElementContainer {
6 | svg: string;
7 | intrinsicWidth: number;
8 | intrinsicHeight: number;
9 |
10 | constructor(context: Context, img: SVGSVGElement) {
11 | super(context, img);
12 | const s = new XMLSerializer();
13 | const bounds = parseBounds(context, img);
14 | img.setAttribute('width', `${bounds.width}px`);
15 | img.setAttribute('height', `${bounds.height}px`);
16 |
17 | this.svg = `data:image/svg+xml,${encodeURIComponent(s.serializeToString(img))}`;
18 | this.intrinsicWidth = img.width.baseVal.value;
19 | this.intrinsicHeight = img.height.baseVal.value;
20 |
21 | this.context.cache.addImage(this.svg);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/dom/text-container.ts:
--------------------------------------------------------------------------------
1 | import {CSSParsedDeclaration} from '../css/index';
2 | import {TEXT_TRANSFORM} from '../css/property-descriptors/text-transform';
3 | import {parseTextBounds, TextBounds} from '../css/layout/text';
4 | import {Context} from '../core/context';
5 |
6 | export class TextContainer {
7 | text: string;
8 | textBounds: TextBounds[];
9 |
10 | constructor(context: Context, node: Text, styles: CSSParsedDeclaration) {
11 | this.text = transform(node.data, styles.textTransform);
12 | this.textBounds = parseTextBounds(context, this.text, styles, node);
13 | }
14 | }
15 |
16 | const transform = (text: string, transform: TEXT_TRANSFORM) => {
17 | switch (transform) {
18 | case TEXT_TRANSFORM.LOWERCASE:
19 | return text.toLowerCase();
20 | case TEXT_TRANSFORM.CAPITALIZE:
21 | return text.replace(CAPITALIZE, capitalize);
22 | case TEXT_TRANSFORM.UPPERCASE:
23 | return text.toUpperCase();
24 | default:
25 | return text;
26 | }
27 | };
28 |
29 | const CAPITALIZE = /(^|\s|:|-|\(|\))([a-z])/g;
30 |
31 | const capitalize = (m: string, p1: string, p2: string) => {
32 | if (m.length > 0) {
33 | return p1 + p2.toUpperCase();
34 | }
35 |
36 | return m;
37 | };
38 |
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
1 | interface CSSStyleDeclaration {
2 | textDecorationColor: string;
3 | textDecorationLine: string;
4 | overflowWrap: string;
5 | }
6 |
7 | interface DocumentType extends Node, ChildNode {
8 | readonly internalSubset: string | null;
9 | }
10 |
11 | interface Document {
12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
13 | fonts: any;
14 | }
15 |
--------------------------------------------------------------------------------
/src/invariant.ts:
--------------------------------------------------------------------------------
1 | export const invariant = (assertion: boolean, error: string): void => {
2 | if (!assertion) {
3 | console.error(error);
4 | }
5 | };
6 |
--------------------------------------------------------------------------------
/src/render/bezier-curve.ts:
--------------------------------------------------------------------------------
1 | import {Vector} from './vector';
2 | import {IPath, PathType, Path} from './path';
3 |
4 | const lerp = (a: Vector, b: Vector, t: number): Vector => {
5 | return new Vector(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
6 | };
7 |
8 | export class BezierCurve implements IPath {
9 | type: PathType;
10 | start: Vector;
11 | startControl: Vector;
12 | endControl: Vector;
13 | end: Vector;
14 |
15 | constructor(start: Vector, startControl: Vector, endControl: Vector, end: Vector) {
16 | this.type = PathType.BEZIER_CURVE;
17 | this.start = start;
18 | this.startControl = startControl;
19 | this.endControl = endControl;
20 | this.end = end;
21 | }
22 |
23 | subdivide(t: number, firstHalf: boolean): BezierCurve {
24 | const ab = lerp(this.start, this.startControl, t);
25 | const bc = lerp(this.startControl, this.endControl, t);
26 | const cd = lerp(this.endControl, this.end, t);
27 | const abbc = lerp(ab, bc, t);
28 | const bccd = lerp(bc, cd, t);
29 | const dest = lerp(abbc, bccd, t);
30 | return firstHalf ? new BezierCurve(this.start, ab, abbc, dest) : new BezierCurve(dest, bccd, cd, this.end);
31 | }
32 |
33 | add(deltaX: number, deltaY: number): BezierCurve {
34 | return new BezierCurve(
35 | this.start.add(deltaX, deltaY),
36 | this.startControl.add(deltaX, deltaY),
37 | this.endControl.add(deltaX, deltaY),
38 | this.end.add(deltaX, deltaY)
39 | );
40 | }
41 |
42 | reverse(): BezierCurve {
43 | return new BezierCurve(this.end, this.endControl, this.startControl, this.start);
44 | }
45 | }
46 |
47 | export const isBezierCurve = (path: Path): path is BezierCurve => path.type === PathType.BEZIER_CURVE;
48 |
--------------------------------------------------------------------------------
/src/render/box-sizing.ts:
--------------------------------------------------------------------------------
1 | import {getAbsoluteValue} from '../css/types/length-percentage';
2 | import {Bounds} from '../css/layout/bounds';
3 | import {ElementContainer} from '../dom/element-container';
4 |
5 | export const paddingBox = (element: ElementContainer): Bounds => {
6 | const bounds = element.bounds;
7 | const styles = element.styles;
8 | return bounds.add(
9 | styles.borderLeftWidth,
10 | styles.borderTopWidth,
11 | -(styles.borderRightWidth + styles.borderLeftWidth),
12 | -(styles.borderTopWidth + styles.borderBottomWidth)
13 | );
14 | };
15 |
16 | export const contentBox = (element: ElementContainer): Bounds => {
17 | const styles = element.styles;
18 | const bounds = element.bounds;
19 |
20 | const paddingLeft = getAbsoluteValue(styles.paddingLeft, bounds.width);
21 | const paddingRight = getAbsoluteValue(styles.paddingRight, bounds.width);
22 | const paddingTop = getAbsoluteValue(styles.paddingTop, bounds.width);
23 | const paddingBottom = getAbsoluteValue(styles.paddingBottom, bounds.width);
24 |
25 | return bounds.add(
26 | paddingLeft + styles.borderLeftWidth,
27 | paddingTop + styles.borderTopWidth,
28 | -(styles.borderRightWidth + styles.borderLeftWidth + paddingLeft + paddingRight),
29 | -(styles.borderTopWidth + styles.borderBottomWidth + paddingTop + paddingBottom)
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/render/effects.ts:
--------------------------------------------------------------------------------
1 | import {Matrix} from '../css/property-descriptors/transform';
2 | import {Path} from './path';
3 |
4 | export const enum EffectType {
5 | TRANSFORM = 0,
6 | CLIP = 1,
7 | OPACITY = 2
8 | }
9 |
10 | export const enum EffectTarget {
11 | BACKGROUND_BORDERS = 1 << 1,
12 | CONTENT = 1 << 2
13 | }
14 |
15 | export interface IElementEffect {
16 | readonly type: EffectType;
17 | readonly target: number;
18 | }
19 |
20 | export class TransformEffect implements IElementEffect {
21 | readonly type: EffectType = EffectType.TRANSFORM;
22 | readonly target: number = EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT;
23 |
24 | constructor(readonly offsetX: number, readonly offsetY: number, readonly matrix: Matrix) {}
25 | }
26 |
27 | export class ClipEffect implements IElementEffect {
28 | readonly type: EffectType = EffectType.CLIP;
29 |
30 | constructor(readonly path: Path[], readonly target: EffectTarget) {}
31 | }
32 |
33 | export class OpacityEffect implements IElementEffect {
34 | readonly type: EffectType = EffectType.OPACITY;
35 | readonly target: number = EffectTarget.BACKGROUND_BORDERS | EffectTarget.CONTENT;
36 |
37 | constructor(readonly opacity: number) {}
38 | }
39 |
40 | export const isTransformEffect = (effect: IElementEffect): effect is TransformEffect =>
41 | effect.type === EffectType.TRANSFORM;
42 | export const isClipEffect = (effect: IElementEffect): effect is ClipEffect => effect.type === EffectType.CLIP;
43 | export const isOpacityEffect = (effect: IElementEffect): effect is OpacityEffect => effect.type === EffectType.OPACITY;
44 |
--------------------------------------------------------------------------------
/src/render/path.ts:
--------------------------------------------------------------------------------
1 | import {BezierCurve} from './bezier-curve';
2 | import {Vector} from './vector';
3 | export const enum PathType {
4 | VECTOR = 0,
5 | BEZIER_CURVE = 1
6 | }
7 |
8 | export interface IPath {
9 | type: PathType;
10 | add(deltaX: number, deltaY: number): IPath;
11 | }
12 |
13 | export const equalPath = (a: Path[], b: Path[]): boolean => {
14 | if (a.length === b.length) {
15 | return a.some((v, i) => v === b[i]);
16 | }
17 |
18 | return false;
19 | };
20 |
21 | export const transformPath = (path: Path[], deltaX: number, deltaY: number, deltaW: number, deltaH: number): Path[] => {
22 | return path.map((point, index) => {
23 | switch (index) {
24 | case 0:
25 | return point.add(deltaX, deltaY);
26 | case 1:
27 | return point.add(deltaX + deltaW, deltaY);
28 | case 2:
29 | return point.add(deltaX + deltaW, deltaY + deltaH);
30 | case 3:
31 | return point.add(deltaX, deltaY + deltaH);
32 | }
33 | return point;
34 | });
35 | };
36 |
37 | export type Path = Vector | BezierCurve;
38 |
--------------------------------------------------------------------------------
/src/render/renderer.ts:
--------------------------------------------------------------------------------
1 | import {Context} from '../core/context';
2 | import {RenderConfigurations} from './canvas/canvas-renderer';
3 |
4 | export class Renderer {
5 | constructor(protected readonly context: Context, protected readonly options: RenderConfigurations) {}
6 | }
7 |
--------------------------------------------------------------------------------
/src/render/vector.ts:
--------------------------------------------------------------------------------
1 | import {IPath, Path, PathType} from './path';
2 |
3 | export class Vector implements IPath {
4 | type: PathType;
5 | x: number;
6 | y: number;
7 |
8 | constructor(x: number, y: number) {
9 | this.type = PathType.VECTOR;
10 | this.x = x;
11 | this.y = y;
12 | }
13 |
14 | add(deltaX: number, deltaY: number): Vector {
15 | return new Vector(this.x + deltaX, this.y + deltaY);
16 | }
17 | }
18 |
19 | export const isVector = (path: Path): path is Vector => path.type === PathType.VECTOR;
20 |
--------------------------------------------------------------------------------
/tests/assets/bg-sliver.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/niklasvh/html2canvas/6020386bbeed60ad68e675fdcaa6220e292fd35a/tests/assets/bg-sliver.png
--------------------------------------------------------------------------------
/tests/assets/cc0-video.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/niklasvh/html2canvas/6020386bbeed60ad68e675fdcaa6220e292fd35a/tests/assets/cc0-video.mp4
--------------------------------------------------------------------------------
/tests/assets/iframe/frame1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | frame 1
6 |
12 |
13 |
14 | this is the content of frame1
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tests/assets/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/niklasvh/html2canvas/6020386bbeed60ad68e675fdcaa6220e292fd35a/tests/assets/image.jpg
--------------------------------------------------------------------------------
/tests/assets/image2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/niklasvh/html2canvas/6020386bbeed60ad68e675fdcaa6220e292fd35a/tests/assets/image2.jpg
--------------------------------------------------------------------------------
/tests/assets/image2_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/niklasvh/html2canvas/6020386bbeed60ad68e675fdcaa6220e292fd35a/tests/assets/image2_1.jpg
--------------------------------------------------------------------------------
/tests/assets/image_1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/niklasvh/html2canvas/6020386bbeed60ad68e675fdcaa6220e292fd35a/tests/assets/image_1.jpg
--------------------------------------------------------------------------------
/tests/karma.ts:
--------------------------------------------------------------------------------
1 | import {screenshotApp, corsApp} from './server';
2 | import {Server} from 'http';
3 | import {config as KarmaConfig, Server as KarmaServer, TestResults} from 'karma';
4 | import * as path from 'path';
5 |
6 | const karmaTestRunner = (): Promise =>
7 | new Promise((resolve, reject) => {
8 | const karmaConfig = KarmaConfig.parseConfig(path.resolve(__dirname, '../karma.conf.js'), {});
9 | const server = new KarmaServer(karmaConfig, (exitCode: number) => {
10 | if (exitCode > 0) {
11 | reject(`Karma has exited with ${exitCode}`);
12 | } else {
13 | resolve();
14 | }
15 | });
16 | server.on('run_complete', (_browsers: any, _results: TestResults) => {
17 | server.stop();
18 | });
19 | server.start();
20 | });
21 | const servers: Server[] = [];
22 |
23 | servers.push(screenshotApp.listen(8000));
24 | servers.push(corsApp.listen(8081));
25 |
26 | karmaTestRunner()
27 | .then(() => {
28 | servers.forEach((server) => server.close());
29 | })
30 | .catch((e) => {
31 | console.error(e);
32 | process.exit(1);
33 | });
34 |
--------------------------------------------------------------------------------
/tests/node/package.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const html2canvas = require('../../');
3 |
4 | describe('Package', () => {
5 | it('should have html2canvas defined', () => {
6 | assert.equal(typeof html2canvas, 'function');
7 | });
8 |
9 | it('should have html2canvas defined', done => {
10 | html2canvas('').catch(err => {
11 | assert.equal(err, 'Provided element is not within a Document');
12 | done();
13 | });
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/tests/rangetest.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Range tests
5 |
6 |
11 |
12 |
13 |
14 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/tests/reftest-diff.ts:
--------------------------------------------------------------------------------
1 | import {sync} from 'glob';
2 | import {resolve, basename} from 'path';
3 | import {existsSync, promises} from 'fs';
4 | import {toMatchImageSnapshot} from 'jest-image-snapshot';
5 |
6 | const resultsDir = resolve(__dirname, '../results');
7 | const customSnapshotsDir = resolve(__dirname, '../tmp/snapshots');
8 | const customDiffDir = resolve(__dirname, '../tmp/snapshot-diffs');
9 |
10 | expect.extend({toMatchImageSnapshot});
11 |
12 | describe('Image diff', () => {
13 | const files: string[] = sync('../tmp/reftests/**/*.png', {
14 | cwd: __dirname,
15 | root: resolve(__dirname, '../../')
16 | }).filter((path) => existsSync(resolve(resultsDir, basename(path))));
17 |
18 | it.each(files.map((path) => basename(path)))('%s', async (filename) => {
19 | const previous = resolve(resultsDir, filename);
20 | const previousSnap = resolve(customSnapshotsDir, `${filename}-snap.png`);
21 | await promises.copyFile(previous, previousSnap);
22 | const updated = resolve(__dirname, '../tmp/reftests/', filename);
23 | const buffer = await promises.readFile(updated);
24 |
25 | // @ts-ignore
26 | expect(buffer).toMatchImageSnapshot({
27 | customSnapshotsDir,
28 | customSnapshotIdentifier: () => filename,
29 | customDiffDir
30 | });
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/tests/reftests/background/multi.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Background attribute tests
5 |
6 |
7 |
39 |
40 |
41 |
42 |
43 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/tests/reftests/background/origin.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/tests/reftests/background/repeat.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Background attribute tests
5 |
6 |
7 |
39 |
40 |
41 |
42 |
43 |
49 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/tests/reftests/border/dashed.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Borders tests
5 |
6 |
7 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/tests/reftests/border/dotted.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Borders tests
5 |
6 |
7 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/tests/reftests/border/double.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Borders tests
5 |
6 |
7 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/tests/reftests/border/inset.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Borders tests
5 |
6 |
7 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/tests/reftests/border/solid.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Borders tests
5 |
6 |
7 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/tests/reftests/clip.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Inline text in the top element
5 |
6 |
7 |
22 |
23 |
24 |
25 | Some inline text
followed by text in span followed by more inline text.
26 |
Then a block level element.
27 | Then more inline text.
28 |
29 | Some inline text
followed by text in span followed by more inline text.
30 |
Then a block level element.
31 | Then more inline text.
32 |
33 | Some inline text
followed by text in span followed by more inline text.
34 |
Then a block level element.
35 | Then more inline text.
36 |
37 | Some inline text
followed by text in span followed by more inline text.
38 |
Then a block level element.
39 | Then more inline text.
40 |
41 |
42 | Some inline text
followed by text in span followed by more inline text.
43 |
Then a block level element.
44 | Then more inline text.
45 |
46 |