├── .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 |
44 |
45 |
46 |
47 |
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 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
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 | 47 | -------------------------------------------------------------------------------- /tests/reftests/crossorigin-iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cross-origin iframe test 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/reftests/dynamicstyle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dynamic style 5 | 6 | 7 | 30 | 36 | 37 | 38 |
Static styles
39 |
Dynamic styles
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /tests/reftests/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | iframe test 5 | 8 | 9 | 10 | 11 |
Parent document content
12 | 13 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/reftests/ignore.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklasvh/html2canvas/6020386bbeed60ad68e675fdcaa6220e292fd35a/tests/reftests/ignore.txt -------------------------------------------------------------------------------- /tests/reftests/images/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | External content tests 5 | 6 | 7 | 12 | 13 | 14 | 15 |

External image

16 | 17 | 18 |

External svg image

19 | 20 | 21 |

External image (using <base> href)

22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/reftests/images/canvas.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Image tests 5 | 6 | 7 | 8 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/reftests/images/cross-origin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | External content tests 5 | 6 | 9 | 10 | 15 | 16 | 17 |

External image (CORS)

18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/reftests/images/doctype.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/reftests/images/empty.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Image tests 5 | 6 | 7 | 12 | 13 | 14 | Image without src attribute, should not crash: 15 | 16 | Image with broken src attribute, should not crash: 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/reftests/images/svg/base64.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Base64 svg 5 | 6 | 7 | 12 | 13 | 14 |
15 | Inline svg image:
16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/reftests/images/svg/external.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Image tests 5 | 6 | 7 | 12 | 13 | 14 | SVG taints image:
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/reftests/images/svg/inline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Inline svg 5 | 6 | 7 | 12 | 13 | 14 |
15 | Inline svg image:
16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/reftests/images/video.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Video tests 6 | 7 | 8 | 9 |

Same origin

10 | 14 |

Cross-origin (doesn't taint)

15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/reftests/list/decimal-leading-zero.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | List tests 5 | 6 | 7 | 8 | 44 | 45 | 46 | 47 | 48 |
    49 |
  1. 1
  2. 50 |
  3. 2
  4. 51 |
  5. 3
  6. 52 |
  7. 4
  8. 53 |
  9. 5
  10. 54 |
  11. 6
  12. 55 |
  13. 7
  14. 56 |
  15. 8
  16. 57 |
  17. 9
  18. 58 |
  19. 10
  20. 59 |
  21. 11
  22. 60 |
  23. 12
  24. 61 |
  25. 13
  26. 62 |
  27. 14
  28. 63 |
  29. 15
  30. 64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /tests/reftests/list/decimal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | List tests 5 | 6 | 7 | 8 | 44 | 45 | 46 | 47 | 48 | 49 |
    50 |
  1. 1
  2. 51 |
  3. 2
  4. 52 |
  5. 3
  6. 53 |
  7. 4
  8. 54 |
  9. 5
  10. 55 |
  11. 6
  12. 56 |
  13. 7
  14. 57 |
  15. 8
  16. 58 |
  17. 9
  18. 59 |
  19. 10
  20. 60 |
  21. 11
  22. 61 |
  23. 12
  24. 62 |
  25. 13
  26. 63 |
  27. 14
  28. 64 |
  29. 15
  30. 65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /tests/reftests/list/lower-alpha.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | List tests 5 | 6 | 7 | 43 | 44 | 45 | 46 | 47 |
    48 |
  1. 1
  2. 49 |
  3. 2
  4. 50 |
  5. 3
  6. 51 |
  7. 4
  8. 52 |
  9. 5
  10. 53 |
  11. 6
  12. 54 |
  13. 7
  14. 55 |
  15. 8
  16. 56 |
  17. 9
  18. 57 |
  19. 10
  20. 58 |
  21. 11
  22. 59 |
  23. 12
  24. 60 |
  25. 13
  26. 61 |
  27. 14
  28. 62 |
  29. 15
  30. 63 |
64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /tests/reftests/list/upper-roman.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | List tests 5 | 6 | 7 | 8 | 44 | 45 | 46 | 47 | 48 | 49 |
    50 |
  1. 1
  2. 51 |
  3. 2
  4. 52 |
  5. 3
  6. 53 |
  7. 4
  8. 54 |
  9. 5
  10. 55 |
  11. 6
  12. 56 |
  13. 7
  14. 57 |
  15. 8
  16. 58 |
  17. 9
  18. 59 |
  19. 10
  20. 60 |
  21. 11
  22. 61 |
  23. 12
  24. 62 |
  25. 13
  26. 63 |
  27. 14
  28. 64 |
  29. 15
  30. 65 |
66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /tests/reftests/options/crop-2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | crop test 5 | 6 | 14 | 15 | 32 | 33 | 34 | 35 | 36 |
37 | great success 38 |
39 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/reftests/options/crop.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | crop test 5 | 6 | 14 | 15 | 29 | 30 | 31 | 32 | 33 |
34 | great success 35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/reftests/options/element.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | element render test 5 | 6 | 7 | 21 | 22 | 23 | 24 | 25 |
26 | great success 27 |
28 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/reftests/options/ignore-2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | element render test 5 | 6 | 11 | 12 | 31 | 32 | 33 | 34 |
35 | great failure 36 |
37 |
38 | ignore predicate 39 |
40 |
41 | great success 42 |
43 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/reftests/options/ignore.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | element render test 5 | 6 | 11 | 12 | 31 | 32 | 33 | 34 |
35 | great failure 36 |
37 |
38 | ignore predicate 39 |
40 |
41 | great success 42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/reftests/options/onclone.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | element render test 5 | 6 | 12 | 13 | 32 | 33 | 34 | 35 |
36 | ignore during onclone 37 |
38 |
39 | great success 40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /tests/reftests/options/scroll-2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | scroll 2 test 5 | 6 | 12 | 13 | 29 | 30 | 31 | 32 | 33 |
34 | great success 35 |
36 | 37 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /tests/reftests/options/scroll.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | scroll test 5 | 6 | 16 | 17 | 42 | 43 | 44 | 45 | 46 |
47 | great success 48 |
49 | 50 |
51 | fixed great success 52 |
53 | 54 | 55 | -------------------------------------------------------------------------------- /tests/reftests/pseudoelements.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pseudoelement tests 5 | 6 | 7 | 48 | 49 | 50 | 51 |
52 | Content 1 53 | Content 2 54 |
55 | 56 |
57 | Content 1 58 | Content 2 59 |
60 | 61 |
62 | Content 1 63 | Content 2 64 |
65 | 66 |
67 | Content 1 68 | Content 2 69 |
70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /tests/reftests/text/child-textnodes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Inline text in the top element 5 | 6 | 7 | 18 | 19 | 20 | 21 | Some inline text followed by text in span followed by more inline text. 22 |

Then a block level element.

23 | Then more inline text. 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/reftests/text/fontawesome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | fontawesome icons 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 | Fontawesome icons 18 | fa-5x 19 | 20 |
    21 |
  • List icons
  • 22 |
  • can be used
  • 23 |
  • as bullets
  • 24 |
  • in lists
  • 25 |
26 | 27 |
28 | 29 | 30 | 31 | 32 | fa-twitter on fa-square-o
33 | 34 | 35 | 36 | 37 | fa-flag on fa-circle
38 | 39 | 40 | 41 | 42 | fa-terminal on fa-square
43 | 44 | 45 | 46 | 47 | fa-ban on fa-camera 48 |
49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /tests/reftests/text/lang/persian.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Persian rtl 5 | 6 | 7 | 8 | 17 | 18 | 19 |
20 |

21 | سلام دنیا! این یک تست است... 22 |

23 |
24 | من می‌توانم. این است قدرت جاوااسکریپت! 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/reftests/text/lang/thai.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Thai text 5 | 6 | 7 | 16 | 17 | 18 | 19 |
20 |

.....

21 |

ทดสอบ แบบกำหนดรูปแบบ ทำ ที นี่ นู่น นั่น นี้ มี หรือ ไม่

22 |

ภาษาไทย เป็นภาษาราชการของประเทศไทย และภาษาแม่ของชาวไทย และชนเชื้อสายอื่นในประเทศไทย ภาษาไทยเป็นภาษาในกลุ่มภาษาไท ซึ่งเป็นกลุ่มย่อยของตระกูลภาษาไท-กะได สันนิษฐานว่า ภาษาในตระกูลนี้มีถิ่นกำเนิดจากทางตอนใต้ของประเทศจีน และนักภาษาศาสตร์บางส่วนเสนอว่า ภาษาไทยน่าจะมีความเชื่อมโยงกับตระกูลภาษาออสโตร-เอเชียติก ตระกูลภาษาออสโตรนีเซียน และตระกูลภาษาจีน-ทิเบต

23 |

ภาษาไทยเป็นภาษาที่มีระดับเสียงของคำแน่นอนหรือวรรณยุกต์เช่นเดียวกับภาษาจีน และออกเสียงแยกคำต่อคำ ทำให้เป็นที่ลำบากของชาวต่างชาติเนื่องจากการออกเสียงวรรณยุกต์ที่เป็นเอกลักษณ์ของแต่ละคำ และการสะกดคำที่ซับซ้อน

24 |

คำว่า ไทย หมายความว่า อิสรภาพ เสรีภาพ หรืออีกความหมายหนึ่งคือ ใหญ่ ยิ่งใหญ่ เพราะการจะเป็นอิสระได้จะต้องมีกำลังที่มากกว่า แข็งแกร่งกว่า เพื่อป้องกันการรุกรานจากข้าศึก คำนี้เป็นคำไทยแท้ที่เกิดจากการสร้างคำที่เรียก "การลากคำเข้าวัด" ซึ่งเป็นการลากความวิธีหนึ่ง ตามหลักคติชนวิทยา คนไทยเป็นชนชาติที่นับถือกันว่า ภาษาบาลี ซึ่งเป็นภาษาที่บันทึกพระธรรมคำสอนของพระพุทธเจ้าเป็นภาษาอันศักดิ์สิทธิ์และเป็นมงคล เมื่อคนไทยต้องการตั้งชื่อประเทศว่า ไท ซึ่งเป็นคำไทยแท้ จึงเติมตัว ย เข้าไปข้างท้าย เพื่อให้มีลักษณะคล้ายคำในภาษาบาลี - สันสกฤตเพื่อความเป็นมงคลตามความเชื่อของตน ภาษาไทยจึงหมายถึงภาษาของชนชาติไทยผู้เป็นไทนั่นเอง

25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/reftests/text/line-break.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | word-break 5 | 6 | 7 | 24 | 25 | 26 | 27 | 28 |

29 | サンプルぁルぁルぁルぁルぁルぁルぁぁぁぁ文ンプル–文々サンプル文 30 |

31 | 32 |

33 | サンプルぁルぁルぁルぁルぁルぁルぁぁぁぁ文文文文文‐–〜゠サンプル文々サンプル文 34 |

35 | 36 | 37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/reftests/text/linethrough.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Text-decoration:line-through tests 5 | 6 | 7 | 18 | 19 | 44 | 45 | 46 | 47 | Creating content through JavaScript 48 | 49 | 50 | -------------------------------------------------------------------------------- /tests/reftests/text/multiple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Text-decoration:underline tests 5 | 6 | 7 | 19 | 20 | 45 | 46 | 47 | 48 | Creating content through JavaScript 49 | 50 | 51 | -------------------------------------------------------------------------------- /tests/reftests/text/overflow-wrap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | word-break 5 | 6 | 7 | 35 | 36 | 37 | 38 |
39 |

1. overflow-wrap: normal

40 |

FStrPrivFinÄndG (Gesetz zur Änderung des 41 | Fernstraßenbauprivatfinanzierungsgesetzes 42 | und straßenverkehrsrechtlicher Vorschriften)

43 |

2. overflow-wrap: break-word

44 |

FStrPrivFinÄndG (Gesetz zur Änderung des 45 | Fernstraßenbauprivatfinanzierungsgesetzes 46 | und straßenverkehrsrechtlicher Vorschriften)

47 |

3. word-wrap: normal

48 |

FStrPrivFinÄndG (Gesetz zur Änderung des 49 | Fernstraßenbauprivatfinanzierungsgesetzes 50 | und straßenverkehrsrechtlicher Vorschriften)

51 |

4. word-wrap: break-word

52 |

FStrPrivFinÄndG (Gesetz zur Änderung des 53 | Fernstraßenbauprivatfinanzierungsgesetzes 54 | und straßenverkehrsrechtlicher Vorschriften)

55 |
56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /tests/reftests/text/underline-lineheight.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Text-decoration:underline tests 5 | 6 | 7 | 19 | 20 | 43 | 44 | 45 | 46 | Creating content through JavaScript 47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/reftests/text/underline.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Text-decoration:underline tests 5 | 6 | 7 | 26 | 27 | 52 | 53 | 54 | 55 | test       label u 56 |
57 | Creating content through JavaScript 58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /tests/reftests/text/word-break.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | word-break 5 | 6 | 7 | 29 | 30 | 31 | 32 |
33 |

1. word-break: normal

34 |

This is a long and 35 | Supercalifragilisticexpialidocious sentence. 36 | 次の単語グレートブリテンおよび北アイルランド連合王国で本当に大きな言葉

37 | 38 |

2. word-break: break-all

39 |

This is a long and 40 | Supercalifragilisticexpialidocious sentence. 41 | 次の単語グレートブリテンおよび北アイルランド連合王国で本当に大きな言葉

42 | 43 |

3. word-break: keep-all

44 |

This is a long and 45 | Supercalifragilisticexpialidocious sentence. 46 | 次の単語グレートブリテンおよび北アイルランド連合王国で本当に大きな言葉

47 |
48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /tests/reftests/transform/nested.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Nested transform tests 5 | 6 | 7 | 43 | 44 | 45 |
First level content
with second level content
and third level content
, ending second
, ending first
46 |
something else
47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/reftests/transform/translate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Nested transform tests 5 | 6 | 7 | 44 | 45 | 46 |
First level content
with second level content
and third level content
, ending second
, ending first
47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/reftests/visibility.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Visible elements tests 5 | 6 | 7 | 19 | 20 | 21 |
22 |

Display:none and visible:hidden tests

23 |
This should be visible
24 |
display:none, This should be hidden
25 |
visibility:hidden, This should be hidden
26 |
display:none, This should be hidden
27 |
visibility:hidden, This should be hidden
28 |
29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/reftests/webcomponents/autonomous-custom-element.js: -------------------------------------------------------------------------------- 1 | class AutonomousCustomElement extends HTMLElement { 2 | constructor() { 3 | super(); 4 | 5 | const shadow = this.attachShadow({mode: 'open'}); 6 | const wrapper = document.createElement('span'); 7 | wrapper.setAttribute('class', 'wrapper'); 8 | 9 | const info = document.createElement('span'); 10 | info.setAttribute('class', 'info'); 11 | 12 | info.textContent = this.getAttribute('text'); 13 | 14 | const img = document.createElement('img'); 15 | img.src = this.getAttribute('img'); 16 | 17 | // Create some CSS to apply to the shadow dom 18 | const style = document.createElement('style'); 19 | 20 | style.textContent = ` 21 | .wrapper { 22 | position: relative; 23 | } 24 | .info { 25 | font-size: 0.8rem; 26 | width: 200px; 27 | display: inline-block; 28 | border: 1px solid black; 29 | padding: 10px; 30 | background: white; 31 | border-radius: 10px; 32 | } 33 | img { 34 | width: 100px; 35 | } 36 | `; 37 | 38 | shadow.appendChild(style); 39 | shadow.appendChild(wrapper); 40 | wrapper.appendChild(img); 41 | wrapper.appendChild(info); 42 | } 43 | 44 | connectedCallback() { 45 | this.shadowRoot.adoptedStyleSheets = [sheet]; 46 | } 47 | } 48 | 49 | customElements.define('autonomous-custom-element', AutonomousCustomElement); 50 | -------------------------------------------------------------------------------- /tests/reftests/webcomponents/slot-element.js: -------------------------------------------------------------------------------- 1 | customElements.define('summary-display', 2 | class extends HTMLElement { 3 | constructor() { 4 | super(); 5 | 6 | const template = document.getElementById('summary-display-template'); 7 | const templateContent = template.content; 8 | 9 | const shadowRoot = this.attachShadow({mode: 'open'}); 10 | shadowRoot.appendChild(templateContent.cloneNode(true)); 11 | 12 | const items = Array.from(this.querySelectorAll('li')); 13 | const descriptions = Array.from(this.querySelectorAll('p')); 14 | 15 | items.forEach(item => { 16 | handleClick(item); 17 | }); 18 | 19 | descriptions.forEach(description => { 20 | updateDisplay(description, items[1]); 21 | }); 22 | 23 | function handleClick(item) { 24 | item.addEventListener('click', function() { 25 | items.forEach(item => { 26 | item.style.backgroundColor = 'white'; 27 | }); 28 | 29 | descriptions.forEach(description => { 30 | updateDisplay(description, item); 31 | }); 32 | }); 33 | } 34 | 35 | function updateDisplay(description, item) { 36 | description.removeAttribute('slot'); 37 | 38 | if(description.getAttribute('data-name') === item.textContent) { 39 | description.setAttribute('slot', 'choice'); 40 | item.style.backgroundColor = '#bad0e4'; 41 | } 42 | } 43 | 44 | const slots = this.shadowRoot.querySelectorAll('slot'); 45 | slots[1].addEventListener('slotchange', function(e) { 46 | const nodes = slots[1].assignedNodes(); 47 | console.log(`Element in Slot "${slots[1].name}" changed to "${nodes[0].outerHTML}".`); 48 | }); 49 | } 50 | } 51 | ); 52 | -------------------------------------------------------------------------------- /tests/reftests/zindex/z-index11.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | static position inside position relative 7 | 8 | 9 | 32 | 33 | 34 | 35 |
36 |

Div Element #1

37 | position: relative; 38 |
39 |

Div Element #2

40 | position: static; 41 |
42 |
43 |

Div Element #3

44 | float: left; 45 |
46 |
47 |
48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /tests/reftests/zindex/z-index12.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Negative z-indexes 7 | 8 | 9 | 35 | 36 | 37 |
38 |
39 |

Div Element #2

40 |
41 |
42 |

Div Element #3

43 |
44 |
45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /tests/reftests/zindex/z-index13.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | text above children with negative z-index; z-index tests #13 5 | 6 | 7 | 28 | 29 | 30 |
outer 31 |
32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/reftests/zindex/z-index14.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | body text above children with negative index but body bgcolor below 5 | 6 | 7 | 22 | 23 | body 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/reftests/zindex/z-index15.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | body text and bgcolor above children with negative z-index 5 | 6 | 7 | 25 | 26 | body 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/reftests/zindex/z-index16.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Z-order positioning 5 | 17 | 18 | 19 | 20 |

21 | A butterfly image 24 | 25 |

27 | This text will overlay the butterfly image. 28 |
29 | 30 |
31 | This text will be beneath everything. 32 |
33 | 34 |
36 | This text will underlay text1, but overlay the butterfly image 37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/reftests/zindex/z-index17.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | z-index17 6 | 23 | 24 | 25 | 26 |
fixed z-index 10
27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/reftests/zindex/z-index19.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | z-index19 6 | 59 | 60 | 61 | 62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 71 | 72 | -------------------------------------------------------------------------------- /tests/reftests/zindex/z-index20.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | z-index20 6 | 33 | 34 | 35 | 36 |
37 |
38 |
39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /tests/reftests/zindex/z-index4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | z-index tests #4 5 | 6 | 7 | 29 | 30 | 31 |
32 | LEVEL #1 33 |
34 |
35 |
36 | LEVEL #1 37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/reftests/zindex/z-index5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | z-index tests #5 5 | 6 | 7 | 27 | 28 | 29 |
30 | LEVEL #1 31 |
32 |
33 |
34 | LEVEL #1 35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/reftests/zindex/z-index6.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | z-index tests #6 5 | 6 | 7 | 30 | 31 | 32 |
z-index:0
33 |
default z-index
34 |
z-index:1
35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/results/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /tests/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import nodeResolve 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 | import {resolve} from 'path'; 7 | 8 | const pkg = require('../package.json'); 9 | 10 | const banner = `/* 11 | * ${pkg.title} ${pkg.version} <${pkg.homepage}> 12 | * Copyright (c) ${new Date().getFullYear()} ${pkg.author.name} <${pkg.author.url}> 13 | * Released under ${pkg.license} License 14 | */`; 15 | 16 | export default { 17 | input: `tests/testrunner.ts`, 18 | output: [ 19 | { 20 | file: resolve(__dirname, '../build/testrunner.js'), 21 | name: 'testrunner', 22 | format: 'iife', 23 | banner, 24 | sourcemap: true 25 | } 26 | ], 27 | external: [], 28 | watch: { 29 | include: 'tests/**' 30 | }, 31 | plugins: [ 32 | // Allow node_modules resolution, so you can use 'external' to control 33 | // which external modules to include in the bundle 34 | // https://github.com/rollup/rollup-plugin-node-resolve#usage 35 | nodeResolve(), 36 | // Allow json resolution 37 | json(), 38 | // Compile TypeScript files 39 | typescript({ 40 | tsconfig: resolve(__dirname, 'tsconfig.json') 41 | }), 42 | // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) 43 | commonjs({ 44 | include: 'node_modules/**' 45 | }), 46 | 47 | // Resolve source maps to the original source 48 | sourceMaps() 49 | ] 50 | }; 51 | -------------------------------------------------------------------------------- /tests/sauceconnect.js: -------------------------------------------------------------------------------- 1 | const sauceConnectLauncher = require('sauce-connect-launcher'); 2 | 3 | sauceConnectLauncher( 4 | { 5 | username: process.env.SAUCE_USERNAME, 6 | accessKey: process.env.SAUCE_ACCESS_KEY, 7 | logger: console.log, 8 | // Log output from the `sc` process to stdout? 9 | verbose: true, 10 | 11 | // Enable verbose debugging (optional) 12 | verboseDebugging: true 13 | }, 14 | err => { 15 | if (err) { 16 | console.error(err.message); 17 | return; 18 | } 19 | console.log('Sauce Connect ready'); 20 | } 21 | ); 22 | -------------------------------------------------------------------------------- /tests/testrunner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test runner 7 | 8 | 9 |
10 | 11 | 14 | 15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["node", "mocha"], 5 | "rootDir": "../", 6 | "declaration": false 7 | }, 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tests/types.ts: -------------------------------------------------------------------------------- 1 | export interface PlatformDetails { 2 | name: string; 3 | version: string; 4 | } 5 | 6 | export interface ScreenshotRequest { 7 | screenshot: string; 8 | test: string; 9 | platform: PlatformDetails; 10 | devicePixelRatio: number; 11 | windowWidth: number; 12 | windowHeight: number; 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "noImplicitThis": true, 5 | "noUnusedLocals": true, 6 | "noUnusedParameters": true, 7 | "strictNullChecks": true, 8 | "strictPropertyInitialization": true, 9 | "types": ["node", "jest"], 10 | "target": "es5", 11 | "lib": ["es2015", "dom"], 12 | "sourceMap": true, 13 | "outDir": "dist/lib", 14 | "declaration": true, 15 | "declarationDir": "dist/types", 16 | "resolveJsonModule": true 17 | }, 18 | "include": [ 19 | "src" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | # Project dependencies 2 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 3 | node_modules 4 | .cache/ 5 | # Build directory 6 | public/ 7 | .DS_Store 8 | yarn-error.log 9 | src/results.json 10 | static/tests/preview.js 11 | src/preview.js 12 | .docusaurus 13 | build/ 14 | -------------------------------------------------------------------------------- /www/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 gatsbyjs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /www/README.md: -------------------------------------------------------------------------------- 1 | # gatsby-starter-default 2 | The default Gatsby starter 3 | 4 | For an overview of the project structure please refer to the [Gatsby documentation - Building with Components](https://www.gatsbyjs.org/docs/building-with-components/) 5 | 6 | Install this starter (assuming Gatsby is installed) by running from your CLI: 7 | ``` 8 | gatsby new gatsby-example-site 9 | ``` 10 | 11 | ## Deploy 12 | 13 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/gatsbyjs/gatsby-starter-default) 14 | -------------------------------------------------------------------------------- /www/gatsby-browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Browser APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/browser-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /www/gatsby-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Node APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/node-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | const {createFilePath} = require(`gatsby-source-filesystem`); 9 | const path = require('path'); 10 | 11 | exports.onCreateNode = ({node, getNode, boundActionCreators}) => { 12 | const {createNodeField} = boundActionCreators; 13 | if (node.internal.type === `MarkdownRemark`) { 14 | const slug = createFilePath({node, getNode}); 15 | createNodeField({ 16 | node, 17 | name: `slug`, 18 | value: slug 19 | }); 20 | } 21 | }; 22 | 23 | exports.createPages = ({graphql, boundActionCreators}) => { 24 | const {createPage} = boundActionCreators; 25 | return new Promise((resolve, reject) => { 26 | graphql(` 27 | { 28 | allMarkdownRemark { 29 | edges { 30 | node { 31 | fields { 32 | slug 33 | } 34 | } 35 | } 36 | } 37 | } 38 | `).then(result => { 39 | result.data.allMarkdownRemark.edges.map(({node}) => { 40 | createPage({ 41 | path: node.fields.slug, 42 | component: path.resolve(__dirname, `./src/templates/docs.js`), 43 | context: { 44 | // Data passed to context is available in page queries as GraphQL variables. 45 | slug: node.fields.slug 46 | } 47 | }); 48 | }); 49 | resolve(); 50 | }); 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /www/gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's SSR (Server Side Rendering) APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/ssr-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html2canvas-website", 3 | "description": "https://html2canvas.hertzen.com", 4 | "version": "1.0.0", 5 | "private": true, 6 | "author": "Niklas von Hertzen", 7 | "dependencies": { 8 | "cpy-cli": "^2.0.0", 9 | "gatsby": "^2.7.1", 10 | "gatsby-link": "^2.0.16", 11 | "gatsby-plugin-catch-links": "^2.0.13", 12 | "gatsby-plugin-glamor": "^2.0.9", 13 | "gatsby-plugin-google-analytics": "^2.0.18", 14 | "gatsby-plugin-react-helmet": "^3.0.12", 15 | "gatsby-plugin-twitter": "^2.0.13", 16 | "gatsby-plugin-typography": "^2.2.10", 17 | "gatsby-remark-prismjs": "^3.2.7", 18 | "gatsby-source-filesystem": "^2.0.28", 19 | "gatsby-transformer-remark": "^2.3.8", 20 | "glamor": "^2.20.40", 21 | "gzip-size": "^5.0.0", 22 | "html2canvas": "file:../", 23 | "mkdirp": "^0.5.1", 24 | "prismjs": "^1.16.0", 25 | "react": "^16.8.6", 26 | "react-dom": "^16.8.6", 27 | "react-helmet": "^5.2.0", 28 | "react-typography": "^0.16.19", 29 | "typography": "^0.16.19", 30 | "typography-theme-github": "^0.16.19" 31 | }, 32 | "license": "MIT", 33 | "main": "n/a", 34 | "scripts": { 35 | "copy:build": "mkdirp public/dist && cpy ../dist/*.js public/dist", 36 | "copy:src": "mkdirp public/src && cpy ../src/**/*.ts public/src --parents", 37 | "copy": "npm run copy:build && npm run copy:src", 38 | "build": "npm run copy && gatsby build", 39 | "start": "gatsby develop", 40 | "test": "echo \"Error: no test specified\" && exit 1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /www/src/components/carbon.css: -------------------------------------------------------------------------------- 1 | 2 | #carbonads { 3 | display: block; 4 | overflow: hidden; 5 | text-align: center; 6 | font-size: 14px; 7 | line-height: 1.5; 8 | } 9 | #carbonads span { 10 | display: block; 11 | overflow: hidden; 12 | } 13 | .carbon-img { 14 | display: block; 15 | } 16 | .carbon-text { 17 | display: block; 18 | margin-bottom: .5em; 19 | } 20 | .carbon-poweredby { 21 | display: block; 22 | color: rgba(255, 255, 255, 0.6); 23 | font-size: 9px; 24 | } 25 | 26 | #carbonads a { 27 | color: #fff; 28 | } 29 | 30 | 31 | -------------------------------------------------------------------------------- /www/src/components/carbon.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import './carbon.css'; 3 | 4 | export default class Carbon extends Component { 5 | componentDidMount() { 6 | if (this.container) { 7 | const script = document.createElement('script'); 8 | 9 | script.src = 10 | '//cdn.carbonads.com/carbon.js?zoneid=1673&serve=C6AILKT&placement=html2canvashertzencom'; 11 | script.async = true; 12 | script.id = '_carbonads_js'; 13 | this.container.appendChild(script); 14 | } 15 | } 16 | 17 | render() { 18 | return ( 19 |
{ 22 | this.container = container; 23 | }} 24 | /> 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /www/src/components/example.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklasvh/html2canvas/6020386bbeed60ad68e675fdcaa6220e292fd35a/www/src/components/example.css -------------------------------------------------------------------------------- /www/src/components/footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => 4 | ; 32 | -------------------------------------------------------------------------------- /www/src/components/layout.css: -------------------------------------------------------------------------------- 1 | p { 2 | color: rgba(0,0,0,0.71); 3 | padding: 0; 4 | -webkit-font-smoothing: antialiased; 5 | } 6 | 7 | h1 { 8 | font-weight: 300; 9 | font-size: 4.2rem; 10 | padding: 0; 11 | } 12 | 13 | h4 { 14 | font-size: 2.28rem; 15 | line-height: 110%; 16 | margin: 1.14rem 0 .912rem 0; 17 | } 18 | 19 | h5 { 20 | font-size: 1.28rem; 21 | line-height: 110%; 22 | margin: 1.14rem 0 .912rem 0; 23 | } 24 | 25 | h6 { 26 | line-height: 110%; 27 | margin: 1.14rem 0 .912rem 0; 28 | } 29 | 30 | a { 31 | color: #7cb342; 32 | text-decoration: none; 33 | } 34 | 35 | table { 36 | border: 1px solid #ddd; 37 | } 38 | 39 | table tr:nth-child(odd) { 40 | background-color: #f9f9f9; 41 | } 42 | 43 | th { 44 | padding: 8px; 45 | border: 1px solid #ddd; 46 | border-bottom-width: 2px; 47 | } 48 | 49 | td { 50 | padding: 8px; 51 | } 52 | 53 | th:first-child, td:first-child { 54 | padding-left: 8px; 55 | } 56 | 57 | :not(pre) > code { 58 | padding: .1em .25em; 59 | border: solid 1px rgba(51,51,51,0.12); 60 | background: #f5f2f0; 61 | border-radius: .3em; 62 | color: black; 63 | font-family: 'Inconsolata', Monaco, Consolas, 'Andale Mono', monospace; 64 | direction: ltr; 65 | text-align: left; 66 | white-space: pre; 67 | word-spacing: normal; 68 | word-break: normal; 69 | line-height: 1.4; 70 | 71 | } 72 | 73 | blockquote { 74 | white-space: pre-wrap; 75 | padding: 0 20px; 76 | margin: 20px 0; 77 | border: 1px solid #eee; 78 | border-left-width: 5px; 79 | border-radius: 3px; 80 | border-left-color: #558b2f; 81 | 82 | } 83 | 84 | blockquote p { 85 | padding: 0; 86 | 87 | } 88 | 89 | * { 90 | box-sizing: border-box; 91 | } 92 | -------------------------------------------------------------------------------- /www/src/components/layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import './layout.css'; 4 | import Example from '../components/example'; 5 | 6 | require('prismjs/themes/prism-solarizedlight.css'); 7 | const TemplateWrapper = ({children}) => 8 |
9 | 10 | {children} 11 | 12 |
; 13 | 14 | export default TemplateWrapper; 15 | -------------------------------------------------------------------------------- /www/src/images/ic_arrow_back_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /www/src/images/ic_arrow_forward_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /www/src/images/ic_camera_alt_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /www/src/images/ic_close_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /www/src/images/ic_menu_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /www/src/images/logo_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /www/src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from '../components/layout'; 3 | 4 | const NotFoundPage = () => 5 | 6 |

NOT FOUND

7 |

You just hit a route that doesn't exist... the sadness.

8 |
; 9 | 10 | export default NotFoundPage; 11 | -------------------------------------------------------------------------------- /www/src/utils/typography.js: -------------------------------------------------------------------------------- 1 | import Typography from 'typography'; 2 | 3 | const theme = { 4 | googleFonts: [ 5 | { 6 | name: 'Roboto', 7 | styles: ['100', '300', '400', '500', '700'] 8 | } 9 | ], 10 | scale: 4.2, 11 | baseFontSize: '14.5px', 12 | baseLineHeight: 1.5, 13 | headerFontFamily: ['Roboto', 'sans-serif'], 14 | bodyFontFamily: ['Roboto', 'sans-serif'] 15 | }; 16 | 17 | const typography = new Typography(theme); 18 | 19 | export default typography; 20 | -------------------------------------------------------------------------------- /www/static/CNAME: -------------------------------------------------------------------------------- 1 | html2canvas.hertzen.com 2 | -------------------------------------------------------------------------------- /www/static/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | html2canvas - Test result preview 6 | 7 | 8 | 9 |
10 | 11 | 12 | Test link 13 |
14 |
15 | Preview image 16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /www/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | target: 'web', 6 | entry: path.resolve(__dirname, './src/preview.ts'), 7 | output: { 8 | path: path.resolve(__dirname, './static/tests'), 9 | filename: 'preview.js' 10 | }, 11 | resolve: { 12 | extensions: [".tsx", ".ts", ".js", ".json"] 13 | }, 14 | module: { 15 | rules: [ 16 | // all files with a '.ts' or '.tsx' extension will be handled by 'ts-loader' 17 | { test: /\.tsx?$/, use: ["ts-loader"], exclude: /node_modules/ } 18 | ] 19 | } 20 | }; 21 | --------------------------------------------------------------------------------