[
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"env\": {\n    \"browser\": true,\n    \"es2021\": true,\n    \"node\": true\n  },\n  \"extends\": [\n    \"eslint:recommended\",\n    \"plugin:react/recommended\",\n    \"plugin:react/jsx-runtime\",\n    \"plugin:@typescript-eslint/recommended\"\n  ],\n  \"overrides\": [\n    {\n      \"files\": [\"src/**/*.js\", \"src/**/*.ts\"],\n      \"rules\": {\n        \"prefer-const\": \"off\"\n      }\n    },\n    {\n      \"files\": [\"test/**/*.ts`\", \"test/**/*.tsx\"],\n      \"rules\": {\n        \"react/jsx-key\": \"off\"\n      }\n    }\n  ],\n  \"parser\": \"@typescript-eslint/parser\",\n  \"plugins\": [\"react\", \"react-hooks\", \"@typescript-eslint\"],\n  \"rules\": {\n    \"no-inner-declarations\": 0,\n    \"no-useless-escape\": 1,\n    \"@typescript-eslint/ban-ts-comment\": 1,\n    \"@typescript-eslint/no-extra-semi\": 0,\n    \"@typescript-eslint/no-shadow\": 2,\n    \"@typescript-eslint/ban-types\": 0,\n    \"@typescript-eslint/no-namespace\": 0,\n    \"react-hooks/rules-of-hooks\": 2,\n    \"react-hooks/exhaustive-deps\": 1,\n    \"react/prop-types\": 0\n  },\n  \"ignorePatterns\": [\"dist/\", \"node_modules\", \"vendor\"]\n}\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "* @shuding\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a bug report for Satori\n---\n\n# Bug report\n\n## Description / Observed Behavior\n\nWhat kind of issues did you encounter with Satori?\n\n## Expected Behavior\n\nHow did you expect Satori to behave here?\n\n## Reproduction\n\nCreate a shareable reproduction link for the issue using https://og-playground.vercel.app.\n\n## Additional Context\n\nSatori version, and any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Question & Ideas\n    url: https://github.com/vercel/satori/discussions\n    about: Ask questions and share your thoughts with other community members\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Request a new feature for Satori\n---\n\n# Feature Request\n\n## Description\n\nWhat do you want to add to Satori, and why?\n\n## Additional Context\n\nYou can add a shareable link using https://og-playground.vercel.app, or any other context that helps explaining this feature request here.\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: ['main']\n  pull_request:\n    branches: ['main']\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}\n\njobs:\n  integrity:\n    # prevents this action from running on forks\n    if: github.repository_owner == 'vercel'\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node: [ 20 ]\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v3\n    - name: Use pnpm\n      run: corepack enable pnpm && pnpm --version\n    - name: Use Node.js ${{ matrix.node }}\n      uses: actions/setup-node@v3\n      with:\n        node-version: ${{ matrix.node }}\n        cache: 'pnpm'\n    - run: pnpm install\n    - run: pnpm ci-check\n\n  test:\n    # prevents this action from running on forks\n    if: github.repository_owner == 'vercel'\n    name: Node.js ${{ matrix.node }} on ${{ matrix.os }}\n    timeout-minutes: 5\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest]\n        node: [18, 20]\n    runs-on: ${{ matrix.os }}\n    permissions:\n      contents: write # to be able to publish a GitHub release\n      issues: write # to be able to comment on released issues\n      pull-requests: write # to be able to comment on released pull requests\n      id-token: write # to enable use of OIDC for npm provenance\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n      - name: Use pnpm\n        run: corepack enable pnpm && pnpm --version\n      - name: Use Node.js ${{ matrix.node }}\n        uses: actions/setup-node@v3\n        with:\n          node-version: ${{ matrix.node }}\n          cache: 'pnpm'\n      - run: pnpm install\n      - run: pnpm build\n      - run: pnpm test\n      - name: Maybe Release\n        if: matrix.os == 'ubuntu-latest' && matrix.node == 20 && github.event_name == 'push' && github.ref == 'refs/heads/main'\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          NPM_TOKEN: ${{ secrets.NPM_TOKEN_ELEVATED }}\n          NPM_CONFIG_PROVENANCE: 'true'\n        run: pnpm dlx semantic-release@24.2.3\n"
  },
  {
    "path": ".github/workflows/pr.yml",
    "content": "name: PR\non:\n  pull_request:\n    types: [opened, edited, synchronize]\n  pull_request_target:\n    types: [opened, edited, synchronize]\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: amannn/action-semantic-pull-request@0b14f54ac155d88e12522156e52cb6e397745cfd\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n.DS_Store\n.vercel\n.vscode\n.next\n.idea\n.turbo\ndist\n.pnpm-debug.log\n__diff_output__\n.eslintcache\ncoverage\n\nplayground/public/yoga.wasm\nplayground/tsconfig.tsbuildinfo\n\n# Vendor files\n# yoga.wasm\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\npnpm lint-staged\n"
  },
  {
    "path": ".npmrc",
    "content": "shell-emulator=true\nprovenance=true\n"
  },
  {
    "path": ".prettierignore",
    "content": ".github/\nnode_modules\n**/.next/**\n**/_next/**\n**/dist/**\nsrc/vendor/\npnpm-lock.yaml\n*.md\ncoverage/"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"tabWidth\": 2,\n  \"useTabs\": false,\n  \"singleQuote\": true,\n  \"jsxSingleQuote\": true,\n  \"semi\": false\n}\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Satori Contribution Guidelines\n\nThank you for reading this guide and we appreciate any contribution.\n\n## Ask a Question\n\nYou can use the repository's [Discussions](https://github.com/vercel/satori/discussions) page to ask any questions, post feedback, or share your experience on how you use this library.\n\n## Report a Bug\n\nWhenever you find something which is not working properly, please first search the repository's [Issues](https://github.com/vercel/satori/issues) page and make sure it's not reported by someone else already.\n\nIf not, feel free to open an issue with a detailed description of the problem and the expected behavior. A bug reproduction using [Satori’s playground](https://og-playground.vercel.app) will be extremely helpful.\n\n## Request for a New Feature\n\nFor new features, it would be great to have some discussions from the community before starting working on it. You can either create an issue (if there isn't one) or post a thread on the [Discussions](https://github.com/vercel/satori/discussions) page to describe the feature that you want to have.\n\nIf possible, you can add another additional context like how this feature can be implemented technically, what other alternative solutions we can have, etc.\n\n## Local Development\n\nThis project uses [pnpm](https://pnpm.io). To install dependencies, run:\n\n```bash\npnpm install\n```\n\nTo start the playground together with Satori locally, run:\n\n```bash\npnpm dev:playground\n```\n\nAnd visit localhost:3000.\n\nTo only start the development mode of Satori, run `pnpm dev` in the root directory (recommended to test together with the playground to see changes in live).\n\n## Adding Tests\n\nSatori uses [Vitest](https://vitest.dev) to test and generate snapshots. To start and live-watch the tests, run:\n\n```bash\npnpm dev:test\n```\n\nIt will update snapshot images as well.\n\nYou can also use `pnpm test` to only run the test.\n"
  },
  {
    "path": "LICENSE",
    "content": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in\n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "README.md",
    "content": "![Satori](.github/card.png)\n\n**Satori**: Enlightened library to convert HTML and CSS to SVG.\n\n> **Note**\n>\n> To use Satori in your project to generate PNG images like Open Graph images and social cards, check out our [announcement](https://vercel.com/blog/introducing-vercel-og-image-generation-fast-dynamic-social-card-images) and [Vercel’s Open Graph Image Generation →](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation)\n>\n> To use it in Next.js, take a look at the [Next.js Open Graph Image Generation template →](https://vercel.com/templates/next.js/og-image-generation)\n\n## Overview\n\nSatori supports the JSX syntax, which makes it very straightforward to use. Here’s an overview of the basic usage:\n\n```jsx\n// api.jsx\nimport satori from 'satori'\n\nconst svg = await satori(\n  <div style={{ color: 'black' }}>hello, world</div>,\n  {\n    width: 600,\n    height: 400,\n    fonts: [\n      {\n        name: 'Roboto',\n        // Use `fs` (Node.js only) or `fetch` to read the font as Buffer/ArrayBuffer and provide `data` here.\n        data: robotoArrayBuffer,\n        weight: 400,\n        style: 'normal',\n      },\n    ],\n  },\n)\n```\n\nSatori will render the element into a 600×400 SVG, and return the SVG string:\n\n```js\n'<svg ...><path d=\"...\" fill=\"black\"></path></svg>'\n```\n\nUnder the hood, it handles layout calculation, font, typography and more, to generate a SVG that matches the exact same HTML and CSS in a browser.\n\n<br/>\n\n## Documentation\n\n### JSX\n\nSatori only accepts JSX elements that are pure and stateless. You can use a subset of HTML\nelements (see section below), or custom React components, but React APIs such as `useState`, `useEffect`, `dangerouslySetInnerHTML` are not supported.\n\n#### Experimental: builtin JSX support\n\nSatori has an experimental JSX runtime that you can use without having to install React. You can enable it on a per-file basis with [`@jsxImportSource` pragmas](https://www.typescriptlang.org/tsconfig/#jsxImportSource). In the future, it will autocomplete only the subset of HTML elements and CSS properties that Satori supports for better type-safety.\n\n```tsx\n/** @jsxRuntime automatic */\n/** @jsxImportSource satori/jsx */\n\nimport satori from 'satori';\nimport { FC, JSXNode } from 'satori/jsx';\n\nconst MyComponent: FC<{ children: JSXNode }> = ({ children }) => (\n  <div style={{ color: 'black' }}>{children}</div>\n)\n\nconst svg = await satori(\n  <MyComponent>hello, world</MyComponent>,\n  options,\n)\n```\n\n#### Use without JSX\n\nIf you don't have JSX transpiler enabled, you can simply pass [React-elements-like objects](https://reactjs.org/docs/introducing-jsx.html) that have `type`, `props.children` and `props.style` (and other properties too) directly:\n\n```js\nawait satori(\n  {\n    type: 'div',\n    props: {\n      children: 'hello, world',\n      style: { color: 'black' },\n    },\n  },\n  options\n)\n```\n\n### HTML Elements\n\nSatori supports a limited subset of HTML and CSS features, due to its special use cases. In general, only these static and visible elements and properties that are implemented.\n\nFor example, the `<input>` HTML element, the `cursor` CSS property are not in consideration. And you can't use `<style>` tags or external resources via `<link>` or `<script>`.\n\nAlso, Satori does not guarantee that the SVG will 100% match the browser-rendered HTML output since Satori implements its own layout engine based on the [SVG 1.1 spec](https://www.w3.org/TR/SVG11).\n\nYou can find the list of supported HTML elements and their preset styles [here](https://github.com/vercel/satori/blob/main/src/handler/presets.ts).\n\n#### Images\n\nYou can use `<img>` to embed images. However, `width`, and `height` attributes are recommended to set:\n\n```jsx\nawait satori(\n  <img src=\"https://picsum.photos/200/300\" width={200} height={300} />,\n  options\n)\n```\n\nWhen using `background-image`, the image will be stretched to fit the element by default if you don't specify the size.\n\nIf you want to render the generated SVG to another image format such as PNG, it would be better to use base64 encoded image data (or buffer) directly as `props.src` so no extra I/O is needed in Satori:\n\n```jsx\nawait satori(\n  <img src=\"data:image/png;base64,...\" width={200} height={300} />,\n  // Or src={arrayBuffer}, src={buffer}\n  options\n)\n```\n\n### CSS\n\nSatori uses the same Flexbox [layout engine](https://yogalayout.com) as React Native, and it’s **not** a complete CSS implementation. However, it supports a subset of the spec that covers most common CSS features:\n\n<table>\n<thead>\n<tr>\n  <th>Property</th>\n  <th>Property Expanded</th>\n  <th>Supported Values</th>\n  <th>Example</th>\n</tr>\n</thead>\n<tbody>\n\n<tr>\n<td colspan=\"2\"><b>CSS Variables</b></td>\n<td>Supported, including <code>--var-name</code> declaration and <code>var(--var-name)</code> usage with fallback values</td>\n<td><a href=\"https://og-playground.vercel.app/?share=rVLRTsIwFP2V5hIzTbY4wBjTIC9oos-a8MJLt95tha4lXQfOZf9uOxwRlTeeentO7zntuW0h1RyBwoyL3UoRUtlG4mPb-pqQIIpsgSVGqZbaBJQEnJlNImsMwsOJAkVeWEeM4_hqAPeC2-IXxkW1laxxaCbxY0B9_SQMplZo5TjnU5dqYJkUuXq1WFaeQmXRDNS6rqzImoV2oPL-p3TC0k1udK34wt_c8aMsy46urutNfCIl08kPaPn9lvs47tGuW6m5L3w4x2RIn4VT3DFzfZLPTeBa5i8opQ7JUhvJZ7eu8x-Jv7lqw1TuUr2E-lmJaBKSUTaNx_H4vNqwQgh668dSAW2hHynQBxcNHGYO9M5vOCZ1DjRjssIQsNRr8d5s_Zey-37ndHy4z2WCHKg1NXYhWJa4E4W333tz6L4A\">Example</a></td>\n</tr>\n\n<tr>\n<td colspan=\"2\"><code>display</code></td>\n<td><code>flex</code>, <code>contents</code>, <code>none</code>, default to <code>flex</code></td>\n<td></td>\n</tr>\n\n<tr>\n<td colspan=\"2\"><code>position</code></td>\n<td><code>relative</code>, <code>static</code> and <code>absolute</code>, default to <code>relative</code></td>\n<td></td>\n</tr>\n\n<tr>\n<td colspan=\"2\"><code>color</code></td>\n<td>Supported</td>\n<td></td>\n</tr>\n\n<tr><td rowspan=\"5\"><code>margin</code></td></tr>\n<tr><td><code>marginTop</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>marginRight</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>marginBottom</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>marginLeft</code></td><td>Supported</td><td></td></tr>\n\n<tr><td rowspan=\"5\">Position</td></tr>\n<tr><td><code>top</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>right</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>bottom</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>left</code></td><td>Supported</td><td></td></tr>\n\n<tr><td rowspan=\"3\">Size</td></tr>\n<tr><td><code>width</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>height</code></td><td>Supported</td><td></td></tr>\n\n<tr><td rowspan=\"5\">Min & max size</td></tr>\n<tr><td><code>minWidth</code></td><td>Supported except for <code>min-content</code>, <code>max-content</code> and <code>fit-content</code></td><td></td></tr>\n<tr><td><code>minHeight</code></td><td>Supported except for <code>min-content</code>, <code>max-content</code> and <code>fit-content</code></td><td></td></tr>\n<tr><td><code>maxWidth</code></td><td>Supported except for <code>min-content</code>, <code>max-content</code> and <code>fit-content</code></td><td></td></tr>\n<tr><td><code>maxHeight</code></td><td>Supported except for <code>min-content</code>, <code>max-content</code> and <code>fit-content</code></td><td></td></tr>\n\n<tr><td rowspan=\"5\"><code>border</code></td></tr>\n<tr><td>Width (<code>borderWidth</code>, <code>borderTopWidth</code>, ...)</td><td>Supported</td><td></td></tr>\n<tr><td>Style (<code>borderStyle</code>, <code>borderTopStyle</code>, ...)</td><td><code>solid</code> and <code>dashed</code>, default to <code>solid</code></td><td></td></tr>\n<tr><td>Color (<code>borderColor</code>, <code>borderTopColor</code>, ...)</td><td>Supported</td><td></td></tr>\n<tr><td>\n  Shorthand (<code>border</code>, <code>borderTop</code>, ...)</td><td>Supported, i.e. <code>1px solid gray</code><br/>\n</td><td></td></tr>\n\n<tr><td rowspan=\"6\"><code>borderRadius</code></td></tr>\n<tr><td><code>borderTopLeftRadius</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>borderTopRightRadius</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>borderBottomLeftRadius</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>borderBottomRightRadius</code></td><td>Supported</td><td></td></tr>\n<tr><td>Shorthand</td><td>Supported, i.e. <code>5px</code>, <code>50% / 5px</code></td><td></td></tr>\n\n<tr><td rowspan=\"11\">Flex</td></tr>\n<tr><td><code>flexDirection</code></td><td><code>column</code>, <code>row</code>, <code>row-reverse</code>, <code>column-reverse</code>, default to <code>row</code></td><td></td></tr>\n<tr><td><code>flexWrap</code></td><td><code>wrap</code>, <code>nowrap</code>, <code>wrap-reverse</code>, default to <code>wrap</code></td><td></td></tr>\n<tr><td><code>flexGrow</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>flexShrink</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>flexBasis</code></td><td>Supported except for <code>auto</code></td><td></td></tr>\n<tr><td><code>alignItems</code></td><td><code>stretch</code>, <code>center</code>, <code>flex-start</code>, <code>flex-end</code>, <code>baseline</code>, <code>normal</code>, default to <code>stretch</code></td><td></td></tr>\n<tr><td><code>alignContent</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>alignSelf</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>justifyContent</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>gap</code></td><td>Supported</td><td></td></tr>\n\n<tr><td rowspan=\"5\">Font</td></tr>\n<tr><td><code>fontFamily</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>fontSize</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>fontWeight</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>fontStyle</code></td><td>Supported</td><td></td></tr>\n\n<tr><td rowspan=\"13\">Text</td></tr>\n<tr><td><code>tabSize</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>textAlign</code></td><td><code>start</code>, <code>end</code>, <code>left</code>, <code>right</code>, <code>center</code>, <code>justify</code>, default to <code>start</code></td><td></td></tr>\n<tr><td><code>textIndent</code></td><td>Supported, including negative values (hanging indent)</td><td></td></tr>\n<tr><td><code>textTransform</code></td><td><code>none</code>, <code>lowercase</code>, <code>uppercase</code>, <code>capitalize</code>, defaults to <code>none</code></td><td></td></tr>\n<tr><td><code>textOverflow</code></td><td><code>clip</code>, <code>ellipsis</code>, defaults to <code>clip</code></td><td></td></tr>\n<tr><td><code>textDecoration</code></td><td>Support line types <code>underline</code> and <code>line-through</code>, and styles <code>dotted</code>, <code>dashed</code>, <code>double</code>, <code>solid</code></td><td><a href=\"https://og-playground.vercel.app/?share=pVPLTsMwEPwVaytUkAKkPCRklV4oXwDHXhx7YxtcO3Ic2hLl37GTtEKIQynywTvjndGstG6BO4FAYS70x8oSUoedwce2TTUhCrVUgZLpLM_PptlAbrQI6gcndF0ZtotsaXC7Z1O91B550M7GN-5Ms7b714oJoa2kZJaPTMH4u_SuseLJGeejYlKW5cHN2fCiP5GS25uRkqxK8gS6bmUXqUiTHMYgAbdhidx5NmawzuI0di9SMb-OzceoYiT0Ro_SAzpan5ovg4qzSdVbfCf-noIIFwIK4lH0bgM8KQ0RrFbRqjDNMNyAT8rUFAbJhHP-_1CDl5cFO8-z_lzdX_ySb39DBq5KTjXQFvoVBfqQ5xkMOwz0LgGBRSOBlszUmAGu3Zt-3VXpA4RNj6JP2rPndYECaPANdhkEVsQOhca4jfNGQPcF\">Example</a></td></tr>\n<tr><td><code>textShadow</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>lineHeight</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>letterSpacing</code></td><td>Supported</td><td></td></tr>\n<tr><td><code>whiteSpace</code></td><td><code>normal</code>, <code>pre</code>, <code>pre-wrap</code>, <code>pre-line</code>, <code>nowrap</code>, defaults to <code>normal</code></td><td></td></tr>\n<tr><td><code>wordBreak</code></td><td><code>normal</code>, <code>break-all</code>, <code>break-word</code>, <code>keep-all</code>, defaults to <code>normal</code></td><td></td></tr>\n<tr><td><code>textWrap</code></td><td><code>wrap</code>, <code>balance</code>, defaults to <code>wrap</code></td><td></td></tr>\n\n<tr><td rowspan=\"7\">Background</td></tr>\n<tr><td><code>backgroundColor</code></td><td>Supported, single value</td><td></td></tr>\n<tr><td><code>backgroundImage</code></td><td><code>linear-gradient</code>, <code>repeating-linear-gradient</code>, <code>radial-gradient</code>, <code>repeating-radial-gradient</code>, <code>url</code>, single value</td><td></td></tr>\n<tr><td><code>backgroundPosition</code></td><td>Support single value</td><td></td></tr>\n<tr><td><code>backgroundSize</code></td><td>Support <code>cover</code>, <code>contain</code>, <code>auto</code>, and two-value sizes i.e. <code>10px 20%</code></td><td><a href=\"https://og-playground.vercel.app/?share=ZZXXjqNIFIZfpeWb2RW9Itik3tmRwAQDJgeDNTfkHEwwYdTvvnhGWq003HDOqZ8fDlX11Y9D2Ebx4ePwNcqf35u3t2Fcq_ifHz9e8dtbFudpNn68wRD0_qsy59GY_b8Q-GGZ9u3UREbcxf4u_tK0f_U_4y-_acx8i3dF2Dajnze_jwu1n74EU1_9Efmj_5G_CmDXpH8H_hBjp_fcoVVjhiQ-ban9Ukw7Y-10j3j9ldtnyttvND6LubMHTABVrO4YJypeXGSdBtuwwCRWANMRpy7W49jmUFvuyU0fnmZeduAiZ6ZZaueUXM0V5VDBNs2OtfX6MimW0iTTBD0JeYbFeSOrpLGR5_iMkrHGYSAGEFJl77xRCsjNDC_MmIHV8KDOdsboszJAXchEZifrokEtSyrT5cUkbkifODIuyKC2oblXy8yAqyt-VLcJr8UhYJc2PGoWrW4dppebvMnexThypKZD-IRPx2dFjjnj3eAxqMU1XLTipNKFhrv-1Te3ZWE41aKHtHUogVtC98ZlniyqI9lkcOS5xQnRSlTV0-tKaHckalSGADnQlda0i12YNG7wcO3J-IrGTiWLXtviZ6tc1LDpTrmyhe5uybE1CSYwMHinsikaLkdSepmcxdufvxhlwGcYZFBUF9yqXt9fIT5MC0L3H99JUi4UOqz4PPHkFsq72zM8WjYnYGqRWukUlGh4gcFkBi7zyiOuA0c-D4-VW-zWoSnBLSFHV2qkeLMWcwGd7jWpWVCi5AgElPbN7TB1vJpQbagUW4USzTgmUwZs5ckWiKtbeoovGVYKVZYplg37_NjrsGiWVdUBSYO2HADFotULPEJ1kl-3_QMDRpkEjjNQJkq5rpYMXk6gMN14z9Nh9lIhURHsoqlSm5QUTb2sFTs1ag4JjyqDx8eRoJY24GdbW3E7mrfC2jYpulOIfsX9q8znWkYJIU1Lpl3RHbd1jvHq2hVJ27w8Hu0DQtHereC9qSlsH2pBzdY8HbMclOVBaxkCNQPbCS280YnTjDj1vRVdXk2GTcHUtcLIC2kuZmc-F0CjT5O0bxLguUZX0-46gocAOemyO9ud16EQRYoEzoUHKbUT2c6Z6iVo8ikGz_c28GlfMMZFXId-3ndkk55Y6pEpLu00ERw5DiE5ibPw6Jg0pxiXMCDPY5Xv0ooyy07SOY2ItUJA0GcAc95NKKS17ERr91XBFBjo7kTiSi-nM-MjY9_WyXH_6BRO6OxOPK9k9bA925tNd9HmcmRzKI6YXmAFWWNmoOixTTtukLU1GRFeZIuaqTM24QNH063N0Ubq86kAJkdSfl7B89Dt0_KkVXVtMRYgb4Wsogggys-O-89XYZGtuDtasBrKY6ZmsIg42314VQNix3wBqQC70a0ODgxY3BbqKvgnxsscyioJTTAUoJHSrXUiwWwtLqtAakcVCILGBrn4C1p0-WKUzlCXhIAKTFUJTXzV9zXLWaU56fX5_OfvsDy3VdvvsJyzfIy_vEY_P783376CO8u_fW8O74e2G_O2GQ4fPw4_wX34IHZwH35h_fBxeiVRHEzp4SPxqyF-P8R1W-TW2r3OhHH-me0-yc5rtg7i6PAx9lP8-X4Y_WBXZHFVtXPbV9Hh818\">Example</a></td></tr>\n<tr><td><code>backgroundClip</code></td><td><code>border-box</code>, <code>text</code></td><td></td></tr>\n<tr><td><code>backgroundRepeat</code></td><td><code>repeat</code>, <code>repeat-x</code>, <code>repeat-y</code>, <code>no-repeat</code>, defaults to <code>repeat</code></td><td></td></tr>\n\n<tr><td rowspan=\"5\"><code>transform</code></td></tr>\n<tr><td>Translate (<code>translate</code>, <code>translateX</code>, <code>translateY</code>)</td><td>Supported</td><td></td></tr>\n<tr><td>Rotate</td><td>Supported</td><td></td></tr>\n<tr><td>Scale (<code>scale</code>, <code>scaleX</code>, <code>scaleY</code>)</td><td>Supported</td><td></td></tr>\n<tr><td>Skew (<code>skew</code>, <code>skewX</code>, <code>skewY</code>)</td><td>Supported</td><td></td></tr>\n\n<tr>\n<td colspan=\"2\"><code>transformOrigin</code></td>\n<td>Support one-value and two-value syntax (both relative and absolute values)</td>\n<td></td>\n</tr>\n\n<tr>\n<td colspan=\"2\"><code>objectFit</code></td>\n<td>Supported</td>\n<td><a href=\"https://og-playground.vercel.app/?share=7VVNj5swEP0ro6mqJFJaslJVVVbYQ6X2F_TIBewBvHVsZMwmEeK_75BAAvsl7WUPq-WC5j0P896zNLQonSIUuFxBfAttYgHyxsqgnYXf7rBsQZbaKE8WutWZB_AUGm9hq_Q91OFoKG4HBmCvVSgF3Gw26xEqSRdlmGNK15VJjwIWuaHD4oJnqfxfeNdYxdSXPM8nlPOKPMM31QFqZ7RiIWpxprvudjzXjoq7M7KNWOeJZaB_DfKXA83s2PrYzMs6L0Z_TEzNrI5gN8i46NtyrpeCS70rrhVr8DJOsAyhqkUU8XBJpTPqu3TRz83mwPOiyhYJTntOWuKW-WHYVE3ccs8Mf2pzYmjB2r9OjU59PUu67I5k-Kt7PtfGDFcyt98_0TWDaBrCh05Eunvyn5HMI7Eh1fZdQ-FMXonkhUTeK5Bapoa-Kbd_aybX3bZKbAeQWFyjq_r1XaNo8aQOxS9eUnhWg6LfWKgoawoUeWpqWiPt3J3-d6z6P0HYnyr-Ts7X9GeXkUIRfEPdGkOa8YmSjHF7543C7gE\">Example</a></td>\n</tr>\n\n<tr>\n<td colspan=\"2\"><code>objectPosition</code></td>\n<td>Supports keywords (<code>top</code>, <code>bottom</code>, <code>left</code>, <code>right</code>, <code>center</code>), percentages (e.g., <code>25% 75%</code>), lengths (e.g., <code>10px 20px</code>), and mixed values (e.g., <code>left 20%</code>). Defaults to <code>center</code> (<code>50% 50%</code>).</td>\n<td><a href=\"https://og-playground.vercel.app/?share=7VTBitswEP2VQaUkgbTOQilFxHsotOceevTFlsa2torGyHKTYPzvHSV24rS7Xfayp_XFzHsj6b0nMb1QpFFIsVxBeg995gDKzqlgyMFXOix7ULWx2qODYXXmATyGzjvYavMb2nC0mPYjA7A3OtQS7jab9QTVaKo63GLatI3NjxIWpcXD4oIXufpVeeqcZupdWZYzirxGz_Bdc4CWrNEsRC_O9DDcT339pHg4I9uEdZ5YBuJvlL8caWanpX-beVrnxeinmakbqxM4jDIu-rac66Xg0uyqa8UavEozUYfQtDJJ-HCFNVn9UVHyebM58HlJ46pMzNectKQ98-NhczVpz2tu8H9tzgwtWPv7udG5r0dJKh5Qhe8m8opcyI17vOUHtSa-rNiHLqAfL-82qPgl17SSeVxv2XFfQSHQ7lWz4-j-k9wTwb1Wbq3KLX7QtH8-ukAN-LjtC9O7zpBV5gaAzIm1oCbu2grZi5MPIb_wMBBn3ULGySA0Fl0lZJnbFtcCd_Rgfh6bOHHD_lTxPiXf-7ddgVrI4Dsc1iLkBXfUaC3tyVsthj8\">Example</a></td>\n</tr>\n\n<tr>\n<td colspan=\"2\"><code>opacity</code></td>\n<td>Supported</td>\n<td></td>\n</tr>\n\n<tr>\n<td colspan=\"2\"><code>boxSizing</code></td>\n<td>Supported</td>\n<td></td>\n</tr>\n\n<tr>\n<td colspan=\"2\"><code>boxShadow</code></td>\n<td>Supported</td>\n<td></td>\n</tr>\n\n<tr>\n<td colspan=\"2\"><code>overflow</code></td>\n<td><code>visible</code> and <code>hidden</code>, default to <code>visible</code></td>\n<td></td>\n</tr>\n\n<tr>\n<td colspan=\"2\"><code>filter</code></td>\n<td>Supported</td>\n<td></td>\n</tr>\n\n<tr>\n<td colspan=\"2\"><code>clipPath</code></td>\n<td>Supported</td>\n<td><a href=\"https://og-playground.vercel.app/?share=XVJNb9wgEP0rI6poW8lJnX6pstpe0h7aQ1UlrXLJBZvBZosZBDgbZ7X_PQMbZze5wPCGmXmPx1Z0pFA04osytzcOIKbZ4tftNscAA5p-SA2szuv6ZFXtwY1RaXiBKRO9lTOj2uLdgub4uwnYJUOOcx3ZaXRLVlrTu58Jx5hT6BKGJbWeYjJ6viAGXZ7_PN3K7n8faHLqgiwFzr_SWj9N5aorc48NvH93BF0_avlU1wXd7W7ctxws0l-KP8j_8FhypP4Y8lIp4_oGzg_YgSKzY6FDau2EC0WAzhr_R5Z39GTnntzrj_UJ1BU34Z3jKi_lVEGd4zerfXEmDlCoA_yLqKCdIdKIQBrSgLChYNUqgpWhx5igo9FLZzBW8Bvv0tk6AjrZWoww0wSJoAsoE4KerD2NianDNbYgvbemk9m8mGdwLbqstEyxXMHNL1F2CTTXTyFPkE6BYbP6wIV81dMGAzeGS_b0tJWZ7y95K6-6YHzi4WTzNU2hdNUylrbtZKyKZ8Wft2wQy112UQnyhZRotqL4IZrP7IfY-yWabI5Q2E69aLS0ESuBI63N39nnv5425cR98r_4MbaoRJPChLtKJNnyjQGtpfKMYvcA\">Example</a></td>\n</tr>\n\n<tr>\n<td colspan=\"2\"><code>lineClamp</code></td>\n<td>Supported</td>\n<td><a href=\"https://og-playground.vercel.app/?share=5VPBbtQwEP2VkRFakNKSshxQBBwoXDhwaEFc9uLYk6xbx2PZk-6G1Up8DR_GlzDOkgr13FtPGb_xvPf8ojkoQxZVo95Zd7cJAJknj-8Ph1IDbNH1W25gdVHXz1fVCdw5y9sHmHU5ej0J2nncL2ipP7mEhh0F6Rny4xCWbtTWutA3cFH_Q1ptbvtEY7CX5CnJxLOu6-7ZKPC1-4kNrF_P0PG4CR9KsZh_aP9_X60nc7tQAXgX8NLrIQrbPTjo1LvwkZhpkJF1HferU69IAcxiAN8zWmgnyDQgUAe8RdhR8naVwQsFZgZDQ9TBYa7gK-75_CYDBt16zDDRCExgEmpG6EbvzzLLy-EHtqBj9M7oElguGjKLocQ0q3iZEPIr1Iahk_kxFQUdLLjA2CcZlKuRdpiEGK7GzGetLn6_6Dt9bZKLLOIkz-8l0DSzdjrPtO3ovM3nc6KvJNJHyHa1ho368-s3vDBihQb5fVayEa-BX27UE093-apKUZxNqeag5v1Szdu6rtRpAVXzphwstmOvmk77jJXCgW7ctymW7eXdfBKesiSfhxatajiNeKwU61ZubNF7mmNUx78\">Example</a></td>\n</tr>\n\n<tr><td rowspan=\"5\">Mask</td></tr>\n<tr><td><code>maskImage</code></td><td><code>linear-gradient(...)</code>, <code>radial-gradient(...)</code>, <code>url(...)</code></td><td><a href=\"https://og-playground.vercel.app/?share=pZJfb9MwFMW_imVp2ZDS5s_I1kULSMAkhgRoYlJf-uLYN8ltHTvYDm2o-t2xuxXBXvcQXed3rONj37unXAugJb0V-GulCLFuklDt92FNSAfYdq4k51manp3HT3CLwnUvmEA7SDZ52kjYnWhYf0ID3KFWXuNajr06qUxiq-4d9DZIoByYk7QercNm-qg9VOH8_-XG8x_4G0pymf-Dls9pr9L0mdaMb1qjRyW8x2jkRefcYMskwZ61YOejCrFtN-e6T4ZOOz3LinyRL65v3ubZdTZrari8KkQmbhh_jzuJdWXqWTbP51n0s1oUUdNX66GNuNFD5TP6MkXbKsvTNOK2sqatI9yhqGD60vHPHxq2fMDv67v022NbNA9vTjfqmd3ch0w-p2ECmZy1oXrLC46GSyDMkSI9C19MajlCTJxhPj8zftNfoyXUG3RfX21HkuTYBf-whhhowGMOBBXpXC_DWYfDSr1bqdvET46vNKZ6CH22tNzT44zQMrxDTJ-miJahL1RAPba0bJi0EFPo9RofpyGMoNse_7xRaOZdX4OgpTMjHGLqWO13dCCl3mojBT38AQ\">Example</a></td></tr>\n<tr><td><code>maskPosition</code></td><td>Supported</td><td><a href=\"https://og-playground.vercel.app/?share=pVJda9swFP0rQlC3Ayf-6NKmpt5gW2Ed7KOskJe8yNK1fRNZ8iR5iRfy3yelCayFPfXBvtK5h3uP7j07yrUAWtBbgb-XihDrRgnlbhfOhLSATesKcp6l6dl5_ARuULj2BSbQ9pKNHq0lbE9oOH9CA9yhVj7HtRw6dcoyiY26d9DZkALlwJxSq8E6rMeP2oMq9H-erj3-E_9AQS7zf6DFUe1Vmh7RivF1Y_SghK8xGHnROtfbIkmwYw3Y6aCCbNtOue6SvtVOT7JZPs_n1zdv8-w6m9QVXF7NRCZuGH-PW4lVaapJNs2nWfSrnM-iuitXfRNxo_vSa_RhjDZllqdpxG1pTVNFuEVRwvil5Z8_1GzxgN9Xd-m3x2ZWP7w5vahjdn0fNHmdhglkctKE6EtecDRcAmGOzNKz8MWkkgPExBnm9TPjSc8K_dAWjxP3O-q35PA_MRZQrdF9fX1DkiSHRfnZG2KgBo9zIKhI6zr5stl_RAXafr9U75bqNvEe9JHGVPeBammxowe30SJMNKZPfqRF2DAVUA0NLWomLcQUOr3Cx7EPZnabw80XCra46yoQtHBmgH1MHas8owUp9UYbKej-Lw\">Example</a></td></tr>\n<tr><td><code>maskSize</code></td><td>Support two-value size i.e. <code>10px 20%</code></td><td><a href=\"https://og-playground.vercel.app/?share=pVLfb9MwEP5XLEvLhpQ2P0a3LlpAAiYxJEATk_rSF8e-JNc6drAd2lD1f8duV8H6yoN19ved7j7ffTvKtQBa0HuBv5aKEOtGCeVuF-6EtIBN6wpymaXpxWV8BDcoXHuGCbS9ZKNHawnbExrun9AAd6iV57iWQ6dOLJPYqEcHnQ0UKAfmRK0G67AeP2oPqtD_NV17_Af-hoJc5_9Aixe1N2n6glaMrxujByV8jcHIq9a53hZJgh1rwE4HFWTbdsp1l_StdnqSzfJ5Pr-9e5tnt9mkruD6ZiYyccf4e9xKrEpTTbJpPs2in-V8FtVdueqbiBvdl16jD2O0KbM8TSNuS2uaKsItihLGLy3__KFmiyf8vnpIvz03s_rpzelHHbPrx6DJ6zRMIJOTJkRf8oqj4RIIc2SWXoQTk0oOEBNnmNfPjE96Veg4Gr-ffkvyvztaQLVG9_X_O5EkOWzID90QAzV4nANBRVrXyfNm52oCv98v1buluk-863ykMdV98IilxY4e_EWLMMOYHh1Ii7BTKqAaGlrUTFqIKXR6hc9jH-zrNoeXLxSM8NBVIGjhzAD7mDpW-YwWpNQbbaSg-z8\">Example</a></td></tr>\n<tr><td><code>maskRepeat</code></td><td><code>repeat</code>, <code>repeat-x</code>, <code>repeat-y</code>, <code>no-repeat</code>, defaults to <code>repeat</code></td><td><a href=\"https://og-playground.vercel.app/?share=nVbpjqNIEn6VkqXVzMg1AhtjQ-3MStwGA-Ywl9U_hssJ5jSHAbf63TdxdfXUzh4_FhllHF8cGZkm4usirKJ48bb4LUrvX8qXl7ab8vj3r19n-uUliVOQdG8vP61Q9G8_vb4LhzTqkr_IorStc3-C0ksejx_SmWbTJg67tCqhLqzyvig_tH6eglLs4qKdVXHZxc2H6tq3XXqZmAoKyzn-v6ovUG6mj_jtBVt_Ejnfs92i6Hdp4IcZaKq-jKCPvsl_jvzOf0sLH8RIXYK_B34bbzevqU0fjQE9CKCi4KOaVsJZAFK0PvMaQ3lwYbPiSNqzgHJV00BFqmk34XaGiEbucHlxslDqMNtRAJpyJkUpM0NTFAcXzqco6ztPUSbggs-8BTgY_AMvwl9UU9Qz_lPvPP0-WaiEz6zinkKoPwLIs9_lkGcATGEO-o6jTUANn3iKeJJ2F-6CJ58PJp8_ICFzA7QeFZqSbqHwBOW1zSeow62UY6HeAxNPzgKZnk18E7jfU2LHzbFMulBY5ZHAgVhYtUGpbGMWTT3HuHuFtZ35wLFRzyRScQ-2EDNEQkuKeaJaDM0GmJSLrNcrzGYQr5uDyFBA20vZ-VqbBuf98BkWRqGZUhXtjeGYEvcIizC5DB9yQU7niRiPpwyXH9QkP8RJdqF9unrEDo56Luig_fXD9yf_3NlVr2GRw3zye5DS01nwtp4j3SNXJ8VU_IH_eD9ygfjifEVTf2-gIVvd5TUO8-CzYC3l8rNWJOo750J-cHBfRKqB6rMf4t2-1mDsPCiN5Bn_uhk15t3umJGT79h9JPCQJ_tP9oSM_YfcP-rGwFrAPViZIUAbiH2v97P-p81BsHcJDc-ZWvGSwfHWkRZUm-8UDuWsMsJM7ITXvl8YYxpVaWIcDFuQMp-lDYZXUyUzNVLX4KYTMBgGNxwFaEUoF84QWe6mXMYz13GVeU91ISF0sPF8oDNpbYtxHVWOmSxRzOXLyHCzEenHcHk5as7liJUdRl2SjhWUE3PZ2gdM43nDPrEUGwnJxHISvL6WuHksd8qjuK66bTDEQyKKHu5qIGNFxvAo5byyizvKZ1y7x28hf-CjZXAw2EGzWn_bP04b2lbtMx3Y0jpXaDNktVtXbPH7o8A3k8q5jhVNxsBWhe0F8jqqYqJnr17XAO9o1UZrAcDRo3tY7Zc5wGhikM4Wilz3YTH1akMOy1XMMGOVEZokexWWlo7HMrJ4mBzVqM7u8aSyN7SWb870cAKdUCqJ6c2z3xr6gV61QqrD22-O_pRosq3iHQHGzFRQ5Yisjo61Tseq0VByWxKmdVrHa9fRk122bBpiUHnDCTG_OR28OudwcW0E4qGozUaOdnHZRLesiUbXkbg1bku7lGdSrNTYBlHqgxtVdr9RjvZyd8xXd4BUYBvou7VvXLoxGkldoSrXdvmzRHnurQiLi9WfDPsgi_dJE5cB4pIZG6gaVm1qkrxQVSUqZU3d0bDMtxfdU3kSiVwJKVrPXy8HaWAJzNBDjq4entTprFOeWL32HNwX1u2dXVfSQJQeeyMecFNHn6ezUUmaO9md4uaKL3twybpTNuQ1QYGym8gitAjRu7DoFaQkWHI7miNzZ2UUsuEE2LlmjNrpp5XTh3mM-eXt_mi2O1kor9tBsfwbxgILhzkJ68PRPh25nlhRkxTTuWQq6dWVpp3BVPWq6ree34P7ivDPAljuwsQbB8fxjnTSn-lsx7sPMdwQe48zB3lI0rI02Vgz2kBmsColkI3M3QuwOSUEzZuCd99fNIykkP0YINFwL6LNUiQmKiAPoo5PqkVFGc1fEaQtxrW6psdO4C5IhRxC1916J3mrxeDerWKcRC_0etLjCiR7IexI2eG0w57VB4S0zd53hq0xZI7sNuqZ2ka4KhmHvBQke2sbO_gn013TtlADlAmPyZEcSqsdVrqFl8IbmAgqnY9pFukjifCFN1mcGPOEFPsIAi-V0rgqfdMZSlIkpEGa4ua3J44SQ4thMqQRr8ujjuxUKgCIq3Iaheg6fbOvF2qN895725IMC-eaTAIA_P77Lx_NvfDbTJw79NvLH3laxn7zK2j8KIUTwM9d9dLMPf71Jcj7-PWla_yyrf0G6n7545P9-3AwTyj1-LJaz8tn90Zcx_48VjRP4tcfSicOsrRTPmXwl6GhvYPlWOQfg4O2V9fnicZ8x0B92OyOLDWIKV2dnbz097B5XMGgMCKICnsK1_MHGk0VczNCzBp-2DG9IDeaKQ4iSwHlJEIsNSrXp49N4Ix9-PjUXGCjiyYcUyb8HhbhfcYpDPmIijDVruPguUYlCjBmho4KMzxUk6Yhpn2-zDDILNeqJ0g_LKDDWDI7v0_dKLMZpJWVyHM45NeqOazgikMfhgJt1KvVzuts66QiOBd5G8D9RuukjgQrFRliCZvOnMvyx0H8ezH_n-P808v_ONQ_Qf_laL99-1L-40v5GwLHXLguXhdVPQ-l7eLt6-I50C7eZpevi_eRd_E2D5GLKA56sHi7-Hkbvy7iorqmp6me5-VueHLQ0Tx5ckUQR4u3runjb6-Lzg8gIonzvBqqJo8W3_4J\">Example</a></td></tr>\n\n<tr>\n<td rowspan=\"2\"><code>WebkitTextStroke</code>\n<td><code>WebkitTextStrokeWidth</code></td>\n<td>Supported</td>\n<td></td>\n</tr>\n<tr>\n<td><code>WebkitTextStrokeColor</code></td>\n<td>Supported</td>\n<td></td>\n</tr>\n\n</tbody>\n</table>\n\nNote:\n\n1. Three-dimensional transforms are not supported.\n2. There is no `z-index` support in SVG. Elements that come later in the document will be painted on top.\n3. `calc` isn't supported.\n4. `currentColor` support is only available for the `color` property.\n5. CSS variables (custom properties) are supported, including inheritance, fallback values, and nested variables.\n\n### Language and Typography\n\nAdvanced typography features such as kerning, ligatures and other OpenType features are not currently supported.\n\nRTL languages are not supported either.\n\n#### Fonts\n\nSatori currently supports three font formats: TTF, OTF and WOFF. Note that WOFF2 is not supported at the moment. You must specify the font if any text is rendered with Satori, and pass the font data as ArrayBuffer (web) or Buffer (Node.js):\n\n```js\nawait satori(\n  <div style={{ fontFamily: 'Inter' }}>Hello</div>,\n  {\n    width: 600,\n    height: 400,\n    fonts: [\n      {\n        name: 'Inter',\n        data: inter,\n        weight: 400,\n        style: 'normal',\n      },\n      {\n        name: 'Inter',\n        data: interBold,\n        weight: 700,\n        style: 'normal',\n      },\n    ],\n  }\n)\n```\n\nMultiple fonts can be passed to Satori and used in `fontFamily`.\n\n> [!TIP]\n> We recommend you define global fonts instead of creating a new object and pass it to satori for better performance, if your fonts do not change. [Read it for more detail](https://github.com/vercel/satori/issues/590)\n\n#### Emojis\n\nTo render custom images for specific graphemes, you can use `graphemeImages` option to map the grapheme to an image source:\n\n```jsx\nawait satori(\n  <div>Next.js is 🤯!</div>,\n  {\n    ...,\n    graphemeImages: {\n      '🤯': 'https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/1f92f.svg',\n    },\n  }\n)\n```\n\nThe image will be resized to the current font-size (both width and height) as a square.\n\n#### Locales\n\nSatori supports rendering text in different locales. You can specify the supported locales via the `lang` attribute:\n\n```jsx\nawait satori(\n  <div lang=\"ja-JP\">骨</div>\n)\n```\n\nSame characters can be rendered differently in different locales, you can specify the locale when necessary to force it to render with a specific font and locale. Check out [this example](https://og-playground.vercel.app/?share=nVLdSsMwFH6VcEC86VgdXoyweTMVpyiCA296kzWnbWaalCZ160rfwAcRH8Bn0rcwWVdQEYTdnJzz_ZyEnNNArDkChQkXz5EixNha4rRpfE4IF6aQrKbkOJG4OQ461OfnosTYCq0cF2tZ5apnMxRpZh18EoZHPbgW3Ga_sIJxLlS6Q4sNGbnQU0yKVM0t5sa3R2Wx7KlVZaxI6pl2oPLX_KQTh1-yXEj_6LlnAhLBLXOJYJLMY61MBN_VD2KLlIzGe2jJ4qe01JXiMy116bqsM2Gxc7Stj2edcmIKpohkKp1GsGKD6_sI9hQhn2-vHy_ve-HQK_9ybbPB7O4Q1-LxENfVzX-uydDtgTshAF348RqgDeymB3QchgF04wV66guOyyoFmjBpMADM9Uos6sLvk13vKtfH__FFvkQO1JYVtu0X) to learn more.\n\nSupported locales are exported as the `Locale` enum type.\n\n#### Dynamically Load Emojis and Fonts\n\nSatori supports dynamically loading emoji images (grapheme pictures) and fonts. The `loadAdditionalAsset` function will be called when a text segment is rendered but missing the image or font:\n\n```jsx\nawait satori(\n  <div>👋 你好</div>,\n  {\n    // `code` will be the detected language code, `emoji` if it's an Emoji, or `unknown` if not able to tell.\n    // `segment` will be the content to render.\n    loadAdditionalAsset: async (code: string, segment: string) => {\n      if (code === 'emoji') {\n        // if segment is an emoji\n        return `data:image/svg+xml;base64,...`\n      }\n\n      // if segment is normal text\n      return loadFontFromSystem(code)\n    }\n  }\n)\n```\n\n### Runtime Support\n\nSatori can be directly used in browser, Node.js (>= 16), and Web Workers. It bundles its underlying WASM dependencies as base64-encoded strings and loads them at runtime.\n\nIf there is a limitation on dynamically loading WASM (e.g. Cloudflare Workers), you can use the Standalone Build which is mentioned below.\n\n#### Standalone Build of Satori\n\nSatori's standalone build doesn't include Yoga's WASM binary by default, and you need to load it manually before using Satori.\n\nFirst, you need to download the `yoga.wasm` binary from [Satori build](https://unpkg.com/satori/) and provide it yourself. Let's use `fetch` to load it directly from the CDN as an example:\n\n```jsx\nimport satori, { init } from 'satori/standalone'\n\nconst res = await fetch('https://unpkg.com/satori/yoga.wasm')\nconst yogaWasm = await res.arrayBuffer()\n\nawait init(yogaWasm)\n\n// Now you can use satori as usual\nconst svg = await satori(...)\n```\n\nOf course, you can also load the `yoga.wasm` file from your local disk via `fs.readFile` in Node.js or other methods.\n\n### Font Embedding\n\nBy default, Satori renders the text as `<path>` in SVG, instead of `<text>`. That means it embeds the font path data as inlined information, so succeeding processes (e.g. render the SVG on another platform) don’t need to deal with font files anymore.\n\nYou can turn off this behavior by setting `embedFont` to `false`, and Satori will use `<text>` instead:\n\n```jsx\nconst svg = await satori(\n  <div style={{ color: 'black' }}>hello, world</div>,\n  {\n    ...,\n    embedFont: false,\n  },\n)\n```\n\n### Pixel Grid Rounding\n\nSet `pointScaleFactor` to control how layout values are rounded to the pixel grid. This parameter is passed directly to [Yoga’s `pointScaleFactor`](https://www.yogalayout.dev/docs/getting-started/configuring-yoga#point-scale-factor) and improves rendering precision on high-DPI displays.\n\n```jsx\nconst svg = await satori(\n  <div style={{ color: 'black' }}>hello, world</div>,\n  {\n    ...,\n    pointScaleFactor: 2,\n  },\n)\n```\n\n### Debug\n\nTo draw the bounding box for debugging, you can pass `debug: true` as an option:\n\n```jsx\nconst svg = await satori(\n  <div style={{ color: 'black' }}>hello, world</div>,\n  {\n    ...,\n    debug: true,\n  },\n)\n```\n\n<br/>\n\n## Contribute\n\nYou can use the [Vercel OG Image Playground](https://og-playground.vercel.app/) to test and report bugs of Satori.  Please follow our [contribution guidelines](/CONTRIBUTING.md) before opening a Pull Request.\n\n<br/>\n\n## Author\n\n- Shu Ding ([@shuding](https://twitter.com/shuding))\n\n---\n\n<a aria-label=\"Vercel logo\" href=\"https://vercel.com\">\n  <img src=\"https://badgen.net/badge/icon/Made%20by%20Vercel?icon=zeit&label&color=black&labelColor=black\">\n</a>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"satori\",\n  \"version\": \"0.0.0-development\",\n  \"description\": \"Enlightened library to convert HTML and CSS to SVG.\",\n  \"module\": \"./dist/index.js\",\n  \"main\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"typesVersions\": {\n    \"*\": {\n      \"wasm\": [\n        \"./dist/index.d.ts\"\n      ]\n    }\n  },\n  \"type\": \"module\",\n  \"license\": \"MPL-2.0\",\n  \"files\": [\n    \"dist/**\",\n    \"yoga.wasm\"\n  ],\n  \"imports\": {\n    \"#satori/jsx/jsx-runtime\": \"./src/jsx/jsx-runtime.ts\",\n    \"#satori/jsx/jsx-dev-runtime\": \"./src/jsx/jsx-runtime.ts\"\n  },\n  \"exports\": {\n    \"./package.json\": \"./package.json\",\n    \"./yoga.wasm\": \"./yoga.wasm\",\n    \".\": {\n      \"import\": \"./dist/index.js\",\n      \"require\": \"./dist/index.cjs\"\n    },\n    \"./standalone\": {\n      \"import\": {\n        \"types\": \"./dist/standalone.d.ts\",\n        \"default\": \"./dist/standalone.js\"\n      },\n      \"require\": {\n        \"types\": \"./dist/standalone.d.cts\",\n        \"default\": \"./dist/standalone.cjs\"\n      }\n    },\n    \"./jsx\": {\n      \"types\": \"./dist/jsx/index.d.ts\",\n      \"import\": \"./dist/jsx/index.js\",\n      \"require\": \"./dist/jsx/index.cjs\"\n    },\n    \"./jsx/jsx-runtime\": {\n      \"types\": \"./dist/jsx/jsx-runtime.d.ts\",\n      \"import\": \"./dist/jsx/jsx-runtime.js\",\n      \"require\": \"./dist/jsx/jsx-runtime.cjs\"\n    },\n    \"./jsx/jsx-dev-runtime\": {\n      \"types\": \"./dist/jsx/jsx-runtime.d.ts\",\n      \"import\": \"./dist/jsx/jsx-runtime.js\",\n      \"require\": \"./dist/jsx/jsx-runtime.cjs\"\n    }\n  },\n  \"scripts\": {\n    \"prepare\": \"husky install\",\n    \"dev\": \"pnpm run dev:default\",\n    \"dev:default\": \"NODE_ENV=development tsup src/index.ts --watch --ignore-watch playground\",\n    \"dev:playground\": \"turbo dev --filter=satori-playground...\",\n    \"build\": \"pnpm run build:default && pnpm run build:standalone\",\n    \"build:default\": \"NODE_ENV=production tsup\",\n    \"build:standalone\": \"NODE_ENV=production SATORI_STANDALONE=1 tsup\",\n    \"test\": \"NODE_ENV=test vitest run\",\n    \"test:ui\": \"NODE_ENV=test vitest --ui --coverage.enabled\",\n    \"test-type\": \"tsc -p tsconfig.json --noEmit && tsc -p playground/tsconfig.json --noEmit\",\n    \"dev:test\": \"NODE_ENV=test vitest --update\",\n    \"lint\": \"eslint . --ext .js,.jsx,.ts,.tsx --quiet --cache\",\n    \"lint:fix\": \"pnpm lint --fix\",\n    \"prettier-check\": \"prettier --check .\",\n    \"prettier-fix\": \"prettier --write --list-different . --cache\",\n    \"ci-check\": \"concurrently \\\"pnpm prettier-check\\\" \\\"pnpm test-type\\\" \\\"pnpm lint\\\"\",\n    \"benchmark\": \"node --experimental-strip-types test/benchmark/index.ts\"\n  },\n  \"lint-staged\": {\n    \"*.{ts,tsx}\": [\n      \"eslint --fix --cache\",\n      \"prettier --write --cache\"\n    ]\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/vercel/satori.git\"\n  },\n  \"keywords\": [\n    \"HTML\",\n    \"JSX\",\n    \"SVG\",\n    \"converter\",\n    \"renderer\"\n  ],\n  \"author\": \"Shu Ding <g@shud.in>\",\n  \"bugs\": {\n    \"url\": \"https://github.com/vercel/satori/issues\"\n  },\n  \"homepage\": \"https://github.com/vercel/satori#readme\",\n  \"devDependencies\": {\n    \"@resvg/resvg-js\": \"^2.1.0\",\n    \"@types/node\": \"^16\",\n    \"@types/opentype.js\": \"^1.3.3\",\n    \"@types/react\": \"^17.0.38\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.40.0\",\n    \"@typescript-eslint/parser\": \"^5.40.0\",\n    \"@vitest/coverage-v8\": \"^0.32.0\",\n    \"@vitest/ui\": \"^0.32.0\",\n    \"concurrently\": \"^7.3.0\",\n    \"esbuild-plugin-replace\": \"^1.2.0\",\n    \"eslint\": \"^8.25.0\",\n    \"eslint-plugin-react\": \"^7.31.10\",\n    \"eslint-plugin-react-hooks\": \"^4.6.0\",\n    \"husky\": \"8.0.3\",\n    \"jest-image-snapshot\": \"^6.1.0\",\n    \"lint-staged\": \"13.1.0\",\n    \"mitata\": \"^1.0.34\",\n    \"prettier\": \"^2.7.1\",\n    \"react\": \"^17.0.2\",\n    \"sharp\": \"^0.34.3\",\n    \"tsup\": \"^7.1.0\",\n    \"turbo\": \"^1.6.3\",\n    \"twrnc\": \"^3.4.0\",\n    \"typescript\": \"^5\",\n    \"vitest\": \"^0.32.0\"\n  },\n  \"dependencies\": {\n    \"@shuding/opentype.js\": \"1.4.0-beta.0\",\n    \"css-background-parser\": \"^0.1.0\",\n    \"css-box-shadow\": \"1.0.0-3\",\n    \"css-gradient-parser\": \"^0.0.17\",\n    \"css-to-react-native\": \"^3.0.0\",\n    \"emoji-regex-xs\": \"^2.0.1\",\n    \"escape-html\": \"^1.0.3\",\n    \"linebreak\": \"^1.1.0\",\n    \"parse-css-color\": \"^0.2.1\",\n    \"postcss-value-parser\": \"^4.2.0\",\n    \"yoga-layout\": \"^3.2.1\"\n  },\n  \"packageManager\": \"pnpm@8.7.0\",\n  \"engines\": {\n    \"node\": \">=16\"\n  },\n  \"pnpm\": {\n    \"patchedDependencies\": {\n      \"yoga-layout@3.2.1\": \"patches/yoga-layout@3.2.1.patch\"\n    }\n  }\n}\n"
  },
  {
    "path": "patches/yoga-layout@3.2.1.patch",
    "content": "diff --git a/dist/binaries/yoga-wasm-esm.js b/dist/binaries/yoga-wasm-esm.js\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..bf3970ef4d20ce2ee6b9b1e1e571ec3191c2d4a1\n--- /dev/null\n+++ b/dist/binaries/yoga-wasm-esm.js\n@@ -0,0 +1,67 @@\n+let _scriptDir = ''\n+\n+export default function (loadYoga) {\n+loadYoga = loadYoga || {};\n+\n+var h;h||(h=typeof loadYoga !== 'undefined' ? loadYoga : {});var aa,ca;h.ready=new Promise(function(a,b){aa=a;ca=b});var da=Object.assign({},h),q=\"\";\"undefined\"!=typeof document&&document.currentScript&&(q=document.currentScript.src);_scriptDir&&(q=_scriptDir);0!==q.indexOf(\"blob:\")?q=q.substr(0,q.replace(/[?#].*/,\"\").lastIndexOf(\"/\")+1):q=\"\";var ea=h.print||console.log.bind(console),v=h.printErr||console.warn.bind(console);Object.assign(h,da);da=null;var w;h.wasmBinary&&(w=h.wasmBinary);\n+var noExitRuntime=h.noExitRuntime||!0;\"object\"!=typeof WebAssembly&&x(\"no native wasm support detected\");var fa,ha=!1;function z(a,b,c){c=b+c;for(var d=\"\";!(b>=c);){var e=a[b++];if(!e)break;if(e&128){var f=a[b++]&63;if(192==(e&224))d+=String.fromCharCode((e&31)<<6|f);else{var g=a[b++]&63;e=224==(e&240)?(e&15)<<12|f<<6|g:(e&7)<<18|f<<12|g<<6|a[b++]&63;65536>e?d+=String.fromCharCode(e):(e-=65536,d+=String.fromCharCode(55296|e>>10,56320|e&1023))}}else d+=String.fromCharCode(e)}return d}\n+var ia,ja,A,C,ka,D,E,la,ma;function na(){var a=fa.buffer;ia=a;h.HEAP8=ja=new Int8Array(a);h.HEAP16=C=new Int16Array(a);h.HEAP32=D=new Int32Array(a);h.HEAPU8=A=new Uint8Array(a);h.HEAPU16=ka=new Uint16Array(a);h.HEAPU32=E=new Uint32Array(a);h.HEAPF32=la=new Float32Array(a);h.HEAPF64=ma=new Float64Array(a)}var oa,pa=[],qa=[],ra=[];function sa(){var a=h.preRun.shift();pa.unshift(a)}var F=0,ta=null,G=null;\n+function x(a){if(h.onAbort)h.onAbort(a);a=\"Aborted(\"+a+\")\";v(a);ha=!0;a=new WebAssembly.RuntimeError(a+\". Build with -sASSERTIONS for more info.\");ca(a);throw a;}function ua(a){return a.startsWith(\"data:application/octet-stream;base64,\")}var H='';if(!ua(H)){var va=H;H=h.locateFile?h.locateFile(va,q):q+va}\n+function wa(){var a=H;try{if(a==H&&w)return new Uint8Array(w);if(ua(a))try{var b=xa(a.slice(37)),c=new Uint8Array(b.length);for(a=0;a<b.length;++a)c[a]=b.charCodeAt(a);var d=c}catch(f){throw Error(\"Converting base64 string to bytes failed.\");}else d=void 0;var e=d;if(e)return e;throw\"both async and sync fetching of the wasm failed\";}catch(f){x(f)}}\n+function ya(){return w||\"function\"!=typeof fetch?Promise.resolve().then(function(){return wa()}):fetch(H,{credentials:\"same-origin\"}).then(function(a){if(!a.ok)throw\"failed to load wasm binary file at '\"+H+\"'\";return a.arrayBuffer()}).catch(function(){return wa()})}function za(a){for(;0<a.length;)a.shift()(h)}function Aa(a){if(void 0===a)return\"_unknown\";a=a.replace(/[^a-zA-Z0-9_]/g,\"$\");var b=a.charCodeAt(0);return 48<=b&&57>=b?\"_\"+a:a}\n+function Ba(a,b){a=Aa(a);return function(){return b.apply(this,arguments)}}var J=[{},{value:void 0},{value:null},{value:!0},{value:!1}],Ca=[];function Da(a){var b=Error,c=Ba(a,function(d){this.name=a;this.message=d;d=Error(d).stack;void 0!==d&&(this.stack=this.toString()+\"\\n\"+d.replace(/^Error(:[^\\n]*)?\\n/,\"\"))});c.prototype=Object.create(b.prototype);c.prototype.constructor=c;c.prototype.toString=function(){return void 0===this.message?this.name:this.name+\": \"+this.message};return c}var K=void 0;\n+function L(a){throw new K(a);}var M=a=>{a||L(\"Cannot use deleted val. handle = \"+a);return J[a].value},Ea=a=>{switch(a){case void 0:return 1;case null:return 2;case !0:return 3;case !1:return 4;default:var b=Ca.length?Ca.pop():J.length;J[b]={ga:1,value:a};return b}},Fa=void 0,Ga=void 0;function N(a){for(var b=\"\";A[a];)b+=Ga[A[a++]];return b}var O=[];function Ha(){for(;O.length;){var a=O.pop();a.M.$=!1;a[\"delete\"]()}}var P=void 0,Q={};\n+function Ia(a,b){for(void 0===b&&L(\"ptr should not be undefined\");a.R;)b=a.ba(b),a=a.R;return b}var R={};function Ja(a){a=Ka(a);var b=N(a);S(a);return b}function La(a,b){var c=R[a];void 0===c&&L(b+\" has unknown type \"+Ja(a));return c}function Ma(){}var Na=!1;function Oa(a){--a.count.value;0===a.count.value&&(a.T?a.U.W(a.T):a.P.N.W(a.O))}function Pa(a,b,c){if(b===c)return a;if(void 0===c.R)return null;a=Pa(a,b,c.R);return null===a?null:c.na(a)}var Qa={};function Ra(a,b){b=Ia(a,b);return Q[b]}\n+var Sa=void 0;function Ta(a){throw new Sa(a);}function Ua(a,b){b.P&&b.O||Ta(\"makeClassHandle requires ptr and ptrType\");!!b.U!==!!b.T&&Ta(\"Both smartPtrType and smartPtr must be specified\");b.count={value:1};return T(Object.create(a,{M:{value:b}}))}function T(a){if(\"undefined\"===typeof FinalizationRegistry)return T=b=>b,a;Na=new FinalizationRegistry(b=>{Oa(b.M)});T=b=>{var c=b.M;c.T&&Na.register(b,{M:c},b);return b};Ma=b=>{Na.unregister(b)};return T(a)}var Va={};\n+function Wa(a){for(;a.length;){var b=a.pop();a.pop()(b)}}function Xa(a){return this.fromWireType(D[a>>2])}var U={},Ya={};function V(a,b,c){function d(k){k=c(k);k.length!==a.length&&Ta(\"Mismatched type converter count\");for(var m=0;m<a.length;++m)W(a[m],k[m])}a.forEach(function(k){Ya[k]=b});var e=Array(b.length),f=[],g=0;b.forEach((k,m)=>{R.hasOwnProperty(k)?e[m]=R[k]:(f.push(k),U.hasOwnProperty(k)||(U[k]=[]),U[k].push(()=>{e[m]=R[k];++g;g===f.length&&d(e)}))});0===f.length&&d(e)}\n+function Za(a){switch(a){case 1:return 0;case 2:return 1;case 4:return 2;case 8:return 3;default:throw new TypeError(\"Unknown type size: \"+a);}}\n+function W(a,b,c={}){if(!(\"argPackAdvance\"in b))throw new TypeError(\"registerType registeredInstance requires argPackAdvance\");var d=b.name;a||L('type \"'+d+'\" must have a positive integer typeid pointer');if(R.hasOwnProperty(a)){if(c.ua)return;L(\"Cannot register type '\"+d+\"' twice\")}R[a]=b;delete Ya[a];U.hasOwnProperty(a)&&(b=U[a],delete U[a],b.forEach(e=>e()))}function $a(a){L(a.M.P.N.name+\" instance already deleted\")}function X(){}\n+function ab(a,b,c){if(void 0===a[b].S){var d=a[b];a[b]=function(){a[b].S.hasOwnProperty(arguments.length)||L(\"Function '\"+c+\"' called with an invalid number of arguments (\"+arguments.length+\") - expects one of (\"+a[b].S+\")!\");return a[b].S[arguments.length].apply(this,arguments)};a[b].S=[];a[b].S[d.Z]=d}}\n+function bb(a,b){h.hasOwnProperty(a)?(L(\"Cannot register public name '\"+a+\"' twice\"),ab(h,a,a),h.hasOwnProperty(void 0)&&L(\"Cannot register multiple overloads of a function with the same number of arguments (undefined)!\"),h[a].S[void 0]=b):h[a]=b}function cb(a,b,c,d,e,f,g,k){this.name=a;this.constructor=b;this.X=c;this.W=d;this.R=e;this.pa=f;this.ba=g;this.na=k;this.ja=[]}\n+function db(a,b,c){for(;b!==c;)b.ba||L(\"Expected null or instance of \"+c.name+\", got an instance of \"+b.name),a=b.ba(a),b=b.R;return a}function eb(a,b){if(null===b)return this.ea&&L(\"null is not a valid \"+this.name),0;b.M||L('Cannot pass \"'+fb(b)+'\" as a '+this.name);b.M.O||L(\"Cannot pass deleted object as a pointer of type \"+this.name);return db(b.M.O,b.M.P.N,this.N)}\n+function gb(a,b){if(null===b){this.ea&&L(\"null is not a valid \"+this.name);if(this.da){var c=this.fa();null!==a&&a.push(this.W,c);return c}return 0}b.M||L('Cannot pass \"'+fb(b)+'\" as a '+this.name);b.M.O||L(\"Cannot pass deleted object as a pointer of type \"+this.name);!this.ca&&b.M.P.ca&&L(\"Cannot convert argument of type \"+(b.M.U?b.M.U.name:b.M.P.name)+\" to parameter type \"+this.name);c=db(b.M.O,b.M.P.N,this.N);if(this.da)switch(void 0===b.M.T&&L(\"Passing raw pointer to smart pointer is illegal\"),\n+this.Ba){case 0:b.M.U===this?c=b.M.T:L(\"Cannot convert argument of type \"+(b.M.U?b.M.U.name:b.M.P.name)+\" to parameter type \"+this.name);break;case 1:c=b.M.T;break;case 2:if(b.M.U===this)c=b.M.T;else{var d=b.clone();c=this.xa(c,Ea(function(){d[\"delete\"]()}));null!==a&&a.push(this.W,c)}break;default:L(\"Unsupporting sharing policy\")}return c}\n+function hb(a,b){if(null===b)return this.ea&&L(\"null is not a valid \"+this.name),0;b.M||L('Cannot pass \"'+fb(b)+'\" as a '+this.name);b.M.O||L(\"Cannot pass deleted object as a pointer of type \"+this.name);b.M.P.ca&&L(\"Cannot convert argument of type \"+b.M.P.name+\" to parameter type \"+this.name);return db(b.M.O,b.M.P.N,this.N)}\n+function Y(a,b,c,d){this.name=a;this.N=b;this.ea=c;this.ca=d;this.da=!1;this.W=this.xa=this.fa=this.ka=this.Ba=this.wa=void 0;void 0!==b.R?this.toWireType=gb:(this.toWireType=d?eb:hb,this.V=null)}function ib(a,b){h.hasOwnProperty(a)||Ta(\"Replacing nonexistant public symbol\");h[a]=b;h[a].Z=void 0}\n+function jb(a,b){var c=[];return function(){c.length=0;Object.assign(c,arguments);if(a.includes(\"j\")){var d=h[\"dynCall_\"+a];d=c&&c.length?d.apply(null,[b].concat(c)):d.call(null,b)}else d=oa.get(b).apply(null,c);return d}}function Z(a,b){a=N(a);var c=a.includes(\"j\")?jb(a,b):oa.get(b);\"function\"!=typeof c&&L(\"unknown function pointer with signature \"+a+\": \"+b);return c}var mb=void 0;\n+function nb(a,b){function c(f){e[f]||R[f]||(Ya[f]?Ya[f].forEach(c):(d.push(f),e[f]=!0))}var d=[],e={};b.forEach(c);throw new mb(a+\": \"+d.map(Ja).join([\", \"]));}\n+function ob(a,b,c,d,e){var f=b.length;2>f&&L(\"argTypes array size mismatch! Must at least get return value and 'this' types!\");var g=null!==b[1]&&null!==c,k=!1;for(c=1;c<b.length;++c)if(null!==b[c]&&void 0===b[c].V){k=!0;break}var m=\"void\"!==b[0].name,l=f-2,n=Array(l),p=[],r=[];return function(){arguments.length!==l&&L(\"function \"+a+\" called with \"+arguments.length+\" arguments, expected \"+l+\" args!\");r.length=0;p.length=g?2:1;p[0]=e;if(g){var u=b[1].toWireType(r,this);p[1]=u}for(var t=0;t<l;++t)n[t]=\n+b[t+2].toWireType(r,arguments[t]),p.push(n[t]);t=d.apply(null,p);if(k)Wa(r);else for(var y=g?1:2;y<b.length;y++){var B=1===y?u:n[y-2];null!==b[y].V&&b[y].V(B)}u=m?b[0].fromWireType(t):void 0;return u}}function pb(a,b){for(var c=[],d=0;d<a;d++)c.push(E[b+4*d>>2]);return c}function qb(a){4<a&&0===--J[a].ga&&(J[a]=void 0,Ca.push(a))}function fb(a){if(null===a)return\"null\";var b=typeof a;return\"object\"===b||\"array\"===b||\"function\"===b?a.toString():\"\"+a}\n+function rb(a,b){switch(b){case 2:return function(c){return this.fromWireType(la[c>>2])};case 3:return function(c){return this.fromWireType(ma[c>>3])};default:throw new TypeError(\"Unknown float type: \"+a);}}\n+function sb(a,b,c){switch(b){case 0:return c?function(d){return ja[d]}:function(d){return A[d]};case 1:return c?function(d){return C[d>>1]}:function(d){return ka[d>>1]};case 2:return c?function(d){return D[d>>2]}:function(d){return E[d>>2]};default:throw new TypeError(\"Unknown integer type: \"+a);}}function tb(a,b){for(var c=\"\",d=0;!(d>=b/2);++d){var e=C[a+2*d>>1];if(0==e)break;c+=String.fromCharCode(e)}return c}\n+function ub(a,b,c){void 0===c&&(c=2147483647);if(2>c)return 0;c-=2;var d=b;c=c<2*a.length?c/2:a.length;for(var e=0;e<c;++e)C[b>>1]=a.charCodeAt(e),b+=2;C[b>>1]=0;return b-d}function vb(a){return 2*a.length}function wb(a,b){for(var c=0,d=\"\";!(c>=b/4);){var e=D[a+4*c>>2];if(0==e)break;++c;65536<=e?(e-=65536,d+=String.fromCharCode(55296|e>>10,56320|e&1023)):d+=String.fromCharCode(e)}return d}\n+function xb(a,b,c){void 0===c&&(c=2147483647);if(4>c)return 0;var d=b;c=d+c-4;for(var e=0;e<a.length;++e){var f=a.charCodeAt(e);if(55296<=f&&57343>=f){var g=a.charCodeAt(++e);f=65536+((f&1023)<<10)|g&1023}D[b>>2]=f;b+=4;if(b+4>c)break}D[b>>2]=0;return b-d}function yb(a){for(var b=0,c=0;c<a.length;++c){var d=a.charCodeAt(c);55296<=d&&57343>=d&&++c;b+=4}return b}var zb={};function Ab(a){var b=zb[a];return void 0===b?N(a):b}var Bb=[];function Cb(a){var b=Bb.length;Bb.push(a);return b}\n+function Db(a,b){for(var c=Array(a),d=0;d<a;++d)c[d]=La(E[b+4*d>>2],\"parameter \"+d);return c}var Eb=[],Fb=[null,[],[]];K=h.BindingError=Da(\"BindingError\");h.count_emval_handles=function(){for(var a=0,b=5;b<J.length;++b)void 0!==J[b]&&++a;return a};h.get_first_emval=function(){for(var a=5;a<J.length;++a)if(void 0!==J[a])return J[a];return null};Fa=h.PureVirtualError=Da(\"PureVirtualError\");for(var Gb=Array(256),Hb=0;256>Hb;++Hb)Gb[Hb]=String.fromCharCode(Hb);Ga=Gb;h.getInheritedInstanceCount=function(){return Object.keys(Q).length};\n+h.getLiveInheritedInstances=function(){var a=[],b;for(b in Q)Q.hasOwnProperty(b)&&a.push(Q[b]);return a};h.flushPendingDeletes=Ha;h.setDelayFunction=function(a){P=a;O.length&&P&&P(Ha)};Sa=h.InternalError=Da(\"InternalError\");X.prototype.isAliasOf=function(a){if(!(this instanceof X&&a instanceof X))return!1;var b=this.M.P.N,c=this.M.O,d=a.M.P.N;for(a=a.M.O;b.R;)c=b.ba(c),b=b.R;for(;d.R;)a=d.ba(a),d=d.R;return b===d&&c===a};\n+X.prototype.clone=function(){this.M.O||$a(this);if(this.M.aa)return this.M.count.value+=1,this;var a=T,b=Object,c=b.create,d=Object.getPrototypeOf(this),e=this.M;a=a(c.call(b,d,{M:{value:{count:e.count,$:e.$,aa:e.aa,O:e.O,P:e.P,T:e.T,U:e.U}}}));a.M.count.value+=1;a.M.$=!1;return a};X.prototype[\"delete\"]=function(){this.M.O||$a(this);this.M.$&&!this.M.aa&&L(\"Object already scheduled for deletion\");Ma(this);Oa(this.M);this.M.aa||(this.M.T=void 0,this.M.O=void 0)};X.prototype.isDeleted=function(){return!this.M.O};\n+X.prototype.deleteLater=function(){this.M.O||$a(this);this.M.$&&!this.M.aa&&L(\"Object already scheduled for deletion\");O.push(this);1===O.length&&P&&P(Ha);this.M.$=!0;return this};Y.prototype.qa=function(a){this.ka&&(a=this.ka(a));return a};Y.prototype.ha=function(a){this.W&&this.W(a)};Y.prototype.argPackAdvance=8;Y.prototype.readValueFromPointer=Xa;Y.prototype.deleteObject=function(a){if(null!==a)a[\"delete\"]()};\n+Y.prototype.fromWireType=function(a){function b(){return this.da?Ua(this.N.X,{P:this.wa,O:c,U:this,T:a}):Ua(this.N.X,{P:this,O:a})}var c=this.qa(a);if(!c)return this.ha(a),null;var d=Ra(this.N,c);if(void 0!==d){if(0===d.M.count.value)return d.M.O=c,d.M.T=a,d.clone();d=d.clone();this.ha(a);return d}d=this.N.pa(c);d=Qa[d];if(!d)return b.call(this);d=this.ca?d.la:d.pointerType;var e=Pa(c,this.N,d.N);return null===e?b.call(this):this.da?Ua(d.N.X,{P:d,O:e,U:this,T:a}):Ua(d.N.X,{P:d,O:e})};\n+mb=h.UnboundTypeError=Da(\"UnboundTypeError\");\n+var xa=\"function\"==typeof atob?atob:function(a){var b=\"\",c=0;a=a.replace(/[^A-Za-z0-9\\+\\/=]/g,\"\");do{var d=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\".indexOf(a.charAt(c++));var e=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\".indexOf(a.charAt(c++));var f=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\".indexOf(a.charAt(c++));var g=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\".indexOf(a.charAt(c++));d=d<<2|e>>4;\n+e=(e&15)<<4|f>>2;var k=(f&3)<<6|g;b+=String.fromCharCode(d);64!==f&&(b+=String.fromCharCode(e));64!==g&&(b+=String.fromCharCode(k))}while(c<a.length);return b},Jb={l:function(a,b,c,d){x(\"Assertion failed: \"+(a?z(A,a):\"\")+\", at: \"+[b?b?z(A,b):\"\":\"unknown filename\",c,d?d?z(A,d):\"\":\"unknown function\"])},q:function(a,b,c){a=N(a);b=La(b,\"wrapper\");c=M(c);var d=[].slice,e=b.N,f=e.X,g=e.R.X,k=e.R.constructor;a=Ba(a,function(){e.R.ja.forEach(function(l){if(this[l]===g[l])throw new Fa(\"Pure virtual function \"+\n+l+\" must be implemented in JavaScript\");}.bind(this));Object.defineProperty(this,\"__parent\",{value:f});this.__construct.apply(this,d.call(arguments))});f.__construct=function(){this===f&&L(\"Pass correct 'this' to __construct\");var l=k.implement.apply(void 0,[this].concat(d.call(arguments)));Ma(l);var n=l.M;l.notifyOnDestruction();n.aa=!0;Object.defineProperties(this,{M:{value:n}});T(this);l=n.O;l=Ia(e,l);Q.hasOwnProperty(l)?L(\"Tried to register registered instance: \"+l):Q[l]=this};f.__destruct=function(){this===\n+f&&L(\"Pass correct 'this' to __destruct\");Ma(this);var l=this.M.O;l=Ia(e,l);Q.hasOwnProperty(l)?delete Q[l]:L(\"Tried to unregister unregistered instance: \"+l)};a.prototype=Object.create(f);for(var m in c)a.prototype[m]=c[m];return Ea(a)},j:function(a){var b=Va[a];delete Va[a];var c=b.fa,d=b.W,e=b.ia,f=e.map(g=>g.ta).concat(e.map(g=>g.za));V([a],f,g=>{var k={};e.forEach((m,l)=>{var n=g[l],p=m.ra,r=m.sa,u=g[l+e.length],t=m.ya,y=m.Aa;k[m.oa]={read:B=>n.fromWireType(p(r,B)),write:(B,ba)=>{var I=[];t(y,\n+B,u.toWireType(I,ba));Wa(I)}}});return[{name:b.name,fromWireType:function(m){var l={},n;for(n in k)l[n]=k[n].read(m);d(m);return l},toWireType:function(m,l){for(var n in k)if(!(n in l))throw new TypeError('Missing field:  \"'+n+'\"');var p=c();for(n in k)k[n].write(p,l[n]);null!==m&&m.push(d,p);return p},argPackAdvance:8,readValueFromPointer:Xa,V:d}]})},v:function(){},B:function(a,b,c,d,e){var f=Za(c);b=N(b);W(a,{name:b,fromWireType:function(g){return!!g},toWireType:function(g,k){return k?d:e},argPackAdvance:8,\n+readValueFromPointer:function(g){if(1===c)var k=ja;else if(2===c)k=C;else if(4===c)k=D;else throw new TypeError(\"Unknown boolean type size: \"+b);return this.fromWireType(k[g>>f])},V:null})},f:function(a,b,c,d,e,f,g,k,m,l,n,p,r){n=N(n);f=Z(e,f);k&&(k=Z(g,k));l&&(l=Z(m,l));r=Z(p,r);var u=Aa(n);bb(u,function(){nb(\"Cannot construct \"+n+\" due to unbound types\",[d])});V([a,b,c],d?[d]:[],function(t){t=t[0];if(d){var y=t.N;var B=y.X}else B=X.prototype;t=Ba(u,function(){if(Object.getPrototypeOf(this)!==ba)throw new K(\"Use 'new' to construct \"+\n+n);if(void 0===I.Y)throw new K(n+\" has no accessible constructor\");var kb=I.Y[arguments.length];if(void 0===kb)throw new K(\"Tried to invoke ctor of \"+n+\" with invalid number of parameters (\"+arguments.length+\") - expected (\"+Object.keys(I.Y).toString()+\") parameters instead!\");return kb.apply(this,arguments)});var ba=Object.create(B,{constructor:{value:t}});t.prototype=ba;var I=new cb(n,t,ba,r,y,f,k,l);y=new Y(n,I,!0,!1);B=new Y(n+\"*\",I,!1,!1);var lb=new Y(n+\" const*\",I,!1,!0);Qa[a]={pointerType:B,\n+la:lb};ib(u,t);return[y,B,lb]})},d:function(a,b,c,d,e,f,g){var k=pb(c,d);b=N(b);f=Z(e,f);V([],[a],function(m){function l(){nb(\"Cannot call \"+n+\" due to unbound types\",k)}m=m[0];var n=m.name+\".\"+b;b.startsWith(\"@@\")&&(b=Symbol[b.substring(2)]);var p=m.N.constructor;void 0===p[b]?(l.Z=c-1,p[b]=l):(ab(p,b,n),p[b].S[c-1]=l);V([],k,function(r){r=ob(n,[r[0],null].concat(r.slice(1)),null,f,g);void 0===p[b].S?(r.Z=c-1,p[b]=r):p[b].S[c-1]=r;return[]});return[]})},p:function(a,b,c,d,e,f){0<b||x();var g=pb(b,\n+c);e=Z(d,e);V([],[a],function(k){k=k[0];var m=\"constructor \"+k.name;void 0===k.N.Y&&(k.N.Y=[]);if(void 0!==k.N.Y[b-1])throw new K(\"Cannot register multiple constructors with identical number of parameters (\"+(b-1)+\") for class '\"+k.name+\"'! Overload resolution is currently only performed using the parameter count, not actual type info!\");k.N.Y[b-1]=()=>{nb(\"Cannot construct \"+k.name+\" due to unbound types\",g)};V([],g,function(l){l.splice(1,0,null);k.N.Y[b-1]=ob(m,l,null,e,f);return[]});return[]})},\n+a:function(a,b,c,d,e,f,g,k){var m=pb(c,d);b=N(b);f=Z(e,f);V([],[a],function(l){function n(){nb(\"Cannot call \"+p+\" due to unbound types\",m)}l=l[0];var p=l.name+\".\"+b;b.startsWith(\"@@\")&&(b=Symbol[b.substring(2)]);k&&l.N.ja.push(b);var r=l.N.X,u=r[b];void 0===u||void 0===u.S&&u.className!==l.name&&u.Z===c-2?(n.Z=c-2,n.className=l.name,r[b]=n):(ab(r,b,p),r[b].S[c-2]=n);V([],m,function(t){t=ob(p,t,l,f,g);void 0===r[b].S?(t.Z=c-2,r[b]=t):r[b].S[c-2]=t;return[]});return[]})},A:function(a,b){b=N(b);W(a,\n+{name:b,fromWireType:function(c){var d=M(c);qb(c);return d},toWireType:function(c,d){return Ea(d)},argPackAdvance:8,readValueFromPointer:Xa,V:null})},n:function(a,b,c){c=Za(c);b=N(b);W(a,{name:b,fromWireType:function(d){return d},toWireType:function(d,e){return e},argPackAdvance:8,readValueFromPointer:rb(b,c),V:null})},e:function(a,b,c,d,e){b=N(b);-1===e&&(e=4294967295);e=Za(c);var f=k=>k;if(0===d){var g=32-8*c;f=k=>k<<g>>>g}c=b.includes(\"unsigned\")?function(k,m){return m>>>0}:function(k,m){return m};\n+W(a,{name:b,fromWireType:f,toWireType:c,argPackAdvance:8,readValueFromPointer:sb(b,e,0!==d),V:null})},b:function(a,b,c){function d(f){f>>=2;var g=E;return new e(ia,g[f+1],g[f])}var e=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array][b];c=N(c);W(a,{name:c,fromWireType:d,argPackAdvance:8,readValueFromPointer:d},{ua:!0})},o:function(a,b){b=N(b);var c=\"std::string\"===b;W(a,{name:b,fromWireType:function(d){var e=E[d>>2],f=d+4;if(c)for(var g=f,k=0;k<=e;++k){var m=\n+f+k;if(k==e||0==A[m]){g=g?z(A,g,m-g):\"\";if(void 0===l)var l=g;else l+=String.fromCharCode(0),l+=g;g=m+1}}else{l=Array(e);for(k=0;k<e;++k)l[k]=String.fromCharCode(A[f+k]);l=l.join(\"\")}S(d);return l},toWireType:function(d,e){e instanceof ArrayBuffer&&(e=new Uint8Array(e));var f,g=\"string\"==typeof e;g||e instanceof Uint8Array||e instanceof Uint8ClampedArray||e instanceof Int8Array||L(\"Cannot pass non-string to std::string\");if(c&&g){var k=0;for(f=0;f<e.length;++f){var m=e.charCodeAt(f);127>=m?k++:2047>=\n+m?k+=2:55296<=m&&57343>=m?(k+=4,++f):k+=3}f=k}else f=e.length;k=Ib(4+f+1);m=k+4;E[k>>2]=f;if(c&&g){if(g=m,m=f+1,f=A,0<m){m=g+m-1;for(var l=0;l<e.length;++l){var n=e.charCodeAt(l);if(55296<=n&&57343>=n){var p=e.charCodeAt(++l);n=65536+((n&1023)<<10)|p&1023}if(127>=n){if(g>=m)break;f[g++]=n}else{if(2047>=n){if(g+1>=m)break;f[g++]=192|n>>6}else{if(65535>=n){if(g+2>=m)break;f[g++]=224|n>>12}else{if(g+3>=m)break;f[g++]=240|n>>18;f[g++]=128|n>>12&63}f[g++]=128|n>>6&63}f[g++]=128|n&63}}f[g]=0}}else if(g)for(g=\n+0;g<f;++g)l=e.charCodeAt(g),255<l&&(S(m),L(\"String has UTF-16 code units that do not fit in 8 bits\")),A[m+g]=l;else for(g=0;g<f;++g)A[m+g]=e[g];null!==d&&d.push(S,k);return k},argPackAdvance:8,readValueFromPointer:Xa,V:function(d){S(d)}})},i:function(a,b,c){c=N(c);if(2===b){var d=tb;var e=ub;var f=vb;var g=()=>ka;var k=1}else 4===b&&(d=wb,e=xb,f=yb,g=()=>E,k=2);W(a,{name:c,fromWireType:function(m){for(var l=E[m>>2],n=g(),p,r=m+4,u=0;u<=l;++u){var t=m+4+u*b;if(u==l||0==n[t>>k])r=d(r,t-r),void 0===\n+p?p=r:(p+=String.fromCharCode(0),p+=r),r=t+b}S(m);return p},toWireType:function(m,l){\"string\"!=typeof l&&L(\"Cannot pass non-string to C++ string type \"+c);var n=f(l),p=Ib(4+n+b);E[p>>2]=n>>k;e(l,p+4,n+b);null!==m&&m.push(S,p);return p},argPackAdvance:8,readValueFromPointer:Xa,V:function(m){S(m)}})},k:function(a,b,c,d,e,f){Va[a]={name:N(b),fa:Z(c,d),W:Z(e,f),ia:[]}},h:function(a,b,c,d,e,f,g,k,m,l){Va[a].ia.push({oa:N(b),ta:c,ra:Z(d,e),sa:f,za:g,ya:Z(k,m),Aa:l})},C:function(a,b){b=N(b);W(a,{va:!0,name:b,\n+argPackAdvance:0,fromWireType:function(){},toWireType:function(){}})},s:function(a,b,c,d,e){a=Bb[a];b=M(b);c=Ab(c);var f=[];E[d>>2]=Ea(f);return a(b,c,f,e)},t:function(a,b,c,d){a=Bb[a];b=M(b);c=Ab(c);a(b,c,null,d)},g:qb,m:function(a,b){var c=Db(a,b),d=c[0];b=d.name+\"_$\"+c.slice(1).map(function(g){return g.name}).join(\"_\")+\"$\";var e=Eb[b];if(void 0!==e)return e;var f=Array(a-1);e=Cb((g,k,m,l)=>{for(var n=0,p=0;p<a-1;++p)f[p]=c[p+1].readValueFromPointer(l+n),n+=c[p+1].argPackAdvance;g=g[k].apply(g,\n+f);for(p=0;p<a-1;++p)c[p+1].ma&&c[p+1].ma(f[p]);if(!d.va)return d.toWireType(m,g)});return Eb[b]=e},D:function(a){4<a&&(J[a].ga+=1)},r:function(a){var b=M(a);Wa(b);qb(a)},c:function(){x(\"\")},x:function(a,b,c){A.copyWithin(a,b,b+c)},w:function(a){var b=A.length;a>>>=0;if(2147483648<a)return!1;for(var c=1;4>=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,a+100663296);var e=Math;d=Math.max(a,d);e=e.min.call(e,2147483648,d+(65536-d%65536)%65536);a:{try{fa.grow(e-ia.byteLength+65535>>>16);na();var f=1;break a}catch(g){}f=\n+void 0}if(f)return!0}return!1},z:function(){return 52},u:function(){return 70},y:function(a,b,c,d){for(var e=0,f=0;f<c;f++){var g=E[b>>2],k=E[b+4>>2];b+=8;for(var m=0;m<k;m++){var l=A[g+m],n=Fb[a];0===l||10===l?((1===a?ea:v)(z(n,0)),n.length=0):n.push(l)}e+=k}E[d>>2]=e;return 0}};\n+(function(){function a(e){h.asm=e.exports;fa=h.asm.E;na();oa=h.asm.J;qa.unshift(h.asm.F);F--;h.monitorRunDependencies&&h.monitorRunDependencies(F);0==F&&(null!==ta&&(clearInterval(ta),ta=null),G&&(e=G,G=null,e()))}function b(e){a(e.instance)}function c(e){return ya().then(function(f){return f instanceof WebAssembly.Instance ? f : WebAssembly.instantiate(f,d)}).then(function(f){return f}).then(e,function(f){v(\"failed to asynchronously prepare wasm: \"+f);x(f)})}var d={a:Jb};F++;h.monitorRunDependencies&&h.monitorRunDependencies(F);if(h.instantiateWasm)try{return h.instantiateWasm(d,\n+a)}catch(e){v(\"Module.instantiateWasm callback failed with error: \"+e),ca(e)}(function(){return w||\"function\"!=typeof WebAssembly.instantiateStreaming||ua(H)||\"function\"!=typeof fetch?c(b):fetch(H,{credentials:\"same-origin\"}).then(function(e){return WebAssembly.instantiateStreaming(e,d).then(b,function(f){v(\"wasm streaming compile failed: \"+f);v(\"falling back to ArrayBuffer instantiation\");return c(b)})})})().catch(ca);return{}})();\n+h.___wasm_call_ctors=function(){return(h.___wasm_call_ctors=h.asm.F).apply(null,arguments)};var Ka=h.___getTypeName=function(){return(Ka=h.___getTypeName=h.asm.G).apply(null,arguments)};h.__embind_initialize_bindings=function(){return(h.__embind_initialize_bindings=h.asm.H).apply(null,arguments)};var Ib=h._malloc=function(){return(Ib=h._malloc=h.asm.I).apply(null,arguments)},S=h._free=function(){return(S=h._free=h.asm.K).apply(null,arguments)};\n+h.dynCall_jiji=function(){return(h.dynCall_jiji=h.asm.L).apply(null,arguments)};var Kb;G=function Lb(){Kb||Mb();Kb||(G=Lb)};\n+function Mb(){function a(){if(!Kb&&(Kb=!0,h.calledRun=!0,!ha)){za(qa);aa(h);if(h.onRuntimeInitialized)h.onRuntimeInitialized();if(h.postRun)for(\"function\"==typeof h.postRun&&(h.postRun=[h.postRun]);h.postRun.length;){var b=h.postRun.shift();ra.unshift(b)}za(ra)}}if(!(0<F)){if(h.preRun)for(\"function\"==typeof h.preRun&&(h.preRun=[h.preRun]);h.preRun.length;)sa();za(pa);0<F||(h.setStatus?(h.setStatus(\"Running...\"),setTimeout(function(){setTimeout(function(){h.setStatus(\"\")},1);a()},1)):a())}}\n+if(h.preInit)for(\"function\"==typeof h.preInit&&(h.preInit=[h.preInit]);0<h.preInit.length;)h.preInit.pop()();Mb();\n+\n+  return loadYoga.ready\n+}\ndiff --git a/dist/src/load.js b/dist/src/load.js\nindex efd1554faeebc6d6d792878f0ec63e4122ff3d96..232f122ef8fb2ba1c43844f88114ba46de2d5245 100644\n--- a/dist/src/load.js\n+++ b/dist/src/load.js\n@@ -7,11 +7,14 @@\n  * @format\n  */\n \n-// @ts-ignore untyped from Emscripten\n-import loadYogaImpl from '../binaries/yoga-wasm-base64-esm.js';\n import wrapAssembly from \"./wrapAssembly.js\";\n-export async function loadYoga() {\n-  return wrapAssembly(await loadYogaImpl());\n+export async function loadYoga(wasmOptions) {\n+  const { default: loadYogaImpl } =\n+    process.env.SATORI_STANDALONE === '1'\n+      ? await import('../binaries/yoga-wasm-esm.js')\n+      : await import('../binaries/yoga-wasm-base64-esm.js')\n+\n+  return wrapAssembly(await loadYogaImpl(wasmOptions));\n }\n export * from \"./generated/YGEnums.js\";\n //# sourceMappingURL=load.js.map\n"
  },
  {
    "path": "playground/LICENSE",
    "content": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in\n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "playground/cards/playground-data.ts",
    "content": "export type Tabs = {\n  [x: string]: string\n}\n\nconst playgroundTabs: Tabs = {\n  helloworld: `<div\n  style={{\n    height: '100%',\n    width: '100%',\n    display: 'flex',\n    flexDirection: 'column',\n    alignItems: 'center',\n    justifyContent: 'center',\n    backgroundColor: '#fff',\n    fontSize: 32,\n    fontWeight: 600,\n  }}\n>\n  <svg\n    width=\"75\"\n    viewBox=\"0 0 75 65\"\n    fill=\"#000\"\n    style={{ margin: '0 75px' }}\n  >\n    <path d=\"M37.59.25l36.95 64H.64l36.95-64z\"></path>\n  </svg>\n  <div style={{ marginTop: 40 }}>Hello, World</div>\n</div>\n`,\n  Vercel: `<div\n  style={{\n    height: '100%',\n    width: '100%',\n    display: 'flex',\n    textAlign: 'center',\n    alignItems: 'center',\n    justifyContent: 'center',\n    flexDirection: 'column',\n    flexWrap: 'nowrap',\n    backgroundColor: 'white',\n    backgroundImage: 'radial-gradient(circle at 25px 25px, lightgray 2%, transparent 0%), radial-gradient(circle at 75px 75px, lightgray 2%, transparent 0%)',\n    backgroundSize: '100px 100px',\n  }}\n>\n  <div\n    style={{\n      display: 'flex',\n      alignItems: 'center',\n      justifyContent: 'center',\n    }}\n  >\n    <svg\n      height={80}\n      viewBox=\"0 0 75 65\"\n      fill=\"black\"\n      style={{ margin: '0 75px' }}\n    >\n      <path d=\"M37.59.25l36.95 64H.64l36.95-64z\"></path>\n    </svg>\n  </div>\n  <div\n    style={{\n      display: 'flex',\n      fontSize: 40,\n      fontStyle: 'normal',\n      color: 'black',\n      marginTop: 30,\n      lineHeight: 1.8,\n      whiteSpace: 'pre-wrap',\n    }}\n  >\n    <b>Vercel Edge Network</b>\n  </div>\n</div>\n`,\n  rauchg: `<div\n  style={{\n    display: 'flex',\n    height: '100%',\n    width: '100%',\n    alignItems: 'center',\n    justifyContent: 'center',\n    letterSpacing: '-.02em',\n    fontWeight: 700,\n    background: 'white',\n  }}\n>\n  <div\n    style={{\n      left: 42,\n      top: 42,\n      position: 'absolute',\n      display: 'flex',\n      alignItems: 'center',\n    }}\n  >\n    <span\n      style={{\n        width: 24,\n        height: 24,\n        background: 'black',\n      }}\n    />\n    <span\n      style={{\n        marginLeft: 8,\n        fontSize: 20,\n      }}\n    >\n      rauchg.com\n    </span>\n  </div>\n  <div\n    style={{\n      display: 'flex',\n      flexWrap: 'wrap',\n      justifyContent: 'center',\n      padding: '20px 50px',\n      margin: '0 42px',\n      fontSize: 40,\n      width: 'auto',\n      maxWidth: 550,\n      textAlign: 'center',\n      backgroundColor: 'black',\n      color: 'white',\n      lineHeight: 1.4,\n    }}\n  >\n    Making the Web. Faster.\n  </div>\n</div>\n`,\n  'Tailwind (experimental)': `// Modified based on https://tailwindui.com/components/marketing/sections/cta-sections\n\n<div tw=\"flex flex-col w-full h-full items-center justify-center bg-white\">\n  <div tw=\"bg-gray-50 flex w-full\">\n    <div tw=\"flex flex-col md:flex-row w-full py-12 px-4 md:items-center justify-between p-8\">\n      <h2 tw=\"flex flex-col text-3xl sm:text-4xl font-bold tracking-tight text-gray-900 text-left\">\n        <span>Ready to dive in?</span>\n        <span tw=\"text-indigo-600\">Start your free trial today.</span>\n      </h2>\n      <div tw=\"mt-8 flex md:mt-0\">\n        <div tw=\"flex rounded-md shadow\">\n          <a tw=\"flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-5 py-3 text-base font-medium text-white\">Get started</a>\n        </div>\n        <div tw=\"ml-3 flex rounded-md shadow\">\n          <a tw=\"flex items-center justify-center rounded-md border border-transparent bg-white px-5 py-3 text-base font-medium text-indigo-600\">Learn more</a>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>`,\n  Gradients: `<div\n  style={{\n    display: 'flex',\n    height: '100%',\n    width: '100%',\n    alignItems: 'center',\n    justifyContent: 'center',\n    flexDirection: 'column',\n    backgroundImage: 'linear-gradient(to bottom, #dbf4ff, #fff1f1)',\n    fontSize: 60,\n    letterSpacing: -2,\n    fontWeight: 700,\n    textAlign: 'center',\n  }}\n  >\n  <div\n    style={{\n      backgroundImage: 'linear-gradient(90deg, rgb(0, 124, 240), rgb(0, 223, 216))',\n      backgroundClip: 'text',\n      '-webkit-background-clip': 'text',\n      color: 'transparent',\n    }}\n  >\n    Develop\n  </div>\n  <div\n    style={{\n      backgroundImage: 'linear-gradient(90deg, rgb(121, 40, 202), rgb(255, 0, 128))',\n      backgroundClip: 'text',\n      '-webkit-background-clip': 'text',\n      color: 'transparent',\n    }}\n  >\n    Preview\n  </div>\n  <div\n    style={{\n      backgroundImage: 'linear-gradient(90deg, rgb(255, 77, 77), rgb(249, 203, 40))',\n      backgroundClip: 'text',\n      '-webkit-background-clip': 'text',\n      color: 'transparent',\n    }}\n  >\n    Ship\n  </div>\n</div>\n`,\n  'Color Models': `<div\n  style={{\n    display: 'flex',\n    justifyContent: 'space-between',\n    alignItems: 'flex-start',\n    fontSize: 24,\n    fontWeight: 600,\n    textAlign: 'left',\n    padding: 70,\n    color: 'red',\n    backgroundImage: 'linear-gradient(to right, #334d50, #cbcaa5)',\n    height: '100%',\n    width: '100%'\n  }}\n>\n\n  <div style={{ display: 'flex', flexDirection: 'column' }}>\n    <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '15px', color: '#fff' }}>\n      #fff\n      <div style={{ fontWeight: 100 }}>hexadecimal</div>\n    </div>\n    <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '15px', color: '#ffffff70' }}>\n      #ffffff70\n      <div style={{ fontWeight: 100 }}>hexadecimal + transparency</div>\n    </div>\n    <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '15px', color: 'rgb(45, 45, 45)' }}>\n      rgb(45, 45, 45)\n      <div style={{ fontWeight: 100 }}>rgb</div>\n    </div>\n    <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '15px', color: 'rgb(45, 45, 45, 0.3)' }}>\n      rgb(45, 45, 45, 0.3)\n      <div style={{ fontWeight: 100 }}>rgba</div>\n    </div>\n  </div>\n\n  <div style={{ display: 'flex', flexDirection: 'column' }}>\n    <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '15px', color: 'hsl(186, 22%, 26%)' }}>\n      hsl(186, 22%, 26%)\n      <div style={{ fontWeight: 100 }}>hsl</div>\n    </div>\n    <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '15px', color: 'hsla(186, 22%, 26%, 40%)' }}>\n      hsla(186, 22%, 26%, 40%)\n      <div style={{ fontWeight: 100 }}>hsla</div>\n    </div>\n    <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '15px', color: 'white' }}>\n      \"white\"\n      <div style={{ fontWeight: 100 }}>predefined color names</div>\n    </div>\n    <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '15px', color: 'currentcolor' }}>\n      should be red\n      <div style={{ fontWeight: 100 }}>\"currentcolor\"</div>\n    </div>\n  </div>\n</div>`,\n  Advanced: `// Fallback fonts and Emoji are dynamically loaded\n// from Google Fonts and CDNs in this demo.\n\n// You can also return a function component in the playground.\n() => {\n  function Label({ children }) {\n    return <label style={{\n      fontSize: 15,\n      fontWeight: 600,\n      textTransform: 'uppercase',\n      letterSpacing: 1,\n      margin: '25px 0 10px',\n      color: 'gray',\n    }}>\n      {children}\n    </label>\n  }\n\n  return (\n    <div\n      style={{\n        display: 'flex',\n        flexDirection: 'column',\n        height: '100%',\n        width: '100%',\n        padding: '10px 20px',\n        justifyContent: 'center',\n        fontFamily: 'Inter, \"Material Icons\"',\n        fontSize: 28,\n        backgroundColor: 'white',\n      }}\n      >\n      <Label>Language & Font subsets</Label>\n      <div>\n        Hello! 你好! 안녕! こんにちは! Χαίρετε! Hallå!\n      </div>\n      <Label>Emoji</Label>\n      <div>\n        👋 😄 🎉 🎄 🦋\n      </div>\n      <Label>Icon font</Label>\n      <div>\n          &#xe766; &#xeb9b; &#xf089;\n      </div>\n      <Label>Lang attribute</Label>\n      <div style={{ display: 'flex' }}>\n        <span lang=\"ja-JP\">\n          骨茶\n        </span>/\n        <span lang=\"zh-CN\">\n          骨茶\n        </span>/\n        <span lang=\"zh-TW\">\n          骨茶\n        </span>/\n        <span lang=\"zh-HK\">\n          骨茶\n        </span>\n      </div>\n    </div>\n  )\n}  \n`,\n}\n\nexport default playgroundTabs\n"
  },
  {
    "path": "playground/cards/preview-tabs.ts",
    "content": "const previewTabs = [\n  'SVG (Satori)',\n  'PNG (Satori + resvg-js)', // https://github.com/yisibl/resvg-js\n  'PDF (Satori + PDFKit)',\n  'HTML (Native)',\n]\n\nexport default previewTabs\n"
  },
  {
    "path": "playground/components/introduction.module.css",
    "content": ".container {\n  position: fixed;\n  left: 20px;\n  bottom: 20px;\n  width: 700px;\n  min-height: 200px;\n  max-width: calc(100vw - 40px);\n  max-height: calc(100vh - 40px);\n  margin: auto;\n  border-radius: 8px;\n  display: flex;\n  flex-direction: column;\n  justify-content: flex-start;\n  padding: 20px;\n  color: #ddd;\n  font-size: 14px;\n  backdrop-filter: blur(24px);\n  background-color: rgb(0 0 0 / 88%);\n  box-shadow: 0 20px 40px #0000005c, 0 0 0 1px #868686;\n  z-index: 4;\n  opacity: 0;\n  line-height: 1.6;\n  letter-spacing: -0.01rem;\n  word-spacing: -0.12rem;\n  animation: fadein 0.4s ease 0.4s forwards;\n}\n\n.container p {\n  margin: 0;\n  margin-bottom: 1em;\n}\n\n.container code {\n  background-color: #4a4a4a;\n  padding: 0 4px;\n  border-radius: 4px;\n}\n\n.container button {\n  appearance: none;\n  border: none;\n  background: white;\n  border-radius: 5px;\n  padding: 8px 12px;\n  width: 120px;\n  align-self: flex-start;\n  font-family: inherit;\n  font-weight: 600;\n  cursor: pointer;\n  transition: all 0.2s ease;\n}\n\n.container button:hover {\n  background: #ccc;\n}\n\n@keyframes fadein {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n"
  },
  {
    "path": "playground/components/introduction.tsx",
    "content": "import React from 'react'\nimport styles from './introduction.module.css'\n\ninterface IProps {\n  onClose: React.MouseEventHandler<HTMLButtonElement>\n}\n\nexport default function Introduction({ onClose }: IProps) {\n  return (\n    <div className={styles.container}>\n      <p>👋 Welcome to the Vercel OG Image playground!</p>\n      <p style={{ flex: 1 }}>\n        You can use this tool to test and preview OG image cards generated with{' '}\n        <code>@vercel/og</code>. To learn more about how to add it to your\n        project, please read{' '}\n        <a\n          href='https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation'\n          target='_blank'\n          rel='noreferrer'\n        >\n          our documentation\n        </a>{' '}\n        or the{' '}\n        <a\n          href='https://vercel.com/blog/introducing-vercel-og-image-generation-fast-dynamic-social-card-images'\n          target='_blank'\n          rel='noreferrer'\n        >\n          announcement post\n        </a>\n        .\n      </p>\n      <button onClick={onClose}>Okay!</button>\n    </div>\n  )\n}\n"
  },
  {
    "path": "playground/components/panel-resize-handle.module.css",
    "content": ".handle {\n  flex: 0 0 1.5em;\n  position: relative;\n  outline: none;\n  --background-color: #efefef;\n}\n.handle:hover {\n  --background-color: #dbdbdb;\n}\n.handle[data-resize-handle-active] {\n  --background-color: #f5f5f5;\n}\n\n.handle > div {\n  position: absolute;\n  top: 0.25em;\n  bottom: 0.25em;\n  left: 0.25em;\n  right: 0.25em;\n  background-color: var(--background-color);\n  border: 1px solid #0000000f;\n  border-radius: 4px;\n  transition: background-color 0.2s linear;\n}\n\n.handle[data-panel-group-direction='horizontal'] > div {\n  top: 0;\n  bottom: 0;\n}\n\n.handle[data-panel-group-direction='vertical'] > div {\n  left: 0;\n  right: 0;\n}\n\n.handle svg {\n  width: 1em;\n  height: 1em;\n  position: absolute;\n  left: calc(50% - 0.5rem);\n  top: calc(50% - 0.5rem);\n}\n\n.handle[data-panel-group-direction='horizontal'] svg {\n  transform: rotate(90deg);\n}\n"
  },
  {
    "path": "playground/components/panel-resize-handle.tsx",
    "content": "import React from 'react'\nimport { PanelResizeHandle as PanelResizeHandleImpl } from 'react-resizable-panels'\n\nimport styles from './panel-resize-handle.module.css'\n\nexport default function PanelResizeHandle() {\n  return (\n    <PanelResizeHandleImpl className={styles.handle}>\n      <div>\n        <svg\n          xmlns='http://www.w3.org/2000/svg'\n          fill='none'\n          viewBox='0 0 24 24'\n          strokeWidth={1.5}\n          stroke='#adadad'\n        >\n          <path\n            strokeLinecap='round'\n            strokeLinejoin='round'\n            d='M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5'\n          />\n        </svg>\n      </div>\n    </PanelResizeHandleImpl>\n  )\n}\n"
  },
  {
    "path": "playground/components/resvg_worker.ts",
    "content": "import * as resvg from '@resvg/resvg-wasm'\n\nconst wasmPath = new URL('@resvg/resvg-wasm/index_bg.wasm', import.meta.url)\nfetch(wasmPath).then((res) => resvg.initWasm(res))\n\nself.onmessage = (e) => {\n  const { svg, width, _id } = e.data\n\n  const renderer = new resvg.Resvg(svg, {\n    fitTo: {\n      mode: 'width',\n      value: width,\n    },\n  })\n  const image = renderer.render()\n  const pngBuffer = image.asPng()\n  const url = URL.createObjectURL(new Blob([pngBuffer], { type: 'image/png' }))\n  self.postMessage({ _id, url })\n}\n"
  },
  {
    "path": "playground/decs.d.ts",
    "content": "declare module 'pdfkit/js/pdfkit.standalone'\ndeclare module 'satori'\n"
  },
  {
    "path": "playground/index.d.ts",
    "content": "export {}\n\ndeclare global {\n  interface Window {\n    __resource: any\n  }\n}\n"
  },
  {
    "path": "playground/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/basic-features/typescript for more information.\n"
  },
  {
    "path": "playground/package.json",
    "content": "{\n  \"private\": true,\n  \"name\": \"satori-playground\",\n  \"license\": \"MPL-2.0\",\n  \"scripts\": {\n    \"dev\": \"next\",\n    \"debug\": \"node --inspect node_modules/next/dist/bin/next\",\n    \"build\": \"next build\",\n    \"start\": \"next start\"\n  },\n  \"dependencies\": {\n    \"@babel/runtime\": \"^7.19.0\",\n    \"@monaco-editor/react\": \"^4.4.5\",\n    \"@resvg/resvg-wasm\": \"^2.3.1\",\n    \"blob-stream\": \"^0.1.3\",\n    \"copy-to-clipboard\": \"^3.3.2\",\n    \"fflate\": \"^0.7.3\",\n    \"intl-segmenter-polyfill\": \"^0.4.4\",\n    \"js-base64\": \"^3.7.2\",\n    \"next\": \"^12.2.5\",\n    \"pdfkit\": \"^0.13.0\",\n    \"react\": \"^17.0.2\",\n    \"react-dom\": \"^17.0.2\",\n    \"react-hot-toast\": \"^2.3.0\",\n    \"react-live\": \"^2.4.1\",\n    \"react-resizable-panels\": \"^0.0.30\",\n    \"satori\": \"workspace:*\",\n    \"svg-to-pdfkit\": \"^0.1.8\"\n  },\n  \"devDependencies\": {\n    \"@types/blob-stream\": \"^0.1.30\",\n    \"@types/pdfkit\": \"^0.12.7\",\n    \"@types/react-dom\": \"^18.0.6\",\n    \"@types/svg-to-pdfkit\": \"^0.1.0\",\n    \"regenerator\": \"link:@babel/runtime/regenerator\"\n  }\n}\n"
  },
  {
    "path": "playground/pages/_app.tsx",
    "content": "import React from 'react'\nimport Head from 'next/head'\nimport { AppProps } from 'next/app'\n\nimport '../styles.css'\n\nexport default function App({ Component, pageProps }: AppProps) {\n  return (\n    <>\n      <Head>\n        <title>Vercel OG Image Playground</title>\n        <meta\n          name='viewport'\n          content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'\n        />\n        <meta name='theme-color' content='#fff' />\n        <meta name='title' content='Vercel OG Image Playground' />\n        <meta\n          name='description'\n          content='Generate Open Graph images with Vercel’s Edge Function.'\n        />\n        <meta property='og:type' content='website' />\n        <meta property='og:url' content='https://og-playground.vercel.app/' />\n        <meta property='og:title' content='Vercel OG Image Playground' />\n        <meta\n          property='og:description'\n          content='Generate Open Graph images with Vercel’s Edge Function.'\n        />\n        <meta\n          property='og:image'\n          content='https://og-playground.vercel.app/og.png'\n        />\n        <meta property='twitter:card' content='summary_large_image' />\n        <meta\n          property='twitter:url'\n          content='https://og-playground.vercel.app/'\n        />\n        <meta property='twitter:title' content='Vercel OG Image Playground' />\n        <meta\n          property='twitter:description'\n          content='Generate Open Graph images with Vercel’s Edge Function.'\n        />\n        <meta\n          property='twitter:image'\n          content='https://og-playground.vercel.app/og.png'\n        />\n        <link\n          rel='preload'\n          href='/inter-latin-ext-400-normal.woff'\n          as='fetch'\n          crossOrigin='anonymous'\n        />\n        <link\n          rel='preload'\n          href='/inter-latin-ext-700-normal.woff'\n          as='fetch'\n          crossOrigin='anonymous'\n        />\n        <link\n          rel='preload'\n          href='/material-icons-base-400-normal.woff'\n          as='fetch'\n          crossOrigin='anonymous'\n        />\n        <link\n          rel='preload'\n          href='/iaw-mono-var.woff2'\n          as='fetch'\n          crossOrigin='anonymous'\n        />\n        <link rel='shortcut icon' href='/favicon.ico' type='image/x-icon' />\n        <link rel='icon' href='/favicon.ico' type='image/x-icon' />\n      </Head>\n      <Component {...pageProps} />\n    </>\n  )\n}\n"
  },
  {
    "path": "playground/pages/_document.tsx",
    "content": "import React from 'react'\nimport { Html, Head, Main, NextScript } from 'next/document'\n\nexport default function Document() {\n  return (\n    <Html lang='en'>\n      <Head />\n      <body>\n        <Main />\n        <NextScript />\n      </body>\n    </Html>\n  )\n}\n"
  },
  {
    "path": "playground/pages/api/font.ts",
    "content": "import type { NextRequest } from 'next/server'\nimport { FontDetector, languageFontMap } from '../../utils/font'\n\nexport const config = {\n  runtime: 'experimental-edge',\n}\n\nconst detector = new FontDetector()\n\n// Our own encoding of multiple fonts and their code, so we can fetch them in one request. The structure is:\n// [1 byte = X, length of language code][X bytes of language code string][4 bytes = Y, length of font][Y bytes of font data]\n// Note that:\n// - The language code can't be longer than 255 characters.\n// - The language code can't contain non-ASCII characters.\n// - The font data can't be longer than 4GB.\n// When there are multiple fonts, they are concatenated together.\nfunction encodeFontInfoAsArrayBuffer(code: string, fontData: ArrayBuffer) {\n  // 1 byte per char\n  const buffer = new ArrayBuffer(1 + code.length + 4 + fontData.byteLength)\n  const bufferView = new Uint8Array(buffer)\n  // 1 byte for the length of the language code\n  bufferView[0] = code.length\n  // X bytes for the language code\n  for (let i = 0; i < code.length; i++) {\n    bufferView[i + 1] = code.charCodeAt(i)\n  }\n\n  // 4 bytes for the length of the font data\n  new DataView(buffer).setUint32(1 + code.length, fontData.byteLength, false)\n\n  // Y bytes for the font data\n  bufferView.set(new Uint8Array(fontData), 1 + code.length + 4)\n\n  return buffer\n}\n\nexport default async function loadGoogleFont(req: NextRequest) {\n  if (req.nextUrl.pathname !== '/api/font') return\n\n  const { searchParams } = new URL(req.url)\n\n  const fonts = searchParams.getAll('fonts')\n  const text = searchParams.get('text')\n\n  if (!fonts || fonts.length === 0 || !text) return\n\n  const textByFont = await detector.detect(text, fonts)\n\n  const _fonts = Object.keys(textByFont)\n\n  const encodedFontBuffers: ArrayBuffer[] = []\n  let fontBufferByteLength = 0\n  ;(\n    await Promise.all(_fonts.map((font) => fetchFont(textByFont[font], font)))\n  ).forEach((fontData, i) => {\n    if (fontData) {\n      // TODO: We should be able to directly get the language code here :)\n      const langCode = Object.entries(languageFontMap).find(\n        ([, v]) => v === _fonts[i]\n      )?.[0]\n\n      if (langCode) {\n        const buffer = encodeFontInfoAsArrayBuffer(langCode, fontData)\n        encodedFontBuffers.push(buffer)\n        fontBufferByteLength += buffer.byteLength\n      }\n    }\n  })\n\n  const responseBuffer = new ArrayBuffer(fontBufferByteLength)\n  const responseBufferView = new Uint8Array(responseBuffer)\n  let offset = 0\n  encodedFontBuffers.forEach((buffer) => {\n    responseBufferView.set(new Uint8Array(buffer), offset)\n    offset += buffer.byteLength\n  })\n\n  return new Response(responseBuffer, {\n    headers: {\n      'Content-Type': 'font/woff',\n      'Cache-Control': 'public, max-age=31536000, immutable',\n    },\n  })\n}\n\nasync function fetchFont(\n  text: string,\n  font: string\n): Promise<ArrayBuffer | null> {\n  const API = `https://fonts.googleapis.com/css2?family=${font}&text=${encodeURIComponent(\n    text\n  )}`\n\n  const css = await (\n    await fetch(API, {\n      headers: {\n        // Make sure it returns TTF.\n        'User-Agent':\n          'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1',\n      },\n    })\n  ).text()\n\n  const resource = css.match(/src: url\\((.+)\\) format\\('(opentype|truetype)'\\)/)\n\n  if (!resource) return null\n\n  const res = await fetch(resource[1])\n\n  return res.arrayBuffer()\n}\n"
  },
  {
    "path": "playground/pages/index.tsx",
    "content": "import React from 'react'\nimport satori from 'satori'\nimport { LiveProvider, LiveContext, withLive } from 'react-live'\nimport { useEffect, useState, useRef, useContext, useCallback } from 'react'\nimport { createPortal } from 'react-dom'\nimport Editor, { useMonaco } from '@monaco-editor/react'\nimport toast, { Toaster } from 'react-hot-toast'\nimport copy from 'copy-to-clipboard'\nimport packageJson from 'satori/package.json'\nimport * as fflate from 'fflate'\nimport { Base64 } from 'js-base64'\nimport PDFDocument from 'pdfkit/js/pdfkit.standalone'\nimport SVGtoPDF from 'svg-to-pdfkit'\nimport blobStream from 'blob-stream'\nimport { createIntlSegmenterPolyfill } from 'intl-segmenter-polyfill'\nimport { Panel, PanelGroup } from 'react-resizable-panels'\n\nimport { loadEmoji, getIconCode, apis } from '../utils/twemoji'\nimport Introduction from '../components/introduction'\nimport PanelResizeHandle from '../components/panel-resize-handle'\nimport { languageFontMap } from '../utils/font'\n\nimport playgroundTabs, { Tabs } from '../cards/playground-data'\nimport previewTabs from '../cards/preview-tabs'\n\nconst cardNames = Object.keys(playgroundTabs)\nconst editedCards: Tabs = { ...playgroundTabs }\n\nasync function init() {\n  if (typeof window === 'undefined') return []\n\n  const [font, fontBold, fontIcon, Segmenter] =\n    window.__resource ||\n    (window.__resource = await Promise.all([\n      fetch('/inter-latin-ext-400-normal.woff').then((res) =>\n        res.arrayBuffer()\n      ),\n      fetch('/inter-latin-ext-700-normal.woff').then((res) =>\n        res.arrayBuffer()\n      ),\n      fetch('/material-icons-base-400-normal.woff').then((res) =>\n        res.arrayBuffer()\n      ),\n      !globalThis.Intl || !globalThis.Intl.Segmenter\n        ? createIntlSegmenterPolyfill(\n            fetch(\n              new URL(\n                'intl-segmenter-polyfill/dist/break_iterator.wasm',\n                import.meta.url\n              )\n            )\n          )\n        : null,\n    ]))\n\n  if (Segmenter) {\n    globalThis.Intl = globalThis.Intl || {}\n    //@ts-expect-error\n    globalThis.Intl.Segmenter = Segmenter\n  }\n\n  return [\n    {\n      name: 'Inter',\n      data: font,\n      weight: 400,\n      style: 'normal',\n    },\n    {\n      name: 'Inter',\n      data: fontBold,\n      weight: 700,\n      style: 'normal',\n    },\n    {\n      name: 'Material Icons',\n      data: fontIcon,\n      weight: 400,\n      style: 'normal',\n    },\n  ]\n}\n\nfunction withCache(fn: Function) {\n  const cache = new Map()\n  return async (...args: string[]) => {\n    const key = args.join(':')\n    if (cache.has(key)) return cache.get(key)\n    const result = await fn(...args)\n    cache.set(key, result)\n    return result\n  }\n}\n\ntype LanguageCode = keyof typeof languageFontMap | 'emoji'\n\nconst loadDynamicAsset = withCache(\n  async (emojiType: keyof typeof apis, _code: string, text: string) => {\n    if (_code === 'emoji') {\n      // It's an emoji, load the image.\n      return (\n        `data:image/svg+xml;base64,` +\n        btoa(await loadEmoji(emojiType, getIconCode(text)))\n      )\n    }\n\n    const codes = _code.split('|')\n\n    // Try to load from Google Fonts.\n    const names = codes\n      .map((code) => languageFontMap[code as keyof typeof languageFontMap])\n      .filter(Boolean)\n\n    if (names.length === 0) return []\n\n    const params = new URLSearchParams()\n    for (const name of names.flat()) {\n      params.append('fonts', name)\n    }\n    params.set('text', text)\n\n    try {\n      const response = await fetch(`/api/font?${params.toString()}`)\n\n      if (response.status === 200) {\n        const data = await response.arrayBuffer()\n        const fonts: any[] = []\n\n        // Decode the encoded font format.\n        const decodeFontInfoFromArrayBuffer = (buffer: ArrayBuffer) => {\n          let offset = 0\n          const bufferView = new Uint8Array(buffer)\n\n          while (offset < bufferView.length) {\n            // 1 byte for font name length.\n            const languageCodeLength = bufferView[offset]\n            offset += 1\n            let languageCode = ''\n            for (let i = 0; i < languageCodeLength; i++) {\n              languageCode += String.fromCharCode(bufferView[offset + i])\n            }\n            offset += languageCodeLength\n\n            // 4 bytes for font data length.\n            const fontDataLength = new DataView(buffer).getUint32(offset, false)\n            offset += 4\n            const fontData = buffer.slice(offset, offset + fontDataLength)\n            offset += fontDataLength\n\n            fonts.push({\n              name: `satori_${languageCode}_fallback_${text}`,\n              data: fontData,\n              weight: 400,\n              style: 'normal',\n              lang: languageCode === 'unknown' ? undefined : languageCode,\n            })\n          }\n        }\n\n        decodeFontInfoFromArrayBuffer(data)\n\n        return fonts\n      }\n    } catch (e) {\n      console.error('Failed to load dynamic font for', text, '. Error:', e)\n    }\n  }\n)\n\n// https://raw.githubusercontent.com/n3r4zzurr0/svg-spinners/main/svg/90-ring.svg\nconst spinner = (\n  <svg\n    width='24'\n    height='24'\n    viewBox='0 0 24 24'\n    xmlns='http://www.w3.org/2000/svg'\n    style={{\n      position: 'absolute',\n      top: 0,\n      bottom: 0,\n      left: 0,\n      right: 0,\n      margin: 'auto',\n      fill: 'white',\n      zIndex: 1,\n    }}\n  >\n    <path d='M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z'>\n      <animateTransform\n        attributeName='transform'\n        type='rotate'\n        dur='0.75s'\n        values='0 12 12;360 12 12'\n        repeatCount='indefinite'\n      />\n    </path>\n  </svg>\n)\n\nfunction initResvgWorker() {\n  if (typeof window === 'undefined') return\n\n  const worker = new Worker(\n    new URL('../components/resvg_worker.ts', import.meta.url)\n  )\n\n  const pending = new Map()\n  worker.onmessage = (e) => {\n    const { _id, url } = e.data\n    const resolve = pending.get(_id)\n    if (resolve) {\n      resolve(url)\n      pending.delete(_id)\n    }\n  }\n\n  return async (msg: object) => {\n    const _id = Math.random()\n    worker.postMessage({\n      ...msg,\n      _id,\n    })\n    return new Promise((resolve) => {\n      pending.set(_id, resolve)\n    })\n  }\n}\n\nconst loadFonts = init()\nconst renderPNG = initResvgWorker()\n\ninterface ITabs {\n  options: string[]\n  onChange: (value: string) => void\n  children: React.ReactNode\n}\n\nfunction Tabs({ options, onChange, children }: ITabs) {\n  const [active, setActive] = useState(options[0])\n\n  return (\n    <div className='tabs'>\n      <div className='tabs-container'>\n        {options.map((option) => (\n          <div\n            title={option}\n            className={'tab' + (active === option ? ' active' : '')}\n            key={option}\n            onClick={() => {\n              setActive(option)\n              onChange(option)\n            }}\n          >\n            {option}\n          </div>\n        ))}\n      </div>\n      {children}\n    </div>\n  )\n}\n\nfunction LiveEditor({ id }: { id: string }) {\n  const { onChange } = useContext(LiveContext) as unknown as {\n    onChange: (val: string) => void\n  }\n\n  const monaco = useMonaco()\n  useEffect(() => {\n    if (monaco) {\n      monaco.editor.defineTheme('IDLE', {\n        base: 'vs',\n        inherit: false,\n        rules: [\n          {\n            background: 'FFFFFF',\n            token: '',\n          },\n          {\n            token: 'delimiter',\n            foreground: '999999',\n          },\n          {\n            token: 'aaa',\n            foreground: '00ff00',\n          },\n          {\n            foreground: '919191',\n            token: 'comment',\n          },\n          {\n            foreground: '00a33f',\n            token: 'string',\n          },\n          {\n            foreground: '3b54bf',\n            token: 'number',\n          },\n          {\n            foreground: 'a535ae',\n            token: 'constant.language',\n          },\n          {\n            foreground: 'ff5600',\n            token: 'keyword',\n          },\n          {\n            foreground: 'ff5600',\n            token: 'storage',\n          },\n          {\n            foreground: '21439c',\n            token: 'entity.name.type',\n          },\n          {\n            foreground: '21439c',\n            token: 'entity.name.function',\n          },\n          {\n            foreground: 'a535ae',\n            token: 'support.function',\n          },\n          {\n            foreground: 'a535ae',\n            token: 'support.constant',\n          },\n          {\n            foreground: 'a535ae',\n            token: 'support.type',\n          },\n          {\n            foreground: 'a535ae',\n            token: 'support.class',\n          },\n          {\n            foreground: 'a535ae',\n            token: 'support.variable',\n          },\n          {\n            foreground: '000000',\n            background: '990000',\n            token: 'invalid',\n          },\n          {\n            foreground: '990000',\n            token: 'constant.other.placeholder.py',\n          },\n        ],\n        colors: {\n          'editor.foreground': '#000000',\n          'editor.background': '#FFFFFF',\n          'editor.selectionBackground': '#BAD6FD',\n          'editor.lineHighlightBackground': '#00000012',\n          'editorCursor.foreground': '#000000',\n          'editorWhitespace.foreground': '#BFBFBF',\n        },\n      })\n      monaco.editor.setTheme('IDLE')\n    }\n  }, [monaco])\n\n  const ref = useRef<HTMLDivElement>(null)\n\n  return (\n    <div ref={ref} style={{ height: '100%', position: 'relative' }}>\n      <div style={{ position: 'absolute' }}>\n        <Editor\n          height='100%'\n          theme='IDLE'\n          defaultLanguage='javascript'\n          value={editedCards[id]}\n          onChange={(newCode) => {\n            // We also update the code in memory so switching tabs will preserve the\n            // edited code (until refreshing).\n            editedCards[id] = newCode ?? ''\n            onChange(newCode ?? '')\n          }}\n          onMount={async (editor, _monaco) => {\n            if (ref.current) {\n              const relayout = ([e]: any) => {\n                editor.layout({\n                  width: e.borderBoxSize[0].inlineSize,\n                  height: e.borderBoxSize[0].blockSize,\n                })\n              }\n              const resizeObserver = new ResizeObserver(relayout)\n              resizeObserver.observe(ref.current)\n            }\n          }}\n          options={{\n            fontFamily: 'iaw-mono-var',\n            fontSize: 14,\n            wordWrap: 'on',\n            tabSize: 2,\n            minimap: {\n              enabled: false,\n            },\n            smoothScrolling: true,\n            cursorSmoothCaretAnimation: 'on',\n            contextmenu: false,\n            automaticLayout: true,\n          }}\n        />\n      </div>\n    </div>\n  )\n}\n\n// For sharing & resuming.\nconst currentOptions = {}\nlet overrideOptions: any = null\n\nconst LiveSatori = withLive(function ({\n  live,\n}: {\n  live?: { element: React.ComponentType; error: string }\n}) {\n  const [options, setOptions] = useState<object | null>(null)\n  const [debug, setDebug] = useState(false)\n  const [fontEmbed, setFontEmbed] = useState(true)\n  const [emojiType, setEmojiType] = useState('twemoji')\n  const [objectURL, setObjectURL] = useState<string>('')\n  const [renderType, setRenderType] = useState('svg')\n  const [renderError, setRenderError] = useState(null)\n  const [width, setWidth] = useState(400 * 2)\n  const [height, setHeight] = useState(200 * 2)\n  const [iframeNode, setIframeNode] = useState<HTMLElement | undefined>()\n  const previewContainerRef = useRef<HTMLDivElement>(null)\n  const [scaleRatio, setScaleRatio] = useState(1)\n  const [loadingResources, setLoadingResources] = useState(true)\n  const updateIframeRef = useCallback(\n    (node: HTMLIFrameElement) => {\n      if (node) {\n        if (node.contentWindow?.document) {\n          /* Force tailwindcss to create stylesheets on first render */\n          const forceUpdate = () => {\n            return setTimeout(() => {\n              const div = doc.createElement('div')\n              div.classList.add('hidden')\n              doc.body.appendChild(div)\n              setTimeout(() => {\n                doc.body.removeChild(div)\n              }, 300)\n            }, 200)\n          }\n          const doc = node.contentWindow.document\n          const script = doc.createElement('script')\n          script.src = 'https://cdn.tailwindcss.com'\n          doc.head.appendChild(script)\n          script.addEventListener('load', () => {\n            const configScript = doc.createElement('script')\n            configScript.text = `\n            tailwind.config = {\n              plugins: [{\n                handler({ addBase }) {\n                  addBase({\n                    'html': {\n                      'line-height': 1.2,\n                    }\n                  })\n                }\n              }]\n            }\n          `\n            doc.head.appendChild(configScript)\n          })\n          const updateClass = () => {\n            Array.from(doc.querySelectorAll('[tw]')).forEach((v) => {\n              const tw = v.getAttribute('tw')\n              if (tw) {\n                v.setAttribute('class', tw)\n                v.removeAttribute('tw')\n              }\n            })\n          }\n          forceUpdate()\n          const observer = new MutationObserver(updateClass)\n          observer.observe(doc.body, { childList: true, subtree: true })\n          setIframeNode(doc.body)\n        }\n      }\n    },\n    [setIframeNode]\n  ) // eslint-disable-line]\n  useEffect(() => {\n    if (overrideOptions) {\n      setWidth(Math.min(overrideOptions.width || 800, 2000))\n      setHeight(Math.min(overrideOptions.height || 800, 2000))\n      setDebug(!!overrideOptions.debug)\n      setEmojiType(overrideOptions.emojiType || 'twemoji')\n      setFontEmbed(!!overrideOptions.fontEmbed)\n    }\n  }, [overrideOptions])\n\n  const sizeRef = useRef([width, height])\n  sizeRef.current = [width, height]\n\n  function updateScaleRatio() {\n    if (!previewContainerRef.current) return\n\n    const [w, h] = sizeRef.current\n    const containerWidth = previewContainerRef.current.clientWidth\n    const containerHeight = previewContainerRef.current.clientHeight\n    setScaleRatio(\n      Math.min(1, Math.min(containerWidth / w, containerHeight / h))\n    )\n  }\n\n  useEffect(() => {\n    ;(async () => {\n      setOptions({\n        fonts: await loadFonts,\n      })\n      setLoadingResources(false)\n    })()\n  }, [])\n\n  useEffect(() => {\n    if (!previewContainerRef.current) return\n\n    const observer = new ResizeObserver(updateScaleRatio)\n    observer.observe(previewContainerRef.current)\n\n    return () => {\n      observer.disconnect()\n    }\n  }, [])\n\n  useEffect(() => {\n    updateScaleRatio()\n  }, [width, height])\n\n  const [result, setResult] = useState('')\n  const [renderedTimeSpent, setRenderTime] = useState<number>(0)\n\n  useEffect(() => {\n    let cancelled = false\n\n    ;(async () => {\n      // We leave a small buffer here to debounce if it's PNG.\n      if (renderType === 'png') {\n        await new Promise((resolve) => setTimeout(resolve, 15))\n      }\n      if (cancelled) return\n\n      let _result = ''\n      let _renderedTimeSpent = 0\n\n      if (live?.element && options) {\n        const start = (\n          typeof performance !== 'undefined' ? performance : Date\n        ).now()\n        if (renderType !== 'html') {\n          try {\n            _result = await satori(live.element.prototype.render(), {\n              ...options,\n              embedFont: fontEmbed,\n              width,\n              height,\n              debug,\n              loadAdditionalAsset: (code: string, text: string) =>\n                loadDynamicAsset(emojiType, code, text),\n            })\n            if (renderType === 'png') {\n              const url = (await renderPNG?.({\n                svg: _result,\n                width,\n              })) as string\n\n              if (!cancelled) {\n                setObjectURL(url)\n\n                // After rendering the PNG @1x quickly, we render the PNG @2x for\n                // the playground only to make it look less blurry.\n                // We only do that for images that are not too big (1200^2).\n                if (width * height <= 1440000) {\n                  setTimeout(async () => {\n                    if (cancelled) return\n                    const _url = (await renderPNG?.({\n                      svg: _result,\n                      width: width * 2,\n                    })) as string\n\n                    if (cancelled) return\n                    setObjectURL(_url)\n                  }, 20)\n                }\n              }\n            }\n            if (renderType === 'pdf') {\n              const doc = new PDFDocument({\n                compress: false,\n                size: [width, height],\n              })\n              SVGtoPDF(doc, _result, 0, 0, {\n                width,\n                height,\n                preserveAspectRatio: `xMidYMid meet`,\n              })\n              const stream = doc.pipe(blobStream())\n              stream.on('finish', () => {\n                const blob = stream.toBlob('application/pdf')\n                setObjectURL(URL.createObjectURL(blob))\n              })\n              doc.end()\n            }\n            setRenderError(null)\n          } catch (e: any) {\n            console.error(e)\n            setRenderError(e.message)\n            return null\n          }\n        } else {\n          setRenderError(null)\n        }\n        _renderedTimeSpent =\n          (typeof performance !== 'undefined' ? performance : Date).now() -\n          start\n      }\n\n      Object.assign(currentOptions, {\n        width,\n        height,\n        debug,\n        emojiType,\n        fontEmbed,\n      })\n      setResult(_result)\n      setRenderTime(_renderedTimeSpent)\n    })()\n\n    return () => {\n      cancelled = true\n    }\n  }, [\n    live?.element,\n    options,\n    width,\n    height,\n    debug,\n    emojiType,\n    fontEmbed,\n    renderType,\n  ])\n\n  return (\n    <>\n      <Panel>\n        <Tabs\n          options={previewTabs}\n          onChange={(text) => {\n            const _renderType = text.split(' ')[0].toLowerCase()\n            // 'svg' | 'png' | 'html' | 'pdf'\n            setRenderType(_renderType)\n          }}\n        >\n          <div className='preview-card'>\n            {live?.error || renderError ? (\n              <div className='error'>\n                <pre>{live?.error || renderError}</pre>\n              </div>\n            ) : null}\n            {loadingResources ? spinner : null}\n            <div\n              className='result-container'\n              ref={previewContainerRef}\n              dangerouslySetInnerHTML={\n                renderType !== 'svg'\n                  ? undefined\n                  : {\n                      __html: `<div class=\"content-wrapper\" style=\"position:absolute;width:100%;height:100%;max-width:${width}px;max-height:${height}px;display:flex;align-items:center;justify-content:center\">${result}</div>`,\n                    }\n              }\n            >\n              {renderType === 'html' ? (\n                <iframe\n                  key='html'\n                  ref={updateIframeRef}\n                  width={width}\n                  height={height}\n                  style={{\n                    transform: `scale(${scaleRatio})`,\n                  }}\n                >\n                  {iframeNode &&\n                    createPortal(\n                      <>\n                        <style\n                          dangerouslySetInnerHTML={{\n                            __html: `@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Material+Icons');body{display:flex;height:100%;margin:0;tab-size:8;font-family:Inter,sans-serif;overflow:hidden}body>div,body>div *{box-sizing:border-box;display:flex}`,\n                          }}\n                        />\n                        {live?.element ? <live.element /> : null}\n                      </>,\n                      iframeNode\n                    )}\n                </iframe>\n              ) : renderType === 'png' && objectURL ? (\n                <img\n                  src={objectURL}\n                  width={width}\n                  height={height}\n                  style={{\n                    maxHeight: '100%',\n                    maxWidth: '100%',\n                    objectFit: 'contain',\n                  }}\n                  alt='Preview'\n                />\n              ) : renderType === 'pdf' && objectURL ? (\n                <iframe\n                  key='pdf'\n                  width={width}\n                  height={height}\n                  src={\n                    objectURL +\n                    '#toolbar=0&navpanes=0&scrollbar=0&statusbar=0&messages=0&scrollbar=0'\n                  }\n                  style={{\n                    transform: `scale(${scaleRatio})`,\n                  }}\n                />\n              ) : null}\n            </div>\n            <footer>\n              <span className='ellipsis'>\n                {renderType === 'html'\n                  ? '[HTML] Rendered.'\n                  : `[${renderType.toUpperCase()}] Generated in `}\n              </span>\n              <span className='data'>\n                {renderType === 'html'\n                  ? ''\n                  : `${~~(renderedTimeSpent * 100) / 100}ms.`}\n                {renderType === 'pdf' || renderType === 'png' ? (\n                  <>\n                    {' '}\n                    <a href={objectURL ?? ''} target='_blank' rel='noreferrer'>\n                      (View in New Tab ↗)\n                    </a>\n                  </>\n                ) : (\n                  ''\n                )}\n              </span>\n              <span>{`[${width}×${height}]`}</span>\n            </footer>\n          </div>\n        </Tabs>\n      </Panel>\n      <PanelResizeHandle />\n      <Panel>\n        <div className='controller'>\n          <h2 className='title'>Configurations</h2>\n          <div className='content'>\n            <div className='control'>\n              <label htmlFor='width'>Container Width</label>\n              <div>\n                <input\n                  type='range'\n                  value={width}\n                  onChange={(e) => setWidth(Number(e.target.value))}\n                  min={100}\n                  max={1200}\n                  step={1}\n                />\n                <input\n                  id='width'\n                  type='number'\n                  value={width}\n                  onChange={(e) => setWidth(Number(e.target.value))}\n                  min={100}\n                  max={1200}\n                  step={1}\n                />\n                px\n              </div>\n            </div>\n            <div className='control'>\n              <label htmlFor='height'>Container Height</label>\n              <div>\n                <input\n                  type='range'\n                  value={height}\n                  onChange={(e) => setHeight(Number(e.target.value))}\n                  min={100}\n                  max={1200}\n                  step={1}\n                />\n                <input\n                  id='height'\n                  type='number'\n                  value={height}\n                  onChange={(e) => setHeight(Number(e.target.value))}\n                  min={100}\n                  max={1200}\n                  step={1}\n                />\n                px\n              </div>\n            </div>\n            <div className='control'>\n              <label htmlFor='reset'>Size</label>\n              <button\n                id='reset'\n                onClick={() => {\n                  setWidth(800)\n                  setHeight(400)\n                }}\n              >\n                Reset\n              </button>\n              <button\n                type='button'\n                onClick={() => {\n                  setWidth(1200)\n                  setHeight(600)\n                }}\n              >\n                2:1\n              </button>\n              <button\n                type='button'\n                onClick={() => {\n                  setWidth(1200)\n                  setHeight(630)\n                }}\n              >\n                1.9:1\n              </button>\n            </div>\n            <div className='control'>\n              <label htmlFor='debug'>Debug Mode</label>\n              <input\n                id='debug'\n                type='checkbox'\n                checked={debug}\n                onChange={() => setDebug(!debug)}\n              />\n            </div>\n            <div className='control'>\n              <label htmlFor='font'>Embed Font</label>\n              <input\n                id='font'\n                type='checkbox'\n                checked={fontEmbed}\n                onChange={() => setFontEmbed(!fontEmbed)}\n              />\n            </div>\n            <div className='control'>\n              <label htmlFor='emoji'>Emoji Provider</label>\n              <select\n                id='emoji'\n                onChange={(e) => setEmojiType(e.target.value)}\n                value={emojiType}\n              >\n                <option value='twemoji'>Twemoji</option>\n                <option value='fluent'>Fluent Emoji</option>\n                <option value='fluentFlat'>Fluent Emoji Flat</option>\n                <option value='noto'>Noto Emoji</option>\n                <option value='blobmoji'>Blobmoji</option>\n                <option value='openmoji'>OpenMoji</option>\n              </select>\n            </div>\n            <div className='control'>\n              <label htmlFor='export'>Export</label>\n              <a\n                className={!result || renderType === 'html' ? 'disabled' : ''}\n                href={\n                  result\n                    ? `data:image/svg+xml;charset=utf-8,${encodeURIComponent(\n                        result\n                      )}`\n                    : undefined\n                }\n                target={result ? '_blank' : ''}\n                download={result ? 'image.svg' : false}\n                rel='noreferrer'\n              >\n                Export SVG\n              </a>\n              <a\n                className={!result || renderType === 'html' ? 'disabled' : ''}\n                href='#'\n                onClick={(e) => {\n                  e.preventDefault()\n                  if (!result) return false\n                  window.open?.('')?.document.write(result)\n                }}\n              >\n                (View in New Tab ↗)\n              </a>\n            </div>\n            <div className='control'>\n              <label>Satori Version</label>\n              <a\n                href='https://github.com/vercel/satori'\n                target='_blank'\n                rel='noreferrer'\n              >\n                {packageJson.version}\n              </a>\n            </div>\n          </div>\n        </div>\n      </Panel>\n    </>\n  )\n})\n\nfunction ResetCode({ activeCard }: { activeCard: string }) {\n  const { onChange } = useContext(LiveContext) as unknown as {\n    onChange: (val: string) => void\n  }\n\n  useEffect(() => {\n    const params = new URL(String(document.location)).searchParams\n    const shared = params.get('share')\n    // we just need change editedCards on mounted\n    if (shared) {\n      try {\n        const decompressedData = fflate.strFromU8(\n          fflate.decompressSync(Base64.toUint8Array(shared))\n        )\n        let card\n        let tab\n        try {\n          const decoded = JSON.parse(decompressedData)\n          card = decoded.code\n          overrideOptions = decoded.options\n          tab = decoded.tab || 'helloworld'\n        } catch (e) {\n          card = decompressedData\n        }\n\n        editedCards[tab] = card\n        onChange(editedCards[tab])\n      } catch (e) {\n        console.error('Failed to parse shared card:', e)\n      }\n    }\n  }, [])\n\n  return (\n    <button\n      onClick={() => {\n        editedCards[activeCard] = playgroundTabs[activeCard]\n        onChange(editedCards[activeCard])\n        window.history.replaceState(null, '', '/')\n        toast.success('Content reset')\n      }}\n    >\n      Reset\n    </button>\n  )\n}\n\nexport default function Playground() {\n  const [activeCard, setActiveCard] = useState<string>('helloworld')\n  const [showIntroduction, setShowIntroduction] = useState(false)\n  const [isMobileView, setIsMobileView] = useState(false)\n\n  // set isMobileView to true if the screen is less than 600px wide\n  useEffect(() => {\n    const handleResize = () => {\n      setIsMobileView(window.innerWidth < 600)\n    }\n    handleResize()\n    window.addEventListener('resize', handleResize)\n    return () => window.removeEventListener('resize', handleResize)\n  }, [])\n\n  useEffect(() => {\n    try {\n      const hasVisited = localStorage.getItem('_vercel_og_playground_visited')\n      if (hasVisited) return\n    } catch (e) {\n      console.error(e)\n    }\n\n    setShowIntroduction(true)\n  }, [])\n\n  const [hydrated, setHydrated] = useState(false)\n  useEffect(() => {\n    setHydrated(true)\n  }, [])\n\n  const editorPanel = (\n    <Panel>\n      <Tabs\n        options={cardNames}\n        onChange={(name: string) => {\n          setActiveCard(name)\n        }}\n      >\n        <div className='editor'>\n          <div className='editor-controls'>\n            <ResetCode activeCard={activeCard} />\n            <button\n              onClick={() => {\n                const code = editedCards[activeCard]\n                const compressed = Base64.fromUint8Array(\n                  fflate.deflateSync(\n                    fflate.strToU8(\n                      JSON.stringify({\n                        code,\n                        options: currentOptions,\n                        tab: activeCard,\n                      })\n                    )\n                  ),\n                  true\n                )\n\n                window.history.replaceState(null, '', '?share=' + compressed)\n                copy(window.location.href)\n                toast.success('Copied to clipboard')\n              }}\n            >\n              Share\n            </button>\n          </div>\n          <div className='monaco-container'>\n            <LiveEditor key={activeCard} id={activeCard} />\n          </div>\n        </div>\n      </Tabs>\n    </Panel>\n  )\n\n  const previewPanel = (\n    <Panel>\n      <PanelGroup direction='vertical'>\n        <LiveSatori />\n      </PanelGroup>\n    </Panel>\n  )\n\n  return (\n    <>\n      {showIntroduction ? (\n        <Introduction\n          onClose={() => {\n            setShowIntroduction(false)\n            localStorage.setItem('_vercel_og_playground_visited', '1')\n          }}\n        />\n      ) : null}\n      <Toaster\n        toastOptions={{\n          style: {\n            fontSize: 13,\n            borderRadius: 6,\n            padding: '2px 4px 2px 12px',\n          },\n        }}\n      />\n      <nav>\n        <h1>\n          <svg viewBox='0 0 75 65' fill='#000' height='12'>\n            <title>Vercel</title>\n            <path d='M37.59.25l36.95 64H.64l36.95-64z'></path>\n          </svg>\n          OG Image Playground\n        </h1>\n        <ul>\n          <li>\n            <a href='https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation'>\n              Docs\n            </a>\n          </li>\n          <li>\n            <a href='https://nextjs.org/discord'>Discord</a>\n          </li>\n          <li>\n            <a href='https://github.com/vercel/satori'>GitHub</a>\n          </li>\n        </ul>\n      </nav>\n      <div className='container'>\n        <LiveProvider code={editedCards[activeCard]}>\n          {hydrated ? (\n            <PanelGroup\n              autoSaveId='og-playground'\n              direction={isMobileView ? 'vertical' : 'horizontal'}\n            >\n              {isMobileView ? previewPanel : editorPanel}\n              <PanelResizeHandle />\n              {isMobileView ? editorPanel : previewPanel}\n            </PanelGroup>\n          ) : null}\n        </LiveProvider>\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "playground/styles.css",
    "content": "/* Fonts for the demo */\n@font-face {\n  font-family: 'Inter';\n  font-style: normal;\n  font-weight: 700;\n  src: url(/inter-latin-ext-700-normal.woff) format('woff2');\n}\n\n/* UI */\n\n@font-face {\n  font-family: 'iaw-mono-var';\n  font-weight: 100 700;\n  font-style: normal;\n  font-named-instance: 'Regular';\n  font-display: block;\n  src: url('/iaw-mono-var.woff2') format('woff2');\n}\n\n:root {\n  --font: iaw-mono-var, SF Mono, SFMono-Regular, ui-monospace, Menlo, Consolas,\n    monospace;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nhtml,\nbody,\n#__next {\n  height: 100%;\n}\n\nbody {\n  font-family: var(--font);\n  font-variant: common-ligatures contextual;\n  letter-spacing: -0.015em;\n  margin: 0;\n  background: fixed 0 0 /20px 20px radial-gradient(#d1d1d1 1px, transparent 0),\n    fixed 10px 10px /20px 20px radial-gradient(#d1d1d1 1px, transparent 0);\n  --border: 1px solid #d7d7d7;\n  --border-inactive: 1px solid #e4e4e4;\n}\n\nnav {\n  display: flex;\n  position: sticky;\n  top: 0;\n  height: 40px;\n  align-items: center;\n  padding: 0 15px;\n  background: white;\n  box-shadow: 0 0 24px rgb(0 0 0 / 10%), 0 0 0 1px rgb(0 0 0 / 4%);\n  font-size: 0.9rem;\n  z-index: 2;\n}\n\nh1 {\n  flex: 1;\n  margin: 0;\n  font-size: 0.9rem;\n  font-weight: 500;\n  color: #444;\n  display: flex;\n  align-items: center;\n  gap: 0.3rem;\n  letter-spacing: -0.015rem;\n  word-spacing: -0.09rem;\n  cursor: default;\n}\n\nnav ul {\n  display: flex;\n  list-style: none;\n  margin: 0;\n  gap: 10px;\n  padding: 0;\n}\n\na {\n  color: inherit;\n}\n\niframe {\n  position: absolute;\n  border: none;\n  appearance: none;\n}\n\ntextarea,\npre,\ncode {\n  font-family: var(--font) !important;\n}\n.editor {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  background: white;\n  border-radius: 12px;\n  border-top-left-radius: 0;\n  border: var(--border);\n  overflow: auto;\n}\n.editor .editor-controls {\n  padding: 4px 8px;\n  gap: 8px;\n  display: flex;\n  justify-content: flex-end;\n}\n.editor .editor-controls button {\n  font-family: var(--font);\n  font-size: 12px;\n  user-select: none;\n  padding: 0px 4px;\n  height: 20px;\n}\n.editor .monaco-container {\n  flex: 1;\n}\n\n.container {\n  display: flex;\n  width: 100%;\n  height: calc(100% - 40px);\n  margin: 0;\n  padding: 10px;\n  overflow: auto;\n  gap: 10px;\n}\n\n.content-wrapper svg {\n  width: 100%;\n  height: 100%;\n}\n\n.tabs {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  width: 100%;\n}\n\n.container > .tabs {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  flex: 1 1 50%;\n  max-width: 50%;\n}\n\n.preview {\n  flex: 1 1 50%;\n  max-width: calc(50% - 5px);\n  align-self: flex-start;\n}\n\n.result-container {\n  position: relative;\n  display: flex;\n  height: 100%;\n  width: 100%;\n  align-items: center;\n  justify-content: center;\n  overflow: hidden;\n}\n\n.preview-card {\n  flex: 1;\n  position: relative;\n  border-radius: 12px;\n  border-top-left-radius: 0;\n  background: #111;\n  border: var(--border);\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n}\n\n.preview-card footer {\n  font-size: 12px;\n  display: flex;\n  align-items: center;\n  padding: 3px 10px 4px;\n  height: 24px;\n  color: #444;\n  border-top: 1px solid rgb(0 0 0 / 8%);\n  background: #fafafa;\n  white-space: nowrap;\n}\n.preview-card footer .ellipsis {\n  white-space: pre;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n.preview-card footer .data {\n  flex: 1;\n}\n\n.error {\n  position: absolute;\n  width: 100%;\n  height: calc(100% - 24px);\n  padding: 10px 20px;\n  overflow: auto;\n  color: #ff3737;\n  font-size: 13px;\n  z-index: 1;\n  background: white;\n}\n.error pre {\n  margin: 0;\n  white-space: pre-wrap;\n}\n\n.preview {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n}\n\n.tabs-container {\n  display: inline-flex;\n  position: relative;\n  font-size: 12px;\n  margin-bottom: -1px;\n  letter-spacing: -0.02em;\n  z-index: 1;\n  user-select: none;\n  white-space: nowrap;\n  max-width: calc(100% - 15px);\n}\n\n.tab {\n  color: #a5a5a5;\n  background: #efefef;\n  height: 22px;\n  border-top-left-radius: 6px;\n  border-top-right-radius: 6px;\n  border: var(--border-inactive);\n  border-bottom: var(--border);\n  padding: 2px 10px 4px;\n  cursor: default;\n  text-overflow: ellipsis;\n  overflow: hidden;\n}\n.tab:hover {\n  background: #fafafa;\n}\n\n.tab.active {\n  color: #111;\n  font-weight: 600;\n  border: var(--border);\n  border-bottom: none;\n  background: white;\n}\n\n.controller {\n  flex: 1;\n  height: 100%;\n  border-radius: 12px;\n  background: white;\n  border: var(--border);\n  overflow: hidden;\n}\n\n.controller h2.title {\n  font-size: 12px;\n  letter-spacing: -0.02em;\n  font-weight: 500;\n  color: #444;\n  margin: 0;\n  padding: 8px 10px;\n  text-transform: uppercase;\n  background: #fafafa;\n  border-bottom: 1px solid rgb(0 0 0 / 8%);\n  user-select: none;\n}\n.controller .content {\n  display: flex;\n  padding: 8px 10px;\n  height: calc(100% - 34px);\n  overflow: auto;\n  flex-direction: column;\n}\n.controller .control {\n  font-size: 12px;\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n.controller .control > div {\n  display: flex;\n  align-items: center;\n  flex-wrap: wrap;\n  gap: 4px;\n}\n.controller .control:not(:last-child) {\n  padding-bottom: 8px;\n  margin-bottom: 8px;\n  border-bottom: 1px solid rgb(0 0 0 / 8%);\n}\n.controller .control label {\n  flex: 0 0 140px;\n  user-select: none;\n}\n.controller input {\n  font-family: var(--font);\n  outline: none;\n}\n.controller input[type='number'] {\n  appearance: none;\n  border: 1px solid rgb(0 0 0 / 20%);\n  border-radius: 4px;\n}\n.controller input[type='number']:hover {\n  border: 1px solid rgb(0 0 0 / 30%);\n}\n.controller input[type='number']:focus {\n  border: 1px solid rgb(0 0 0 / 40%);\n}\n.controller a.disabled {\n  color: #a5a5a5;\n  cursor: not-allowed;\n}\n.controller .copyright {\n  flex: 1;\n}\n\n@media screen and (max-width: 999px) {\n  .container {\n    flex-direction: column-reverse;\n  }\n  .preview {\n    height: unset;\n    width: 100%;\n    max-height: calc((50vw - 15px) * 9 / 16 + 44px);\n    max-width: 100%;\n    flex-direction: row;\n  }\n  .container > .tabs {\n    max-width: unset;\n    max-height: calc(100% - (50vw - 15px) * 9 / 16 - 54px);\n  }\n  .preview > .tabs {\n    width: calc(50% - 5px);\n  }\n  .controller .control label {\n    flex: 0 0 120px;\n  }\n}\n\n@media screen and (max-width: 599px) {\n  .preview > .tabs,\n  .controller {\n    width: 100%;\n  }\n  .container {\n    gap: 0;\n  }\n  .preview {\n    flex-direction: column;\n    max-height: calc((100vw - 20px) * 9 / 16 + 214px - 5px);\n    padding-bottom: 10px;\n  }\n  .container > .tabs {\n    max-height: calc(100% - (100vw - 20px) * 9 / 16 - 214px + 5px);\n  }\n}\n"
  },
  {
    "path": "playground/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2015\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"incremental\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\"\n  },\n  \"include\": [\"decs.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "playground/utils/font.ts",
    "content": "type UnicodeRange = Array<number | number[]>\n\nexport class FontDetector {\n  private rangesByLang: {\n    [font: string]: UnicodeRange\n  } = {}\n\n  public async detect(\n    text: string,\n    fonts: string[]\n  ): Promise<{\n    [lang: string]: string\n  }> {\n    await this.load(fonts)\n\n    const result: {\n      [lang: string]: string\n    } = {}\n\n    for (const segment of text) {\n      const lang = this.detectSegment(segment, fonts)\n      if (lang) {\n        result[lang] = result[lang] || ''\n        result[lang] += segment\n      }\n    }\n\n    return result\n  }\n\n  private detectSegment(segment: string, fonts: string[]): string | null {\n    for (const font of fonts) {\n      const range = this.rangesByLang[font]\n      if (range && checkSegmentInRange(segment, range)) {\n        return font\n      }\n    }\n\n    return null\n  }\n\n  private async load(fonts: string[]): Promise<void> {\n    let params = ''\n\n    const existingLang = Object.keys(this.rangesByLang)\n    const langNeedsToLoad = fonts.filter((font) => !existingLang.includes(font))\n\n    if (langNeedsToLoad.length === 0) {\n      return\n    }\n\n    for (const font of langNeedsToLoad) {\n      params += `family=${font}&`\n    }\n    params += 'display=swap'\n\n    const API = `https://fonts.googleapis.com/css2?${params}`\n\n    const fontFace = await (\n      await fetch(API, {\n        headers: {\n          // Make sure it returns TTF.\n          'User-Agent':\n            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',\n        },\n      })\n    ).text()\n\n    this.addDetectors(fontFace)\n  }\n\n  private addDetectors(input: string) {\n    const regex = /font-family:\\s*'(.+?)';.+?unicode-range:\\s*(.+?);/gms\n    const matches = input.matchAll(regex)\n\n    for (const [, _lang, range] of matches) {\n      const lang = _lang.replaceAll(' ', '+')\n      if (!this.rangesByLang[lang]) {\n        this.rangesByLang[lang] = []\n      }\n\n      this.rangesByLang[lang].push(...convert(range))\n    }\n  }\n}\n\nfunction convert(input: string): UnicodeRange {\n  return input.split(', ').map((range) => {\n    range = range.replaceAll('U+', '')\n    const [start, end] = range.split('-').map((hex) => parseInt(hex, 16))\n\n    if (isNaN(end)) {\n      return start\n    }\n\n    return [start, end]\n  })\n}\n\nfunction checkSegmentInRange(segment: string, range: UnicodeRange): boolean {\n  const codePoint = segment.codePointAt(0)\n\n  if (!codePoint) return false\n\n  return range.some((val) => {\n    if (typeof val === 'number') {\n      return codePoint === val\n    } else {\n      const [start, end] = val\n      return start <= codePoint && codePoint <= end\n    }\n  })\n}\n\n// @TODO: Support font style and weights, and make this option extensible rather\n// than built-in.\n// @TODO: Cover most languages with Noto Sans.\nexport const languageFontMap = {\n  'ja-JP': 'Noto+Sans+JP',\n  'ko-KR': 'Noto+Sans+KR',\n  'zh-CN': 'Noto+Sans+SC',\n  'zh-TW': 'Noto+Sans+TC',\n  'zh-HK': 'Noto+Sans+HK',\n  'th-TH': 'Noto+Sans+Thai',\n  'bn-IN': 'Noto+Sans+Bengali',\n  'ar-AR': 'Noto+Sans+Arabic',\n  'ta-IN': 'Noto+Sans+Tamil',\n  'ml-IN': 'Noto+Sans+Malayalam',\n  'he-IL': 'Noto+Sans+Hebrew',\n  'te-IN': 'Noto+Sans+Telugu',\n  devanagari: 'Noto+Sans+Devanagari',\n  kannada: 'Noto+Sans+Kannada',\n  symbol: ['Noto+Sans+Symbols', 'Noto+Sans+Symbols+2'],\n  math: 'Noto+Sans+Math',\n  unknown: 'Noto+Sans',\n}\n"
  },
  {
    "path": "playground/utils/twemoji.ts",
    "content": "/**\n * Modified version of https://unpkg.com/twemoji@13.1.0/dist/twemoji.esm.js.\n */\n\n/*! Copyright Twitter Inc. and other contributors. Licensed under MIT */\n\nconst U200D = String.fromCharCode(8205)\nconst UFE0Fg = /\\uFE0F/g\n\nexport function getIconCode(char: string) {\n  return toCodePoint(char.indexOf(U200D) < 0 ? char.replace(UFE0Fg, '') : char)\n}\n\nfunction toCodePoint(unicodeSurrogates: string) {\n  const r = []\n  let c = 0,\n    p = 0,\n    i = 0\n\n  while (i < unicodeSurrogates.length) {\n    c = unicodeSurrogates.charCodeAt(i++)\n    if (p) {\n      r.push((65536 + ((p - 55296) << 10) + (c - 56320)).toString(16))\n      p = 0\n    } else if (55296 <= c && c <= 56319) {\n      p = c\n    } else {\n      r.push(c.toString(16))\n    }\n  }\n  return r.join('-')\n}\n\nexport const apis = {\n  twemoji: (code: string) =>\n    'https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/' +\n    code.toLowerCase() +\n    '.svg',\n  openmoji: 'https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/',\n  blobmoji: 'https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/',\n  noto: 'https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/',\n  fluent: (code: string) =>\n    'https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/' +\n    code.toLowerCase() +\n    '_color.svg',\n  fluentFlat: (code: string) =>\n    'https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/' +\n    code.toLowerCase() +\n    '_flat.svg',\n}\n\nconst emojiCache: Record<string, Promise<any>> = {}\n\nexport function loadEmoji(type: keyof typeof apis, code: string) {\n  const key = type + ':' + code\n  if (key in emojiCache) return emojiCache[key]\n\n  if (!type || !apis[type]) {\n    type = 'twemoji'\n  }\n\n  const api = apis[type]\n  if (typeof api === 'function') {\n    return (emojiCache[key] = fetch(api(code)).then((r) => r.text()))\n  }\n  return (emojiCache[key] = fetch(`${api}${code.toUpperCase()}.svg`).then((r) =>\n    r.text()\n  ))\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - 'playground'\n  - '.'\n"
  },
  {
    "path": "release.config.cjs",
    "content": "module.exports = {\n  branches: ['main'],\n  tagFormat: '${version}',\n}\n"
  },
  {
    "path": "src/builder/background-image.ts",
    "content": "import CssDimension from '../vendor/parse-css-dimension/index.js'\nimport { buildXMLString } from '../utils.js'\n\nimport { resolveImageData } from '../handler/image.js'\nimport { buildLinearGradient } from './gradient/linear.js'\nimport { buildRadialGradient } from './gradient/radial.js'\nimport cssColorParse from 'parse-css-color'\n\ninterface Background {\n  attachment?: string\n  color?: string\n  clip: string\n  image: string\n  origin?: string\n  position: string\n  size: string\n  repeat: string\n}\n\nfunction toAbsoluteValue(v: string | number, base: number) {\n  if (typeof v === 'string' && v.endsWith('%')) {\n    return (base * parseFloat(v)) / 100\n  }\n  return +v\n}\n\nfunction calculateKeywordSize(\n  keyword: string,\n  containerWidth: number,\n  containerHeight: number,\n  imageWidth: number,\n  imageHeight: number\n): [number, number] {\n  if (!imageWidth || !imageHeight) {\n    return [containerWidth, containerHeight]\n  }\n\n  if (keyword === 'cover') {\n    // Scale to cover the container (use max scale to ensure it covers)\n    const scaleX = containerWidth / imageWidth\n    const scaleY = containerHeight / imageHeight\n    const scale = Math.max(scaleX, scaleY)\n    return [imageWidth * scale, imageHeight * scale]\n  }\n\n  if (keyword === 'contain') {\n    // Scale to fit within the container (use min scale to ensure it fits)\n    const scaleX = containerWidth / imageWidth\n    const scaleY = containerHeight / imageHeight\n    const scale = Math.min(scaleX, scaleY)\n    return [imageWidth * scale, imageHeight * scale]\n  }\n\n  // For 'auto' or other values, handle auto\n  if (keyword === 'auto' || keyword.includes('auto')) {\n    const parts = keyword.split(' ')\n    const widthPart = parts[0] || 'auto'\n    const heightPart = parts[1] || parts[0] || 'auto'\n\n    let finalWidth = imageWidth\n    let finalHeight = imageHeight\n\n    if (widthPart === 'auto' && heightPart !== 'auto') {\n      // Width is auto, height is specified\n      const parsedHeight = toAbsoluteValue(heightPart, containerHeight)\n      finalHeight = parsedHeight\n      finalWidth = (imageWidth / imageHeight) * parsedHeight\n    } else if (heightPart === 'auto' && widthPart !== 'auto') {\n      // Height is auto, width is specified\n      const parsedWidth = toAbsoluteValue(widthPart, containerWidth)\n      finalWidth = parsedWidth\n      finalHeight = (imageHeight / imageWidth) * parsedWidth\n    }\n    // If both are auto, use intrinsic dimensions\n\n    return [finalWidth, finalHeight]\n  }\n\n  return [containerWidth, containerHeight]\n}\n\nfunction parseLengthPairs(\n  str: string,\n  {\n    x,\n    y,\n    defaultX,\n    defaultY,\n  }: {\n    x: number\n    y: number\n    defaultX: number | string\n    defaultY: number | string\n  }\n) {\n  return (\n    str\n      ? str\n          .split(' ')\n          .map((value) => {\n            try {\n              const parsed = new CssDimension(value)\n              return parsed.type === 'length' || parsed.type === 'number'\n                ? parsed.value\n                : parsed.value + parsed.unit\n            } catch (e) {\n              return null\n            }\n          })\n          .filter((v) => v !== null)\n      : [defaultX, defaultY]\n  ).map((v, index) => toAbsoluteValue(v, [x, y][index]))\n}\n\nexport default async function backgroundImage(\n  {\n    id,\n    width,\n    height,\n    left,\n    top,\n  }: { id: string; width: number; height: number; left: number; top: number },\n  { image, size, position, repeat }: Background,\n  inheritableStyle: Record<string, number | string>,\n  from?: 'background' | 'mask'\n): Promise<string[]> {\n  // Default to `repeat`.\n  repeat = repeat || 'repeat'\n  from = from || 'background'\n\n  const repeatX = repeat === 'repeat-x' || repeat === 'repeat'\n  const repeatY = repeat === 'repeat-y' || repeat === 'repeat'\n\n  // Check if size is a keyword (cover, contain, auto) that needs to be calculated later\n  const isKeywordSize =\n    size &&\n    (size === 'cover' ||\n      size === 'contain' ||\n      size === 'auto' ||\n      size.includes('auto'))\n\n  // For gradients, keyword sizes (cover, contain, auto) resolve to the\n  // container dimensions since gradients have no intrinsic size.\n  // For url() images, keyword sizes are calculated later using the image's\n  // intrinsic dimensions.\n  const isGradient =\n    image.startsWith('linear-gradient(') ||\n    image.startsWith('repeating-linear-gradient(') ||\n    image.startsWith('radial-gradient(') ||\n    image.startsWith('repeating-radial-gradient(')\n\n  const dimensions =\n    isKeywordSize && isGradient\n      ? [width, height] // Gradients have no intrinsic size; keyword sizes resolve to container\n      : isKeywordSize\n      ? [0, 0] // Will be calculated later when we have image dimensions\n      : parseLengthPairs(size, {\n          x: width,\n          y: height,\n          defaultX: width,\n          defaultY: height,\n        })\n  const offsets = parseLengthPairs(position, {\n    x: width,\n    y: height,\n    defaultX: 0,\n    defaultY: 0,\n  })\n\n  if (\n    image.startsWith('linear-gradient(') ||\n    image.startsWith('repeating-linear-gradient(')\n  ) {\n    return buildLinearGradient(\n      { id, width, height, repeatX, repeatY },\n      image,\n      dimensions,\n      offsets,\n      inheritableStyle,\n      from\n    )\n  }\n\n  if (\n    image.startsWith('radial-gradient(') ||\n    image.startsWith('repeating-radial-gradient(')\n  ) {\n    return buildRadialGradient(\n      { id, width, height, repeatX, repeatY },\n      image,\n      dimensions,\n      offsets,\n      inheritableStyle,\n      from\n    )\n  }\n\n  if (image.startsWith('url(')) {\n    const [src, imageWidth, imageHeight] = await resolveImageData(\n      image.slice(4, -1)\n    )\n\n    let resolvedWidth: number\n    let resolvedHeight: number\n\n    if (isKeywordSize) {\n      // Calculate dimensions based on keyword (cover, contain, auto)\n      const [calcWidth, calcHeight] = calculateKeywordSize(\n        size,\n        width,\n        height,\n        imageWidth,\n        imageHeight\n      )\n      resolvedWidth = calcWidth\n      resolvedHeight = calcHeight\n    } else {\n      // Use the previously parsed dimensions\n      const dimensionsWithoutFallback = parseLengthPairs(size, {\n        x: width,\n        y: height,\n        defaultX: 0,\n        defaultY: 0,\n      })\n      resolvedWidth =\n        from === 'mask'\n          ? imageWidth || dimensionsWithoutFallback[0]\n          : dimensionsWithoutFallback[0] || imageWidth\n      resolvedHeight =\n        from === 'mask'\n          ? imageHeight || dimensionsWithoutFallback[1]\n          : dimensionsWithoutFallback[1] || imageHeight\n    }\n\n    return [\n      `satori_bi${id}`,\n      buildXMLString(\n        'pattern',\n        {\n          id: `satori_bi${id}`,\n          patternContentUnits: 'userSpaceOnUse',\n          patternUnits: 'userSpaceOnUse',\n          x: offsets[0] + left,\n          y: offsets[1] + top,\n          width: repeatX ? resolvedWidth : '100%',\n          height: repeatY ? resolvedHeight : '100%',\n        },\n        buildXMLString('image', {\n          x: 0,\n          y: 0,\n          width: resolvedWidth,\n          height: resolvedHeight,\n          preserveAspectRatio: 'none',\n          href: src,\n        })\n      ),\n    ]\n  }\n\n  if (cssColorParse(image)) {\n    const colorObj = cssColorParse(image)\n    const [r, g, b, a] = colorObj.values\n    const alpha = a !== undefined ? a : 1\n    const color = `rgba(${r},${g},${b},${alpha})`\n\n    return [\n      `satori_bi${id}`,\n      buildXMLString(\n        'pattern',\n        {\n          id: `satori_bi${id}`,\n          patternContentUnits: 'userSpaceOnUse',\n          patternUnits: 'userSpaceOnUse',\n          x: left,\n          y: top,\n          width: width,\n          height: height,\n        },\n        buildXMLString('rect', {\n          x: 0,\n          y: 0,\n          width: width,\n          height: height,\n          fill: color,\n        })\n      ),\n    ]\n  }\n\n  throw new Error(`Invalid background image: \"${image}\"`)\n}\n"
  },
  {
    "path": "src/builder/border-radius.ts",
    "content": "/**\n * CSS border radius to SVG path.\n */\n\n// TODO: Support the `border-radius: 10px / 20px` syntax.\n// https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius\n\nimport { buildXMLString, lengthToNumber } from '../utils.js'\n\n// Getting the intersection of a 45deg ray with the elliptical arc x^2/rx^2 + y^2/ry^2 = 1.\n// Reference:\n// https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter\nfunction svgArcCenterOffset([rx, ry]: number[]) {\n  if (Math.round(rx * 1000) === 0 && Math.round(ry * 1000) === 0) {\n    return 0\n  }\n  return Math.round(((rx * ry) / Math.sqrt(rx * rx + ry * ry)) * 1000) / 1000\n}\n\nfunction resolveSize(a: number, b: number, limit: number) {\n  if (limit < a + b) {\n    if (limit / 2 < a && limit / 2 < b) {\n      a = b = limit / 2\n    } else if (limit / 2 < a) {\n      a = limit - b\n    } else if (limit / 2 < b) {\n      b = limit - a\n    }\n  }\n  return [a, b]\n}\n\nfunction makeSmaller(arr: [number, number]) {\n  arr[0] = arr[1] = Math.min(arr[0], arr[1])\n}\n\n// Each corner can have 2 values, the first is the horizontal radius, the second is the vertical radius.\nfunction resolveRadius(\n  v: number | string | undefined,\n  width: number,\n  height: number,\n  fontSize: number,\n  style: any\n): [boolean, undefined | [number, number]] {\n  if (typeof v === 'string') {\n    const sides = v.split(' ').map((s) => s.trim())\n    const singleValue = !sides[1] && !sides[0].endsWith('%')\n    sides[1] = sides[1] || sides[0]\n    return [\n      singleValue,\n      [\n        Math.min(lengthToNumber(sides[0], fontSize, width, style, true), width),\n        Math.min(\n          lengthToNumber(sides[1], fontSize, height, style, true),\n          height\n        ),\n      ],\n    ]\n  }\n  if (typeof v === 'number') {\n    return [true, [Math.min(v, width), Math.min(v, height)]]\n  }\n  return [true, undefined]\n}\n\nconst radiusZeroOrNull = (_radius?: [number, number]) =>\n  _radius && _radius[0] !== 0 && _radius[1] !== 0\n\nexport function getBorderRadiusClipPath(\n  {\n    id,\n    borderRadiusPath,\n    borderType,\n    left,\n    top,\n    width,\n    height,\n  }: {\n    id: string\n    borderRadiusPath?: string\n    borderType?: 'rect' | 'path'\n    left: number\n    top: number\n    width: number\n    height: number\n  },\n  style: Record<string, number | string>\n) {\n  const rectClipId = `satori_brc-${id}`\n  const defs = buildXMLString(\n    'clipPath',\n    {\n      id: rectClipId,\n    },\n    buildXMLString(borderType, {\n      x: left,\n      y: top,\n      width,\n      height,\n      d: borderRadiusPath ? borderRadiusPath : undefined,\n    })\n  )\n\n  return [defs, rectClipId]\n}\n\nexport default function radius(\n  {\n    left,\n    top,\n    width,\n    height,\n  }: {\n    left: number\n    top: number\n    width: number\n    height: number\n  },\n  style: Record<string, any>,\n  partialSides?: boolean[]\n) {\n  let {\n    borderTopLeftRadius,\n    borderTopRightRadius,\n    borderBottomLeftRadius,\n    borderBottomRightRadius,\n    fontSize,\n  } = style\n\n  let singleAbsValueTopLeftCorner\n  let singleAbsValueTopRightCorner\n  let singleAbsValueBottomLeftCorner\n  let singleAbsValueBottomRightCorner\n  ;[singleAbsValueTopLeftCorner, borderTopLeftRadius] = resolveRadius(\n    borderTopLeftRadius,\n    width,\n    height,\n    fontSize,\n    style\n  )\n  ;[singleAbsValueTopRightCorner, borderTopRightRadius] = resolveRadius(\n    borderTopRightRadius,\n    width,\n    height,\n    fontSize,\n    style\n  )\n  ;[singleAbsValueBottomLeftCorner, borderBottomLeftRadius] = resolveRadius(\n    borderBottomLeftRadius,\n    width,\n    height,\n    fontSize,\n    style\n  )\n  ;[singleAbsValueBottomRightCorner, borderBottomRightRadius] = resolveRadius(\n    borderBottomRightRadius,\n    width,\n    height,\n    fontSize,\n    style\n  )\n\n  if (\n    !partialSides &&\n    !radiusZeroOrNull(borderTopLeftRadius) &&\n    !radiusZeroOrNull(borderTopRightRadius) &&\n    !radiusZeroOrNull(borderBottomLeftRadius) &&\n    !radiusZeroOrNull(borderBottomRightRadius)\n  ) {\n    return ''\n  }\n  borderTopLeftRadius ||= [0, 0]\n  borderTopRightRadius ||= [0, 0]\n  borderBottomLeftRadius ||= [0, 0]\n  borderBottomRightRadius ||= [0, 0]\n\n  // Limit the radius sizes of each edge to make sure they will never overlap.\n\n  // Top\n  ;[borderTopLeftRadius[0], borderTopRightRadius[0]] = resolveSize(\n    borderTopLeftRadius[0],\n    borderTopRightRadius[0],\n    width\n  )\n  // Bottom\n  ;[borderBottomLeftRadius[0], borderBottomRightRadius[0]] = resolveSize(\n    borderBottomLeftRadius[0],\n    borderBottomRightRadius[0],\n    width\n  )\n  // Left\n  ;[borderTopLeftRadius[1], borderBottomLeftRadius[1]] = resolveSize(\n    borderTopLeftRadius[1],\n    borderBottomLeftRadius[1],\n    height\n  )\n  // Right\n  ;[borderTopRightRadius[1], borderBottomRightRadius[1]] = resolveSize(\n    borderTopRightRadius[1],\n    borderBottomRightRadius[1],\n    height\n  )\n\n  // If the specified border radius is a single value (e.g. 10px or 10em), we take\n  // the minimum of the resolved horizontal and vertical radius and apply to both.\n  if (singleAbsValueTopLeftCorner) {\n    makeSmaller(borderTopLeftRadius)\n  }\n  if (singleAbsValueTopRightCorner) {\n    makeSmaller(borderTopRightRadius)\n  }\n  if (singleAbsValueBottomLeftCorner) {\n    makeSmaller(borderBottomLeftRadius)\n  }\n  if (singleAbsValueBottomRightCorner) {\n    makeSmaller(borderBottomRightRadius)\n  }\n\n  type Arc = [[number, number], [number, number]]\n  const p: Arc[] = []\n  p[0] = [borderTopRightRadius, borderTopRightRadius]\n  p[1] = [\n    borderBottomRightRadius,\n    [-borderBottomRightRadius[0], borderBottomRightRadius[1]],\n  ]\n  p[2] = [\n    borderBottomLeftRadius,\n    [-borderBottomLeftRadius[0], -borderBottomLeftRadius[1]],\n  ]\n  p[3] = [\n    borderTopLeftRadius,\n    [borderTopLeftRadius[0], -borderTopLeftRadius[1]],\n  ]\n\n  const T = `h${width - borderTopLeftRadius[0] - borderTopRightRadius[0]} a${\n    p[0][0]\n  } 0 0 1 ${p[0][1]}`\n  const R = `v${\n    height - borderTopRightRadius[1] - borderBottomRightRadius[1]\n  } a${p[1][0]} 0 0 1 ${p[1][1]}`\n  const B = `h${\n    borderBottomRightRadius[0] + borderBottomLeftRadius[0] - width\n  } a${p[2][0]} 0 0 1 ${p[2][1]}`\n  const L = `v${borderBottomLeftRadius[1] + borderTopLeftRadius[1] - height} a${\n    p[3][0]\n  } 0 0 1 ${p[3][1]}`\n\n  if (partialSides) {\n    // \"However it is not defined what these transitions look like or what function maps from this ratio to a point on the curve.\"\n    // https://w3c.github.io/csswg-drafts/css-backgrounds-3/#corner-transitions\n    let start = partialSides.indexOf(false)\n\n    if (!partialSides.includes(true)) throw new Error('Invalid `partialSides`.')\n\n    if (start === -1) {\n      start = 0\n    } else {\n      while (!partialSides[start]) {\n        start = (start + 1) % 4\n      }\n    }\n\n    function getArc(i: number) {\n      const c0 = svgArcCenterOffset(\n        [\n          borderTopLeftRadius,\n          borderTopRightRadius,\n          borderBottomRightRadius,\n          borderBottomLeftRadius,\n        ][i]\n      )\n      return i === 0\n        ? [\n            [\n              left + borderTopLeftRadius[0] - c0,\n              top + borderTopLeftRadius[1] - c0,\n            ],\n            [left + borderTopLeftRadius[0], top],\n          ]\n        : i === 1\n        ? [\n            [\n              left + width - borderTopRightRadius[0] + c0,\n              top + borderTopRightRadius[1] - c0,\n            ],\n            [left + width, top + borderTopRightRadius[1]],\n          ]\n        : i === 2\n        ? [\n            [\n              left + width - borderBottomRightRadius[0] + c0,\n              top + height - borderBottomRightRadius[1] + c0,\n            ],\n            [left + width - borderBottomRightRadius[0], top + height],\n          ]\n        : [\n            [\n              left + borderBottomLeftRadius[0] - c0,\n              top + height - borderBottomLeftRadius[1] + c0,\n            ],\n            [left, top + height - borderBottomLeftRadius[1]],\n          ]\n    }\n\n    let result = ''\n\n    const arc0 = getArc(start)\n\n    let l = `M${arc0[0]} A${p[(start + 3) % 4][0]} 0 0 1 ${arc0[1]}`\n\n    let len = 0\n    for (; len < 4 && partialSides[(start + len) % 4]; len++) {\n      result += l + ' '\n      l = [T, R, B, L][(start + len) % 4]\n    }\n    const end = (start + len) % 4\n\n    // For the last segment, we skip the full arc and add the half arc.\n    result += l.split(' ')[0]\n\n    const arc1 = getArc(end)\n    result += ` A${p[(end + 3) % 4][0]} 0 0 1 ${arc1[0]}`\n\n    return result\n  }\n\n  // Generate the path\n  return `M${left + borderTopLeftRadius[0]},${top} ${T} ${R} ${B} ${L}`\n}\n"
  },
  {
    "path": "src/builder/border.ts",
    "content": "import { buildXMLString } from '../utils.js'\nimport radius from './border-radius.js'\n\nfunction compareBorderDirections(a: string, b: string, style: any) {\n  return (\n    style[a + 'Width'] === style[b + 'Width'] &&\n    style[a + 'Style'] === style[b + 'Style'] &&\n    style[a + 'Color'] === style[b + 'Color']\n  )\n}\n\nexport function getBorderClipPath(\n  {\n    id,\n    // Can be `overflow: hidden` from parent containers.\n    currentClipPathId,\n    borderPath,\n    borderType,\n    left,\n    top,\n    width,\n    height,\n  }: {\n    id: string\n    currentClipPathId?: string | number\n    borderPath?: string\n    borderType?: 'rect' | 'path'\n    left: number\n    top: number\n    width: number\n    height: number\n  },\n  style: Record<string, number | string>\n) {\n  const hasBorder =\n    style.borderTopWidth ||\n    style.borderRightWidth ||\n    style.borderBottomWidth ||\n    style.borderLeftWidth\n\n  if (!hasBorder) return null\n\n  // In SVG, stroke is always centered on the path and there is no\n  // existing property to make it behave like CSS border. So here we\n  // 2x the border width and introduce another clip path to clip the\n  // overflowed part.\n  const rectClipId = `satori_bc-${id}`\n  const defs = buildXMLString(\n    'clipPath',\n    {\n      id: rectClipId,\n      'clip-path': currentClipPathId ? `url(#${currentClipPathId})` : undefined,\n    },\n    buildXMLString(borderType, {\n      x: left,\n      y: top,\n      width,\n      height,\n      d: borderPath ? borderPath : undefined,\n    })\n  )\n\n  return [defs, rectClipId]\n}\n\nexport default function border(\n  {\n    left,\n    top,\n    width,\n    height,\n    props,\n    asContentMask,\n    maskBorderOnly,\n  }: {\n    left: number\n    top: number\n    width: number\n    height: number\n    props: any\n    asContentMask?: boolean\n    maskBorderOnly?: boolean\n  },\n  style: Record<string, number | string>\n) {\n  const directions = ['borderTop', 'borderRight', 'borderBottom', 'borderLeft']\n\n  // No border\n  if (\n    !asContentMask &&\n    !directions.some((direction) => style[direction + 'Width'])\n  )\n    return ''\n\n  let fullBorder = ''\n\n  let start = 0\n  while (\n    start > 0 &&\n    compareBorderDirections(\n      directions[start],\n      directions[(start + 3) % 4],\n      style\n    )\n  ) {\n    start = (start + 3) % 4\n  }\n\n  let partialSides = [false, false, false, false]\n  let currentStyle = []\n  for (let _i = 0; _i < 4; _i++) {\n    const i = (start + _i) % 4\n    const ni = (start + _i + 1) % 4\n\n    const d = directions[i]\n    const nd = directions[ni]\n\n    partialSides[i] = true\n    currentStyle = [\n      style[d + 'Width'],\n      style[d + 'Style'],\n      style[d + 'Color'],\n      d,\n    ]\n\n    if (!compareBorderDirections(d, nd, style)) {\n      const w =\n        (currentStyle[0] || 0) +\n        (asContentMask && !maskBorderOnly\n          ? style[d.replace('border', 'padding')] || 0\n          : 0)\n      if (w) {\n        fullBorder += buildXMLString('path', {\n          width,\n          height,\n          ...props,\n          fill: 'none',\n          stroke: asContentMask ? '#000' : currentStyle[2],\n          'stroke-width': w * 2,\n          'stroke-dasharray':\n            !asContentMask && currentStyle[1] === 'dashed'\n              ? w * 2 + ' ' + w\n              : undefined,\n          d: radius(\n            { left, top, width, height },\n            style as Record<string, number>,\n            partialSides\n          ),\n        })\n      }\n      partialSides = [false, false, false, false]\n    }\n  }\n\n  if (partialSides.some(Boolean)) {\n    const w =\n      (currentStyle[0] || 0) +\n      (asContentMask && !maskBorderOnly\n        ? style[currentStyle[3].replace('border', 'padding')] || 0\n        : 0)\n    if (w) {\n      fullBorder += buildXMLString('path', {\n        width,\n        height,\n        ...props,\n        fill: 'none',\n        stroke: asContentMask ? '#000' : currentStyle[2],\n        'stroke-width': w * 2,\n        'stroke-dasharray':\n          !asContentMask && currentStyle[1] === 'dashed'\n            ? w * 2 + ' ' + w\n            : undefined,\n        d: radius(\n          { left, top, width, height },\n          style as Record<string, number>,\n          partialSides\n        ),\n      })\n    }\n  }\n\n  return fullBorder\n}\n"
  },
  {
    "path": "src/builder/clip-path.ts",
    "content": "import { buildXMLString } from '../utils.js'\nimport { createShapeParser } from '../parser/shape.js'\n\nexport function genClipPathId(id: string) {\n  return `satori_cp-${id}`\n}\nexport function genClipPath(id: string) {\n  return `url(#${genClipPathId(id)})`\n}\n\nexport function buildClipPath(\n  v: {\n    left: number\n    top: number\n    width: number\n    height: number\n    path: string\n    matrix: string | undefined\n    id: string\n    currentClipPath: string | string\n    src?: string\n  },\n  style: Record<string, string | number>,\n  inheritedStyle: Record<string, string | number>\n) {\n  if (style.clipPath === 'none') return ''\n\n  const parser = createShapeParser(v, style, inheritedStyle)\n  const clipPath = style.clipPath as string\n\n  let tmp: { type: string; [p: string]: string | number } = { type: '' }\n\n  for (const k of Object.keys(parser)) {\n    tmp = parser[k](clipPath)\n    if (tmp) break\n  }\n\n  if (tmp) {\n    const { type, ...rest } = tmp\n    return buildXMLString(\n      'clipPath',\n      {\n        id: genClipPathId(v.id),\n        'clip-path': v.currentClipPath,\n        transform: `translate(${v.left}, ${v.top})`,\n      },\n      buildXMLString(type, rest)\n    )\n  }\n  return ''\n}\n"
  },
  {
    "path": "src/builder/content-mask.ts",
    "content": "/**\n * When there is border radius, the content area should be clipped by the\n * inner path of border + padding. This applies to <img> element as well as any\n * child element inside a `overflow: hidden` container.\n */\n\nimport { buildXMLString } from '../utils.js'\nimport border from './border.js'\n\nexport default function contentMask(\n  {\n    id,\n    left,\n    top,\n    width,\n    height,\n    matrix,\n    borderOnly,\n  }: {\n    id: string\n    left: number\n    top: number\n    width: number\n    height: number\n    matrix: string | undefined\n    borderOnly?: boolean\n  },\n  style: Record<string, number | string>\n) {\n  const offsetLeft =\n    ((style.borderLeftWidth as number) || 0) +\n    (borderOnly ? 0 : (style.paddingLeft as number) || 0)\n  const offsetTop =\n    ((style.borderTopWidth as number) || 0) +\n    (borderOnly ? 0 : (style.paddingTop as number) || 0)\n  const offsetRight =\n    ((style.borderRightWidth as number) || 0) +\n    (borderOnly ? 0 : (style.paddingRight as number) || 0)\n  const offsetBottom =\n    ((style.borderBottomWidth as number) || 0) +\n    (borderOnly ? 0 : (style.paddingBottom as number) || 0)\n\n  const contentArea = {\n    x: left + offsetLeft,\n    y: top + offsetTop,\n    width: width - offsetLeft - offsetRight,\n    height: height - offsetTop - offsetBottom,\n  }\n\n  const _contentMask = buildXMLString(\n    'mask',\n    { id },\n    buildXMLString('rect', {\n      ...contentArea,\n      fill: '#fff',\n      // add transformation matrix to mask if overflow is hidden AND a\n      // transformation style is defined, otherwise children will be clipped\n      // incorrectly\n      transform:\n        style.overflow === 'hidden' && style.transform && matrix\n          ? matrix\n          : undefined,\n      mask: style._inheritedMaskId\n        ? `url(#${style._inheritedMaskId})`\n        : undefined,\n    }) +\n      border(\n        {\n          left,\n          top,\n          width,\n          height,\n          props: {\n            transform: matrix ? matrix : undefined,\n          },\n          asContentMask: true,\n          maskBorderOnly: borderOnly,\n        },\n        style\n      )\n  )\n\n  return _contentMask\n}\n"
  },
  {
    "path": "src/builder/gradient/linear.ts",
    "content": "import { parseLinearGradient, ColorStop } from 'css-gradient-parser'\nimport { normalizeStops } from './utils.js'\nimport { buildXMLString, calcDegree, lengthToNumber } from '../../utils.js'\n\nexport function buildLinearGradient(\n  {\n    id,\n    width,\n    height,\n    repeatX,\n    repeatY,\n  }: {\n    id: string\n    width: number\n    height: number\n    repeatX: boolean\n    repeatY: boolean\n  },\n  image: string,\n  dimensions: number[],\n  offsets: number[],\n  inheritableStyle: Record<string, number | string>,\n  from?: 'background' | 'mask'\n) {\n  const parsed = parseLinearGradient(image)\n  const [imageWidth, imageHeight] = dimensions\n  const repeating = image.startsWith('repeating')\n\n  // Calculate the direction.\n  let points, length, xys\n\n  if (parsed.orientation.type === 'directional') {\n    points = resolveXYFromDirection(parsed.orientation.value)\n\n    length = Math.sqrt(\n      Math.pow((points.x2 - points.x1) * imageWidth, 2) +\n        Math.pow((points.y2 - points.y1) * imageHeight, 2)\n    )\n  } else if (parsed.orientation.type === 'angular') {\n    const { length: l, ...p } = calcNormalPoint(\n      (calcDegree(\n        `${parsed.orientation.value.value}${parsed.orientation.value.unit}`\n      ) /\n        180) *\n        Math.PI,\n      imageWidth,\n      imageHeight\n    )\n\n    length = l\n    points = p\n  }\n\n  xys = repeating\n    ? calcPercentage(parsed.stops, length, points, inheritableStyle)\n    : points\n\n  const stops = normalizeStops(\n    repeating ? resolveRepeatingCycle(parsed.stops, length) : length,\n    parsed.stops,\n    inheritableStyle,\n    repeating,\n    from\n  )\n\n  const gradientId = `satori_bi${id}`\n  const patternId = `satori_pattern_${id}`\n\n  const defs = buildXMLString(\n    'pattern',\n    {\n      id: patternId,\n      x: offsets[0] / width,\n      y: offsets[1] / height,\n      width: repeatX ? imageWidth / width : '1',\n      height: repeatY ? imageHeight / height : '1',\n      patternUnits: 'objectBoundingBox',\n    },\n    buildXMLString(\n      'linearGradient',\n      {\n        id: gradientId,\n        ...xys,\n        spreadMethod: repeating ? 'repeat' : 'pad',\n      },\n      stops\n        .map((stop) =>\n          buildXMLString('stop', {\n            offset: (stop.offset ?? 0) * 100 + '%',\n            'stop-color': stop.color,\n          })\n        )\n        .join('')\n    ) +\n      buildXMLString('rect', {\n        x: 0,\n        y: 0,\n        width: imageWidth,\n        height: imageHeight,\n        fill: `url(#${gradientId})`,\n      })\n  )\n  return [patternId, defs]\n}\n\nfunction resolveRepeatingCycle(stops: ColorStop[], length: number) {\n  const last = stops[stops.length - 1]\n  const { offset } = last\n  if (!offset) return length\n\n  if (offset.unit === '%') return (Number(offset.value) / 100) * length\n\n  return Number(offset.value)\n}\n\nfunction resolveXYFromDirection(dir: string) {\n  let x1 = 0,\n    y1 = 0,\n    x2 = 0,\n    y2 = 0\n\n  if (dir.includes('top')) {\n    y1 = 1\n  } else if (dir.includes('bottom')) {\n    y2 = 1\n  }\n\n  if (dir.includes('left')) {\n    x1 = 1\n  } else if (dir.includes('right')) {\n    x2 = 1\n  }\n\n  if (!x1 && !x2 && !y1 && !y2) {\n    y1 = 1\n  }\n\n  return { x1, y1, x2, y2 }\n}\n\n/**\n * calc start point and end point of linear gradient\n */\nfunction calcNormalPoint(v: number, w: number, h: number) {\n  const r = Math.pow(h / w, 2)\n\n  // make sure angle is 0 <= angle <= 360\n  v = ((v % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2)\n\n  let x1, y1, x2, y2, length, tmp, a, b\n\n  const dfs = (angle: number) => {\n    if (angle === 0) {\n      x1 = 0\n      y1 = h\n      x2 = 0\n      y2 = 0\n      length = h\n      return\n    } else if (angle === Math.PI / 2) {\n      x1 = 0\n      y1 = 0\n      x2 = w\n      y2 = 0\n      length = w\n      return\n    }\n    if (angle > 0 && angle < Math.PI / 2) {\n      x1 =\n        ((r * w) / 2 / Math.tan(angle) - h / 2) /\n        (Math.tan(angle) + r / Math.tan(angle))\n      y1 = Math.tan(angle) * x1 + h\n      x2 = Math.abs(w / 2 - x1) + w / 2\n      y2 = h / 2 - Math.abs(y1 - h / 2)\n      length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))\n      // y = -1 / tan * x = h / 2 +1 / tan * w/2\n      // y = tan * x + h\n      a =\n        (w / 2 / Math.tan(angle) - h / 2) /\n        (Math.tan(angle) + 1 / Math.tan(angle))\n      b = Math.tan(angle) * a + h\n      length = 2 * Math.sqrt(Math.pow(w / 2 - a, 2) + Math.pow(h / 2 - b, 2))\n      return\n    } else if (angle > Math.PI / 2 && angle < Math.PI) {\n      x1 =\n        (h / 2 + (r * w) / 2 / Math.tan(angle)) /\n        (Math.tan(angle) + r / Math.tan(angle))\n      y1 = Math.tan(angle) * x1\n      x2 = Math.abs(w / 2 - x1) + w / 2\n      y2 = h / 2 + Math.abs(y1 - h / 2)\n      // y = -1 / tan * x + h / 2 + 1 / tan * w / 2\n      // y = tan * x\n      a =\n        (w / 2 / Math.tan(angle) + h / 2) /\n        (Math.tan(angle) + 1 / Math.tan(angle))\n      b = Math.tan(angle) * a\n      length = 2 * Math.sqrt(Math.pow(w / 2 - a, 2) + Math.pow(h / 2 - b, 2))\n      return\n    } else if (angle >= Math.PI) {\n      dfs(angle - Math.PI)\n\n      tmp = x1\n      x1 = x2\n      x2 = tmp\n      tmp = y1\n      y1 = y2\n      y2 = tmp\n    }\n  }\n\n  dfs(v)\n\n  return {\n    x1: x1 / w,\n    y1: y1 / h,\n    x2: x2 / w,\n    y2: y2 / h,\n    length,\n  }\n}\n\nfunction calcPercentage(\n  stops: ColorStop[],\n  length: number,\n  points: {\n    x1: number\n    y1: number\n    x2: number\n    y2: number\n  },\n  inheritableStyle: Record<string, string | number>\n) {\n  const { x1, x2, y1, y2 } = points\n  const p1 = !stops[0].offset\n    ? 0\n    : stops[0].offset.unit === '%'\n    ? Number(stops[0].offset.value) / 100\n    : lengthToNumber(\n        `${stops[0].offset.value}${stops[0].offset.unit}`,\n        inheritableStyle.fontSize as number,\n        length,\n        inheritableStyle,\n        true\n      ) / length\n  const p2 = !stops.at(-1).offset\n    ? 1\n    : stops.at(-1).offset.unit === '%'\n    ? Number(stops.at(-1).offset.value) / 100\n    : lengthToNumber(\n        `${stops.at(-1).offset.value}${stops.at(-1).offset.unit}`,\n        inheritableStyle.fontSize as number,\n        length,\n        inheritableStyle,\n        true\n      ) / length\n\n  const sx = (x2 - x1) * p1 + x1\n  const sy = (y2 - y1) * p1 + y1\n  const ex = (x2 - x1) * p2 + x1\n  const ey = (y2 - y1) * p2 + y1\n\n  return {\n    x1: sx,\n    y1: sy,\n    x2: ex,\n    y2: ey,\n  }\n}\n"
  },
  {
    "path": "src/builder/gradient/radial.ts",
    "content": "import {\n  parseRadialGradient,\n  RadialResult,\n  RadialPropertyValue,\n  ColorStop,\n} from 'css-gradient-parser'\nimport { buildXMLString, lengthToNumber } from '../../utils.js'\nimport { normalizeStops } from './utils.js'\n\nexport function buildRadialGradient(\n  {\n    id,\n    width,\n    height,\n    repeatX,\n    repeatY,\n  }: {\n    id: string\n    width: number\n    height: number\n    repeatX: boolean\n    repeatY: boolean\n  },\n  image: string,\n  dimensions: number[],\n  offsets: number[],\n  inheritableStyle: Record<string, number | string>,\n  from?: 'background' | 'mask'\n) {\n  const {\n    shape,\n    stops: colorStops,\n    position,\n    size,\n    repeating,\n  } = parseRadialGradient(image)\n  const [xDelta, yDelta] = dimensions\n\n  let cx: number = xDelta / 2\n  let cy: number = yDelta / 2\n\n  const pos = calcRadialGradient(\n    position.x,\n    position.y,\n    xDelta,\n    yDelta,\n    inheritableStyle.fontSize as number,\n    inheritableStyle\n  )\n  cx = pos.x\n  cy = pos.y\n\n  const colorStopTotalLength = calcColorStopTotalLength(\n    width,\n    colorStops,\n    repeating,\n    inheritableStyle\n  )\n\n  const stops = normalizeStops(\n    colorStopTotalLength,\n    colorStops,\n    inheritableStyle,\n    repeating,\n    from\n  )\n\n  const gradientId = `satori_radial_${id}`\n  const patternId = `satori_pattern_${id}`\n  const maskId = `satori_mask_${id}`\n\n  // https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/radial-gradient()#values\n  const spread = calcRadius(\n    shape as Shape,\n    size,\n    inheritableStyle.fontSize as number,\n    { x: cx, y: cy },\n    [xDelta, yDelta],\n    inheritableStyle,\n    repeating\n  )\n\n  const props = calcRadialGradientProps(\n    shape as Shape,\n    inheritableStyle.fontSize as number,\n    colorStops,\n    [xDelta, yDelta],\n    inheritableStyle,\n    repeating,\n    spread\n  )\n\n  // TODO: check for repeat-x/repeat-y\n  const defs = buildXMLString(\n    'pattern',\n    {\n      id: patternId,\n      x: offsets[0] / width,\n      y: offsets[1] / height,\n      width: repeatX ? xDelta / width : '1',\n      height: repeatY ? yDelta / height : '1',\n      patternUnits: 'objectBoundingBox',\n    },\n    buildXMLString(\n      'radialGradient',\n      {\n        id: gradientId,\n        ...props,\n      },\n      stops\n        .map((stop) =>\n          buildXMLString('stop', {\n            offset: stop.offset || 0,\n            'stop-color': stop.color,\n          })\n        )\n        .join('')\n    ) +\n      buildXMLString(\n        'mask',\n        {\n          id: maskId,\n        },\n        buildXMLString('rect', {\n          x: 0,\n          y: 0,\n          width: xDelta,\n          height: yDelta,\n          fill: '#fff',\n        })\n      ) +\n      buildXMLString('rect', {\n        x: 0,\n        y: 0,\n        width: xDelta,\n        height: yDelta,\n        fill: stops.at(-1)?.color || 'transparent',\n      }) +\n      buildXMLString(shape, {\n        cx: cx,\n        cy: cy,\n        width: xDelta,\n        height: yDelta,\n        ...spread,\n        fill: `url(#${gradientId})`,\n        mask: `url(#${maskId})`,\n      })\n  )\n\n  const result = [patternId, defs]\n  return result\n}\n\ntype PositionKeyWord = 'center' | 'left' | 'right' | 'top' | 'bottom'\n\nfunction calcColorStopTotalLength(\n  width: number,\n  stops: ColorStop[],\n  repeating: boolean,\n  inheritableStyle: Record<string, string | number>\n) {\n  if (!repeating) return width\n  const lastStop = stops.at(-1)\n  if (!lastStop || !lastStop.offset || lastStop.offset.unit === '%')\n    return width\n\n  return lengthToNumber(\n    `${lastStop.offset.value}${lastStop.offset.unit}`,\n    +inheritableStyle.fontSize,\n    width,\n    inheritableStyle,\n    true\n  )\n}\n\nfunction calcRadialGradient(\n  cx: RadialPropertyValue,\n  cy: RadialPropertyValue,\n  xDelta: number,\n  yDelta: number,\n  baseFontSize: number,\n  style: Record<string, string | number>\n) {\n  const pos: { x: number; y: number } = { x: xDelta / 2, y: yDelta / 2 }\n  if (cx.type === 'keyword') {\n    Object.assign(\n      pos,\n      calcPos(cx.value as PositionKeyWord, xDelta, yDelta, 'x')\n    )\n  } else {\n    pos.x =\n      lengthToNumber(\n        `${cx.value.value}${cx.value.unit}`,\n        baseFontSize,\n        xDelta,\n        style,\n        true\n      ) ?? xDelta / 2\n  }\n\n  if (cy.type === 'keyword') {\n    Object.assign(\n      pos,\n      calcPos(cy.value as PositionKeyWord, xDelta, yDelta, 'y')\n    )\n  } else {\n    pos.y =\n      lengthToNumber(\n        `${cy.value.value}${cy.value.unit}`,\n        baseFontSize,\n        yDelta,\n        style,\n        true\n      ) ?? yDelta / 2\n  }\n\n  return pos\n}\n\nfunction calcPos(\n  key: PositionKeyWord,\n  xDelta: number,\n  yDelta: number,\n  dir: 'x' | 'y'\n) {\n  switch (key) {\n    case 'center':\n      return { [dir]: dir === 'x' ? xDelta / 2 : yDelta / 2 }\n    case 'left':\n      return { x: 0 }\n    case 'top':\n      return { y: 0 }\n    case 'right':\n      return { x: xDelta }\n    case 'bottom':\n      return { y: yDelta }\n  }\n}\n\ntype Shape = 'circle' | 'ellipse'\n\nfunction calcRadialGradientProps(\n  shape: Shape,\n  baseFontSize: number,\n  colorStops: ColorStop[],\n  [xDelta, yDelta]: [number, number],\n  inheritableStyle: Record<string, string | number>,\n  repeating: boolean,\n  spread: Record<string, number>\n) {\n  const { r, rx, ratio = 1 } = spread\n  if (!repeating) {\n    return {\n      spreadMethod: 'pad',\n    }\n  }\n  const last = colorStops.at(-1)\n  const radius = shape === 'circle' ? r * 2 : rx * 2\n  return {\n    spreadMethod: 'repeat',\n    cx: '50%',\n    cy: '50%',\n    r:\n      last.offset.unit === '%'\n        ? `${\n            (Number(last.offset.value) * Math.min(yDelta / xDelta, 1)) / ratio\n          }%`\n        : Number(\n            lengthToNumber(\n              `${last.offset.value}${last.offset.unit}`,\n              baseFontSize,\n              xDelta,\n              inheritableStyle,\n              true\n            ) / radius\n          ),\n  }\n}\n\nfunction calcRadius(\n  shape: Shape,\n  endingShape: RadialResult['size'],\n  baseFontSize: number,\n  centerAxis: { x: number; y: number },\n  length: [number, number],\n  inheritableStyle: Record<string, string | number>,\n  repeating: boolean\n) {\n  const [xDelta, yDelta] = length\n  const { x: cx, y: cy } = centerAxis\n  const spread: Record<string, number> = {}\n  let fx = 0\n  let fy = 0\n\n  if (isSizeAllLength(endingShape)) {\n    if (endingShape.some((v) => v.value.value.startsWith('-'))) {\n      throw new Error(\n        'disallow setting negative values to the size of the shape. Check https://w3c.github.io/csswg-drafts/css-images/#valdef-rg-size-length-0'\n      )\n    }\n    if (shape === 'circle') {\n      Object.assign(spread, {\n        r: Number(\n          lengthToNumber(\n            `${endingShape[0].value.value}${endingShape[0].value.unit}`,\n            baseFontSize,\n            xDelta,\n            inheritableStyle,\n            true\n          )\n        ),\n      })\n    } else {\n      Object.assign(spread, {\n        rx: Number(\n          lengthToNumber(\n            `${endingShape[0].value.value}${endingShape[0].value.unit}`,\n            baseFontSize,\n            xDelta,\n            inheritableStyle,\n            true\n          )\n        ),\n        ry: Number(\n          lengthToNumber(\n            `${endingShape[1].value.value}${endingShape[1].value.unit}`,\n            baseFontSize,\n            yDelta,\n            inheritableStyle,\n            true\n          )\n        ),\n      })\n    }\n    patchSpread(spread, xDelta, yDelta, cx, cy, repeating, shape)\n    return spread\n  }\n\n  switch (endingShape[0].value) {\n    case 'farthest-corner':\n      fx = Math.max(Math.abs(xDelta - cx), Math.abs(cx))\n      fy = Math.max(Math.abs(yDelta - cy), Math.abs(cy))\n      break\n    case 'closest-corner':\n      fx = Math.min(Math.abs(xDelta - cx), Math.abs(cx))\n      fy = Math.min(Math.abs(yDelta - cy), Math.abs(cy))\n      break\n    case 'farthest-side':\n      if (shape === 'circle') {\n        spread.r = Math.max(\n          Math.abs(xDelta - cx),\n          Math.abs(cx),\n          Math.abs(yDelta - cy),\n          Math.abs(cy)\n        )\n      } else {\n        spread.rx = Math.max(Math.abs(xDelta - cx), Math.abs(cx))\n        spread.ry = Math.max(Math.abs(yDelta - cy), Math.abs(cy))\n      }\n      patchSpread(spread, xDelta, yDelta, cx, cy, repeating, shape)\n      return spread\n    case 'closest-side':\n      if (shape === 'circle') {\n        spread.r = Math.min(\n          Math.abs(xDelta - cx),\n          Math.abs(cx),\n          Math.abs(yDelta - cy),\n          Math.abs(cy)\n        )\n      } else {\n        spread.rx = Math.min(Math.abs(xDelta - cx), Math.abs(cx))\n        spread.ry = Math.min(Math.abs(yDelta - cy), Math.abs(cy))\n      }\n      patchSpread(spread, xDelta, yDelta, cx, cy, repeating, shape)\n\n      return spread\n  }\n  if (shape === 'circle') {\n    spread.r = Math.sqrt(fx * fx + fy * fy)\n  } else {\n    Object.assign(spread, f2r(fx, fy))\n  }\n\n  patchSpread(spread, xDelta, yDelta, cx, cy, repeating, shape)\n\n  return spread\n}\n\n// compare with farthest-corner to make it cover the whole container\nfunction patchSpread(\n  spread: Record<string, number>,\n  xDelta: number,\n  yDelta: number,\n  cx: number,\n  cy: number,\n  repeating: boolean,\n  shape: Shape\n) {\n  if (repeating) {\n    if (shape === 'ellipse') {\n      const mfx = Math.max(Math.abs(xDelta - cx), Math.abs(cx))\n      const mfy = Math.max(Math.abs(yDelta - cy), Math.abs(cy))\n\n      const { rx: mrx, ry: mry } = f2r(mfx, mfy)\n\n      spread.ratio = Math.max(mrx / spread.rx, mry / spread.ry)\n      if (spread.ratio > 1) {\n        spread.rx *= spread.ratio\n        spread.ry *= spread.ratio\n      }\n    } else {\n      const mfx = Math.max(Math.abs(xDelta - cx), Math.abs(cx))\n      const mfy = Math.max(Math.abs(yDelta - cy), Math.abs(cy))\n\n      const mr = Math.sqrt(mfx * mfx + mfy * mfy)\n\n      spread.ratio = mr / spread.r\n      if (spread.ratio > 1) {\n        spread.r = mr\n      }\n    }\n  }\n}\n\nfunction f2r(fx: number, fy: number) {\n  // Spec: https://drafts.csswg.org/css-images/#typedef-size\n  // Get the aspect ratio of the closest-side size.\n  const ratio = fy !== 0 ? fx / fy : 1\n\n  if (fx === 0) {\n    return {\n      rx: 0,\n      ry: 0,\n    }\n  } else {\n    // fx^2/a^2 + fy^2/b^2 = 1\n    // fx^2/(b*ratio)^2 + fy^2/b^2 = 1\n    // (fx^2+fy^2*ratio^2) = (b*ratio)^2\n    // b = sqrt(fx^2+fy^2*ratio^2)/ratio\n\n    const ry = Math.sqrt(fx * fx + fy * fy * ratio * ratio) / ratio\n    return {\n      ry,\n      rx: ry * ratio,\n    }\n  }\n}\n\nfunction isSizeAllLength(v: RadialPropertyValue[]): v is Array<{\n  type: 'length'\n  value: {\n    unit: string\n    value: string\n  }\n}> {\n  return !v.some((s) => s.type === 'keyword')\n}\n"
  },
  {
    "path": "src/builder/gradient/utils.ts",
    "content": "import { lengthToNumber } from '../../utils.js'\nimport cssColorParse from 'parse-css-color'\nimport type { ColorStop } from 'css-gradient-parser'\n\ninterface Stop {\n  color: string\n  offset?: number\n}\n\nexport function normalizeStops(\n  totalLength: number,\n  colorStops: ColorStop[],\n  inheritedStyle: Record<string, string | number>,\n  repeating: boolean,\n  from?: 'background' | 'mask'\n) {\n  // Resolve the color stops based on the spec:\n  // https://drafts.csswg.org/css-images/#color-stop-syntax\n  const stops: Stop[] = []\n  const lastColorStop = colorStops.at(-1)\n  const totalPercentage =\n    lastColorStop &&\n    lastColorStop.offset &&\n    lastColorStop.offset.unit === '%' &&\n    repeating\n      ? +lastColorStop.offset.value\n      : 100\n  for (const stop of colorStops) {\n    const { color } = stop\n    if (!stops.length) {\n      // First stop, ensure it's at the start.\n      stops.push({\n        offset: 0,\n        color,\n      })\n\n      if (!stop.offset) continue\n      if (stop.offset.value === '0') continue\n    }\n\n    // All offsets are relative values (0-1) in SVG.\n    const offset =\n      typeof stop.offset === 'undefined'\n        ? undefined\n        : stop.offset.unit === '%'\n        ? +stop.offset.value / totalPercentage\n        : Number(\n            lengthToNumber(\n              `${stop.offset.value}${stop.offset.unit}`,\n              inheritedStyle.fontSize as number,\n              totalLength,\n              inheritedStyle,\n              true\n            )\n          ) / totalLength\n\n    stops.push({\n      offset,\n      color,\n    })\n  }\n  if (!stops.length) {\n    stops.push({\n      offset: 0,\n      color: 'transparent',\n    })\n  }\n  // Last stop, ensure it's at the end.\n  const lastStop = stops[stops.length - 1]\n  if (lastStop.offset !== 1) {\n    if (typeof lastStop.offset === 'undefined') {\n      lastStop.offset = 1\n    } else if (repeating) {\n      stops[stops.length - 1] = {\n        offset: 1,\n        color: lastStop.color,\n      }\n    } else {\n      stops.push({\n        offset: 1,\n        color: lastStop.color,\n      })\n    }\n  }\n\n  let previousStop = 0\n  let nextStop = 1\n  // Evenly distribute the missing stop offsets.\n  for (let i = 0; i < stops.length; i++) {\n    if (typeof stops[i].offset === 'undefined') {\n      // Find the next stop that has an offset.\n      if (nextStop < i) nextStop = i\n      while (typeof stops[nextStop].offset === 'undefined') nextStop++\n\n      stops[i].offset =\n        ((stops[nextStop].offset - stops[previousStop].offset) /\n          (nextStop - previousStop)) *\n          (i - previousStop) +\n        stops[previousStop].offset\n    } else {\n      previousStop = i\n    }\n  }\n\n  if (from === 'mask') {\n    return stops.map((stop) => {\n      const color = cssColorParse(stop.color)\n      if (!color) return stop\n      if (color.alpha === 0) {\n        return { ...stop, color: `rgba(0, 0, 0, 1)` }\n      } else {\n        return { ...stop, color: `rgba(255, 255, 255, ${color.alpha})` }\n      }\n    })\n  }\n\n  return stops\n}\n"
  },
  {
    "path": "src/builder/mask-image.ts",
    "content": "import { buildXMLString } from '../utils.js'\nimport buildBackgroundImage from './background-image.js'\nimport type { MaskProperty } from '../parser/mask.js'\n\nconst genMaskImageId = (id: string) => `satori_mi-${id}`\n\nexport default async function buildMaskImage(\n  v: {\n    id: string\n    left: number\n    top: number\n    width: number\n    height: number\n  },\n  style: Record<string, string | number>,\n  inheritedStyle: Record<string, string | number>\n): Promise<[string, string]> {\n  if (!style.maskImage) return ['', '']\n  const { left, top, width, height, id } = v\n  const maskImage = style.maskImage as unknown as MaskProperty[]\n  const length = maskImage.length\n  if (!length) return ['', '']\n  const miId = genMaskImageId(id)\n\n  let mask = ''\n\n  for (let i = 0; i < length; i++) {\n    const m = maskImage[i]\n\n    const [_id, def] = await buildBackgroundImage(\n      { id: `${miId}-${i}`, left, top, width, height },\n      m,\n      inheritedStyle,\n      'mask'\n    )\n\n    mask +=\n      def +\n      buildXMLString('rect', {\n        x: left,\n        y: top,\n        width,\n        height,\n        fill: `url(#${_id})`,\n      })\n  }\n\n  mask = buildXMLString('mask', { id: miId }, mask)\n\n  return [miId, mask]\n}\n"
  },
  {
    "path": "src/builder/overflow.ts",
    "content": "/**\n * Generate clip path for the given element.\n */\n\nimport { buildXMLString } from '../utils.js'\nimport mask from './content-mask.js'\nimport { buildClipPath, genClipPathId } from './clip-path.js'\n\nexport default function overflow(\n  {\n    left,\n    top,\n    width,\n    height,\n    path,\n    matrix,\n    id,\n    currentClipPath,\n    src,\n  }: {\n    left: number\n    top: number\n    width: number\n    height: number\n    path: string\n    matrix: string | undefined\n    id: string\n    currentClipPath: string | string\n    src?: string\n  },\n  style: Record<string, string | number>,\n  inheritableStyle: Record<string, string | number>\n) {\n  let overflowClipPath = ''\n  const clipPath =\n    style.clipPath && style.clipPath !== 'none'\n      ? buildClipPath(\n          { left, top, width, height, path, id, matrix, currentClipPath, src },\n          style as Record<string, number>,\n          inheritableStyle\n        )\n      : ''\n\n  if (style.overflow !== 'hidden' && !src) {\n    overflowClipPath = ''\n  } else {\n    const _id = clipPath ? `satori_ocp-${id}` : genClipPathId(id)\n\n    overflowClipPath = buildXMLString(\n      'clipPath',\n      {\n        id: _id,\n        'clip-path': currentClipPath,\n      },\n      buildXMLString(path ? 'path' : 'rect', {\n        x: left,\n        y: top,\n        width,\n        height,\n        d: path ? path : undefined,\n        // add transformation matrix to clip path if overflow is hidden AND a\n        // transformation style is defined, otherwise children will be clipped\n        // relative to the parent's original plane instead of the transformed\n        // plane\n        transform:\n          style.overflow === 'hidden' && style.transform && matrix\n            ? matrix\n            : undefined,\n      })\n    )\n  }\n\n  const contentMask = mask(\n    {\n      id: `satori_om-${id}`,\n      left,\n      top,\n      width,\n      height,\n      matrix,\n      borderOnly: src ? false : true,\n    },\n    style\n  )\n\n  return clipPath + overflowClipPath + contentMask\n}\n"
  },
  {
    "path": "src/builder/rect.ts",
    "content": "import type { ParsedTransformOrigin } from '../transform-origin.js'\n\nimport backgroundImage from './background-image.js'\nimport radius, { getBorderRadiusClipPath } from './border-radius.js'\nimport { boxShadow } from './shadow.js'\nimport transform from './transform.js'\nimport overflow from './overflow.js'\nimport { buildXMLString } from '../utils.js'\nimport border, { getBorderClipPath } from './border.js'\nimport { genClipPath } from './clip-path.js'\nimport buildMaskImage from './mask-image.js'\nimport CssDimension from '../vendor/parse-css-dimension/index.js'\n\n/**\n * Parse object-position value into [xOffset, yOffset] in pixels.\n * Supports keywords (left, center, right, top, bottom), percentages, and lengths.\n * Similar to background-position parsing.\n */\nfunction parseObjectPosition(\n  position: string,\n  containerWidth: number,\n  containerHeight: number\n): [number, number] {\n  const parts = position.toLowerCase().trim().split(/\\s+/)\n\n  // Convert keyword to percentage\n  const keywordToPercent = (keyword: string, axis: 'x' | 'y'): string => {\n    const map = {\n      left: '0%',\n      center: '50%',\n      right: '100%',\n      top: '0%',\n      bottom: '100%',\n    }\n    return map[keyword] || keyword\n  }\n\n  let xValue: string\n  let yValue: string\n\n  if (parts.length === 1) {\n    const part = parts[0]\n    // Single value\n    if (part === 'left' || part === 'center' || part === 'right') {\n      xValue = keywordToPercent(part, 'x')\n      yValue = '50%' // center\n    } else if (part === 'top' || part === 'bottom') {\n      xValue = '50%' // center\n      yValue = keywordToPercent(part, 'y')\n    } else {\n      // Assume it's x value, y defaults to center\n      xValue = part\n      yValue = '50%'\n    }\n  } else {\n    // Two or more values\n    const first = parts[0]\n    const second = parts[1]\n\n    // Check if first is a y-axis keyword (top/bottom)\n    if (first === 'top' || first === 'bottom') {\n      yValue = keywordToPercent(first, 'y')\n      if (second === 'left' || second === 'right' || second === 'center') {\n        xValue = keywordToPercent(second, 'x')\n      } else {\n        // Second is a length/percentage, default x to center\n        xValue = '50%'\n        yValue =\n          first === 'top' || first === 'bottom'\n            ? keywordToPercent(first, 'y')\n            : second\n      }\n    } else {\n      // Normal order: x then y\n      xValue = keywordToPercent(first, 'x')\n      yValue = keywordToPercent(second, 'y')\n    }\n  }\n\n  // Convert to absolute pixels\n  const parseValue = (value: string, containerSize: number): number => {\n    try {\n      if (value.endsWith('%')) {\n        return (containerSize * parseFloat(value)) / 100\n      }\n      const parsed = new CssDimension(value)\n      if (parsed.type === 'length' || parsed.type === 'number') {\n        return parsed.value\n      }\n      return 0\n    } catch (e) {\n      return 0\n    }\n  }\n\n  return [\n    parseValue(xValue, containerWidth),\n    parseValue(yValue, containerHeight),\n  ]\n}\n\nexport default async function rect(\n  {\n    id,\n    left,\n    top,\n    width,\n    height,\n    isInheritingTransform,\n    src,\n    debug,\n  }: {\n    id: string\n    left: number\n    top: number\n    width: number\n    height: number\n    isInheritingTransform: boolean\n    src?: string\n    debug?: boolean\n  },\n  style: Record<string, number | string>,\n  inheritableStyle: Record<string, number | string>\n) {\n  if (style.display === 'none') return ''\n\n  const isImage = !!src\n\n  let type: 'rect' | 'path' = 'rect'\n  let matrix = ''\n  let defs = ''\n  let fills: string[] = []\n  let opacity = 1\n  let extra = ''\n\n  if (style.backgroundColor) {\n    fills.push(style.backgroundColor as string)\n  }\n\n  if (style.opacity !== undefined) {\n    opacity = +style.opacity\n  }\n\n  if (style.transform) {\n    matrix = transform(\n      {\n        left,\n        top,\n        width,\n        height,\n      },\n      style.transform as unknown as number[],\n      isInheritingTransform,\n      style.transformOrigin as ParsedTransformOrigin | undefined\n    )\n  }\n\n  let backgroundShapes = ''\n  if (style.backgroundImage) {\n    const backgrounds: string[][] = []\n\n    for (\n      let index = 0;\n      index < (style.backgroundImage as any).length;\n      index++\n    ) {\n      const background = (style.backgroundImage as any)[index]\n      const image = await backgroundImage(\n        { id: id + '_' + index, width, height, left, top },\n        background,\n        inheritableStyle\n      )\n      if (image) {\n        // Background images that come first in the array are rendered last.\n        backgrounds.unshift(image)\n      }\n    }\n\n    for (const background of backgrounds) {\n      fills.push(`url(#${background[0]})`)\n      defs += background[1]\n      if (background[2]) {\n        backgroundShapes += background[2]\n      }\n    }\n  }\n\n  const [miId, mi] = await buildMaskImage(\n    { id, left, top, width, height },\n    style,\n    inheritableStyle\n  )\n\n  defs += mi\n  const maskId = miId\n    ? `url(#${miId})`\n    : style._inheritedMaskId\n    ? `url(#${style._inheritedMaskId})`\n    : undefined\n\n  const path = radius(\n    { left, top, width, height },\n    style as Record<string, number>\n  )\n  if (path) {\n    type = 'path'\n  }\n\n  const clipPathId = style._inheritedClipPathId as number | undefined\n\n  if (debug) {\n    extra = buildXMLString('rect', {\n      x: left,\n      y: top,\n      width,\n      height,\n      fill: 'transparent',\n      stroke: '#ff5757',\n      'stroke-width': 1,\n      transform: matrix || undefined,\n      'clip-path': clipPathId ? `url(#${clipPathId})` : undefined,\n    })\n  }\n\n  const { backgroundClip, filter: cssFilter } = style\n\n  const currentClipPath =\n    backgroundClip === 'text'\n      ? `url(#satori_bct-${id})`\n      : clipPathId\n      ? `url(#${clipPathId})`\n      : style.clipPath\n      ? genClipPath(id)\n      : undefined\n\n  const clip = overflow(\n    { left, top, width, height, path, id, matrix, currentClipPath, src },\n    style as Record<string, number>,\n    inheritableStyle\n  )\n\n  // Each background generates a new rectangle.\n  // @TODO: Not sure if this is the best way to do it, maybe <pattern> with\n  // multiple <image>s is better.\n  let shape = fills\n    .map((fill) =>\n      buildXMLString(type, {\n        x: left,\n        y: top,\n        width,\n        height,\n        fill,\n        d: path ? path : undefined,\n        transform: matrix ? matrix : undefined,\n        'clip-path': style.transform ? undefined : currentClipPath,\n        style: cssFilter ? `filter:${cssFilter}` : undefined,\n        mask: style.transform ? undefined : maskId,\n      })\n    )\n    .join('')\n\n  const borderClip = getBorderClipPath(\n    {\n      id,\n      left,\n      top,\n      width,\n      height,\n      currentClipPathId: clipPathId,\n      borderPath: path,\n      borderType: type,\n    },\n    style\n  )\n\n  // border radius for images with transform property\n  let imageBorderRadius = undefined\n\n  // If it's an image (<img>) tag, we add an extra layer of the image itself.\n  if (isImage) {\n    // We need to subtract the border and padding sizes from the image size.\n    const offsetLeft =\n      ((style.borderLeftWidth as number) || 0) +\n      ((style.paddingLeft as number) || 0)\n    const offsetTop =\n      ((style.borderTopWidth as number) || 0) +\n      ((style.paddingTop as number) || 0)\n    const offsetRight =\n      ((style.borderRightWidth as number) || 0) +\n      ((style.paddingRight as number) || 0)\n    const offsetBottom =\n      ((style.borderBottomWidth as number) || 0) +\n      ((style.paddingBottom as number) || 0)\n\n    const containerInnerWidth = width - offsetLeft - offsetRight\n    const containerInnerHeight = height - offsetTop - offsetBottom\n\n    // Parse object-position\n    const position = (style.objectPosition || 'center').toString()\n    const [objPosX, objPosY] = parseObjectPosition(\n      position,\n      containerInnerWidth,\n      containerInnerHeight\n    )\n\n    // Get natural image dimensions if available\n    const naturalWidth = (style.__naturalWidth as number) || containerInnerWidth\n    const naturalHeight =\n      (style.__naturalHeight as number) || containerInnerHeight\n\n    // Calculate objectFit behavior\n    let preserveAspectRatio: string\n    let imageWidth = containerInnerWidth\n    let imageHeight = containerInnerHeight\n    let imageX = left + offsetLeft\n    let imageY = top + offsetTop\n\n    if (style.objectFit === 'contain') {\n      // Scale to fit within container while preserving aspect ratio\n      const scaleX = containerInnerWidth / naturalWidth\n      const scaleY = containerInnerHeight / naturalHeight\n      const scale = Math.min(scaleX, scaleY)\n\n      imageWidth = naturalWidth * scale\n      imageHeight = naturalHeight * scale\n\n      // Apply object-position to center the image within the container\n      imageX =\n        left +\n        offsetLeft +\n        objPosX -\n        (imageWidth * objPosX) / containerInnerWidth\n      imageY =\n        top +\n        offsetTop +\n        objPosY -\n        (imageHeight * objPosY) / containerInnerHeight\n\n      preserveAspectRatio = 'none'\n    } else if (style.objectFit === 'cover') {\n      // Scale to cover the container while preserving aspect ratio\n      const scaleX = containerInnerWidth / naturalWidth\n      const scaleY = containerInnerHeight / naturalHeight\n      const scale = Math.max(scaleX, scaleY)\n\n      imageWidth = naturalWidth * scale\n      imageHeight = naturalHeight * scale\n\n      // Apply object-position\n      imageX =\n        left +\n        offsetLeft +\n        objPosX -\n        (imageWidth * objPosX) / containerInnerWidth\n      imageY =\n        top +\n        offsetTop +\n        objPosY -\n        (imageHeight * objPosY) / containerInnerHeight\n\n      preserveAspectRatio = 'none'\n    } else if (style.objectFit === 'fill') {\n      // Stretch to fill (ignore aspect ratio)\n      preserveAspectRatio = 'none'\n    } else if (style.objectFit === 'scale-down') {\n      if (naturalWidth && naturalHeight) {\n        // Calculate if we need to scale down\n        const scaleX = containerInnerWidth / naturalWidth\n        const scaleY = containerInnerHeight / naturalHeight\n        const minScale = Math.min(scaleX, scaleY)\n\n        if (minScale >= 1) {\n          // Image is smaller than or equal to container\n          // Use natural size (don't scale up)\n          imageWidth = naturalWidth\n          imageHeight = naturalHeight\n          preserveAspectRatio = 'none'\n\n          // Apply object-position to position the un-scaled image\n          imageX =\n            left +\n            offsetLeft +\n            objPosX -\n            (imageWidth * objPosX) / containerInnerWidth\n          imageY =\n            top +\n            offsetTop +\n            objPosY -\n            (imageHeight * objPosY) / containerInnerHeight\n        } else {\n          // Image is larger than container, scale down like 'contain'\n          const scale = minScale\n          imageWidth = naturalWidth * scale\n          imageHeight = naturalHeight * scale\n\n          // Apply object-position\n          imageX =\n            left +\n            offsetLeft +\n            objPosX -\n            (imageWidth * objPosX) / containerInnerWidth\n          imageY =\n            top +\n            offsetTop +\n            objPosY -\n            (imageHeight * objPosY) / containerInnerHeight\n\n          preserveAspectRatio = 'none'\n        }\n      } else {\n        // Fall back to 'contain' behavior if natural dimensions are unavailable\n        const scaleX = containerInnerWidth / naturalWidth\n        const scaleY = containerInnerHeight / naturalHeight\n        const scale = Math.min(scaleX, scaleY)\n\n        imageWidth = naturalWidth * scale\n        imageHeight = naturalHeight * scale\n\n        imageX =\n          left +\n          offsetLeft +\n          objPosX -\n          (imageWidth * objPosX) / containerInnerWidth\n        imageY =\n          top +\n          offsetTop +\n          objPosY -\n          (imageHeight * objPosY) / containerInnerHeight\n\n        preserveAspectRatio = 'none'\n      }\n    } else {\n      // Default/none: fill (stretch)\n      preserveAspectRatio = 'none'\n    }\n\n    if (style.transform) {\n      imageBorderRadius = getBorderRadiusClipPath(\n        {\n          id,\n          borderRadiusPath: path,\n          borderType: type,\n          left,\n          top,\n          width,\n          height,\n        },\n        style\n      )\n    }\n\n    shape += buildXMLString('image', {\n      x: imageX,\n      y: imageY,\n      width: imageWidth,\n      height: imageHeight,\n      href: src,\n      preserveAspectRatio,\n      transform: matrix ? matrix : undefined,\n      style: cssFilter ? `filter:${cssFilter}` : undefined,\n      'clip-path': style.transform\n        ? imageBorderRadius\n          ? `url(#${imageBorderRadius[1]})`\n          : undefined\n        : `url(#satori_cp-${id})`,\n      mask: style.transform\n        ? undefined\n        : miId\n        ? `url(#${miId})`\n        : `url(#satori_om-${id})`,\n    })\n  }\n\n  if (borderClip) {\n    defs += borderClip[0]\n    const rectClipId = borderClip[1]\n\n    shape += border(\n      {\n        left,\n        top,\n        width,\n        height,\n        props: {\n          transform: matrix ? matrix : undefined,\n          // When using `background-clip: text`, we need to draw the extra border because\n          // it shouldn't be clipped by the clip path, so we are not using currentClipPath here.\n          'clip-path': `url(#${rectClipId})`,\n        },\n      },\n      style\n    )\n  }\n\n  // box-shadow.\n  const shadow = boxShadow(\n    {\n      width,\n      height,\n      id,\n      opacity,\n      shape: buildXMLString(type, {\n        x: left,\n        y: top,\n        width,\n        height,\n        fill: '#fff',\n        stroke: '#fff',\n        'stroke-width': 0,\n        d: path ? path : undefined,\n        transform: matrix ? matrix : undefined,\n        'clip-path': currentClipPath,\n        mask: maskId,\n      }),\n    },\n    style\n  )\n\n  return (\n    (defs ? buildXMLString('defs', {}, defs) : '') +\n    (shadow ? shadow[0] : '') +\n    (imageBorderRadius ? imageBorderRadius[0] : '') +\n    clip +\n    (opacity !== 1 ? `<g opacity=\"${opacity}\">` : '') +\n    (style.transform && (currentClipPath || maskId)\n      ? `<g${currentClipPath ? ` clip-path=\"${currentClipPath}\"` : ''}${\n          maskId ? ` mask=\"${maskId}\"` : ''\n        }>`\n      : '') +\n    (backgroundShapes || shape) +\n    (style.transform && (currentClipPath || maskId) ? '</g>' : '') +\n    (opacity !== 1 ? `</g>` : '') +\n    (shadow ? shadow[1] : '') +\n    extra\n  )\n}\n"
  },
  {
    "path": "src/builder/shadow.ts",
    "content": "// @TODO: It seems that SVG filters are pretty expensive for resvg, PNG\n// generation time 10x'd when adding this filter (WASM in browser).\n// https://drafts.fxtf.org/filter-effects/#feGaussianBlurElement\n\nimport { buildXMLString } from '../utils.js'\n\nfunction shiftPath(path: string, dx: number, dy: number) {\n  return path.replace(\n    /([MA])([0-9.-]+),([0-9.-]+)/g,\n    function (_, command, x, y) {\n      return command + (parseFloat(x) + dx) + ',' + (parseFloat(y) + dy)\n    }\n  )\n}\n\n// The scale is used to make the filter area larger than the bounding box,\n// because usually the given measured text bounding is larger than the path\n// bounding.\n// The text bounding box is measured via the font metrics, which is not the same\n// as the actual content. For example, the text bounding box of \"A\" is larger\n// than the actual \"a\" path but they have the same font metrics.\n// This scale can be adjusted to prevent the filter from cutting off the text.\nconst SCALE = 1.1\n\nexport function buildDropShadow(\n  { id, width, height }: { id: string; width: number; height: number },\n  style: {\n    shadowColor: string[]\n    shadowOffset: {\n      width: number\n      height: number\n    }[]\n    shadowRadius: number[]\n  },\n  transparentText = false\n) {\n  if (\n    !style.shadowColor ||\n    !style.shadowOffset ||\n    typeof style.shadowRadius === 'undefined'\n  ) {\n    return ''\n  }\n\n  const shadowCount = style.shadowColor.length\n  let effects = ''\n  let merge = ''\n\n  // There could be multiple shadows, we need to get the maximum bounding box\n  // and use `feMerge` to merge them together.\n  let left = 0\n  let right = width\n  let top = 0\n  let bottom = height\n  for (let i = 0; i < shadowCount; i++) {\n    // Expand the area for the filter to prevent it from cutting off.\n    const grow = (style.shadowRadius[i] * style.shadowRadius[i]) / 4\n    left = Math.min(style.shadowOffset[i].width - grow, left)\n    right = Math.max(style.shadowOffset[i].width + grow + width, right)\n    top = Math.min(style.shadowOffset[i].height - grow, top)\n    bottom = Math.max(style.shadowOffset[i].height + grow + height, bottom)\n\n    if (transparentText) {\n      // For transparent text, use primitive filters instead of feDropShadow\n      // because feDropShadow automatically includes source in output and\n      // source has unwanted text color\n      const resultId = `satori_s-${id}-result-${i}`\n      effects +=\n        buildXMLString('feGaussianBlur', {\n          in: 'SourceAlpha',\n          stdDeviation: style.shadowRadius[i] / 2,\n          result: `${resultId}-blur`,\n        }) +\n        buildXMLString('feOffset', {\n          in: `${resultId}-blur`,\n          dx: style.shadowOffset[i].width,\n          dy: style.shadowOffset[i].height,\n          result: `${resultId}-offset`,\n        }) +\n        buildXMLString('feFlood', {\n          'flood-color': style.shadowColor[i],\n          'flood-opacity': 1,\n          result: `${resultId}-color`,\n        }) +\n        buildXMLString('feComposite', {\n          in: `${resultId}-color`,\n          in2: `${resultId}-offset`,\n          operator: 'in',\n          result: shadowCount > 1 ? resultId : undefined,\n        })\n    } else {\n      effects += buildXMLString('feDropShadow', {\n        dx: style.shadowOffset[i].width,\n        dy: style.shadowOffset[i].height,\n        stdDeviation:\n          // According to the spec, we use the half of the blur radius as the standard\n          // deviation for the filter.\n          // > the image that would be generated by applying to the shadow a Gaussian\n          // > blur with a standard deviation equal to half the blur radius\n          // > https://www.w3.org/TR/css-backgrounds-3/#shadow-blur\n          style.shadowRadius[i] / 2,\n        'flood-color': style.shadowColor[i],\n        'flood-opacity': 1,\n        ...(shadowCount > 1\n          ? {\n              in: 'SourceGraphic',\n              result: `satori_s-${id}-result-${i}`,\n            }\n          : {}),\n      })\n    }\n\n    if (shadowCount > 1) {\n      // Merge needs to be in reverse order.\n      merge =\n        buildXMLString('feMergeNode', {\n          in: `satori_s-${id}-result-${i}`,\n        }) + merge\n    }\n  }\n\n  return buildXMLString(\n    'filter',\n    {\n      id: `satori_s-${id}`,\n      x: ((left / width) * 100 * SCALE).toFixed(2) + '%',\n      y: ((top / height) * 100 * SCALE).toFixed(2) + '%',\n      width: (((right - left) / width) * 100 * SCALE).toFixed(2) + '%',\n      height: (((bottom - top) / height) * 100 * SCALE).toFixed(2) + '%',\n    },\n    effects + (merge ? buildXMLString('feMerge', {}, merge) : '')\n  )\n}\n\nexport function boxShadow(\n  {\n    width,\n    height,\n    shape,\n    opacity,\n    id,\n  }: {\n    width: number\n    height: number\n    shape: string\n    opacity: number\n    id: string\n  },\n  style: Record<string, any>\n) {\n  if (!style.boxShadow) return null\n\n  let shadow = ''\n  let innerShadow = ''\n\n  for (let i = style.boxShadow.length - 1; i >= 0; i--) {\n    let s = ''\n\n    const shadowStyle = style.boxShadow[i]\n\n    if (shadowStyle.spreadRadius && shadowStyle.inset) {\n      shadowStyle.spreadRadius = -shadowStyle.spreadRadius\n    }\n\n    // Expand the area for the filter to prevent it from cutting off.\n    const grow =\n      (shadowStyle.blurRadius * shadowStyle.blurRadius) / 4 +\n      (shadowStyle.spreadRadius || 0)\n\n    const left = Math.min(\n      -grow - (shadowStyle.inset ? shadowStyle.offsetX : 0),\n      0\n    )\n    const right = Math.max(\n      grow + width - (shadowStyle.inset ? shadowStyle.offsetX : 0),\n      width\n    )\n    const top = Math.min(\n      -grow - (shadowStyle.inset ? shadowStyle.offsetY : 0),\n      0\n    )\n    const bottom = Math.max(\n      grow + height - (shadowStyle.inset ? shadowStyle.offsetY : 0),\n      height\n    )\n\n    const sid = `satori_s-${id}-${i}`\n    const maskId = `satori_ms-${id}-${i}`\n    const shapeWithSpread = shadowStyle.spreadRadius\n      ? shape.replace(\n          'stroke-width=\"0\"',\n          `stroke-width=\"${shadowStyle.spreadRadius * 2}\"`\n        )\n      : shape\n\n    s += buildXMLString(\n      'mask',\n      {\n        id: maskId,\n        maskUnits: 'userSpaceOnUse',\n      },\n      buildXMLString('rect', {\n        x: 0,\n        y: 0,\n        width: style._viewportWidth || '100%',\n        height: style._viewportHeight || '100%',\n        fill: shadowStyle.inset ? '#000' : '#fff',\n      }) +\n        shapeWithSpread\n          .replace(\n            'fill=\"#fff\"',\n            shadowStyle.inset ? 'fill=\"#fff\"' : 'fill=\"#000\"'\n          )\n          .replace('stroke=\"#fff\"', '')\n    )\n\n    let finalShape = shapeWithSpread\n      .replace(/d=\"([^\"]+)\"/, (_, path) => {\n        return (\n          'd=\"' +\n          shiftPath(path, shadowStyle.offsetX, shadowStyle.offsetY) +\n          '\"'\n        )\n      })\n      .replace(/x=\"([^\"]+)\"/, (_, x) => {\n        return 'x=\"' + (parseFloat(x) + shadowStyle.offsetX) + '\"'\n      })\n      .replace(/y=\"([^\"]+)\"/, (_, y) => {\n        return 'y=\"' + (parseFloat(y) + shadowStyle.offsetY) + '\"'\n      })\n\n    // Negative spread radius, we need another mask here.\n    if (shadowStyle.spreadRadius && shadowStyle.spreadRadius < 0) {\n      s += buildXMLString(\n        'mask',\n        {\n          id: maskId + '-neg',\n          maskUnits: 'userSpaceOnUse',\n        },\n        finalShape\n          .replace('stroke=\"#fff\"', 'stroke=\"#000\"')\n          .replace(\n            /stroke-width=\"[^\"]+\"/,\n            `stroke-width=\"${-shadowStyle.spreadRadius * 2}\"`\n          )\n      )\n    }\n\n    if (shadowStyle.spreadRadius && shadowStyle.spreadRadius < 0) {\n      finalShape = buildXMLString(\n        'g',\n        {\n          mask: `url(#${maskId}-neg)`,\n        },\n        finalShape\n      )\n    }\n\n    s +=\n      buildXMLString(\n        'defs',\n        {},\n        buildXMLString(\n          'filter',\n          {\n            id: sid,\n            x: `${(left / width) * 100}%`,\n            y: `${(top / height) * 100}%`,\n            width: `${((right - left) / width) * 100}%`,\n            height: `${((bottom - top) / height) * 100}%`,\n          },\n          buildXMLString('feGaussianBlur', {\n            // According to the spec, we use the half of the blur radius as the standard\n            // deviation for the filter.\n            // > the image that would be generated by applying to the shadow a Gaussian\n            // > blur with a standard deviation equal to half the blur radius\n            // > https://www.w3.org/TR/css-backgrounds-3/#shadow-blur\n            stdDeviation: shadowStyle.blurRadius / 2,\n            result: 'b',\n          }) +\n            buildXMLString('feFlood', {\n              'flood-color': shadowStyle.color,\n              in: 'SourceGraphic',\n              result: 'f',\n            }) +\n            buildXMLString('feComposite', {\n              in: 'f',\n              in2: 'b',\n              operator: shadowStyle.inset ? 'out' : 'in',\n            })\n        )\n      ) +\n      buildXMLString(\n        'g',\n        {\n          mask: `url(#${maskId})`,\n          filter: `url(#${sid})`,\n          opacity: opacity,\n        },\n        finalShape\n      )\n\n    if (shadowStyle.inset) {\n      innerShadow += s\n    } else {\n      shadow += s\n    }\n  }\n\n  return [shadow, innerShadow]\n}\n"
  },
  {
    "path": "src/builder/svg.ts",
    "content": "import { buildXMLString } from '../utils.js'\n\nexport default function svg({\n  width,\n  height,\n  content,\n}: {\n  width: number\n  height: number\n  content: string\n}) {\n  return buildXMLString(\n    'svg',\n    {\n      width,\n      height,\n      viewBox: `0 0 ${width} ${height}`,\n      xmlns: 'http://www.w3.org/2000/svg',\n    },\n    content\n  )\n}\n"
  },
  {
    "path": "src/builder/text-decoration.ts",
    "content": "import { buildXMLString } from '../utils.js'\nimport type { GlyphBox } from '../font.js'\n\nfunction buildSkipInkSegments(\n  start: number,\n  end: number,\n  glyphBoxes: GlyphBox[],\n  y: number,\n  strokeWidth: number,\n  baseline: number\n) {\n  const halfStroke = strokeWidth / 2\n  const bleed = Math.max(halfStroke, strokeWidth * 1.25)\n  const skipRanges: [number, number][] = []\n\n  for (const box of glyphBoxes) {\n    // Only skip glyphs that actually cross the underline position and extend below the baseline.\n    if (box.y2 < baseline + halfStroke || box.y1 > y + halfStroke) continue\n\n    const from = Math.max(start, box.x1 - bleed)\n    const to = Math.min(end, box.x2 + bleed)\n\n    if (from >= to) continue\n    if (skipRanges.length === 0) {\n      skipRanges.push([from, to])\n      continue\n    }\n\n    const last = skipRanges[skipRanges.length - 1]\n    if (from <= last[1]) {\n      last[1] = Math.max(last[1], to)\n    } else {\n      skipRanges.push([from, to])\n    }\n  }\n\n  if (!skipRanges.length) {\n    return [[start, end]] as [number, number][]\n  }\n\n  const segments: [number, number][] = []\n  let cursor = start\n\n  for (const [from, to] of skipRanges) {\n    if (from > cursor) {\n      segments.push([cursor, from])\n    }\n    cursor = Math.max(cursor, to)\n    if (cursor >= end) break\n  }\n\n  if (cursor < end) {\n    segments.push([cursor, end])\n  }\n\n  return segments\n}\n\nexport default function buildDecoration(\n  {\n    width,\n    left,\n    top,\n    ascender,\n    clipPathId,\n    matrix,\n    glyphBoxes,\n  }: {\n    width: number\n    left: number\n    top: number\n    ascender: number\n    clipPathId?: string\n    matrix?: string\n    glyphBoxes?: GlyphBox[]\n  },\n  style: Record<string, any>\n) {\n  const {\n    textDecorationColor,\n    textDecorationStyle,\n    textDecorationLine,\n    textDecorationSkipInk,\n    fontSize,\n    color,\n  } = style\n  if (!textDecorationLine || textDecorationLine === 'none') return ''\n\n  // The UA should use such font-based information when choosing auto line thicknesses wherever appropriate.\n  // https://drafts.csswg.org/css-text-decor-4/#text-decoration-thickness\n  const height = Math.max(1, fontSize * 0.1)\n\n  const y =\n    textDecorationLine === 'line-through'\n      ? top + ascender * 0.7\n      : textDecorationLine === 'underline'\n      ? top + ascender * 1.1\n      : top\n\n  const dasharray =\n    textDecorationStyle === 'dashed'\n      ? `${height * 1.2} ${height * 2}`\n      : textDecorationStyle === 'dotted'\n      ? `0 ${height * 2}`\n      : undefined\n\n  const applySkipInk =\n    textDecorationLine === 'underline' &&\n    (textDecorationSkipInk || 'auto') !== 'none' &&\n    glyphBoxes?.length\n\n  const baseline = top + ascender\n\n  const segments = applySkipInk\n    ? buildSkipInkSegments(left, left + width, glyphBoxes, y, height, baseline)\n    : ([[left, left + width]] as [number, number][])\n\n  // https://www.w3.org/TR/css-backgrounds-3/#valdef-line-style-double\n  const extraLine =\n    textDecorationStyle === 'double'\n      ? segments\n          .map(([x1, x2]) =>\n            buildXMLString('line', {\n              x1,\n              y1: y + height + 1,\n              x2,\n              y2: y + height + 1,\n              stroke: textDecorationColor || color,\n              'stroke-width': height,\n              'stroke-dasharray': dasharray,\n              'stroke-linecap':\n                textDecorationStyle === 'dotted' ? 'round' : 'square',\n              transform: matrix,\n            })\n          )\n          .join('')\n      : ''\n\n  return (\n    (clipPathId ? `<g clip-path=\"url(#${clipPathId})\">` : '') +\n    segments\n      .map(([x1, x2]) =>\n        buildXMLString('line', {\n          x1,\n          y1: y,\n          x2,\n          y2: y,\n          stroke: textDecorationColor || color,\n          'stroke-width': height,\n          'stroke-dasharray': dasharray,\n          'stroke-linecap':\n            textDecorationStyle === 'dotted' ? 'round' : 'square',\n          transform: matrix,\n        })\n      )\n      .join('') +\n    extraLine +\n    (clipPathId ? '</g>' : '')\n  )\n}\n"
  },
  {
    "path": "src/builder/text.ts",
    "content": "import escapeHTML from 'escape-html'\nimport type { ParsedTransformOrigin } from '../transform-origin.js'\nimport transform from './transform.js'\nimport { buildXMLString } from '../utils.js'\n\nexport function container(\n  {\n    left,\n    top,\n    width,\n    height,\n    isInheritingTransform,\n  }: {\n    left: number\n    top: number\n    width: number\n    height: number\n    isInheritingTransform: boolean\n  },\n  style: Record<string, number | string>\n) {\n  let matrix = ''\n  let opacity = 1\n\n  if (style.transform) {\n    matrix = transform(\n      {\n        left,\n        top,\n        width,\n        height,\n      },\n      style.transform as unknown as number[],\n      isInheritingTransform,\n      style.transformOrigin as ParsedTransformOrigin | undefined\n    )\n  }\n\n  if (style.opacity !== undefined) {\n    opacity = +style.opacity\n  }\n\n  return { matrix, opacity }\n}\n\nexport default function buildText(\n  {\n    id,\n    content,\n    filter,\n    left,\n    top,\n    width,\n    height,\n    matrix,\n    opacity,\n    image,\n    clipPathId,\n    debug,\n    shape,\n    decorationShape,\n  }: {\n    content: string\n    filter: string\n    id: string\n    left: number\n    top: number\n    width: number\n    height: number\n    matrix: string\n    opacity: number\n    image: string | null\n    clipPathId?: string\n    debug?: boolean\n    shape?: boolean\n    decorationShape?: string\n  },\n  style: Record<string, number | string>\n) {\n  let extra = ''\n  if (debug) {\n    extra = buildXMLString('rect', {\n      x: left,\n      y: top - height,\n      width,\n      height,\n      fill: 'transparent',\n      stroke: '#575eff',\n      'stroke-width': 1,\n      transform: matrix || undefined,\n      'clip-path': clipPathId ? `url(#${clipPathId})` : undefined,\n    })\n  }\n\n  // This grapheme should be rendered as an image.\n  if (image) {\n    const shapeProps = {\n      href: image,\n      x: left,\n      y: top,\n      width,\n      height,\n      transform: matrix || undefined,\n      'clip-path': clipPathId ? `url(#${clipPathId})` : undefined,\n      style: style.filter ? `filter:${style.filter}` : undefined,\n    }\n    return [\n      (filter ? `${filter}<g filter=\"url(#satori_s-${id})\">` : '') +\n        buildXMLString('image', {\n          ...shapeProps,\n          opacity: opacity !== 1 ? opacity : undefined,\n        }) +\n        (decorationShape || '') +\n        (filter ? '</g>' : '') +\n        extra,\n      // SVG doesn't support `<image>` as the shape.\n      '',\n    ]\n  }\n\n  // Do not embed the font, use <text> with the raw content instead.\n  const shapeProps = {\n    x: left,\n    y: top,\n    width,\n    height,\n    'font-weight': style.fontWeight,\n    'font-style': style.fontStyle,\n    'font-size': style.fontSize,\n    'font-family': style.fontFamily,\n    'letter-spacing': style.letterSpacing || undefined,\n    transform: matrix || undefined,\n    'clip-path': clipPathId ? `url(#${clipPathId})` : undefined,\n    style: style.filter ? `filter:${style.filter}` : undefined,\n    'stroke-width': style.WebkitTextStrokeWidth\n      ? `${style.WebkitTextStrokeWidth}px`\n      : undefined,\n    stroke: style.WebkitTextStrokeWidth\n      ? style.WebkitTextStrokeColor\n      : undefined,\n    'stroke-linejoin': style.WebkitTextStrokeWidth ? 'round' : undefined,\n    'paint-order': style.WebkitTextStrokeWidth ? 'stroke' : undefined,\n  }\n  return [\n    (filter ? `${filter}<g filter=\"url(#satori_s-${id})\">` : '') +\n      buildXMLString(\n        'text',\n        {\n          ...shapeProps,\n          fill: style.color,\n          opacity: opacity !== 1 ? opacity : undefined,\n        },\n        escapeHTML(content)\n      ) +\n      (decorationShape || '') +\n      (filter ? '</g>' : '') +\n      extra,\n    shape ? buildXMLString('text', shapeProps, escapeHTML(content)) : '',\n  ]\n}\n"
  },
  {
    "path": "src/builder/transform.ts",
    "content": "import { multiply } from '../utils.js'\nimport type { ParsedTransformOrigin } from '../transform-origin.js'\n\nconst baseMatrix = [1, 0, 0, 1, 0, 0]\n\n// Mutate the array in place.\nfunction resolveTransforms(transforms: any[], width: number, height: number) {\n  let matrix = [...baseMatrix]\n\n  // Handle CSS transforms To make it easier, we convert different transform\n  // types directly to a matrix and apply it recursively to all its children.\n  // Transforms are applied from right to left.\n  // eslint-disable-next-line @typescript-eslint/no-shadow\n  for (const transform of transforms) {\n    const type = Object.keys(transform)[0]\n    let v = transform[type]\n\n    // Resolve percentages based on the element's final size.\n    if (typeof v === 'string') {\n      if (type === 'translateX') {\n        v = (parseFloat(v) / 100) * width\n        // Override the original object.\n        transform[type] = v\n      } else if (type === 'translateY') {\n        v = (parseFloat(v) / 100) * height\n        transform[type] = v\n      } else {\n        throw new Error(`Invalid transform: \"${type}: ${v}\".`)\n      }\n    }\n\n    let len = v as number\n\n    const transformMatrix = [...baseMatrix]\n    switch (type) {\n      case 'translateX':\n        transformMatrix[4] = len\n        break\n      case 'translateY':\n        transformMatrix[5] = len\n        break\n      case 'scale':\n        transformMatrix[0] = len\n        transformMatrix[3] = len\n        break\n      case 'scaleX':\n        transformMatrix[0] = len\n        break\n      case 'scaleY':\n        transformMatrix[3] = len\n        break\n      case 'rotate': {\n        const rad = (len * Math.PI) / 180\n        const c = Math.cos(rad)\n        const s = Math.sin(rad)\n        transformMatrix[0] = c\n        transformMatrix[1] = s\n        transformMatrix[2] = -s\n        transformMatrix[3] = c\n        break\n      }\n      case 'skewX':\n        transformMatrix[2] = Math.tan((len * Math.PI) / 180)\n        break\n      case 'skewY':\n        transformMatrix[1] = Math.tan((len * Math.PI) / 180)\n        break\n    }\n    matrix = multiply(transformMatrix, matrix)\n  }\n\n  transforms.splice(0, transforms.length)\n  transforms.push(...matrix)\n  ;(transforms as any).__resolved = true\n}\n\nexport default function transform(\n  {\n    left,\n    top,\n    width,\n    height,\n  }: {\n    left: number\n    top: number\n    width: number\n    height: number\n  },\n  transforms: number[],\n  isInheritingTransform: boolean,\n  transformOrigin?: ParsedTransformOrigin\n) {\n  let result: number[]\n\n  if (!(transforms as any).__resolved) {\n    resolveTransforms(transforms, width, height)\n  }\n\n  let matrix = transforms\n\n  // Calculate the transform origin.\n  if (isInheritingTransform) {\n    result = matrix\n  } else {\n    const xOrigin =\n      transformOrigin?.xAbsolute ??\n      ((transformOrigin?.xRelative ?? 50) * width) / 100\n    const yOrigin =\n      transformOrigin?.yAbsolute ??\n      ((transformOrigin?.yRelative ?? 50) * height) / 100\n\n    // If this element is the transform target, we attach the origin coordinates\n    // to this matrix.\n    const x = left + xOrigin\n    const y = top + yOrigin\n\n    // Due to the different coordinate systems, we need to move the shape to the\n    // origin first, then apply the matrix, then move it back.\n    result = multiply(\n      [1, 0, 0, 1, x, y],\n      multiply(matrix, [1, 0, 0, 1, -x, -y])\n    )\n\n    // And we need to apply its parent transform if it has one.\n    if ((matrix as any).__parent) {\n      result = multiply((matrix as any).__parent, result)\n    }\n\n    // Mutate self.\n    matrix.splice(0, 6, ...result)\n  }\n\n  return `matrix(${result.map((v) => v.toFixed(2)).join(',')})`\n}\n"
  },
  {
    "path": "src/font.ts",
    "content": "/**\n * This class handles everything related to fonts.\n */\nimport opentype from '@shuding/opentype.js'\nimport { Locale, locales, isValidLocale } from './language.js'\n\nexport type Weight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900\nexport type WeightName = 'normal' | 'bold'\nexport type FontWeight = Weight | WeightName\nexport type FontStyle = 'normal' | 'italic'\nconst SUFFIX_WHEN_LANG_NOT_SET = 'unknown'\n\nexport interface FontOptions {\n  data: Buffer | ArrayBuffer\n  name: string\n  weight?: Weight\n  style?: FontStyle\n  lang?: string\n}\n\nexport type GlyphBox = {\n  x1: number\n  x2: number\n  y1: number\n  y2: number\n}\ntype SkipInkBand = {\n  underlineY: number\n  strokeWidth: number\n}\n\nexport type FontEngine = {\n  has: (s: string) => boolean\n  baseline: (s?: string, resolvedFont?: any) => number\n  height: (s?: string, resolvedFont?: any) => number\n  measure: (\n    s: string,\n    style: {\n      fontSize: number\n      letterSpacing: number\n    }\n  ) => number\n  getSVG: (\n    s: string,\n    style: {\n      fontSize: number\n      top: number\n      left: number\n      letterSpacing: number\n    },\n    band?: SkipInkBand\n  ) => { path: string; boxes: GlyphBox[] }\n}\n\ntype BandPoint = [number, number]\n\ntype LineSegment = {\n  from: BandPoint\n  to: BandPoint\n}\n\nfunction flattenPath(commands: opentype.Path['commands']): LineSegment[] {\n  const segments: LineSegment[] = []\n  let start: BandPoint = [0, 0]\n  let current: BandPoint = [0, 0]\n\n  const addCurve = (points: BandPoint[], steps: number) => {\n    let prev = points[0]\n    for (let i = 1; i <= steps; i++) {\n      const t = i / steps\n      const next = evaluateBezier(points, t)\n      segments.push({ from: prev, to: next })\n      prev = next\n    }\n    current = points[points.length - 1]\n  }\n\n  for (const cmd of commands) {\n    if (cmd.type === 'M') {\n      start = current = [cmd.x, cmd.y]\n      continue\n    }\n\n    if (cmd.type === 'L') {\n      const next: BandPoint = [cmd.x, cmd.y]\n      segments.push({ from: current, to: next })\n      current = next\n      continue\n    }\n\n    if (cmd.type === 'Q') {\n      addCurve([current, [cmd.x1, cmd.y1], [cmd.x, cmd.y]], 12)\n      continue\n    }\n\n    if (cmd.type === 'C') {\n      addCurve(\n        [current, [cmd.x1, cmd.y1], [cmd.x2, cmd.y2], [cmd.x, cmd.y]],\n        16\n      )\n      continue\n    }\n\n    if (cmd.type === 'Z') {\n      segments.push({ from: current, to: start })\n      current = start\n    }\n  }\n\n  return segments\n}\n\nfunction evaluateBezier(points: BandPoint[], t: number): BandPoint {\n  let working = points\n\n  while (working.length > 1) {\n    const next: BandPoint[] = []\n    for (let i = 0; i < working.length - 1; i++) {\n      next.push([\n        working[i][0] + (working[i + 1][0] - working[i][0]) * t,\n        working[i][1] + (working[i + 1][1] - working[i][1]) * t,\n      ])\n    }\n    working = next\n  }\n\n  return working[0]\n}\n\nfunction computeBandBox(\n  commands: opentype.Path['commands'],\n  band?: SkipInkBand\n): GlyphBox[] {\n  if (!band) return []\n\n  const strokeWidth = band.strokeWidth\n  const bandMin = band.underlineY - strokeWidth * 0.25\n  const bandMax = band.underlineY + strokeWidth * 2.5\n\n  const segments = flattenPath(commands)\n  if (!segments.length) return []\n\n  const bandHeight = bandMax - bandMin\n  const ySamples = Math.max(12, Math.ceil(bandHeight / 0.25))\n  const yStep = bandHeight / ySamples\n  const yStart = bandMin + yStep / 2\n\n  const columnHits = new Set<number>()\n\n  for (let i = 0; i < ySamples; i++) {\n    const y = yStart + yStep * i\n    const intersections: number[] = []\n\n    for (const seg of segments) {\n      const [x1, y1] = seg.from\n      const [x2, y2] = seg.to\n\n      if (y1 === y2) continue\n      const yMin = Math.min(y1, y2)\n      const yMax = Math.max(y1, y2)\n      if (y < yMin || y >= yMax) continue\n\n      const t = (y - y1) / (y2 - y1)\n      const x = x1 + (x2 - x1) * t\n      intersections.push(x)\n    }\n\n    if (!intersections.length) continue\n    intersections.sort((a, b) => a - b)\n\n    for (let j = 0; j < intersections.length - 1; j += 2) {\n      const from = Math.min(intersections[j], intersections[j + 1])\n      const to = Math.max(intersections[j], intersections[j + 1])\n      const start = Math.floor(from)\n      const end = Math.ceil(to)\n      for (let col = start; col < end; col++) {\n        columnHits.add(col)\n      }\n    }\n  }\n\n  if (!columnHits.size) return []\n\n  const columns = Array.from(columnHits.values()).sort((a, b) => a - b)\n  const inkRanges: [number, number][] = []\n\n  let rangeStart = columns[0]\n  let prev = columns[0]\n  for (let i = 1; i < columns.length; i++) {\n    const col = columns[i]\n    if (col > prev + 1) {\n      inkRanges.push([rangeStart, prev + 1])\n      rangeStart = col\n    }\n    prev = col\n  }\n  inkRanges.push([rangeStart, prev + 1])\n\n  const boxes: GlyphBox[] = []\n  const bleed = strokeWidth * 0.6\n  const minX = inkRanges[0][0]\n  const maxX = inkRanges[inkRanges.length - 1][1]\n\n  for (const [x1, x2] of inkRanges) {\n    const left = Math.min(x1, minX) - bleed\n    const right = Math.max(x2, maxX) + bleed\n    boxes.push({\n      x1: left,\n      x2: right,\n      y1: bandMin,\n      y2: bandMax,\n    })\n  }\n\n  return boxes\n}\n\nfunction computeBoundingBox(\n  commands: opentype.Path['commands']\n): GlyphBox | null {\n  const xs: number[] = []\n  const ys: number[] = []\n\n  for (const cmd of commands) {\n    if ('x' in cmd && typeof cmd.x === 'number') xs.push(cmd.x)\n    if ('y' in cmd && typeof cmd.y === 'number') ys.push(cmd.y)\n    if ('x1' in cmd && typeof cmd.x1 === 'number') xs.push(cmd.x1)\n    if ('y1' in cmd && typeof cmd.y1 === 'number') ys.push(cmd.y1)\n    if ('x2' in cmd && typeof cmd.x2 === 'number') xs.push(cmd.x2)\n    if ('y2' in cmd && typeof cmd.y2 === 'number') ys.push(cmd.y2)\n  }\n\n  if (!xs.length || !ys.length) {\n    return null\n  }\n\n  return {\n    x1: Math.min(...xs),\n    x2: Math.max(...xs),\n    y1: Math.min(...ys),\n    y2: Math.max(...ys),\n  }\n}\n\nfunction compareFont(\n  weight,\n  style,\n  [matchedWeight, matchedStyle],\n  [nextWeight, nextStyle]\n) {\n  if (matchedWeight !== nextWeight) {\n    // Put the defined weight first.\n    if (!matchedWeight) return 1\n    if (!nextWeight) return -1\n\n    // Exact match.\n    if (matchedWeight === weight) return -1\n    if (nextWeight === weight) return 1\n\n    // 400 and 500.\n    if (weight === 400 && matchedWeight === 500) return -1\n    if (weight === 500 && matchedWeight === 400) return -1\n    if (weight === 400 && nextWeight === 500) return 1\n    if (weight === 500 && nextWeight === 400) return 1\n\n    // Less than 400.\n    if (weight < 400) {\n      if (matchedWeight < weight && nextWeight < weight)\n        return nextWeight - matchedWeight\n      if (matchedWeight < weight) return -1\n      if (nextWeight < weight) return 1\n      return matchedWeight - nextWeight\n    }\n\n    // Greater than 500.\n    if (weight < matchedWeight && weight < nextWeight)\n      return matchedWeight - nextWeight\n    if (weight < matchedWeight) return -1\n    if (weight < nextWeight) return 1\n    return nextWeight - matchedWeight\n  }\n\n  if (matchedStyle !== nextStyle) {\n    // Exact match.\n    if (matchedStyle === style) return -1\n    if (nextStyle === style) return 1\n  }\n\n  return -1\n}\n\nconst cachedParsedFont = new WeakMap<\n  Buffer | ArrayBuffer,\n  opentype.Font | null | undefined\n>()\n\nexport default class FontLoader {\n  defaultFont: opentype.Font\n  fonts = new Map<string, [opentype.Font, Weight?, FontStyle?][]>()\n  constructor(fontOptions: FontOptions[]) {\n    this.addFonts(fontOptions)\n  }\n\n  // Get font by name and weight.\n  private get({\n    name,\n    weight,\n    style,\n  }: {\n    name: string\n    weight: Weight | WeightName\n    style: FontStyle\n  }) {\n    if (!this.fonts.has(name)) {\n      return null\n    }\n\n    if (weight === 'normal') weight = 400\n    if (weight === 'bold') weight = 700\n    if (typeof weight === 'string')\n      weight = Number.parseInt(weight, 10) as Weight\n\n    const fonts = [...this.fonts.get(name)]\n\n    let matchedFont = fonts[0]\n\n    // Fallback to the closest weight and style according to the strategy here:\n    // https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#fallback_weights\n    for (let i = 1; i < fonts.length; i++) {\n      const [, weight1, style1] = matchedFont\n      const [, weight2, style2] = fonts[i]\n      if (\n        compareFont(weight, style, [weight1, style1], [weight2, style2]) > 0\n      ) {\n        matchedFont = fonts[i]\n      }\n    }\n\n    return matchedFont[0]\n  }\n\n  public addFonts(fontOptions: FontOptions[]) {\n    for (const fontOption of fontOptions) {\n      const { name, data, lang } = fontOption\n      if (lang && !isValidLocale(lang)) {\n        throw new Error(\n          `Invalid value for props \\`lang\\`: \"${lang}\". The value must be one of the following: ${locales.join(\n            ', '\n          )}.`\n        )\n      }\n      const _lang = lang ?? SUFFIX_WHEN_LANG_NOT_SET\n      let font\n\n      if (cachedParsedFont.has(data)) {\n        font = cachedParsedFont.get(data)\n      } else {\n        font = opentype.parse(\n          // Buffer to ArrayBuffer.\n          'buffer' in data\n            ? data.buffer.slice(\n                data.byteOffset,\n                data.byteOffset + data.byteLength\n              )\n            : data,\n          // @ts-ignore\n          { lowMemory: true }\n        )\n        // Modify the `charToGlyphIndex` method, so we can know which char is\n        // being mapped to which glyph.\n        const originalCharToGlyphIndex = font.charToGlyphIndex\n        font.charToGlyphIndex = (char) => {\n          const index = originalCharToGlyphIndex.call(font, char)\n          if (index === 0) {\n            // The current requested char is missing a glyph.\n            if ((font as any)._trackBrokenChars) {\n              ;(font as any)._trackBrokenChars.push(char)\n            }\n          }\n          return index\n        }\n\n        cachedParsedFont.set(data, font)\n      }\n\n      // We use the first font as the default font fallback.\n      if (!this.defaultFont) this.defaultFont = font\n\n      const _name = `${name.toLowerCase()}_${_lang}`\n\n      if (!this.fonts.has(_name)) {\n        this.fonts.set(_name, [])\n      }\n      this.fonts.get(_name).push([font, fontOption.weight, fontOption.style])\n    }\n  }\n\n  public getEngine(\n    fontSize = 16,\n    lineHeight: number | string = 'normal',\n    {\n      fontFamily = 'sans-serif',\n      fontWeight = 400,\n      fontStyle = 'normal',\n    }: {\n      fontFamily?: string | string[]\n      fontWeight?: FontWeight\n      fontStyle?: FontStyle\n    },\n    locale: Locale | undefined\n  ): FontEngine {\n    if (!this.fonts.size) {\n      throw new Error(\n        'No fonts are loaded. At least one font is required to calculate the layout.'\n      )\n    }\n\n    fontFamily = (Array.isArray(fontFamily) ? fontFamily : [fontFamily]).map(\n      (name) => name.toLowerCase()\n    )\n    const fonts = []\n    fontFamily.forEach((face) => {\n      const getNormal = this.get({\n        name: face,\n        weight: fontWeight,\n        style: fontStyle,\n      })\n      if (getNormal) {\n        fonts.push(getNormal)\n        return\n      }\n\n      const getUnknown = this.get({\n        name: face + '_unknown',\n        weight: fontWeight,\n        style: fontStyle,\n      })\n\n      if (getUnknown) {\n        fonts.push(getUnknown)\n        return\n      }\n    })\n\n    // Add additional fonts as the fallback.\n    const keys = Array.from(this.fonts.keys())\n    const specifiedLangFonts = []\n    const nonSpecifiedLangFonts = []\n    const additionalFonts = []\n    for (const name of keys) {\n      if (fontFamily.includes(name)) continue\n      if (locale) {\n        const lang = getLangFromFontName(name)\n        if (lang) {\n          if (lang === locale) {\n            specifiedLangFonts.push(\n              this.get({\n                name,\n                weight: fontWeight,\n                style: fontStyle,\n              })\n            )\n          } else {\n            nonSpecifiedLangFonts.push(\n              this.get({\n                name,\n                weight: fontWeight,\n                style: fontStyle,\n              })\n            )\n          }\n        } else {\n          additionalFonts.push(\n            this.get({\n              name,\n              weight: fontWeight,\n              style: fontStyle,\n            })\n          )\n        }\n      } else {\n        additionalFonts.push(\n          this.get({\n            name,\n            weight: fontWeight,\n            style: fontStyle,\n          })\n        )\n      }\n    }\n\n    const cachedFontResolver = new Map<number, opentype.Font | undefined>()\n    const resolveFont = (word: string, fallback = true) => {\n      const _fonts = [\n        ...fonts,\n        ...additionalFonts,\n        ...specifiedLangFonts,\n        ...(fallback ? nonSpecifiedLangFonts : []),\n      ]\n\n      if (typeof word === 'undefined') {\n        if (fallback) {\n          return _fonts[_fonts.length - 1]\n        }\n        return undefined\n      }\n\n      const code = word.charCodeAt(0)\n      if (cachedFontResolver.has(code)) return cachedFontResolver.get(code)\n\n      const font = _fonts.find((_font, index) => {\n        return (\n          !!_font.charToGlyphIndex(word) ||\n          (fallback && index === _fonts.length - 1)\n        )\n      })\n\n      if (font) {\n        cachedFontResolver.set(code, font)\n      }\n\n      return font\n    }\n\n    const ascender = (resolvedFont: opentype.Font, useOS2Table = false) => {\n      const _ascender =\n        (useOS2Table ? resolvedFont.tables?.os2?.sTypoAscender : 0) ||\n        resolvedFont.ascender\n      return (_ascender / resolvedFont.unitsPerEm) * fontSize\n    }\n\n    const descender = (resolvedFont: opentype.Font, useOS2Table = false) => {\n      const _descender =\n        (useOS2Table ? resolvedFont.tables?.os2?.sTypoDescender : 0) ||\n        resolvedFont.descender\n      return (_descender / resolvedFont.unitsPerEm) * fontSize\n    }\n\n    const height = (resolvedFont: opentype.Font, useOS2Table = false) => {\n      if ('string' === typeof lineHeight && 'normal' === lineHeight) {\n        const _lineGap =\n          (useOS2Table ? resolvedFont.tables?.os2?.sTypoLineGap : 0) || 0\n        return (\n          ascender(resolvedFont, useOS2Table) -\n          descender(resolvedFont, useOS2Table) +\n          (_lineGap / resolvedFont.unitsPerEm) * fontSize\n        )\n      } else if ('number' === typeof lineHeight) {\n        return fontSize * lineHeight\n      }\n    }\n\n    const resolve = (s: string) => {\n      return resolveFont(s, false)\n    }\n\n    const engine = {\n      has: (s: string) => {\n        if (s === '\\n') return true\n        const font = resolve(s)\n        if (!font) return false\n        ;(font as any)._trackBrokenChars = []\n        font.stringToGlyphs(s)\n        if (!(font as any)._trackBrokenChars.length) return true\n        ;(font as any)._trackBrokenChars = undefined\n        return false\n      },\n      baseline: (\n        s?: string,\n        resolvedFont = typeof s === 'undefined' ? fonts[0] : resolveFont(s)\n      ) => {\n        const asc = ascender(resolvedFont)\n        const desc = descender(resolvedFont)\n        const contentHeight = asc - desc\n\n        return asc + (height(resolvedFont) - contentHeight) / 2\n      },\n      height: (\n        s?: string,\n        resolvedFont = typeof s === 'undefined' ? fonts[0] : resolveFont(s)\n      ) => {\n        return height(resolvedFont)\n      },\n      measure: (\n        s: string,\n        style: {\n          fontSize: number\n          letterSpacing: number\n        }\n      ) => {\n        return this.measure(resolveFont, s, style)\n      },\n      getSVG: (\n        s: string,\n        style: {\n          fontSize: number\n          top: number\n          left: number\n          letterSpacing: number\n        },\n        band?: SkipInkBand\n      ) => {\n        return this.getSVG(resolveFont, s, style, band)\n      },\n    }\n\n    return engine\n  }\n\n  private patchFontFallbackResolver(\n    font: opentype.Font,\n    resolveFont: (word: string, fallback?: boolean) => opentype.Font\n  ) {\n    const brokenChars = []\n    ;(font as any)._trackBrokenChars = brokenChars\n\n    const originalStringToGlyphs = font.stringToGlyphs\n    font.stringToGlyphs = (s: string, ...args: any) => {\n      const glyphs = originalStringToGlyphs.call(font, s, ...args)\n\n      for (let i = 0; i < glyphs.length; i++) {\n        // Hitting an undefined glyph. We have to try to resolve it from other\n        // fonts.\n        // @TODO: This affects the kerning resolution but should be fine for now.\n        if (glyphs[i].unicode === undefined) {\n          const char = brokenChars.shift()\n          const anotherFont = resolveFont(char)\n          if (anotherFont !== font) {\n            const glyph = anotherFont.charToGlyph(char)\n            // Scale the glyph to match the current units per em.\n            const scale = font.unitsPerEm / anotherFont.unitsPerEm\n            const p = new opentype.Path()\n            p.unitsPerEm = font.unitsPerEm\n            p.commands = glyph.path.commands.map((command) => {\n              const scaledCommand = { ...command }\n              for (let k in scaledCommand) {\n                if (typeof scaledCommand[k] === 'number') {\n                  scaledCommand[k] *= scale\n                }\n              }\n              return scaledCommand\n            })\n            const g = new opentype.Glyph({\n              ...glyph,\n              advanceWidth: glyph.advanceWidth * scale,\n              xMin: glyph.xMin * scale,\n              xMax: glyph.xMax * scale,\n              yMin: glyph.yMin * scale,\n              yMax: glyph.yMax * scale,\n              path: p,\n            })\n\n            glyphs[i] = g\n          }\n        }\n      }\n\n      return glyphs\n    }\n\n    return () => {\n      font.stringToGlyphs = originalStringToGlyphs\n      ;(font as any)._trackBrokenChars = undefined\n    }\n  }\n\n  private measure(\n    resolveFont: (word: string, fallback?: boolean) => opentype.Font,\n    content: string,\n    {\n      fontSize,\n      letterSpacing = 0,\n    }: {\n      fontSize: number\n      letterSpacing: number\n    }\n  ) {\n    const font = resolveFont(content)\n    const unpatch = this.patchFontFallbackResolver(font, resolveFont)\n\n    try {\n      return font.getAdvanceWidth(content, fontSize, {\n        letterSpacing: letterSpacing / fontSize,\n      })\n    } finally {\n      unpatch()\n    }\n  }\n\n  private getSVG(\n    resolveFont: (word: string, fallback?: boolean) => opentype.Font,\n    content: string,\n    {\n      fontSize,\n      top,\n      left,\n      letterSpacing = 0,\n    }: {\n      fontSize: number\n      top: number\n      left: number\n      letterSpacing: number\n    },\n    band?: SkipInkBand\n  ): { path: string; boxes: GlyphBox[] } {\n    const font = resolveFont(content)\n    const unpatch = this.patchFontFallbackResolver(font, resolveFont)\n\n    try {\n      if (fontSize === 0) {\n        return { path: '', boxes: [] }\n      }\n\n      const fullPath = new opentype.Path()\n      const boxes: GlyphBox[] = []\n\n      const options = {\n        letterSpacing: letterSpacing / fontSize,\n      }\n\n      const cachedPath = new WeakMap<\n        opentype.Glyph,\n        [number, number, opentype.Path]\n      >()\n\n      font.forEachGlyph(\n        content.replace(/\\n/g, ''),\n        left,\n        top,\n        fontSize,\n        options,\n        function (glyph, gX, gY, gFontSize) {\n          let glyphPath: opentype.Path\n          if (!cachedPath.has(glyph)) {\n            glyphPath = glyph.getPath(gX, gY, gFontSize, options)\n            cachedPath.set(glyph, [gX, gY, glyphPath])\n          } else {\n            const [_x, _y, _glyphPath] = cachedPath.get(glyph)\n            glyphPath = new opentype.Path()\n            glyphPath.commands = _glyphPath.commands.map((command) => {\n              const movedCommand = { ...command }\n              for (let k in movedCommand) {\n                if (typeof movedCommand[k] === 'number') {\n                  if (k === 'x' || k === 'x1' || k === 'x2') {\n                    movedCommand[k] += gX - _x\n                  }\n                  if (k === 'y' || k === 'y1' || k === 'y2') {\n                    movedCommand[k] += gY - _y\n                  }\n                }\n              }\n              return movedCommand\n            })\n          }\n\n          const bandBoxes = band ? computeBandBox(glyphPath.commands, band) : []\n          if (bandBoxes.length) {\n            boxes.push(...bandBoxes)\n          }\n\n          fullPath.extend(glyphPath)\n        }\n      )\n\n      return {\n        path: fullPath.toPathData(1),\n        boxes,\n      }\n    } finally {\n      unpatch()\n    }\n  }\n}\n\nfunction getLangFromFontName(name: string): Locale | undefined {\n  const arr = name.split('_')\n  const lang = arr[arr.length - 1]\n\n  return lang === SUFFIX_WHEN_LANG_NOT_SET ? undefined : (lang as Locale)\n}\n"
  },
  {
    "path": "src/handler/compute.ts",
    "content": "/**\n * Handler to update the Yoga node properties with the given element type and\n * style. Each supported element has its own preset styles, so this function\n * also returns the inherited style for children of the element.\n */\n\nimport presets from './presets.js'\nimport inheritable from './inheritable.js'\nimport expand, { SerializedStyle } from './expand.js'\nimport {\n  asPointAutoPercentageLength,\n  asPointPercentageLength,\n  lengthToNumber,\n  parseViewBox,\n  v,\n} from '../utils.js'\nimport { getYoga, YogaNode } from '../yoga.js'\nimport { resolveImageData } from './image.js'\n\ntype SatoriElement = keyof typeof presets\n\nexport default async function compute(\n  node: YogaNode,\n  type: SatoriElement | string,\n  inheritedStyle: SerializedStyle,\n  definedStyle: Record<string, string | number>,\n  props: Record<string, any>\n): Promise<[SerializedStyle, SerializedStyle]> {\n  const Yoga = await getYoga()\n\n  // Extend the default style with defined and inherited styles.\n  const style: SerializedStyle = Object.assign(\n    {},\n    inheritedStyle,\n    expand(presets[type], inheritedStyle),\n    expand(definedStyle, inheritedStyle)\n  )\n\n  if (type === 'img') {\n    let [resolvedSrc, imageWidth, imageHeight] = await resolveImageData(\n      props.src\n    )\n\n    // Cannot parse the image size (e.g. base64 data URI).\n    if (imageWidth === undefined && imageHeight === undefined) {\n      if (props.width === undefined || props.height === undefined) {\n        throw new Error(\n          'Image size cannot be determined. Please provide the width and height of the image.'\n        )\n      }\n      imageWidth = parseInt(props.width)\n      imageHeight = parseInt(props.height)\n    }\n    const r = imageHeight / imageWidth\n\n    // Before calculating the missing width or height based on the image ratio,\n    // we must subtract the padding and border due to how box model works.\n    // TODO: Ensure these are absolute length values, not relative values.\n    let extraHorizontal =\n      (style.borderLeftWidth || 0) +\n      (style.borderRightWidth || 0) +\n      (style.paddingLeft || 0) +\n      (style.paddingRight || 0)\n    let extraVertical =\n      (style.borderTopWidth || 0) +\n      (style.borderBottomWidth || 0) +\n      (style.paddingTop || 0) +\n      (style.paddingBottom || 0)\n\n    let contentBoxWidth = style.width || props.width\n    let contentBoxHeight = style.height || props.height\n\n    const isAbsoluteContentSize =\n      typeof contentBoxWidth === 'number' &&\n      typeof contentBoxHeight === 'number'\n\n    if (isAbsoluteContentSize) {\n      contentBoxWidth -= extraHorizontal\n      contentBoxHeight -= extraVertical\n    }\n\n    // When no content size is defined, we use the image size as the content size.\n    if (contentBoxWidth === undefined && contentBoxHeight === undefined) {\n      contentBoxWidth = '100%'\n      node.setAspectRatio(1 / r)\n    } else {\n      // If only one sisde is not defined, we can calculate the other one.\n      if (contentBoxWidth === undefined) {\n        if (typeof contentBoxHeight === 'number') {\n          contentBoxWidth = contentBoxHeight / r\n        } else {\n          // If it uses a relative value (e.g. 50%), we can rely on aspect ratio.\n          // Note: this doesn't work well if there are paddings or borders.\n          node.setAspectRatio(1 / r)\n        }\n      } else if (contentBoxHeight === undefined) {\n        if (typeof contentBoxWidth === 'number') {\n          contentBoxHeight = contentBoxWidth * r\n        } else {\n          // If it uses a relative value (e.g. 50%), we can rely on aspect ratio.\n          // Note: this doesn't work well if there are paddings or borders.\n          node.setAspectRatio(1 / r)\n        }\n      }\n    }\n\n    style.width = isAbsoluteContentSize\n      ? (contentBoxWidth as number) + extraHorizontal\n      : contentBoxWidth\n    style.height = isAbsoluteContentSize\n      ? (contentBoxHeight as number) + extraVertical\n      : contentBoxHeight\n    style.__src = resolvedSrc\n    style.__naturalWidth = imageWidth\n    style.__naturalHeight = imageHeight\n  }\n\n  if (type === 'svg') {\n    const viewBox = props.viewBox || props.viewbox\n    const viewBoxSize = parseViewBox(viewBox)\n    const ratio = viewBoxSize ? viewBoxSize[3] / viewBoxSize[2] : null\n\n    let { width, height } = props\n    if (typeof width === 'undefined' && height) {\n      if (ratio == null) {\n        width = 0\n      } else if (typeof height === 'string' && height.endsWith('%')) {\n        width = parseInt(height) / ratio + '%'\n      } else {\n        height = lengthToNumber(\n          height,\n          inheritedStyle.fontSize,\n          1,\n          inheritedStyle\n        )\n        width = height / ratio\n      }\n    } else if (typeof height === 'undefined' && width) {\n      if (ratio == null) {\n        width = 0\n      } else if (typeof width === 'string' && width.endsWith('%')) {\n        height = parseInt(width) * ratio + '%'\n      } else {\n        width = lengthToNumber(\n          width,\n          inheritedStyle.fontSize,\n          1,\n          inheritedStyle\n        )\n        height = width * ratio\n      }\n    } else {\n      if (typeof width !== 'undefined') {\n        width =\n          lengthToNumber(width, inheritedStyle.fontSize, 1, inheritedStyle) ||\n          width\n      }\n      if (typeof height !== 'undefined') {\n        height =\n          lengthToNumber(height, inheritedStyle.fontSize, 1, inheritedStyle) ||\n          height\n      }\n      width ||= viewBoxSize?.[2]\n      height ||= viewBoxSize?.[3]\n    }\n\n    if (!style.width && width) style.width = width\n    if (!style.height && height) style.height = height\n  }\n\n  // Set properties for Yoga.\n  node.setDisplay(\n    v(\n      style.display,\n      {\n        flex: Yoga.DISPLAY_FLEX,\n        block: Yoga.DISPLAY_FLEX,\n        contents: Yoga.DISPLAY_CONTENTS,\n        none: Yoga.DISPLAY_NONE,\n        '-webkit-box': Yoga.DISPLAY_FLEX,\n      },\n      Yoga.DISPLAY_FLEX,\n      'display'\n    )\n  )\n\n  node.setAlignContent(\n    v(\n      style.alignContent,\n      {\n        stretch: Yoga.ALIGN_STRETCH,\n        center: Yoga.ALIGN_CENTER,\n        'flex-start': Yoga.ALIGN_FLEX_START,\n        'flex-end': Yoga.ALIGN_FLEX_END,\n        'space-between': Yoga.ALIGN_SPACE_BETWEEN,\n        'space-around': Yoga.ALIGN_SPACE_AROUND,\n        baseline: Yoga.ALIGN_BASELINE,\n        normal: Yoga.ALIGN_AUTO,\n      },\n      Yoga.ALIGN_AUTO,\n      'alignContent'\n    )\n  )\n\n  node.setAlignItems(\n    v(\n      style.alignItems,\n      {\n        stretch: Yoga.ALIGN_STRETCH,\n        center: Yoga.ALIGN_CENTER,\n        'flex-start': Yoga.ALIGN_FLEX_START,\n        'flex-end': Yoga.ALIGN_FLEX_END,\n        baseline: Yoga.ALIGN_BASELINE,\n        normal: Yoga.ALIGN_AUTO,\n      },\n      Yoga.ALIGN_STRETCH,\n      'alignItems'\n    )\n  )\n  node.setAlignSelf(\n    v(\n      style.alignSelf,\n      {\n        stretch: Yoga.ALIGN_STRETCH,\n        center: Yoga.ALIGN_CENTER,\n        'flex-start': Yoga.ALIGN_FLEX_START,\n        'flex-end': Yoga.ALIGN_FLEX_END,\n        baseline: Yoga.ALIGN_BASELINE,\n        normal: Yoga.ALIGN_AUTO,\n      },\n      Yoga.ALIGN_AUTO,\n      'alignSelf'\n    )\n  )\n  node.setJustifyContent(\n    v(\n      style.justifyContent,\n      {\n        center: Yoga.JUSTIFY_CENTER,\n        'flex-start': Yoga.JUSTIFY_FLEX_START,\n        'flex-end': Yoga.JUSTIFY_FLEX_END,\n        'space-between': Yoga.JUSTIFY_SPACE_BETWEEN,\n        'space-around': Yoga.JUSTIFY_SPACE_AROUND,\n      },\n      Yoga.JUSTIFY_FLEX_START,\n      'justifyContent'\n    )\n  )\n  // @TODO: node.setAspectRatio\n\n  node.setFlexDirection(\n    v(\n      style.flexDirection,\n      {\n        row: Yoga.FLEX_DIRECTION_ROW,\n        column: Yoga.FLEX_DIRECTION_COLUMN,\n        'row-reverse': Yoga.FLEX_DIRECTION_ROW_REVERSE,\n        'column-reverse': Yoga.FLEX_DIRECTION_COLUMN_REVERSE,\n      },\n      Yoga.FLEX_DIRECTION_ROW,\n      'flexDirection'\n    )\n  )\n  node.setFlexWrap(\n    v(\n      style.flexWrap,\n      {\n        wrap: Yoga.WRAP_WRAP,\n        nowrap: Yoga.WRAP_NO_WRAP,\n        'wrap-reverse': Yoga.WRAP_WRAP_REVERSE,\n      },\n      Yoga.WRAP_NO_WRAP,\n      'flexWrap'\n    )\n  )\n\n  if (typeof style.gap !== 'undefined') {\n    node.setGap(Yoga.GUTTER_ALL, style.gap)\n  }\n\n  if (typeof style.rowGap !== 'undefined') {\n    node.setGap(Yoga.GUTTER_ROW, style.rowGap)\n  }\n\n  if (typeof style.columnGap !== 'undefined') {\n    node.setGap(Yoga.GUTTER_COLUMN, style.columnGap)\n  }\n\n  // @TODO: node.setFlex\n\n  if (typeof style.flexBasis !== 'undefined') {\n    node.setFlexBasis(asPointAutoPercentageLength(style.flexBasis, 'flexBasis'))\n  }\n  node.setFlexGrow(typeof style.flexGrow === 'undefined' ? 0 : style.flexGrow)\n  node.setFlexShrink(\n    typeof style.flexShrink === 'undefined' ? 0 : style.flexShrink\n  )\n\n  if (typeof style.maxHeight !== 'undefined') {\n    node.setMaxHeight(asPointPercentageLength(style.maxHeight, 'maxHeight'))\n  }\n  if (typeof style.maxWidth !== 'undefined') {\n    node.setMaxWidth(asPointPercentageLength(style.maxWidth, 'maxWidth'))\n  }\n  if (typeof style.minHeight !== 'undefined') {\n    node.setMinHeight(asPointPercentageLength(style.minHeight, 'minHeight'))\n  }\n  if (typeof style.minWidth !== 'undefined') {\n    node.setMinWidth(asPointPercentageLength(style.minWidth, 'minWidth'))\n  }\n\n  node.setOverflow(\n    v(\n      style.overflow,\n      {\n        visible: Yoga.OVERFLOW_VISIBLE,\n        hidden: Yoga.OVERFLOW_HIDDEN,\n      },\n      Yoga.OVERFLOW_VISIBLE,\n      'overflow'\n    )\n  )\n\n  node.setMargin(\n    Yoga.EDGE_TOP,\n    asPointAutoPercentageLength(style.marginTop || 0)\n  )\n  node.setMargin(\n    Yoga.EDGE_BOTTOM,\n    asPointAutoPercentageLength(style.marginBottom || 0)\n  )\n  node.setMargin(\n    Yoga.EDGE_LEFT,\n    asPointAutoPercentageLength(style.marginLeft || 0)\n  )\n  node.setMargin(\n    Yoga.EDGE_RIGHT,\n    asPointAutoPercentageLength(style.marginRight || 0)\n  )\n\n  node.setBorder(Yoga.EDGE_TOP, style.borderTopWidth || 0)\n  node.setBorder(Yoga.EDGE_BOTTOM, style.borderBottomWidth || 0)\n  node.setBorder(Yoga.EDGE_LEFT, style.borderLeftWidth || 0)\n  node.setBorder(Yoga.EDGE_RIGHT, style.borderRightWidth || 0)\n\n  node.setPadding(Yoga.EDGE_TOP, style.paddingTop || 0)\n  node.setPadding(Yoga.EDGE_BOTTOM, style.paddingBottom || 0)\n  node.setPadding(Yoga.EDGE_LEFT, style.paddingLeft || 0)\n  node.setPadding(Yoga.EDGE_RIGHT, style.paddingRight || 0)\n\n  node.setBoxSizing(\n    v(\n      style.boxSizing,\n      {\n        'border-box': Yoga.BOX_SIZING_BORDER_BOX,\n        'content-box': Yoga.BOX_SIZING_CONTENT_BOX,\n      },\n      Yoga.BOX_SIZING_BORDER_BOX,\n      'boxSizing'\n    )\n  )\n\n  node.setPositionType(\n    v(\n      style.position,\n      {\n        absolute: Yoga.POSITION_TYPE_ABSOLUTE,\n        relative: Yoga.POSITION_TYPE_RELATIVE,\n        static: Yoga.POSITION_TYPE_STATIC,\n      },\n      Yoga.POSITION_TYPE_RELATIVE,\n      'position'\n    )\n  )\n\n  if (typeof style.top !== 'undefined') {\n    node.setPosition(Yoga.EDGE_TOP, asPointPercentageLength(style.top, 'top'))\n  }\n  if (typeof style.bottom !== 'undefined') {\n    node.setPosition(\n      Yoga.EDGE_BOTTOM,\n      asPointPercentageLength(style.bottom, 'bottom')\n    )\n  }\n  if (typeof style.left !== 'undefined') {\n    node.setPosition(\n      Yoga.EDGE_LEFT,\n      asPointPercentageLength(style.left, 'left')\n    )\n  }\n  if (typeof style.right !== 'undefined') {\n    node.setPosition(\n      Yoga.EDGE_RIGHT,\n      asPointPercentageLength(style.right, 'right')\n    )\n  }\n\n  if (typeof style.height !== 'undefined') {\n    node.setHeight(asPointAutoPercentageLength(style.height, 'height'))\n  } else {\n    node.setHeightAuto()\n  }\n  if (typeof style.width !== 'undefined') {\n    node.setWidth(asPointAutoPercentageLength(style.width, 'width'))\n  } else {\n    node.setWidthAuto()\n  }\n\n  return [style, inheritable(style)]\n}\n"
  },
  {
    "path": "src/handler/expand.ts",
    "content": "/**\n * This module expands the CSS properties to get rid of shorthands, as well as\n * cleaning up some properties.\n */\n\nimport { getPropertyName, getStylesForProperty } from 'css-to-react-native'\nimport { parseElementStyle } from 'css-background-parser'\nimport { parse as parseBoxShadow } from 'css-box-shadow'\nimport cssColorParse from 'parse-css-color'\n\nimport CssDimension from '../vendor/parse-css-dimension/index.js'\nimport parseTransformOrigin, {\n  ParsedTransformOrigin,\n} from '../transform-origin.js'\nimport { isString, lengthToNumber, v, splitEffects } from '../utils.js'\nimport { MaskProperty, parseMask } from '../parser/mask.js'\nimport { FontWeight, FontStyle } from '../font.js'\nimport {\n  extractCustomProperties,\n  mergeVariables,\n  resolveVariables,\n  CSSVariables,\n} from './variables.js'\n\n// https://react-cn.github.io/react/tips/style-props-value-px.html\nconst optOutPx = new Set([\n  'flex',\n  'flexGrow',\n  'flexShrink',\n  'flexBasis',\n  'fontWeight',\n  'lineHeight',\n  'opacity',\n  'scale',\n  'scaleX',\n  'scaleY',\n])\nconst keepNumber = new Set(['lineHeight'])\n\nfunction handleFallbackColor(\n  prop: string,\n  parsed: Record<string, string>,\n  rawInput: string,\n  currentColor: string\n) {\n  if (\n    prop === 'textDecoration' &&\n    !rawInput.includes(parsed.textDecorationColor)\n  ) {\n    parsed.textDecorationColor = currentColor\n  }\n  return parsed\n}\n\nfunction purify(name: string, value?: string | number) {\n  const num = Number(value)\n  if (isNaN(num)) return value\n  if (!optOutPx.has(name)) return num + 'px'\n  if (keepNumber.has(name)) return num\n  return String(value)\n}\n\nfunction handleSpecialCase(\n  name: string,\n  value: string | number,\n  currentColor: string\n) {\n  if (name === 'zIndex') {\n    console.warn('`z-index` is currently not supported.')\n    return { [name]: value }\n  }\n\n  if (name === 'lineHeight') {\n    return { lineHeight: purify(name, value) }\n  }\n\n  if (name === 'fontFamily') {\n    return {\n      fontFamily: (value as string).split(',').map((_v) => {\n        return _v\n          .trim()\n          .replace(/(^['\"])|(['\"]$)/g, '')\n          .toLocaleLowerCase()\n      }),\n    }\n  }\n\n  if (name === 'borderRadius') {\n    if (typeof value !== 'string' || !value.includes('/')) {\n      // Regular border radius\n      return\n    }\n    // Support the `border-radius: 10px / 20px` syntax.\n    const [horizontal, vertical] = value.split('/')\n    const vh = getStylesForProperty(name, horizontal, true)\n    const vv = getStylesForProperty(name, vertical, true)\n    for (const k in vh) {\n      vv[k] = purify(name, vh[k]) + ' ' + purify(name, vv[k])\n    }\n    return vv\n  }\n\n  if (/^border(Top|Right|Bottom|Left)?$/.test(name)) {\n    const resolved = getStylesForProperty('border', value, true)\n\n    // Border width should be default to 3px (medium) instead of 1px:\n    // https://w3c.github.io/csswg-drafts/css-backgrounds-3/#border-width\n    // Although on Chrome it will be displayed as 1.5px but let's stick to the\n    // spec.\n    if (resolved.borderWidth === 1 && !String(value).includes('1px')) {\n      resolved.borderWidth = 3\n    }\n\n    // A trick to fix `border: 1px solid` to not use `black` but the inherited\n    // `color` value. This is necessary because css-to-react-native automatically\n    // fallbacks to default color values.\n    if (resolved.borderColor === 'black' && !String(value).includes('black')) {\n      resolved.borderColor = currentColor\n    }\n\n    const purified = {\n      Width: purify(name + 'Width', resolved.borderWidth),\n      Style: v(\n        resolved.borderStyle,\n        {\n          solid: 'solid',\n          dashed: 'dashed',\n        },\n        'solid',\n        name + 'Style'\n      ),\n      Color: resolved.borderColor,\n    }\n\n    const full = {}\n    for (const k of name === 'border'\n      ? ['Top', 'Right', 'Bottom', 'Left']\n      : [name.slice(6)]) {\n      for (const p in purified) {\n        full['border' + k + p] = purified[p]\n      }\n    }\n    return full\n  }\n\n  if (name === 'boxShadow') {\n    if (!value) {\n      throw new Error('Invalid `boxShadow` value: \"' + value + '\".')\n    }\n    return {\n      [name]: typeof value === 'string' ? parseBoxShadow(value) : value,\n    }\n  }\n\n  if (name === 'transform') {\n    if (typeof value !== 'string') throw new Error('Invalid `transform` value.')\n    // To support percentages in transform (which is not supported in RN), we\n    // replace them with random symbols and then replace them back after parsing.\n    const symbols = {}\n    const replaced = value.replace(/(-?[\\d.]+%)/g, (_, _v) => {\n      const symbol = ~~(Math.random() * 1e9)\n      symbols[symbol] = _v\n      return symbol + 'px'\n    })\n    const parsed = getStylesForProperty('transform', replaced, true)\n    for (const t of parsed.transform) {\n      for (const k in t) {\n        if (symbols[t[k]]) {\n          t[k] = symbols[t[k]]\n        }\n      }\n    }\n    return parsed\n  }\n\n  if (name === 'background') {\n    value = value.toString().trim()\n    if (\n      /^(linear-gradient|radial-gradient|url|repeating-linear-gradient|repeating-radial-gradient)\\(/.test(\n        value\n      )\n    ) {\n      return getStylesForProperty('backgroundImage', value, true)\n    }\n    return getStylesForProperty('background', value, true)\n  }\n\n  if (name === 'textShadow') {\n    // Handle multiple text shadows if provided.\n    value = value.toString().trim()\n    const result = {}\n\n    const shadows = splitEffects(value)\n\n    for (const shadow of shadows) {\n      const styles = getStylesForProperty('textShadow', shadow, true)\n      for (const k in styles) {\n        if (!result[k]) {\n          result[k] = [styles[k]]\n        } else {\n          result[k].push(styles[k])\n        }\n      }\n    }\n\n    return result\n  }\n\n  if (name === 'WebkitTextStroke') {\n    value = value.toString().trim()\n    const values = value.split(' ')\n    if (values.length !== 2) {\n      throw new Error('Invalid `WebkitTextStroke` value.')\n    }\n\n    return {\n      WebkitTextStrokeWidth: purify(name, values[0]),\n      WebkitTextStrokeColor: purify(name, values[1]),\n    }\n  }\n\n  if (name === 'textDecorationSkipInk') {\n    const normalized = value.toString().trim().toLowerCase()\n    if (!['auto', 'none', 'all'].includes(normalized)) {\n      throw new Error('Invalid `textDecorationSkipInk` value.')\n    }\n\n    return { textDecorationSkipInk: normalized }\n  }\n\n  return\n}\n\nfunction getErrorHint(name: string) {\n  if (name === 'transform') {\n    return ' Only absolute lengths such as `10px` are supported.'\n  }\n  return ''\n}\n\nconst RGB_SLASH = /rgb\\((\\d+)\\s+(\\d+)\\s+(\\d+)\\s*\\/\\s*([\\.\\d]+)\\)/\nfunction normalizeColor(value: string | object) {\n  if (typeof value === 'string') {\n    if (RGB_SLASH.test(value.trim())) {\n      // rgb(255 122 127 / .2) -> rgba(255, 122, 127, .2)\n      return value.trim().replace(RGB_SLASH, (_, r, g, b, a) => {\n        return `rgba(${r}, ${g}, ${b}, ${a})`\n      })\n    }\n  }\n\n  // Recursively normalize colors in arrays and objects.\n  if (typeof value === 'object' && value !== null) {\n    for (const k in value) {\n      value[k] = normalizeColor(value[k])\n    }\n    return value\n  }\n\n  return value\n}\n\ntype MainStyle = {\n  color: string\n  fontSize: number\n  transformOrigin: ParsedTransformOrigin\n  maskImage: MaskProperty[]\n  opacity: number\n  textTransform: string\n  whiteSpace: string\n  wordBreak: string\n  textAlign: string\n  textIndent: number | string\n  lineHeight: number | string\n  letterSpacing: number\n\n  fontFamily: string | string[]\n  fontWeight: FontWeight\n  fontStyle: FontStyle\n\n  borderTopWidth: number\n  borderLeftWidth: number\n  borderRightWidth: number\n  borderBottomWidth: number\n\n  paddingTop: number\n  paddingLeft: number\n  paddingRight: number\n  paddingBottom: number\n\n  flexGrow: number\n  flexShrink: number\n\n  gap: number\n  rowGap: number\n  columnGap: number\n\n  textShadowOffset: {\n    width: number\n    height: number\n  }[]\n  textShadowColor: string[]\n  textShadowRadius: number[]\n  WebkitTextStrokeWidth: number\n  WebkitTextStrokeColor: string\n  textDecorationSkipInk: 'auto' | 'none' | 'all'\n}\n\ntype OtherStyle = Exclude<Record<PropertyKey, string | number>, keyof MainStyle>\n\nexport type SerializedStyle = Partial<MainStyle & OtherStyle>\n\nexport default function expand(\n  style: Record<string, string | number> | undefined,\n  inheritedStyle: SerializedStyle\n): SerializedStyle {\n  const serializedStyle: SerializedStyle = {}\n\n  // Extract inherited CSS variables\n  const inheritedVariables: CSSVariables = {}\n  for (const prop in inheritedStyle) {\n    if (prop.startsWith('--')) {\n      inheritedVariables[prop] = String(inheritedStyle[prop])\n    }\n  }\n\n  // Extract and resolve CSS variables from current style\n  let currentVariables: CSSVariables = {}\n  let processableStyle = style\n\n  if (style) {\n    const { variables, remainingStyle } = extractCustomProperties(style)\n    currentVariables = variables\n    processableStyle = remainingStyle\n  }\n\n  // Merge variables (current overrides inherited)\n  const mergedVariables = mergeVariables(inheritedVariables, currentVariables)\n\n  // Store merged variables in the serialized style for inheritance\n  for (const varName in mergedVariables) {\n    serializedStyle[varName] = mergedVariables[varName]\n  }\n\n  if (processableStyle) {\n    // Resolve CSS variables in color property before processing\n    const resolvedColor = processableStyle.color\n      ? resolveVariables(processableStyle.color, mergedVariables)\n      : undefined\n\n    const currentColor = getCurrentColor(\n      resolvedColor as string,\n      inheritedStyle.color\n    )\n\n    serializedStyle.color = currentColor\n\n    for (const prop in processableStyle) {\n      // Internal properties.\n      if (prop.startsWith('_')) {\n        serializedStyle[prop] = processableStyle[prop]\n        continue\n      }\n\n      if (prop === 'color') {\n        continue\n      }\n\n      const name = getPropertyName(prop)\n      // Resolve CSS variables before preprocessing\n      const resolvedValue = resolveVariables(\n        processableStyle[prop],\n        mergedVariables\n      )\n      const value = preprocess(resolvedValue, currentColor)\n\n      try {\n        const resolvedStyle =\n          handleSpecialCase(name, value, currentColor) ||\n          handleFallbackColor(\n            name,\n            getStylesForProperty(name, purify(name, value), true),\n            value as string,\n            currentColor\n          )\n\n        Object.assign(serializedStyle, resolvedStyle)\n      } catch (err) {\n        throw new Error(\n          err.message +\n            // Attach the extra information of the rule itself if it's not included in\n            // the error message.\n            (err.message.includes(value)\n              ? '\\n  ' + getErrorHint(name)\n              : `\\n  in CSS rule \\`${name}: ${value}\\`.${getErrorHint(name)}`)\n        )\n      }\n    }\n  }\n\n  // Parse background images.\n  if (serializedStyle.backgroundImage) {\n    const { backgrounds } = parseElementStyle(serializedStyle)\n    serializedStyle.backgroundImage = backgrounds\n  }\n\n  if (serializedStyle.maskImage || serializedStyle['WebkitMaskImage']) {\n    serializedStyle.maskImage = parseMask(serializedStyle)\n  }\n\n  // Calculate the base font size.\n  const baseFontSize = calcBaseFontSize(\n    serializedStyle.fontSize,\n    inheritedStyle.fontSize\n  )\n  if (typeof serializedStyle.fontSize !== 'undefined') {\n    serializedStyle.fontSize = baseFontSize\n  }\n\n  if (serializedStyle.transformOrigin) {\n    serializedStyle.transformOrigin = parseTransformOrigin(\n      serializedStyle.transformOrigin as any,\n      baseFontSize\n    )\n  }\n\n  for (const prop in serializedStyle) {\n    let value = serializedStyle[prop]\n\n    // Line height needs to be relative.\n    if (prop === 'lineHeight') {\n      if (typeof value === 'string' && value !== 'normal') {\n        value = serializedStyle[prop] =\n          lengthToNumber(\n            value,\n            baseFontSize,\n            baseFontSize,\n            inheritedStyle,\n            true\n          ) / baseFontSize\n      }\n    } else {\n      // Convert em and rem values to px (number).\n      if (typeof value === 'string') {\n        const len = lengthToNumber(\n          value,\n          baseFontSize,\n          baseFontSize,\n          inheritedStyle\n        )\n        if (typeof len !== 'undefined') serializedStyle[prop] = len\n        value = serializedStyle[prop]\n      }\n\n      if (typeof value === 'string' || typeof value === 'object') {\n        const color = normalizeColor(value)\n        if (color) {\n          serializedStyle[prop] = color as any\n        }\n        value = serializedStyle[prop]\n      }\n    }\n\n    // Inherit the opacity.\n    if (prop === 'opacity' && typeof value === 'number') {\n      serializedStyle.opacity = value * inheritedStyle.opacity\n    }\n\n    if (prop === 'transform') {\n      const transforms = value as any as { [type: string]: number | string }[]\n\n      for (const transform of transforms) {\n        const type = Object.keys(transform)[0]\n        const _v = transform[type]\n\n        // Convert em, rem, vw, vh values to px (number), but keep % values.\n        const len =\n          typeof _v === 'string'\n            ? lengthToNumber(_v, baseFontSize, baseFontSize, inheritedStyle) ??\n              _v\n            : _v\n        transform[type] = len\n      }\n    }\n\n    if (prop === 'textShadowRadius') {\n      const textShadowRadius = value as unknown as Array<number | string>\n\n      serializedStyle.textShadowRadius = textShadowRadius.map((_v) =>\n        lengthToNumber(_v, baseFontSize, 0, inheritedStyle, false)\n      )\n    }\n\n    if (prop === 'textShadowOffset') {\n      const textShadowOffset = value as unknown as Array<{\n        width: number | string\n        height: number | string\n      }>\n\n      serializedStyle.textShadowOffset = textShadowOffset.map(\n        ({ height, width }) => ({\n          height: lengthToNumber(\n            height,\n            baseFontSize,\n            0,\n            inheritedStyle,\n            false\n          ),\n          width: lengthToNumber(width, baseFontSize, 0, inheritedStyle, false),\n        })\n      )\n    }\n  }\n\n  return serializedStyle\n}\n\nfunction calcBaseFontSize(\n  size: number | string,\n  inheritedSize: number\n): number {\n  if (typeof size === 'number') return size\n\n  try {\n    const parsed = new CssDimension(size)\n    switch (parsed.unit) {\n      case 'em':\n        return parsed.value * inheritedSize\n      case 'rem':\n        return parsed.value * 16\n    }\n  } catch (err) {\n    return inheritedSize\n  }\n}\n\n/**\n * @see https://github.com/RazrFalcon/resvg/issues/579\n */\nfunction refineHSL(color: string) {\n  if (color.startsWith('hsl')) {\n    const t = cssColorParse(color)\n    const [h, s, l] = t.values\n\n    return `hsl(${[h, `${s}%`, `${l}%`]\n      .concat(t.alpha === 1 ? [] : [t.alpha])\n      .join(',')})`\n  }\n\n  return color\n}\n\nfunction getCurrentColor(\n  color: string | undefined,\n  inheritedColor: string\n): string {\n  if (color && color.toLowerCase() !== 'currentcolor') {\n    return refineHSL(color)\n  }\n\n  return refineHSL(inheritedColor)\n}\n\nfunction convertCurrentColorToActualValue(\n  value: string,\n  currentColor: string\n): string {\n  return value.replace(/currentcolor/gi, currentColor)\n}\n\nfunction preprocess(\n  value: string | number,\n  currentColor: string\n): string | number {\n  if (isString(value)) {\n    value = convertCurrentColorToActualValue(value, currentColor)\n  }\n\n  return value\n}\n"
  },
  {
    "path": "src/handler/image.ts",
    "content": "/**\n * This module is used to fetch image from the given URL and resolve it as\n * base64 inlined data URI, so the toolchain can process it.\n * The image data will be cached in a LRU so we don't need to fetch it again\n * in new render processes. But to invalidate the cache the workaround is to\n * add a random query string to the URL.\n * TODO: We might want another option to disable image caching by default.\n */\n\nconst AVIF = 'image/avif'\nconst WEBP = 'image/webp'\nconst APNG = 'image/apng'\nconst PNG = 'image/png'\nconst JPEG = 'image/jpeg'\nconst GIF = 'image/gif'\nconst SVG = 'image/svg+xml'\n\nfunction parseJPEG(buf: ArrayBuffer) {\n  const view = new DataView(buf)\n\n  // Skip magic bytes\n  let offset = 4\n\n  const len = view.byteLength\n  while (offset < len) {\n    const i = view.getUint16(offset, false)\n\n    if (i > len) {\n      throw new TypeError('Invalid JPEG')\n    }\n\n    const next = view.getUint8(i + 1 + offset)\n    if (next === 0xc0 || next === 0xc1 || next === 0xc2) {\n      return [\n        view.getUint16(i + 7 + offset, false),\n        view.getUint16(i + 5 + offset, false),\n      ] as [number, number]\n    }\n\n    // TODO: Support orientations from EXIF.\n\n    offset += i + 2\n  }\n\n  throw new TypeError('Invalid JPEG')\n}\n\nfunction parseGIF(buf: ArrayBuffer) {\n  const view = new Uint8Array(buf.slice(6, 10))\n  return [view[0] | (view[1] << 8), view[2] | (view[3] << 8)] as [\n    number,\n    number\n  ]\n}\n\nfunction parsePNG(buf: ArrayBuffer) {\n  const v = new DataView(buf)\n  return [v.getUint16(18, false), v.getUint16(22, false)] as [number, number]\n}\n\nimport { createLRU, parseViewBox } from '../utils.js'\n\ntype ResolvedImageData = [string, number?, number?] | readonly []\nexport const cache = createLRU<ResolvedImageData>(500)\nexport const inflightRequests = new Map<string, Promise<ResolvedImageData>>()\n\nconst ALLOWED_IMAGE_TYPES = [PNG, APNG, JPEG, GIF, SVG]\n\n// Pre-compiled regex patterns for SVG parsing\nconst SVG_ATTRS_REGEX = /<svg[^>]*>/i\nconst VIEWBOX_REGEX = /viewBox=['\"]([^'\"]+)['\"]/\nconst WIDTH_REGEX = /width=['\"](\\d*\\.?\\d+)['\"]/\nconst HEIGHT_REGEX = /height=['\"](\\d*\\.?\\d+)['\"]/\n\nfunction arrayBufferToBase64(buffer) {\n  const bytes = new Uint8Array(buffer)\n  const CHUNK_SIZE = 0x8000 // 32KB chunks\n  let binary = ''\n\n  for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {\n    const chunk = bytes.subarray(i, Math.min(i + CHUNK_SIZE, bytes.length))\n    binary += String.fromCharCode(...chunk)\n  }\n\n  return btoa(binary)\n}\n\nfunction base64ToArrayBuffer(base64: string): ArrayBuffer {\n  let binaryString = atob(base64)\n  let len = binaryString.length\n  let bytes = new Uint8Array(len)\n  for (let i = 0; i < len; i++) {\n    bytes[i] = binaryString.charCodeAt(i)\n  }\n  return bytes.buffer\n}\n\nfunction parseSvgImageSize(src: string, data: string) {\n  // Parse the SVG image size\n  const svgMatch = data.match(SVG_ATTRS_REGEX)\n  if (!svgMatch) throw new Error(`Failed to parse SVG from ${src}`)\n\n  const svgTag = svgMatch[0]\n  const viewBoxMatch = VIEWBOX_REGEX.exec(svgTag)\n  const widthMatch = WIDTH_REGEX.exec(svgTag)\n  const heightMatch = HEIGHT_REGEX.exec(svgTag)\n\n  let viewBox = viewBoxMatch ? parseViewBox(viewBoxMatch[1]) : null\n\n  if (!viewBox && (!widthMatch || !heightMatch)) {\n    throw new Error(`Failed to parse SVG from ${src}: missing \"viewBox\"`)\n  }\n\n  const size = viewBox\n    ? [viewBox[2], viewBox[3]]\n    : [+widthMatch[1], +heightMatch[1]]\n\n  const ratio = size[0] / size[1]\n  const imageSize: [number, number] =\n    widthMatch && heightMatch\n      ? [+widthMatch[1], +heightMatch[1]]\n      : widthMatch\n      ? [+widthMatch[1], +widthMatch[1] / ratio]\n      : heightMatch\n      ? [+heightMatch[1] * ratio, +heightMatch[1]]\n      : [size[0], size[1]]\n\n  return imageSize\n}\n\nfunction arrayBufferToDataUri(data: ArrayBuffer) {\n  let imageSize: [number, number]\n\n  const imageType = detectContentType(new Uint8Array(data))\n\n  switch (imageType) {\n    case PNG:\n    case APNG:\n      imageSize = parsePNG(data)\n      break\n    case GIF:\n      imageSize = parseGIF(data)\n      break\n    case JPEG:\n      imageSize = parseJPEG(data)\n      break\n  }\n\n  if (!ALLOWED_IMAGE_TYPES.includes(imageType)) {\n    throw new Error(`Unsupported image type: ${imageType || 'unknown'}`)\n  }\n  return [\n    `data:${imageType};base64,${arrayBufferToBase64(data)}`,\n    imageSize,\n  ] as const\n}\n\nexport async function resolveImageData(\n  src: string | ArrayBuffer\n): Promise<ResolvedImageData> {\n  if (!src) {\n    throw new Error('Image source is not provided.')\n  }\n\n  // ArrayBuffer\n  if (typeof src === 'object') {\n    const [newSrc, imageSize] = arrayBufferToDataUri(src)\n    return [newSrc, ...imageSize] as ResolvedImageData\n  }\n\n  if (\n    (src.startsWith('\"') && src.endsWith('\"')) ||\n    (src.startsWith(\"'\") && src.endsWith(\"'\"))\n  ) {\n    src = src.slice(1, -1)\n  }\n\n  // Throw error if the image source is not an absolute URL in server environment\n  // Should be after slicing quotes to avoid throwing error too early\n  if (typeof window === 'undefined') {\n    if (!src.startsWith('http') && !src.startsWith('data:')) {\n      throw new Error(`Image source must be an absolute URL: ${src}`)\n    }\n  }\n\n  if (src.startsWith('data:')) {\n    let decodedURI: { imageType; encodingType; dataString }\n\n    try {\n      decodedURI =\n        /data:(?<imageType>[a-z/+]+)(;[^;=]+=[^;=]+)*?(;(?<encodingType>[^;,]+))?,(?<dataString>.*)/g.exec(\n          src\n        ).groups as typeof decodedURI\n    } catch (err) {\n      console.warn('Image data URI resolved without size:' + src)\n      return [src]\n    }\n\n    const { imageType, encodingType, dataString } = decodedURI\n    if (imageType === SVG) {\n      const utf8Src =\n        encodingType === 'base64'\n          ? atob(dataString)\n          : decodeURIComponent(dataString.replace(/ /g, '%20'))\n      const base64Src =\n        encodingType === 'base64'\n          ? src\n          : `data:image/svg+xml;base64,${btoa(utf8Src)}`\n      let imageSize = parseSvgImageSize(src, utf8Src)\n      cache.set(src, [base64Src, ...imageSize])\n      return [base64Src, ...imageSize]\n    } else if (encodingType === 'base64') {\n      let imageSize: [number, number]\n      const data = base64ToArrayBuffer(dataString)\n      switch (imageType) {\n        case PNG:\n        case APNG:\n          imageSize = parsePNG(data)\n          break\n        case GIF:\n          imageSize = parseGIF(data)\n          break\n        case JPEG:\n          imageSize = parseJPEG(data)\n          break\n      }\n      cache.set(src, [src, ...imageSize])\n      return [src, ...imageSize]\n    } else {\n      console.warn('Image data URI resolved without size:' + src)\n      cache.set(src, [src])\n      return [src]\n    }\n  }\n\n  if (!globalThis.fetch) {\n    throw new Error('`fetch` is required to be polyfilled to load images.')\n  }\n\n  if (inflightRequests.has(src)) {\n    return inflightRequests.get(src)\n  }\n  const cached = cache.get(src)\n  if (cached) {\n    return cached\n  }\n\n  const url = src\n  const promise = fetch(url)\n    .then((res): Promise<string | ArrayBuffer> => {\n      const type = res.headers.get('content-type')\n\n      // Handle SVG specially\n      if (type === 'image/svg+xml' || type === 'application/svg+xml') {\n        return res.text()\n      }\n\n      return res.arrayBuffer()\n    })\n    .then((data) => {\n      if (typeof data === 'string') {\n        try {\n          const newSrc = `data:image/svg+xml;base64,${btoa(data)}`\n          // Parse the SVG image size\n          const imageSize = parseSvgImageSize(url, data)\n          return [newSrc, ...imageSize] as ResolvedImageData\n        } catch (e) {\n          throw new Error(`Failed to parse SVG image: ${e.message}`)\n        }\n      }\n\n      const [newSrc, imageSize] = arrayBufferToDataUri(data)\n      return [newSrc, ...imageSize] as ResolvedImageData\n    })\n    .then((result) => {\n      cache.set(url, result)\n      return result\n    })\n    .catch((err) => {\n      console.error(`Can't load image ${url}: ` + err.message)\n      cache.set(url, [])\n      return [] as const\n    })\n\n  inflightRequests.set(url, promise)\n  return promise\n}\n\n/**\n * Inspects the first few bytes of a buffer to determine if\n * it matches the \"magic number\" of known file signatures.\n * https://en.wikipedia.org/wiki/List_of_file_signatures\n */\nfunction detectContentType(buffer: Uint8Array) {\n  if ([0xff, 0xd8, 0xff].every((b, i) => buffer[i] === b)) {\n    return JPEG\n  }\n  if (\n    [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every(\n      (b, i) => buffer[i] === b\n    )\n  ) {\n    if (detectAPNG(buffer)) {\n      return APNG\n    }\n    return PNG\n  }\n  if ([0x47, 0x49, 0x46, 0x38].every((b, i) => buffer[i] === b)) {\n    return GIF\n  }\n  if (\n    [0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50].every(\n      (b, i) => !b || buffer[i] === b\n    )\n  ) {\n    return WEBP\n  }\n  if ([0x3c, 0x3f, 0x78, 0x6d, 0x6c].every((b, i) => buffer[i] === b)) {\n    return SVG\n  }\n  if (\n    [0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66].every(\n      (b, i) => !b || buffer[i] === b\n    )\n  ) {\n    return AVIF\n  }\n  return null\n}\n\nfunction detectAPNG(bytes: Uint8Array) {\n  const dv = new DataView(bytes.buffer)\n  let type: string,\n    length: number,\n    off = 8,\n    isAPNG = false\n  while (!isAPNG && type !== 'IEND' && off < bytes.length) {\n    length = dv.getUint32(off)\n    const chars = bytes.subarray(off + 4, off + 8)\n    type = String.fromCharCode(...chars)\n    isAPNG = type === 'acTL'\n    off += 12 + length\n  }\n  return isAPNG\n}\n"
  },
  {
    "path": "src/handler/inheritable.ts",
    "content": "import { SerializedStyle } from './expand.js'\n\nconst list = new Set([\n  'color',\n  'font',\n  'fontFamily',\n  'fontSize',\n  'fontStyle',\n  'fontWeight',\n  'letterSpacing',\n  'lineHeight',\n  'textAlign',\n  'textIndent',\n  'textTransform',\n  'textShadowOffset',\n  'textShadowColor',\n  'textShadowRadius',\n  'WebkitTextStrokeWidth',\n  'WebkitTextStrokeColor',\n  'textDecorationLine',\n  'textDecorationStyle',\n  'textDecorationColor',\n  'textDecorationSkipInk',\n  'whiteSpace',\n  'transform',\n  'wordBreak',\n  'tabSize',\n\n  // Special case: SVG doesn't apply these to children elements so we need to\n  // make it inheritable here.\n  'opacity',\n  'filter',\n\n  // Special properties of Satori:\n  '_viewportWidth',\n  '_viewportHeight',\n  '_inheritedClipPathId',\n  '_inheritedMaskId',\n  '_inheritedBackgroundClipTextPath',\n  '_inheritedBackgroundClipTextHasBackground',\n])\n\nexport default function inheritable(style: SerializedStyle): SerializedStyle {\n  const inheritedStyle: SerializedStyle = {}\n  for (const prop in style) {\n    // CSS custom properties (--*) always inherit\n    if (list.has(prop) || prop.startsWith('--')) {\n      inheritedStyle[prop] = style[prop]\n    }\n  }\n  return inheritedStyle\n}\n"
  },
  {
    "path": "src/handler/preprocess.ts",
    "content": "import type { ReactElement, ReactNode } from 'react'\nimport { resolveImageData, cache } from './image.js'\nimport { isReactElement, parseViewBox, midline } from '../utils.js'\n\n// Based on\n// https://raw.githubusercontent.com/facebook/react/master/packages/react-dom/src/shared/possibleStandardNames.js\nconst ATTRIBUTE_MAPPING = {\n  accentHeight: 'accent-height',\n  alignmentBaseline: 'alignment-baseline',\n  arabicForm: 'arabic-form',\n  baselineShift: 'baseline-shift',\n  capHeight: 'cap-height',\n  clipPath: 'clip-path',\n  clipRule: 'clip-rule',\n  colorInterpolation: 'color-interpolation',\n  colorInterpolationFilters: 'color-interpolation-filters',\n  colorProfile: 'color-profile',\n  colorRendering: 'color-rendering',\n  dominantBaseline: 'dominant-baseline',\n  enableBackground: 'enable-background',\n  fillOpacity: 'fill-opacity',\n  fillRule: 'fill-rule',\n  floodColor: 'flood-color',\n  floodOpacity: 'flood-opacity',\n  fontFamily: 'font-family',\n  fontSize: 'font-size',\n  fontSizeAdjust: 'font-size-adjust',\n  fontStretch: 'font-stretch',\n  fontStyle: 'font-style',\n  fontVariant: 'font-variant',\n  fontWeight: 'font-weight',\n  glyphName: 'glyph-name',\n  glyphOrientationHorizontal: 'glyph-orientation-horizontal',\n  glyphOrientationVertical: 'glyph-orientation-vertical',\n  horizAdvX: 'horiz-adv-x',\n  horizOriginX: 'horiz-origin-x',\n  href: 'href',\n  imageRendering: 'image-rendering',\n  letterSpacing: 'letter-spacing',\n  lightingColor: 'lighting-color',\n  markerEnd: 'marker-end',\n  markerMid: 'marker-mid',\n  markerStart: 'marker-start',\n  overlinePosition: 'overline-position',\n  overlineThickness: 'overline-thickness',\n  paintOrder: 'paint-order',\n  panose1: 'panose-1',\n  pointerEvents: 'pointer-events',\n  renderingIntent: 'rendering-intent',\n  shapeRendering: 'shape-rendering',\n  stopColor: 'stop-color',\n  stopOpacity: 'stop-opacity',\n  strikethroughPosition: 'strikethrough-position',\n  strikethroughThickness: 'strikethrough-thickness',\n  strokeDasharray: 'stroke-dasharray',\n  strokeDashoffset: 'stroke-dashoffset',\n  strokeLinecap: 'stroke-linecap',\n  strokeLinejoin: 'stroke-linejoin',\n  strokeMiterlimit: 'stroke-miterlimit',\n  strokeOpacity: 'stroke-opacity',\n  strokeWidth: 'stroke-width',\n  textAnchor: 'text-anchor',\n  textDecoration: 'text-decoration',\n  textRendering: 'text-rendering',\n  underlinePosition: 'underline-position',\n  underlineThickness: 'underline-thickness',\n  unicodeBidi: 'unicode-bidi',\n  unicodeRange: 'unicode-range',\n  unitsPerEm: 'units-per-em',\n  vAlphabetic: 'v-alphabetic',\n  vHanging: 'v-hanging',\n  vIdeographic: 'v-ideographic',\n  vMathematical: 'v-mathematical',\n  vectorEffect: 'vector-effect',\n  vertAdvY: 'vert-adv-y',\n  vertOriginX: 'vert-origin-x',\n  vertOriginY: 'vert-origin-y',\n  wordSpacing: 'word-spacing',\n  writingMode: 'writing-mode',\n  xHeight: 'x-height',\n  xlinkActuate: 'xlink:actuate',\n  xlinkArcrole: 'xlink:arcrole',\n  xlinkHref: 'xlink:href',\n  xlinkRole: 'xlink:role',\n  xlinkShow: 'xlink:show',\n  xlinkTitle: 'xlink:title',\n  xlinkType: 'xlink:type',\n  xmlBase: 'xml:base',\n  xmlLang: 'xml:lang',\n  xmlSpace: 'xml:space',\n  xmlnsXlink: 'xmlns:xlink',\n} as const\n\n// From https://github.com/yoksel/url-encoder/blob/master/src/js/script.js\nconst SVGSymbols = /[\\r\\n%#()<>?[\\\\\\]^`{|}\"']/g\n\nfunction translateSVGNodeToSVGString(\n  node: ReactElement | string | (ReactElement | string)[],\n  inheritedColor: string\n): string {\n  if (!node) return ''\n  if (Array.isArray(node)) {\n    return node\n      .map((n) => translateSVGNodeToSVGString(n, inheritedColor))\n      .join('')\n  }\n  if (typeof node !== 'object') return String(node)\n\n  const type = node.type\n  if (type === 'text') {\n    throw new Error(\n      '<text> nodes are not currently supported, please convert them to <path>'\n    )\n  }\n\n  const { children, style, ...restProps } = node.props || {}\n  const currentColor = style?.color || inheritedColor\n\n  const attrs = `${Object.entries(restProps)\n    .map(([k, _v]) => {\n      if (typeof _v === 'string' && _v.toLowerCase() === 'currentcolor') {\n        _v = currentColor\n      }\n\n      if ((k === 'href' || k === 'xlinkHref') && type === 'image') {\n        return ` ${ATTRIBUTE_MAPPING[k] || k}=\"${cache.get(_v as string)[0]}\"`\n      }\n      return ` ${ATTRIBUTE_MAPPING[k] || k}=\"${_v}\"`\n    })\n    .join('')}`\n\n  const styles = style\n    ? ` style=\"${Object.entries(style)\n        .map(([k, _v]) => `${midline(k)}:${_v}`)\n        .join(';')}\"`\n    : ''\n\n  return `<${type}${attrs}${styles}>${translateSVGNodeToSVGString(\n    children,\n    currentColor\n  )}</${type}>`\n}\n/**\n * pre process node and resolve absolute link to img data for image element\n * @param node ReactNode\n * @returns\n */\nexport async function preProcessNode(node: ReactNode) {\n  const set = new Set<string | Buffer | ArrayBuffer>()\n  const walk = (_node: ReactNode) => {\n    if (!_node) return\n    if (!isReactElement(_node)) return\n\n    if (Array.isArray(_node)) {\n      _node.forEach((v) => walk(v))\n      return\n    } else if (typeof _node === 'object') {\n      if (_node.type === 'image') {\n        const imageSrc = _node.props.href || _node.props.xlinkHref\n        if (imageSrc) {\n          if (set.has(imageSrc)) {\n            // do nothing\n          } else {\n            set.add(imageSrc)\n          }\n        }\n      } else if (_node.type === 'img') {\n        if (set.has(_node.props.src)) {\n          // do nothing\n        } else {\n          set.add(_node.props.src)\n        }\n      } else {\n        // do nothing\n      }\n    }\n\n    Array.isArray(_node.props.children)\n      ? _node.props.children.map((c) => walk(c))\n      : walk(_node.props.children)\n  }\n\n  walk(node)\n\n  return Promise.all(Array.from(set).map((s) => resolveImageData(s)))\n}\n\nexport async function SVGNodeToImage(\n  node: ReactElement,\n  inheritedColor: string\n): Promise<string> {\n  let {\n    viewBox,\n    viewbox,\n    width,\n    height,\n    className,\n    style,\n    children,\n    ...restProps\n  } = node.props || {}\n\n  viewBox ||= viewbox\n\n  // We directly assign the xmlns attribute here to deduplicate.\n  restProps.xmlns = 'http://www.w3.org/2000/svg'\n\n  const currentColor = style?.color || inheritedColor\n  const viewBoxSize = parseViewBox(viewBox)\n\n  // ratio = height / width\n  const ratio = viewBoxSize ? viewBoxSize[3] / viewBoxSize[2] : null\n  width = width || (ratio && height) ? height / ratio : null\n  height = height || (ratio && width) ? width * ratio : null\n\n  restProps.width = width\n  restProps.height = height\n  if (viewBox) restProps.viewBox = viewBox\n\n  return `data:image/svg+xml;utf8,${`<svg ${Object.entries(restProps)\n    .map(([k, _v]) => {\n      if (typeof _v === 'string' && _v.toLowerCase() === 'currentcolor') {\n        _v = currentColor\n      }\n      return ` ${ATTRIBUTE_MAPPING[k] || k}=\"${_v}\"`\n    })\n    .join('')}>${translateSVGNodeToSVGString(\n    children,\n    currentColor\n  )}</svg>`.replace(SVGSymbols, encodeURIComponent)}`\n}\n"
  },
  {
    "path": "src/handler/presets.ts",
    "content": "/**\n * Pre-defined styles for elements. Here we hand pick some from Chromium's\n * default styles:\n * https://chromium.googlesource.com/chromium/blink/+/master/Source/core/css/html.css\n *\n * We try to only include commonly used, styling elements rather than semantic elements.\n */\n\nconst DEFAULT_DISPLAY = 'flex'\n\nexport default {\n  // Generic block-level elements\n  p: {\n    display: DEFAULT_DISPLAY,\n    marginTop: '1em',\n    marginBottom: '1em',\n  },\n  div: {\n    display: DEFAULT_DISPLAY,\n  },\n  blockquote: {\n    display: DEFAULT_DISPLAY,\n    marginTop: '1em',\n    marginBottom: '1em',\n    marginLeft: 40,\n    marginRight: 40,\n  },\n  center: {\n    display: DEFAULT_DISPLAY,\n    textAlign: 'center',\n  },\n  hr: {\n    display: DEFAULT_DISPLAY,\n    marginTop: '0.5em',\n    marginBottom: '0.5em',\n    marginLeft: 'auto',\n    marginRight: 'auto',\n    borderWidth: 1,\n    // We don't have `inset`\n    borderStyle: 'solid',\n  },\n  // Heading elements\n  h1: {\n    display: DEFAULT_DISPLAY,\n    fontSize: '2em',\n    marginTop: '0.67em',\n    marginBottom: '0.67em',\n    marginLeft: 0,\n    marginRight: 0,\n    fontWeight: 'bold',\n  },\n  h2: {\n    display: DEFAULT_DISPLAY,\n    fontSize: '1.5em',\n    marginTop: '0.83em',\n    marginBottom: '0.83em',\n    marginLeft: 0,\n    marginRight: 0,\n    fontWeight: 'bold',\n  },\n  h3: {\n    display: DEFAULT_DISPLAY,\n    fontSize: '1.17em',\n    marginTop: '1em',\n    marginBottom: '1em',\n    marginLeft: 0,\n    marginRight: 0,\n    fontWeight: 'bold',\n  },\n  h4: {\n    display: DEFAULT_DISPLAY,\n    marginTop: '1.33em',\n    marginBottom: '1.33em',\n    marginLeft: 0,\n    marginRight: 0,\n    fontWeight: 'bold',\n  },\n  h5: {\n    display: DEFAULT_DISPLAY,\n    fontSize: '0.83em',\n    marginTop: '1.67em',\n    marginBottom: '1.67em',\n    marginLeft: 0,\n    marginRight: 0,\n    fontWeight: 'bold',\n  },\n  h6: {\n    display: DEFAULT_DISPLAY,\n    fontSize: '0.67em',\n    marginTop: '2.33em',\n    marginBottom: '2.33em',\n    marginLeft: 0,\n    marginRight: 0,\n    fontWeight: 'bold',\n  },\n  // Tables\n  // Lists\n  // Form elements\n  // Inline elements\n  u: {\n    textDecoration: 'underline',\n  },\n  strong: {\n    fontWeight: 'bold',\n  },\n  b: {\n    fontWeight: 'bold',\n  },\n  i: {\n    fontStyle: 'italic',\n  },\n  em: {\n    fontStyle: 'italic',\n  },\n  code: {\n    fontFamily: 'monospace',\n  },\n  kbd: {\n    fontFamily: 'monospace',\n  },\n  pre: {\n    display: DEFAULT_DISPLAY,\n    fontFamily: 'monospace',\n    whiteSpace: 'pre',\n    marginTop: '1em',\n    marginBottom: '1em',\n  },\n  mark: {\n    backgroundColor: 'yellow',\n    color: 'black',\n  },\n  big: {\n    fontSize: 'larger',\n  },\n  small: {\n    fontSize: 'smaller',\n  },\n  s: {\n    textDecoration: 'line-through',\n  },\n}\n"
  },
  {
    "path": "src/handler/tailwind.ts",
    "content": "import type { TwConfig } from 'twrnc'\n\nimport * as twrnc from 'twrnc/create'\n\ntype TwPlugin = TwConfig['plugins'][number]\n\nconst defaultShadows: TwPlugin = {\n  handler: ({ addUtilities }) => {\n    const presets = {\n      'shadow-sm': { boxShadow: '0 1px 2px 0 rgb(0 0 0 / 0.05)' },\n      shadow: {\n        boxShadow:\n          '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',\n      },\n      'shadow-md': {\n        boxShadow:\n          '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',\n      },\n      'shadow-lg': {\n        boxShadow:\n          '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',\n      },\n      'shadow-xl': {\n        boxShadow:\n          '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',\n      },\n      'shadow-2xl': {\n        boxShadow: '0 25px 50px -12px rgb(0 0 0 / 0.25)',\n      },\n      'shadow-inner': {\n        boxShadow: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)',\n      },\n      'shadow-none': { boxShadow: '0 0 #0000' },\n    }\n\n    addUtilities(presets)\n  },\n}\n\nfunction createTw(config?: TwConfig) {\n  return twrnc.create(\n    {\n      ...config,\n      plugins: [...(config?.plugins ?? []), defaultShadows],\n    },\n    'web'\n  )\n}\n\nlet tw\nexport default function getTw({\n  width,\n  height,\n  config,\n}: {\n  width: number\n  height: number\n  config?: TwConfig\n}) {\n  if (!tw) {\n    tw = createTw(config)\n  }\n  tw.setWindowDimensions({ width: +width, height: +height })\n  return tw\n}\n"
  },
  {
    "path": "src/handler/variables.ts",
    "content": "/**\n * This module handles CSS custom properties (CSS variables) including:\n * - Extracting custom properties from styles (--property-name)\n * - Resolving var() references\n * - Handling fallback values\n * - Detecting circular references\n */\n\nimport valueParser from 'postcss-value-parser'\n\nexport type CSSVariables = Record<string, string>\n\n/**\n * Extracts custom properties (--*) from a style object\n * Returns both the variables and the remaining style properties\n */\nexport function extractCustomProperties(\n  style: Record<string, string | number>\n): {\n  variables: CSSVariables\n  remainingStyle: Record<string, string | number>\n} {\n  const variables: CSSVariables = {}\n  const remainingStyle: Record<string, string | number> = {}\n\n  for (const prop in style) {\n    if (prop.startsWith('--')) {\n      // Custom property\n      variables[prop] = String(style[prop])\n    } else {\n      remainingStyle[prop] = style[prop]\n    }\n  }\n\n  return { variables, remainingStyle }\n}\n\n/**\n * Merges inherited variables with current variables\n * Current variables override inherited ones (cascading)\n */\nexport function mergeVariables(\n  inherited: CSSVariables,\n  current: CSSVariables\n): CSSVariables {\n  return { ...inherited, ...current }\n}\n\n/**\n * Resolves var() references in a CSS value\n * Supports fallback values: var(--name, fallback)\n * Handles nested var() calls\n */\nexport function resolveVariables(\n  value: string | number,\n  variables: CSSVariables,\n  visitedVars = new Set<string>()\n): string | number {\n  // Only process strings\n  if (typeof value !== 'string') {\n    return value\n  }\n\n  // Quick check: does this value contain var()?\n  if (!value.includes('var(')) {\n    return value\n  }\n\n  try {\n    const parsed = valueParser(value)\n    let hasChanges = false\n\n    parsed.walk((node) => {\n      if (node.type === 'function' && node.value === 'var') {\n        hasChanges = true\n\n        // Extract variable name and optional fallback\n        const args = extractVarArgs(node)\n        if (!args) {\n          // Invalid var() syntax, leave as-is\n          return\n        }\n\n        const { varName, fallback } = args\n\n        // Check for circular reference\n        if (visitedVars.has(varName)) {\n          console.warn(\n            `Circular reference detected for CSS variable: ${varName}`\n          )\n          // Use fallback if available, otherwise use invalid value\n          if (fallback !== undefined) {\n            replaceNode(node, fallback)\n          } else {\n            replaceNode(node, 'initial')\n          }\n          return\n        }\n\n        // Look up the variable\n        const variableValue = variables[varName]\n\n        if (variableValue !== undefined) {\n          // Mark this variable as being resolved to detect circular references\n          const newVisitedVars = new Set(visitedVars)\n          newVisitedVars.add(varName)\n\n          // Recursively resolve the variable value in case it contains more var()\n          const resolvedValue = resolveVariables(\n            variableValue,\n            variables,\n            newVisitedVars\n          )\n\n          replaceNode(node, String(resolvedValue))\n        } else if (fallback !== undefined) {\n          // Variable not found, use fallback\n          // Recursively resolve fallback in case it contains var()\n          const resolvedFallback = resolveVariables(\n            fallback,\n            variables,\n            visitedVars\n          )\n          replaceNode(node, String(resolvedFallback))\n        } else {\n          // Variable not found and no fallback, use initial value\n          // According to CSS spec, this should be treated as invalid\n          replaceNode(node, 'initial')\n        }\n      }\n    })\n\n    if (hasChanges) {\n      return parsed.toString()\n    }\n  } catch (err) {\n    // If parsing fails, return the original value\n    console.warn(`Failed to parse CSS value for variable resolution: ${value}`)\n  }\n\n  return value\n}\n\n/**\n * Extracts variable name and fallback from var() function arguments\n * Handles: var(--name) and var(--name, fallback)\n */\nfunction extractVarArgs(\n  node: valueParser.FunctionNode\n): { varName: string; fallback?: string } | null {\n  if (!node.nodes || node.nodes.length === 0) {\n    return null\n  }\n\n  // Find the variable name (first word node)\n  let varNameNode: valueParser.Node | undefined\n  let commaIndex = -1\n\n  for (let i = 0; i < node.nodes.length; i++) {\n    const child = node.nodes[i]\n    if (child.type === 'word' && !varNameNode) {\n      varNameNode = child\n    } else if (child.type === 'div' && child.value === ',') {\n      commaIndex = i\n      break\n    }\n  }\n\n  if (!varNameNode || varNameNode.type !== 'word') {\n    return null\n  }\n\n  const varName = varNameNode.value.trim()\n\n  // Check if there's a fallback value after the comma\n  if (commaIndex !== -1 && commaIndex < node.nodes.length - 1) {\n    // Collect all nodes after the comma as the fallback\n    const fallbackNodes = node.nodes.slice(commaIndex + 1)\n    const fallback = valueParser.stringify(fallbackNodes).trim()\n    return { varName, fallback }\n  }\n\n  return { varName }\n}\n\n/**\n * Replaces a function node with a raw string value\n */\nfunction replaceNode(node: valueParser.Node, value: string) {\n  // Replace the function node with a word node\n  node.type = 'word'\n  node.value = value\n  // Remove function-specific properties\n  delete (node as any).nodes\n}\n\n/**\n * Resolves all variables in a style object\n * Returns a new style object with var() references resolved\n */\nexport function resolveStyleVariables(\n  style: Record<string, string | number>,\n  variables: CSSVariables\n): Record<string, string | number> {\n  const resolved: Record<string, string | number> = {}\n\n  for (const prop in style) {\n    resolved[prop] = resolveVariables(style[prop], variables)\n  }\n\n  return resolved\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "export type {\n  FontOptions as Font,\n  Weight as FontWeight,\n  FontStyle,\n} from './font.js'\nexport type { Locale } from './language.js'\n\nexport * from './satori.js'\nexport { default } from './satori.js'\nexport { init } from './yoga.js'\n"
  },
  {
    "path": "src/jsx/index.ts",
    "content": "import { jsx } from './jsx-runtime.js'\nimport type { JSXNode, JSXElement, JSXKey, FC } from './types.js'\n\nexport type * from './types.js'\nexport { Fragment, type JSX } from './jsx-runtime.js'\n\n/**\n * Create a `ReactElement`-like object.\n *\n * @param type - Tag name string or a function component.\n * @param props - Optional props to create the element with.\n * @param children - Zero or more child nodes.\n * @returns A `ReactElement`-like object with properties like `type`, `key`, `props`, and `props.children`.\n */\nexport function createElement<P extends {}>(\n  type: string | FC<P>,\n  props?: P | null,\n  ...children: JSXNode[]\n): JSXElement<P> {\n  if (!props) {\n    const newProps = children.length ? { children } : {}\n    return jsx(type, newProps, null) as JSXElement<P>\n  }\n\n  // Destructure key from props.\n  const { key, ...restProps } = props as {\n    key?: JSXKey | undefined | null\n    [x: string]: unknown\n  }\n  // Pass children as props.\n  if (children.length) restProps.children = children\n\n  return jsx(type, restProps, key) as JSXElement<P>\n}\n"
  },
  {
    "path": "src/jsx/intrinsic-elements.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\n/* eslint-disable @typescript-eslint/no-empty-interface */\n/**\n * @file\n * HTML elements and CSS properties that Satori supports.\n *\n * This code is adapted from React v19.1 types.\n * MIT License\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * @see {@link https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts React typings `@types/react`}\n */\nimport type { JSXNode } from './types.js'\n\n/**\n * Subset of CSS properties that Satori supports.\n *\n * @todo Add properties.\n */\nexport interface CSSProperties {\n  [prop: string]: string | number | undefined\n}\n\ntype Booleanish = 'true' | 'false' | boolean\ntype CrossOrigin = 'anonymous' | 'use-credentials' | '' | undefined\n\ninterface SVGProps<T> extends SVGAttributes<T> {}\n\ninterface SVGLineElementAttributes<T> extends SVGProps<T> {}\ninterface SVGTextElementAttributes<T> extends SVGProps<T> {}\n\ninterface DOMAttributes<T> {\n  children?: JSXNode | undefined\n}\n\ntype AriaRole =\n  | 'alert'\n  | 'alertdialog'\n  | 'application'\n  | 'article'\n  | 'banner'\n  | 'button'\n  | 'cell'\n  | 'checkbox'\n  | 'columnheader'\n  | 'combobox'\n  | 'complementary'\n  | 'contentinfo'\n  | 'definition'\n  | 'dialog'\n  | 'directory'\n  | 'document'\n  | 'feed'\n  | 'figure'\n  | 'form'\n  | 'grid'\n  | 'gridcell'\n  | 'group'\n  | 'heading'\n  | 'img'\n  | 'link'\n  | 'list'\n  | 'listbox'\n  | 'listitem'\n  | 'log'\n  | 'main'\n  | 'marquee'\n  | 'math'\n  | 'menu'\n  | 'menubar'\n  | 'menuitem'\n  | 'menuitemcheckbox'\n  | 'menuitemradio'\n  | 'navigation'\n  | 'none'\n  | 'note'\n  | 'option'\n  | 'presentation'\n  | 'progressbar'\n  | 'radio'\n  | 'radiogroup'\n  | 'region'\n  | 'row'\n  | 'rowgroup'\n  | 'rowheader'\n  | 'scrollbar'\n  | 'search'\n  | 'searchbox'\n  | 'separator'\n  | 'slider'\n  | 'spinbutton'\n  | 'status'\n  | 'switch'\n  | 'tab'\n  | 'table'\n  | 'tablist'\n  | 'tabpanel'\n  | 'term'\n  | 'textbox'\n  | 'timer'\n  | 'toolbar'\n  | 'tooltip'\n  | 'tree'\n  | 'treegrid'\n  | 'treeitem'\n  | (string & {})\n\ninterface AriaAttributes {\n  /** Identifies the currently active element when DOM focus is on a composite widget, textbox, group, or application. */\n  'aria-activedescendant'?: string | undefined\n  /** Indicates whether assistive technologies will present all, or only parts of, the changed region based on the change notifications defined by the aria-relevant attribute. */\n  'aria-atomic'?: Booleanish | undefined\n  /**\n   * Indicates whether inputting text could trigger display of one or more predictions of the user's intended value for an input and specifies how predictions would be\n   * presented if they are made.\n   */\n  'aria-autocomplete'?: 'none' | 'inline' | 'list' | 'both' | undefined\n  /** Indicates an element is being modified and that assistive technologies MAY want to wait until the modifications are complete before exposing them to the user. */\n  /**\n   * Defines a string value that labels the current element, which is intended to be converted into Braille.\n   * @see aria-label.\n   */\n  'aria-braillelabel'?: string | undefined\n  /**\n   * Defines a human-readable, author-localized abbreviated description for the role of an element, which is intended to be converted into Braille.\n   * @see aria-roledescription.\n   */\n  'aria-brailleroledescription'?: string | undefined\n  'aria-busy'?: Booleanish | undefined\n  /**\n   * Indicates the current \"checked\" state of checkboxes, radio buttons, and other widgets.\n   * @see aria-pressed @see aria-selected.\n   */\n  'aria-checked'?: boolean | 'false' | 'mixed' | 'true' | undefined\n  /**\n   * Defines the total number of columns in a table, grid, or treegrid.\n   * @see aria-colindex.\n   */\n  'aria-colcount'?: number | undefined\n  /**\n   * Defines an element's column index or position with respect to the total number of columns within a table, grid, or treegrid.\n   * @see aria-colcount @see aria-colspan.\n   */\n  'aria-colindex'?: number | undefined\n  /**\n   * Defines a human readable text alternative of aria-colindex.\n   * @see aria-rowindextext.\n   */\n  'aria-colindextext'?: string | undefined\n  /**\n   * Defines the number of columns spanned by a cell or gridcell within a table, grid, or treegrid.\n   * @see aria-colindex @see aria-rowspan.\n   */\n  'aria-colspan'?: number | undefined\n  /**\n   * Identifies the element (or elements) whose contents or presence are controlled by the current element.\n   * @see aria-owns.\n   */\n  'aria-controls'?: string | undefined\n  /** Indicates the element that represents the current item within a container or set of related elements. */\n  'aria-current'?:\n    | boolean\n    | 'false'\n    | 'true'\n    | 'page'\n    | 'step'\n    | 'location'\n    | 'date'\n    | 'time'\n    | undefined\n  /**\n   * Identifies the element (or elements) that describes the object.\n   * @see aria-labelledby\n   */\n  'aria-describedby'?: string | undefined\n  /**\n   * Defines a string value that describes or annotates the current element.\n   * @see related aria-describedby.\n   */\n  'aria-description'?: string | undefined\n  /**\n   * Identifies the element that provides a detailed, extended description for the object.\n   * @see aria-describedby.\n   */\n  'aria-details'?: string | undefined\n  /**\n   * Indicates that the element is perceivable but disabled, so it is not editable or otherwise operable.\n   * @see aria-hidden @see aria-readonly.\n   */\n  'aria-disabled'?: Booleanish | undefined\n  /**\n   * Indicates what functions can be performed when a dragged object is released on the drop target.\n   * @deprecated in ARIA 1.1\n   */\n  'aria-dropeffect'?:\n    | 'none'\n    | 'copy'\n    | 'execute'\n    | 'link'\n    | 'move'\n    | 'popup'\n    | undefined\n  /**\n   * Identifies the element that provides an error message for the object.\n   * @see aria-invalid @see aria-describedby.\n   */\n  'aria-errormessage'?: string | undefined\n  /** Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. */\n  'aria-expanded'?: Booleanish | undefined\n  /**\n   * Identifies the next element (or elements) in an alternate reading order of content which, at the user's discretion,\n   * allows assistive technology to override the general default of reading in document source order.\n   */\n  'aria-flowto'?: string | undefined\n  /**\n   * Indicates an element's \"grabbed\" state in a drag-and-drop operation.\n   * @deprecated in ARIA 1.1\n   */\n  'aria-grabbed'?: Booleanish | undefined\n  /** Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. */\n  'aria-haspopup'?:\n    | boolean\n    | 'false'\n    | 'true'\n    | 'menu'\n    | 'listbox'\n    | 'tree'\n    | 'grid'\n    | 'dialog'\n    | undefined\n  /**\n   * Indicates whether the element is exposed to an accessibility API.\n   * @see aria-disabled.\n   */\n  'aria-hidden'?: Booleanish | undefined\n  /**\n   * Indicates the entered value does not conform to the format expected by the application.\n   * @see aria-errormessage.\n   */\n  'aria-invalid'?:\n    | boolean\n    | 'false'\n    | 'true'\n    | 'grammar'\n    | 'spelling'\n    | undefined\n  /** Indicates keyboard shortcuts that an author has implemented to activate or give focus to an element. */\n  'aria-keyshortcuts'?: string | undefined\n  /**\n   * Defines a string value that labels the current element.\n   * @see aria-labelledby.\n   */\n  'aria-label'?: string | undefined\n  /**\n   * Identifies the element (or elements) that labels the current element.\n   * @see aria-describedby.\n   */\n  'aria-labelledby'?: string | undefined\n  /** Defines the hierarchical level of an element within a structure. */\n  'aria-level'?: number | undefined\n  /** Indicates that an element will be updated, and describes the types of updates the user agents, assistive technologies, and user can expect from the live region. */\n  'aria-live'?: 'off' | 'assertive' | 'polite' | undefined\n  /** Indicates whether an element is modal when displayed. */\n  'aria-modal'?: Booleanish | undefined\n  /** Indicates whether a text box accepts multiple lines of input or only a single line. */\n  'aria-multiline'?: Booleanish | undefined\n  /** Indicates that the user may select more than one item from the current selectable descendants. */\n  'aria-multiselectable'?: Booleanish | undefined\n  /** Indicates whether the element's orientation is horizontal, vertical, or unknown/ambiguous. */\n  'aria-orientation'?: 'horizontal' | 'vertical' | undefined\n  /**\n   * Identifies an element (or elements) in order to define a visual, functional, or contextual parent/child relationship\n   * between DOM elements where the DOM hierarchy cannot be used to represent the relationship.\n   * @see aria-controls.\n   */\n  'aria-owns'?: string | undefined\n  /**\n   * Defines a short hint (a word or short phrase) intended to aid the user with data entry when the control has no value.\n   * A hint could be a sample value or a brief description of the expected format.\n   */\n  'aria-placeholder'?: string | undefined\n  /**\n   * Defines an element's number or position in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM.\n   * @see aria-setsize.\n   */\n  'aria-posinset'?: number | undefined\n  /**\n   * Indicates the current \"pressed\" state of toggle buttons.\n   * @see aria-checked @see aria-selected.\n   */\n  'aria-pressed'?: boolean | 'false' | 'mixed' | 'true' | undefined\n  /**\n   * Indicates that the element is not editable, but is otherwise operable.\n   * @see aria-disabled.\n   */\n  'aria-readonly'?: Booleanish | undefined\n  /**\n   * Indicates what notifications the user agent will trigger when the accessibility tree within a live region is modified.\n   * @see aria-atomic.\n   */\n  'aria-relevant'?:\n    | 'additions'\n    | 'additions removals'\n    | 'additions text'\n    | 'all'\n    | 'removals'\n    | 'removals additions'\n    | 'removals text'\n    | 'text'\n    | 'text additions'\n    | 'text removals'\n    | undefined\n  /** Indicates that user input is required on the element before a form may be submitted. */\n  'aria-required'?: Booleanish | undefined\n  /** Defines a human-readable, author-localized description for the role of an element. */\n  'aria-roledescription'?: string | undefined\n  /**\n   * Defines the total number of rows in a table, grid, or treegrid.\n   * @see aria-rowindex.\n   */\n  'aria-rowcount'?: number | undefined\n  /**\n   * Defines an element's row index or position with respect to the total number of rows within a table, grid, or treegrid.\n   * @see aria-rowcount @see aria-rowspan.\n   */\n  'aria-rowindex'?: number | undefined\n  /**\n   * Defines a human readable text alternative of aria-rowindex.\n   * @see aria-colindextext.\n   */\n  'aria-rowindextext'?: string | undefined\n  /**\n   * Defines the number of rows spanned by a cell or gridcell within a table, grid, or treegrid.\n   * @see aria-rowindex @see aria-colspan.\n   */\n  'aria-rowspan'?: number | undefined\n  /**\n   * Indicates the current \"selected\" state of various widgets.\n   * @see aria-checked @see aria-pressed.\n   */\n  'aria-selected'?: Booleanish | undefined\n  /**\n   * Defines the number of items in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM.\n   * @see aria-posinset.\n   */\n  'aria-setsize'?: number | undefined\n  /** Indicates if items in a table or grid are sorted in ascending or descending order. */\n  'aria-sort'?: 'none' | 'ascending' | 'descending' | 'other' | undefined\n  /** Defines the maximum allowed value for a range widget. */\n  'aria-valuemax'?: number | undefined\n  /** Defines the minimum allowed value for a range widget. */\n  'aria-valuemin'?: number | undefined\n  /**\n   * Defines the current value for a range widget.\n   * @see aria-valuetext.\n   */\n  'aria-valuenow'?: number | undefined\n  /** Defines the human readable text alternative of aria-valuenow for a range widget. */\n  'aria-valuetext'?: string | undefined\n}\n\ninterface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {\n  // React-specific Attributes\n  defaultChecked?: boolean | undefined\n  defaultValue?: string | number | readonly string[] | undefined\n  suppressContentEditableWarning?: boolean | undefined\n  suppressHydrationWarning?: boolean | undefined\n\n  // Standard HTML Attributes\n  accessKey?: string | undefined\n  autoCapitalize?:\n    | 'off'\n    | 'none'\n    | 'on'\n    | 'sentences'\n    | 'words'\n    | 'characters'\n    | undefined\n    | (string & {})\n  autoFocus?: boolean | undefined\n  className?: string | undefined\n  contentEditable?: Booleanish | 'inherit' | 'plaintext-only' | undefined\n  contextMenu?: string | undefined\n  dir?: string | undefined\n  draggable?: Booleanish | undefined\n  enterKeyHint?:\n    | 'enter'\n    | 'done'\n    | 'go'\n    | 'next'\n    | 'previous'\n    | 'search'\n    | 'send'\n    | undefined\n  hidden?: boolean | undefined\n  id?: string | undefined\n  lang?: string | undefined\n  nonce?: string | undefined\n  slot?: string | undefined\n  spellCheck?: Booleanish | undefined\n  style?: CSSProperties | undefined\n  tabIndex?: number | undefined\n  title?: string | undefined\n  translate?: 'yes' | 'no' | undefined\n\n  // Unknown\n  radioGroup?: string | undefined // <command>, <menuitem>\n\n  // WAI-ARIA\n  role?: AriaRole | undefined\n\n  // RDFa Attributes\n  about?: string | undefined\n  content?: string | undefined\n  datatype?: string | undefined\n  inlist?: any\n  prefix?: string | undefined\n  property?: string | undefined\n  rel?: string | undefined\n  resource?: string | undefined\n  rev?: string | undefined\n  typeof?: string | undefined\n  vocab?: string | undefined\n\n  // Non-standard Attributes\n  autoCorrect?: string | undefined\n  autoSave?: string | undefined\n  color?: string | undefined\n  itemProp?: string | undefined\n  itemScope?: boolean | undefined\n  itemType?: string | undefined\n  itemID?: string | undefined\n  itemRef?: string | undefined\n  results?: number | undefined\n  security?: string | undefined\n  unselectable?: 'on' | 'off' | undefined\n\n  // Popover API\n  popover?: '' | 'auto' | 'manual' | undefined\n  popoverTargetAction?: 'toggle' | 'show' | 'hide' | undefined\n  popoverTarget?: string | undefined\n\n  // Living Standard\n  /**\n   * @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/inert\n   */\n  inert?: boolean | undefined\n  /**\n   * Hints at the type of data that might be entered by the user while editing the element or its contents\n   * @see {@link https://html.spec.whatwg.org/multipage/interaction.html#input-modalities:-the-inputmode-attribute}\n   */\n  inputMode?:\n    | 'none'\n    | 'text'\n    | 'tel'\n    | 'url'\n    | 'email'\n    | 'numeric'\n    | 'decimal'\n    | 'search'\n    | undefined\n  /**\n   * Specify that a standard HTML element should behave like a defined custom built-in element\n   * @see {@link https://html.spec.whatwg.org/multipage/custom-elements.html#attr-is}\n   */\n  is?: string | undefined\n  /**\n   * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/exportparts}\n   */\n  exportparts?: string | undefined\n  /**\n   * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/part}\n   */\n  part?: string | undefined\n}\n\ntype DetailedHTMLProps<E extends HTMLAttributes<T>, T> = E\n\nexport type HTMLElementType =\n  | 'a'\n  | 'abbr'\n  | 'address'\n  | 'area'\n  | 'article'\n  | 'aside'\n  | 'audio'\n  | 'b'\n  | 'base'\n  | 'bdi'\n  | 'bdo'\n  | 'big'\n  | 'blockquote'\n  | 'body'\n  | 'br'\n  | 'button'\n  | 'canvas'\n  | 'caption'\n  | 'center'\n  | 'cite'\n  | 'code'\n  | 'col'\n  | 'colgroup'\n  | 'data'\n  | 'datalist'\n  | 'dd'\n  | 'del'\n  | 'details'\n  | 'dfn'\n  | 'dialog'\n  | 'div'\n  | 'dl'\n  | 'dt'\n  | 'em'\n  | 'embed'\n  | 'fieldset'\n  | 'figcaption'\n  | 'figure'\n  | 'footer'\n  | 'form'\n  | 'h1'\n  | 'h2'\n  | 'h3'\n  | 'h4'\n  | 'h5'\n  | 'h6'\n  | 'head'\n  | 'header'\n  | 'hgroup'\n  | 'hr'\n  | 'html'\n  | 'i'\n  | 'iframe'\n  | 'img'\n  | 'input'\n  | 'ins'\n  | 'kbd'\n  | 'keygen'\n  | 'label'\n  | 'legend'\n  | 'li'\n  | 'link'\n  | 'main'\n  | 'map'\n  | 'mark'\n  | 'menu'\n  | 'menuitem'\n  | 'meta'\n  | 'meter'\n  | 'nav'\n  | 'noscript'\n  | 'object'\n  | 'ol'\n  | 'optgroup'\n  | 'option'\n  | 'output'\n  | 'p'\n  | 'param'\n  | 'picture'\n  | 'pre'\n  | 'progress'\n  | 'q'\n  | 'rp'\n  | 'rt'\n  | 'ruby'\n  | 's'\n  | 'samp'\n  | 'search'\n  | 'slot'\n  | 'script'\n  | 'section'\n  | 'select'\n  | 'small'\n  | 'source'\n  | 'span'\n  | 'strong'\n  | 'style'\n  | 'sub'\n  | 'summary'\n  | 'sup'\n  | 'table'\n  | 'template'\n  | 'tbody'\n  | 'td'\n  | 'textarea'\n  | 'tfoot'\n  | 'th'\n  | 'thead'\n  | 'time'\n  | 'title'\n  | 'tr'\n  | 'track'\n  | 'u'\n  | 'ul'\n  | 'var'\n  | 'video'\n  | 'wbr'\n  | 'webview'\n\nexport type SVGElementType =\n  | 'animate'\n  | 'circle'\n  | 'clipPath'\n  | 'defs'\n  | 'desc'\n  | 'ellipse'\n  | 'feBlend'\n  | 'feColorMatrix'\n  | 'feComponentTransfer'\n  | 'feComposite'\n  | 'feConvolveMatrix'\n  | 'feDiffuseLighting'\n  | 'feDisplacementMap'\n  | 'feDistantLight'\n  | 'feDropShadow'\n  | 'feFlood'\n  | 'feFuncA'\n  | 'feFuncB'\n  | 'feFuncG'\n  | 'feFuncR'\n  | 'feGaussianBlur'\n  | 'feImage'\n  | 'feMerge'\n  | 'feMergeNode'\n  | 'feMorphology'\n  | 'feOffset'\n  | 'fePointLight'\n  | 'feSpecularLighting'\n  | 'feSpotLight'\n  | 'feTile'\n  | 'feTurbulence'\n  | 'filter'\n  | 'foreignObject'\n  | 'g'\n  | 'image'\n  | 'line'\n  | 'linearGradient'\n  | 'marker'\n  | 'mask'\n  | 'metadata'\n  | 'path'\n  | 'pattern'\n  | 'polygon'\n  | 'polyline'\n  | 'radialGradient'\n  | 'rect'\n  | 'stop'\n  | 'svg'\n  | 'switch'\n  | 'symbol'\n  | 'text'\n  | 'textPath'\n  | 'tspan'\n  | 'use'\n  | 'view'\n\ntype HTMLAttributeReferrerPolicy =\n  | ''\n  | 'no-referrer'\n  | 'no-referrer-when-downgrade'\n  | 'origin'\n  | 'origin-when-cross-origin'\n  | 'same-origin'\n  | 'strict-origin'\n  | 'strict-origin-when-cross-origin'\n  | 'unsafe-url'\n\ntype HTMLAttributeAnchorTarget =\n  | '_self'\n  | '_blank'\n  | '_parent'\n  | '_top'\n  | (string & {})\n\ninterface AnchorHTMLAttributes<T> extends HTMLAttributes<T> {\n  download?: any\n  href?: string | undefined\n  hrefLang?: string | undefined\n  media?: string | undefined\n  ping?: string | undefined\n  target?: HTMLAttributeAnchorTarget | undefined\n  type?: string | undefined\n  referrerPolicy?: HTMLAttributeReferrerPolicy | undefined\n}\n\ninterface AudioHTMLAttributes<T> extends MediaHTMLAttributes<T> {}\n\ninterface AreaHTMLAttributes<T> extends HTMLAttributes<T> {\n  alt?: string | undefined\n  coords?: string | undefined\n  download?: any\n  href?: string | undefined\n  hrefLang?: string | undefined\n  media?: string | undefined\n  referrerPolicy?: HTMLAttributeReferrerPolicy | undefined\n  shape?: string | undefined\n  target?: string | undefined\n}\n\ninterface BaseHTMLAttributes<T> extends HTMLAttributes<T> {\n  href?: string | undefined\n  target?: string | undefined\n}\n\ninterface BlockquoteHTMLAttributes<T> extends HTMLAttributes<T> {\n  cite?: string | undefined\n}\n\ninterface ButtonHTMLAttributes<T> extends HTMLAttributes<T> {\n  disabled?: boolean | undefined\n  form?: string | undefined\n  formAction?:\n    | string\n    | ((formData: FormData) => void | Promise<void>)\n    | undefined\n  formEncType?: string | undefined\n  formMethod?: string | undefined\n  formNoValidate?: boolean | undefined\n  formTarget?: string | undefined\n  name?: string | undefined\n  type?: 'submit' | 'reset' | 'button' | undefined\n  value?: string | readonly string[] | number | undefined\n}\n\ninterface CanvasHTMLAttributes<T> extends HTMLAttributes<T> {\n  height?: number | string | undefined\n  width?: number | string | undefined\n}\n\ninterface ColHTMLAttributes<T> extends HTMLAttributes<T> {\n  span?: number | undefined\n  width?: number | string | undefined\n}\n\ninterface ColgroupHTMLAttributes<T> extends HTMLAttributes<T> {\n  span?: number | undefined\n}\n\ninterface DataHTMLAttributes<T> extends HTMLAttributes<T> {\n  value?: string | readonly string[] | number | undefined\n}\n\ninterface DetailsHTMLAttributes<T> extends HTMLAttributes<T> {\n  open?: boolean | undefined\n  name?: string | undefined\n}\n\ninterface DelHTMLAttributes<T> extends HTMLAttributes<T> {\n  cite?: string | undefined\n  dateTime?: string | undefined\n}\n\ninterface DialogHTMLAttributes<T> extends HTMLAttributes<T> {\n  open?: boolean | undefined\n}\n\ninterface EmbedHTMLAttributes<T> extends HTMLAttributes<T> {\n  height?: number | string | undefined\n  src?: string | undefined\n  type?: string | undefined\n  width?: number | string | undefined\n}\n\ninterface FieldsetHTMLAttributes<T> extends HTMLAttributes<T> {\n  disabled?: boolean | undefined\n  form?: string | undefined\n  name?: string | undefined\n}\n\ninterface FormHTMLAttributes<T> extends HTMLAttributes<T> {\n  acceptCharset?: string | undefined\n  action?: string | undefined | ((formData: FormData) => void | Promise<void>)\n  autoComplete?: string | undefined\n  encType?: string | undefined\n  method?: string | undefined\n  name?: string | undefined\n  noValidate?: boolean | undefined\n  target?: string | undefined\n}\n\ninterface HtmlHTMLAttributes<T> extends HTMLAttributes<T> {\n  manifest?: string | undefined\n}\n\ninterface IframeHTMLAttributes<T> extends HTMLAttributes<T> {\n  allow?: string | undefined\n  allowFullScreen?: boolean | undefined\n  allowTransparency?: boolean | undefined\n  /** @deprecated */\n  frameBorder?: number | string | undefined\n  height?: number | string | undefined\n  loading?: 'eager' | 'lazy' | undefined\n  /** @deprecated */\n  marginHeight?: number | undefined\n  /** @deprecated */\n  marginWidth?: number | undefined\n  name?: string | undefined\n  referrerPolicy?: HTMLAttributeReferrerPolicy | undefined\n  sandbox?: string | undefined\n  /** @deprecated */\n  scrolling?: string | undefined\n  seamless?: boolean | undefined\n  src?: string | undefined\n  srcDoc?: string | undefined\n  width?: number | string | undefined\n}\n\ninterface ImgHTMLAttributes<T> extends HTMLAttributes<T> {\n  alt?: string | undefined\n  crossOrigin?: CrossOrigin\n  decoding?: 'async' | 'auto' | 'sync' | undefined\n  fetchPriority?: 'high' | 'low' | 'auto'\n  height?: number | string | undefined\n  loading?: 'eager' | 'lazy' | undefined\n  referrerPolicy?: HTMLAttributeReferrerPolicy | undefined\n  sizes?: string | undefined\n  src?: string | undefined\n  srcSet?: string | undefined\n  useMap?: string | undefined\n  width?: number | string | undefined\n}\n\ninterface InsHTMLAttributes<T> extends HTMLAttributes<T> {\n  cite?: string | undefined\n  dateTime?: string | undefined\n}\n\ntype HTMLInputTypeAttribute =\n  | 'button'\n  | 'checkbox'\n  | 'color'\n  | 'date'\n  | 'datetime-local'\n  | 'email'\n  | 'file'\n  | 'hidden'\n  | 'image'\n  | 'month'\n  | 'number'\n  | 'password'\n  | 'radio'\n  | 'range'\n  | 'reset'\n  | 'search'\n  | 'submit'\n  | 'tel'\n  | 'text'\n  | 'time'\n  | 'url'\n  | 'week'\n  | (string & {})\n\ntype AutoFillAddressKind = 'billing' | 'shipping'\ntype AutoFillBase = '' | 'off' | 'on'\ntype AutoFillContactField =\n  | 'email'\n  | 'tel'\n  | 'tel-area-code'\n  | 'tel-country-code'\n  | 'tel-extension'\n  | 'tel-local'\n  | 'tel-local-prefix'\n  | 'tel-local-suffix'\n  | 'tel-national'\ntype AutoFillContactKind = 'home' | 'mobile' | 'work'\ntype AutoFillCredentialField = 'webauthn'\ntype AutoFillNormalField =\n  | 'additional-name'\n  | 'address-level1'\n  | 'address-level2'\n  | 'address-level3'\n  | 'address-level4'\n  | 'address-line1'\n  | 'address-line2'\n  | 'address-line3'\n  | 'bday-day'\n  | 'bday-month'\n  | 'bday-year'\n  | 'cc-csc'\n  | 'cc-exp'\n  | 'cc-exp-month'\n  | 'cc-exp-year'\n  | 'cc-family-name'\n  | 'cc-given-name'\n  | 'cc-name'\n  | 'cc-number'\n  | 'cc-type'\n  | 'country'\n  | 'country-name'\n  | 'current-password'\n  | 'family-name'\n  | 'given-name'\n  | 'honorific-prefix'\n  | 'honorific-suffix'\n  | 'name'\n  | 'new-password'\n  | 'one-time-code'\n  | 'organization'\n  | 'postal-code'\n  | 'street-address'\n  | 'transaction-amount'\n  | 'transaction-currency'\n  | 'username'\ntype OptionalPrefixToken<T extends string> = `${T} ` | ''\ntype OptionalPostfixToken<T extends string> = ` ${T}` | ''\ntype AutoFillField =\n  | AutoFillNormalField\n  | `${OptionalPrefixToken<AutoFillContactKind>}${AutoFillContactField}`\ntype AutoFillSection = `section-${string}`\ntype AutoFill =\n  | AutoFillBase\n  | `${OptionalPrefixToken<AutoFillSection>}${OptionalPrefixToken<AutoFillAddressKind>}${AutoFillField}${OptionalPostfixToken<AutoFillCredentialField>}`\ntype HTMLInputAutoCompleteAttribute = AutoFill | (string & {})\n\ninterface InputHTMLAttributes<T> extends HTMLAttributes<T> {\n  accept?: string | undefined\n  alt?: string | undefined\n  autoComplete?: HTMLInputAutoCompleteAttribute | undefined\n  capture?: boolean | 'user' | 'environment' | undefined // https://www.w3.org/TR/html-media-capture/#the-capture-attribute\n  checked?: boolean | undefined\n  disabled?: boolean | undefined\n  form?: string | undefined\n  formAction?:\n    | string\n    | ((formData: FormData) => void | Promise<void>)\n    | undefined\n  formEncType?: string | undefined\n  formMethod?: string | undefined\n  formNoValidate?: boolean | undefined\n  formTarget?: string | undefined\n  height?: number | string | undefined\n  list?: string | undefined\n  max?: number | string | undefined\n  maxLength?: number | undefined\n  min?: number | string | undefined\n  minLength?: number | undefined\n  multiple?: boolean | undefined\n  name?: string | undefined\n  pattern?: string | undefined\n  placeholder?: string | undefined\n  readOnly?: boolean | undefined\n  required?: boolean | undefined\n  size?: number | undefined\n  src?: string | undefined\n  step?: number | string | undefined\n  type?: HTMLInputTypeAttribute | undefined\n  value?: string | readonly string[] | number | undefined\n  width?: number | string | undefined\n}\n\ninterface KeygenHTMLAttributes<T> extends HTMLAttributes<T> {\n  challenge?: string | undefined\n  disabled?: boolean | undefined\n  form?: string | undefined\n  keyType?: string | undefined\n  keyParams?: string | undefined\n  name?: string | undefined\n}\n\ninterface LabelHTMLAttributes<T> extends HTMLAttributes<T> {\n  form?: string | undefined\n  htmlFor?: string | undefined\n}\n\ninterface LiHTMLAttributes<T> extends HTMLAttributes<T> {\n  value?: string | readonly string[] | number | undefined\n}\n\ninterface LinkHTMLAttributes<T> extends HTMLAttributes<T> {\n  as?: string | undefined\n  blocking?: 'render' | (string & {}) | undefined\n  crossOrigin?: CrossOrigin\n  fetchPriority?: 'high' | 'low' | 'auto'\n  href?: string | undefined\n  hrefLang?: string | undefined\n  integrity?: string | undefined\n  media?: string | undefined\n  imageSrcSet?: string | undefined\n  imageSizes?: string | undefined\n  referrerPolicy?: HTMLAttributeReferrerPolicy | undefined\n  sizes?: string | undefined\n  type?: string | undefined\n  charSet?: string | undefined\n\n  // React props\n  precedence?: string | undefined\n}\n\ninterface MapHTMLAttributes<T> extends HTMLAttributes<T> {\n  name?: string | undefined\n}\n\ninterface MenuHTMLAttributes<T> extends HTMLAttributes<T> {\n  type?: string | undefined\n}\n\ninterface MediaHTMLAttributes<T> extends HTMLAttributes<T> {\n  autoPlay?: boolean | undefined\n  controls?: boolean | undefined\n  controlsList?: string | undefined\n  crossOrigin?: CrossOrigin\n  loop?: boolean | undefined\n  mediaGroup?: string | undefined\n  muted?: boolean | undefined\n  playsInline?: boolean | undefined\n  preload?: string | undefined\n  src?: string | undefined\n}\n\ninterface MetaHTMLAttributes<T> extends HTMLAttributes<T> {\n  charSet?: string | undefined\n  content?: string | undefined\n  httpEquiv?: string | undefined\n  media?: string | undefined\n  name?: string | undefined\n}\n\ninterface MeterHTMLAttributes<T> extends HTMLAttributes<T> {\n  form?: string | undefined\n  high?: number | undefined\n  low?: number | undefined\n  max?: number | string | undefined\n  min?: number | string | undefined\n  optimum?: number | undefined\n  value?: string | readonly string[] | number | undefined\n}\n\ninterface QuoteHTMLAttributes<T> extends HTMLAttributes<T> {\n  cite?: string | undefined\n}\n\ninterface ObjectHTMLAttributes<T> extends HTMLAttributes<T> {\n  classID?: string | undefined\n  data?: string | undefined\n  form?: string | undefined\n  height?: number | string | undefined\n  name?: string | undefined\n  type?: string | undefined\n  useMap?: string | undefined\n  width?: number | string | undefined\n  wmode?: string | undefined\n}\n\ninterface OlHTMLAttributes<T> extends HTMLAttributes<T> {\n  reversed?: boolean | undefined\n  start?: number | undefined\n  type?: '1' | 'a' | 'A' | 'i' | 'I' | undefined\n}\n\ninterface OptgroupHTMLAttributes<T> extends HTMLAttributes<T> {\n  disabled?: boolean | undefined\n  label?: string | undefined\n}\n\ninterface OptionHTMLAttributes<T> extends HTMLAttributes<T> {\n  disabled?: boolean | undefined\n  label?: string | undefined\n  selected?: boolean | undefined\n  value?: string | readonly string[] | number | undefined\n}\n\ninterface OutputHTMLAttributes<T> extends HTMLAttributes<T> {\n  form?: string | undefined\n  htmlFor?: string | undefined\n  name?: string | undefined\n}\n\ninterface ParamHTMLAttributes<T> extends HTMLAttributes<T> {\n  name?: string | undefined\n  value?: string | readonly string[] | number | undefined\n}\n\ninterface ProgressHTMLAttributes<T> extends HTMLAttributes<T> {\n  max?: number | string | undefined\n  value?: string | readonly string[] | number | undefined\n}\n\ninterface SlotHTMLAttributes<T> extends HTMLAttributes<T> {\n  name?: string | undefined\n}\n\ninterface ScriptHTMLAttributes<T> extends HTMLAttributes<T> {\n  async?: boolean | undefined\n  blocking?: 'render' | (string & {}) | undefined\n  /** @deprecated */\n  charSet?: string | undefined\n  crossOrigin?: CrossOrigin\n  defer?: boolean | undefined\n  integrity?: string | undefined\n  noModule?: boolean | undefined\n  referrerPolicy?: HTMLAttributeReferrerPolicy | undefined\n  src?: string | undefined\n  type?: string | undefined\n}\n\ninterface SelectHTMLAttributes<T> extends HTMLAttributes<T> {\n  autoComplete?: string | undefined\n  disabled?: boolean | undefined\n  form?: string | undefined\n  multiple?: boolean | undefined\n  name?: string | undefined\n  required?: boolean | undefined\n  size?: number | undefined\n  value?: string | readonly string[] | number | undefined\n}\n\ninterface SourceHTMLAttributes<T> extends HTMLAttributes<T> {\n  height?: number | string | undefined\n  media?: string | undefined\n  sizes?: string | undefined\n  src?: string | undefined\n  srcSet?: string | undefined\n  type?: string | undefined\n  width?: number | string | undefined\n}\n\ninterface StyleHTMLAttributes<T> extends HTMLAttributes<T> {\n  blocking?: 'render' | (string & {}) | undefined\n  media?: string | undefined\n  scoped?: boolean | undefined\n  type?: string | undefined\n\n  // React props\n  href?: string | undefined\n  precedence?: string | undefined\n}\n\ninterface TableHTMLAttributes<T> extends HTMLAttributes<T> {\n  align?: 'left' | 'center' | 'right' | undefined\n  bgcolor?: string | undefined\n  border?: number | undefined\n  cellPadding?: number | string | undefined\n  cellSpacing?: number | string | undefined\n  frame?: boolean | undefined\n  rules?: 'none' | 'groups' | 'rows' | 'columns' | 'all' | undefined\n  summary?: string | undefined\n  width?: number | string | undefined\n}\n\ninterface TextareaHTMLAttributes<T> extends HTMLAttributes<T> {\n  autoComplete?: string | undefined\n  cols?: number | undefined\n  dirName?: string | undefined\n  disabled?: boolean | undefined\n  form?: string | undefined\n  maxLength?: number | undefined\n  minLength?: number | undefined\n  name?: string | undefined\n  placeholder?: string | undefined\n  readOnly?: boolean | undefined\n  required?: boolean | undefined\n  rows?: number | undefined\n  value?: string | readonly string[] | number | undefined\n  wrap?: string | undefined\n}\n\ninterface TdHTMLAttributes<T> extends HTMLAttributes<T> {\n  align?: 'left' | 'center' | 'right' | 'justify' | 'char' | undefined\n  colSpan?: number | undefined\n  headers?: string | undefined\n  rowSpan?: number | undefined\n  scope?: string | undefined\n  abbr?: string | undefined\n  height?: number | string | undefined\n  width?: number | string | undefined\n  valign?: 'top' | 'middle' | 'bottom' | 'baseline' | undefined\n}\n\ninterface ThHTMLAttributes<T> extends HTMLAttributes<T> {\n  align?: 'left' | 'center' | 'right' | 'justify' | 'char' | undefined\n  colSpan?: number | undefined\n  headers?: string | undefined\n  rowSpan?: number | undefined\n  scope?: string | undefined\n  abbr?: string | undefined\n}\n\ninterface TimeHTMLAttributes<T> extends HTMLAttributes<T> {\n  dateTime?: string | undefined\n}\n\ninterface TrackHTMLAttributes<T> extends HTMLAttributes<T> {\n  default?: boolean | undefined\n  kind?: string | undefined\n  label?: string | undefined\n  src?: string | undefined\n  srcLang?: string | undefined\n}\n\ninterface VideoHTMLAttributes<T> extends MediaHTMLAttributes<T> {\n  height?: number | string | undefined\n  playsInline?: boolean | undefined\n  poster?: string | undefined\n  width?: number | string | undefined\n  disablePictureInPicture?: boolean | undefined\n  disableRemotePlayback?: boolean | undefined\n}\n\n// this list is \"complete\" in that it contains every SVG attribute\n// that React supports, but the types can be improved.\n// Full list here: https://facebook.github.io/react/docs/dom-elements.html\n//\n// The three broad type categories are (in order of restrictiveness):\n//   - \"number | string\"\n//   - \"string\"\n//   - union of string literals\ninterface SVGAttributes<T> extends AriaAttributes, DOMAttributes<T> {\n  // React-specific Attributes\n  suppressHydrationWarning?: boolean | undefined\n\n  // Attributes which also defined in HTMLAttributes\n  // See comment in SVGDOMPropertyConfig.js\n  className?: string | undefined\n  color?: string | undefined\n  height?: number | string | undefined\n  id?: string | undefined\n  lang?: string | undefined\n  max?: number | string | undefined\n  media?: string | undefined\n  method?: string | undefined\n  min?: number | string | undefined\n  name?: string | undefined\n  style?: CSSProperties | undefined\n  target?: string | undefined\n  type?: string | undefined\n  width?: number | string | undefined\n\n  // Other HTML properties supported by SVG elements in browsers\n  role?: AriaRole | undefined\n  tabIndex?: number | undefined\n  crossOrigin?: CrossOrigin\n\n  // SVG Specific attributes\n  accentHeight?: number | string | undefined\n  accumulate?: 'none' | 'sum' | undefined\n  additive?: 'replace' | 'sum' | undefined\n  alignmentBaseline?:\n    | 'auto'\n    | 'baseline'\n    | 'before-edge'\n    | 'text-before-edge'\n    | 'middle'\n    | 'central'\n    | 'after-edge'\n    | 'text-after-edge'\n    | 'ideographic'\n    | 'alphabetic'\n    | 'hanging'\n    | 'mathematical'\n    | 'inherit'\n    | undefined\n  allowReorder?: 'no' | 'yes' | undefined\n  alphabetic?: number | string | undefined\n  amplitude?: number | string | undefined\n  arabicForm?: 'initial' | 'medial' | 'terminal' | 'isolated' | undefined\n  ascent?: number | string | undefined\n  attributeName?: string | undefined\n  attributeType?: string | undefined\n  autoReverse?: Booleanish | undefined\n  azimuth?: number | string | undefined\n  baseFrequency?: number | string | undefined\n  baselineShift?: number | string | undefined\n  baseProfile?: number | string | undefined\n  bbox?: number | string | undefined\n  begin?: number | string | undefined\n  bias?: number | string | undefined\n  by?: number | string | undefined\n  calcMode?: number | string | undefined\n  capHeight?: number | string | undefined\n  clip?: number | string | undefined\n  clipPath?: string | undefined\n  clipPathUnits?: number | string | undefined\n  clipRule?: number | string | undefined\n  colorInterpolation?: number | string | undefined\n  colorInterpolationFilters?:\n    | 'auto'\n    | 'sRGB'\n    | 'linearRGB'\n    | 'inherit'\n    | undefined\n  colorProfile?: number | string | undefined\n  colorRendering?: number | string | undefined\n  contentScriptType?: number | string | undefined\n  contentStyleType?: number | string | undefined\n  cursor?: number | string | undefined\n  cx?: number | string | undefined\n  cy?: number | string | undefined\n  d?: string | undefined\n  decelerate?: number | string | undefined\n  descent?: number | string | undefined\n  diffuseConstant?: number | string | undefined\n  direction?: number | string | undefined\n  display?: number | string | undefined\n  divisor?: number | string | undefined\n  dominantBaseline?: number | string | undefined\n  dur?: number | string | undefined\n  dx?: number | string | undefined\n  dy?: number | string | undefined\n  edgeMode?: number | string | undefined\n  elevation?: number | string | undefined\n  enableBackground?: number | string | undefined\n  end?: number | string | undefined\n  exponent?: number | string | undefined\n  externalResourcesRequired?: Booleanish | undefined\n  fill?: string | undefined\n  fillOpacity?: number | string | undefined\n  fillRule?: 'nonzero' | 'evenodd' | 'inherit' | undefined\n  filter?: string | undefined\n  filterRes?: number | string | undefined\n  filterUnits?: number | string | undefined\n  floodColor?: number | string | undefined\n  floodOpacity?: number | string | undefined\n  focusable?: Booleanish | 'auto' | undefined\n  fontFamily?: string | undefined\n  fontSize?: number | string | undefined\n  fontSizeAdjust?: number | string | undefined\n  fontStretch?: number | string | undefined\n  fontStyle?: number | string | undefined\n  fontVariant?: number | string | undefined\n  fontWeight?: number | string | undefined\n  format?: number | string | undefined\n  fr?: number | string | undefined\n  from?: number | string | undefined\n  fx?: number | string | undefined\n  fy?: number | string | undefined\n  g1?: number | string | undefined\n  g2?: number | string | undefined\n  glyphName?: number | string | undefined\n  glyphOrientationHorizontal?: number | string | undefined\n  glyphOrientationVertical?: number | string | undefined\n  glyphRef?: number | string | undefined\n  gradientTransform?: string | undefined\n  gradientUnits?: string | undefined\n  hanging?: number | string | undefined\n  horizAdvX?: number | string | undefined\n  horizOriginX?: number | string | undefined\n  href?: string | undefined\n  ideographic?: number | string | undefined\n  imageRendering?: number | string | undefined\n  in2?: number | string | undefined\n  in?: string | undefined\n  intercept?: number | string | undefined\n  k1?: number | string | undefined\n  k2?: number | string | undefined\n  k3?: number | string | undefined\n  k4?: number | string | undefined\n  k?: number | string | undefined\n  kernelMatrix?: number | string | undefined\n  kernelUnitLength?: number | string | undefined\n  kerning?: number | string | undefined\n  keyPoints?: number | string | undefined\n  keySplines?: number | string | undefined\n  keyTimes?: number | string | undefined\n  lengthAdjust?: number | string | undefined\n  letterSpacing?: number | string | undefined\n  lightingColor?: number | string | undefined\n  limitingConeAngle?: number | string | undefined\n  local?: number | string | undefined\n  markerEnd?: string | undefined\n  markerHeight?: number | string | undefined\n  markerMid?: string | undefined\n  markerStart?: string | undefined\n  markerUnits?: number | string | undefined\n  markerWidth?: number | string | undefined\n  mask?: string | undefined\n  maskContentUnits?: number | string | undefined\n  maskUnits?: number | string | undefined\n  mathematical?: number | string | undefined\n  mode?: number | string | undefined\n  numOctaves?: number | string | undefined\n  offset?: number | string | undefined\n  opacity?: number | string | undefined\n  operator?: number | string | undefined\n  order?: number | string | undefined\n  orient?: number | string | undefined\n  orientation?: number | string | undefined\n  origin?: number | string | undefined\n  overflow?: number | string | undefined\n  overlinePosition?: number | string | undefined\n  overlineThickness?: number | string | undefined\n  paintOrder?: number | string | undefined\n  panose1?: number | string | undefined\n  path?: string | undefined\n  pathLength?: number | string | undefined\n  patternContentUnits?: string | undefined\n  patternTransform?: number | string | undefined\n  patternUnits?: string | undefined\n  pointerEvents?: number | string | undefined\n  points?: string | undefined\n  pointsAtX?: number | string | undefined\n  pointsAtY?: number | string | undefined\n  pointsAtZ?: number | string | undefined\n  preserveAlpha?: Booleanish | undefined\n  preserveAspectRatio?: string | undefined\n  primitiveUnits?: number | string | undefined\n  r?: number | string | undefined\n  radius?: number | string | undefined\n  refX?: number | string | undefined\n  refY?: number | string | undefined\n  renderingIntent?: number | string | undefined\n  repeatCount?: number | string | undefined\n  repeatDur?: number | string | undefined\n  requiredExtensions?: number | string | undefined\n  requiredFeatures?: number | string | undefined\n  restart?: number | string | undefined\n  result?: string | undefined\n  rotate?: number | string | undefined\n  rx?: number | string | undefined\n  ry?: number | string | undefined\n  scale?: number | string | undefined\n  seed?: number | string | undefined\n  shapeRendering?: number | string | undefined\n  slope?: number | string | undefined\n  spacing?: number | string | undefined\n  specularConstant?: number | string | undefined\n  specularExponent?: number | string | undefined\n  speed?: number | string | undefined\n  spreadMethod?: string | undefined\n  startOffset?: number | string | undefined\n  stdDeviation?: number | string | undefined\n  stemh?: number | string | undefined\n  stemv?: number | string | undefined\n  stitchTiles?: number | string | undefined\n  stopColor?: string | undefined\n  stopOpacity?: number | string | undefined\n  strikethroughPosition?: number | string | undefined\n  strikethroughThickness?: number | string | undefined\n  string?: number | string | undefined\n  stroke?: string | undefined\n  strokeDasharray?: string | number | undefined\n  strokeDashoffset?: string | number | undefined\n  strokeLinecap?: 'butt' | 'round' | 'square' | 'inherit' | undefined\n  strokeLinejoin?: 'miter' | 'round' | 'bevel' | 'inherit' | undefined\n  strokeMiterlimit?: number | string | undefined\n  strokeOpacity?: number | string | undefined\n  strokeWidth?: number | string | undefined\n  surfaceScale?: number | string | undefined\n  systemLanguage?: number | string | undefined\n  tableValues?: number | string | undefined\n  targetX?: number | string | undefined\n  targetY?: number | string | undefined\n  textAnchor?: string | undefined\n  textDecoration?: number | string | undefined\n  textLength?: number | string | undefined\n  textRendering?: number | string | undefined\n  to?: number | string | undefined\n  transform?: string | undefined\n  u1?: number | string | undefined\n  u2?: number | string | undefined\n  underlinePosition?: number | string | undefined\n  underlineThickness?: number | string | undefined\n  unicode?: number | string | undefined\n  unicodeBidi?: number | string | undefined\n  unicodeRange?: number | string | undefined\n  unitsPerEm?: number | string | undefined\n  vAlphabetic?: number | string | undefined\n  values?: string | undefined\n  vectorEffect?: number | string | undefined\n  version?: string | undefined\n  vertAdvY?: number | string | undefined\n  vertOriginX?: number | string | undefined\n  vertOriginY?: number | string | undefined\n  vHanging?: number | string | undefined\n  vIdeographic?: number | string | undefined\n  viewBox?: string | undefined\n  viewTarget?: number | string | undefined\n  visibility?: number | string | undefined\n  vMathematical?: number | string | undefined\n  widths?: number | string | undefined\n  wordSpacing?: number | string | undefined\n  writingMode?: number | string | undefined\n  x1?: number | string | undefined\n  x2?: number | string | undefined\n  x?: number | string | undefined\n  xChannelSelector?: string | undefined\n  xHeight?: number | string | undefined\n  xlinkActuate?: string | undefined\n  xlinkArcrole?: string | undefined\n  xlinkHref?: string | undefined\n  xlinkRole?: string | undefined\n  xlinkShow?: string | undefined\n  xlinkTitle?: string | undefined\n  xlinkType?: string | undefined\n  xmlBase?: string | undefined\n  xmlLang?: string | undefined\n  xmlns?: string | undefined\n  xmlnsXlink?: string | undefined\n  xmlSpace?: string | undefined\n  y1?: number | string | undefined\n  y2?: number | string | undefined\n  y?: number | string | undefined\n  yChannelSelector?: string | undefined\n  z?: number | string | undefined\n  zoomAndPan?: string | undefined\n}\n\ninterface WebViewHTMLAttributes<T> extends HTMLAttributes<T> {\n  allowFullScreen?: boolean | undefined\n  allowpopups?: boolean | undefined\n  autosize?: boolean | undefined\n  blinkfeatures?: string | undefined\n  disableblinkfeatures?: string | undefined\n  disableguestresize?: boolean | undefined\n  disablewebsecurity?: boolean | undefined\n  guestinstance?: string | undefined\n  httpreferrer?: string | undefined\n  nodeintegration?: boolean | undefined\n  partition?: string | undefined\n  plugins?: boolean | undefined\n  preload?: string | undefined\n  src?: string | undefined\n  useragent?: string | undefined\n  webpreferences?: string | undefined\n}\n\n/**\n * Subset of HTML elements that Satori supports.\n *\n * @todo remove unsupported elements.\n */\nexport interface DefinedIntrinsicElements {\n  // HTML\n  a: DetailedHTMLProps<\n    AnchorHTMLAttributes<HTMLAnchorElement>,\n    HTMLAnchorElement\n  >\n  abbr: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  address: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  area: DetailedHTMLProps<AreaHTMLAttributes<HTMLAreaElement>, HTMLAreaElement>\n  article: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  aside: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  audio: DetailedHTMLProps<\n    AudioHTMLAttributes<HTMLAudioElement>,\n    HTMLAudioElement\n  >\n  b: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  base: DetailedHTMLProps<BaseHTMLAttributes<HTMLBaseElement>, HTMLBaseElement>\n  bdi: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  bdo: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  big: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  blockquote: DetailedHTMLProps<\n    BlockquoteHTMLAttributes<HTMLQuoteElement>,\n    HTMLQuoteElement\n  >\n  body: DetailedHTMLProps<HTMLAttributes<HTMLBodyElement>, HTMLBodyElement>\n  br: DetailedHTMLProps<HTMLAttributes<HTMLBRElement>, HTMLBRElement>\n  button: DetailedHTMLProps<\n    ButtonHTMLAttributes<HTMLButtonElement>,\n    HTMLButtonElement\n  >\n  canvas: DetailedHTMLProps<\n    CanvasHTMLAttributes<HTMLCanvasElement>,\n    HTMLCanvasElement\n  >\n  caption: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  center: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  cite: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  code: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  col: DetailedHTMLProps<\n    ColHTMLAttributes<HTMLTableColElement>,\n    HTMLTableColElement\n  >\n  colgroup: DetailedHTMLProps<\n    ColgroupHTMLAttributes<HTMLTableColElement>,\n    HTMLTableColElement\n  >\n  data: DetailedHTMLProps<DataHTMLAttributes<HTMLDataElement>, HTMLDataElement>\n  datalist: DetailedHTMLProps<\n    HTMLAttributes<HTMLDataListElement>,\n    HTMLDataListElement\n  >\n  dd: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  del: DetailedHTMLProps<DelHTMLAttributes<HTMLModElement>, HTMLModElement>\n  details: DetailedHTMLProps<\n    DetailsHTMLAttributes<HTMLDetailsElement>,\n    HTMLDetailsElement\n  >\n  dfn: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  dialog: DetailedHTMLProps<\n    DialogHTMLAttributes<HTMLDialogElement>,\n    HTMLDialogElement\n  >\n  div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>\n  dl: DetailedHTMLProps<HTMLAttributes<HTMLDListElement>, HTMLDListElement>\n  dt: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  em: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  embed: DetailedHTMLProps<\n    EmbedHTMLAttributes<HTMLEmbedElement>,\n    HTMLEmbedElement\n  >\n  fieldset: DetailedHTMLProps<\n    FieldsetHTMLAttributes<HTMLFieldSetElement>,\n    HTMLFieldSetElement\n  >\n  figcaption: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  figure: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  footer: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  form: DetailedHTMLProps<FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>\n  h1: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>\n  h2: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>\n  h3: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>\n  h4: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>\n  h5: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>\n  h6: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>\n  head: DetailedHTMLProps<HTMLAttributes<HTMLHeadElement>, HTMLHeadElement>\n  header: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  hgroup: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  hr: DetailedHTMLProps<HTMLAttributes<HTMLHRElement>, HTMLHRElement>\n  html: DetailedHTMLProps<HtmlHTMLAttributes<HTMLHtmlElement>, HTMLHtmlElement>\n  i: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  iframe: DetailedHTMLProps<\n    IframeHTMLAttributes<HTMLIFrameElement>,\n    HTMLIFrameElement\n  >\n  img: DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>\n  input: DetailedHTMLProps<\n    InputHTMLAttributes<HTMLInputElement>,\n    HTMLInputElement\n  >\n  ins: DetailedHTMLProps<InsHTMLAttributes<HTMLModElement>, HTMLModElement>\n  kbd: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  keygen: DetailedHTMLProps<KeygenHTMLAttributes<HTMLElement>, HTMLElement>\n  label: DetailedHTMLProps<\n    LabelHTMLAttributes<HTMLLabelElement>,\n    HTMLLabelElement\n  >\n  legend: DetailedHTMLProps<\n    HTMLAttributes<HTMLLegendElement>,\n    HTMLLegendElement\n  >\n  li: DetailedHTMLProps<LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>\n  link: DetailedHTMLProps<LinkHTMLAttributes<HTMLLinkElement>, HTMLLinkElement>\n  main: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  map: DetailedHTMLProps<MapHTMLAttributes<HTMLMapElement>, HTMLMapElement>\n  mark: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  menu: DetailedHTMLProps<MenuHTMLAttributes<HTMLElement>, HTMLElement>\n  menuitem: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  meta: DetailedHTMLProps<MetaHTMLAttributes<HTMLMetaElement>, HTMLMetaElement>\n  meter: DetailedHTMLProps<\n    MeterHTMLAttributes<HTMLMeterElement>,\n    HTMLMeterElement\n  >\n  nav: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  noindex: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  noscript: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  object: DetailedHTMLProps<\n    ObjectHTMLAttributes<HTMLObjectElement>,\n    HTMLObjectElement\n  >\n  ol: DetailedHTMLProps<OlHTMLAttributes<HTMLOListElement>, HTMLOListElement>\n  optgroup: DetailedHTMLProps<\n    OptgroupHTMLAttributes<HTMLOptGroupElement>,\n    HTMLOptGroupElement\n  >\n  option: DetailedHTMLProps<\n    OptionHTMLAttributes<HTMLOptionElement>,\n    HTMLOptionElement\n  >\n  output: DetailedHTMLProps<\n    OutputHTMLAttributes<HTMLOutputElement>,\n    HTMLOutputElement\n  >\n  p: DetailedHTMLProps<\n    HTMLAttributes<HTMLParagraphElement>,\n    HTMLParagraphElement\n  >\n  param: DetailedHTMLProps<\n    ParamHTMLAttributes<HTMLParamElement>,\n    HTMLParamElement\n  >\n  picture: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  pre: DetailedHTMLProps<HTMLAttributes<HTMLPreElement>, HTMLPreElement>\n  progress: DetailedHTMLProps<\n    ProgressHTMLAttributes<HTMLProgressElement>,\n    HTMLProgressElement\n  >\n  q: DetailedHTMLProps<QuoteHTMLAttributes<HTMLQuoteElement>, HTMLQuoteElement>\n  rp: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  rt: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  ruby: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  s: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  samp: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  search: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  slot: DetailedHTMLProps<SlotHTMLAttributes<HTMLSlotElement>, HTMLSlotElement>\n  script: DetailedHTMLProps<\n    ScriptHTMLAttributes<HTMLScriptElement>,\n    HTMLScriptElement\n  >\n  section: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  select: DetailedHTMLProps<\n    SelectHTMLAttributes<HTMLSelectElement>,\n    HTMLSelectElement\n  >\n  small: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  source: DetailedHTMLProps<\n    SourceHTMLAttributes<HTMLSourceElement>,\n    HTMLSourceElement\n  >\n  span: DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>\n  strong: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  style: DetailedHTMLProps<\n    StyleHTMLAttributes<HTMLStyleElement>,\n    HTMLStyleElement\n  >\n  sub: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  summary: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  sup: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  table: DetailedHTMLProps<\n    TableHTMLAttributes<HTMLTableElement>,\n    HTMLTableElement\n  >\n  template: DetailedHTMLProps<\n    HTMLAttributes<HTMLTemplateElement>,\n    HTMLTemplateElement\n  >\n  tbody: DetailedHTMLProps<\n    HTMLAttributes<HTMLTableSectionElement>,\n    HTMLTableSectionElement\n  >\n  td: DetailedHTMLProps<\n    TdHTMLAttributes<HTMLTableDataCellElement>,\n    HTMLTableDataCellElement\n  >\n  textarea: DetailedHTMLProps<\n    TextareaHTMLAttributes<HTMLTextAreaElement>,\n    HTMLTextAreaElement\n  >\n  tfoot: DetailedHTMLProps<\n    HTMLAttributes<HTMLTableSectionElement>,\n    HTMLTableSectionElement\n  >\n  th: DetailedHTMLProps<\n    ThHTMLAttributes<HTMLTableHeaderCellElement>,\n    HTMLTableHeaderCellElement\n  >\n  thead: DetailedHTMLProps<\n    HTMLAttributes<HTMLTableSectionElement>,\n    HTMLTableSectionElement\n  >\n  time: DetailedHTMLProps<TimeHTMLAttributes<HTMLTimeElement>, HTMLTimeElement>\n  title: DetailedHTMLProps<HTMLAttributes<HTMLTitleElement>, HTMLTitleElement>\n  tr: DetailedHTMLProps<\n    HTMLAttributes<HTMLTableRowElement>,\n    HTMLTableRowElement\n  >\n  track: DetailedHTMLProps<\n    TrackHTMLAttributes<HTMLTrackElement>,\n    HTMLTrackElement\n  >\n  u: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  ul: DetailedHTMLProps<HTMLAttributes<HTMLUListElement>, HTMLUListElement>\n  var: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  video: DetailedHTMLProps<\n    VideoHTMLAttributes<HTMLVideoElement>,\n    HTMLVideoElement\n  >\n  wbr: DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>\n  webview: DetailedHTMLProps<\n    WebViewHTMLAttributes<HTMLWebViewElement>,\n    HTMLWebViewElement\n  >\n\n  // SVG\n  svg: SVGProps<SVGSVGElement>\n\n  animate: SVGProps<SVGElement> // TODO: It is SVGAnimateElement but is not in TypeScript's lib.dom.d.ts for now.\n  animateMotion: SVGProps<SVGElement>\n  animateTransform: SVGProps<SVGElement> // TODO: It is SVGAnimateTransformElement but is not in TypeScript's lib.dom.d.ts for now.\n  circle: SVGProps<SVGCircleElement>\n  clipPath: SVGProps<SVGClipPathElement>\n  defs: SVGProps<SVGDefsElement>\n  desc: SVGProps<SVGDescElement>\n  ellipse: SVGProps<SVGEllipseElement>\n  feBlend: SVGProps<SVGFEBlendElement>\n  feColorMatrix: SVGProps<SVGFEColorMatrixElement>\n  feComponentTransfer: SVGProps<SVGFEComponentTransferElement>\n  feComposite: SVGProps<SVGFECompositeElement>\n  feConvolveMatrix: SVGProps<SVGFEConvolveMatrixElement>\n  feDiffuseLighting: SVGProps<SVGFEDiffuseLightingElement>\n  feDisplacementMap: SVGProps<SVGFEDisplacementMapElement>\n  feDistantLight: SVGProps<SVGFEDistantLightElement>\n  feDropShadow: SVGProps<SVGFEDropShadowElement>\n  feFlood: SVGProps<SVGFEFloodElement>\n  feFuncA: SVGProps<SVGFEFuncAElement>\n  feFuncB: SVGProps<SVGFEFuncBElement>\n  feFuncG: SVGProps<SVGFEFuncGElement>\n  feFuncR: SVGProps<SVGFEFuncRElement>\n  feGaussianBlur: SVGProps<SVGFEGaussianBlurElement>\n  feImage: SVGProps<SVGFEImageElement>\n  feMerge: SVGProps<SVGFEMergeElement>\n  feMergeNode: SVGProps<SVGFEMergeNodeElement>\n  feMorphology: SVGProps<SVGFEMorphologyElement>\n  feOffset: SVGProps<SVGFEOffsetElement>\n  fePointLight: SVGProps<SVGFEPointLightElement>\n  feSpecularLighting: SVGProps<SVGFESpecularLightingElement>\n  feSpotLight: SVGProps<SVGFESpotLightElement>\n  feTile: SVGProps<SVGFETileElement>\n  feTurbulence: SVGProps<SVGFETurbulenceElement>\n  filter: SVGProps<SVGFilterElement>\n  foreignObject: SVGProps<SVGForeignObjectElement>\n  g: SVGProps<SVGGElement>\n  image: SVGProps<SVGImageElement>\n  line: SVGLineElementAttributes<SVGLineElement>\n  linearGradient: SVGProps<SVGLinearGradientElement>\n  marker: SVGProps<SVGMarkerElement>\n  mask: SVGProps<SVGMaskElement>\n  metadata: SVGProps<SVGMetadataElement>\n  mpath: SVGProps<SVGElement>\n  path: SVGProps<SVGPathElement>\n  pattern: SVGProps<SVGPatternElement>\n  polygon: SVGProps<SVGPolygonElement>\n  polyline: SVGProps<SVGPolylineElement>\n  radialGradient: SVGProps<SVGRadialGradientElement>\n  rect: SVGProps<SVGRectElement>\n  set: SVGProps<SVGSetElement>\n  stop: SVGProps<SVGStopElement>\n  switch: SVGProps<SVGSwitchElement>\n  symbol: SVGProps<SVGSymbolElement>\n  text: SVGTextElementAttributes<SVGTextElement>\n  textPath: SVGProps<SVGTextPathElement>\n  tspan: SVGProps<SVGTSpanElement>\n  use: SVGProps<SVGUseElement>\n  view: SVGProps<SVGViewElement>\n}\n"
  },
  {
    "path": "src/jsx/jsx-runtime.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\n/* eslint-disable @typescript-eslint/no-empty-interface */\n/**\n * @file\n * Minimal JSX runtime for Satori adapted from React v19.1.\n *\n * Use the `@jsxImportSource` pragma directive in files containing JSX for Satori.\n *\n * @see {@link https://github.com/facebook/react/blob/v19.1.0/packages/react/src/jsx/ReactJSXElement.js React JSX runtime implementation}\n * @see {@link https://www.typescriptlang.org/docs/handbook/jsx.html TypeScript: JSX reference}\n * @see {@link https://www.typescriptlang.org/tsconfig/#jsxImportSource TSConfig: using \"jsxImportSource\" or `@jsxImportSource` pragma directive}\n * @see {@link https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts React typings `@types/react`}\n * @see {@link https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/jsx-runtime.d.ts React typings for `jsx-runtime`}\n */\nimport type { JSXElement, JSXKey, FC } from './types.js'\nimport type { DefinedIntrinsicElements } from './intrinsic-elements.js'\n\nexport namespace JSX {\n  /**\n   * **WARNING**: Satori does not support class components.\n   * @see {@link https://github.com/vercel/satori?tab=readme-ov-file#jsx Satori JSX documentation}\n   */\n  export type ElementClass = never\n\n  export type ElementType = string | FC<any>\n\n  export type Element = JSXElement<any, any>\n\n  export interface ElementAttributesProperty {\n    props: {}\n  }\n\n  export interface ElementChildrenAttribute {\n    children: {}\n  }\n\n  // TODO: define IntrinsicElements supported by Satori.\n  export interface IntrinsicElements extends DefinedIntrinsicElements {}\n\n  export interface IntrinsicAttributes {\n    /** **INFO**: Allowed as prop, but will be ignored by Satori. */\n    key?: JSXKey | undefined | null\n  }\n}\n\nexport function jsx(\n  type: string | FC<any>,\n  props: Record<string, unknown>,\n  key?: JSXKey | undefined | null\n): JSXElement {\n  if ('key' in props) {\n    // Destructure spread key from props.\n    const { key: keyProp, ...restProps } = props\n    // Key param takes precedence over spread key prop.\n    key = arguments.length === 3 ? key : (keyProp as JSXKey)\n    // Shallow copy of props without key.\n    props = restProps\n  }\n  // Coerce key to string if not nullish.\n  key = key != null ? String(key) : null\n  return { type, props, key }\n}\n\nexport const jsxs = jsx\nexport const jsxDEV = jsx\n\n// HACK: Symbol used internally by React.\nexport const Fragment = Symbol.for('react.fragment')\n"
  },
  {
    "path": "src/jsx/types.ts",
    "content": "/**\n * @file\n * These types are adapted from React v19.1\n *\n * @see {@link https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts `@types/react`}\n */\nexport type { CSSProperties } from './intrinsic-elements.js'\n\nexport type JSXKey = string | number | bigint\n\n/**\n * Represents a JSX element.\n *\n * Where {@link JSXNode} represents everything that can be rendered,\n * `JSXElement` only represents JSX.\n *\n * @template P The type of the props object\n * @template T The type of the component or tag\n *\n * @example\n *\n * ```tsx\n * const element: JSXElement = <div />;\n * ```\n */\nexport interface JSXElement<\n  P = unknown,\n  T extends string | FC<P> = string | FC<P>\n> {\n  type: T\n  props: P\n  key: string | null\n}\n\ntype AwaitedJSXNode =\n  | JSXElement\n  | string\n  | number\n  | bigint\n  | Iterable<JSXNode>\n  | boolean\n  | null\n  | undefined\n\n/**\n * Represents all of the things React can render.\n *\n * Where {@link JSXElement} only represents JSX, `JSXNode` represents\n * everything that can be rendered.\n *\n * @see {@link https://react-typescript-cheatsheet.netlify.app/docs/react-types/reactnode/ React TypeScript Cheatsheet}\n *\n * @example\n *\n * ```tsx\n * // Typing children\n * type Props = { children: JSXNode }\n *\n * const Component = ({ children }: Props) => <div>{children}</div>\n *\n * <Component>hello</Component>\n * ```\n *\n * @example\n *\n * ```tsx\n * // Typing a custom element\n * type Props = { customElement: JSXNode }\n *\n * const Component = ({ customElement }: Props) => <div>{customElement}</div>\n *\n * <Component customElement={<div>hello</div>} />\n * ```\n */\nexport type JSXNode =\n  | JSXElement\n  | string\n  | number\n  | bigint\n  | Iterable<JSXNode>\n  | boolean\n  | null\n  | undefined\n  | Promise<AwaitedJSXNode>\n\n/**\n * Represents the type of a function component. Can optionally receive a type\n * argument that represents the props the component receives.\n *\n * @template P The props the component accepts.\n * @see {@link https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/function_components React TypeScript Cheatsheet}\n *\n * @example\n *\n * ```tsx\n * // With props:\n * type Props = { name: string }\n *\n * const MyComponent: FC<Props> = (props) => {\n *  return <div>{props.name}</div>\n * }\n * ```\n *\n * @example\n *\n * ```tsx\n * // Without props:\n * const MyComponentWithoutProps: FC = () => {\n *   return <div>MyComponentWithoutProps</div>\n * }\n * ```\n */\nexport type FC<P = {}> = (props: P) => JSXNode | Promise<JSXNode>\n"
  },
  {
    "path": "src/language.ts",
    "content": "// This function guesses the human language (writing system) of the given\n// JavaScript string, using the Unicode Alias in extended RegExp.\n//\n// You can learn more about this in:\n// - https://en.wikipedia.org/wiki/Script_(Unicode)\n// - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes\n// - https://unicode.org/reports/tr18/#General_Category_Property\n// - https://tc39.es/ecma262/multipage/text-processing.html#table-unicode-script-values\n\nimport createEmojiRegex from 'emoji-regex-xs'\n\nconst emojiRegex = new RegExp(createEmojiRegex(), 'u')\n\n// Supported languages. The order matters.\n// Usually, this is only for \"special cases\" like CJKV languages as latin\n// characters are usually included in the base font, and can be safely fallback\n// to the Noto Sans font. A list of special cases we want to support can be\n// found here (sort by popularity):\n// - https://fonts.google.com/noto/fonts?sort=popularity&noto.query=sans\n//\n// We can't tell if a hanzi(kanji) is Chinese or Japanese by regular expressions.\n// - https://unicode.org/faq/han_cjk.html\n\nconst specialCode = {\n  emoji: emojiRegex,\n  symbol: /\\p{Symbol}/u,\n  math: /\\p{Math}/u,\n} as const\n\nconst code = {\n  'ja-JP': /\\p{scx=Hira}|\\p{scx=Kana}|\\p{scx=Han}|[\\u3000]|[\\uFF00-\\uFFEF]/u,\n  'ko-KR': /\\p{scx=Hangul}/u,\n  'zh-CN': /\\p{scx=Han}/u,\n  'zh-TW': /\\p{scx=Han}/u,\n  'zh-HK': /\\p{scx=Han}/u,\n  'th-TH': /\\p{scx=Thai}/u,\n  'bn-IN': /\\p{scx=Bengali}/u,\n  'ar-AR': /\\p{scx=Arabic}/u,\n  'ta-IN': /\\p{scx=Tamil}/u,\n  'ml-IN': /\\p{scx=Malayalam}/u,\n  'he-IL': /\\p{scx=Hebrew}/u,\n  'te-IN': /\\p{scx=Telugu}/u,\n  devanagari: /\\p{scx=Devanagari}/u,\n  kannada: /\\p{scx=Kannada}/u,\n} as const\n\ntype SpecialCodeKey = keyof typeof specialCode\ntype CodeKey = keyof typeof specialCode | keyof typeof code\nexport type Locale = keyof typeof code\nexport type LangCode = CodeKey | 'unknown'\n\nexport const locales = Object.keys({ ...code, ...specialCode }) as Locale[]\nexport function isValidLocale(x: any): x is Locale {\n  return locales.includes(x)\n}\n\nexport function detectLanguageCode(\n  segment: string,\n  locale?: Locale\n): Array<Locale> | ['unknown'] | [SpecialCodeKey] {\n  for (const c of Object.keys(specialCode) as SpecialCodeKey[]) {\n    if (specialCode[c].test(segment)) {\n      return [c]\n    }\n  }\n\n  const languages = Object.keys(code).filter((lang) =>\n    code[lang].test(segment)\n  ) as Locale[]\n\n  if (languages.length === 0) {\n    return ['unknown']\n  }\n\n  if (locale) {\n    const index = languages.findIndex((lang) => lang === locale)\n    if (index !== -1) {\n      languages.splice(index, 1)\n      languages.unshift(locale)\n    }\n  }\n\n  return languages\n}\n\nexport function normalizeLocale(locale?: string): Locale | undefined {\n  if (locale) {\n    return locales.find((l) => l.toLowerCase().startsWith(locale.toLowerCase()))\n  }\n}\n"
  },
  {
    "path": "src/layout.ts",
    "content": "/**\n * This module is used to calculate the layout of the current sub-tree.\n */\n\nimport type { ReactNode } from 'react'\nimport {\n  isReactElement,\n  isClass,\n  buildXMLString,\n  normalizeChildren,\n  hasDangerouslySetInnerHTMLProp,\n  isReactComponent,\n  isForwardRefComponent,\n} from './utils.js'\nimport { getYoga, YogaNode } from './yoga.js'\nimport { SVGNodeToImage } from './handler/preprocess.js'\nimport computeStyle from './handler/compute.js'\nimport FontLoader from './font.js'\nimport buildTextNodes from './text/index.js'\nimport rect from './builder/rect.js'\nimport { Locale, normalizeLocale } from './language.js'\nimport { SerializedStyle } from './handler/expand.js'\n\nexport interface LayoutContext {\n  id: string\n  parentStyle: SerializedStyle\n  inheritedStyle: SerializedStyle\n  isInheritingTransform?: boolean\n  parent: YogaNode\n  font: FontLoader\n  embedFont: boolean\n  debug?: boolean\n  graphemeImages?: Record<string, string>\n  canLoadAdditionalAssets: boolean\n  locale?: Locale\n  getTwStyles: (tw: string, style: any) => any\n  onNodeDetected?: (node: SatoriNode) => void\n}\n\nexport interface SatoriNode {\n  // Layout information.\n  left: number\n  top: number\n  width: number\n  height: number\n  type: string\n  key?: string | number\n  props: Record<string, any>\n  textContent?: string\n}\n\nexport default async function* layout(\n  element: ReactNode,\n  context: LayoutContext\n): AsyncGenerator<\n  { word: string; locale?: string }[],\n  string,\n  [number, number]\n> {\n  const Yoga = await getYoga()\n  const {\n    id,\n    inheritedStyle,\n    parent,\n    font,\n    debug,\n    locale,\n    embedFont = true,\n    graphemeImages,\n    canLoadAdditionalAssets,\n    getTwStyles,\n  } = context\n\n  // 1. Pre-process the node.\n  if (element === null || typeof element === 'undefined') {\n    yield\n    yield\n    return ''\n  }\n\n  // Not a regular element.\n  if (!isReactElement(element) || isReactComponent(element.type)) {\n    let iter: ReturnType<typeof layout>\n\n    if (!isReactElement(element)) {\n      // Process as text node.\n      iter = buildTextNodes(String(element), context)\n      yield (await iter.next()).value as { word: string; locale?: Locale }[]\n    } else {\n      if (isClass(element.type as Function)) {\n        throw new Error('Class component is not supported.')\n      }\n\n      let render: Function\n\n      // This is a hack to support React.forwardRef wrapped components.\n      // https://github.com/vercel/satori/issues/600\n      if (isForwardRefComponent(element.type)) {\n        render = (element.type as any).render\n      } else {\n        render = element.type as Function\n      }\n\n      // If it's a custom component, Satori strictly requires it to be pure,\n      // stateless, and not relying on any React APIs such as hooks or suspense.\n      // So we can safely evaluate it to render. Otherwise, an error will be\n      // thrown by React.\n      iter = layout(await render(element.props), context)\n      yield (await iter.next()).value as { word: string; locale?: string }[]\n    }\n\n    await iter.next()\n    const offset = yield\n    return (await iter.next(offset)).value as string\n  }\n\n  // Process as element.\n  const { type: $type, props } = element\n  // type must be a string here.\n  const type = $type as string\n\n  if (props && hasDangerouslySetInnerHTMLProp(props)) {\n    throw new Error(\n      'dangerouslySetInnerHTML property is not supported. See documentation for more information https://github.com/vercel/satori#jsx.'\n    )\n  }\n  let { style, children, tw, lang: _newLocale = locale } = props || {}\n  const newLocale = normalizeLocale(_newLocale)\n\n  // Extend Tailwind styles.\n  if (tw) {\n    const twStyles = getTwStyles(tw, style)\n    style = Object.assign(twStyles, style)\n  }\n\n  const node = Yoga.Node.create()\n  parent.insertChild(node, parent.getChildCount())\n\n  const [computedStyle, newInheritableStyle] = await computeStyle(\n    node,\n    type,\n    inheritedStyle,\n    style,\n    props\n  )\n  // Post-process styles to attach inheritable properties for Satori.\n\n  // If the element is inheriting the parent `transform`, or applying its own.\n  // This affects the coordinate system.\n  const isInheritingTransform =\n    computedStyle.transform === inheritedStyle.transform\n  if (!isInheritingTransform) {\n    ;(computedStyle.transform as any).__parent = inheritedStyle.transform\n  }\n\n  // If the element has `overflow` set to `hidden` or clip-path is set, we need to create a clip\n  // path and use it in all its children.\n  if (\n    computedStyle.overflow === 'hidden' ||\n    (computedStyle.clipPath && computedStyle.clipPath !== 'none')\n  ) {\n    newInheritableStyle._inheritedClipPathId = `satori_cp-${id}`\n    newInheritableStyle._inheritedMaskId = `satori_om-${id}`\n  }\n\n  if (computedStyle.maskImage) {\n    newInheritableStyle._inheritedMaskId = `satori_mi-${id}`\n  }\n\n  // If the element has `background-clip: text` set, we need to create a clip\n  // path and use it in all its children.\n  if (computedStyle.backgroundClip === 'text') {\n    const mutateRefValue = { value: '' } as any\n    newInheritableStyle._inheritedBackgroundClipTextPath = mutateRefValue\n    computedStyle._inheritedBackgroundClipTextPath = mutateRefValue\n\n    if (computedStyle.backgroundImage) {\n      newInheritableStyle._inheritedBackgroundClipTextHasBackground = 'true'\n      computedStyle._inheritedBackgroundClipTextHasBackground = 'true'\n    }\n  }\n\n  // 2. Do layout recursively for its children.\n  const normalizedChildren = normalizeChildren(children)\n  const iterators: ReturnType<typeof layout>[] = []\n\n  let i = 0\n  const segmentsMissingFont: { word: string; locale?: string }[] = []\n  for (const child of normalizedChildren) {\n    const iter = layout(child, {\n      id: id + '-' + i++,\n      parentStyle: computedStyle,\n      inheritedStyle: newInheritableStyle,\n      isInheritingTransform: true,\n      parent: node,\n      font,\n      embedFont,\n      debug,\n      graphemeImages,\n      canLoadAdditionalAssets,\n      locale: newLocale,\n      getTwStyles,\n      onNodeDetected: context.onNodeDetected,\n    })\n    if (canLoadAdditionalAssets) {\n      segmentsMissingFont.push(...(((await iter.next()).value as any) || []))\n    } else {\n      await iter.next()\n    }\n    iterators.push(iter)\n  }\n  yield segmentsMissingFont\n  for (const iter of iterators) await iter.next()\n\n  // 3. Post-process the node.\n  const [x, y] = yield\n  let { left, top, width, height } = node.getComputedLayout()\n  // Attach offset to the current node.\n  left += x\n  top += y\n\n  let childrenRenderResult = ''\n  let baseRenderResult = ''\n  let depsRenderResult = ''\n\n  // Emit event for the current node. We don't pass the children prop to the\n  // event handler because everything is already flattened, unless it's a text\n  // node.\n  const { children: childrenNode, ...restProps } = props\n  context.onNodeDetected?.({\n    left,\n    top,\n    width,\n    height,\n    type,\n    props: restProps,\n    key: element.key,\n    textContent: isReactElement(childrenNode) ? undefined : childrenNode,\n  })\n\n  // Generate the rendered markup for the current node.\n  if (type === 'img') {\n    const src = computedStyle.__src as string\n    baseRenderResult = await rect(\n      {\n        id,\n        left,\n        top,\n        width,\n        height,\n        src,\n        isInheritingTransform,\n        debug,\n      },\n      computedStyle,\n      newInheritableStyle\n    )\n  } else if (type === 'svg') {\n    // When entering a <svg> node, we need to convert it to a <img> with the\n    // SVG data URL embedded.\n    const currentColor = computedStyle.color\n    const src = await SVGNodeToImage(element, currentColor)\n    baseRenderResult = await rect(\n      {\n        id,\n        left,\n        top,\n        width,\n        height,\n        src,\n        isInheritingTransform,\n        debug,\n      },\n      computedStyle,\n      newInheritableStyle\n    )\n  } else {\n    const display = style?.display\n    if (\n      type === 'div' &&\n      children &&\n      typeof children !== 'string' &&\n      display !== 'flex' &&\n      display !== 'none' &&\n      display !== 'contents'\n    ) {\n      throw new Error(\n        `Expected <div> to have explicit \"display: flex\", \"display: contents\", or \"display: none\" if it has more than one child node.`\n      )\n    }\n    baseRenderResult = await rect(\n      { id, left, top, width, height, isInheritingTransform, debug },\n      computedStyle,\n      newInheritableStyle\n    )\n  }\n\n  // Generate the rendered markup for the children.\n  for (const iter of iterators) {\n    childrenRenderResult += (await iter.next([left, top])).value\n  }\n\n  // An extra pass to generate the special background-clip shape collected from\n  // children.\n  if (computedStyle._inheritedBackgroundClipTextPath) {\n    depsRenderResult += buildXMLString(\n      'clipPath',\n      {\n        id: `satori_bct-${id}`,\n        'clip-path': computedStyle._inheritedClipPathId\n          ? `url(#${computedStyle._inheritedClipPathId})`\n          : undefined,\n      },\n      (computedStyle._inheritedBackgroundClipTextPath as any).value\n    )\n  }\n\n  return depsRenderResult + baseRenderResult + childrenRenderResult\n}\n"
  },
  {
    "path": "src/parser/mask.ts",
    "content": "import { getPropertyName } from 'css-to-react-native'\nimport { splitEffects } from '../utils.js'\n\nfunction getMaskProperty(style: Record<string, string | number>, name: string) {\n  const key = getPropertyName(`mask-${name}`)\n  return (style[key] || style[`WebkitM${key.substring(1)}`]) as string\n}\n\nexport interface MaskProperty {\n  image: string\n  position: string\n  size: string\n  repeat: string\n  origin: string\n  clip: string\n}\n\nexport function parseMask(\n  style: Record<string, string | number>\n): MaskProperty[] {\n  const maskImage = (style.maskImage || style.WebkitMaskImage) as string\n\n  const common = {\n    position: getMaskProperty(style, 'position') || '0% 0%',\n    size: getMaskProperty(style, 'size') || '100% 100%',\n    repeat: getMaskProperty(style, 'repeat') || 'repeat',\n    origin: getMaskProperty(style, 'origin') || 'border-box',\n    clip: getMaskProperty(style, 'origin') || 'border-box',\n  }\n\n  let maskImages = splitEffects(maskImage).filter((v) => v && v !== 'none')\n\n  return maskImages.reverse().map((m) => ({\n    image: m,\n    ...common,\n  }))\n}\n"
  },
  {
    "path": "src/parser/shape.ts",
    "content": "import { lengthToNumber } from '../utils.js'\nimport { default as buildBorderRadius } from '../builder/border-radius.js'\nimport { getStylesForProperty } from 'css-to-react-native'\n\nconst regexMap = {\n  circle: /circle\\((.+)\\)/,\n  ellipse: /ellipse\\((.+)\\)/,\n  path: /path\\((.+)\\)/,\n  polygon: /polygon\\((.+)\\)/,\n  inset: /inset\\((.+)\\)/,\n}\n\nexport function createShapeParser(\n  {\n    width,\n    height,\n  }: {\n    width: number\n    height: number\n  },\n  style: Record<string, string | number>,\n  inheritedStyle: Record<string, string | number>\n) {\n  function parseCircle(str: string) {\n    const res = str.match(regexMap['circle'])\n\n    if (!res) return null\n\n    const [, value] = res\n    const [radius, pos = ''] = value.split('at').map((v) => v.trim())\n    const { x, y } = resolvePosition(pos, width, height)\n\n    return {\n      type: 'circle',\n      r: lengthToNumber(\n        radius,\n        inheritedStyle.fontSize as number,\n        Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)) / Math.sqrt(2),\n        inheritedStyle,\n        true\n      ),\n      cx: lengthToNumber(\n        x,\n        inheritedStyle.fontSize as number,\n        width,\n        inheritedStyle,\n        true\n      ),\n      cy: lengthToNumber(\n        y,\n        inheritedStyle.fontSize as number,\n        height,\n        inheritedStyle,\n        true\n      ),\n    }\n  }\n  function parseEllipse(str: string) {\n    const res = str.match(regexMap['ellipse'])\n\n    if (!res) return null\n\n    const [, value] = res\n    const [radius, pos = ''] = value.split('at').map((v) => v.trim())\n    const [rx, ry] = radius.split(' ')\n    const { x, y } = resolvePosition(pos, width, height)\n\n    return {\n      type: 'ellipse',\n      rx: lengthToNumber(\n        rx || '50%',\n        inheritedStyle.fontSize as number,\n        width,\n        inheritedStyle,\n        true\n      ),\n      ry: lengthToNumber(\n        ry || '50%',\n        inheritedStyle.fontSize as number,\n        height,\n        inheritedStyle,\n        true\n      ),\n      cx: lengthToNumber(\n        x,\n        inheritedStyle.fontSize as number,\n        width,\n        inheritedStyle,\n        true\n      ),\n      cy: lengthToNumber(\n        y,\n        inheritedStyle.fontSize as number,\n        height,\n        inheritedStyle,\n        true\n      ),\n    }\n  }\n  function parsePath(str: string) {\n    const res = str.match(regexMap['path'])\n\n    if (!res) return null\n\n    const [fillRule, d] = resolveFillRule(res[1])\n\n    return {\n      type: 'path',\n      d,\n      'fill-rule': fillRule,\n    }\n  }\n  function parsePolygon(str: string) {\n    const res = str.match(regexMap['polygon'])\n\n    if (!res) return null\n\n    const [fillRule, points] = resolveFillRule(res[1])\n\n    return {\n      type: 'polygon',\n      'fill-rule': fillRule,\n      points: points\n        .split(',')\n        .map((v) =>\n          v\n            .split(' ')\n            .map((k, i) =>\n              lengthToNumber(\n                k,\n                inheritedStyle.fontSize as number,\n                i === 0 ? width : height,\n                inheritedStyle,\n                true\n              )\n            )\n            .join(' ')\n        )\n        .join(','),\n    }\n  }\n  function parseInset(str: string) {\n    const res = str.match(regexMap['inset'])\n\n    if (!res) return null\n\n    const [inset, radius] = (\n      res[1].includes('round') ? res[1] : `${res[1].trim()} round 0`\n    ).split('round')\n    const radiusMap = getStylesForProperty('borderRadius', radius, true)\n    const r = Object.values(radiusMap)\n      .map((s) => String(s))\n      .map(\n        (s, i) =>\n          lengthToNumber(\n            s,\n            inheritedStyle.fontSize as number,\n            i === 0 || i === 2 ? height : width,\n            inheritedStyle,\n            true\n          ) || 0\n      )\n    const offsets = Object.values(getStylesForProperty('margin', inset, true))\n      .map((s) => String(s))\n      .map(\n        (s, i) =>\n          lengthToNumber(\n            s,\n            inheritedStyle.fontSize as number,\n            i === 0 || i === 2 ? height : width,\n            inheritedStyle,\n            true\n          ) || 0\n      )\n    const x = offsets[3]\n    const y = offsets[0]\n    const w = width - (offsets[1] + offsets[3])\n    const h = height - (offsets[0] + offsets[2])\n\n    if (r.some((v) => v > 0)) {\n      const d = buildBorderRadius(\n        { left: x, top: y, width: w, height: h },\n        { ...style, ...radiusMap }\n      )\n\n      return { type: 'path', d }\n    }\n\n    return {\n      type: 'rect',\n      x,\n      y,\n      width: w,\n      height: h,\n    }\n  }\n\n  return {\n    parseCircle,\n    parseEllipse,\n    parsePath,\n    parsePolygon,\n    parseInset,\n  }\n}\n\nfunction resolveFillRule(str: string) {\n  const [, fillRule = 'nonzero', d] =\n    str.replace(/('|\")/g, '').match(/^(nonzero|evenodd)?,?(.+)/) || []\n\n  return [fillRule, d]\n}\n\nfunction resolvePosition(position: string, xDelta: number, yDelta: number) {\n  const pos = position.split(' ')\n  const res: { x: number | string; y: number | string } = {\n    x: pos[0] || '50%',\n    y: pos[1] || '50%',\n  }\n\n  pos.forEach((v) => {\n    if (v === 'top') {\n      res.y = 0\n    } else if (v === 'bottom') {\n      res.y = yDelta\n    } else if (v === 'left') {\n      res.x = 0\n    } else if (v === 'right') {\n      res.x = xDelta\n    } else if (v === 'center') {\n      res.x = xDelta / 2\n      res.y = yDelta / 2\n    } else {\n      // do nothing\n    }\n  })\n\n  return res\n}\n"
  },
  {
    "path": "src/satori.ts",
    "content": "import type { ReactNode } from 'react'\nimport type { TwConfig } from 'twrnc'\nimport type { SatoriNode } from './layout.js'\n\nimport layout from './layout.js'\nimport FontLoader, { FontOptions } from './font.js'\nimport svg from './builder/svg.js'\nimport { getYoga, TYoga } from './yoga.js'\nimport { detectLanguageCode, LangCode, Locale } from './language.js'\nimport getTw from './handler/tailwind.js'\nimport { preProcessNode } from './handler/preprocess.js'\nimport { cache, inflightRequests } from './handler/image.js'\nimport { segment } from './utils.js'\n\n// We don't need to initialize the opentype instances every time.\nconst fontCache = new WeakMap()\n\nexport type SatoriOptions = (\n  | {\n      width: number\n      height: number\n    }\n  | {\n      width: number\n    }\n  | {\n      height: number\n    }\n) & {\n  fonts: FontOptions[]\n  embedFont?: boolean\n  debug?: boolean\n  graphemeImages?: Record<string, string>\n  loadAdditionalAsset?: (\n    languageCode: string,\n    segment: string\n  ) => Promise<string | Array<FontOptions>>\n  tailwindConfig?: TwConfig\n  onNodeDetected?: (node: SatoriNode) => void\n  pointScaleFactor?: number\n}\nexport type { SatoriNode }\n\nexport default async function satori(\n  element: ReactNode,\n  options: SatoriOptions\n): Promise<string> {\n  const Yoga = await getYoga()\n  if (!Yoga || !Yoga.Node) {\n    throw new Error(\n      'Satori is not initialized: expect `yoga` to be loaded, got ' + Yoga\n    )\n  }\n  options.fonts = options.fonts || []\n\n  let font: FontLoader\n  if (fontCache.has(options.fonts)) {\n    font = fontCache.get(options.fonts)\n  } else {\n    fontCache.set(options.fonts, (font = new FontLoader(options.fonts)))\n  }\n\n  const definedWidth = 'width' in options ? options.width : undefined\n  const definedHeight = 'height' in options ? options.height : undefined\n\n  const root = getRootNode(Yoga, options.pointScaleFactor)\n  if (definedWidth) root.setWidth(definedWidth)\n  if (definedHeight) root.setHeight(definedHeight)\n  root.setFlexDirection(Yoga.FLEX_DIRECTION_ROW)\n  root.setFlexWrap(Yoga.WRAP_WRAP)\n  root.setAlignContent(Yoga.ALIGN_AUTO)\n  root.setAlignItems(Yoga.ALIGN_FLEX_START)\n  root.setJustifyContent(Yoga.JUSTIFY_FLEX_START)\n  root.setOverflow(Yoga.OVERFLOW_HIDDEN)\n\n  const graphemeImages = { ...options.graphemeImages }\n  // Some Chinese characters have different glyphs in Chinese and\n  // Japanese, but their Unicode is the same. If the user needs to display\n  // the Chinese and Japanese characters simultaneously correctly, the user\n  // needs to download the Chinese and Japanese fonts, respectively.\n  // Assuming that the user has downloaded the corresponding Japanese font,\n  // to let the program realize that the font has not been downloaded in Chinese,\n  // we need to prohibit Japanese as the fallback when executing `engine.has`.\n  //\n  // This causes a problem. Consider a scenario where we need to display Chinese\n  // correctly under tags with `lang=\"ja\"` set. `engine.has` will repeatedly treat\n  // the Chinese as missing font because we have removed the Chinese as a fallback.\n  // To address this situation, we may need to add `processedWordsMissingFont`\n  const processedWordsMissingFonts = new Set()\n\n  cache.clear()\n  inflightRequests.clear()\n  await preProcessNode(element)\n\n  const handler = layout(element, {\n    id: 'id',\n    parentStyle: {},\n    inheritedStyle: {\n      fontSize: 16,\n      fontWeight: 'normal',\n      fontFamily: 'serif',\n      fontStyle: 'normal',\n      lineHeight: 'normal',\n      color: 'black',\n      opacity: 1,\n      whiteSpace: 'normal',\n\n      // Special style properties:\n      _viewportWidth: definedWidth,\n      _viewportHeight: definedHeight,\n    },\n    parent: root,\n    font,\n    embedFont: options.embedFont,\n    debug: options.debug,\n    graphemeImages,\n    canLoadAdditionalAssets: !!options.loadAdditionalAsset,\n    onNodeDetected: options.onNodeDetected,\n    getTwStyles: (tw, style) => {\n      const twToStyles = getTw({\n        width: definedWidth,\n        height: definedHeight,\n        config: options.tailwindConfig,\n      })\n      const twStyles = { ...twToStyles([tw] as any) }\n      if (typeof twStyles.lineHeight === 'number') {\n        twStyles.lineHeight =\n          twStyles.lineHeight / (+twStyles.fontSize || style.fontSize || 16)\n      }\n      if (twStyles.shadowColor && twStyles.boxShadow) {\n        twStyles.boxShadow = (twStyles.boxShadow as string).replace(\n          /rgba?\\([^)]+\\)/,\n          twStyles.shadowColor as string\n        )\n      }\n      return twStyles\n    },\n  })\n\n  const segmentsMissingFont = (await handler.next()).value as {\n    word: string\n    locale?: Locale\n  }[]\n\n  if (options.loadAdditionalAsset) {\n    if (segmentsMissingFont.length) {\n      const languageCodes = convertToLanguageCodes(segmentsMissingFont)\n      const fonts: FontOptions[] = []\n      const images: Record<string, string> = {}\n\n      await Promise.all(\n        Object.entries(languageCodes).flatMap(([code, segments]) =>\n          segments.map((_segment) => {\n            const key = `${code}_${_segment}`\n            if (processedWordsMissingFonts.has(key)) {\n              return null\n            }\n            processedWordsMissingFonts.add(key)\n\n            return options\n              .loadAdditionalAsset(code, _segment)\n              .then((asset: any) => {\n                if (typeof asset === 'string') {\n                  images[_segment] = asset\n                } else if (asset) {\n                  if (Array.isArray(asset)) {\n                    fonts.push(...asset)\n                  } else {\n                    fonts.push(asset)\n                  }\n                }\n              })\n          })\n        )\n      )\n\n      // Directly mutate the font provider and the grapheme map.\n      font.addFonts(fonts)\n      Object.assign(graphemeImages, images)\n    }\n  }\n\n  await handler.next()\n  root.calculateLayout(definedWidth, definedHeight, Yoga.DIRECTION_LTR)\n\n  const content = (await handler.next([0, 0])).value as string\n\n  const computedWidth = root.getComputedWidth()\n  const computedHeight = root.getComputedHeight()\n\n  root.freeRecursive()\n\n  return svg({ width: computedWidth, height: computedHeight, content })\n}\n\nfunction getRootNode(\n  Yoga: TYoga,\n  pointScaleFactor?: SatoriOptions['pointScaleFactor']\n) {\n  if (!pointScaleFactor) {\n    return Yoga.Node.create()\n  } else {\n    const config = Yoga.Config.create()\n    config.setPointScaleFactor(pointScaleFactor)\n    return Yoga.Node.createWithConfig(config)\n  }\n}\n\nfunction convertToLanguageCodes(\n  segmentsMissingFont: { word: string; locale?: Locale }[]\n): Partial<Record<LangCode, string[]>> {\n  const languageCodes = {}\n  let wordsByCode = {}\n\n  for (const { word, locale } of segmentsMissingFont) {\n    const code = detectLanguageCode(word, locale).join('|')\n    wordsByCode[code] = wordsByCode[code] || ''\n    wordsByCode[code] += word\n  }\n\n  Object.keys(wordsByCode).forEach((code: LangCode) => {\n    languageCodes[code] = languageCodes[code] || []\n    if (code === 'emoji') {\n      languageCodes[code].push(\n        ...unique(segment(wordsByCode[code], 'grapheme'))\n      )\n    } else {\n      languageCodes[code][0] = languageCodes[code][0] || ''\n      languageCodes[code][0] += unique(\n        segment(\n          wordsByCode[code],\n          'grapheme',\n          code === 'unknown' ? undefined : code\n        )\n      ).join('')\n    }\n  })\n\n  return languageCodes\n}\n\nfunction unique<T>(arr: T[]): T[] {\n  return Array.from(new Set(arr))\n}\n"
  },
  {
    "path": "src/text/characters.ts",
    "content": "export function stringFromCode(code: string): string {\n  code = code.replace('U+', '0x')\n\n  return String.fromCodePoint(Number(code))\n}\n\nexport const Space = stringFromCode('U+0020')\nexport const Tab = stringFromCode('U+0009')\nexport const HorizontalEllipsis = stringFromCode('U+2026')\n"
  },
  {
    "path": "src/text/index.ts",
    "content": "/**\n * This module calculates the layout of a text string. Currently the only\n * supported inline node is text. All other nodes are using block layout.\n */\nimport type { LayoutContext } from '../layout.js'\nimport {\n  v,\n  segment,\n  wordSeparators,\n  buildXMLString,\n  isUndefined,\n  isString,\n  lengthToNumber,\n} from '../utils.js'\nimport { getYoga, TYoga, YogaNode } from '../yoga.js'\nimport buildText, { container } from '../builder/text.js'\nimport { buildDropShadow } from '../builder/shadow.js'\nimport buildDecoration from '../builder/text-decoration.js'\nimport type { GlyphBox } from '../font.js'\nimport { Locale } from '../language.js'\nimport { HorizontalEllipsis, Space, Tab } from './characters.js'\nimport { genMeasurer } from './measurer.js'\nimport { preprocess } from './processor.js'\nimport cssColorParse from 'parse-css-color'\n\nconst skippedWordWhenFindingMissingFont = new Set([Tab])\n\nfunction shouldSkipWhenFindingMissingFont(word: string): boolean {\n  return skippedWordWhenFindingMissingFont.has(word)\n}\n\nfunction isFullyTransparent(color: string): boolean {\n  if (color === 'transparent') return true\n  const parsed = cssColorParse(color)\n  return parsed ? parsed.alpha === 0 : false\n}\n\nfunction isOpaqueWhite(color: string): boolean {\n  if (!color) return false\n  const parsed = cssColorParse(color)\n  if (!parsed) return false\n  const [r, g, b, a] = parsed.values\n  return r === 255 && g === 255 && b === 255 && (a === undefined || a === 1)\n}\n\nexport default async function* buildTextNodes(\n  content: string,\n  context: LayoutContext\n): AsyncGenerator<{ word: string; locale?: Locale }[], string, [any, any]> {\n  const Yoga = await getYoga()\n\n  const {\n    parentStyle,\n    inheritedStyle,\n    parent,\n    font,\n    id,\n    isInheritingTransform,\n    debug,\n    embedFont,\n    graphemeImages,\n    locale,\n    canLoadAdditionalAssets,\n  } = context\n\n  const {\n    textAlign,\n    textIndent = 0,\n    lineHeight,\n    textWrap,\n    fontSize,\n    filter: cssFilter,\n    tabSize = 8,\n    letterSpacing,\n    _inheritedBackgroundClipTextPath,\n    _inheritedBackgroundClipTextHasBackground,\n    flexShrink,\n  } = parentStyle\n\n  const {\n    words,\n    requiredBreaks,\n    allowSoftWrap,\n    allowBreakWord,\n    processedContent,\n    shouldCollapseTabsAndSpaces,\n    lineLimit,\n    blockEllipsis,\n  } = preprocess(content, parentStyle, locale)\n\n  const textContainer = createTextContainerNode(Yoga, textAlign)\n  parent.insertChild(textContainer, parent.getChildCount())\n\n  if (isUndefined(flexShrink)) {\n    parent.setFlexShrink(1)\n  }\n\n  // Get the correct font according to the container style.\n  // https://www.w3.org/TR/CSS2/visudet.html\n  let engine = font.getEngine(fontSize, lineHeight, parentStyle, locale)\n\n  // Yield segments that are missing a font.\n  const wordsMissingFont = canLoadAdditionalAssets\n    ? segment(processedContent, 'grapheme').filter(\n        (word) => !shouldSkipWhenFindingMissingFont(word) && !engine.has(word)\n      )\n    : []\n\n  yield wordsMissingFont.map((word) => {\n    return {\n      word,\n      locale,\n    }\n  })\n\n  if (wordsMissingFont.length) {\n    // Reload the engine with additional fonts.\n    engine = font.getEngine(fontSize, lineHeight, parentStyle, locale)\n  }\n\n  function isImage(s: string): boolean {\n    return !!(graphemeImages && graphemeImages[s])\n  }\n\n  const { measureGrapheme, measureGraphemeArray, measureText } = genMeasurer(\n    engine,\n    isImage,\n    {\n      fontSize,\n      letterSpacing,\n    }\n  )\n\n  const tabWidth = isString(tabSize)\n    ? lengthToNumber(tabSize, fontSize, 1, parentStyle)\n    : measureGrapheme(Space) * tabSize\n\n  const calc = (\n    text: string,\n    currentWidth: number\n  ): {\n    originWidth: number\n    endingSpacesWidth: number\n    text: string\n  } => {\n    if (text.length === 0) {\n      return {\n        originWidth: 0,\n        endingSpacesWidth: 0,\n        text,\n      }\n    }\n\n    const { index, tabCount } = detectTabs(text)\n\n    let originWidth = 0\n\n    if (tabCount > 0) {\n      const textBeforeTab = text.slice(0, index)\n      const textAfterTab = text.slice(index + tabCount)\n      const textWidthBeforeTab = measureText(textBeforeTab)\n      const offsetBeforeTab = textWidthBeforeTab + currentWidth\n      const tabMoveDistance =\n        tabWidth === 0\n          ? textWidthBeforeTab\n          : (Math.floor(offsetBeforeTab / tabWidth) + tabCount) * tabWidth\n      originWidth = tabMoveDistance + measureText(textAfterTab)\n    } else {\n      originWidth = measureText(text)\n    }\n\n    const afterTrimEndWidth =\n      text.trimEnd() === text ? originWidth : measureText(text.trimEnd())\n\n    return {\n      originWidth,\n      endingSpacesWidth: originWidth - afterTrimEndWidth,\n      text,\n    }\n  }\n\n  // Global variables used to compute the text layout.\n  // @TODO: Use segments instead of words to properly support kerning.\n  let lineWidths = []\n  let baselines = []\n  let lineSegmentNumber = []\n  let texts: string[] = []\n  let wordPositionInLayout: (null | {\n    x: number\n    y: number\n    width: number\n    line: number\n    lineIndex: number\n    isImage: boolean\n  })[] = []\n\n  // With the given container width, compute the text layout.\n  function flow(width: number) {\n    let lines = 0\n    let maxWidth = 0\n    let lineIndex = -1\n    let height = 0\n    let currentWidth = 0\n    let currentLineHeight = 0\n    let currentBaselineOffset = 0\n\n    lineWidths = []\n    lineSegmentNumber = [0]\n    texts = []\n    wordPositionInLayout = []\n\n    // We naively implement the width calculation without proper kerning.\n    // @TODO: Support different writing modes.\n    // @TODO: Support RTL languages.\n    let i = 0\n    let prevLineEndingSpacesWidth = 0\n    while (i < words.length && lines < lineLimit) {\n      let word = words[i]\n      const forceBreak = requiredBreaks[i]\n\n      let w = 0\n\n      const {\n        originWidth,\n        endingSpacesWidth,\n        text: _word,\n      } = calc(word, currentWidth)\n      word = _word\n\n      w = originWidth\n      const lineEndingSpacesWidth = endingSpacesWidth\n\n      // When starting a new line from an empty line, we should push one extra\n      // line height.\n      if (forceBreak && currentLineHeight === 0) {\n        currentLineHeight = engine.height(word)\n      }\n\n      const allowedToJustify = textAlign === 'justify'\n\n      const willWrap =\n        i &&\n        // When determining whether a line break is necessary, the width of the\n        // trailing spaces is not included in the calculation, as the end boundary\n        // can be closely adjacent to the last non-space character.\n        // e.g.\n        // 'aaa bbb ccc'\n        // When the break line happens at the end of the `bbb`, what we see looks like this\n        // |aaa bbb|\n        // |ccc    |\n        currentWidth + w > width + lineEndingSpacesWidth &&\n        allowSoftWrap\n\n      // Need to break the word if:\n      // - we have break-word\n      // - the word is wider than the container width\n      // - the word will be put at the beginning of the line\n      const needToBreakWord =\n        allowBreakWord && w > width && (!currentWidth || willWrap || forceBreak)\n\n      if (needToBreakWord) {\n        // Break the word into multiple segments and continue the loop.\n        const chars = segment(word, 'grapheme')\n        words.splice(i, 1, ...chars)\n        if (currentWidth > 0) {\n          // Start a new line, spaces can be ignored.\n          lineWidths.push(currentWidth - prevLineEndingSpacesWidth)\n          baselines.push(currentBaselineOffset)\n          lines++\n          height += currentLineHeight\n          currentWidth = 0\n          currentLineHeight = 0\n          currentBaselineOffset = 0\n          lineSegmentNumber.push(1)\n          lineIndex = -1\n        }\n        prevLineEndingSpacesWidth = lineEndingSpacesWidth\n        continue\n      }\n      if (forceBreak || willWrap) {\n        // Start a new line, spaces can be ignored.\n        if (shouldCollapseTabsAndSpaces && word === Space) {\n          w = 0\n        }\n\n        lineWidths.push(currentWidth - prevLineEndingSpacesWidth)\n        baselines.push(currentBaselineOffset)\n        lines++\n        height += currentLineHeight\n        currentWidth = w\n        currentLineHeight = w ? Math.round(engine.height(word)) : 0\n        currentBaselineOffset = w ? Math.round(engine.baseline(word)) : 0\n        lineSegmentNumber.push(1)\n        lineIndex = -1\n\n        // If it's naturally broken, we update the max width.\n        // Since if there are multiple lines, the width should fit the\n        // container.\n        if (!forceBreak) {\n          maxWidth = Math.max(maxWidth, width)\n        }\n      } else {\n        // It fits into the current line.\n        currentWidth += w\n        const glyphHeight = Math.round(engine.height(word))\n        if (glyphHeight > currentLineHeight) {\n          // Use the baseline of the highest segment as the baseline of the line.\n          currentLineHeight = glyphHeight\n          currentBaselineOffset = Math.round(engine.baseline(word))\n        }\n        if (allowedToJustify) {\n          lineSegmentNumber[lineSegmentNumber.length - 1]++\n        }\n      }\n\n      if (allowedToJustify) {\n        lineIndex++\n      }\n\n      maxWidth = Math.max(maxWidth, currentWidth)\n\n      let x = currentWidth - w\n\n      if (w === 0) {\n        wordPositionInLayout.push({\n          y: height,\n          x,\n          width: 0,\n          line: lines,\n          lineIndex,\n          isImage: false,\n        })\n      } else {\n        const _texts = segment(word, 'word')\n\n        for (let j = 0; j < _texts.length; j++) {\n          const _text = _texts[j]\n          let _width = 0\n          let _isImage = false\n\n          if (isImage(_text)) {\n            _width = fontSize\n            _isImage = true\n          } else if (!embedFont && _text.length > 1) {\n            // When embedFont is false, use measureText for multi-character strings\n            // to ensure consistency with how currentWidth is accumulated (sum of\n            // grapheme widths). measureGrapheme uses getAdvanceWidth which includes\n            // kerning, causing position mismatches between consecutive <text> elements.\n            _width = measureText(_text)\n          } else {\n            _width = measureGrapheme(_text)\n          }\n\n          texts.push(_text)\n          wordPositionInLayout.push({\n            y: height,\n            x,\n            width: _width,\n            line: lines,\n            lineIndex,\n            isImage: _isImage,\n          })\n\n          x += _width\n        }\n      }\n\n      i++\n      prevLineEndingSpacesWidth = lineEndingSpacesWidth\n    }\n\n    if (currentWidth) {\n      if (lines < lineLimit) {\n        height += currentLineHeight\n      }\n      lines++\n      lineWidths.push(currentWidth)\n      baselines.push(currentBaselineOffset)\n    }\n\n    // @TODO: Support `line-height`.\n    return { width: maxWidth, height }\n  }\n\n  // It's possible that the text's measured size is different from the container's\n  // size, because the container might have a fixed width or height or being\n  // expanded by its parent.\n  let measuredTextSize = { width: 0, height: 0 }\n  textContainer.setMeasureFunc((containerWidth) => {\n    const { width, height } = flow(containerWidth)\n\n    // When doing `text-wrap: balance`, we reflow the text multiple times\n    // using binary search to find the best width.\n    // https://www.w3.org/TR/css-text-4/#valdef-text-wrap-balance\n    if (textWrap === 'balance') {\n      let l = width / 2\n      let r = width\n      let m: number = width\n      while (l + 1 < r) {\n        m = (l + r) / 2\n        const { height: mHeight } = flow(m)\n        if (mHeight > height) {\n          l = m\n        } else {\n          r = m\n        }\n      }\n      flow(r)\n      const _width = Math.ceil(r)\n      measuredTextSize = { width: _width, height }\n      return { width: _width, height }\n    }\n\n    // When doing `text-wrap: pretty`, we try to avoid ending a paragraph with a single word\n    // by reshaping all lines in a way that achieves more balanced line lengths\n    // This \"pretty\" line breaking algorithm tries to achieve optimal line breaks\n    // that avoid orphans (single words at the end of a paragraph) and create\n    // visually pleasing line lengths.\n    if (textWrap === 'pretty') {\n      // Check if the last line has a single word or is very short\n      // (typically less than 1/3 of the container width)\n      const lastLineWidth = lineWidths[lineWidths.length - 1]\n      const isLastLineShort = lastLineWidth < width / 3\n\n      if (isLastLineShort) {\n        // Reflow the paragraph with slightly adjusted line breaks\n        // to avoid orphans and create more even line lengths\n        // This is a simplified approach - a real implementation would use a\n        // more sophisticated algorithm to find optimal line breaks\n\n        // We'll just reflow once with slightly reduced width to force\n        // redistribution of words. This is much simplified from the actual\n        // paragraph-level line breaking algorithm which would compute scores\n        // for different line break combinations.\n        const adjustedWidth = width * 0.9\n        const result = flow(adjustedWidth)\n\n        // Use the result if it reduces orphans without adding too many lines\n        if (result.height <= height * 1.3) {\n          measuredTextSize = { width, height: result.height }\n          return { width, height: result.height }\n        }\n      }\n    }\n\n    const _width = Math.ceil(width)\n    measuredTextSize = { width: _width, height }\n    // This may be a temporary fix, I didn't dig deep into yoga.\n    // But when the return value of width here doesn't change (assuming the value of width is 216.9),\n    // when we later get the width through `parent.getComputedWidth()`, sometimes it returns 216 and sometimes 217.\n    // I'm not sure if this is a yoga bug, but it seems related to the entire page width.\n    // So I use Math.ceil.\n    return { width: _width, height }\n  })\n\n  const [x, y] = yield\n\n  let result = ''\n  let backgroundClipDef = ''\n\n  const clipPathId = inheritedStyle._inheritedClipPathId as string | undefined\n  const overflowMaskId = inheritedStyle._inheritedMaskId as number | undefined\n\n  const {\n    left: containerLeft,\n    top: containerTop,\n    width: containerWidth,\n    height: containerHeight,\n  } = textContainer.getComputedLayout()\n\n  // Convert textIndent to number if it's a string (e.g., percentage)\n  const textIndentNumber =\n    typeof textIndent === 'string'\n      ? lengthToNumber(\n          textIndent,\n          fontSize,\n          containerWidth,\n          parentStyle,\n          true\n        ) || 0\n      : textIndent\n\n  const parentContainerInnerWidth =\n    parent.getComputedWidth() -\n    parent.getComputedPadding(Yoga.EDGE_LEFT) -\n    parent.getComputedPadding(Yoga.EDGE_RIGHT) -\n    parent.getComputedBorder(Yoga.EDGE_LEFT) -\n    parent.getComputedBorder(Yoga.EDGE_RIGHT)\n\n  // Attach offset to the current node.\n  const left = x + containerLeft\n  const top = y + containerTop\n\n  const { matrix, opacity } = container(\n    {\n      left: containerLeft,\n      top: containerTop,\n      width: containerWidth,\n      height: containerHeight,\n      isInheritingTransform,\n    },\n    parentStyle\n  )\n\n  let filter = ''\n  if (parentStyle.textShadowOffset) {\n    const { textShadowColor, textShadowOffset, textShadowRadius } = parentStyle\n\n    filter = buildDropShadow(\n      {\n        width: measuredTextSize.width,\n        height: measuredTextSize.height,\n        id,\n      },\n      {\n        shadowColor: textShadowColor,\n        shadowOffset: textShadowOffset,\n        shadowRadius: textShadowRadius,\n      },\n      isFullyTransparent(parentStyle.color) ||\n        (_inheritedBackgroundClipTextHasBackground &&\n          isOpaqueWhite(parentStyle.color))\n    )\n\n    filter = buildXMLString('defs', {}, filter)\n  }\n\n  let decorationShape = ''\n  let mergedPath = ''\n  let extra = ''\n  let skippedLine = -1\n  type DecorationLine = {\n    left: number\n    top: number\n    ascender: number\n    width: number\n  }\n  let decorationLines: Record<number, DecorationLine | null> = {}\n  let decorationGlyphs: Record<number, GlyphBox[]> = {}\n  let wordBuffer: string | null = null\n  let bufferedOffset = 0\n\n  for (let i = 0; i < texts.length; i++) {\n    // Skip whitespace and empty characters.\n    const layout = wordPositionInLayout[i]\n    const nextLayout = wordPositionInLayout[i + 1]\n\n    if (!layout) continue\n\n    let text = texts[i]\n    let path: string | null = null\n    let isLastDisplayedBeforeEllipsis = false\n\n    const image = graphemeImages ? graphemeImages[text] : null\n\n    let topOffset = layout.y\n    let leftOffset = layout.x\n    const width = layout.width\n    const line = layout.line\n    const shouldCollectDecorationBoxes =\n      parentStyle.textDecorationLine === 'underline' &&\n      (parentStyle.textDecorationSkipInk || 'auto') !== 'none'\n\n    if (line === skippedLine) {\n      continue\n    }\n\n    // When `text-align` is `justify`, the width of the line will be adjusted.\n    let extendedWidth = false\n\n    // Apply text-indent to the first line (for both single and multi-line text)\n    if (line === 0 && textIndentNumber !== 0) {\n      leftOffset += textIndentNumber\n    }\n\n    if (lineWidths.length > 1) {\n      // Calculate alignment. Note that for Flexbox, there is only text\n      // alignment when the container is multi-line.\n      const remainingWidth = containerWidth - lineWidths[line]\n      if (textAlign === 'right' || textAlign === 'end') {\n        leftOffset += remainingWidth\n      } else if (textAlign === 'center') {\n        leftOffset += remainingWidth / 2\n      } else if (textAlign === 'justify') {\n        // Don't justify the last line.\n        if (line < lineWidths.length - 1) {\n          const segments = lineSegmentNumber[line]\n          const gutter = segments > 1 ? remainingWidth / (segments - 1) : 0\n          leftOffset += gutter * layout.lineIndex\n          extendedWidth = true\n        }\n      }\n\n      // Only round for embedded fonts (paths benefit from pixel alignment).\n      // For non-embedded fonts (<text> elements), keep fractional positions\n      // to maintain consistent spacing between consecutive elements.\n      if (embedFont) {\n        leftOffset = Math.round(leftOffset)\n      }\n    }\n\n    const baselineOfLine = baselines[line]\n    const baselineOfWord = engine.baseline(text)\n    const heightOfWord = engine.height(text)\n    const baselineDelta = baselineOfLine - baselineOfWord\n\n    const buildUnderlineBand = (offset: number) => {\n      if (\n        !shouldCollectDecorationBoxes ||\n        parentStyle.textDecorationLine !== 'underline'\n      ) {\n        return undefined\n      }\n      const baseline = top + offset + baselineDelta + baselineOfWord\n      return {\n        underlineY: baseline + baselineOfWord * 0.1,\n        strokeWidth: Math.max(1, fontSize * 0.1),\n      }\n    }\n\n    if (!decorationLines[line]) {\n      decorationLines[line] = {\n        left: leftOffset,\n        top: top + topOffset + baselineDelta,\n        ascender: baselineOfWord,\n        width: extendedWidth ? containerWidth : lineWidths[line],\n      }\n    }\n\n    if (lineLimit !== Infinity) {\n      let _blockEllipsis = blockEllipsis\n      let ellipsisWidth = measureGrapheme(blockEllipsis)\n      if (ellipsisWidth > parentContainerInnerWidth) {\n        _blockEllipsis = HorizontalEllipsis\n        ellipsisWidth = measureGrapheme(_blockEllipsis)\n      }\n      const spaceWidth = measureGrapheme(Space)\n      const isNotLastLine = line < lineWidths.length - 1\n      const isLastAllowedLine = line + 1 === lineLimit\n\n      function calcEllipsis(baseWidth: number, _text: string) {\n        const chars = segment(_text, 'grapheme', locale)\n\n        let subset = ''\n        let resolvedWidth = 0\n\n        for (const char of chars) {\n          const w = baseWidth + measureGraphemeArray([subset + char])\n          if (\n            // Keep at least one character:\n            // > The first character or atomic inline-level element on a line\n            // must be clipped rather than ellipsed.\n            // https://drafts.csswg.org/css-overflow/#text-overflow\n            subset &&\n            w + ellipsisWidth > parentContainerInnerWidth\n          ) {\n            break\n          }\n          subset += char\n          resolvedWidth = w\n        }\n\n        return {\n          subset,\n          resolvedWidth,\n        }\n      }\n\n      if (\n        isLastAllowedLine &&\n        (isNotLastLine || lineWidths[line] > parentContainerInnerWidth)\n      ) {\n        if (\n          leftOffset + width + ellipsisWidth + spaceWidth >\n          parentContainerInnerWidth\n        ) {\n          const { subset, resolvedWidth } = calcEllipsis(leftOffset, text)\n\n          text = subset + _blockEllipsis\n          skippedLine = line\n          decorationLines[line].width = Math.max(\n            0,\n            resolvedWidth - decorationLines[line].left\n          )\n          isLastDisplayedBeforeEllipsis = true\n        } else if (nextLayout && nextLayout.line !== line) {\n          if (textAlign === 'center') {\n            const { subset, resolvedWidth } = calcEllipsis(leftOffset, text)\n\n            text = subset + _blockEllipsis\n            skippedLine = line\n            decorationLines[line].width = Math.max(\n              0,\n              resolvedWidth - decorationLines[line].left\n            )\n            isLastDisplayedBeforeEllipsis = true\n          } else {\n            const nextLineText = texts[i + 1]\n\n            const { subset, resolvedWidth } = calcEllipsis(\n              width + leftOffset,\n              nextLineText\n            )\n\n            text = text + subset + _blockEllipsis\n            skippedLine = line\n            decorationLines[line].width = Math.max(\n              0,\n              resolvedWidth - decorationLines[line].left\n            )\n            isLastDisplayedBeforeEllipsis = true\n          }\n        }\n      }\n    }\n\n    if (image) {\n      // For images, we remove the baseline offset.\n      topOffset += 0\n    } else if (embedFont) {\n      // If the current word and the next word are on the same line, we try to\n      // merge them together to better handle the kerning.\n      if (\n        !text.includes(Tab) &&\n        !wordSeparators.includes(text) &&\n        texts[i + 1] &&\n        nextLayout &&\n        !nextLayout.isImage &&\n        topOffset === nextLayout.y &&\n        !isLastDisplayedBeforeEllipsis\n      ) {\n        if (wordBuffer === null) {\n          bufferedOffset = leftOffset\n        }\n        wordBuffer = wordBuffer === null ? text : wordBuffer + text\n        continue\n      }\n\n      const finalizedSegment = wordBuffer === null ? text : wordBuffer + text\n      const finalizedLeftOffset =\n        wordBuffer === null ? leftOffset : bufferedOffset\n      const finalizedWidth = layout.width + leftOffset - finalizedLeftOffset\n\n      const band = buildUnderlineBand(topOffset)\n\n      const svg = engine.getSVG(\n        finalizedSegment.replace(/(\\t)+/g, ''),\n        {\n          fontSize,\n          left: left + finalizedLeftOffset,\n          // Since we need to pass the baseline position, add the ascender to the top.\n          top: top + topOffset + baselineOfWord + baselineDelta,\n          letterSpacing,\n        },\n        band\n      )\n\n      path = svg.path\n\n      if (shouldCollectDecorationBoxes && svg.boxes && svg.boxes.length) {\n        ;(decorationGlyphs[line] || (decorationGlyphs[line] = [])).push(\n          ...svg.boxes\n        )\n      }\n\n      wordBuffer = null\n\n      if (debug) {\n        extra +=\n          // Glyph\n          buildXMLString('rect', {\n            x: left + finalizedLeftOffset,\n            y: top + topOffset + baselineDelta,\n            width: finalizedWidth,\n            height: heightOfWord,\n            fill: 'transparent',\n            stroke: '#575eff',\n            'stroke-width': 1,\n            transform: matrix ? matrix : undefined,\n            'clip-path': clipPathId ? `url(#${clipPathId})` : undefined,\n          }) +\n          // Baseline\n          buildXMLString('line', {\n            x1: left + leftOffset,\n            x2: left + leftOffset + layout.width,\n            y1: top + topOffset + baselineDelta + baselineOfWord,\n            y2: top + topOffset + baselineDelta + baselineOfWord,\n            stroke: '#14c000',\n            'stroke-width': 1,\n            transform: matrix ? matrix : undefined,\n            'clip-path': clipPathId ? `url(#${clipPathId})` : undefined,\n          })\n      }\n    } else {\n      // We need manually add the font ascender height to ensure it starts\n      // at the baseline because <text>'s alignment baseline is set to `hanging`\n      // by default and supported to change in SVG 1.1.\n      topOffset += baselineOfWord + baselineDelta\n\n      if (shouldCollectDecorationBoxes && !image) {\n        const band = buildUnderlineBand(topOffset)\n\n        const svg = engine.getSVG(\n          text.replace(/(\\t)+/g, ''),\n          {\n            fontSize,\n            left: left + leftOffset,\n            top: top + topOffset,\n            letterSpacing,\n          },\n          band\n        )\n\n        if (svg.boxes && svg.boxes.length) {\n          ;(decorationGlyphs[line] || (decorationGlyphs[line] = [])).push(\n            ...svg.boxes\n          )\n        }\n      }\n    }\n\n    if (path !== null) {\n      mergedPath += path + ' '\n    } else {\n      const [t, shape] = buildText(\n        {\n          content: text,\n          filter,\n          id,\n          left: left + leftOffset,\n          top: top + topOffset,\n          width,\n          height: heightOfWord,\n          matrix,\n          opacity,\n          image,\n          clipPathId,\n          debug,\n          shape: !!_inheritedBackgroundClipTextPath,\n        },\n        parentStyle\n      )\n      result += t\n      backgroundClipDef += shape\n    }\n\n    if (isLastDisplayedBeforeEllipsis) {\n      break\n    }\n  }\n\n  if (parentStyle.textDecorationLine) {\n    decorationShape = Object.entries(decorationLines)\n      .map(([lineIndex, deco]) => {\n        if (!deco) return ''\n        const glyphBoxes = decorationGlyphs[lineIndex] || []\n\n        return buildDecoration(\n          {\n            left: left + deco.left,\n            top: deco.top,\n            width: deco.width,\n            ascender: deco.ascender,\n            clipPathId,\n            matrix,\n            glyphBoxes,\n          },\n          parentStyle\n        )\n      })\n      .join('')\n  }\n\n  // Embed the font as path.\n  if (mergedPath) {\n    const p =\n      (!isFullyTransparent(parentStyle.color) || filter) && opacity !== 0\n        ? `<g ${overflowMaskId ? `mask=\"url(#${overflowMaskId})\"` : ''} ${\n            clipPathId ? `clip-path=\"url(#${clipPathId})\"` : ''\n          }>` +\n          buildXMLString('path', {\n            fill:\n              filter &&\n              (isFullyTransparent(parentStyle.color) ||\n                (_inheritedBackgroundClipTextHasBackground &&\n                  isOpaqueWhite(parentStyle.color)))\n                ? 'black'\n                : parentStyle.color,\n            d: mergedPath,\n            transform: matrix ? matrix : undefined,\n            opacity: opacity !== 1 ? opacity : undefined,\n            style: cssFilter ? `filter:${cssFilter}` : undefined,\n            'stroke-width': inheritedStyle.WebkitTextStrokeWidth\n              ? `${inheritedStyle.WebkitTextStrokeWidth}px`\n              : undefined,\n            stroke: inheritedStyle.WebkitTextStrokeWidth\n              ? inheritedStyle.WebkitTextStrokeColor\n              : undefined,\n            'stroke-linejoin': inheritedStyle.WebkitTextStrokeWidth\n              ? 'round'\n              : undefined,\n            'paint-order': inheritedStyle.WebkitTextStrokeWidth\n              ? 'stroke'\n              : undefined,\n          }) +\n          '</g>'\n        : ''\n\n    if (_inheritedBackgroundClipTextPath) {\n      backgroundClipDef = buildXMLString('path', {\n        d: mergedPath,\n        transform: matrix ? matrix : undefined,\n      })\n    }\n\n    result +=\n      (filter\n        ? filter +\n          buildXMLString(\n            'g',\n            { filter: `url(#satori_s-${id})` },\n            p + decorationShape\n          )\n        : p + decorationShape) + extra\n  } else if (decorationShape) {\n    result += filter\n      ? buildXMLString('g', { filter: `url(#satori_s-${id})` }, decorationShape)\n      : decorationShape\n  }\n\n  // Attach information to the parent node.\n  if (backgroundClipDef) {\n    ;(parentStyle._inheritedBackgroundClipTextPath as any).value +=\n      backgroundClipDef\n  }\n\n  return result\n}\n\nfunction createTextContainerNode(Yoga: TYoga, textAlign: string): YogaNode {\n  // Create a container node for this text fragment.\n  const textContainer = Yoga.Node.create()\n  textContainer.setAlignItems(Yoga.ALIGN_BASELINE)\n  textContainer.setJustifyContent(\n    v(\n      textAlign,\n      {\n        left: Yoga.JUSTIFY_FLEX_START,\n        right: Yoga.JUSTIFY_FLEX_END,\n        center: Yoga.JUSTIFY_CENTER,\n        justify: Yoga.JUSTIFY_SPACE_BETWEEN,\n        // We don't have other writing modes yet.\n        start: Yoga.JUSTIFY_FLEX_START,\n        end: Yoga.JUSTIFY_FLEX_END,\n      },\n      Yoga.JUSTIFY_FLEX_START,\n      'textAlign'\n    )\n  )\n\n  return textContainer\n}\n\nfunction detectTabs(text: string):\n  | {\n      index: null\n      tabCount: 0\n    }\n  | {\n      index: number\n      tabCount: number\n    } {\n  const result = /(\\t)+/.exec(text)\n  return result\n    ? {\n        index: result.index,\n        tabCount: result[0].length,\n      }\n    : {\n        index: null,\n        tabCount: 0,\n      }\n}\n"
  },
  {
    "path": "src/text/measurer.ts",
    "content": "import { FontEngine } from '../font.js'\nimport { segment } from '../utils.js'\n\nexport function genMeasurer(\n  engine: FontEngine,\n  isImage: (grapheme: string) => boolean,\n  style: {\n    fontSize: number\n    letterSpacing: number\n  }\n): {\n  measureGrapheme: (grapheme: string) => number\n  measureGraphemeArray: (graphemes: string[]) => number\n  measureText: (text: string) => number\n} {\n  const { fontSize, letterSpacing } = style\n\n  const cache = new Map<string, number>()\n\n  function measureGrapheme(grapheme: string): number {\n    let width = cache.get(grapheme)\n\n    if (width === undefined) {\n      width = engine.measure(grapheme, { fontSize, letterSpacing })\n      cache.set(grapheme, width)\n    }\n\n    return width\n  }\n\n  function measureGraphemeArray(graphemes: string[]): number {\n    let width = 0\n\n    for (const grapheme of graphemes) {\n      if (isImage(grapheme)) {\n        width += fontSize\n      } else {\n        width += measureGrapheme(grapheme)\n      }\n    }\n\n    return width\n  }\n\n  function measureText(text: string): number {\n    return measureGraphemeArray(segment(text, 'grapheme'))\n  }\n\n  return {\n    measureGrapheme,\n    measureGraphemeArray,\n    measureText,\n  }\n}\n"
  },
  {
    "path": "src/text/processor.ts",
    "content": "import { Locale } from '../language.js'\nimport { isNumber, segment, splitByBreakOpportunities } from '../utils.js'\nimport { HorizontalEllipsis, Space } from './characters.js'\nimport { SerializedStyle } from '../handler/expand.js'\n\nexport function preprocess(\n  content: string,\n  style: SerializedStyle,\n  locale?: Locale\n): {\n  words: string[]\n  requiredBreaks: boolean[]\n  allowSoftWrap: boolean\n  allowBreakWord: boolean\n  processedContent: string\n  shouldCollapseTabsAndSpaces: boolean\n  lineLimit: number\n  blockEllipsis: string\n} {\n  const { textTransform, whiteSpace, wordBreak } = style\n\n  content = processTextTransform(content, textTransform, locale)\n\n  const {\n    content: processedContent,\n    shouldCollapseTabsAndSpaces,\n    allowSoftWrap,\n  } = processWhiteSpace(content, whiteSpace)\n\n  const { words, requiredBreaks, allowBreakWord } = processWordBreak(\n    processedContent,\n    wordBreak\n  )\n\n  const [lineLimit, blockEllipsis] = processTextOverflow(style, allowSoftWrap)\n\n  return {\n    words,\n    requiredBreaks,\n    allowSoftWrap,\n    allowBreakWord,\n    processedContent,\n    shouldCollapseTabsAndSpaces,\n    lineLimit,\n    blockEllipsis,\n  }\n}\n\nfunction processTextTransform(\n  content: string,\n  textTransform: string,\n  locale?: Locale\n): string {\n  if (textTransform === 'uppercase') {\n    content = content.toLocaleUpperCase(locale)\n  } else if (textTransform === 'lowercase') {\n    content = content.toLocaleLowerCase(locale)\n  } else if (textTransform === 'capitalize') {\n    content = segment(content, 'word', locale)\n      // For each word...\n      .map((word) => {\n        // ...split into graphemes...\n        return segment(word, 'grapheme', locale)\n          .map((grapheme, index) => {\n            // ...and make the first grapheme uppercase\n            return index === 0 ? grapheme.toLocaleUpperCase(locale) : grapheme\n          })\n          .join('')\n      })\n      .join('')\n  }\n\n  return content\n}\n\nfunction processTextOverflow(\n  style: SerializedStyle,\n  allowSoftWrap: boolean\n): [number, string?] {\n  const {\n    textOverflow,\n    lineClamp,\n    WebkitLineClamp,\n    WebkitBoxOrient,\n    overflow,\n    display,\n  } = style\n\n  if (display === 'block' && lineClamp) {\n    const [lineLimit, blockEllipsis = HorizontalEllipsis] =\n      parseLineClamp(lineClamp)\n    if (lineLimit) {\n      return [lineLimit, blockEllipsis]\n    }\n  }\n\n  if (\n    textOverflow === 'ellipsis' &&\n    display === '-webkit-box' &&\n    WebkitBoxOrient === 'vertical' &&\n    isNumber(WebkitLineClamp) &&\n    WebkitLineClamp > 0\n  ) {\n    return [WebkitLineClamp, HorizontalEllipsis]\n  }\n\n  if (textOverflow === 'ellipsis' && overflow === 'hidden' && !allowSoftWrap) {\n    return [1, HorizontalEllipsis]\n  }\n\n  return [Infinity]\n}\n\nfunction processWordBreak(\n  content,\n  wordBreak: string\n): { words: string[]; requiredBreaks: boolean[]; allowBreakWord: boolean } {\n  const allowBreakWord = ['break-all', 'break-word'].includes(wordBreak)\n\n  const { words, requiredBreaks } = splitByBreakOpportunities(\n    content,\n    wordBreak\n  )\n\n  return { words, requiredBreaks, allowBreakWord }\n}\n\nfunction processWhiteSpace(\n  content: string,\n  whiteSpace: string\n): {\n  content: string\n  shouldCollapseTabsAndSpaces: boolean\n  allowSoftWrap: boolean\n} {\n  const shouldKeepLinebreak = ['pre', 'pre-wrap', 'pre-line'].includes(\n    whiteSpace\n  )\n\n  const shouldCollapseTabsAndSpaces = ['normal', 'nowrap', 'pre-line'].includes(\n    whiteSpace\n  )\n\n  const allowSoftWrap = !['pre', 'nowrap'].includes(whiteSpace)\n\n  if (!shouldKeepLinebreak) {\n    content = content.replace(/\\n/g, Space)\n  }\n\n  if (shouldCollapseTabsAndSpaces) {\n    content = content.replace(/([ ]|\\t)+/g, Space).replace(/^[ ]|[ ]$/g, '')\n  }\n\n  return { content, shouldCollapseTabsAndSpaces, allowSoftWrap }\n}\n\nfunction parseLineClamp(input: number | string): [number?, string?] {\n  if (typeof input === 'number') return [input]\n\n  const regex1 = /^(\\d+)\\s*\"(.*)\"$/\n  const regex2 = /^(\\d+)\\s*'(.*)'$/\n  const match1 = regex1.exec(input)\n  const match2 = regex2.exec(input)\n\n  if (match1) {\n    const number = +match1[1]\n    const text = match1[2]\n\n    return [number, text]\n  } else if (match2) {\n    const number = +match2[1]\n    const text = match2[2]\n\n    return [number, text]\n  }\n\n  return []\n}\n"
  },
  {
    "path": "src/transform-origin.ts",
    "content": "import valueParser from 'postcss-value-parser'\n\nimport CssDimension from './vendor/parse-css-dimension/index.js'\n\n/**\n * If key for each direction is missing, assume default (50%)\n */\nexport interface ParsedTransformOrigin {\n  /** Relative horizontal transform origin in % */\n  xRelative?: number\n  /** Relative vertical transform origin in % */\n  yRelative?: number\n  /** Absolute horizontal transform origin in pixels */\n  xAbsolute?: number\n  /** Absolute horizontal transform origin in pixels */\n  yAbsolute?: number\n}\n\ninterface ParsedUnit {\n  /** Relative unit in % */\n  relative?: number\n  /** Absolute unit in pixels */\n  absolute?: number\n}\n\nfunction parseUnit(word: string, baseFontSize: number): ParsedUnit {\n  try {\n    const parsed = new CssDimension(word)\n    switch (parsed.unit) {\n      case 'px':\n        return { absolute: parsed.value }\n      case 'em':\n        return { absolute: parsed.value * baseFontSize }\n      case 'rem':\n        return { absolute: parsed.value * 16 }\n      case '%':\n        return { relative: parsed.value }\n      default:\n        return {}\n    }\n  } catch (e) {\n    return {}\n  }\n}\n\nfunction handleWord(\n  word: string,\n  baseFontSize: number,\n  unitIsHorizontal: boolean\n) {\n  switch (word) {\n    case 'top':\n      return { yRelative: 0 }\n    case 'left':\n      return { xRelative: 0 }\n    case 'right':\n      return { xRelative: 100 }\n    case 'bottom':\n      return { yRelative: 100 }\n    case 'center':\n      return {}\n    default: {\n      const parsedUnit = parseUnit(word, baseFontSize)\n      return parsedUnit.absolute\n        ? {\n            [unitIsHorizontal ? 'xAbsolute' : 'yAbsolute']: parsedUnit.absolute,\n          }\n        : parsedUnit.relative\n        ? {\n            [unitIsHorizontal ? 'xRelative' : 'yRelative']: parsedUnit.relative,\n          }\n        : {}\n    }\n  }\n}\n\nexport default function parseTransformOrigin(\n  value: string | number,\n  baseFontSize: number\n): ParsedTransformOrigin {\n  // If it's a single value and a number, then it's horizontal\n  if (typeof value === 'number') {\n    return { xAbsolute: value }\n  }\n  let words: string[]\n  try {\n    words = valueParser(value)\n      .nodes.filter((node) => node.type === 'word')\n      .map((node) => node.value)\n  } catch (e) {\n    return {}\n  }\n\n  if (words.length === 1) {\n    // If it's a single value and a number, then it's horizontal, so\n    // pass `true` to `unitIsHorizontal`\n    return handleWord(words[0], baseFontSize, true)\n  } else if (words.length === 2) {\n    // Make words to be [horizontal, vertical]\n    if (\n      words[0] === 'top' ||\n      words[0] === 'bottom' ||\n      words[1] === 'left' ||\n      words[1] === 'right'\n    ) {\n      words.reverse()\n    }\n\n    return {\n      ...handleWord(words[0], baseFontSize, true),\n      ...handleWord(words[1], baseFontSize, false),\n    }\n  } else {\n    return {}\n  }\n}\n"
  },
  {
    "path": "src/types.d.ts",
    "content": "declare module '@shuding/opentype.js' {\n  export = opentype\n}\n"
  },
  {
    "path": "src/utils.ts",
    "content": "import type { ReactNode, ReactElement } from 'react'\nimport LineBreaker from 'linebreak'\n\nimport CssDimension from './vendor/parse-css-dimension/index.js'\n\nexport function isReactElement(node: ReactNode): node is ReactElement {\n  const type = typeof node\n  if (\n    type === 'number' ||\n    type === 'bigint' ||\n    type === 'string' ||\n    type === 'boolean'\n  ) {\n    return false\n  }\n  return true\n}\n\nexport function isClass(f: Function) {\n  return /^class\\s/.test(f.toString())\n}\n\nexport function isForwardRefComponent(type: any) {\n  return type && type.$$typeof === Symbol.for('react.forward_ref')\n}\n\nexport function isReactComponent(type: any) {\n  return typeof type === 'function' || isForwardRefComponent(type)\n}\n\nexport function hasDangerouslySetInnerHTMLProp(props: any) {\n  return 'dangerouslySetInnerHTML' in props\n}\n\nexport function normalizeChildren(children: any) {\n  const flattend =\n    typeof children === 'undefined' ? [] : [].concat(children).flat(Infinity)\n\n  const res = []\n  for (let i = 0; i < flattend.length; i++) {\n    let value = flattend[i]\n    if (\n      typeof value === 'undefined' ||\n      typeof value === 'boolean' ||\n      value === null\n    ) {\n      continue\n    }\n    if (typeof value === 'number') {\n      value = String(value)\n    }\n    if (\n      typeof value === 'string' &&\n      res.length &&\n      typeof res[res.length - 1] === 'string'\n    ) {\n      res[res.length - 1] += value\n    } else {\n      res.push(value)\n    }\n  }\n  return res\n}\n\nexport function lengthToNumber(\n  length: string | number,\n  baseFontSize: number,\n  baseLength: number,\n  inheritedStyle: Record<string, string | number>,\n  percentage = false\n): number | undefined {\n  if (typeof length === 'number') return length\n\n  // Convert em and rem values to number (px), convert rad to deg.\n  try {\n    length = length.trim()\n\n    // Not length: `1px/2px`, `1px 2px`, `1px, 2px`, `calc(1px)`.\n    if (/[ /\\(,]/.test(length)) return\n\n    // Just a number as string: '100'\n    if (length === String(+length)) return +length\n\n    const parsed = new CssDimension(length)\n    if (parsed.type === 'length') {\n      switch (parsed.unit) {\n        case 'em':\n          return parsed.value * baseFontSize\n        case 'rem':\n          return parsed.value * 16\n        case 'vw':\n          return ~~(\n            (parsed.value * (inheritedStyle._viewportWidth as number)) /\n            100\n          )\n        case 'vh':\n          return ~~(\n            (parsed.value * (inheritedStyle._viewportHeight as number)) /\n            100\n          )\n        default:\n          return parsed.value\n      }\n    } else if (parsed.type === 'angle') {\n      return calcDegree(length)\n    } else if (parsed.type === 'percentage') {\n      if (percentage) {\n        return (parsed.value / 100) * baseLength\n      }\n    }\n  } catch {\n    // Not a length unit, silently ignore.\n  }\n}\n\nexport function calcDegree(deg: string) {\n  const parsed = new CssDimension(deg)\n\n  switch (parsed.unit) {\n    case 'deg':\n      return parsed.value\n    case 'rad':\n      return (parsed.value * 180) / Math.PI\n    case 'turn':\n      return parsed.value * 360\n    case 'grad':\n      return 0.9 * parsed.value\n  }\n}\n\n// Multiplies two 2d transform matrices.\nexport function multiply(m1: number[], m2: number[]) {\n  return [\n    m1[0] * m2[0] + m1[2] * m2[1],\n    m1[1] * m2[0] + m1[3] * m2[1],\n    m1[0] * m2[2] + m1[2] * m2[3],\n    m1[1] * m2[2] + m1[3] * m2[3],\n    m1[0] * m2[4] + m1[2] * m2[5] + m1[4],\n    m1[1] * m2[4] + m1[3] * m2[5] + m1[5],\n  ]\n}\n\nexport function v(\n  field: string | number | undefined,\n  map: Record<string, any>,\n  fallback: any,\n  errorIfNotAllowedForProperty?: string\n) {\n  let value = map[field]\n  if (typeof value === 'undefined') {\n    if (errorIfNotAllowedForProperty && typeof field !== 'undefined') {\n      throw new Error(\n        `Invalid value for CSS property \"${errorIfNotAllowedForProperty}\". Allowed values: ${Object.keys(\n          map\n        )\n          .map((_v) => `\"${_v}\"`)\n          .join(' | ')}. Received: \"${field}\".`\n      )\n    }\n    value = fallback\n  }\n  return value\n}\n\nlet wordSegmenter\nlet graphemeSegmenter\n\n// Implementation modified from\n// https://github.com/niklasvh/html2canvas/blob/6521a487d78172f7179f7c973c1a3af40eb92009/src/css/layout/text.ts\n// https://drafts.csswg.org/css-text/#word-separator\nexport const wordSeparators = [\n  0x0020, 0x00a0, 0x1361, 0x10100, 0x10101, 0x1039, 0x1091, 0xa,\n].map((point) => String.fromCodePoint(point))\n\nconst segmentCache = new Map<string, string[]>()\nconst MAX_SEGMENT_CACHE_SIZE = 500\n\nexport function segment(\n  content: string,\n  granularity: 'word' | 'grapheme',\n  locale?: string\n): string[] {\n  const cacheKey = `${granularity}:${locale || ''}:${content}`\n\n  if (segmentCache.has(cacheKey)) {\n    return segmentCache.get(cacheKey)!\n  }\n  if (!wordSegmenter || !graphemeSegmenter) {\n    if (!(typeof Intl !== 'undefined' && 'Segmenter' in Intl)) {\n      // https://caniuse.com/mdn-javascript_builtins_intl_segments\n      throw new Error(\n        'Intl.Segmenter does not exist, please use import a polyfill.'\n      )\n    }\n\n    wordSegmenter = new Intl.Segmenter(locale, { granularity: 'word' })\n    graphemeSegmenter = new Intl.Segmenter(locale, {\n      granularity: 'grapheme',\n    })\n  }\n\n  let result: string[]\n\n  if (granularity === 'grapheme') {\n    result = [...graphemeSegmenter.segment(content)].map((seg) => seg.segment)\n  } else {\n    const segmented = [...wordSegmenter.segment(content)].map(\n      (seg) => seg.segment\n    ) as string[]\n\n    const output = []\n\n    let i = 0\n    // When there is a non-breaking space, join the previous and next words together.\n    // This change causes them to be treated as a single segment.\n    while (i < segmented.length) {\n      const s = segmented[i]\n\n      if (s == '\\u00a0') {\n        const previousWord = i === 0 ? '' : output.pop()\n        const nextWord = i === segmented.length - 1 ? '' : segmented[i + 1]\n\n        output.push(previousWord + '\\u00a0' + nextWord)\n        i += 2\n      } else {\n        output.push(s)\n        i++\n      }\n    }\n\n    result = output\n  }\n\n  if (segmentCache.size >= MAX_SEGMENT_CACHE_SIZE) {\n    const firstKey = segmentCache.keys().next().value\n    segmentCache.delete(firstKey)\n  }\n\n  segmentCache.set(cacheKey, result)\n  return result\n}\n\nexport function buildXMLString(\n  type: string,\n  attrs: Record<string, string | number>,\n  children?: string\n) {\n  let attrString = ''\n\n  for (const [k, _v] of Object.entries(attrs)) {\n    if (typeof _v !== 'undefined') {\n      attrString += ` ${k}=\"${_v}\"`\n    }\n  }\n\n  if (children) {\n    return `<${type}${attrString}>${children}</${type}>`\n  }\n  return `<${type}${attrString}/>`\n}\n\nexport function createLRU<T>(max = 20) {\n  const store: Map<string, T> = new Map()\n  function get(key: string): T | undefined {\n    const value = store.get(key)\n    if (value === undefined) return undefined\n\n    // Move to end (most recently used)\n    store.delete(key)\n    store.set(key, value)\n    return value\n  }\n  function set(key: string, value: T) {\n    if (store.has(key)) {\n      store.delete(key)\n    } else if (store.size >= max) {\n      const firstKey = store.keys().next().value\n      store.delete(firstKey)\n    }\n\n    store.set(key, value)\n  }\n  function clear() {\n    store.clear()\n  }\n\n  return {\n    set,\n    get,\n    clear,\n  }\n}\n\nexport function parseViewBox(viewBox?: string | null | undefined) {\n  return viewBox ? viewBox.split(/[, ]/).filter(Boolean).map(Number) : null\n}\n\nexport function toString(x: unknown): string {\n  return Object.prototype.toString.call(x)\n}\n\nexport function isString(x: unknown): x is string {\n  return typeof x === 'string'\n}\n\nexport function isNumber(x: unknown): x is number {\n  return typeof x === 'number'\n}\n\nexport function isUndefined(x: unknown): x is undefined {\n  return typeof x === 'undefined'\n}\n\nexport function asPointPercentageLength(\n  x: string | number,\n  propertyName?: string\n): number | `${number}%` | undefined {\n  if (typeof x === 'number') {\n    return x\n  }\n  if (x.endsWith('%')) {\n    const percentageValue = parseFloat(x.slice(0, -1))\n    if (isNaN(percentageValue)) {\n      console.warn(\n        `Invalid value \"${x}\"${\n          typeof propertyName === 'string' ? ` for \"${propertyName}\"` : ''\n        }. Expected a percentage value (e.g., \"50%\").`\n      )\n      return undefined\n    }\n    return `${percentageValue}%`\n  }\n\n  console.warn(\n    `Invalid value \"${x}\"${\n      typeof propertyName === 'string' ? ` for \"${propertyName}\"` : ''\n    }. Expected a number or a percentage value (e.g., \"50%\").`\n  )\n  return undefined\n}\n\nexport function asPointAutoPercentageLength(\n  x: string | number,\n  propertyName?: string\n): number | 'auto' | `${number}%` | undefined {\n  if (typeof x === 'number') {\n    return x\n  }\n  if (x === 'auto') {\n    return 'auto'\n  }\n  if (x.endsWith('%')) {\n    const percentageValue = parseFloat(x.slice(0, -1))\n    if (isNaN(percentageValue)) {\n      console.warn(\n        `Invalid value \"${x}\"${\n          typeof propertyName === 'string' ? ` for \"${propertyName}\"` : ''\n        }. Expected a percentage value (e.g., \"50%\").`\n      )\n      return undefined\n    }\n    return `${percentageValue}%`\n  }\n\n  console.warn(\n    `Invalid value \"${x}\"${\n      typeof propertyName === 'string' ? ` for \"${propertyName}\"` : ''\n    }. Expected a number, \"auto\", or a percentage value (e.g., \"50%\").`\n  )\n  return undefined\n}\n\nexport function splitByBreakOpportunities(\n  content: string,\n  wordBreak: string\n): {\n  words: string[]\n  requiredBreaks: boolean[]\n} {\n  if (wordBreak === 'break-all') {\n    return { words: segment(content, 'grapheme'), requiredBreaks: [] }\n  }\n\n  if (wordBreak === 'keep-all') {\n    return { words: segment(content, 'word'), requiredBreaks: [] }\n  }\n\n  const breaker = new LineBreaker(content)\n  let last = 0\n  let bk = breaker.nextBreak()\n  const words = []\n  const requiredBreaks = [false]\n\n  while (bk) {\n    const word = content.slice(last, bk.position)\n    words.push(word)\n\n    if (bk.required) {\n      requiredBreaks.push(true)\n    } else {\n      requiredBreaks.push(false)\n    }\n\n    last = bk.position\n    bk = breaker.nextBreak()\n  }\n\n  return { words, requiredBreaks }\n}\n\nexport const midline = (s: string) => {\n  return s.replaceAll(\n    /([A-Z])/g,\n    (_, letter: string) => `-${letter.toLowerCase()}`\n  )\n}\n\nexport function splitEffects(\n  input: string,\n  separator: string | RegExp = ','\n): string[] {\n  const result = []\n  let l = 0\n  let parenCount = 0\n  separator = new RegExp(separator)\n\n  for (let i = 0; i < input.length; i++) {\n    if (input[i] === '(') {\n      parenCount++\n    } else if (input[i] === ')') {\n      parenCount--\n    }\n\n    if (parenCount === 0 && separator.test(input[i])) {\n      result.push(input.slice(l, i).trim())\n      l = i + 1\n    }\n  }\n\n  result.push(input.slice(l).trim())\n\n  return result\n}\n"
  },
  {
    "path": "src/vendor/parse-css-dimension/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 Jed Mao\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "src/vendor/parse-css-dimension/index.js",
    "content": "var e=(t,r)=>()=>(r||t((r={exports:{}}).exports,r),r.exports);var u=e((k,g)=>{g.exports=[\"em\",\"ex\",\"ch\",\"rem\",\"vh\",\"vw\",\"vmin\",\"vmax\",\"px\",\"mm\",\"cm\",\"in\",\"pt\",\"pc\",\"mozmm\"]});var a=e((z,v)=>{v.exports=[\"deg\",\"grad\",\"rad\",\"turn\"]});var c=e((L,w)=>{w.exports=[\"dpi\",\"dpcm\",\"dppx\"]});var h=e(($,y)=>{y.exports=[\"Hz\",\"kHz\"]});var m=e((j,b)=>{b.exports=[\"s\",\"ms\"]});var q=u(),f=a(),p=c(),l=h(),d=m();function s(t){if(/\\.\\D?$/.test(t))throw new Error(\"The dot should be followed by a number\");if(/^[+-]{2}/.test(t))throw new Error(\"Only one leading +/- is allowed\");if(x(t)>1)throw new Error(\"Only one dot is allowed\");if(/%$/.test(t)){this.type=\"percentage\",this.value=o(t),this.unit=\"%\";return}var r=O(t);if(!r){this.type=\"number\",this.value=o(t);return}this.type=F(r),this.value=o(t.substr(0,t.length-r.length)),this.unit=r}s.prototype.valueOf=function(){return this.value};s.prototype.toString=function(){return this.value+(this.unit||\"\")};function U(t){return new s(t)}function x(t){var r=t.match(/\\./g);return r?r.length:0}function o(t){var r=parseFloat(t);if(isNaN(r))throw new Error(\"Invalid number: \"+t);return r}var E=[].concat(f,l,q,p,d);function O(t){var r=t.match(/\\D+$/),n=r&&r[0];if(n&&E.indexOf(n)===-1)throw new Error(\"Invalid unit: \"+n);return n}var D=Object.assign(i(f,\"angle\"),i(l,\"frequency\"),i(p,\"resolution\"),i(d,\"time\"));function i(t,r){return Object.fromEntries(t.map(n=>[n,r]))}function F(t){return D[t]||\"length\"}export{U as default};\n"
  },
  {
    "path": "src/vendor/parse-css-dimension/package.json",
    "content": "{\n  \"name\": \"parse-css-dimension\",\n  \"version\": \"0.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"esbuild src.js --bundle --outfile=index.js --format=esm --minify\"\n  },\n  \"dependencies\": {\n    \"css-angle-units\": \"^1.0.1\",\n    \"css-frequency-units\": \"^1.0.1\",\n    \"css-length-units\": \"^1.0.0\",\n    \"css-resolution-units\": \"^1.0.1\",\n    \"css-time-units\": \"^1.0.1\"\n  },\n  \"devDependencies\": {\n    \"esbuild\": \"^0.14.28\"\n  }\n}\n"
  },
  {
    "path": "src/vendor/parse-css-dimension/src.js",
    "content": "var cssLengthUnits = require('css-length-units')\nvar cssAngleUnits = require('css-angle-units')\nvar cssResolutionUnits = require('css-resolution-units')\nvar cssFrequencyUnits = require('css-frequency-units')\nvar cssTimeUnits = require('css-time-units')\n\nfunction CssDimension(value) {\n  if (/\\.\\D?$/.test(value)) {\n    throw new Error('The dot should be followed by a number')\n  }\n\n  if (/^[+-]{2}/.test(value)) {\n    throw new Error('Only one leading +/- is allowed')\n  }\n\n  if (countDots(value) > 1) {\n    throw new Error('Only one dot is allowed')\n  }\n\n  if (/%$/.test(value)) {\n    this.type = 'percentage'\n    this.value = tryParseFloat(value)\n    this.unit = '%'\n    return\n  }\n\n  var unit = parseUnit(value)\n  if (!unit) {\n    this.type = 'number'\n    this.value = tryParseFloat(value)\n    return\n  }\n\n  this.type = getTypeFromUnit(unit)\n  this.value = tryParseFloat(value.substr(0, value.length - unit.length))\n  this.unit = unit\n}\n\nCssDimension.prototype.valueOf = function () {\n  return this.value\n}\n\nCssDimension.prototype.toString = function () {\n  return this.value + (this.unit || '')\n}\n\nexport default function factory(value) {\n  return new CssDimension(value)\n}\n\nfunction countDots(value) {\n  var m = value.match(/\\./g)\n  return m ? m.length : 0\n}\n\nfunction tryParseFloat(value) {\n  var result = parseFloat(value)\n  if (isNaN(result)) {\n    throw new Error('Invalid number: ' + value)\n  }\n  return result\n}\n\nvar units = [].concat(\n  cssAngleUnits,\n  cssFrequencyUnits,\n  cssLengthUnits,\n  cssResolutionUnits,\n  cssTimeUnits\n)\n\nfunction parseUnit(value) {\n  var m = value.match(/\\D+$/)\n  var unit = m && m[0]\n  if (unit && units.indexOf(unit) === -1) {\n    throw new Error('Invalid unit: ' + unit)\n  }\n  return unit\n}\n\nvar unitTypeLookup = Object.assign(\n  createLookups(cssAngleUnits, 'angle'),\n  createLookups(cssFrequencyUnits, 'frequency'),\n  createLookups(cssResolutionUnits, 'resolution'),\n  createLookups(cssTimeUnits, 'time')\n)\n\nfunction createLookups(list, value) {\n  return Object.fromEntries(list.map((unit) => [unit, value]))\n}\n\nfunction getTypeFromUnit(unit) {\n  return unitTypeLookup[unit] || 'length'\n}\n"
  },
  {
    "path": "src/vendor/twrnc/deprecate.js",
    "content": "module.exports = function deprecate(fn, message) {\n  return function (...args) {\n    console.warn(message)\n    return fn(...args)\n  }\n}\n"
  },
  {
    "path": "src/vendor/twrnc/log.js",
    "content": "export default {\n  info(key, messages) {\n    console.info(...(Array.isArray(key) ? [key] : [messages, key]))\n  },\n  warn(key, messages) {\n    console.warn(...(Array.isArray(key) ? [key] : [messages, key]))\n  },\n  risk(key, messages) {\n    console.error(...(Array.isArray(key) ? [key] : [messages, key]))\n  },\n}\n"
  },
  {
    "path": "src/vendor/twrnc/picocolors.js",
    "content": "export default {\n  yellow: (s) => s,\n}\n"
  },
  {
    "path": "src/yoga.bundled.ts",
    "content": "import { loadYoga } from 'yoga-layout/load'\n\n// Always preload Yoga.\nconst loadingYoga = loadYoga()\nexport function getYoga() {\n  return loadingYoga\n}\n"
  },
  {
    "path": "src/yoga.external.ts",
    "content": "import { loadYoga as loadYogaUntyped, type Yoga } from 'yoga-layout/load'\n\nconst loadYoga = loadYogaUntyped as (options: {\n  wasmBinary?: ArrayBuffer | ArrayBufferLike\n  instantiateWasm?: (\n    imports: WebAssembly.Imports,\n    successCallback: (instance: WebAssembly.Instance) => void\n  ) => WebAssembly.Exports | false | undefined\n}) => Promise<Yoga>\n\nlet resolveYoga: (yoga: Yoga) => void\nlet rejectYoga: (error: unknown) => void\nconst yogaPromise: Promise<Yoga> = new Promise((resolve, reject) => {\n  resolveYoga = resolve\n  rejectYoga = reject\n})\n\nexport type InitInput =\n  | string\n  | Request\n  | URL\n  | Response\n  | BufferSource\n  | Buffer\n  | WebAssembly.Module\n  | Promise<Response | BufferSource | Buffer | WebAssembly.Module>\n\nasync function loadWasm(\n  input: InitInput,\n  imports: WebAssembly.Imports\n): Promise<WebAssembly.WebAssemblyInstantiatedSource> {\n  let source: Response | BufferSource | Buffer | WebAssembly.Module\n\n  if (\n    typeof input === 'string' ||\n    (typeof Request === 'function' && input instanceof Request) ||\n    (typeof URL === 'function' && input instanceof URL)\n  ) {\n    source = await fetch(input)\n  } else {\n    source = await input\n  }\n\n  if (typeof Response === 'function' && source instanceof Response) {\n    if (typeof WebAssembly.instantiateStreaming === 'function') {\n      try {\n        return await WebAssembly.instantiateStreaming(source, imports)\n      } catch (e) {\n        if (source.headers.get('Content-Type') !== 'application/wasm') {\n          console.warn(\n            '`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\\n',\n            e\n          )\n        }\n      }\n    }\n\n    const bytes = await source.arrayBuffer()\n    return await WebAssembly.instantiate(bytes, imports)\n  }\n\n  const instantiated = (await WebAssembly.instantiate(\n    'buffer' in source\n      ? source.buffer.slice(\n          source.byteOffset,\n          source.byteOffset + source.byteLength\n        )\n      : source,\n    imports\n  )) as WebAssembly.Instance | WebAssembly.WebAssemblyInstantiatedSource\n\n  if (instantiated instanceof WebAssembly.Instance) {\n    return { instance: instantiated, module: source as WebAssembly.Module }\n  }\n\n  return instantiated\n}\n\nexport function init(input: InitInput) {\n  loadYoga({\n    instantiateWasm(imports, successCallback) {\n      loadWasm(input, imports)\n        .then(({ instance }) => {\n          successCallback(instance)\n        })\n        .catch(rejectYoga)\n\n      return {}\n    },\n  })\n    .then(resolveYoga)\n    .catch(rejectYoga)\n}\n\nexport function getYoga() {\n  return yogaPromise\n}\n"
  },
  {
    "path": "src/yoga.ts",
    "content": "import { type Yoga } from 'yoga-layout/load'\nimport { type Node } from 'yoga-layout'\nimport { type InitInput } from './yoga.external.js'\n\nexport { Yoga as TYoga, Node as YogaNode, type InitInput }\n\nexport function init(input: InitInput) {\n  if (process.env.SATORI_STANDALONE === '1') {\n    return import('./yoga.external.js').then((mod) => mod.init(input))\n  } else {\n    // Do nothing. It's bundled.\n  }\n}\n\nexport function getYoga() {\n  if (process.env.SATORI_STANDALONE === '1') {\n    return import('./yoga.external.js').then((mod) => mod.getYoga())\n  } else {\n    return import('./yoga.bundled.js').then((mod) => mod.getYoga())\n  }\n}\n\nif (process.env.SATORI_STANDALONE !== '1') {\n  // Preload Yoga in bundled mode.\n  import('./yoga.bundled.js')\n}\n"
  },
  {
    "path": "test/background-clip.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('backgroundClip', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should render background-clip:text', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          fontSize: 30,\n          flexDirection: 'column',\n          background: '#ffffff',\n        }}\n      >\n        <div\n          style={{\n            backgroundImage: 'linear-gradient(to right, red, green)',\n            WebkitBackgroundClip: 'text',\n            backgroundClip: 'text',\n            color: 'transparent',\n          }}\n        >\n          lynn\n        </div>\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n\n    expect(toImage(svg)).toMatchImageSnapshot()\n  })\n\n  it('should render background-clip:text compatible with transform', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          fontSize: 30,\n          flexDirection: 'column',\n          background: '#ffffff',\n        }}\n      >\n        <div\n          style={{\n            transform: 'translateX(25px)',\n            backgroundImage: 'linear-gradient(to right, red, green)',\n            WebkitBackgroundClip: 'text',\n            backgroundClip: 'text',\n            color: 'transparent',\n          }}\n        >\n          lynn\n        </div>\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n\n    expect(toImage(svg)).toMatchImageSnapshot()\n  })\n\n  it('should render background-clip:text compatible with mask', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          fontSize: 30,\n          flexDirection: 'column',\n          background: '#ffffff',\n        }}\n      >\n        <div\n          style={{\n            transform: 'translateX(25px)',\n            backgroundImage: 'linear-gradient(to right, red, green)',\n            WebkitBackgroundClip: 'text',\n            backgroundClip: 'text',\n            maskImage: 'linear-gradient(to right, blue, transparent)',\n            color: 'transparent',\n          }}\n        >\n          lynn\n        </div>\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n\n    expect(toImage(svg)).toMatchImageSnapshot()\n  })\n\n  it('should preserve color', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          background: 'radial-gradient(#eb10ff, #d700ff)',\n          backgroundClip: 'text',\n          color: 'green',\n          textShadow:\n            '0px 0px 5px #ffffff9c,-1px -1px 1px #ffffff9c,1px 0px 5px #c0f,0px -2px 1px #ea2eff00,1px 0px 5px #d325ff,0px -2px 1px #fff,0px 1px 1px #a600f88f,-1px 3px 1px #a600f854',\n        }}\n      >\n        Hello\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n\n    expect(toImage(svg)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/basic.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Basic', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should render empty div', async () => {\n    const svg = await satori(<div></div>, { width: 100, height: 100, fonts })\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render basic div with text', async () => {\n    const svg = await satori(<div>Hello</div>, {\n      width: 100,\n      height: 100,\n      fonts,\n    })\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render basic div with background color', async () => {\n    const svg = await satori(\n      <div\n        style={{ backgroundColor: 'red', width: '100%', height: '100%' }}\n      ></div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render basic div with text and background color', async () => {\n    const svg = await satori(\n      <div style={{ backgroundColor: 'red', width: '100%', height: '100%' }}>\n        Hello\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support skipping embedded fonts', async () => {\n    const svg = await satori(<div>Hello</div>, {\n      width: 100,\n      height: 100,\n      fonts,\n      embedFont: false,\n    })\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support hex colors', async () => {\n    const svg = await satori(\n      <div\n        style={{ backgroundColor: '#ff0', width: '100%', height: '100%' }}\n      ></div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support array in JSX children', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          backgroundColor: '#ff0',\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n        }}\n      >\n        <div>1</div>\n        {[\n          <div style={{ display: 'flex' }}>2{[<div>3</div>]}</div>,\n          <div style={{ display: 'flex' }}>{[4]}</div>,\n        ]}\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support custom components', async () => {\n    function MyComponent() {\n      return <h1 style={{ fontSize: 16 }}>Hello from My Component</h1>\n    }\n\n    const svg = await satori(\n      <div\n        style={{\n          backgroundColor: '#ff0',\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n        }}\n      >\n        <MyComponent />\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n\n    async function MyAsyncComponent() {\n      await new Promise((resolve) => setTimeout(resolve, 0))\n      return <h1 style={{ fontSize: 16 }}>Hello from My Async Component</h1>\n    }\n    const svg2 = await satori(\n      <div\n        style={{\n          backgroundColor: '#ff0',\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n        }}\n      >\n        {/* @ts-ignore */}\n        <MyAsyncComponent />\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg2, 100)).toMatchImageSnapshot()\n  })\n\n  it('should combine textNodes correctly', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          height: '100%',\n          width: '100%',\n          background: 'white',\n        }}\n      >\n        Hi {0} <div>hi</div> {0} {false} {undefined} {0} {null} {0} {true} {'x'}{' '}\n        {0}\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should respect points scale factor', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          backgroundColor: 'white',\n          height: '300px',\n          width: '100%',\n        }}\n      >\n        <div\n          style={{\n            top: '0.666px',\n            position: 'absolute',\n            display: 'flex',\n            flexDirection: 'column',\n          }}\n        >\n          <div\n            style={{\n              width: '100px',\n              height: '100px',\n              backgroundColor: 'red',\n              borderWidth: '0.5px',\n              borderColor: 'blue',\n            }}\n          ></div>\n          <div\n            style={{\n              width: '100px',\n              height: '100px',\n              backgroundColor: 'red',\n              borderWidth: '0.5px',\n              borderColor: 'blue',\n            }}\n          ></div>\n        </div>\n      </div>,\n      {\n        width: 100,\n        height: 300,\n        fonts,\n        pointScaleFactor: 2,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/benchmark/index.ts",
    "content": "import { run, bench, summary } from 'mitata'\nimport { join } from 'path'\nimport { readFileSync } from 'fs'\n\nimport { Resvg } from '@resvg/resvg-js'\nimport Sharp from 'sharp'\nimport satori from '../../dist/index.js'\n\nconst fonts = [\n  {\n    name: 'Geist',\n    data: readFileSync(\n      join(process.cwd(), 'test', 'benchmark', 'Geist-Regular.otf')\n    ),\n    weight: 400 as const,\n    style: 'normal' as const,\n  },\n  {\n    name: 'Geist',\n    data: readFileSync(\n      join(process.cwd(), 'test', 'benchmark', 'Geist-Medium.otf')\n    ),\n    weight: 500 as const,\n    style: 'normal' as const,\n  },\n  {\n    name: 'Geist',\n    data: readFileSync(\n      join(process.cwd(), 'test', 'benchmark', 'Geist-SemiBold.otf')\n    ),\n    weight: 600 as const,\n    style: 'normal' as const,\n  },\n  {\n    name: 'Geist',\n    data: readFileSync(\n      join(process.cwd(), 'test', 'benchmark', 'Geist-Bold.otf')\n    ),\n    weight: 700 as const,\n    style: 'normal' as const,\n  },\n  {\n    name: 'Geist',\n    data: readFileSync(\n      join(process.cwd(), 'test', 'benchmark', 'Geist-Black.otf')\n    ),\n    weight: 800 as const,\n    style: 'normal' as const,\n  },\n]\n\n// Simulated from a real world example:\n// https://gist.github.com/BurnedChris/616a72a6b41927b699de3564d4c51a12\n\nasync function generateSVG() {\n  return await satori(\n    {\n      type: 'div',\n      props: {\n        style: {\n          fontFamily: 'Geist',\n          width: '1200px',\n          height: '630px',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          fontSize: '100px',\n          backgroundColor: '#fff',\n        },\n        children: [\n          {\n            type: 'div',\n            props: {\n              style: {\n                display: 'flex',\n                flexDirection: 'column',\n                alignItems: 'flex-start',\n                flex: '1',\n                gap: 20,\n                padding: 20,\n              },\n              children: [\n                {\n                  type: 'span',\n                  props: {\n                    style: {\n                      color: '#000',\n                      fontSize: '40px',\n                      fontWeight: 'bold',\n                    },\n                    children: 'Next.js Quickstart',\n                  },\n                },\n                {\n                  type: 'span',\n                  props: {\n                    style: {\n                      color: '#000',\n                      fontSize: '26px',\n                      lineHeight: '1.4',\n                    },\n                    children:\n                      'Learn how to integrate LIB_NAME into your Next.js application with this step-by-step guide. Well cover installation, configuration, and basic usage.',\n                  },\n                },\n                {\n                  type: 'span',\n                  props: {\n                    style: {\n                      color: '#444',\n                      fontSize: '20px',\n                      padding: '20px',\n                      marginTop: '20px',\n                      border: '2px solid #888',\n                      borderRadius: '10px',\n                    },\n                    children: 'npx LIB_NAME',\n                  },\n                },\n              ],\n            },\n          },\n          {\n            type: 'div',\n            props: {\n              style: {\n                display: 'flex',\n                flex: '1',\n                height: '100%',\n                alignItems: 'center',\n                justifyContent: 'center',\n                backgroundImage:\n                  'linear-gradient(to bottom, lightgray 1px, transparent 0), linear-gradient(to right, lightgray 1px, transparent 0)',\n                backgroundSize: '100px 100px',\n              },\n              children: [\n                {\n                  type: 'div',\n                  props: {\n                    style: {\n                      display: 'flex',\n                      flexDirection: 'column',\n                      flex: '1',\n                      border: '2px solid #ccc',\n                      borderRadius: '10px',\n                      padding: '20px',\n                      backgroundColor: '#fff',\n                      transform: 'translateX(50px)',\n                    },\n                    children: [\n                      {\n                        type: 'span',\n                        props: {\n                          style: {\n                            color: '#333',\n                            fontSize: '24px',\n                            fontWeight: '600',\n                            marginBottom: '10px',\n                          },\n                          children: 'We value your privacy',\n                        },\n                      },\n                      {\n                        type: 'span',\n                        props: {\n                          style: {\n                            color: '#333',\n                            fontSize: '16px',\n                          },\n                          children:\n                            'This site uses cookies to improve your browsing experience, analyze site traffic, and show personalized content.',\n                        },\n                      },\n                      {\n                        type: 'div',\n                        props: {\n                          style: {\n                            display: 'flex',\n                            justifyContent: 'flex-start',\n                            gap: '10px',\n                            marginTop: '20px',\n                            paddingTop: '20px',\n                            borderTop: '1px solid #ccc',\n                            width: '100%',\n                          },\n                          children: [\n                            {\n                              type: 'button',\n                              props: {\n                                style: {\n                                  padding: '10px 20px',\n                                  fontSize: '16px',\n                                  color: '#777',\n                                  border: '1px solid #aaa',\n                                  borderRadius: '5px',\n                                },\n                                children: 'Reject All',\n                              },\n                            },\n                            {\n                              type: 'button',\n                              props: {\n                                style: {\n                                  padding: '10px 20px',\n                                  fontSize: '16px',\n                                  color: '#777',\n                                  border: '1px solid #aaa',\n                                  borderRadius: '5px',\n                                },\n                                children: 'Accept All',\n                              },\n                            },\n                            {\n                              type: 'div',\n                              props: {\n                                style: {\n                                  flex: '1',\n                                },\n                              },\n                            },\n                            {\n                              type: 'button',\n                              props: {\n                                style: {\n                                  padding: '10px 20px',\n                                  fontSize: '16px',\n                                  color: '#66d1bd',\n                                  border: '1px solid #66d1bd',\n                                  borderRadius: '5px',\n                                },\n                                children: 'Customize',\n                              },\n                            },\n                          ],\n                        },\n                      },\n                    ],\n                  },\n                },\n              ],\n            },\n          },\n        ],\n      },\n    },\n    {\n      width: 1200,\n      height: 630,\n      fonts,\n    }\n  )\n}\n\nfunction generatePNGWithResvg(svg: string) {\n  const resvg = new Resvg(svg, {\n    fitTo: {\n      mode: 'width',\n      value: 1200,\n    },\n    font: {\n      loadSystemFonts: false,\n      fontFiles: [],\n      fontDirs: [],\n    },\n  })\n  const pngData = resvg.render()\n  return pngData.asPng()\n}\n\nasync function generatePNGWithSharp(svg: string) {\n  await Sharp(Buffer.from(svg)).png({ compressionLevel: 2 }).toBuffer()\n}\n\nsummary(() => {\n  bench('satori', () => generateSVG())\n  bench('satori + resvg', async () => {\n    const svg = await generateSVG()\n    return generatePNGWithResvg(svg)\n  })\n  bench('satori + sharp', async () => {\n    const svg = await generateSVG()\n    return generatePNGWithSharp(svg)\n  })\n})\n\nawait run()\n"
  },
  {
    "path": "test/border.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Border', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  describe('border', () => {\n    it('should support the shorthand', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            border: '1px solid',\n            width: '100%',\n            height: '100%',\n          }}\n        ></div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('border-color', () => {\n    it('should render black border by default', async () => {\n      const svg = await satori(\n        <div\n          style={{ border: '1px solid', width: '50%', height: '50%' }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should fallback border color to the current color', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            border: '1px solid',\n            color: 'red',\n            width: '50%',\n            height: '50%',\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support specifying `borderColor`', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            border: '1px',\n            borderColor: 'green',\n            width: '50%',\n            height: '50%',\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support overriding borderColor', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            border: '1px blue',\n            borderColor: 'red',\n            width: '50%',\n            height: '50%',\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('border-width', () => {\n    it('should render border inside the shape', async () => {\n      const svg = await satori(\n        <div\n          style={{ border: '5px solid black', width: 50, height: 50 }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('border-style', () => {\n    it('should support dashed border', async () => {\n      const svg = await satori(\n        <div\n          style={{ border: '5px dashed black', width: 50, height: 50 }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('border-radius', () => {\n    it('should support the shorthand', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            borderRadius: '10px',\n            background: 'red',\n            width: '100%',\n            height: '100%',\n          }}\n        ></div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support radius for a certain corner', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            borderTopRightRadius: '50px',\n            borderTopLeftRadius: '10px',\n            borderBottomLeftRadius: '60px',\n            background: 'red',\n            width: '100%',\n            height: '100%',\n          }}\n        ></div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should not exceed the length of the short side', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            borderRadius: 100,\n            background: 'red',\n            width: '100%',\n            height: '100%',\n          }}\n        ></div>,\n        {\n          width: 100,\n          height: 50,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support percentage border radius', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            borderRadius: '100% 10px',\n            background: 'red',\n            width: '100%',\n            height: '100%',\n          }}\n        ></div>,\n        {\n          width: 100,\n          height: 50,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support vw vh em and rem units', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            width: '100%',\n            height: '100%',\n            fontSize: '8px',\n          }}\n        >\n          <div\n            style={{\n              borderRadius: '50vw 25vh 1em 1rem',\n              background: 'red',\n              width: '100%',\n              height: '100%',\n            }}\n          ></div>\n        </div>,\n        {\n          width: 100,\n          height: 50,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support slash and 2-value corner', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            width: '100%',\n            height: '100%',\n            fontSize: '8px',\n          }}\n        >\n          <div\n            style={{\n              borderRadius: '50px 25% / 10px 20px',\n              borderTopLeftRadius: '10px 50px',\n              background: 'red',\n              width: '100%',\n              height: '100%',\n            }}\n          ></div>\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('directional', () => {\n    it('should support directional border', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            borderTop: '1px solid red',\n            borderRight: '2px solid green',\n            borderBottom: '3px solid blue',\n            borderLeft: '4px solid yellow',\n            width: '100%',\n            height: '100%',\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support non-complete border', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            borderTop: '10px solid red',\n            borderBottom: '5px dashed blue',\n            width: '100%',\n            height: '100%',\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support advanced border with radius', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            borderRadius: '10px 20%',\n            borderTopLeftRadius: '10px 25px',\n            borderTop: '10px solid red',\n            borderBottom: '5px dashed blue',\n            borderLeft: '2px solid yellow',\n            borderRight: '5px dashed blue',\n            background: 'gray',\n            width: '100%',\n            height: '100%',\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n})\n"
  },
  {
    "path": "test/box-sizing.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('box sizing', () => {\n  it('should default to border-box', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          height: '100%',\n          width: '100%',\n          backgroundColor: '#e2e2e2',\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            width: 50,\n            height: 50,\n            padding: 10,\n            boxSizing: 'border-box',\n            backgroundColor: 'purple',\n          }}\n        >\n          <div\n            style={{\n              height: '100%',\n              width: '100%',\n              backgroundColor: 'white',\n            }}\n          />\n        </div>\n      </div>,\n      { width: 100, height: 100, fonts: [] }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support content-box', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          backgroundColor: '#e2e2e2',\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            width: 50,\n            height: 50,\n            padding: 10,\n            boxSizing: 'content-box',\n            backgroundColor: 'purple',\n          }}\n        >\n          <div\n            style={{\n              height: '100%',\n              width: '100%',\n              backgroundColor: 'white',\n            }}\n          />\n        </div>\n      </div>,\n      { width: 100, height: 100, fonts: [] }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/clip-path.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('clipPath', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should render clip-path', async () => {\n    const svgs = await Promise.all(\n      [\n        'circle(20px)',\n        'circle(20% at bottom left)',\n        'ellipse(10px 0.625em at 10% 20%)',\n        'polygon(50% 0, 100% 50%, 50% 100%, 0 50%)',\n        `path('M 0 200 L 0,75 A 5,5 0,0,1 150,75 L 200 200 z')`,\n        'inset(10px 20px)',\n        'inset(0.5rem round 20% 1em 1rem 2px)',\n      ].map((clipPath) =>\n        satori(\n          <div\n            style={{\n              height: '100%',\n              width: '100%',\n              display: 'flex',\n              flexDirection: 'column',\n              alignItems: 'center',\n              justifyContent: 'center',\n              backgroundColor: '#fff',\n              clipPath,\n              fontSize: 32,\n              fontWeight: 600,\n            }}\n          >\n            <div style={{ marginTop: 40 }}>Hello, World</div>\n          </div>,\n          {\n            width: 100,\n            height: 100,\n            fonts,\n          }\n        )\n      )\n    )\n\n    svgs.forEach((svg) => expect(toImage(svg)).toMatchImageSnapshot())\n  })\n\n  it('should make clip-path compatible with overflow', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          overflow: 'hidden',\n          clipPath: 'circle(60px)',\n          fontSize: 32,\n          fontWeight: 600,\n        }}\n      >\n        <div>Lynnnnnnnnnnnnnnnnnnnnn</div>\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n\n    expect(toImage(svg)).toMatchImageSnapshot()\n  })\n\n  it('should respect the position value', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          clipPath: 'circle(30px at 20px 30%)',\n          fontSize: 32,\n          fontWeight: 600,\n        }}\n      >\n        <div>Lynnnnnnnnnnnnnnnnnnnnn</div>\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n\n    expect(toImage(svg)).toMatchImageSnapshot()\n  })\n\n  it('should respect left and top', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#ee7621',\n          fontSize: 32,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            height: 20,\n            width: 20,\n            clipPath: 'circle(5px at 5px 5px)',\n            background: 'red',\n          }}\n        ></div>\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n\n    expect(toImage(svg)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/color-models.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\n// TODO: no support for 'text-decoration' or 'outline'\n\ndescribe('Color Models', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  // TODO: test `background` shorthand?\n\n  // TODO: `filter` supported?\n\n  describe('backgroundColor and color', () => {\n    it('should support hexadecimal', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            backgroundColor: '#C3D7EE',\n            color: '#132539',\n            height: '100%',\n            width: '100%',\n          }}\n        >\n          Hexadecimal\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support hexadecimal with transparency', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            backgroundColor: '#C3D7EE70',\n            color: '#13253950',\n            height: '100%',\n            width: '100%',\n          }}\n        >\n          Hexadecimal with transparency\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support rgb', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            backgroundColor: 'rgb(148, 183, 223)',\n            color: 'rgb(19, 37, 57)',\n            height: '100%',\n            width: '100%',\n          }}\n        >\n          RGB\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support rgba', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            backgroundColor: 'rgba(148, 183, 223, .7)',\n            color: 'rgba(19, 37, 57, .5)',\n            height: '100%',\n            width: '100%',\n          }}\n        >\n          RGBA\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support hsl', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            backgroundColor: 'hsl(186, 22%, 26%)',\n            color: 'hsl(212, 0%, 100%)',\n            height: '100%',\n            width: '100%',\n          }}\n        >\n          HSL\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support hsla', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            backgroundColor: 'hsla(186, 22%, 26%, .5)',\n            color: 'hsla(212, 0%, 100%, .2)',\n            height: '100%',\n            width: '100%',\n          }}\n        >\n          HSLA\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support predefined color names', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            backgroundColor: 'pink',\n            color: 'red',\n            height: '100%',\n            width: '100%',\n          }}\n        >\n          Predefined color names\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support inherit color', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            backgroundColor: 'pink',\n            color: 'red',\n            height: '100%',\n            width: '100%',\n          }}\n        >\n          <div style={{ display: 'flex' }}>\n            red\n            <div>red</div>\n          </div>\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support currentcolor when inherit', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            backgroundColor: 'pink',\n            color: 'red',\n            height: '100%',\n            width: '100%',\n          }}\n        >\n          <div style={{ display: 'flex', color: 'currentcolor' }}>\n            red\n            <div>red</div>\n          </div>\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support currentcolor when background', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'column',\n            backgroundColor: 'black',\n            color: 'pink',\n            height: '100%',\n            width: '100%',\n          }}\n        >\n          <div style={{ display: 'flex', backgroundColor: 'currentcolor' }}>\n            <span style={{ color: 'white' }}>pink background</span>\n          </div>\n          <div\n            style={{ display: 'flex', backgroundColor: 'gray', padding: '4px' }}\n          >\n            <div style={{ display: 'flex', backgroundColor: 'currentcolor' }}>\n              <span style={{ color: 'white' }}>pink background</span>\n            </div>\n          </div>\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support currentcolor when border', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'column',\n            backgroundColor: 'black',\n            color: 'pink',\n            height: '100%',\n            width: '100%',\n          }}\n        >\n          <div style={{ border: '1px solid currentcolor' }}>pink border</div>\n          <div\n            style={{ display: 'flex', backgroundColor: 'gray', padding: '4px' }}\n          >\n            <div style={{ border: '1px solid currentcolor' }}>pink border</div>\n          </div>\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  it('should support css4 syntax color in hsl', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 16,\n          background: 'white',\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          textAlign: 'center',\n          alignItems: 'center',\n          justifyContent: 'center',\n        }}\n      >\n        <span style={{ color: 'hsl(200deg, 50%, 50%)' }}>A</span>\n        <span style={{ color: 'hsl(200deg, 50%, 50%, 0.6)' }}>A</span>\n        <span style={{ color: 'hsl(200, 50%, 50%)' }}>B</span>\n        <span style={{ color: 'hsl(0.3turn, 50%, 50%)' }}>C</span>\n        <span style={{ color: 'hsl(0.3turn, 50%, 50%, 0.6)' }}>D</span>\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support css4 syntax color in hsl if inherited', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 16,\n          background: 'white',\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          textAlign: 'center',\n          alignItems: 'center',\n          justifyContent: 'center',\n          color: 'hsl(200deg, 50%, 50%)',\n        }}\n      >\n        <span>A</span>\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  // Borders: shorthand, border-bottom-color, border-color, border-left-color, border-right-color, border-top-color\n\n  // Box shadow\n\n  // Filter\n})\n"
  },
  {
    "path": "test/css-variables.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndeclare module 'react' {\n  interface CSSProperties {\n    // Allow any CSS variable starting with '--'\n    [key: `--${string}`]: string | number\n  }\n}\n\ndescribe('CSS Variables', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should support basic CSS variable declaration and usage', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          '--primary-color': 'red',\n          backgroundColor: 'var(--primary-color)',\n          width: '100%',\n          height: '100%',\n        }}\n      />,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support CSS variable inheritance', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          '--primary-color': 'blue',\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n        }}\n      >\n        <div\n          style={{\n            backgroundColor: 'var(--primary-color)',\n            width: '50%',\n            height: '100%',\n          }}\n        />\n        <div\n          style={{\n            backgroundColor: 'var(--primary-color)',\n            width: '50%',\n            height: '100%',\n          }}\n        />\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support CSS variable override in children', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          '--primary-color': 'red',\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n        }}\n      >\n        <div\n          style={{\n            backgroundColor: 'var(--primary-color)',\n            width: '50%',\n            height: '100%',\n          }}\n        />\n        <div\n          style={{\n            '--primary-color': 'green',\n            backgroundColor: 'var(--primary-color)',\n            width: '50%',\n            height: '100%',\n          }}\n        />\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support CSS variable fallback values', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          backgroundColor: 'var(--undefined-color, yellow)',\n          width: '100%',\n          height: '100%',\n        }}\n      />,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support nested CSS variables', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          '--base-color': 'purple',\n          '--primary-color': 'var(--base-color)',\n          backgroundColor: 'var(--primary-color)',\n          width: '100%',\n          height: '100%',\n        }}\n      />,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support CSS variables with dimensions', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          '--box-size': '50px',\n          width: 'var(--box-size)',\n          height: 'var(--box-size)',\n          backgroundColor: 'orange',\n        }}\n      />,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support multiple CSS variables in nested inheritance', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          '--bg-color': 'lightblue',\n          '--text-color': 'darkblue',\n          backgroundColor: 'var(--bg-color)',\n          color: 'var(--text-color)',\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n        }}\n      >\n        <div\n          style={{\n            '--bg-color': 'lightgreen',\n            backgroundColor: 'var(--bg-color)',\n            color: 'var(--text-color)',\n            padding: '10px',\n          }}\n        >\n          Nested\n        </div>\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support CSS variables with border properties', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          '--border-color': '#333',\n          '--border-width': '5px',\n          border: 'solid',\n          borderColor: 'var(--border-color)',\n          borderWidth: 'var(--border-width)',\n          width: '80px',\n          height: '80px',\n        }}\n      />,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should handle undefined variables with fallback chain', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          '--fallback-color': 'pink',\n          backgroundColor: 'var(--undefined, var(--fallback-color))',\n          width: '100%',\n          height: '100%',\n        }}\n      />,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support CSS variables with percentage values', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          '--container-width': '80%',\n          '--container-height': '60%',\n          width: 'var(--container-width)',\n          height: 'var(--container-height)',\n          backgroundColor: 'teal',\n        }}\n      />,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support CSS variable for text color', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          '--theme-color': 'red',\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: 'white',\n        }}\n      >\n        <div\n          style={{\n            color: 'var(--theme-color)',\n            fontSize: '32px',\n          }}\n        >\n          Hello\n        </div>\n      </div>,\n      {\n        width: 200,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('should support CSS variable for inherited text color', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          '--theme-color': 'blue',\n          color: 'var(--theme-color)',\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: 'white',\n        }}\n      >\n        <div style={{ fontSize: '24px' }}>Parent</div>\n        <div\n          style={{\n            '--theme-color': 'green',\n            color: 'var(--theme-color)',\n            fontSize: '24px',\n          }}\n        >\n          Child\n        </div>\n      </div>,\n      {\n        width: 200,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/display-contents.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Display Contents', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should render display: contents', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n          flexDirection: 'column',\n        }}\n      >\n        <div\n          style={{\n            display: 'contents',\n          }}\n        >\n          <div\n            style={{\n              width: 50,\n              height: 30,\n              background: 'red',\n            }}\n          />\n          <div\n            style={{\n              width: 50,\n              height: 30,\n              background: 'blue',\n            }}\n          />\n        </div>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should treat display: contents children as direct children of parent', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'row',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n        }}\n      >\n        <div style={{ width: 20, height: 20, background: 'red' }} />\n        <div style={{ display: 'contents' }}>\n          <div style={{ width: 20, height: 20, background: 'blue' }} />\n          <div style={{ width: 20, height: 20, background: 'green' }} />\n        </div>\n        <div style={{ width: 20, height: 20, background: 'yellow' }} />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should ignore padding and margin on display: contents elements', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'column',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n        }}\n      >\n        <div\n          style={{\n            display: 'contents',\n            padding: 20,\n            margin: 20,\n            border: '10px solid red',\n          }}\n        >\n          <div\n            style={{\n              width: 50,\n              height: 30,\n              background: 'purple',\n            }}\n          />\n        </div>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should work with nested display: contents', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'column',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n        }}\n      >\n        <div style={{ display: 'contents' }}>\n          <div style={{ display: 'contents' }}>\n            <div style={{ width: 50, height: 20, background: 'red' }} />\n            <div style={{ width: 50, height: 20, background: 'blue' }} />\n          </div>\n          <div style={{ width: 50, height: 20, background: 'green' }} />\n        </div>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should apply flex properties to grandchildren through display: contents', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          justifyContent: 'space-between',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n        }}\n      >\n        <div style={{ display: 'contents' }}>\n          <div style={{ width: 20, height: 20, background: 'red' }} />\n          <div style={{ width: 20, height: 20, background: 'blue' }} />\n          <div style={{ width: 20, height: 20, background: 'green' }} />\n        </div>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should work with text children', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'column',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n          fontSize: 16,\n        }}\n      >\n        <div style={{ display: 'contents' }}>\n          <div style={{ background: 'white' }}>Hello</div>\n          <div style={{ background: 'yellow' }}>World</div>\n        </div>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/display.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('display', () => {\n  it('should support display: contents', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          height: '100%',\n          width: '100%',\n          gap: 10,\n          backgroundColor: '#e2e2e2',\n        }}\n      >\n        <div\n          style={{\n            display: 'contents',\n          }}\n        >\n          <div\n            style={{\n              height: 10,\n              width: 10,\n              backgroundColor: 'black',\n            }}\n          />\n          <div\n            style={{\n              height: 10,\n              width: 10,\n              backgroundColor: 'black',\n            }}\n          />\n        </div>\n      </div>,\n      { width: 100, height: 100, fonts: [] }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/dynamic-size.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Dynamic size', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should render image with dynamic height', async () => {\n    const svg = await satori(\n      <div>\n        Lorem Ipsum is simply dummy text of the printing and typesetting\n        industry.\n      </div>,\n      { width: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render image with dynamic width', async () => {\n    const svg = await satori(\n      <div>\n        Lorem Ipsum is simply dummy text of the printing and typesetting\n        industry.\n      </div>,\n      {\n        height: 25,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 300)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/embed-font.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('embedFont: false', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should have consistent x positions for multi-line text', async () => {\n    // This test verifies that when embedFont is false, consecutive text elements\n    // have consistent x positions (x[n] should equal x[n-1] + width[n-1]).\n    //\n    // Regression test for: src/text/index.ts rounding leftOffset to integers\n    // and using inconsistent width measurements (measureGrapheme vs measureText).\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          fontFamily: 'Roboto',\n          fontSize: 16,\n          width: 200,\n        }}\n      >\n        Hello world this is a test of text wrapping behavior\n      </div>,\n      {\n        width: 200,\n        height: 100,\n        fonts,\n        embedFont: false,\n      }\n    )\n\n    // Parse all <text> elements from the SVG\n    const textElementRegex =\n      /<text[^>]*\\bx=\"([^\"]+)\"[^>]*\\bwidth=\"([^\"]+)\"[^>]*>([^<]*)<\\/text>/g\n    const textElements: { x: number; width: number; content: string }[] = []\n\n    let match\n    while ((match = textElementRegex.exec(svg)) !== null) {\n      textElements.push({\n        x: parseFloat(match[1]),\n        width: parseFloat(match[2]),\n        content: match[3],\n      })\n    }\n\n    expect(textElements.length).toBeGreaterThan(1)\n\n    // Check consecutive elements on the same line\n    // The x position of each element should equal the previous element's x + width\n    let maxGap = 0\n\n    for (let i = 1; i < textElements.length; i += 1) {\n      const prev = textElements[i - 1]\n      const curr = textElements[i]\n\n      const expectedX = prev.x + prev.width\n      const gap = Math.abs(curr.x - expectedX)\n\n      // Only check elements that appear to be on the same line (small gap)\n      // Large gaps indicate line breaks\n      if (gap < 50) {\n        maxGap = Math.max(maxGap, gap)\n      }\n    }\n\n    // The gap between consecutive elements should be negligible (< 0.01px)\n    expect(maxGap).toBeLessThan(0.01)\n  })\n})\n"
  },
  {
    "path": "test/emoji.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Emojis', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should detect emojis correctly', async () => {\n    const emojis = []\n    await satori(<div>⛷👨‍👩‍👧❤️‍🔥🏳️‍🌈㊗️🛠👶🏾</div>, {\n      width: 100,\n      height: 100,\n      fonts,\n      loadAdditionalAsset: async (languageCode, segment) => {\n        if (languageCode === 'emoji') {\n          emojis.push(segment)\n        }\n        return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPj/HwADBwIAMCbHYQAAAABJRU5ErkJggg=='\n      },\n    })\n    expect(emojis).toMatchInlineSnapshot(`\n      [\n        \"⛷\",\n        \"👨‍👩‍👧\",\n        \"❤️‍🔥\",\n        \"🏳️‍🌈\",\n        \"㊗️\",\n        \"🛠\",\n        \"👶🏾\",\n      ]\n    `)\n  })\n\n  // https://github.com/vercel/satori/issues/302\n  it('should render emojis correctly', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          backgroundColor: '#fff',\n        }}\n      >\n        🌈 🏳️‍🌈 Luci, 👨‍👨‍👧Nyan👨‍👨‍👧‍👧\n      </div>,\n      {\n        width: 200,\n        height: 100,\n        fonts,\n        graphemeImages: {\n          '🌈': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iIzg3NjdBQyIgZD0iTTM2IDFDMTYuMTE4IDEgMSAxNi4xMTggMSAzNmgxNy4wNDJjMC05LjkxNyA4LjA0Mi0xNy45NTggMTcuOTU4LTE3Ljk1OFYxeiIvPjxwYXRoIGZpbGw9IiNFQjIwMjciIGQ9Ik0wIDM1Ljk5OWgzLjA0MmMwLTE4LjE4OSAxNC43MzQtMzIuOTM1IDMyLjkxNy0zMi45NTdWMEMxNi4wOTUuMDIzIDAgMTYuMTMxIDAgMzUuOTk5eiIvPjxwYXRoIGZpbGw9IiNGMTkwMjAiIGQ9Ik0zLjA4MyAzNmgzQzYuMDgzIDE5LjQ2OCAxOS40NzMgNi4wNjUgMzYgNi4wNDN2LTNDMTcuODE3IDMuMDY1IDMuMDgzIDE3LjgxMSAzLjA4MyAzNnoiLz48cGF0aCBmaWxsPSIjRkZDQjRDIiBkPSJNNi4wODMgMzZoM0M5LjA4MyAyMS4xMjUgMjEuMTMgOS4wNjUgMzYgOS4wNDN2LTNDMTkuNDczIDYuMDY1IDYuMDgzIDE5LjQ2OCA2LjA4MyAzNnoiLz48cGF0aCBmaWxsPSIjNUM5MDNGIiBkPSJNOS4wODMgMzZoM2MwLTEzLjIxNyAxMC43MDUtMjMuOTM1IDIzLjkxNy0yMy45NTd2LTNDMjEuMTMgOS4wNjUgOS4wODMgMjEuMTI1IDkuMDgzIDM2eiIvPjxwYXRoIGZpbGw9IiMyMjY3OTgiIGQ9Ik0xMi4wODMgMzZoM2MwLTExLjU2IDkuMzYyLTIwLjkzNCAyMC45MTctMjAuOTU2di0zLjAwMUMyMi43ODggMTIuMDY1IDEyLjA4MyAyMi43ODMgMTIuMDgzIDM2eiIvPjwvc3ZnPg==',\n          '🏳️‍🌈': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBhdGggZmlsbD0iIzg4MDA4MiIgZD0iTTAgMjdhNCA0IDAgMCAwIDQgNGgyOGE0IDQgMCAwIDAgNC00di0uNUgwdi41eiIvPjxwYXRoIGZpbGw9IiMzNTU4QTAiIGQ9Ik0wIDIyLjA3aDM2djQuNkgweiIvPjxwYXRoIGZpbGw9IiMxMzhGM0UiIGQ9Ik0wIDE3LjgzaDM2djQuNUgweiIvPjxwYXRoIGZpbGw9IiNGQUQyMjAiIGQ9Ik0wIDEzLjVoMzZWMThIMHoiLz48cGF0aCBmaWxsPSIjRkY3MzAwIiBkPSJNMCA5LjE3aDM2djQuNUgweiIvPjxwYXRoIGZpbGw9IiNGRjAwMEUiIGQ9Ik0zMiA1SDRhNCA0IDAgMCAwLTQgNHYuMzNoMzZWOWE0IDQgMCAwIDAtNC00eiIvPjwvc3ZnPg==',\n          '👨‍👨‍👧': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iI0ZGREM1RCIgZD0iTTI2IDIydi00aDV2NGgydjZoLTl2LTZ6Ii8+PHBhdGggZmlsbD0iIzNCOTNEOSIgZD0iTTMxIDIybC0yLjUyOCAzLjc3OEwyNiAyMmgtNWMtMiAwLTMgMS0zIDIuOTczVjM2aDE4VjIyaC01eiIvPjxwYXRoIGZpbGw9IiMxQjYzOTkiIGQ9Ik0yMC45ODYgMjZoMS4wMjh2MTBoLTEuMDI4eiIvPjxwYXRoIGZpbGw9IiNGRkRDNUQiIGQ9Ik0yMSA0bC4wMjQgMTBjLjAxMS4xNzUuMDM2LjM0Ny4wNTkuNTE4LjQ5NCAzLjY0IDMuNjE4IDYuNDUgNy40MTcgNi40NSAzLjk3OCAwIDcuMjIzLTMuMDc5IDcuNDc2LTYuOTY5SDM2VjRIMjF6Ii8+PHBhdGggZmlsbD0iI0ZGQUMzMyIgZD0iTTE5LjA0MiA4Ljg2MWMwIDIuMTUxLjc2NyA0LjEyMyAyLjA0MiA1LjY1OC0uMDIzLS4xNzItLjA0OC0uMzQzLS4wNTktLjUxOGwtLjAxLTQuMDMzYzUuMzYyLS4zMDIgOC41MTMtMi42NzUgMTAuMjAyLTQuNTdDMzEuNzgyIDcuMTcgMzMuMTE0IDEwIDM1Ljg2MSAxMEgzNlYxLjg4N0MzNC45MDYgMS4yMDYgMzMuNjE5LjgwNiAzMi4yMzYuODA2Yy0uMjAxIDAtLjM5OC4wMTQtLjU5NS4wM0MzMC41MDQuMzA1IDI5LjI0IDAgMjcuOTAyIDBjLTQuODk0IDAtOC44NiAzLjk2Ny04Ljg2IDguODYxeiIvPjxwYXRoIGZpbGw9IiNCRjY5NTIiIGQ9Ik0yNS4yNzggMTcuNzIzaDYuNDQ0cy0uODA1IDEuNjEtMy4yMjIgMS42MS0zLjIyMi0xLjYxLTMuMjIyLTEuNjF6TTI4IDE1LjVoMWMuMjc2IDAgLjUtLjIyNC41LS41cy0uMjI0LS41LS41LS41aC0xYy0uMjc2IDAtLjUuMjI0LS41LjVzLjIyNC41LjUuNXoiLz48cGF0aCBmaWxsPSIjNjYyMTEzIiBkPSJNMzIgMTNjLjU1MiAwIDEtLjQ0OCAxLTF2LTFjMC0uNTUyLS40NDgtMS0xLTFzLTEgLjQ0OC0xIDF2MWMwIC41NTIuNDQ4IDEgMSAxem0tNyAwYy41NTIgMCAxLS40NDggMS0xdi0xYzAtLjU1Mi0uNDQ4LTEtMS0xcy0xIC40NDgtMSAxdjFjMCAuNTUyLjQ0OCAxIDEgMXoiLz48cGF0aCBmaWxsPSIjRkZEQzVEIiBkPSJNNSAyMnYtNGg1djRoMnY2SDN2LTZ6Ii8+PHBhdGggZmlsbD0iIzNCOTREOSIgZD0iTTE1IDIyaC01bC0yLjQ3MiAzLjc3OEw1IDIySDB2MTRoMThWMjQuOTczQzE4IDIzIDE3IDIyIDE1IDIyeiIvPjxwYXRoIGZpbGw9IiMxQjYzOTkiIGQ9Ik0xMy45ODYgMjZoMS4wMjh2MTBoLTEuMDI4eiIvPjxwYXRoIGZpbGw9IiNGRkRDNUQiIGQ9Ik0xNSAxNFY1TDAgNHYxMGguMDI0Yy4yNTIgMy44OSAzLjQ5OCA2Ljk2OSA3LjQ3NiA2Ljk2OSAzLjc5MSAwIDYuOTMyLTIuNzk5IDcuNDM4LTYuNDI4LjAyNS0uMTguMDUtLjM1OC4wNjItLjU0MXoiLz48cGF0aCBmaWxsPSIjRkZBQzMzIiBkPSJNMy44MDYuODA2QzIuNDA1LjgwNiAxLjEwMiAxLjIxNSAwIDEuOTEydjguMDgzYzUuOTQ3LS4wNTUgOS4zNzUtMi41OTMgMTEuMTYyLTQuNTk4LjUwNiAxLjU4OSAxLjYzMyA0LjAyMSAzLjgzOCA0LjUxVjE0Yy0uMDEyLjE4My0uMDM3LjM2MS0uMDYyLjU0QzE2LjIyNCAxMy4wMDIgMTcgMTEuMDIzIDE3IDguODYxIDE3IDMuOTY3IDEzLjAzMyAwIDguMTM5IDAgNi44MDEgMCA1LjUzOC4zMDUgNC40LjgzNmMtLjE5Ni0uMDE3LS4zOTQtLjAzLS41OTQtLjAzek0xNSA1di4xNjZsLS4wMjgtLjE2N0wxNSA1eiIvPjxwYXRoIGZpbGw9IiNCRjY5NTIiIGQ9Ik0xMC43MjIgMTcuNzIzSDQuMjc4cy44MDUgMS42MSAzLjIyMiAxLjYxIDMuMjIyLTEuNjEgMy4yMjItMS42MXpNOCAxNS41SDdjLS4yNzYgMC0uNS0uMjI0LS41LS41cy4yMjQtLjUuNS0uNWgxYy4yNzYgMCAuNS4yMjQuNS41cy0uMjI0LjUtLjUuNXoiLz48cGF0aCBmaWxsPSIjNjYyMTEzIiBkPSJNNCAxM2MtLjU1MiAwLTEtLjQ0OC0xLTF2LTFjMC0uNTUyLjQ0OC0xIDEtMXMxIC40NDggMSAxdjFjMCAuNTUyLS40NDggMS0xIDF6bTcgMGMtLjU1MiAwLTEtLjQ0OC0xLTF2LTFjMC0uNTUyLjQ0OC0xIDEtMXMxIC40NDggMSAxdjFjMCAuNTUyLS40NDggMS0xIDF6Ii8+PHBhdGggZmlsbD0iI0Y0OTAwQyIgZD0iTTE5LjQzMiAzMi44NTJjLS4yNjQuNTExLS40MzIuOTIxLS40MzIgMS4xNDggNC42NTYgMCA5LTMgOS0zLTEuMTI1LTMuNjg4LTEuNjY2LTguMTM5LTEuNjY2LTguMTM5IDAtNC4zNDItMy41MTktNy44NjEtNy44NjEtNy44NjEtMS4yNDUgMC0yLjQxOC4yOTctMy40NjQuODEyLTMuMzMuMDctNi4wMDkgMi43ODctNi4wMDkgNi4xMzMgMCAuODI4LjE2NSAxLjYxNy40NjQgMi4zMzdDOS4yMTQgMjUuODkzIDguNzMxIDI4LjYwNCA4IDMxYzAgMCA0LjM0NCAzIDkgMyAwLS4yMjctLjE2Ny0uNjM2LS40MzEtMS4xNDZDMTMuMzkgMzIuMTk4IDExIDI5LjQgMTEgMjYuMDQ3bC4wMDItLjA0N0gxMXYtNmgzLjU1MWMxLjAxOC0uNTc0IDIuMTkyLS45MDYgMy40NDYtLjkwNnMyLjQyOS4zMzIgMy40NDYuOTA2SDI1djZoLS4wMDhsLjAwMi4wNDdjMCAzLjM1MS0yLjM4NiA2LjE0Ny01LjU2MiA2LjgwNXoiLz48cGF0aCBmaWxsPSIjRkZEQzVEIiBkPSJNMjAgMzV2LTRoLTR2NGgtMXYxaDZ2LTF6Ii8+PHBhdGggZmlsbD0iI0VBNTk2RSIgZD0iTTE0IDMzYy0yLjQ5MyAwLTQuMjc2IDEuMzg1LTQuODE0IDNIMThsLTItM2gtMnptOSAwaC0zbC0yIDNoOC44MThjLS41MTgtMS42MDItMi4xNTktMy0zLjgxOC0zeiIvPjxwYXRoIGZpbGw9IiNGRkRDNUQiIGQ9Ik0yNC45OTQgMjYuMDQ3TDI0Ljk5MiAyNkgyNXYtNmgtMy41NTdjLTEuMDE4LS41NzQtMi4xOTItLjkwNi0zLjQ0Ni0uOTA2cy0yLjQyOC4zMzItMy40NDYuOTA2SDExdjZoLjAwMmwtLjAwMi4wNDdjMCAzLjM1MyAyLjM5IDYuMTUyIDUuNTY5IDYuODA3LjQ2MS4wOTUuOTM4LjE0NiAxLjQyOC4xNDYuNDkyIDAgLjk3Mi0uMDUyIDEuNDM1LS4xNDggMy4xNzYtLjY1OCA1LjU2Mi0zLjQ1NCA1LjU2Mi02LjgwNXoiLz48cGF0aCBmaWxsPSIjRjQ5MDBDIiBkPSJNMjYgMjNsLTEtNC0zLTItMTEgMS0uODYxIDVjNS40MSAwIDguNDI1LTEuNzU1IDEwLjA2NS0zLjM2N0MyMS40NCAyMS4wOTIgMjMuNTI4IDIzIDI2IDIzeiIvPjxwYXRoIGZpbGw9IiNCRjY5NTIiIGQ9Ik0yMSAzMGgtNnMuNTgzIDIgMyAyIDMtMiAzLTJ6bS0zLS41Yy0uMTMgMC0uMjYtLjA1LS4zNS0uMTUtLjEtLjA5LS4xNS0uMjItLjE1LS4zNXMuMDUtLjI2LjE1LS4zNmMuMTctLjE3LjUyLS4xOC43MS4wMS4wOS4wOS4xNC4yMi4xNC4zNXMtLjA1LjI2LS4xNS4zNWMtLjA5LjEtLjIyLjE1LS4zNS4xNXoiLz48cGF0aCBmaWxsPSIjNjYyMTEzIiBkPSJNMTUgMjhjLS41NTIgMC0xLS40NDctMS0xdi0xYzAtLjU1My40NDgtMSAxLTFzMSAuNDQ3IDEgMXYxYzAgLjU1My0uNDQ4IDEtMSAxem02IDBjLS41NTMgMC0xLS40NDctMS0xdi0xYzAtLjU1My40NDctMSAxLTEgLjU1MyAwIDEgLjQ0NyAxIDF2MWMwIC41NTMtLjQ0NyAxLTEgMXoiLz48L3N2Zz4=',\n          '👨‍👨‍👧‍👧': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iI0ZGREM1RCIgZD0iTTI2IDIydi00aDV2NGgydjZoLTl2LTZ6Ii8+PHBhdGggZmlsbD0iIzNCOTNEOSIgZD0iTTMxIDIybC0yLjUyOCAzLjc3OEwyNiAyMmgtNWMtMiAwLTMgMS0zIDIuOTczVjM2aDE4VjIyaC01eiIvPjxwYXRoIGZpbGw9IiMxQjYzOTkiIGQ9Ik0yMC45ODYgMjZoMS4wMjh2MTBoLTEuMDI4eiIvPjxwYXRoIGZpbGw9IiNGRkRDNUQiIGQ9Ik0yMSA0bC4wMjQgMTBjLjAxMS4xNzUuMDM2LjM0Ny4wNTkuNTE4LjQ5NCAzLjY0IDMuNjE4IDYuNDUgNy40MTcgNi40NSAzLjk3OCAwIDcuMjIzLTMuMDc5IDcuNDc2LTYuOTY5SDM2VjRIMjF6Ii8+PHBhdGggZmlsbD0iI0ZGQUMzMyIgZD0iTTE5LjA0MiA4Ljg2MWMwIDIuMTUxLjc2NyA0LjEyMyAyLjA0MiA1LjY1OC0uMDIzLS4xNzItLjA0OC0uMzQzLS4wNTktLjUxOGwtLjAxLTQuMDMzYzUuMzYyLS4zMDIgOC41MTMtMi42NzUgMTAuMjAyLTQuNTdDMzEuNzgyIDcuMTcgMzMuMTE0IDEwIDM1Ljg2MSAxMEgzNlYxLjg4N0MzNC45MDYgMS4yMDYgMzMuNjE5LjgwNiAzMi4yMzYuODA2Yy0uMjAxIDAtLjM5OC4wMTQtLjU5NS4wM0MzMC41MDQuMzA1IDI5LjI0IDAgMjcuOTAyIDBjLTQuODk0IDAtOC44NiAzLjk2Ny04Ljg2IDguODYxeiIvPjxwYXRoIGZpbGw9IiNCRjY5NTIiIGQ9Ik0yNS4yNzggMTcuNzIzaDYuNDQ0cy0uODA1IDEuNjEtMy4yMjIgMS42MS0zLjIyMi0xLjYxLTMuMjIyLTEuNjF6TTI4IDE1LjVoMWMuMjc2IDAgLjUtLjIyNC41LS41cy0uMjI0LS41LS41LS41aC0xYy0uMjc2IDAtLjUuMjI0LS41LjVzLjIyNC41LjUuNXoiLz48cGF0aCBmaWxsPSIjNjYyMTEzIiBkPSJNMzIgMTNjLjU1MiAwIDEtLjQ0OCAxLTF2LTFjMC0uNTUyLS40NDgtMS0xLTFzLTEgLjQ0OC0xIDF2MWMwIC41NTIuNDQ4IDEgMSAxem0tNyAwYy41NTIgMCAxLS40NDggMS0xdi0xYzAtLjU1Mi0uNDQ4LTEtMS0xcy0xIC40NDgtMSAxdjFjMCAuNTUyLjQ0OCAxIDEgMXoiLz48cGF0aCBmaWxsPSIjRkZEQzVEIiBkPSJNNSAyMnYtNGg1djRoMnY2SDN2LTZ6Ii8+PHBhdGggZmlsbD0iIzNCOTREOSIgZD0iTTE1IDIyaC01bC0yLjQ3MiAzLjc3OEw1IDIySDB2MTRoMThWMjQuOTczQzE4IDIzIDE3IDIyIDE1IDIyeiIvPjxwYXRoIGZpbGw9IiMxQjYzOTkiIGQ9Ik0xMy45ODYgMjZoMS4wMjh2MTBoLTEuMDI4eiIvPjxwYXRoIGZpbGw9IiNGRkRDNUQiIGQ9Ik0xNSAxNFY1TDAgNHYxMGguMDI0Yy4yNTIgMy44OSAzLjQ5OCA2Ljk2OSA3LjQ3NiA2Ljk2OSAzLjc5MSAwIDYuOTMyLTIuNzk5IDcuNDM4LTYuNDI4LjAyNS0uMTguMDUtLjM1OC4wNjItLjU0MXoiLz48cGF0aCBmaWxsPSIjRkZBQzMzIiBkPSJNMy44MDYuODA2QzIuNDA1LjgwNiAxLjEwMiAxLjIxNSAwIDEuOTEydjguMDgzYzUuOTQ3LS4wNTUgOS4zNzUtMi41OTMgMTEuMTYyLTQuNTk4LjUwNiAxLjU4OSAxLjYzMyA0LjAyMSAzLjgzOCA0LjUxVjE0Yy0uMDEyLjE4My0uMDM3LjM2MS0uMDYyLjU0QzE2LjIyNCAxMy4wMDIgMTcgMTEuMDIzIDE3IDguODYxIDE3IDMuOTY3IDEzLjAzMyAwIDguMTM5IDAgNi44MDEgMCA1LjUzOC4zMDUgNC40LjgzNmMtLjE5Ni0uMDE3LS4zOTQtLjAzLS41OTQtLjAzek0xNSA1di4xNjZsLS4wMjgtLjE2N0wxNSA1eiIvPjxwYXRoIGZpbGw9IiNCRjY5NTIiIGQ9Ik0xMC43MjIgMTcuNzIzSDQuMjc4cy44MDUgMS42MSAzLjIyMiAxLjYxIDMuMjIyLTEuNjEgMy4yMjItMS42MXpNOCAxNS41SDdjLS4yNzYgMC0uNS0uMjI0LS41LS41cy4yMjQtLjUuNS0uNWgxYy4yNzYgMCAuNS4yMjQuNS41cy0uMjI0LjUtLjUuNXoiLz48cGF0aCBmaWxsPSIjNjYyMTEzIiBkPSJNNCAxM2MtLjU1MiAwLTEtLjQ0OC0xLTF2LTFjMC0uNTUyLjQ0OC0xIDEtMXMxIC40NDggMSAxdjFjMCAuNTUyLS40NDggMS0xIDF6bTcgMGMtLjU1MiAwLTEtLjQ0OC0xLTF2LTFjMC0uNTUyLjQ0OC0xIDEtMXMxIC40NDggMSAxdjFjMCAuNTUyLS40NDggMS0xIDF6Ii8+PHBhdGggZmlsbD0iI0ZGREM1RCIgZD0iTTMyLjk5OCAyMi43MTRsLTQuMjg2LS44NTctNy43MTQuODU3VjI3SDIxbC0uMDAyLjA0YzAgMy4yOTEgMi42ODUgNS45NiA1Ljk5OCA1Ljk2IDMuMzEyIDAgNS45OTctMi42NjkgNS45OTctNS45NmwtLjAwMi0uMDRoLjAwN3YtNC4yODZ6Ii8+PHBhdGggZmlsbD0iI0Y0OTAwQyIgZD0iTTMzLjk5NSAyNS4xOTJjMC0uMDIzLjAwMy0uMDQ0LjAwMy0uMDY3bC0uMDAyLS4wMzkuMDAyLS4wODYtLjAwNi4wMDZDMzMuOTI4IDIxLjY3OSAzMS4yMTYgMTkgMjcuODczIDE5Yy0uMjk4IDAtLjU4OC4wMjktLjg3NS4wNy0uMjg2LS4wNDEtLjU3Ny0uMDctLjg3NS0uMDctMy4zODMgMC02LjEyNSAyLjc0Mi02LjEyNSA2LjEyNSAwIDAgLjEyNSAzLjE4OC0xIDYuODc1IDAgMCAzLjM0NCAyIDggMnM4LTIgOC0yYy0uOTY4LTMuMTcyLTEuMDEtNi4wNjEtMS4wMDMtNi44MDh6bS0xLjAwMiAxLjg0OGMwIDMuMjkxLTIuNjg1IDUuOTYtNS45OTcgNS45NnMtNS45OTctMi42NjktNS45OTctNS45NmMtLjAwMS0uMDEzLjAwMS0uMDI3LjAwMS0uMDRoLS4wMDJ2LTEuOTExYzQuMTcyLS4xODEgNi41ODgtMS41OTIgNy45MjktMi45MS45MSAxLjA3NCAyLjM1MSAyLjQxNCA0LjA3MSAyLjgxNlYyN2gtLjAwN2wuMDAyLjA0eiIvPjxwYXRoIGZpbGw9IiNGRkRDNUQiIGQ9Ik0yOC45OTggMzR2LTNoLTR2M2gtMXYyaDZ2LTJ6Ii8+PHBhdGggZmlsbD0iI0VBNTk2RSIgZD0iTTMxLjk5OCAzM2gtM2wtMiAyLTItMmgtMmMtMi40OTMgMC00LjI3NiAxLjM4NS00LjgxNCAzaDE3LjYzMmMtLjUxOC0xLjYwMi0yLjE1OS0zLTMuODE4LTN6Ii8+PHBhdGggZmlsbD0iI0JGNjk1MiIgZD0iTTI4Ljk5OCAzMGgtNHMuMzg5IDEuMzMzIDIgMS4zMzMgMi0xLjMzMyAyLTEuMzMzem0tMi0uNWMtLjEzIDAtLjI2LS4wNS0uMzUtLjE1LS4xLS4wOS0uMTUtLjIyLS4xNS0uMzVzLjA1LS4yNi4xNS0uMzZjLjE3LS4xNy41Mi0uMTguNzEuMDEuMDkuMDkxLjE0LjIyMS4xNC4zNTFzLS4wNS4yNi0uMTUuMzVjLS4wOS4wOTktLjIyLjE0OS0uMzUuMTQ5eiIvPjxwYXRoIGZpbGw9IiM2NjIxMTMiIGQ9Ik0yOC45OTggMjhjLS40NjEgMC0uODMzLS4zNzMtLjgzMy0uODMzdi0uODMzYzAtLjQ2MS4zNzMtLjgzMy44MzMtLjgzMy40NjEgMCAuODMzLjM3My44MzMuODMzdi44MzNjLjAwMS40Ni0uMzcyLjgzMy0uODMzLjgzM3ptLTQgMGMtLjQ2MSAwLS44MzMtLjM3My0uODMzLS44MzN2LS44MzNjMC0uNDYxLjM3My0uODMzLjgzMy0uODMzLjQ2MSAwIC44MzMuMzczLjgzMy44MzN2LjgzM2MuMDAxLjQ2LS4zNzIuODMzLS44MzMuODMzeiIvPjxwYXRoIGZpbGw9IiNGRkRDNUQiIGQ9Ik0xNC45OTggMjIuNzE0bC00LjI4Ni0uODU3LTcuNzE0Ljg1N1YyN0gzbC0uMDAyLjA0YzAgMy4yOTEgMi42ODUgNS45NiA1Ljk5OCA1Ljk2IDMuMzEyIDAgNS45OTctMi42NjkgNS45OTctNS45NmwtLjAwMi0uMDRoLjAwN3YtNC4yODZ6Ii8+PHBhdGggZmlsbD0iI0Y0OTAwQyIgZD0iTTE1Ljk5NSAyNS4xOTJjMC0uMDIzLjAwMy0uMDQ0LjAwMy0uMDY3bC0uMDAyLS4wMzkuMDAyLS4wODYtLjAwNi4wMDZDMTUuOTI4IDIxLjY3OSAxMy4yMTYgMTkgOS44NzMgMTljLS4yOTggMC0uNTg4LjAyOS0uODc1LjA3LS4yODYtLjA0MS0uNTc3LS4wNy0uODc1LS4wNy0zLjM4MyAwLTYuMTI1IDIuNzQyLTYuMTI1IDYuMTI1IDAgMCAuMTI1IDMuMTg4LTEgNi44NzUgMCAwIDMuMzQ0IDIgOCAyczgtMiA4LTJjLS45NjgtMy4xNzItMS4wMS02LjA2MS0xLjAwMy02LjgwOHptLTEuMDAyIDEuODQ4YzAgMy4yOTEtMi42ODUgNS45Ni01Ljk5NyA1Ljk2cy01Ljk5Ny0yLjY2OS01Ljk5Ny01Ljk2QzIuOTk4IDI3LjAyNyAzIDI3LjAxMyAzIDI3aC0uMDAydi0xLjkxMWM0LjE3Mi0uMTgxIDYuNTg4LTEuNTkyIDcuOTI5LTIuOTEuOTEgMS4wNzQgMi4zNTEgMi40MTQgNC4wNzEgMi44MTZWMjdoLS4wMDdsLjAwMi4wNHoiLz48cGF0aCBmaWxsPSIjRkZEQzVEIiBkPSJNMTAuOTk4IDM0di0zaC00djNoLTF2Mmg2di0yeiIvPjxwYXRoIGZpbGw9IiNFQTU5NkUiIGQ9Ik0xMy45OTggMzNoLTNsLTIgMi0yLTJoLTJDMi41MDUgMzMgLjcyMiAzNC4zODUuMTg0IDM2aDE3LjYzMmMtLjUxOC0xLjYwMi0yLjE1OS0zLTMuODE4LTN6Ii8+PHBhdGggZmlsbD0iI0JGNjk1MiIgZD0iTTEwLjk5OCAzMGgtNHMuMzg5IDEuMzMzIDIgMS4zMzMgMi0xLjMzMyAyLTEuMzMzem0tMi0uNWMtLjEzIDAtLjI2LS4wNS0uMzUtLjE1LS4xLS4wOS0uMTUtLjIyLS4xNS0uMzVzLjA1LS4yNi4xNS0uMzZjLjE3LS4xNy41Mi0uMTguNzEuMDEuMDkuMDkxLjE0LjIyMS4xNC4zNTFzLS4wNS4yNi0uMTUuMzVjLS4wOS4wOTktLjIyLjE0OS0uMzUuMTQ5eiIvPjxwYXRoIGZpbGw9IiM2NjIxMTMiIGQ9Ik0xMC45OTggMjhjLS40NjEgMC0uODMzLS4zNzMtLjgzMy0uODMzdi0uODMzYzAtLjQ2MS4zNzMtLjgzMy44MzMtLjgzMy40NjEgMCAuODMzLjM3My44MzMuODMzdi44MzNjLjAwMS40Ni0uMzcyLjgzMy0uODMzLjgzM3ptLTQgMGMtLjQ2MSAwLS44MzMtLjM3My0uODMzLS44MzN2LS44MzNjMC0uNDYxLjM3My0uODMzLjgzMy0uODMzLjQ2MSAwIC44MzMuMzczLjgzMy44MzN2LjgzM2MuMDAxLjQ2LS4zNzIuODMzLS44MzMuODMzeiIvPjwvc3ZnPg==',\n        },\n      }\n    )\n\n    expect(await toImage(svg)).toMatchImageSnapshot()\n  })\n\n  it('should render emojis correctly with `word-break: break-all`', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          backgroundColor: '#fff',\n          wordBreak: 'break-all',\n        }}\n      >\n        🌈 🏳️‍🌈 Luci, 👨‍👨‍👧Nyan👨‍👨‍👧‍👧\n      </div>,\n      {\n        width: 200,\n        height: 100,\n        fonts,\n        graphemeImages: {\n          '🌈': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iIzg3NjdBQyIgZD0iTTM2IDFDMTYuMTE4IDEgMSAxNi4xMTggMSAzNmgxNy4wNDJjMC05LjkxNyA4LjA0Mi0xNy45NTggMTcuOTU4LTE3Ljk1OFYxeiIvPjxwYXRoIGZpbGw9IiNFQjIwMjciIGQ9Ik0wIDM1Ljk5OWgzLjA0MmMwLTE4LjE4OSAxNC43MzQtMzIuOTM1IDMyLjkxNy0zMi45NTdWMEMxNi4wOTUuMDIzIDAgMTYuMTMxIDAgMzUuOTk5eiIvPjxwYXRoIGZpbGw9IiNGMTkwMjAiIGQ9Ik0zLjA4MyAzNmgzQzYuMDgzIDE5LjQ2OCAxOS40NzMgNi4wNjUgMzYgNi4wNDN2LTNDMTcuODE3IDMuMDY1IDMuMDgzIDE3LjgxMSAzLjA4MyAzNnoiLz48cGF0aCBmaWxsPSIjRkZDQjRDIiBkPSJNNi4wODMgMzZoM0M5LjA4MyAyMS4xMjUgMjEuMTMgOS4wNjUgMzYgOS4wNDN2LTNDMTkuNDczIDYuMDY1IDYuMDgzIDE5LjQ2OCA2LjA4MyAzNnoiLz48cGF0aCBmaWxsPSIjNUM5MDNGIiBkPSJNOS4wODMgMzZoM2MwLTEzLjIxNyAxMC43MDUtMjMuOTM1IDIzLjkxNy0yMy45NTd2LTNDMjEuMTMgOS4wNjUgOS4wODMgMjEuMTI1IDkuMDgzIDM2eiIvPjxwYXRoIGZpbGw9IiMyMjY3OTgiIGQ9Ik0xMi4wODMgMzZoM2MwLTExLjU2IDkuMzYyLTIwLjkzNCAyMC45MTctMjAuOTU2di0zLjAwMUMyMi43ODggMTIuMDY1IDEyLjA4MyAyMi43ODMgMTIuMDgzIDM2eiIvPjwvc3ZnPg==',\n          '🏳️‍🌈': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBhdGggZmlsbD0iIzg4MDA4MiIgZD0iTTAgMjdhNCA0IDAgMCAwIDQgNGgyOGE0IDQgMCAwIDAgNC00di0uNUgwdi41eiIvPjxwYXRoIGZpbGw9IiMzNTU4QTAiIGQ9Ik0wIDIyLjA3aDM2djQuNkgweiIvPjxwYXRoIGZpbGw9IiMxMzhGM0UiIGQ9Ik0wIDE3LjgzaDM2djQuNUgweiIvPjxwYXRoIGZpbGw9IiNGQUQyMjAiIGQ9Ik0wIDEzLjVoMzZWMThIMHoiLz48cGF0aCBmaWxsPSIjRkY3MzAwIiBkPSJNMCA5LjE3aDM2djQuNUgweiIvPjxwYXRoIGZpbGw9IiNGRjAwMEUiIGQ9Ik0zMiA1SDRhNCA0IDAgMCAwLTQgNHYuMzNoMzZWOWE0IDQgMCAwIDAtNC00eiIvPjwvc3ZnPg==',\n          '👨‍👨‍👧': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iI0ZGREM1RCIgZD0iTTI2IDIydi00aDV2NGgydjZoLTl2LTZ6Ii8+PHBhdGggZmlsbD0iIzNCOTNEOSIgZD0iTTMxIDIybC0yLjUyOCAzLjc3OEwyNiAyMmgtNWMtMiAwLTMgMS0zIDIuOTczVjM2aDE4VjIyaC01eiIvPjxwYXRoIGZpbGw9IiMxQjYzOTkiIGQ9Ik0yMC45ODYgMjZoMS4wMjh2MTBoLTEuMDI4eiIvPjxwYXRoIGZpbGw9IiNGRkRDNUQiIGQ9Ik0yMSA0bC4wMjQgMTBjLjAxMS4xNzUuMDM2LjM0Ny4wNTkuNTE4LjQ5NCAzLjY0IDMuNjE4IDYuNDUgNy40MTcgNi40NSAzLjk3OCAwIDcuMjIzLTMuMDc5IDcuNDc2LTYuOTY5SDM2VjRIMjF6Ii8+PHBhdGggZmlsbD0iI0ZGQUMzMyIgZD0iTTE5LjA0MiA4Ljg2MWMwIDIuMTUxLjc2NyA0LjEyMyAyLjA0MiA1LjY1OC0uMDIzLS4xNzItLjA0OC0uMzQzLS4wNTktLjUxOGwtLjAxLTQuMDMzYzUuMzYyLS4zMDIgOC41MTMtMi42NzUgMTAuMjAyLTQuNTdDMzEuNzgyIDcuMTcgMzMuMTE0IDEwIDM1Ljg2MSAxMEgzNlYxLjg4N0MzNC45MDYgMS4yMDYgMzMuNjE5LjgwNiAzMi4yMzYuODA2Yy0uMjAxIDAtLjM5OC4wMTQtLjU5NS4wM0MzMC41MDQuMzA1IDI5LjI0IDAgMjcuOTAyIDBjLTQuODk0IDAtOC44NiAzLjk2Ny04Ljg2IDguODYxeiIvPjxwYXRoIGZpbGw9IiNCRjY5NTIiIGQ9Ik0yNS4yNzggMTcuNzIzaDYuNDQ0cy0uODA1IDEuNjEtMy4yMjIgMS42MS0zLjIyMi0xLjYxLTMuMjIyLTEuNjF6TTI4IDE1LjVoMWMuMjc2IDAgLjUtLjIyNC41LS41cy0uMjI0LS41LS41LS41aC0xYy0uMjc2IDAtLjUuMjI0LS41LjVzLjIyNC41LjUuNXoiLz48cGF0aCBmaWxsPSIjNjYyMTEzIiBkPSJNMzIgMTNjLjU1MiAwIDEtLjQ0OCAxLTF2LTFjMC0uNTUyLS40NDgtMS0xLTFzLTEgLjQ0OC0xIDF2MWMwIC41NTIuNDQ4IDEgMSAxem0tNyAwYy41NTIgMCAxLS40NDggMS0xdi0xYzAtLjU1Mi0uNDQ4LTEtMS0xcy0xIC40NDgtMSAxdjFjMCAuNTUyLjQ0OCAxIDEgMXoiLz48cGF0aCBmaWxsPSIjRkZEQzVEIiBkPSJNNSAyMnYtNGg1djRoMnY2SDN2LTZ6Ii8+PHBhdGggZmlsbD0iIzNCOTREOSIgZD0iTTE1IDIyaC01bC0yLjQ3MiAzLjc3OEw1IDIySDB2MTRoMThWMjQuOTczQzE4IDIzIDE3IDIyIDE1IDIyeiIvPjxwYXRoIGZpbGw9IiMxQjYzOTkiIGQ9Ik0xMy45ODYgMjZoMS4wMjh2MTBoLTEuMDI4eiIvPjxwYXRoIGZpbGw9IiNGRkRDNUQiIGQ9Ik0xNSAxNFY1TDAgNHYxMGguMDI0Yy4yNTIgMy44OSAzLjQ5OCA2Ljk2OSA3LjQ3NiA2Ljk2OSAzLjc5MSAwIDYuOTMyLTIuNzk5IDcuNDM4LTYuNDI4LjAyNS0uMTguMDUtLjM1OC4wNjItLjU0MXoiLz48cGF0aCBmaWxsPSIjRkZBQzMzIiBkPSJNMy44MDYuODA2QzIuNDA1LjgwNiAxLjEwMiAxLjIxNSAwIDEuOTEydjguMDgzYzUuOTQ3LS4wNTUgOS4zNzUtMi41OTMgMTEuMTYyLTQuNTk4LjUwNiAxLjU4OSAxLjYzMyA0LjAyMSAzLjgzOCA0LjUxVjE0Yy0uMDEyLjE4My0uMDM3LjM2MS0uMDYyLjU0QzE2LjIyNCAxMy4wMDIgMTcgMTEuMDIzIDE3IDguODYxIDE3IDMuOTY3IDEzLjAzMyAwIDguMTM5IDAgNi44MDEgMCA1LjUzOC4zMDUgNC40LjgzNmMtLjE5Ni0uMDE3LS4zOTQtLjAzLS41OTQtLjAzek0xNSA1di4xNjZsLS4wMjgtLjE2N0wxNSA1eiIvPjxwYXRoIGZpbGw9IiNCRjY5NTIiIGQ9Ik0xMC43MjIgMTcuNzIzSDQuMjc4cy44MDUgMS42MSAzLjIyMiAxLjYxIDMuMjIyLTEuNjEgMy4yMjItMS42MXpNOCAxNS41SDdjLS4yNzYgMC0uNS0uMjI0LS41LS41cy4yMjQtLjUuNS0uNWgxYy4yNzYgMCAuNS4yMjQuNS41cy0uMjI0LjUtLjUuNXoiLz48cGF0aCBmaWxsPSIjNjYyMTEzIiBkPSJNNCAxM2MtLjU1MiAwLTEtLjQ0OC0xLTF2LTFjMC0uNTUyLjQ0OC0xIDEtMXMxIC40NDggMSAxdjFjMCAuNTUyLS40NDggMS0xIDF6bTcgMGMtLjU1MiAwLTEtLjQ0OC0xLTF2LTFjMC0uNTUyLjQ0OC0xIDEtMXMxIC40NDggMSAxdjFjMCAuNTUyLS40NDggMS0xIDF6Ii8+PHBhdGggZmlsbD0iI0Y0OTAwQyIgZD0iTTE5LjQzMiAzMi44NTJjLS4yNjQuNTExLS40MzIuOTIxLS40MzIgMS4xNDggNC42NTYgMCA5LTMgOS0zLTEuMTI1LTMuNjg4LTEuNjY2LTguMTM5LTEuNjY2LTguMTM5IDAtNC4zNDItMy41MTktNy44NjEtNy44NjEtNy44NjEtMS4yNDUgMC0yLjQxOC4yOTctMy40NjQuODEyLTMuMzMuMDctNi4wMDkgMi43ODctNi4wMDkgNi4xMzMgMCAuODI4LjE2NSAxLjYxNy40NjQgMi4zMzdDOS4yMTQgMjUuODkzIDguNzMxIDI4LjYwNCA4IDMxYzAgMCA0LjM0NCAzIDkgMyAwLS4yMjctLjE2Ny0uNjM2LS40MzEtMS4xNDZDMTMuMzkgMzIuMTk4IDExIDI5LjQgMTEgMjYuMDQ3bC4wMDItLjA0N0gxMXYtNmgzLjU1MWMxLjAxOC0uNTc0IDIuMTkyLS45MDYgMy40NDYtLjkwNnMyLjQyOS4zMzIgMy40NDYuOTA2SDI1djZoLS4wMDhsLjAwMi4wNDdjMCAzLjM1MS0yLjM4NiA2LjE0Ny01LjU2MiA2LjgwNXoiLz48cGF0aCBmaWxsPSIjRkZEQzVEIiBkPSJNMjAgMzV2LTRoLTR2NGgtMXYxaDZ2LTF6Ii8+PHBhdGggZmlsbD0iI0VBNTk2RSIgZD0iTTE0IDMzYy0yLjQ5MyAwLTQuMjc2IDEuMzg1LTQuODE0IDNIMThsLTItM2gtMnptOSAwaC0zbC0yIDNoOC44MThjLS41MTgtMS42MDItMi4xNTktMy0zLjgxOC0zeiIvPjxwYXRoIGZpbGw9IiNGRkRDNUQiIGQ9Ik0yNC45OTQgMjYuMDQ3TDI0Ljk5MiAyNkgyNXYtNmgtMy41NTdjLTEuMDE4LS41NzQtMi4xOTItLjkwNi0zLjQ0Ni0uOTA2cy0yLjQyOC4zMzItMy40NDYuOTA2SDExdjZoLjAwMmwtLjAwMi4wNDdjMCAzLjM1MyAyLjM5IDYuMTUyIDUuNTY5IDYuODA3LjQ2MS4wOTUuOTM4LjE0NiAxLjQyOC4xNDYuNDkyIDAgLjk3Mi0uMDUyIDEuNDM1LS4xNDggMy4xNzYtLjY1OCA1LjU2Mi0zLjQ1NCA1LjU2Mi02LjgwNXoiLz48cGF0aCBmaWxsPSIjRjQ5MDBDIiBkPSJNMjYgMjNsLTEtNC0zLTItMTEgMS0uODYxIDVjNS40MSAwIDguNDI1LTEuNzU1IDEwLjA2NS0zLjM2N0MyMS40NCAyMS4wOTIgMjMuNTI4IDIzIDI2IDIzeiIvPjxwYXRoIGZpbGw9IiNCRjY5NTIiIGQ9Ik0yMSAzMGgtNnMuNTgzIDIgMyAyIDMtMiAzLTJ6bS0zLS41Yy0uMTMgMC0uMjYtLjA1LS4zNS0uMTUtLjEtLjA5LS4xNS0uMjItLjE1LS4zNXMuMDUtLjI2LjE1LS4zNmMuMTctLjE3LjUyLS4xOC43MS4wMS4wOS4wOS4xNC4yMi4xNC4zNXMtLjA1LjI2LS4xNS4zNWMtLjA5LjEtLjIyLjE1LS4zNS4xNXoiLz48cGF0aCBmaWxsPSIjNjYyMTEzIiBkPSJNMTUgMjhjLS41NTIgMC0xLS40NDctMS0xdi0xYzAtLjU1My40NDgtMSAxLTFzMSAuNDQ3IDEgMXYxYzAgLjU1My0uNDQ4IDEtMSAxem02IDBjLS41NTMgMC0xLS40NDctMS0xdi0xYzAtLjU1My40NDctMSAxLTEgLjU1MyAwIDEgLjQ0NyAxIDF2MWMwIC41NTMtLjQ0NyAxLTEgMXoiLz48L3N2Zz4=',\n          '👨‍👨‍👧‍👧': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iI0ZGREM1RCIgZD0iTTI2IDIydi00aDV2NGgydjZoLTl2LTZ6Ii8+PHBhdGggZmlsbD0iIzNCOTNEOSIgZD0iTTMxIDIybC0yLjUyOCAzLjc3OEwyNiAyMmgtNWMtMiAwLTMgMS0zIDIuOTczVjM2aDE4VjIyaC01eiIvPjxwYXRoIGZpbGw9IiMxQjYzOTkiIGQ9Ik0yMC45ODYgMjZoMS4wMjh2MTBoLTEuMDI4eiIvPjxwYXRoIGZpbGw9IiNGRkRDNUQiIGQ9Ik0yMSA0bC4wMjQgMTBjLjAxMS4xNzUuMDM2LjM0Ny4wNTkuNTE4LjQ5NCAzLjY0IDMuNjE4IDYuNDUgNy40MTcgNi40NSAzLjk3OCAwIDcuMjIzLTMuMDc5IDcuNDc2LTYuOTY5SDM2VjRIMjF6Ii8+PHBhdGggZmlsbD0iI0ZGQUMzMyIgZD0iTTE5LjA0MiA4Ljg2MWMwIDIuMTUxLjc2NyA0LjEyMyAyLjA0MiA1LjY1OC0uMDIzLS4xNzItLjA0OC0uMzQzLS4wNTktLjUxOGwtLjAxLTQuMDMzYzUuMzYyLS4zMDIgOC41MTMtMi42NzUgMTAuMjAyLTQuNTdDMzEuNzgyIDcuMTcgMzMuMTE0IDEwIDM1Ljg2MSAxMEgzNlYxLjg4N0MzNC45MDYgMS4yMDYgMzMuNjE5LjgwNiAzMi4yMzYuODA2Yy0uMjAxIDAtLjM5OC4wMTQtLjU5NS4wM0MzMC41MDQuMzA1IDI5LjI0IDAgMjcuOTAyIDBjLTQuODk0IDAtOC44NiAzLjk2Ny04Ljg2IDguODYxeiIvPjxwYXRoIGZpbGw9IiNCRjY5NTIiIGQ9Ik0yNS4yNzggMTcuNzIzaDYuNDQ0cy0uODA1IDEuNjEtMy4yMjIgMS42MS0zLjIyMi0xLjYxLTMuMjIyLTEuNjF6TTI4IDE1LjVoMWMuMjc2IDAgLjUtLjIyNC41LS41cy0uMjI0LS41LS41LS41aC0xYy0uMjc2IDAtLjUuMjI0LS41LjVzLjIyNC41LjUuNXoiLz48cGF0aCBmaWxsPSIjNjYyMTEzIiBkPSJNMzIgMTNjLjU1MiAwIDEtLjQ0OCAxLTF2LTFjMC0uNTUyLS40NDgtMS0xLTFzLTEgLjQ0OC0xIDF2MWMwIC41NTIuNDQ4IDEgMSAxem0tNyAwYy41NTIgMCAxLS40NDggMS0xdi0xYzAtLjU1Mi0uNDQ4LTEtMS0xcy0xIC40NDgtMSAxdjFjMCAuNTUyLjQ0OCAxIDEgMXoiLz48cGF0aCBmaWxsPSIjRkZEQzVEIiBkPSJNNSAyMnYtNGg1djRoMnY2SDN2LTZ6Ii8+PHBhdGggZmlsbD0iIzNCOTREOSIgZD0iTTE1IDIyaC01bC0yLjQ3MiAzLjc3OEw1IDIySDB2MTRoMThWMjQuOTczQzE4IDIzIDE3IDIyIDE1IDIyeiIvPjxwYXRoIGZpbGw9IiMxQjYzOTkiIGQ9Ik0xMy45ODYgMjZoMS4wMjh2MTBoLTEuMDI4eiIvPjxwYXRoIGZpbGw9IiNGRkRDNUQiIGQ9Ik0xNSAxNFY1TDAgNHYxMGguMDI0Yy4yNTIgMy44OSAzLjQ5OCA2Ljk2OSA3LjQ3NiA2Ljk2OSAzLjc5MSAwIDYuOTMyLTIuNzk5IDcuNDM4LTYuNDI4LjAyNS0uMTguMDUtLjM1OC4wNjItLjU0MXoiLz48cGF0aCBmaWxsPSIjRkZBQzMzIiBkPSJNMy44MDYuODA2QzIuNDA1LjgwNiAxLjEwMiAxLjIxNSAwIDEuOTEydjguMDgzYzUuOTQ3LS4wNTUgOS4zNzUtMi41OTMgMTEuMTYyLTQuNTk4LjUwNiAxLjU4OSAxLjYzMyA0LjAyMSAzLjgzOCA0LjUxVjE0Yy0uMDEyLjE4My0uMDM3LjM2MS0uMDYyLjU0QzE2LjIyNCAxMy4wMDIgMTcgMTEuMDIzIDE3IDguODYxIDE3IDMuOTY3IDEzLjAzMyAwIDguMTM5IDAgNi44MDEgMCA1LjUzOC4zMDUgNC40LjgzNmMtLjE5Ni0uMDE3LS4zOTQtLjAzLS41OTQtLjAzek0xNSA1di4xNjZsLS4wMjgtLjE2N0wxNSA1eiIvPjxwYXRoIGZpbGw9IiNCRjY5NTIiIGQ9Ik0xMC43MjIgMTcuNzIzSDQuMjc4cy44MDUgMS42MSAzLjIyMiAxLjYxIDMuMjIyLTEuNjEgMy4yMjItMS42MXpNOCAxNS41SDdjLS4yNzYgMC0uNS0uMjI0LS41LS41cy4yMjQtLjUuNS0uNWgxYy4yNzYgMCAuNS4yMjQuNS41cy0uMjI0LjUtLjUuNXoiLz48cGF0aCBmaWxsPSIjNjYyMTEzIiBkPSJNNCAxM2MtLjU1MiAwLTEtLjQ0OC0xLTF2LTFjMC0uNTUyLjQ0OC0xIDEtMXMxIC40NDggMSAxdjFjMCAuNTUyLS40NDggMS0xIDF6bTcgMGMtLjU1MiAwLTEtLjQ0OC0xLTF2LTFjMC0uNTUyLjQ0OC0xIDEtMXMxIC40NDggMSAxdjFjMCAuNTUyLS40NDggMS0xIDF6Ii8+PHBhdGggZmlsbD0iI0ZGREM1RCIgZD0iTTMyLjk5OCAyMi43MTRsLTQuMjg2LS44NTctNy43MTQuODU3VjI3SDIxbC0uMDAyLjA0YzAgMy4yOTEgMi42ODUgNS45NiA1Ljk5OCA1Ljk2IDMuMzEyIDAgNS45OTctMi42NjkgNS45OTctNS45NmwtLjAwMi0uMDRoLjAwN3YtNC4yODZ6Ii8+PHBhdGggZmlsbD0iI0Y0OTAwQyIgZD0iTTMzLjk5NSAyNS4xOTJjMC0uMDIzLjAwMy0uMDQ0LjAwMy0uMDY3bC0uMDAyLS4wMzkuMDAyLS4wODYtLjAwNi4wMDZDMzMuOTI4IDIxLjY3OSAzMS4yMTYgMTkgMjcuODczIDE5Yy0uMjk4IDAtLjU4OC4wMjktLjg3NS4wNy0uMjg2LS4wNDEtLjU3Ny0uMDctLjg3NS0uMDctMy4zODMgMC02LjEyNSAyLjc0Mi02LjEyNSA2LjEyNSAwIDAgLjEyNSAzLjE4OC0xIDYuODc1IDAgMCAzLjM0NCAyIDggMnM4LTIgOC0yYy0uOTY4LTMuMTcyLTEuMDEtNi4wNjEtMS4wMDMtNi44MDh6bS0xLjAwMiAxLjg0OGMwIDMuMjkxLTIuNjg1IDUuOTYtNS45OTcgNS45NnMtNS45OTctMi42NjktNS45OTctNS45NmMtLjAwMS0uMDEzLjAwMS0uMDI3LjAwMS0uMDRoLS4wMDJ2LTEuOTExYzQuMTcyLS4xODEgNi41ODgtMS41OTIgNy45MjktMi45MS45MSAxLjA3NCAyLjM1MSAyLjQxNCA0LjA3MSAyLjgxNlYyN2gtLjAwN2wuMDAyLjA0eiIvPjxwYXRoIGZpbGw9IiNGRkRDNUQiIGQ9Ik0yOC45OTggMzR2LTNoLTR2M2gtMXYyaDZ2LTJ6Ii8+PHBhdGggZmlsbD0iI0VBNTk2RSIgZD0iTTMxLjk5OCAzM2gtM2wtMiAyLTItMmgtMmMtMi40OTMgMC00LjI3NiAxLjM4NS00LjgxNCAzaDE3LjYzMmMtLjUxOC0xLjYwMi0yLjE1OS0zLTMuODE4LTN6Ii8+PHBhdGggZmlsbD0iI0JGNjk1MiIgZD0iTTI4Ljk5OCAzMGgtNHMuMzg5IDEuMzMzIDIgMS4zMzMgMi0xLjMzMyAyLTEuMzMzem0tMi0uNWMtLjEzIDAtLjI2LS4wNS0uMzUtLjE1LS4xLS4wOS0uMTUtLjIyLS4xNS0uMzVzLjA1LS4yNi4xNS0uMzZjLjE3LS4xNy41Mi0uMTguNzEuMDEuMDkuMDkxLjE0LjIyMS4xNC4zNTFzLS4wNS4yNi0uMTUuMzVjLS4wOS4wOTktLjIyLjE0OS0uMzUuMTQ5eiIvPjxwYXRoIGZpbGw9IiM2NjIxMTMiIGQ9Ik0yOC45OTggMjhjLS40NjEgMC0uODMzLS4zNzMtLjgzMy0uODMzdi0uODMzYzAtLjQ2MS4zNzMtLjgzMy44MzMtLjgzMy40NjEgMCAuODMzLjM3My44MzMuODMzdi44MzNjLjAwMS40Ni0uMzcyLjgzMy0uODMzLjgzM3ptLTQgMGMtLjQ2MSAwLS44MzMtLjM3My0uODMzLS44MzN2LS44MzNjMC0uNDYxLjM3My0uODMzLjgzMy0uODMzLjQ2MSAwIC44MzMuMzczLjgzMy44MzN2LjgzM2MuMDAxLjQ2LS4zNzIuODMzLS44MzMuODMzeiIvPjxwYXRoIGZpbGw9IiNGRkRDNUQiIGQ9Ik0xNC45OTggMjIuNzE0bC00LjI4Ni0uODU3LTcuNzE0Ljg1N1YyN0gzbC0uMDAyLjA0YzAgMy4yOTEgMi42ODUgNS45NiA1Ljk5OCA1Ljk2IDMuMzEyIDAgNS45OTctMi42NjkgNS45OTctNS45NmwtLjAwMi0uMDRoLjAwN3YtNC4yODZ6Ii8+PHBhdGggZmlsbD0iI0Y0OTAwQyIgZD0iTTE1Ljk5NSAyNS4xOTJjMC0uMDIzLjAwMy0uMDQ0LjAwMy0uMDY3bC0uMDAyLS4wMzkuMDAyLS4wODYtLjAwNi4wMDZDMTUuOTI4IDIxLjY3OSAxMy4yMTYgMTkgOS44NzMgMTljLS4yOTggMC0uNTg4LjAyOS0uODc1LjA3LS4yODYtLjA0MS0uNTc3LS4wNy0uODc1LS4wNy0zLjM4MyAwLTYuMTI1IDIuNzQyLTYuMTI1IDYuMTI1IDAgMCAuMTI1IDMuMTg4LTEgNi44NzUgMCAwIDMuMzQ0IDIgOCAyczgtMiA4LTJjLS45NjgtMy4xNzItMS4wMS02LjA2MS0xLjAwMy02LjgwOHptLTEuMDAyIDEuODQ4YzAgMy4yOTEtMi42ODUgNS45Ni01Ljk5NyA1Ljk2cy01Ljk5Ny0yLjY2OS01Ljk5Ny01Ljk2QzIuOTk4IDI3LjAyNyAzIDI3LjAxMyAzIDI3aC0uMDAydi0xLjkxMWM0LjE3Mi0uMTgxIDYuNTg4LTEuNTkyIDcuOTI5LTIuOTEuOTEgMS4wNzQgMi4zNTEgMi40MTQgNC4wNzEgMi44MTZWMjdoLS4wMDdsLjAwMi4wNHoiLz48cGF0aCBmaWxsPSIjRkZEQzVEIiBkPSJNMTAuOTk4IDM0di0zaC00djNoLTF2Mmg2di0yeiIvPjxwYXRoIGZpbGw9IiNFQTU5NkUiIGQ9Ik0xMy45OTggMzNoLTNsLTIgMi0yLTJoLTJDMi41MDUgMzMgLjcyMiAzNC4zODUuMTg0IDM2aDE3LjYzMmMtLjUxOC0xLjYwMi0yLjE1OS0zLTMuODE4LTN6Ii8+PHBhdGggZmlsbD0iI0JGNjk1MiIgZD0iTTEwLjk5OCAzMGgtNHMuMzg5IDEuMzMzIDIgMS4zMzMgMi0xLjMzMyAyLTEuMzMzem0tMi0uNWMtLjEzIDAtLjI2LS4wNS0uMzUtLjE1LS4xLS4wOS0uMTUtLjIyLS4xNS0uMzVzLjA1LS4yNi4xNS0uMzZjLjE3LS4xNy41Mi0uMTguNzEuMDEuMDkuMDkxLjE0LjIyMS4xNC4zNTFzLS4wNS4yNi0uMTUuMzVjLS4wOS4wOTktLjIyLjE0OS0uMzUuMTQ5eiIvPjxwYXRoIGZpbGw9IiM2NjIxMTMiIGQ9Ik0xMC45OTggMjhjLS40NjEgMC0uODMzLS4zNzMtLjgzMy0uODMzdi0uODMzYzAtLjQ2MS4zNzMtLjgzMy44MzMtLjgzMy40NjEgMCAuODMzLjM3My44MzMuODMzdi44MzNjLjAwMS40Ni0uMzcyLjgzMy0uODMzLjgzM3ptLTQgMGMtLjQ2MSAwLS44MzMtLjM3My0uODMzLS44MzN2LS44MzNjMC0uNDYxLjM3My0uODMzLjgzMy0uODMzLjQ2MSAwIC44MzMuMzczLjgzMy44MzN2LjgzM2MuMDAxLjQ2LS4zNzIuODMzLS44MzMuODMzeiIvPjwvc3ZnPg==',\n        },\n      }\n    )\n\n    expect(await toImage(svg)).toMatchImageSnapshot()\n  })\n\n  it('should render emojis correctly with alphabetic emoji', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n        }}\n      >\n        <div>⚡Hello⚡</div>\n        <div>⚡ Hello ⚡</div>\n        <div>🚀Hello🚀</div>\n        <div>🚀 Hello 🚀</div>\n      </div>,\n      {\n        width: 200,\n        height: 100,\n        fonts,\n        graphemeImages: {\n          '⚡': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iI0ZGQUMzMyIgZD0iTTMyLjkzOCAxNS42NTFDMzIuNzkyIDE1LjI2IDMyLjQxOCAxNSAzMiAxNUgxOS45MjVMMjYuODkgMS40NThjLjIxOS0uNDI2LjEwNi0uOTQ3LS4yNzEtMS4yNDNDMjYuNDM3LjA3MSAyNi4yMTggMCAyNiAwYy0uMjMzIDAtLjQ2Ni4wODItLjY1My4yNDNMMTggNi41ODggMy4zNDcgMTkuMjQzYy0uMzE2LjI3My0uNDMuNzE0LS4yODQgMS4xMDVTMy41ODIgMjEgNCAyMWgxMi4wNzVMOS4xMSAzNC41NDJjLS4yMTkuNDI2LS4xMDYuOTQ3LjI3MSAxLjI0My4xODIuMTQ0LjQwMS4yMTUuNjE5LjIxNS4yMzMgMCAuNDY2LS4wODIuNjUzLS4yNDNMMTggMjkuNDEybDE0LjY1My0xMi42NTVjLjMxNy0uMjczLjQzLS43MTQuMjg1LTEuMTA2eiIvPjwvc3ZnPg==',\n          '🚀': 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iI0EwMDQxRSIgZD0iTTEgMTdsOC03IDE2IDEgMSAxNi03IDhzLjAwMS01Ljk5OS02LTEyLTEyLTYtMTItNnoiLz48cGF0aCBmaWxsPSIjRkZBQzMzIiBkPSJNLjk3MyAzNXMtLjAzNi03Ljk3OSAyLjk4NS0xMVMxNSAyMS4xODcgMTUgMjEuMTg3IDE0Ljk5OSAyOSAxMS45OTkgMzJjLTMgMy0xMS4wMjYgMy0xMS4wMjYgM3oiLz48Y2lyY2xlIGZpbGw9IiNGRkNDNEQiIGN4PSI4Ljk5OSIgY3k9IjI3IiByPSI0Ii8+PHBhdGggZmlsbD0iIzU1QUNFRSIgZD0iTTM1Ljk5OSAwcy0xMCAwLTIyIDEwYy02IDUtNiAxNC00IDE2czExIDIgMTYtNGMxMC0xMiAxMC0yMiAxMC0yMnoiLz48cGF0aCBkPSJNMjYuOTk5IDVjLTEuNjIzIDAtMy4wMTMuOTcxLTMuNjQxIDIuMzYuNTAyLS4yMjcgMS4wNTUtLjM2IDEuNjQxLS4zNiAyLjIwOSAwIDQgMS43OTEgNCA0IDAgLjU4Ni0uMTMzIDEuMTM5LS4zNTkgMS42NCAxLjM4OS0uNjI3IDIuMzU5LTIuMDE3IDIuMzU5LTMuNjQgMC0yLjIwOS0xLjc5MS00LTQtNHoiLz48cGF0aCBmaWxsPSIjQTAwNDFFIiBkPSJNOCAyOHMwLTQgMS01IDEzLjAwMS0xMC45OTkgMTQtMTAtOS4wMDEgMTMtMTAuMDAxIDE0UzggMjggOCAyOHoiLz48L3N2Zz4=',\n        },\n      }\n    )\n\n    expect(await toImage(svg)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/error.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Error', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should throw if flex missing on div that has children', async () => {\n    let error = new Error()\n    try {\n      await satori(\n        <div>\n          Test <span>satori</span> with space\n        </div>,\n        {\n          width: 10,\n          height: 10,\n          fonts,\n        }\n      )\n    } catch (err) {\n      error = err\n    }\n    expect(error?.message).toBe(\n      'Expected <div> to have explicit \"display: flex\", \"display: contents\", or \"display: none\" if it has more than one child node.'\n    )\n  })\n\n  it('should throw if display inline-block on div that has children', async () => {\n    const result = satori(\n      <div style={{ display: 'inline-block' }}>\n        Test <span>satori</span> with space\n      </div>,\n      {\n        width: 10,\n        height: 10,\n        fonts,\n      }\n    )\n    expect(result).rejects.toThrowError(\n      `Invalid value for CSS property \"display\". Allowed values: \"flex\" | \"block\" | \"contents\" | \"none\" | \"-webkit-box\". Received: \"inline-block\".`\n    )\n  })\n\n  it('should throw if using invalid values', async () => {\n    const result = satori(\n      // @ts-expect-error\n      <div style={{ position: 'fixed ' }}>Test</div>,\n      {\n        width: 10,\n        height: 10,\n        fonts,\n      }\n    )\n    expect(result).rejects.toThrowError(\n      `Invalid value for CSS property \"position\". Allowed values: \"absolute\" | \"relative\" | \"static\". Received: \"fixed\".`\n    )\n  })\n\n  it('should not throw if display none on div that has children', async () => {\n    const svg = await satori(\n      <div style={{ display: 'none' }}>\n        Test <span>satori</span> with space\n      </div>,\n      {\n        width: 10,\n        height: 10,\n        fonts,\n      }\n    )\n    expect(typeof svg).toBe('string')\n  })\n\n  it('should not throw if flex missing on span that has children', async () => {\n    const svg = await satori(\n      <span>\n        Test <span>satori</span> with space\n      </span>,\n      {\n        width: 10,\n        height: 10,\n        fonts,\n      }\n    )\n    expect(typeof svg).toBe('string')\n  })\n\n  it('should not throw if flex missing on div without children', async () => {\n    const svg = await satori(<div></div>, {\n      width: 10,\n      height: 10,\n      fonts,\n    })\n    expect(typeof svg).toBe('string')\n  })\n\n  it('should not allowed to set negative value to rg-size', async () => {\n    const result = satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundImage:\n            'radial-gradient(-20% 20% at top left, yellow, blue)',\n          fontSize: 32,\n          fontWeight: 600,\n        }}\n      ></div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n\n    expect(result).rejects.toThrowError(\n      'disallow setting negative values to the size of the shape. Check https://w3c.github.io/csswg-drafts/css-images/#valdef-rg-size-length-0'\n    )\n  })\n})\n"
  },
  {
    "path": "test/event.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Event', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should trigger the onNodeDetected callback', async () => {\n    const nodes = []\n    await satori(\n      <div style={{ width: '100%', height: 50, display: 'flex' }}>\n        <div>Hello</div>\n        <div>World</div>\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n        onNodeDetected: (node) => {\n          nodes.push(node)\n        },\n      }\n    )\n    expect(nodes).toMatchInlineSnapshot(`\n      [\n        {\n          \"height\": 50,\n          \"key\": null,\n          \"left\": 0,\n          \"props\": {\n            \"style\": {\n              \"display\": \"flex\",\n              \"height\": 50,\n              \"width\": \"100%\",\n            },\n          },\n          \"textContent\": undefined,\n          \"top\": 0,\n          \"type\": \"div\",\n          \"width\": 100,\n        },\n        {\n          \"height\": 50,\n          \"key\": null,\n          \"left\": 0,\n          \"props\": {},\n          \"textContent\": \"Hello\",\n          \"top\": 0,\n          \"type\": \"div\",\n          \"width\": 37,\n        },\n        {\n          \"height\": 50,\n          \"key\": null,\n          \"left\": 37,\n          \"props\": {},\n          \"textContent\": \"World\",\n          \"top\": 0,\n          \"type\": \"div\",\n          \"width\": 42,\n        },\n      ]\n    `)\n  })\n})\n"
  },
  {
    "path": "test/flexbox-advanced.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Flexbox Advanced', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  describe('flex-grow', () => {\n    it('should render elements with flex-grow', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div\n            style={{\n              flexGrow: 1,\n              background: 'red',\n              height: 50,\n            }}\n          />\n          <div\n            style={{\n              flexGrow: 2,\n              background: 'blue',\n              height: 50,\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render with different flex-grow ratios', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div\n            style={{\n              flexGrow: 1,\n              background: 'red',\n              height: 50,\n            }}\n          />\n          <div\n            style={{\n              flexGrow: 3,\n              background: 'blue',\n              height: 50,\n            }}\n          />\n          <div\n            style={{\n              flexGrow: 1,\n              background: 'green',\n              height: 50,\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('flex-shrink', () => {\n    it('should render elements with flex-shrink', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div\n            style={{\n              width: 80,\n              flexShrink: 1,\n              background: 'red',\n              height: 50,\n            }}\n          />\n          <div\n            style={{\n              width: 80,\n              flexShrink: 0,\n              background: 'blue',\n              height: 50,\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render with different flex-shrink values', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div\n            style={{\n              width: 60,\n              flexShrink: 2,\n              background: 'red',\n              height: 50,\n            }}\n          />\n          <div\n            style={{\n              width: 60,\n              flexShrink: 1,\n              background: 'blue',\n              height: 50,\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('flex-basis', () => {\n    it('should render elements with flex-basis', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div\n            style={{\n              flexBasis: '40px',\n              background: 'red',\n              height: 50,\n            }}\n          />\n          <div\n            style={{\n              flexBasis: '60px',\n              background: 'blue',\n              height: 50,\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render flex-basis with flex-grow', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div\n            style={{\n              flexBasis: '20px',\n              flexGrow: 1,\n              background: 'red',\n              height: 50,\n            }}\n          />\n          <div\n            style={{\n              flexBasis: '30px',\n              flexGrow: 2,\n              background: 'blue',\n              height: 50,\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('flex shorthand', () => {\n    it('should render with flex: 1', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div\n            style={{\n              flex: 1,\n              background: 'red',\n              height: 50,\n            }}\n          />\n          <div\n            style={{\n              flex: 1,\n              background: 'blue',\n              height: 50,\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render with different flex values', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div\n            style={{\n              flex: 2,\n              background: 'red',\n              height: 50,\n            }}\n          />\n          <div\n            style={{\n              flex: 1,\n              background: 'blue',\n              height: 50,\n            }}\n          />\n          <div\n            style={{\n              flex: 1,\n              background: 'green',\n              height: 50,\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('align-self', () => {\n    it('should render alignSelf flex-start', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            alignItems: 'center',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div\n            style={{\n              width: 30,\n              height: 30,\n              background: 'red',\n            }}\n          />\n          <div\n            style={{\n              width: 30,\n              height: 30,\n              background: 'blue',\n              alignSelf: 'flex-start',\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render alignSelf flex-end', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            alignItems: 'flex-start',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div\n            style={{\n              width: 30,\n              height: 30,\n              background: 'red',\n            }}\n          />\n          <div\n            style={{\n              width: 30,\n              height: 30,\n              background: 'blue',\n              alignSelf: 'flex-end',\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render alignSelf center', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            alignItems: 'flex-start',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div\n            style={{\n              width: 30,\n              height: 30,\n              background: 'red',\n            }}\n          />\n          <div\n            style={{\n              width: 30,\n              height: 30,\n              background: 'blue',\n              alignSelf: 'center',\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render alignSelf stretch', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            alignItems: 'flex-start',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div\n            style={{\n              width: 30,\n              height: 30,\n              background: 'red',\n            }}\n          />\n          <div\n            style={{\n              width: 30,\n              background: 'blue',\n              alignSelf: 'stretch',\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('align-content', () => {\n    it('should render alignContent flex-start', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            flexWrap: 'wrap',\n            alignContent: 'flex-start',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div style={{ width: 60, height: 30, background: 'red' }} />\n          <div style={{ width: 60, height: 30, background: 'blue' }} />\n          <div style={{ width: 60, height: 30, background: 'green' }} />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render alignContent center', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            flexWrap: 'wrap',\n            alignContent: 'center',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div style={{ width: 60, height: 20, background: 'red' }} />\n          <div style={{ width: 60, height: 20, background: 'blue' }} />\n          <div style={{ width: 60, height: 20, background: 'green' }} />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render alignContent space-between', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            flexWrap: 'wrap',\n            alignContent: 'space-between',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div style={{ width: 60, height: 20, background: 'red' }} />\n          <div style={{ width: 60, height: 20, background: 'blue' }} />\n          <div style={{ width: 60, height: 20, background: 'green' }} />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('complex layouts', () => {\n    it('should render nested flex containers', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'column',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div\n            style={{\n              display: 'flex',\n              flexDirection: 'row',\n              flex: 1,\n              background: 'lightblue',\n            }}\n          >\n            <div style={{ flex: 1, background: 'red' }} />\n            <div style={{ flex: 2, background: 'blue' }} />\n          </div>\n          <div\n            style={{\n              display: 'flex',\n              flexDirection: 'row',\n              flex: 1,\n              background: 'lightgreen',\n            }}\n          >\n            <div style={{ flex: 2, background: 'green' }} />\n            <div style={{ flex: 1, background: 'yellow' }} />\n          </div>\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render flex with gap and wrapping', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            flexWrap: 'wrap',\n            gap: 10,\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div style={{ width: 35, height: 35, background: 'red' }} />\n          <div style={{ width: 35, height: 35, background: 'blue' }} />\n          <div style={{ width: 35, height: 35, background: 'green' }} />\n          <div style={{ width: 35, height: 35, background: 'yellow' }} />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should combine flex-grow and flex-shrink', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div\n            style={{\n              width: 60,\n              flexGrow: 1,\n              flexShrink: 2,\n              background: 'red',\n              height: 50,\n            }}\n          />\n          <div\n            style={{\n              width: 60,\n              flexGrow: 2,\n              flexShrink: 1,\n              background: 'blue',\n              height: 50,\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render complex flex layout with multiple properties', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            justifyContent: 'space-between',\n            alignItems: 'center',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n            padding: 10,\n          }}\n        >\n          <div\n            style={{\n              width: 20,\n              height: 20,\n              background: 'red',\n              flexShrink: 0,\n            }}\n          />\n          <div\n            style={{\n              flexGrow: 1,\n              height: 40,\n              background: 'blue',\n              margin: '0 5px',\n            }}\n          />\n          <div\n            style={{\n              width: 20,\n              height: 60,\n              background: 'green',\n              flexShrink: 0,\n              alignSelf: 'flex-end',\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render gap with percentage values', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            flexWrap: 'wrap',\n            gap: '5%',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div style={{ width: 20, height: 20, background: 'red' }} />\n          <div style={{ width: 20, height: 20, background: 'blue' }} />\n          <div style={{ width: 20, height: 20, background: 'green' }} />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render row-gap with percentage values', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            flexWrap: 'wrap',\n            rowGap: '10%',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div style={{ width: 60, height: 20, background: 'red' }} />\n          <div style={{ width: 60, height: 20, background: 'blue' }} />\n          <div style={{ width: 60, height: 20, background: 'green' }} />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render column-gap with percentage values', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'row',\n            columnGap: '10%',\n            width: 100,\n            height: 100,\n            background: 'lightgray',\n          }}\n        >\n          <div style={{ width: 20, height: 40, background: 'red' }} />\n          <div style={{ width: 20, height: 40, background: 'blue' }} />\n          <div style={{ width: 20, height: 40, background: 'green' }} />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n})\n"
  },
  {
    "path": "test/font.test.tsx",
    "content": "import { join } from 'node:path'\nimport { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\nimport { readFile } from 'node:fs/promises'\n\ndescribe('Font', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should error when no font is specified', async () => {\n    try {\n      await satori(<div>hello</div>, {\n        width: 100,\n        height: 100,\n        fonts: [],\n      })\n    } catch (e) {\n      expect(e.message).toMatchInlineSnapshot(\n        '\"No fonts are loaded. At least one font is required to calculate the layout.\"'\n      )\n    }\n  })\n\n  it('should not error when no font is specified and no text rendered', async () => {\n    const svg = await satori(<div></div>, {\n      width: 100,\n      height: 100,\n      fonts: [],\n    })\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should use correct fonts', async () => {\n    const fontName = 'MontserratSubrayada'\n    const fontPath = join(\n      process.cwd(),\n      'test',\n      'assets',\n      `${fontName}-Regular.ttf`\n    )\n    const fontData = await readFile(fontPath)\n    const montserratFont = {\n      name: fontName,\n      data: fontData,\n      weight: 400,\n      style: 'normal',\n    }\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'column',\n          fontSize: '20px',\n          fontWeight: 400,\n          color: 'yellow',\n        }}\n      >\n        <div>Hello</div>\n        <div style={{ fontFamily: fontName }}>Hello</div>\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts: fonts.concat(montserratFont),\n      }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  describe('font-size', () => {\n    it('should allow font-size to be 0', async () => {\n      const svg = await satori(<div style={{ fontSize: 0 }}>hi</div>, {\n        width: 100,\n        height: 100,\n        fonts,\n      })\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  it('should handle font-family fallback', async () => {\n    const fontName = 'MontserratSubrayada'\n    const fontPath = join(\n      process.cwd(),\n      'test',\n      'assets',\n      `${fontName}-Regular.ttf`\n    )\n    const fontData = await readFile(fontPath)\n    const montserratFont = {\n      name: fontName,\n      data: fontData,\n    }\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: '3rem',\n        }}\n      >\n        Hello\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts: [montserratFont],\n      }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should handle font-size correctly for element like heading', async () => {\n    const svgs = await Promise.all(\n      [20, '0.8em', '1.2rem'].map((fontSize) =>\n        satori(\n          <div\n            style={{\n              height: '100%',\n              width: '100%',\n              display: 'flex',\n              flexDirection: 'column',\n              alignItems: 'center',\n              justifyContent: 'center',\n              backgroundColor: '#fff',\n              fontSize,\n              fontWeight: 600,\n            }}\n          >\n            <h1 style={{ color: 'red' }}>Hello, World</h1>\n            <h2 style={{ color: 'orange' }}>Hello, World</h2>\n            <h5 style={{ color: 'grey', fontSize: 20 }}>Hello, World</h5>\n          </div>,\n          {\n            width: 100,\n            height: 100,\n            fonts,\n          }\n        )\n      )\n    )\n\n    svgs.forEach((svg) => {\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  it('should handle escape html when embedFont is false', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: '16px',\n        }}\n      >\n        {`Hello<>&'\" world`}\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n        embedFont: false,\n      }\n    )\n\n    expect(toImage(svg)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/gap.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { toImage } from './utils.js'\nimport satori from '../src/index.js'\n\nconst items = [\n  'red',\n  'red',\n  'red',\n  'green',\n  'green',\n  'green',\n  'blue',\n  'blue',\n  'blue',\n]\n\ndescribe('flex gap', () => {\n  it('should support gap', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexWrap: 'wrap',\n          backgroundColor: '#e2e2e2',\n          gap: 30,\n        }}\n      >\n        {items.map((color, index) => (\n          <div\n            key={index}\n            style={{\n              width: 10,\n              height: 10,\n              backgroundColor: color,\n            }}\n          ></div>\n        ))}\n      </div>,\n      { width: 100, height: 100, fonts: [] }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support rowGap and columnGap', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexWrap: 'wrap',\n          backgroundColor: '#e2e2e2',\n          rowGap: 60,\n          columnGap: 80,\n        }}\n      >\n        {items.slice(0, 4).map((color, index) => (\n          <div\n            key={index}\n            style={{\n              width: 10,\n              height: 10,\n              backgroundColor: color,\n            }}\n          ></div>\n        ))}\n      </div>,\n      { width: 100, height: 100, fonts: [] }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support percentage values as gap', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexWrap: 'wrap',\n          backgroundColor: '#e2e2e2',\n          gap: '10%',\n        }}\n      >\n        {items.map((color, index) => (\n          <div\n            key={index}\n            style={{\n              width: 10,\n              height: 10,\n              backgroundColor: color,\n            }}\n          ></div>\n        ))}\n      </div>,\n      { width: 100, height: 100, fonts: [] }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/gradient.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Gradient', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  describe('linear-gradient', () => {\n    it('should support linear-gradient', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            backgroundColor: 'white',\n            backgroundImage: 'linear-gradient(to right, red, blue)',\n            height: '100%',\n            width: '100%',\n          }}\n        ></div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support repeating linear-gradient', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            backgroundColor: 'white',\n            backgroundImage: 'linear-gradient(45deg, white, blue)',\n            backgroundSize: '50px 50px',\n            height: '100%',\n            width: '100%',\n          }}\n        ></div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support linear-gradient with transparency', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            backgroundColor: 'green',\n            backgroundImage: 'linear-gradient(45deg, rgba(255, 0, 0, 0), blue)',\n            height: '100%',\n            width: '100%',\n          }}\n        ></div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support linear-gradient with omitted orientation', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            backgroundColor: 'green',\n            backgroundImage: 'linear-gradient(red, blue)',\n            height: '100%',\n            width: '100%',\n          }}\n        ></div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support using background instead of backgroundImage', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            background: 'linear-gradient(to right, red, black)',\n            height: '100%',\n            width: '100%',\n          }}\n        ></div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support multiple direction keywords', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            background: 'linear-gradient(to right top, red, blue)',\n            height: '100%',\n            width: '100%',\n          }}\n        ></div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support other degree unit', async () => {\n      const svgs = await Promise.all(\n        [\n          'linear-gradient(0.5turn, red, blue)',\n          `linear-gradient(${Math.PI}rad, red, blue)`,\n          `linear-gradient(200grad, red, blue)`,\n        ].map((background) =>\n          satori(\n            <div\n              style={{\n                background,\n                height: '100%',\n                width: '100%',\n              }}\n            ></div>,\n            {\n              width: 100,\n              height: 100,\n              fonts,\n            }\n          )\n        )\n      )\n\n      for (const svg of svgs) {\n        expect(toImage(svg, 100)).toMatchImageSnapshot()\n      }\n    })\n  })\n\n  describe('radial-gradient', () => {\n    it('should support radial-gradient', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            backgroundColor: 'white',\n            backgroundImage: 'radial-gradient(circle at 25px 25px, blue, red)',\n            backgroundSize: '100px 100px',\n            height: '100%',\n            width: '100%',\n          }}\n        ></div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should make sense if x of y is zero', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            backgroundColor: 'white',\n            backgroundImage: 'radial-gradient(circle at 0px 0%, blue, red)',\n            backgroundSize: '100px 100px',\n            height: '100%',\n            width: '100%',\n          }}\n        ></div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support radial-gradient with unspecified <ending-shape>', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            height: '100%',\n            width: '100%',\n            display: 'flex',\n            alignItems: 'center',\n            justifyContent: 'center',\n          }}\n        >\n          <div\n            style={{\n              backgroundColor: 'rgb(225, 168, 211)',\n              height: '100%',\n              width: '100%',\n              backgroundImage:\n                'radial-gradient(at 3% 42%, rgb(228, 105, 236) 0px, transparent 50%)',\n            }}\n          ></div>\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support default value', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            backgroundColor: 'white',\n            backgroundImage: 'radial-gradient(blue, red)',\n            backgroundSize: '100px 100px',\n            height: '100%',\n            width: '100%',\n          }}\n        ></div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support releative unit', async () => {\n      const svgs = await Promise.all(\n        [\n          'radial-gradient(ellipse at 1em 25px,blue, red)',\n          'radial-gradient(circle at 1rem 25px,blue, red)',\n          'radial-gradient(circle at 2vw 25px,blue, red)',\n          'radial-gradient(circle at 1vh 50%,blue, red)',\n        ].map((backgroundImage) =>\n          satori(\n            <div\n              style={{\n                backgroundColor: 'white',\n                backgroundImage,\n                backgroundSize: '100px 100px',\n                height: '100%',\n                width: '100%',\n              }}\n            ></div>,\n            {\n              width: 100,\n              height: 100,\n              fonts,\n            }\n          )\n        )\n      )\n      svgs.forEach((svg) => {\n        expect(toImage(svg, 100)).toMatchImageSnapshot()\n      })\n    })\n\n    it('should support rg-size with rg-extent-keyword', async () => {\n      const svgs = await Promise.all(\n        [\n          'radial-gradient(closest-corner at 50% 50%, yellow, green)',\n          'radial-gradient(farthest-side at left bottom, red, yellow 50px, green)',\n          'radial-gradient(closest-side at 20px 30px, red, yellow, green)',\n        ].map((backgroundImage) =>\n          satori(\n            <div\n              style={{\n                backgroundColor: 'white',\n                backgroundImage,\n                backgroundSize: '100px 100px',\n                height: '100%',\n                width: '100%',\n              }}\n            ></div>,\n            {\n              width: 100,\n              height: 100,\n              fonts,\n            }\n          )\n        )\n      )\n\n      svgs.forEach((svg) => {\n        expect(toImage(svg, 100)).toMatchImageSnapshot()\n      })\n    })\n\n    it('should support explicitly setting rg-size', async () => {\n      const svgs = await Promise.all(\n        [\n          'radial-gradient(20% 20% at top left, yellow, blue)',\n          'radial-gradient(30px at top left, yellow, blue)',\n        ].map((backgroundImage) =>\n          satori(\n            <div\n              style={{\n                backgroundColor: 'white',\n                backgroundImage,\n                backgroundSize: '100px 100px',\n                height: '100%',\n                width: '100%',\n              }}\n            ></div>,\n            {\n              width: 100,\n              height: 100,\n              fonts,\n            }\n          )\n        )\n      )\n\n      svgs.forEach((svg) => {\n        expect(toImage(svg, 100)).toMatchImageSnapshot()\n      })\n    })\n  })\n\n  it('should support advanced usage', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          backgroundColor: 'yellow',\n          backgroundImage:\n            'radial-gradient(circle at 45px 45px, red, transparent 40%), radial-gradient(circle at 5px 5px, blue, transparent 40%)',\n          backgroundSize: '50px 50px',\n          backgroundRepeat: 'repeat-y',\n          height: '100%',\n          width: '100%',\n        }}\n      ></div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should resolve gradient layers in the correct order', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          backgroundColor: 'yellow',\n          backgroundImage:\n            'radial-gradient(circle at 45px 45px, red, red 60%, transparent 60%), radial-gradient(circle at 5px 5px, blue, blue 60%, transparent 60%)',\n          backgroundSize: '50px 50px',\n          backgroundRepeat: 'repeat-y',\n          height: '100%',\n          width: '100%',\n        }}\n      ></div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render gradient patterns in the correct object space', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n        }}\n      >\n        <div\n          style={{\n            width: '50%',\n            height: '50%',\n            backgroundImage: 'linear-gradient(to bottom, red, blue)',\n          }}\n        ></div>\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should calculate the gradient angle and length correctly', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          backgroundColor: 'blue',\n          backgroundImage:\n            'linear-gradient(-15deg, green 20px, transparent 10px), linear-gradient(to top, red 10px, transparent 10px), linear-gradient(to left, red 10px, transparent 10px), linear-gradient(470deg, orange 10px, transparent 10px), linear-gradient(-470deg, black 30px, transparent 10px)',\n        }}\n      ></div>,\n      {\n        width: 300,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should calculate the gradient angle and length correctly with offset', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          backgroundColor: 'blue',\n          backgroundImage:\n            'linear-gradient(-15deg, green 20px, transparent 10px), linear-gradient(to top, red 10px, transparent 10px), linear-gradient(to left, red 10px, transparent 10px), linear-gradient(470deg, orange 10px, transparent 10px), linear-gradient(-470deg, black 30px, transparent 10px)',\n          backgroundSize: '100px 50px',\n          backgroundPosition: '25px 25px',\n        }}\n      ></div>,\n      {\n        width: 200,\n        height: 300,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should be able to render grid backgrounds', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          textAlign: 'center',\n          alignItems: 'center',\n          justifyContent: 'center',\n          flexDirection: 'column',\n          flexWrap: 'nowrap',\n          backgroundColor: 'white',\n          backgroundImage:\n            'linear-gradient(#222222 1px , transparent 1px ),linear-gradient(to right, #222222 1px , transparent 1px )',\n          backgroundSize: '100px 100px',\n        }}\n      ></div>,\n      {\n        width: 300,\n        height: 300,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 300)).toMatchImageSnapshot()\n  })\n\n  describe('repeating-linear-gradient', async () => {\n    it('should support repeating-linear-gradient', async () => {\n      const svgs = await Promise.all(\n        [\n          'repeating-linear-gradient(to right, red, blue 50%)',\n          'repeating-linear-gradient(to right top, red, blue 30%)',\n        ].map((backgroundImage) =>\n          satori(\n            <div\n              style={{\n                backgroundColor: 'white',\n                backgroundImage,\n                width: '100%',\n                height: '100%',\n              }}\n            ></div>,\n            {\n              width: 100,\n              height: 100,\n              fonts,\n            }\n          )\n        )\n      )\n      svgs.forEach((svg) => {\n        expect(toImage(svg, 100)).toMatchImageSnapshot()\n      })\n    })\n\n    it('should support degree', async () => {\n      const svgs = await Promise.all(\n        [\n          'repeating-linear-gradient(30deg, red, blue 50%)',\n          'repeating-linear-gradient(150deg, red, blue 30%)',\n          'repeating-linear-gradient(-15deg, red, blue 30%)',\n          'repeating-linear-gradient(210deg, red, blue 30%)',\n        ].map((backgroundImage) =>\n          satori(\n            <div\n              style={{\n                backgroundColor: 'white',\n                backgroundImage,\n                width: '100%',\n                height: '100%',\n              }}\n            ></div>,\n            {\n              width: 200,\n              height: 100,\n              fonts,\n            }\n          )\n        )\n      )\n\n      svgs.forEach((svg) => {\n        expect(toImage(svg, 100)).toMatchImageSnapshot()\n      })\n    })\n\n    it('should support background-size and background-repeat', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            backgroundColor: 'white',\n            backgroundImage: 'repeating-linear-gradient(30deg, red, blue 30%)',\n            backgroundSize: '50px 25px',\n            height: '100%',\n            width: '100%',\n          }}\n        ></div>,\n        {\n          width: 200,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support multiple repeating-linear-gradient', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            backgroundColor: 'white',\n            backgroundImage:\n              'repeating-linear-gradient(rgba(77, 159, 12, .1), #4d9f0c 40px), repeating-linear-gradient(0.25turn, rgba(63, 135, 166, .3), #3f87a6 20px)',\n            height: '100%',\n            width: '100%',\n          }}\n        ></div>,\n        {\n          width: 200,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should compute correct cycle', async () => {\n      const svgs = await Promise.all(\n        [\n          `repeating-linear-gradient(45deg, #606dbc, #606dbc 5px, #465298 5px, #465298 10px)`,\n          `repeating-linear-gradient(45deg, #606dbc, #606dbc 5px, #465298 5px, #465298 10%)`,\n        ].map((backgroundImage) =>\n          satori(\n            <div\n              style={{\n                backgroundColor: 'white',\n                backgroundImage,\n                width: '100px',\n                height: '100px',\n              }}\n            ></div>,\n            {\n              width: 100,\n              height: 100,\n              fonts,\n            }\n          )\n        )\n      )\n      svgs.forEach((svg) => {\n        expect(toImage(svg, 100)).toMatchImageSnapshot()\n      })\n    })\n  })\n\n  describe('repeating-radial-gradient', async () => {\n    it('should support repeating-radial-gradient', async () => {\n      const svgs = await Promise.all(\n        [\n          'repeating-radial-gradient(circle, red, blue 20%)',\n          'repeating-radial-gradient(circle 5px at 20% 20%, blue, blue 10px, red 10px,red 20px)',\n          'repeating-radial-gradient(circle closest-corner at 10% 10%,red,black 5%,blue 5%,green 10%)',\n          'repeating-radial-gradient(ellipse 10% 10% at 10% 10%, #e66465, #9198e5 10%)',\n          'repeating-radial-gradient(ellipse closest-corner at 10% 10%, #e66465, #9198e5 10%)',\n          'repeating-radial-gradient(ellipse 20% 10% at 20% 20%, blue, blue 10px, red 10px,red 20px)',\n        ].map((backgroundImage) =>\n          satori(\n            <div\n              style={{\n                backgroundColor: 'white',\n                backgroundImage,\n                width: '100%',\n                height: '100%',\n              }}\n            ></div>,\n            {\n              width: 100,\n              height: 100,\n              fonts,\n            }\n          )\n        )\n      )\n      svgs.forEach((svg) => {\n        expect(toImage(svg, 100)).toMatchImageSnapshot()\n      })\n    })\n  })\n\n  it('should support gradient with color background', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          background: 'linear-gradient(90deg, #fff 0%, #50ff5050 50%), #fff',\n        }}\n      ></div>,\n      {\n        width: 300,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/image.test.tsx",
    "content": "import { it, describe, expect, beforeEach, afterEach } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\nconst PNG_SAMPLE =\n  'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAAAXNSR0IArs4c6QAAAIRlWElmTU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABIAAAAAQAAAEgAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAABSgAwAEAAAAAQAAAA8AAAAAVtc7bQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDYuMC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KGV7hBwAAA1FJREFUOBGNVFtME0EUnd3S3T5paamRxidoihRKRVGJkaDBRCFNiMkSP9QPiaHhgwRREwOGEUn8MfERIDEqMfxZEtEoSviQGh8Qg4YagQCipVIepdoWSh+03XFndRXkx/uxc+fOPWfO3nt3AfhtDGMTCf6VK3PFFRVTDADdGhyDEJEAAUI4z+7Mzmj1t6bwewhIIY5XPgkDICRYAMZ1Vmv0FkEsH5qcFKFd+bKRDwPUsa6uzT4BVNVZBbVJ2lMBScDPqtm65vzm54yNEXWUdyRwDomVYbL2doe8uDjYtrgYOUpRsUggEBxXK1H+lg3B8wJZU0/jHcmyuNr9w+1eCCxok8PJLYo3inWYDCLIKyWNRoZXOTNDq0pLqWyzWbQ8MoJCuSZasRQKB1sGiQMC4SxayhPLVN5t8gxkSM0MO/1OWU2ixorP096n8SUT2e2XWQh7k+rr8wNFRWc3arTKYplE5I2RbIKmpSrLd7enrdqkrdxXWJE25VJ7iFCi3nPDr/MpKXdoVu4MOV+7Ol1vg/og6bQ72aRftxdx9QMAoXVXFZJAYXqmcmfUuTRVKLIvyPI+b/X9kDbNP3o4p99fAooIMnpAv0cnl6fQYNF9/dXJ/msYa2+wJwD83RQcEBozYJtQDWnTDQpYW5aG2svD2oJw8nYDq8zNFROaFBkxMhrv9tTerj5+ugfktTkwlp8Agnty9mcU8IYfD0jgi9g+E7BE9lW1RmLRWb3ZpCRpWuy1Wucle3anqvzr67LGnj6wMUaq3DgU5wD8G2KOVTOEu81AyJeBlJSMijXJy7RCop1ou6Yaugv18aZGg+7chYxwWWYOBhuZBk7FXzIc48HYESwLwhinnSDePRsbLC3oi8/4LEvmg4Th8BFSGYvHfP39A/Hxb/dwvmd4+I8yAb9KIQ5yNUAdkBFj//smx2N2BxtGEf/o/KcXX6YTL6Pegvt1e590fB2yQeoghHGct9LWEPKHDQw/9VSuaWw6RwkWLXrNZA6d5NJRKlnGJV6VR7f6VQXSVU0RgnhF3LdLcJ173nXmYiyhPhGNBlxikdQx9/FmYyUEIeF8Jea//d7eMrXNBqQCAJMJ/r/rmqasTEAIcUCCk/rIj+OQ/7NAbg/XNEPA/QQBqVjfA25FYgAAAABJRU5ErkJggg=='\n\nfunction dataUriToArrayBuffer(dataUri: string): ArrayBuffer {\n  const binary_string = atob(dataUri.slice(dataUri.indexOf(',') + 1))\n  const len = binary_string.length\n  const bytes = new Uint8Array(len)\n  for (let i = 0; i < len; i++) {\n    bytes[i] = binary_string.charCodeAt(i)\n  }\n  return bytes.buffer\n}\n\nconst PNG_SAMPLE_ARRAYBUFFER = dataUriToArrayBuffer(PNG_SAMPLE)\n\nlet fonts\ninitFonts((f) => (fonts = f))\n\nlet requests = []\n\nbeforeEach(() => {\n  // Polyfill fetch\n  requests = []\n  ;(globalThis as any).fetch = async (url) => {\n    requests.push(url)\n    if (url.includes('wrong-url')) {\n      throw Error('wrong url')\n    } else if (url.startsWith('data:')) {\n      return {\n        headers: {\n          get: () => 'image/png',\n        },\n        text: async () => {\n          const binary_string = atob(url.replace('data:image/png;base64,', ''))\n          const len = binary_string.length\n          const bytes = new Uint8Array(len)\n          for (let i = 0; i < len; i++) {\n            bytes[i] = binary_string.charCodeAt(i)\n          }\n          return bytes.buffer\n        },\n      }\n    }\n\n    if (url.endsWith('.svg')) {\n      return {\n        headers: {\n          get: () => 'image/svg+xml',\n        },\n        text: async () =>\n          '<svg width=\"116.15\" height=\"100\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M57.5 0L115 100H0L57.5 0z\"/></svg>',\n      }\n    }\n\n    return {\n      headers: {\n        get: (key) => {\n          if (key === 'content-type') return 'image/png'\n        },\n      },\n      arrayBuffer: async () => {\n        // 1x1 #00F blue image.\n        const binary_string = atob(\n          `iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPj/HwADBwIAMCbHYQAAAABJRU5ErkJggg==`\n        )\n        const len = binary_string.length\n        const bytes = new Uint8Array(len)\n        for (let i = 0; i < len; i++) {\n          bytes[i] = binary_string.charCodeAt(i)\n        }\n        return bytes.buffer\n      },\n    }\n  }\n})\n\nafterEach(() => {\n  delete globalThis.fetch\n})\n\ndescribe('Image', () => {\n  it('should resolve image data', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          border: '1px solid',\n          width: '50%',\n          height: '50%',\n          display: 'flex',\n        }}\n      >\n        <img width='100%' height='100%' src='https://via.placeholder.com/150' />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n\n    expect(requests).toEqual(['https://via.placeholder.com/150'])\n  })\n\n  it('should render svg with image', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 32,\n          fontWeight: 600,\n        }}\n      >\n        <svg\n          width='100'\n          height='100'\n          viewBox='0 0 100 100'\n          fill='none'\n          xmlns='http://www.w3.org/2000/svg'\n        >\n          <image\n            id='image0_1_2'\n            width='100'\n            height='100'\n            href='data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iI0ZGQ0M0RCIgZD0iTTM2IDE4YzAgOS45NDEtOC4wNTkgMTgtMTggMTgtOS45NCAwLTE4LTguMDU5LTE4LTE4QzAgOC4wNiA4LjA2IDAgMTggMGM5Ljk0MSAwIDE4IDguMDYgMTggMTgiLz48ZWxsaXBzZSBmaWxsPSIjNjY0NTAwIiBjeD0iMTEuNSIgY3k9IjEyLjUiIHJ4PSIyLjUiIHJ5PSI1LjUiLz48ZWxsaXBzZSBmaWxsPSIjNjY0NTAwIiBjeD0iMjQuNSIgY3k9IjEyLjUiIHJ4PSIyLjUiIHJ5PSI1LjUiLz48cGF0aCBmaWxsPSIjNjY0NTAwIiBkPSJNMTggMjJjLTMuNjIzIDAtNi4wMjctLjQyMi05LTEtLjY3OS0uMTMxLTIgMC0yIDIgMCA0IDQuNTk1IDkgMTEgOSA2LjQwNCAwIDExLTUgMTEtOSAwLTItMS4zMjEtMi4xMzItMi0yLTIuOTczLjU3OC01LjM3NyAxLTkgMXoiLz48cGF0aCBmaWxsPSIjRkZGIiBkPSJNOSAyM3MzIDEgOSAxIDktMSA5LTEtMiA0LTkgNC05LTQtOS00eiIvPjwvc3ZnPg=='\n          />\n        </svg>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render svg with image using xlinkHref', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 32,\n          fontWeight: 600,\n        }}\n      >\n        <svg\n          width='100'\n          height='100'\n          viewBox='0 0 100 100'\n          fill='none'\n          xmlns='http://www.w3.org/2000/svg'\n          xmlnsXlink='http://www.w3.org/1999/xlink'\n        >\n          <image\n            id='image0_1_3'\n            width='100'\n            height='100'\n            xlinkHref='data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iI0ZGQ0M0RCIgZD0iTTM2IDE4YzAgOS45NDEtOC4wNTkgMTgtMTggMTgtOS45NCAwLTE4LTguMDU5LTE4LTE4QzAgOC4wNiA4LjA2IDAgMTggMGM5Ljk0MSAwIDE4IDguMDYgMTggMTgiLz48ZWxsaXBzZSBmaWxsPSIjNjY0NTAwIiBjeD0iMTEuNSIgY3k9IjEyLjUiIHJ4PSIyLjUiIHJ5PSI1LjUiLz48ZWxsaXBzZSBmaWxsPSIjNjY0NTAwIiBjeD0iMjQuNSIgY3k9IjEyLjUiIHJ4PSIyLjUiIHJ5PSI1LjUiLz48cGF0aCBmaWxsPSIjNjY0NTAwIiBkPSJNMTggMjJjLTMuNjIzIDAtNi4wMjctLjQyMi05LTEtLjY3OS0uMTMxLTIgMC0yIDIgMCA0IDQuNTk1IDkgMTEgOSA2LjQwNCAwIDExLTUgMTEtOSAwLTItMS4zMjEtMi4xMzItMi0yLTIuOTczLjU3OC01LjM3NyAxLTkgMXoiLz48cGF0aCBmaWxsPSIjRkZGIiBkPSJNOSAyM3MzIDEgOSAxIDktMSA5LTEtMiA0LTkgNC05LTQtOS00eiIvPjwvc3ZnPg=='\n          />\n        </svg>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should throw error when relative path is used', async () => {\n    await expect(\n      satori(\n        <div>\n          <img width='100%' height='100%' src='/image.png' />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n    ).rejects.toThrowError('Image source must be an absolute URL: /image.png')\n  })\n\n  it('should deduplicate image data requests', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          border: '1px solid',\n          width: '50%',\n          height: '50%',\n          display: 'flex',\n        }}\n      >\n        <img width='10%' height='10%' src='https://via.placeholder.com/200' />\n        <img width='20%' height='30%' src='https://via.placeholder.com/200' />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n\n    expect(requests).toEqual(['https://via.placeholder.com/200'])\n  })\n\n  it('should resolve the image size and scale automatically', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          border: '1px solid',\n          width: '50%',\n          height: '50%',\n          display: 'flex',\n        }}\n      >\n        <img width={30} src='https://via.placeholder.com/200' />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should resolve non-square image size correctly', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n        }}\n      >\n        <img width={100} height={50} src='https://via.placeholder.com/200' />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should scale image to fit max-width and max-height but maintain the aspect ratio', async () => {\n    // Hit max-width\n    const svg1 = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          background: 'blue',\n        }}\n      >\n        <img\n          src={PNG_SAMPLE}\n          style={{\n            maxWidth: '100%',\n            maxHeight: '100%',\n            background: 'red',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg1, 100)).toMatchImageSnapshot()\n\n    // Hit max-height\n    const svg2 = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          background: 'blue',\n          flexDirection: 'column',\n        }}\n      >\n        <img\n          src={PNG_SAMPLE}\n          style={{\n            maxWidth: '100%',\n            maxHeight: '100%',\n            background: 'red',\n          }}\n        />\n      </div>,\n      { width: 100, height: 50, fonts }\n    )\n    expect(toImage(svg2, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support styles', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n        }}\n      >\n        <img\n          width='100%'\n          height='100%'\n          src='https://via.placeholder.com/150'\n          style={{\n            transform: 'scale(0.8) skew(10deg, 10deg)',\n            borderRadius: '10px 20%',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support opacity', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n        }}\n      >\n        <img\n          width='100%'\n          height='100%'\n          src='https://via.placeholder.com/150'\n          style={{\n            opacity: 0.5,\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support SVG images and percentage with correct aspect ratio', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n        }}\n      >\n        <img width='100%' src='https://via.placeholder.com/150.svg' />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should clip content in the border area', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n        }}\n      >\n        <img\n          width='100%'\n          height='100%'\n          src='https://via.placeholder.com/150'\n          style={{\n            borderRadius: '10px 20%',\n            border: '10px solid rgba(0, 0, 0, 0.5)',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should clip content in the border and padding areas', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n        }}\n      >\n        <img\n          width='100%'\n          height='100%'\n          src='https://via.placeholder.com/150'\n          style={{\n            padding: 10,\n            borderRadius: '20px 30%',\n            border: '10px solid rgba(0, 0, 0, 0.5)',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should have a separate border radius clip path when transform is used', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          overflow: 'hidden',\n        }}\n      >\n        <img\n          width='100%'\n          height='100%'\n          src='https://via.placeholder.com/150'\n          style={{\n            transform: 'rotate(45deg) translate(30px, 15px)',\n            borderRadius: '20px',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support transparent image with background', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n        }}\n      >\n        <img\n          width='100%'\n          height='100%'\n          src={PNG_SAMPLE}\n          style={{\n            backgroundColor: 'green',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support ArrayBuffer as src', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n        }}\n      >\n        <img width='100%' height='100%' src={PNG_SAMPLE_ARRAYBUFFER as any} />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should not throw when image is not valid', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          border: '1px solid',\n          width: '50%',\n          height: '50%',\n          display: 'flex',\n        }}\n      >\n        <img\n          width='100%'\n          height='100%'\n          src='https://wrong-url.placeholder.com/150'\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n\ndescribe('background-image: url()', () => {\n  it('should resolve image data', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          border: '1px solid',\n          width: '50%',\n          height: '50%',\n          display: 'flex',\n          backgroundImage: 'url(https://via.placeholder.com/300)',\n        }}\n      ></div>,\n      { width: 100, height: 100, fonts }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n\n    expect(requests).toEqual(['https://via.placeholder.com/300'])\n  })\n\n  it('should support single quotes inside url()', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          border: '1px solid',\n          width: '50%',\n          height: '50%',\n          display: 'flex',\n          backgroundImage: \"url('https://via.placeholder.com/301')\",\n        }}\n      ></div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n\n    expect(requests).toEqual(['https://via.placeholder.com/301'])\n  })\n\n  it('should support double quotes inside url()', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          border: '1px solid',\n          width: '50%',\n          height: '50%',\n          display: 'flex',\n          backgroundImage: 'url(\"https://via.placeholder.com/302\")',\n        }}\n      ></div>,\n      { width: 100, height: 100, fonts }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n\n    expect(requests).toEqual(['https://via.placeholder.com/302'])\n  })\n\n  it('should support SVG data uris with various quotes inside url()', async () => {\n    const backgroundImagesWithDoubleQuotes = [\n      `url(data:image/svg+xml,<svg width=\"116\" height=\"100\" fill=\"white\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M57.5 0L115 100H0L57.5 0z\" /></svg>)`,\n      `url(data:image/svg+xml;utf8,<svg width=\"116\" height=\"100\" fill=\"white\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M57.5 0L115 100H0L57.5 0z\" /></svg>)`,\n      `url(data:image/svg+xml,%3Csvg width=\"116\" height=\"100\" fill=\"white\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cpath d=\"M57.5 0L115 100H0L57.5 0z\" /%3E%3C/svg%3E)`,\n      `url(data:image/svg+xml;utf8,%3Csvg width=\"116\" height=\"100\" fill=\"white\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cpath d=\"M57.5 0L115 100H0L57.5 0z\" /%3E%3C/svg%3E)`,\n      `url(data:image/svg+xml,%3Csvg%20width%3D%22116%22%20height%3D%22100%22%20fill%3D%22white%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M57.5%200L115%20100H0L57.5%200z%22%20%2F%3E%3C%2Fsvg%3E)`,\n      `url(data:image/svg+xml;utf8,%3Csvg%20width%3D%22116%22%20height%3D%22100%22%20fill%3D%22white%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M57.5%200L115%20100H0L57.5%200z%22%20%2F%3E%3C%2Fsvg%3E)`,\n      `url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTE2IiBoZWlnaHQ9IjEwMCIgZmlsbD0id2hpdGUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTU3LjUgMEwxMTUgMTAwSDBMNTcuNSAweiIgLz48L3N2Zz4=)`,\n    ]\n\n    const backgroundImagesWithSingleQuotes = [\n      `url(data:image/svg+xml,<svg width='116' height='100' fill='white' xmlns='http://www.w3.org/2000/svg'><path d='M57.5 0L115 100H0L57.5 0z' /></svg>)`,\n      `url(data:image/svg+xml;utf8,<svg width='116' height='100' fill='white' xmlns='http://www.w3.org/2000/svg'><path d='M57.5 0L115 100H0L57.5 0z' /></svg>)`,\n      `url(data:image/svg+xml,%3Csvg width='116' height='100' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M57.5 0L115 100H0L57.5 0z' /%3E%3C/svg%3E)`,\n      `url(data:image/svg+xml;utf8,%3Csvg width='116' height='100' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M57.5 0L115 100H0L57.5 0z' /%3E%3C/svg%3E)`,\n      `url(data:image/svg+xml,%3Csvg%20width%3D%27116%27%20height%3D%27100%27%20fill%3D%27white%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3Cpath%20d%3D%27M57.5%200L115%20100H0L57.5%200z%27%20%2F%3E%3C%2Fsvg%3E)`,\n      `url(data:image/svg+xml;utf8,%3Csvg%20width%3D%27116%27%20height%3D%27100%27%20fill%3D%27white%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3Cpath%20d%3D%27M57.5%200L115%20100H0L57.5%200z%27%20%2F%3E%3C%2Fsvg%3E)`,\n      `url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMTE2JyBoZWlnaHQ9JzEwMCcgZmlsbD0nd2hpdGUnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Zyc+PHBhdGggZD0nTTU3LjUgMEwxMTUgMTAwSDBMNTcuNSAweicgLz48L3N2Zz4=)`,\n    ]\n\n    const backgroundImagesWithDoubleQuotesWrappedInSingleQuotes =\n      backgroundImagesWithDoubleQuotes.map((b) =>\n        b.replace(/^url\\(/, `url('`).replace(/\\)$/, `')`)\n      )\n    const backgroundImagesWithSingleQuotesWrappedInDoubleQuotes =\n      backgroundImagesWithDoubleQuotes.map((b) =>\n        b.replace(/^url\\(/, `url(\"`).replace(/\\)$/, `\")`)\n      )\n\n    const backgroundImages = [\n      ...backgroundImagesWithDoubleQuotes,\n      ...backgroundImagesWithSingleQuotes,\n      ...backgroundImagesWithDoubleQuotesWrappedInSingleQuotes,\n      ...backgroundImagesWithSingleQuotesWrappedInDoubleQuotes,\n    ]\n\n    let lastImageBuffer = null\n    for (const backgroundImage of backgroundImages) {\n      const svg = await satori(\n        <div\n          style={{\n            border: '1px solid',\n            width: '50%',\n            height: '50%',\n            display: 'flex',\n            backgroundImage,\n            backgroundSize: '50px 50px',\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n\n      const newImageBuffer = toImage(svg, 100)\n      if (lastImageBuffer) {\n        expect(newImageBuffer.equals(lastImageBuffer)).toBe(true)\n      }\n      lastImageBuffer = newImageBuffer\n    }\n  })\n\n  it('should resolve data uris with size for supported image formats', async () => {\n    // tests with all the supported image data uri formats.\n    const renderSvg = (imageUri) =>\n      satori(\n        <div\n          style={{\n            border: '1px solid',\n            width: '100%',\n            height: '100%',\n            display: 'flex',\n            backgroundImage: `url(${imageUri})`,\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n\n    const basedOnPlainSvg = await renderSvg(\n      'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2036%2036%22%3E%3Cpath%20fill%3D%22%23FFCC4D%22%20d%3D%22M36%2018c0%209.941-8.059%2018-18%2018-9.94%200-18-8.059-18-18C0%208.06%208.06%200%2018%200c9.941%200%2018%208.06%2018%2018%22%2F%3E%3Cellipse%20fill%3D%22%23664500%22%20cx%3D%2211.5%22%20cy%3D%2212.5%22%20rx%3D%222.5%22%20ry%3D%225.5%22%2F%3E%3Cellipse%20fill%3D%22%23664500%22%20cx%3D%2224.5%22%20cy%3D%2212.5%22%20rx%3D%222.5%22%20ry%3D%225.5%22%2F%3E%3Cpath%20fill%3D%22%23664500%22%20d%3D%22M18%2022c-3.623%200-6.027-.422-9-1-.679-.131-2%200-2%202%200%204%204.595%209%2011%209%206.404%200%2011-5%2011-9%200-2-1.321-2.132-2-2-2.973.578-5.377%201-9%201z%22%2F%3E%3Cpath%20fill%3D%22%23FFF%22%20d%3D%22M9%2023s3%201%209%201%209-1%209-1-2%204-9%204-9-4-9-4z%22%2F%3E%3C%2Fsvg%3E'\n    )\n    const basedOnEncodedSvg = await renderSvg(\n      'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iI0ZGQ0M0RCIgZD0iTTM2IDE4YzAgOS45NDEtOC4wNTkgMTgtMTggMTgtOS45NCAwLTE4LTguMDU5LTE4LTE4QzAgOC4wNiA4LjA2IDAgMTggMGM5Ljk0MSAwIDE4IDguMDYgMTggMTgiLz48ZWxsaXBzZSBmaWxsPSIjNjY0NTAwIiBjeD0iMTEuNSIgY3k9IjEyLjUiIHJ4PSIyLjUiIHJ5PSI1LjUiLz48ZWxsaXBzZSBmaWxsPSIjNjY0NTAwIiBjeD0iMjQuNSIgY3k9IjEyLjUiIHJ4PSIyLjUiIHJ5PSI1LjUiLz48cGF0aCBmaWxsPSIjNjY0NTAwIiBkPSJNMTggMjJjLTMuNjIzIDAtNi4wMjctLjQyMi05LTEtLjY3OS0uMTMxLTIgMC0yIDIgMCA0IDQuNTk1IDkgMTEgOSA2LjQwNCAwIDExLTUgMTEtOSAwLTItMS4zMjEtMi4xMzItMi0yLTIuOTczLjU3OC01LjM3NyAxLTkgMXoiLz48cGF0aCBmaWxsPSIjRkZGIiBkPSJNOSAyM3MzIDEgOSAxIDktMSA5LTEtMiA0LTkgNC05LTQtOS00eiIvPjwvc3ZnPg=='\n    )\n    const basedOnPng = await renderSvg(\n      'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAMAAABiM0N1AAAAh1BMVEVHcEz/zE3/zE3/zE3/zE3/zE3/zE3/zE3/zE3/zE3/zE3/zE3/zE3/zE2pgCJmRQDsu0OfeB31xEjGmTCDXg7isz/Zqjp5VgqWbxiziSfPojWMZxO8kSvs6N/Z0b+zooCWf1CDaDB5XCD18+/////i3M+8ro+MdECfi2BwURDGuZ/Pxa9wTQUxvYUJAAAADnRSTlMAIGCPv9//UJ/vQK+Az1vcyGwAAAJTSURBVHgB5JRFlsQwEENjUhzG+191WM12K7Cbv5WfXFxkMNb5gD+Cd9YUOyhjwBMhlttcjKuQoHJ6XHWDLE2tRRPxlihE1VYQqNp34XQQ6Uy2VRVkqjKTFjaRTC9iI/GwD4m6j+6k10erU4mdPPTOVHdqPww9SF6p7uepe3z97JRSunSBxuGHESSvtMnEpuGHCSSvVCbR+Xn4YwagKJE+Ne5Y+HwBNIX3qXmOnxloSvNXIdyz8vkKVfmtksM9wwWoivsxqo4bVVyOQ0ZclHjUiBMQzjAK7NlRI5jCAsfbD1g2f/9AcgA8gAMrQjxrvX9pWW2QnWeE0OjAYXs20k+tYCQdf8FI4t8ZBZxCeFqRz1bMAsF1GIihH5d3FTQGy3T/65VRtYt+B1Ah0cxIuXi5TiIitgiZNskASKVN5NOIjFYSgE3ItH/IA3sy+6MLsSYSKwr9YzPAPQH+0GDDHfBC+s/2vhUatb/eWKis6qaFl05TVyULvfE6wpK221vSrwZ1PVzTDjfU9aDq95Z0WxZ65QUpsWTUu8IISyQtyNOVHePwnXx027On9vfsiDBYM678OtUYawwdEXTWSGxoSrdM2WCDpLNmxYdrUWA46Z79qMkQrkXy4Tr9MhwY1tP+TqQ/rfcqtCLfnMdojpvI3Zf2F/+4a6S+FPHtMq6fmA92/nE3KsW+CMFG0biIZnP4Y5aZwcvMuPOD+/xPFDyo5M4IKSwcWPFAqM3jGf2oOOcT9Na4LrSVWCOtFjfH9QAFQvBKI1zJEqD2CV9EBajGApR1AerDBQPAoCxnErpYAAAAAElFTkSuQmCC'\n    )\n    const basedOnJpeg = await renderSvg(\n      'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD//gAfQ29tcHJlc3NlZCBieSBqcGVnLXJlY29tcHJlc3P/2wCEAAQEBAQEBAQEBAQGBgUGBggHBwcHCAwJCQkJCQwTDA4MDA4MExEUEA8QFBEeFxUVFx4iHRsdIiolJSo0MjRERFwBBAQEBAQEBAQEBAYGBQYGCAcHBwcIDAkJCQkJDBMMDgwMDgwTERQQDxAUER4XFRUXHiIdGx0iKiUlKjQyNEREXP/CABEIAEgASAMBIgACEQEDEQH/xAAbAAACAwEBAQAAAAAAAAAAAAAGBwQFCAIAAf/aAAgBAQAAAADf3IUNxpJKa/eVeKWEmNXljPChLJrDd6QXmsi8ZjY/Yb9QS82BJjeASG9oh4+9JCkvY+9XOg1JVvk/UxiGZb1ixjVYKbMwmWaZbDQ65DBmNJJTTr//xAAZAQADAQEBAAAAAAAAAAAAAAAFBgcCBAP/2gAIAQIQAAAAXpONd6lBSQjocJ+Z5s5afUuj1opNE6zl/wD/xAAYAQADAQEAAAAAAAAAAAAAAAAEBQYDAf/aAAgBAxAAAABhWEo5e8yKzQ0gunc1vQ3cgNRO4sX/xAAiEAACAwEBAQABBQEAAAAAAAADBAIFBgEHAAgQERIUIhf/2gAIAQEAAQgA+YYAsKRTv6kxO9giZ1tjvem+C42v3nQ1+qMKXIurshZFEwPmGRLAIctnZmsjdnO70NNnFoN3Sbir6oHUvnHFUFTuu0mhptGtNulq7Q9YfhBrHEyEZw6h7sywRh97LaGc1sq7vh9qVmltaon3uFoValqqofjVoVPWxr+fZWxmI00ZOG6w2wbv3rqpV9zZGJ4QqWCejd797uqWaecd55EqVjc1xh/Jm6u2ubn6bnApbUSkp5ugTzNQrTo/aSgT01O1TvYbApYoTcofo6Hq7bAe7m6bz2VtrZGi0XsrtYO6re+q7ZD/ADb/APaLn7nqu2f/AM1F7ovZU6wl1ZYe6b0OVqLd5MPWG1w81SHYHi9DRU47+ks6cuX2Wt8uvGxpUH5K4x4UI3vPePJefXv5KY1AU40Wo2Wt9RvFBu52nHQUlZTiy6Eplm9NkAmQkAa0qz1h+jJufOq7Xw/tiu8NqaCc+Pfxl+/8fqTDam/nDiOG87rshD+2WsrDWRuQgsuNUIwi+OuFkUhHscqYU+yRMm2v3vDfBTbY7zga/LGLLk3l1gqiiIP3/8QANhAAAgEDAAgDBQYHAAAAAAAAAQIDAAQRBRASIjFRYYEyQXEGFCFishMjQkOisVJzpNHS0+H/2gAIAQEACT8ApwqLxJpAi/xsMnsKuZH6EnGq4kToCcUgZeG2o+I7VIHRuBGptlFGSaJEQO4nkP8AtX6W0TvsISGdmboqAk1Ok1vMgeORDlWU6p0ht4ULySOcKqir5LmJH2HIDIyt1VwCKJMZO+nOmDKwyDR3Uwz+p4DU5+x0fbxIq+W3KolZu4Ipiy2Nwjx/KlwDujupOpyq31w8knzJbgbp7sDTn7HSFvLGy+W3EplVuwB1NuuCyZ5jiKPjkYj08tQwtzDbTR9VEQj/AHSh91LNbRKfmiDE/WNQ+6imuYWPzShWH0GhlbaG5lk6KYjH+76j4JAT6a7s2d5bEhJxGJMo3FWXK5rLRxAlpG8Ujscsx1ZWOUArIvijdTlWFXZvLy5IDzmMR4ReCquWxrHgkYD08qC+8wpGsZYZCtLIse122qsZNM2DO6gx28U5DIfirrBiRT0NexLIwODmGeD6w1exzZ/mv/hXsSzsTgYhnn+gLVjJoawR0XMltHbks5wFVLjLsegoL7zOkgkKjALRSNHtd9nNDxyAH0pd191+h8qbZF1AyKx/C/FG7EA05jZJDHeWE+TBLs8xz5MK0dd6Mn/EQvvEPZk3v017Tf0F3/qqwvNKXH4SU92h7s+9+mnMjPII7OxgyIItrkOfNzTbQtYFRmHBn4u3diTQ3Uyqep4ml2kYYIoExk7j86cWulUTCXAGVkA4LKK0TM0K/nwKZYSOe0vDvQOc4xWiZlib8+dTFCBz2m49qcXelXTDzkYWMHisQoERA77+QH96XZRRgDVGGRh8QakDrx2GOD2NW8idSDjVbyP1AOKcIvEopye5pAiLwA1f/8QALREAAgEEAAMGBQUAAAAAAAAAAQIDAAQFEQYxURIhIkFCcRMjYcHREBQVUqH/2gAIAQIBAT8Az+fhwsI7g9y4+Wn3P0q/y2VyHzbqeT4TEgKNqnsAKikmicNBI6v5FCQf8rC8XXdpMtrlizxbA7bDxp79RSOkiLIjBlYAgjkQazV++RyV1cs21LlU+ijuFXKx/wAOmvJEI96xgU3sAbqSPfVZoKLtSOZjG64c4lhtcctteNto3IQk+mjbyC5NqRqQSFO/rvVSYnLfASLXahB2oHKo8Pk1dWSEhgdg7rI2F7b6nvCO05APWrLE3d/G0sCbUN2a4uwstpdnK2qExOwZ9eh+vsaxXELiNUjlUHzjf7VLxDchCWMMY/tr81fXNxmruO2tg0rM3PqfwKwuMXFY+G0Gi48Uh6uedOiSIySKGVhogjYIribh/HWcZurVHjY7JVT4axlrHe3SQzMwUn0msVhsfi4x+0h05A27d7H9P//EACoRAAICAgECBAUFAAAAAAAAAAECAwQAEQUSITEyUWEQExQicUFScoHB/9oACAEDAQE/AKFB7sh79Ma+Zv8ABlenUr6WKNeofqe7Y6I66dQV9xl3h4pUMtQBX/aPKcIKkgjRGUYBWqxRgaOgW/JyMt9UfycsEiF9ZUJ+V/ecrx0j2jJAnZwCdeuCRTGJR5SvVi26vWW6gH8DjW62iC41leeGT7IfAZZvQVXVJD3I3nD3UkiFSU6dRpfdctccjsWZD/JcTjItjzt7ZEkVKFpJNIoGXbJt2HmPYeCj0AwEqQQSCM43krMriGUhh6kd8tzNXiMiAb98s3LFs7mfYHgo7AfD/9k='\n    )\n    const basedOnGif = await renderSvg(\n      'data:image/gif;base64,R0lGODlhSABIANUAAAAAAGZFAHBNBXBREHlWCnlcIINeDoNoMIxnE4x0QJZvGJZ/UJ94HZ+LYKmAIrOJJ7OigLyRK7yuj8aZMMa5n8+iNc/Fr9mqOtnRv+KzP+Lcz+y7Q+zo3/XESPXz7//MTf///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAUKAAAALAAAAABIAEgAAAb/QIBwSCwaj4CPcslkIp/QqJTYrFqd06yWeu16t+CndzwOm5Pk9Pc8VbvLbPF7fo0f6XirfZjv6+N+gU1sgoVLZoaJH2CKiluNiVpqDgEBDn6UlmptaZmVl3iemmlRahuVqAEbdKepqptyZAyuAQx0s662pEhqHbSVHW++v8G7RmoTvwETb8m/zMZcabi0umrUuW7HagbKBm/dv9+wfG7KlW/nAW/Saerp5+zlau/m8dpCc/X09/j6/e4A7vqnDF5BeXPC0RrHzdscNG+wpbI2TRlFSEqc0YKGTBlHjEqG0SrWSxlJkEok1rpVDWWTVqlWzYGJSqbLJaJA0cl5s0om/52hPvUcSrSo0aNIkyodmuHCzQ4XbCbqoJCAggcVpObZUOGBAgKoEJz8UvGcAQQOHkS4wLbthrZsIzxwgEDhr4t1IHpRx7cv34FjRPodPBiwF8EcMEBYcIAw3wILIGDgkMoYmVQHPIDYDEIDBgkQICQYTfoA6dOhJWDQwBmEh8aoyLS7AhZVg9a4c+vevblBKgKy53WRmJm3cd6vs8ERbqUCrQEYjkvnjGEArQrLmVup7WoBh+m8OSz4BTy79iYaaSWgoBm8awoJzn38s82L3V8JIFhgrVuDBdF8MdRFLFdk4NiBrmRgXhFkpIfgYPPRx8sYKj14Dl5VcOJFhRYqt2WGhl080OE5DwSXRRoVCDBiKgJgZ+KJZHSgwIoBKDBWXoykcQECFiLgVDQ5ppEBAyr6JQADCpJzxhwXPIAAd6gQgMADP+Kzh1J7MHhUlvUNxeWELn1ZCkZiwmhImWH4geaVQO4RBAA7'\n    )\n\n    expect(toImage(basedOnPlainSvg, 100)).toMatchImageSnapshot()\n    expect(toImage(basedOnEncodedSvg, 100)).toMatchImageSnapshot()\n    expect(toImage(basedOnPng, 100)).toMatchImageSnapshot()\n    expect(toImage(basedOnJpeg, 100)).toMatchImageSnapshot()\n    expect(toImage(basedOnGif, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support stretched backgroundSize', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '50%',\n          height: '50%',\n          display: 'flex',\n          backgroundImage: 'url(https://via.placeholder.com/300)',\n          backgroundSize: '100% 100%',\n        }}\n      ></div>,\n      { width: 100, height: 50, fonts }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support background-size: cover', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          backgroundImage: `url(${PNG_SAMPLE})`,\n          backgroundSize: 'cover',\n        }}\n      ></div>,\n      { width: 100, height: 100, fonts }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support background-size: contain', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          backgroundImage: `url(${PNG_SAMPLE})`,\n          backgroundSize: 'contain',\n        }}\n      ></div>,\n      { width: 100, height: 100, fonts }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support background-size: auto', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          backgroundImage: `url(${PNG_SAMPLE})`,\n          backgroundSize: 'auto',\n        }}\n      ></div>,\n      { width: 100, height: 100, fonts }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support background-size: cover with non-square container', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          backgroundImage: `url(${PNG_SAMPLE})`,\n          backgroundSize: 'cover',\n        }}\n      ></div>,\n      { width: 200, height: 100, fonts }\n    )\n\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('should correctly position the background pattern', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          margin: '30px 30px',\n          width: '70px',\n          height: '70px',\n          display: 'flex',\n          backgroundImage: `url(${PNG_SAMPLE})`,\n          backgroundSize: '70px 70px',\n        }}\n      ></div>,\n      { width: 100, height: 100, fonts }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should handle charset=utf-8', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100px',\n          height: '100px',\n          display: 'flex',\n          backgroundImage:\n            'url(\\'data:image/svg+xml;charset=utf-8,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"40\" height=\"40\"><circle cx=\"20\" cy=\"20\" r=\"15\" fill=\"#ee7621\"/></svg>\\')',\n          backgroundSize: '100px 100px',\n        }}\n      ></div>,\n      { width: 100, height: 100, fonts }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should handle charset=utf-8 with comma in data', async () => {\n    const svg = await satori(\n      <img\n        src={`data:image/svg+xml;charset=utf-8,<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" viewBox=\"0 0 100 100\"><polygon fill=\"#ffffff\" points=\"50,0 100,85 0,85\" /></svg>`}\n      />,\n      { width: 100, height: 100, fonts }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should handle charset=utf-8 with in base64', async () => {\n    const svg = await satori(\n      <img\n        src={`data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCI+PGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iMTUiIGZpbGw9IiNlZTc2MjEiLz48L3N2Zz4=`}\n      />,\n      { width: 100, height: 100, fonts }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n\ndescribe('objectFit and objectPosition', () => {\n  it('should default to center center with cover', async () => {\n    const svg = await satori(\n      <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n        <img\n          src={PNG_SAMPLE}\n          style={{\n            width: 100,\n            height: 100,\n            objectFit: 'cover',\n            backgroundColor: 'green',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should position to top with cover', async () => {\n    const svg = await satori(\n      <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n        <img\n          src={PNG_SAMPLE}\n          style={{\n            width: 100,\n            height: 100,\n            objectFit: 'cover',\n            objectPosition: 'top',\n            backgroundColor: 'green',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should position to bottom with cover', async () => {\n    const svg = await satori(\n      <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n        <img\n          src={PNG_SAMPLE}\n          style={{\n            width: 100,\n            height: 100,\n            objectFit: 'cover',\n            objectPosition: 'bottom',\n            backgroundColor: 'green',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should position to left with cover', async () => {\n    const svg = await satori(\n      <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n        <img\n          src={PNG_SAMPLE}\n          style={{\n            width: 100,\n            height: 100,\n            objectFit: 'cover',\n            objectPosition: 'left',\n            backgroundColor: 'green',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should position to right with cover', async () => {\n    const svg = await satori(\n      <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n        <img\n          src={PNG_SAMPLE}\n          style={{\n            width: 100,\n            height: 100,\n            objectFit: 'cover',\n            objectPosition: 'right',\n            backgroundColor: 'green',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should position to top left with cover', async () => {\n    const svg = await satori(\n      <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n        <img\n          src={PNG_SAMPLE}\n          style={{\n            width: 100,\n            height: 100,\n            objectFit: 'cover',\n            objectPosition: 'top left',\n            backgroundColor: 'green',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should position to bottom right with cover', async () => {\n    const svg = await satori(\n      <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n        <img\n          src={PNG_SAMPLE}\n          style={{\n            width: 100,\n            height: 100,\n            objectFit: 'cover',\n            objectPosition: 'bottom right',\n            backgroundColor: 'green',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should default to center center with contain', async () => {\n    const svg = await satori(\n      <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n        <img\n          src={PNG_SAMPLE}\n          style={{\n            width: 100,\n            height: 100,\n            objectFit: 'contain',\n            backgroundColor: 'green',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should position to top with contain', async () => {\n    const svg = await satori(\n      <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n        <img\n          src={PNG_SAMPLE}\n          style={{\n            width: 100,\n            height: 100,\n            objectFit: 'contain',\n            objectPosition: 'top',\n            backgroundColor: 'green',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should position to bottom left with contain', async () => {\n    const svg = await satori(\n      <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n        <img\n          src={PNG_SAMPLE}\n          style={{\n            width: 100,\n            height: 100,\n            objectFit: 'contain',\n            objectPosition: 'bottom left',\n            backgroundColor: 'green',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  describe('objectFit: fill', () => {\n    it('should stretch image to fill container (aspect ratio not preserved)', async () => {\n      const svg = await satori(\n        <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n          <img\n            src={PNG_SAMPLE}\n            style={{\n              width: 100,\n              height: 100,\n              objectFit: 'fill',\n              backgroundColor: 'green',\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should stretch with fill on non-square container', async () => {\n      const svg = await satori(\n        <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n          <img\n            src={PNG_SAMPLE}\n            style={{\n              width: 150,\n              height: 100,\n              objectFit: 'fill',\n              backgroundColor: 'green',\n            }}\n          />\n        </div>,\n        { width: 150, height: 100, fonts }\n      )\n      expect(toImage(svg, 150)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('objectFit: scale-down', () => {\n    it('should not scale up when image is smaller than container', async () => {\n      // PNG_SAMPLE is 20x15, container is 100x100\n      // Should show image at 20x15, centered\n      const svg = await satori(\n        <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n          <img\n            src={PNG_SAMPLE}\n            style={{\n              width: 100,\n              height: 100,\n              objectFit: 'scale-down',\n              backgroundColor: 'green',\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should scale down when image is larger than container', async () => {\n      // PNG_SAMPLE is 20x15, container is 10x10\n      // Should scale down like 'contain'\n      const svg = await satori(\n        <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n          <img\n            src={PNG_SAMPLE}\n            style={{\n              width: 10,\n              height: 10,\n              objectFit: 'scale-down',\n              backgroundColor: 'green',\n            }}\n          />\n        </div>,\n        { width: 10, height: 10, fonts }\n      )\n      expect(toImage(svg, 10)).toMatchImageSnapshot()\n    })\n\n    it('should respect objectPosition with scale-down', async () => {\n      const svg = await satori(\n        <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n          <img\n            src={PNG_SAMPLE}\n            style={{\n              width: 100,\n              height: 100,\n              objectFit: 'scale-down',\n              objectPosition: 'top left',\n              backgroundColor: 'green',\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should respect objectPosition bottom right with scale-down', async () => {\n      const svg = await satori(\n        <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n          <img\n            src={PNG_SAMPLE}\n            style={{\n              width: 100,\n              height: 100,\n              objectFit: 'scale-down',\n              objectPosition: 'bottom right',\n              backgroundColor: 'green',\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support percentage values for objectPosition', async () => {\n      const svg = await satori(\n        <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n          <img\n            src={PNG_SAMPLE}\n            style={{\n              width: 100,\n              height: 100,\n              objectFit: 'cover',\n              objectPosition: '25% 75%',\n              backgroundColor: 'green',\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support pixel values for objectPosition', async () => {\n      const svg = await satori(\n        <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n          <img\n            src={PNG_SAMPLE}\n            style={{\n              width: 100,\n              height: 100,\n              objectFit: 'contain',\n              objectPosition: '10px 20px',\n              backgroundColor: 'green',\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support mixed keyword and percentage for objectPosition', async () => {\n      const svg = await satori(\n        <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n          <img\n            src={PNG_SAMPLE}\n            style={{\n              width: 100,\n              height: 100,\n              objectFit: 'cover',\n              objectPosition: 'left 25%',\n              backgroundColor: 'green',\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support 100% 100% for objectPosition (bottom right)', async () => {\n      const svg = await satori(\n        <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n          <img\n            src={PNG_SAMPLE}\n            style={{\n              width: 100,\n              height: 100,\n              objectFit: 'cover',\n              objectPosition: '100% 100%',\n              backgroundColor: 'green',\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support 0% 0% for objectPosition (top left)', async () => {\n      const svg = await satori(\n        <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n          <img\n            src={PNG_SAMPLE}\n            style={{\n              width: 100,\n              height: 100,\n              objectFit: 'cover',\n              objectPosition: '0% 0%',\n              backgroundColor: 'green',\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support objectPosition with contain and percentages', async () => {\n      const svg = await satori(\n        <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n          <img\n            src={PNG_SAMPLE}\n            style={{\n              width: 100,\n              height: 100,\n              objectFit: 'contain',\n              objectPosition: '75% 25%',\n              backgroundColor: 'green',\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support objectPosition with scale-down and percentages', async () => {\n      const svg = await satori(\n        <div style={{ width: '100%', height: '100%', display: 'flex' }}>\n          <img\n            src={PNG_SAMPLE}\n            style={{\n              width: 100,\n              height: 100,\n              objectFit: 'scale-down',\n              objectPosition: '80% 20%',\n              backgroundColor: 'green',\n            }}\n          />\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n})\n"
  },
  {
    "path": "test/jsx-runtime.test.tsx",
    "content": "// TODO: use `#satori/jsx` as import source after upgradine vitest.\n/** @jsxRuntime automatic */\n/** @jsxImportSource ../src/jsx */\n\nimport { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori, { type Font } from '../src/index.js'\n\ndescribe('Minimal JSX runtime', () => {\n  let fonts: Font[]\n  initFonts((f) => (fonts = f))\n\n  it('should support async function components', async () => {\n    function MyComponent() {\n      // @ts-expect-error until we can replace import source with package.json import.\n      return <h1 style={{ fontSize: 16 }}>Hello from My Component</h1>\n    }\n\n    const svg = await satori(\n      <div\n        style={{\n          backgroundColor: '#ff0',\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n        }}\n      >\n        <MyComponent />\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n\n    async function MyAsyncComponent() {\n      await new Promise((resolve) => setTimeout(resolve, 0))\n      return <h1 style={{ fontSize: 16 }}>Hello from My Async Component</h1>\n    }\n    const svg2 = await satori(\n      <div\n        style={{\n          backgroundColor: '#ff0',\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n        }}\n      >\n        {/* @ts-expect-error React v17 doesn't support async components. */}\n        <MyAsyncComponent />\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg2, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support Fragment elements', async () => {\n    const MyComponent = () => (\n      <>\n        <h1 style={{ fontSize: 16 }}>\n          <>Hello from My Component</>\n        </h1>\n      </>\n    )\n    const svg = await satori(<MyComponent />, {\n      width: 100,\n      height: 100,\n      fonts,\n    })\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/language.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\nimport { detectLanguageCode } from '../src/language.js'\n\nlet fonts\ninitFonts((f) => (fonts = f))\n\ndescribe('detectLanguageCode', () => {\n  it('should detect emoji', async () => {\n    expect(detectLanguageCode('🔺')).toEqual(['emoji'])\n    expect(detectLanguageCode('😀')).toEqual(['emoji'])\n    expect(detectLanguageCode('㊗️')).toEqual(['emoji'])\n    expect(detectLanguageCode('🧑🏻‍💻')).toEqual(['emoji'])\n    expect(detectLanguageCode('hello 🌍')).toEqual(['emoji'])\n    expect(detectLanguageCode('👋 vs 🌊')).toEqual(['emoji'])\n  })\n\n  it('should detect japanese(hiragana)', async () => {\n    expect(detectLanguageCode('こんにちは')).toEqual(['ja-JP'])\n  })\n\n  it('should detect japanese(katakana)', async () => {\n    expect(detectLanguageCode('ハナミズキ')).toEqual(['ja-JP'])\n  })\n\n  it('should detect japanese（kanji)', async () => {\n    expect(detectLanguageCode('桜')).toEqual([\n      'ja-JP',\n      'zh-CN',\n      'zh-TW',\n      'zh-HK',\n    ])\n  })\n\n  it('should detect japanese(hiragana) when locale is zh', async () => {\n    expect(detectLanguageCode('こんにちは')).toEqual(['ja-JP'])\n  })\n\n  it('should detect japanese(katakana) when locale is zh', async () => {\n    expect(detectLanguageCode('ハナミズキ')).toEqual(['ja-JP'])\n  })\n\n  it('should detect simplified chinese when locale is zh-cn', async () => {\n    expect(detectLanguageCode('我知道怎么说中文', 'zh-CN')).toEqual([\n      'zh-CN',\n      'ja-JP',\n      'zh-TW',\n      'zh-HK',\n    ])\n  })\n\n  it('should detect traditional chinese(HK) when locale is zh-cn', async () => {\n    expect(detectLanguageCode('我知道怎麼說中文', 'zh-HK')).toEqual([\n      'zh-HK',\n      'ja-JP',\n      'zh-CN',\n      'zh-TW',\n    ])\n  })\n\n  it('should detect traditional chinese(TW) when locale is zh-tw', async () => {\n    expect(detectLanguageCode('我知道怎麼說中文', 'zh-TW')).toEqual([\n      'zh-TW',\n      'ja-JP',\n      'zh-CN',\n      'zh-HK',\n    ])\n  })\n\n  it('should detect korean', async () => {\n    expect(detectLanguageCode('안녕하세요')).toEqual(['ko-KR'])\n  })\n\n  it('should detect thai', async () => {\n    expect(detectLanguageCode('สวัสดี')).toEqual(['th-TH'])\n  })\n\n  it('should detect arabic', async () => {\n    expect(detectLanguageCode('مرحبا')).toEqual(['ar-AR'])\n  })\n\n  it('should detect tamil', async () => {\n    expect(detectLanguageCode('வணக்கம்')).toEqual(['ta-IN'])\n  })\n\n  it('should detect bengali', async () => {\n    expect(detectLanguageCode('হ্যালো')).toEqual(['bn-IN'])\n  })\n\n  it('should detect malayalam', async () => {\n    expect(detectLanguageCode('ഹായ്')).toEqual(['ml-IN'])\n  })\n\n  it('should detect hebrew', async () => {\n    expect(detectLanguageCode('שלום')).toEqual(['he-IL'])\n  })\n\n  it('should detect telegu', async () => {\n    expect(detectLanguageCode('హలో')).toEqual(['te-IN'])\n  })\n\n  it('should detect devanagari', async () => {\n    expect(detectLanguageCode('नमस्ते')).toEqual(['devanagari'])\n  })\n\n  it('should detect unknown', async () => {\n    expect(detectLanguageCode('wat')).toEqual(['unknown'])\n  })\n\n  it('should detect math', async () => {\n    expect(detectLanguageCode('ℵ')).toEqual(['math'])\n  })\n\n  it('should detect symbol', async () => {\n    expect(detectLanguageCode('☻')).toEqual(['symbol'])\n  })\n\n  it('should not crash when rendering Arabic letters', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          background: 'white',\n        }}\n      >\n        سلام\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/layout.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Layout', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should stretch items by default', async () => {\n    const svg = await satori(\n      <div style={{ width: '100%', display: 'flex', flexDirection: 'column' }}>\n        <div style={{ background: 'blue' }}>x</div>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/letter-spacing.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Letter Spacing', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should render text with positive letter-spacing', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 20,\n          letterSpacing: 5,\n        }}\n      >\n        Hello World\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render text with negative letter-spacing', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 20,\n          letterSpacing: -2,\n        }}\n      >\n        Hello World\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render text with zero letter-spacing', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 20,\n          letterSpacing: 0,\n        }}\n      >\n        Hello World\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render text with large letter-spacing', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 20,\n          letterSpacing: 10,\n        }}\n      >\n        Hello\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render text with very small letter-spacing', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 20,\n          letterSpacing: 1,\n        }}\n      >\n        Hello World\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render letter-spacing with different font sizes', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'column',\n        }}\n      >\n        <div style={{ fontSize: 12, letterSpacing: 3 }}>Small Text</div>\n        <div style={{ fontSize: 20, letterSpacing: 3 }}>Medium Text</div>\n        <div style={{ fontSize: 30, letterSpacing: 3 }}>Large</div>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render letter-spacing with text-align left', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 100,\n          fontSize: 16,\n          letterSpacing: 4,\n          textAlign: 'left',\n        }}\n      >\n        Hello\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render letter-spacing with text-align center', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 100,\n          fontSize: 16,\n          letterSpacing: 4,\n          textAlign: 'center',\n        }}\n      >\n        Hello\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render letter-spacing with text-align right', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 100,\n          fontSize: 16,\n          letterSpacing: 4,\n          textAlign: 'right',\n        }}\n      >\n        Hello\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render letter-spacing with wrapped text', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 80,\n          fontSize: 16,\n          letterSpacing: 3,\n        }}\n      >\n        Hello World Testing\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render letter-spacing with text-decoration underline', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 20,\n          letterSpacing: 5,\n          textDecoration: 'underline',\n        }}\n      >\n        Hello\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render letter-spacing with text-decoration line-through', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 20,\n          letterSpacing: 5,\n          textDecoration: 'line-through',\n        }}\n      >\n        Hello\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render letter-spacing with color', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 24,\n          letterSpacing: 4,\n          color: 'blue',\n          background: 'lightyellow',\n        }}\n      >\n        Colored\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render letter-spacing with background-clip text', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 24,\n          letterSpacing: 3,\n          background: 'linear-gradient(90deg, red, blue)',\n          backgroundClip: 'text',\n          color: 'transparent',\n        }}\n      >\n        Gradient\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render letter-spacing with text-shadow', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 24,\n          letterSpacing: 4,\n          textShadow: '2px 2px 4px rgba(0, 0, 0, 0.5)',\n        }}\n      >\n        Shadow\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render letter-spacing with font-weight bold', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 20,\n          letterSpacing: 3,\n          fontWeight: 'bold',\n        }}\n      >\n        Bold Text\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render letter-spacing with opacity', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 24,\n          letterSpacing: 5,\n          opacity: 0.5,\n        }}\n      >\n        Faded\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render letter-spacing with multiple lines', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 90,\n          fontSize: 16,\n          letterSpacing: 2,\n          lineHeight: 1.5,\n        }}\n      >\n        This is a multiline text with letter spacing\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render letter-spacing on single character', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 40,\n          letterSpacing: 10,\n        }}\n      >\n        A\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render letter-spacing with mixed case text', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 20,\n          letterSpacing: 3,\n        }}\n      >\n        HeLLo WoRLd\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render letter-spacing with numbers', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 24,\n          letterSpacing: 5,\n        }}\n      >\n        12345\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/line-clamp.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Line Clamp', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('Should work correctly', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            display: 'block',\n            lineClamp: 2,\n          }}\n        >\n          lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do\n          eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad\n          minim veniam, quis nostrud exercitation ullamco laboris nisi ut\n          aliquip ex ea commodo consequat.\n        </div>\n        <div\n          style={{\n            width: '100%',\n            display: 'block',\n            lineClamp: '2',\n          }}\n        >\n          lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do\n          eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad\n          minim veniam, quis nostrud exercitation ullamco laboris nisi ut\n          aliquip ex ea commodo consequat.\n        </div>\n        <div\n          style={{\n            width: '100%',\n            display: 'block',\n            lineClamp: '2 \"… (continued)\"',\n          }}\n        >\n          lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do\n          eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad\n          minim veniam, quis nostrud exercitation ullamco laboris nisi ut\n          aliquip ex ea commodo consequat.\n        </div>\n        <div\n          style={{\n            width: '100%',\n            display: 'block',\n            lineClamp: \"2 '… (continued)'\",\n          }}\n        >\n          lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do\n          eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad\n          minim veniam, quis nostrud exercitation ullamco laboris nisi ut\n          aliquip ex ea commodo consequat.\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should replace custom block ellipsis with default ellipsis when too long', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            display: 'block',\n            lineClamp: '2 \"… (loooooooooooooooooooooooooog text)\"',\n          }}\n        >\n          lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do\n          eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad\n          minim veniam, quis nostrud exercitation ullamco laboris nisi ut\n          aliquip ex ea commodo consequat.\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should not work when display is not set to block', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            lineClamp: 2,\n          }}\n        >\n          lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do\n          eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad\n          minim veniam, quis nostrud exercitation ullamco laboris nisi ut\n          aliquip ex ea commodo consequat.\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should work correctly when `text-align: center`', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            display: 'block',\n            fontSize: 32,\n            textAlign: 'center',\n            lineClamp: 2,\n            backgroundColor: '#ff6c2f',\n            color: 'white',\n          }}\n        >\n          Making the Web. Superfast\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/line-height.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('line-height', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n  it('should work correctly', async () => {\n    const svgs = await Promise.all(\n      [1, '1'].map((lineHeight) =>\n        satori(\n          <div\n            style={{\n              margin: 0,\n              padding: 0,\n              fontSize: '30px',\n              width: '100px',\n              height: '100px',\n              background: 'white',\n              lineHeight,\n            }}\n          >\n            Hello I am some text that is here.\n          </div>,\n          { width: 100, height: 100, fonts, embedFont: true }\n        )\n      )\n    )\n\n    svgs.forEach((svg) => {\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n})\n"
  },
  {
    "path": "test/margin.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Margin', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should render element with margin shorthand (1 value)', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n        }}\n      >\n        <div\n          style={{\n            width: 40,\n            height: 40,\n            background: 'red',\n            margin: 20,\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render element with margin shorthand (2 values)', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n        }}\n      >\n        <div\n          style={{\n            width: 40,\n            height: 40,\n            background: 'red',\n            margin: '10px 20px',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render element with margin shorthand (3 values)', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n        }}\n      >\n        <div\n          style={{\n            width: 40,\n            height: 40,\n            background: 'red',\n            margin: '10px 15px 20px',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render element with margin shorthand (4 values)', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n        }}\n      >\n        <div\n          style={{\n            width: 40,\n            height: 40,\n            background: 'red',\n            margin: '5px 10px 15px 20px',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render element with individual margin properties', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n        }}\n      >\n        <div\n          style={{\n            width: 40,\n            height: 40,\n            background: 'blue',\n            marginTop: 10,\n            marginRight: 15,\n            marginBottom: 20,\n            marginLeft: 25,\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render element with negative margin', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n          display: 'flex',\n          flexDirection: 'column',\n        }}\n      >\n        <div\n          style={{\n            width: 50,\n            height: 50,\n            background: 'red',\n          }}\n        />\n        <div\n          style={{\n            width: 50,\n            height: 50,\n            background: 'blue',\n            marginTop: -20,\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render element with negative margin left', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n          display: 'flex',\n          flexDirection: 'row',\n        }}\n      >\n        <div\n          style={{\n            width: 50,\n            height: 50,\n            background: 'red',\n          }}\n        />\n        <div\n          style={{\n            width: 50,\n            height: 50,\n            background: 'blue',\n            marginLeft: -20,\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render margin with flexbox column container', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'column',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n        }}\n      >\n        <div\n          style={{\n            width: 40,\n            height: 20,\n            background: 'red',\n            margin: 5,\n          }}\n        />\n        <div\n          style={{\n            width: 40,\n            height: 20,\n            background: 'blue',\n            margin: 5,\n          }}\n        />\n        <div\n          style={{\n            width: 40,\n            height: 20,\n            background: 'green',\n            margin: 5,\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render margin with flexbox row container', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'row',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n        }}\n      >\n        <div\n          style={{\n            width: 20,\n            height: 40,\n            background: 'red',\n            margin: 5,\n          }}\n        />\n        <div\n          style={{\n            width: 20,\n            height: 40,\n            background: 'blue',\n            margin: 5,\n          }}\n        />\n        <div\n          style={{\n            width: 20,\n            height: 40,\n            background: 'green',\n            margin: 5,\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render margin auto horizontally', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n          display: 'flex',\n        }}\n      >\n        <div\n          style={{\n            width: 40,\n            height: 40,\n            background: 'red',\n            margin: '0 auto',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render marginLeft auto', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n          display: 'flex',\n        }}\n      >\n        <div\n          style={{\n            width: 40,\n            height: 40,\n            background: 'blue',\n            marginLeft: 'auto',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render marginRight auto', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n          display: 'flex',\n        }}\n      >\n        <div\n          style={{\n            width: 40,\n            height: 40,\n            background: 'green',\n            marginRight: 'auto',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render margin with different units', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n        }}\n      >\n        <div\n          style={{\n            width: 40,\n            height: 40,\n            background: 'purple',\n            margin: '10px',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render zero margin', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n        }}\n      >\n        <div\n          style={{\n            width: 50,\n            height: 50,\n            background: 'orange',\n            margin: 0,\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render margin collapsing with siblings', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n          display: 'flex',\n          flexDirection: 'column',\n        }}\n      >\n        <div\n          style={{\n            width: 80,\n            height: 30,\n            background: 'red',\n            marginBottom: 20,\n          }}\n        />\n        <div\n          style={{\n            width: 80,\n            height: 30,\n            background: 'blue',\n            marginTop: 10,\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render margin with nested elements', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            width: 80,\n            height: 80,\n            background: 'yellow',\n            margin: 10,\n          }}\n        >\n          <div\n            style={{\n              width: 40,\n              height: 40,\n              background: 'red',\n              margin: 20,\n            }}\n          />\n        </div>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render margin with positioned elements', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n          position: 'relative',\n        }}\n      >\n        <div\n          style={{\n            position: 'absolute',\n            top: 0,\n            left: 0,\n            width: 40,\n            height: 40,\n            background: 'red',\n            margin: 20,\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render large margin values', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n        }}\n      >\n        <div\n          style={{\n            width: 20,\n            height: 20,\n            background: 'red',\n            margin: 40,\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render asymmetric margins', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n        }}\n      >\n        <div\n          style={{\n            width: 40,\n            height: 40,\n            background: 'teal',\n            marginTop: 5,\n            marginRight: 30,\n            marginBottom: 10,\n            marginLeft: 15,\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render margin with text content', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 100,\n          height: 100,\n          background: 'lightgray',\n          display: 'flex',\n          flexDirection: 'column',\n        }}\n      >\n        <div\n          style={{\n            fontSize: 20,\n            background: 'white',\n            margin: 10,\n          }}\n        >\n          Hello\n        </div>\n        <div\n          style={{\n            fontSize: 20,\n            background: 'white',\n            margin: 10,\n          }}\n        >\n          World\n        </div>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/mask-image.test.tsx",
    "content": "import { it, describe, expect, beforeEach, afterEach } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\nconst PNG_SAMPLE =\n  'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAAAXNSR0IArs4c6QAAAIRlWElmTU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABIAAAAAQAAAEgAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAABSgAwAEAAAAAQAAAA8AAAAAVtc7bQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDYuMC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KGV7hBwAAA1FJREFUOBGNVFtME0EUnd3S3T5paamRxidoihRKRVGJkaDBRCFNiMkSP9QPiaHhgwRREwOGEUn8MfERIDEqMfxZEtEoSviQGh8Qg4YagQCipVIepdoWSh+03XFndRXkx/uxc+fOPWfO3nt3AfhtDGMTCf6VK3PFFRVTDADdGhyDEJEAAUI4z+7Mzmj1t6bwewhIIY5XPgkDICRYAMZ1Vmv0FkEsH5qcFKFd+bKRDwPUsa6uzT4BVNVZBbVJ2lMBScDPqtm65vzm54yNEXWUdyRwDomVYbL2doe8uDjYtrgYOUpRsUggEBxXK1H+lg3B8wJZU0/jHcmyuNr9w+1eCCxok8PJLYo3inWYDCLIKyWNRoZXOTNDq0pLqWyzWbQ8MoJCuSZasRQKB1sGiQMC4SxayhPLVN5t8gxkSM0MO/1OWU2ixorP096n8SUT2e2XWQh7k+rr8wNFRWc3arTKYplE5I2RbIKmpSrLd7enrdqkrdxXWJE25VJ7iFCi3nPDr/MpKXdoVu4MOV+7Ol1vg/og6bQ72aRftxdx9QMAoXVXFZJAYXqmcmfUuTRVKLIvyPI+b/X9kDbNP3o4p99fAooIMnpAv0cnl6fQYNF9/dXJ/msYa2+wJwD83RQcEBozYJtQDWnTDQpYW5aG2svD2oJw8nYDq8zNFROaFBkxMhrv9tTerj5+ugfktTkwlp8Agnty9mcU8IYfD0jgi9g+E7BE9lW1RmLRWb3ZpCRpWuy1Wucle3anqvzr67LGnj6wMUaq3DgU5wD8G2KOVTOEu81AyJeBlJSMijXJy7RCop1ou6Yaugv18aZGg+7chYxwWWYOBhuZBk7FXzIc48HYESwLwhinnSDePRsbLC3oi8/4LEvmg4Th8BFSGYvHfP39A/Hxb/dwvmd4+I8yAb9KIQ5yNUAdkBFj//smx2N2BxtGEf/o/KcXX6YTL6Pegvt1e590fB2yQeoghHGct9LWEPKHDQw/9VSuaWw6RwkWLXrNZA6d5NJRKlnGJV6VR7f6VQXSVU0RgnhF3LdLcJ173nXmYiyhPhGNBlxikdQx9/FmYyUEIeF8Jea//d7eMrXNBqQCAJMJ/r/rmqasTEAIcUCCk/rIj+OQ/7NAbg/XNEPA/QQBqVjfA25FYgAAAABJRU5ErkJggg=='\n\nlet fonts\ninitFonts((f) => (fonts = f))\n\nlet requests = []\n\nbeforeEach(() => {\n  // Polyfill fetch\n  requests = []\n  ;(globalThis as any).fetch = async (url) => {\n    requests.push(url)\n    if (url.includes('wrong-url')) {\n      throw Error('wrong url')\n    } else if (url.startsWith('data:')) {\n      return {\n        headers: {\n          get: () => 'image/png',\n        },\n        text: async () => {\n          const binary_string = atob(url.replace('data:image/png;base64,', ''))\n          const len = binary_string.length\n          const bytes = new Uint8Array(len)\n          for (let i = 0; i < len; i++) {\n            bytes[i] = binary_string.charCodeAt(i)\n          }\n          return bytes.buffer\n        },\n      }\n    }\n\n    if (url.endsWith('.svg')) {\n      return {\n        headers: {\n          get: () => 'image/svg+xml',\n        },\n        text: async () =>\n          '<svg width=\"116.15\" height=\"100\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M57.5 0L115 100H0L57.5 0z\"/></svg>',\n      }\n    }\n\n    return {\n      headers: {\n        get: (key) => {\n          if (key === 'content-type') return 'image/png'\n        },\n      },\n      arrayBuffer: async () => {\n        // 1x1 #00F blue image.\n        const binary_string = atob(\n          `iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPj/HwADBwIAMCbHYQAAAABJRU5ErkJggg==`\n        )\n        const len = binary_string.length\n        const bytes = new Uint8Array(len)\n        for (let i = 0; i < len; i++) {\n          bytes[i] = binary_string.charCodeAt(i)\n        }\n        return bytes.buffer\n      },\n    }\n  }\n})\n\nafterEach(() => {\n  delete globalThis.fetch\n})\n\ndescribe('Mask-*', () => {\n  it('should support mask-image', async () => {\n    const svgs = await Promise.all(\n      [\n        'linear-gradient(to right, blue, transparent)',\n        'radial-gradient(circle at 50% 50%, blue, transparent)',\n        'url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAwIiBoZWlnaHQ9IjgwMCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTIwMCAxMjAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xMCwzNSBBMjAsMjAsMCwwLDEsNTAsMzUgQTIwLDIwLDAsMCwxLDkwLDM1IFE5MCw2NSw1MCw5NSBRMTAsNjUsMTAsMzUgWiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+)',\n      ].map((maskImage) =>\n        satori(\n          <div\n            style={{\n              width: '100%',\n              height: '100%',\n              display: 'flex',\n              backgroundImage: `url(${PNG_SAMPLE})`,\n              maskImage,\n            }}\n          ></div>,\n          { width: 100, height: 100, fonts }\n        )\n      )\n    )\n\n    svgs.forEach((svg) => {\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n  it('should support mask-image on img', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n        }}\n      >\n        <img\n          src={PNG_SAMPLE}\n          width='100%'\n          height='100%'\n          style={{\n            maskImage: 'linear-gradient(to right, blue, transparent)',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n  it('should support mask-size', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          backgroundImage: `url(${PNG_SAMPLE})`,\n          maskImage: 'linear-gradient(to right, blue, transparent)',\n          maskSize: '50px 50%',\n        }}\n      ></div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n  it('should support mask-position', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          backgroundImage: `url(${PNG_SAMPLE})`,\n          maskImage: 'linear-gradient(to right, blue, transparent)',\n          maskSize: '10px 10px',\n          maskPosition: '10px 10px',\n        }}\n      ></div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n  it('should support mask-repeat', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          backgroundImage: `url(${PNG_SAMPLE})`,\n          maskImage: 'linear-gradient(to right, blue, transparent)',\n          maskSize: '10px 10px',\n          maskRepeat: 'repeat-x',\n        }}\n      ></div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support mask-image on text', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          backgroundImage: `url(${PNG_SAMPLE})`,\n          maskImage: 'linear-gradient(to right, blue, transparent)',\n          color: 'white',\n        }}\n      >\n        Lynnnnn6666666\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support multiple mask-image', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          backgroundImage: `url(${PNG_SAMPLE})`,\n          maskImage: [\n            'url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAwIiBoZWlnaHQ9IjgwMCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTIwMCAxMjAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xMCwzNSBBMjAsMjAsMCwwLDEsNTAsMzUgQTIwLDIwLDAsMCwxLDkwLDM1IFE5MCw2NSw1MCw5NSBRMTAsNjUsMTAsMzUgWiIgZmlsbD0id2hpdGUiIC8+PC9zdmc+)',\n            'radial-gradient(circle at 100% 100%, blue, transparent)',\n          ].join(','),\n          color: 'white',\n        }}\n      ></div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support mask-image on positioned elements', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n        }}\n      >\n        <div\n          style={{\n            position: 'absolute',\n            top: 20,\n            left: 20,\n            height: 100,\n            width: 100,\n            display: 'flex',\n            background: 'green',\n            maskImage:\n              \"url(data:image/svg+xml,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='white' width='100' height='100' /%3E%3C/svg%3E)\",\n            border: '1px solid red',\n          }}\n        ></div>\n      </div>,\n      { width: 120, height: 120, fonts }\n    )\n    expect(toImage(svg, 120)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/opacity.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Opacity', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should render element with opacity 0', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 50,\n          height: 50,\n          background: 'red',\n          opacity: 0,\n        }}\n      />,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render element with opacity 0.5', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 50,\n          height: 50,\n          background: 'red',\n          opacity: 0.5,\n        }}\n      />,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render element with opacity 1', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 50,\n          height: 50,\n          background: 'red',\n          opacity: 1,\n        }}\n      />,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should apply opacity to text elements', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 40,\n          color: 'black',\n          opacity: 0.5,\n        }}\n      >\n        Hello World\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should combine opacity with linear gradients', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 100,\n          height: 100,\n          background: 'linear-gradient(to right, red, blue)',\n          opacity: 0.7,\n        }}\n      />,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should combine opacity with radial gradients', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 100,\n          height: 100,\n          background: 'radial-gradient(circle, yellow, green)',\n          opacity: 0.6,\n        }}\n      />,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should cascade opacity through nested elements', async () => {\n    const svg = await satori(\n      <div style={{ display: 'flex', opacity: 0.5 }}>\n        <div\n          style={{\n            width: 50,\n            height: 50,\n            background: 'red',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should combine multiple opacity values in nested elements', async () => {\n    const svg = await satori(\n      <div style={{ display: 'flex', opacity: 0.8 }}>\n        <div\n          style={{\n            width: 50,\n            height: 50,\n            background: 'red',\n            opacity: 0.5,\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should apply opacity to elements with box-shadow', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 50,\n          height: 50,\n          margin: '25px 25px',\n          background: 'white',\n          boxShadow: '10px 10px 10px rgba(0, 0, 0, 0.5)',\n          opacity: 0.6,\n        }}\n      />,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should apply opacity to text with text-shadow', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 40,\n          color: 'black',\n          textShadow: '2px 2px 2px red',\n          opacity: 0.7,\n        }}\n      >\n        Hello\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should apply opacity to elements with border', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 50,\n          height: 50,\n          background: 'white',\n          border: '5px solid red',\n          opacity: 0.5,\n        }}\n      />,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should apply opacity to elements with border-radius', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 50,\n          height: 50,\n          background: 'blue',\n          borderRadius: 25,\n          opacity: 0.5,\n        }}\n      />,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should apply opacity with transform', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 50,\n          height: 50,\n          background: 'green',\n          transform: 'rotate(45deg)',\n          opacity: 0.6,\n        }}\n      />,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should handle opacity 0 with nested content', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 100,\n          height: 100,\n          background: 'red',\n          opacity: 0,\n        }}\n      >\n        <div\n          style={{\n            fontSize: 20,\n            color: 'white',\n          }}\n        >\n          Hidden Text\n        </div>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should apply opacity to flex container', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'row',\n          width: 100,\n          height: 100,\n          gap: 10,\n          opacity: 0.5,\n        }}\n      >\n        <div style={{ width: 40, height: 40, background: 'red' }} />\n        <div style={{ width: 40, height: 40, background: 'blue' }} />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should apply opacity to positioned elements', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 100,\n          height: 100,\n          position: 'relative',\n        }}\n      >\n        <div\n          style={{\n            position: 'absolute',\n            top: 20,\n            left: 20,\n            width: 50,\n            height: 50,\n            background: 'purple',\n            opacity: 0.4,\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should combine opacity with background-clip text', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          fontSize: 40,\n          background: 'linear-gradient(90deg, #ff00bc, #6400ff)',\n          backgroundClip: 'text',\n          color: 'transparent',\n          opacity: 0.7,\n        }}\n      >\n        Hello\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should apply very low opacity', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 50,\n          height: 50,\n          background: 'black',\n          opacity: 0.1,\n        }}\n      />,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should apply near-full opacity', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 50,\n          height: 50,\n          background: 'black',\n          opacity: 0.99,\n        }}\n      />,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should apply opacity to multiple siblings', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'column',\n          width: 100,\n          height: 100,\n        }}\n      >\n        <div\n          style={{\n            width: 100,\n            height: 30,\n            background: 'red',\n            opacity: 0.3,\n          }}\n        />\n        <div\n          style={{\n            width: 100,\n            height: 30,\n            background: 'green',\n            opacity: 0.6,\n          }}\n        />\n        <div\n          style={{\n            width: 100,\n            height: 30,\n            background: 'blue',\n            opacity: 0.9,\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should apply opacity to overlapping elements', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 100,\n          height: 100,\n          position: 'relative',\n        }}\n      >\n        <div\n          style={{\n            position: 'absolute',\n            top: 0,\n            left: 0,\n            width: 60,\n            height: 60,\n            background: 'red',\n            opacity: 0.5,\n          }}\n        />\n        <div\n          style={{\n            position: 'absolute',\n            top: 40,\n            left: 40,\n            width: 60,\n            height: 60,\n            background: 'blue',\n            opacity: 0.5,\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/overflow.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Overflow', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should not show overflowed text', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 15,\n          height: 15,\n          backgroundColor: 'white',\n          overflow: 'hidden',\n        }}\n      >\n        Hello\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should work with nested border, border-radius, padding', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          border: '10px solid rgba(0,0,0,0.5)',\n          borderRadius: '100px 20%',\n          display: 'flex',\n          overflow: 'hidden',\n          background: 'green',\n          padding: 5,\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            height: '100%',\n            background: 'red',\n            borderRadius: '0% 60%',\n            display: 'flex',\n            padding: 5,\n            overflow: 'hidden',\n          }}\n        >\n          <div style={{ width: '100%', height: '100%', background: 'blue' }}>\n            Satori\n          </div>\n        </div>\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should work with ellipsis, nowrap', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          height: '100%',\n          width: '100%',\n          alignItems: 'center',\n          justifyContent: 'center',\n          flexDirection: 'column',\n          backgroundColor: 'white',\n          fontSize: 60,\n          fontWeight: 400,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'column',\n            width: 450,\n            overflow: 'hidden',\n            whiteSpace: 'nowrap',\n          }}\n        >\n          <div\n            style={{\n              width: 450,\n              textOverflow: 'ellipsis',\n              overflow: 'hidden',\n            }}\n          >\n            {'LuciNyan 1 2 345'}\n          </div>\n          <div\n            style={{\n              width: 450,\n              textOverflow: 'ellipsis',\n              overflow: 'hidden',\n            }}\n          >\n            {'LuciNyan 1 2 345 6'}\n          </div>\n        </div>\n      </div>,\n      { width: 450, height: 450, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 450)).toMatchImageSnapshot()\n  })\n\n  it(\"should not work when overflow is not 'hidden' and overflow property should not be inherited\", async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          height: '100%',\n          width: '100%',\n          alignItems: 'center',\n          justifyContent: 'center',\n          flexDirection: 'column',\n          backgroundColor: 'white',\n          fontSize: 60,\n          fontWeight: 400,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'column',\n            width: 450,\n            overflow: 'hidden',\n            whiteSpace: 'nowrap',\n          }}\n        >\n          <div\n            style={{\n              width: 450,\n              textOverflow: 'ellipsis',\n            }}\n          >\n            {'LuciNyan 1 2 345'}\n          </div>\n          <div\n            style={{\n              width: 450,\n              textOverflow: 'ellipsis',\n            }}\n          >\n            {'LuciNyan 1 2 345 6'}\n          </div>\n        </div>\n      </div>,\n      { width: 450, height: 450, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 450)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/padding.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Padding', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should render element with padding shorthand (1 value)', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 60,\n          height: 60,\n          background: 'lightblue',\n          padding: 20,\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            height: '100%',\n            background: 'red',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render element with padding shorthand (2 values)', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 60,\n          height: 60,\n          background: 'lightblue',\n          padding: '10px 20px',\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            height: '100%',\n            background: 'red',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render element with padding shorthand (3 values)', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 60,\n          height: 60,\n          background: 'lightblue',\n          padding: '10px 15px 20px',\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            height: '100%',\n            background: 'red',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render element with padding shorthand (4 values)', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 60,\n          height: 60,\n          background: 'lightblue',\n          padding: '5px 10px 15px 20px',\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            height: '100%',\n            background: 'red',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render element with individual padding properties', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 60,\n          height: 60,\n          background: 'lightgreen',\n          paddingTop: 10,\n          paddingRight: 15,\n          paddingBottom: 20,\n          paddingLeft: 5,\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            height: '100%',\n            background: 'blue',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render padding with text content', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          background: 'yellow',\n          padding: 20,\n          fontSize: 20,\n        }}\n      >\n        Hello World\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render padding with border', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 60,\n          height: 60,\n          background: 'lightblue',\n          padding: 15,\n          border: '5px solid red',\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            height: '100%',\n            background: 'white',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render padding with border-radius', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 60,\n          height: 60,\n          background: 'lightcoral',\n          padding: 10,\n          borderRadius: 15,\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            height: '100%',\n            background: 'white',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render zero padding', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 50,\n          height: 50,\n          background: 'lightgray',\n          padding: 0,\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            height: '100%',\n            background: 'orange',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render padding with flexbox column container', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'column',\n          width: 80,\n          height: 80,\n          background: 'lightblue',\n          padding: 10,\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            height: 20,\n            background: 'red',\n          }}\n        />\n        <div\n          style={{\n            width: '100%',\n            height: 20,\n            background: 'blue',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render padding with flexbox row container', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'row',\n          width: 80,\n          height: 80,\n          background: 'lightgreen',\n          padding: 10,\n        }}\n      >\n        <div\n          style={{\n            width: 20,\n            height: '100%',\n            background: 'red',\n          }}\n        />\n        <div\n          style={{\n            width: 20,\n            height: '100%',\n            background: 'blue',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render large padding values', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 20,\n          height: 20,\n          background: 'pink',\n          padding: 40,\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            height: '100%',\n            background: 'purple',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render asymmetric padding', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 50,\n          height: 50,\n          background: 'lightcyan',\n          paddingTop: 5,\n          paddingRight: 25,\n          paddingBottom: 10,\n          paddingLeft: 15,\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            height: '100%',\n            background: 'teal',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render padding with nested elements', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 80,\n          height: 80,\n          background: 'lavender',\n          padding: 10,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            width: '100%',\n            height: '100%',\n            background: 'violet',\n            padding: 10,\n          }}\n        >\n          <div\n            style={{\n              width: '100%',\n              height: '100%',\n              background: 'white',\n            }}\n          />\n        </div>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render padding with multiple text lines', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          background: 'lightyellow',\n          padding: 15,\n          fontSize: 16,\n          width: 80,\n        }}\n      >\n        This is a text with padding that wraps\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render padding with gradient background', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 60,\n          height: 60,\n          background: 'linear-gradient(to right, red, blue)',\n          padding: 20,\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            height: '100%',\n            background: 'white',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render padding with box-shadow', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 60,\n          height: 60,\n          background: 'white',\n          padding: 15,\n          boxShadow: '5px 5px 10px rgba(0, 0, 0, 0.3)',\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            height: '100%',\n            background: 'lightblue',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render different padding on each side', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 40,\n          height: 40,\n          background: 'lightgray',\n          paddingTop: 30,\n          paddingRight: 10,\n          paddingBottom: 5,\n          paddingLeft: 20,\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            height: '100%',\n            background: 'navy',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render padding with opacity', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 60,\n          height: 60,\n          background: 'blue',\n          padding: 20,\n          opacity: 0.5,\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            height: '100%',\n            background: 'yellow',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render padding with transform', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          width: 50,\n          height: 50,\n          background: 'lightpink',\n          padding: 15,\n          transform: 'rotate(15deg)',\n        }}\n      >\n        <div\n          style={{\n            width: '100%',\n            height: '100%',\n            background: 'red',\n          }}\n        />\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/pixel-font.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\nimport { readFileSync } from 'fs'\nimport { join } from 'path'\n\ndescribe('Pixel Font Alignment', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('Should align pixel and hinted fonts with pixel boundaries', async () => {\n    const habboFont = readFileSync(join(process.cwd(), 'test/assets/Habbo.ttf'))\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 16,\n          fontFamily: 'Habbo',\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            flexDirection: 'column',\n            alignItems: 'center',\n            width: '160px',\n            padding: '16px',\n            textAlign: 'center',\n          }}\n        >\n          <div style={{ lineHeight: '16px', marginBottom: '8px' }}>\n            Pixel Perfect Text Aligment Pixel Perfect Text Aligment Pixel\n            Perfect Text Aligment Pixel Perfect Text Aligment Pixel Perfect Text\n            Aligment\n          </div>\n          <div style={{ lineHeight: '16px', marginBottom: '8px' }}>\n            Pixel Perfect Text Aligment Pixel Perfect Text Aligment Pixel\n            Perfect Text Aligment Pixel Perfect Text Aligment Pixel Perfect Text\n            Aligment\n          </div>\n          <div style={{ lineHeight: '16px' }}>Test</div>\n        </div>\n      </div>,\n      {\n        width: 200,\n        height: 200,\n        fonts: [\n          {\n            name: 'Habbo',\n            data: habboFont,\n            weight: 400,\n            style: 'normal',\n          },\n        ],\n        embedFont: true,\n      }\n    )\n\n    // Check path coordinates for integer values\n    const integerPathCoordinates = (svg.match(/\\bd=\"[^\"]*\"/g) ?? []).every(\n      (pathData) => {\n        const coordinates = pathData\n          .match(/\\bd=\"([^\"]+)\"/)?.[1]\n          .match(/[-]?\\d+(?:\\.\\d+)?/g)\n          .map(Number)\n\n        const nonIntegers = coordinates.filter((v) => !Number.isInteger(v))\n        if (nonIntegers.length > 0) {\n          console.log('Non-integer coordinates found:', nonIntegers)\n        }\n\n        return nonIntegers.length === 0\n      }\n    )\n\n    expect(integerPathCoordinates).toBe(true)\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/position.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Position', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  describe('absolute', () => {\n    it('should support absolute position', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            height: '100%',\n            width: '100%',\n            display: 'flex',\n          }}\n        >\n          <div\n            style={{\n              position: 'absolute',\n              bottom: 0,\n              right: 0,\n              width: 10,\n              height: 10,\n              background: 'black',\n            }}\n          ></div>\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    // https://www.yogalayout.dev/blog/announcing-yoga-3.0#better-support-for-absolute-positioning\n    it('should have correct size calculation of absolutely positioned elements', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            height: '100%',\n            width: '100%',\n            display: 'flex',\n            padding: 10,\n            background: 'red',\n          }}\n        >\n          <div\n            style={{\n              position: 'absolute',\n              height: '25%',\n              width: '25%',\n              background: 'black',\n            }}\n          ></div>\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('static', () => {\n    it('should support static position', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            height: '100%',\n            width: '100%',\n            display: 'flex',\n          }}\n        >\n          <div\n            style={{\n              position: 'static',\n              left: 10,\n              top: 10,\n              bottom: 0,\n              right: 0,\n              width: 10,\n              height: 10,\n              background: 'black',\n            }}\n          ></div>\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('relative', () => {\n    it('should support relative position', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            height: '100%',\n            width: '100%',\n            display: 'flex',\n          }}\n        >\n          <div\n            style={{\n              position: 'relative',\n              left: 10,\n              top: 10,\n              bottom: 0,\n              right: 0,\n              width: 10,\n              height: 10,\n              background: 'black',\n            }}\n          ></div>\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n})\n"
  },
  {
    "path": "test/react.test.tsx",
    "content": "import { forwardRef } from 'react'\nimport { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('React APIs', () => {\n  let fonts: any\n  initFonts((f) => (fonts = f))\n\n  it('should support `forwardRef` wrapped components', async () => {\n    const Foo = forwardRef(function _() {\n      return <div>hello</div>\n    })\n\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          color: 'red',\n          fontSize: 14,\n        }}\n      >\n        <Foo />\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/shadow.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Shadow', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  describe('box-shadow', () => {\n    it('should render regular box shadow', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 50,\n            height: 50,\n            margin: '25px 25px',\n            background: 'white',\n            boxShadow: '0 0 10px green',\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render box shadow with offset', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 50,\n            height: 50,\n            margin: '25px 25px',\n            background: 'white',\n            boxShadow: '10px -20px 10px red',\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render box shadow with offset and spread', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 50,\n            height: 50,\n            margin: '25px 25px',\n            background: 'white',\n            boxShadow: '10px -10px 10px 10px red',\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render multiple box shadows', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 50,\n            height: 50,\n            margin: '25px 25px',\n            background: 'white',\n            boxShadow: '10px -10px 10px 10px red, -10px -10px 10px 10px blue',\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support negative spread', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 50,\n            height: 50,\n            margin: '25px 25px',\n            background: 'white',\n            borderRadius: 20,\n            boxShadow: '20px -20px 10px -10px red, -20px -20px 10px -5px blue',\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support box shadow for transparent elements', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 50,\n            height: 50,\n            margin: '25px 25px',\n            borderRadius: 20,\n            boxShadow: '20px -20px 10px 5px red',\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support box shadow spread with transparency', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 50,\n            height: 50,\n            margin: '25px 25px',\n            borderRadius: 20,\n            boxShadow: '20px -20px 4px 5px rgba(0, 0, 0, 0.5)',\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support inset box shadows', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 50,\n            height: 50,\n            margin: '25px 25px',\n            borderRadius: 20,\n            boxShadow: '20px -20px 4px 5px inset rgba(0, 0, 0, 0.5)',\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should be affected by container opacity', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 50,\n            height: 50,\n            margin: '25px 25px',\n            borderRadius: 20,\n            boxShadow: '10px 10px 4px 5px black',\n            opacity: 0.5,\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should work correct with zero border radius', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 50,\n            height: 50,\n            margin: '25px 25px',\n            borderRadius: '0%',\n            boxShadow: '0px 0px 0px 10px black',\n          }}\n        ></div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should show box shadow without specifying height', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            width: 100,\n            padding: 10,\n            background: 'white',\n          }}\n        >\n          <div\n            style={{\n              display: 'flex',\n              boxShadow: '10px 10px 10px green',\n              width: `50px`,\n              height: '50px',\n              background: 'rgba(0,0,0,0.5)',\n            }}\n          ></div>\n        </div>,\n        { width: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support multiple text shadows', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            background: 'white',\n            width: 100,\n            height: 100,\n            fontSize: 40,\n            textShadow: '2px 2px 2px red, 4px .25rem .25rem blue',\n          }}\n        >\n          Hello\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support text shadows if exist unexpected comma', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            background: 'white',\n            width: 100,\n            height: 100,\n            fontSize: 40,\n            textShadow:\n              '2px 2px red, 4px 4px #4bf542, 6px 6px rgb(186, 147, 17)',\n          }}\n        >\n          Lynn\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support text shadows with transparent text color', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            background: 'white',\n            width: 100,\n            height: 100,\n            fontSize: 40,\n            color: 'transparent',\n            textShadow: 'red 0px 0px 5px',\n          }}\n        >\n          Hello\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support text shadows with backgroundClip text', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 100,\n            height: 100,\n            fontSize: 40,\n            background: 'linear-gradient(90deg, #ff00bc, #6400ff)',\n            backgroundClip: 'text',\n            color: '#ffffffff',\n            textShadow: '0px 0px 5px #000',\n          }}\n        >\n          Hello\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support text shadows with backgroundClip and no background', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 100,\n            height: 100,\n            fontSize: 40,\n            backgroundClip: 'text',\n            color: 'white',\n            textShadow:\n              '0px 0px 3px #9400d3,0px 0px 4px #9400d3,0px 0px 5px #9400d3,0px 0px 5px #9400d3,0px 0px 5px #9400d3,0px 0px 5px #9400d3,0px 0px 5px #9400d3,0px 0px 5px #9400d3,0px 0px 5px #9400d3,0px 0px 5px #9400d3',\n          }}\n        >\n          Hello\n        </div>,\n        { width: 100, height: 100, fonts }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n})\n"
  },
  {
    "path": "test/svg.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('SVG', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should render svg nodes', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          background: 'blue',\n          display: 'flex',\n        }}\n      >\n        <svg viewBox='0 0 100 100'>\n          <circle\n            cx='50'\n            cy='50'\n            r='10'\n            stroke='black'\n            strokeWidth='3'\n            fill='red'\n          />\n        </svg>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render svg attributes correctly', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          background: 'blue',\n          display: 'flex',\n        }}\n      >\n        <svg\n          viewBox='0 0 100 100'\n          fill='yellow'\n          xmlns='http://www.w3.org/2000/svg'\n        >\n          <circle\n            cx='50'\n            cy='50'\n            r='10'\n            stroke='black'\n            strokeWidth='3'\n            fill='red'\n          />\n        </svg>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render svg size correctly', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          background: 'blue',\n          display: 'flex',\n        }}\n      >\n        <svg\n          width='100'\n          viewBox='0 0 10 10'\n          fill='yellow'\n          xmlns='http://www.w3.org/2000/svg'\n        >\n          <circle\n            cx='5'\n            cy='5'\n            r='4'\n            stroke='black'\n            strokeWidth='3'\n            fill='red'\n          />\n        </svg>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should parse viewBox correctly', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          background: 'blue',\n          display: 'flex',\n        }}\n      >\n        <svg\n          height='100'\n          viewBox='0, 0,10.5 20.5 '\n          fill='yellow'\n          xmlns='http://www.w3.org/2000/svg'\n        >\n          <circle\n            cx='5'\n            cy='5'\n            r='4'\n            stroke='black'\n            strokeWidth='3'\n            fill='red'\n          />\n        </svg>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support em in svg size', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          background: 'blue',\n          display: 'flex',\n        }}\n      >\n        <svg\n          height='5em'\n          viewBox='0, 0,10.5 20.5 '\n          fill='yellow'\n          xmlns='http://www.w3.org/2000/svg'\n        >\n          <circle\n            cx='5'\n            cy='5'\n            r='4'\n            stroke='black'\n            strokeWidth='3'\n            fill='red'\n          />\n        </svg>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support currentColor for svg fill', async () => {\n    const svg = await satori(\n      <svg\n        width='40'\n        height='40'\n        viewBox='0 0 15 15'\n        fill='none'\n        style={{ color: 'black' }}\n        xmlns='http://www.w3.org/2000/svg'\n      >\n        <path\n          d='M0.877014 7.49988C0.877014 3.84219 3.84216 0.877045 7.49985 0.877045C11.1575 0.877045 14.1227 3.84219 14.1227 7.49988C14.1227 11.1575 11.1575 14.1227 7.49985 14.1227C3.84216 14.1227 0.877014 11.1575 0.877014 7.49988ZM7.49985 1.82704C4.36683 1.82704 1.82701 4.36686 1.82701 7.49988C1.82701 8.97196 2.38774 10.3131 3.30727 11.3213C4.19074 9.94119 5.73818 9.02499 7.50023 9.02499C9.26206 9.02499 10.8093 9.94097 11.6929 11.3208C12.6121 10.3127 13.1727 8.97172 13.1727 7.49988C13.1727 4.36686 10.6328 1.82704 7.49985 1.82704ZM10.9818 11.9787C10.2839 10.7795 8.9857 9.97499 7.50023 9.97499C6.01458 9.97499 4.71624 10.7797 4.01845 11.9791C4.97952 12.7272 6.18765 13.1727 7.49985 13.1727C8.81227 13.1727 10.0206 12.727 10.9818 11.9787ZM5.14999 6.50487C5.14999 5.207 6.20212 4.15487 7.49999 4.15487C8.79786 4.15487 9.84999 5.207 9.84999 6.50487C9.84999 7.80274 8.79786 8.85487 7.49999 8.85487C6.20212 8.85487 5.14999 7.80274 5.14999 6.50487ZM7.49999 5.10487C6.72679 5.10487 6.09999 5.73167 6.09999 6.50487C6.09999 7.27807 6.72679 7.90487 7.49999 7.90487C8.27319 7.90487 8.89999 7.27807 8.89999 6.50487C8.89999 5.73167 8.27319 5.10487 7.49999 5.10487Z'\n          fill='currentColor'\n          fillRule='evenodd'\n          clipRule='evenodd'\n        />\n      </svg>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support currentColor for svg stroke', async () => {\n    const svg = await satori(\n      <svg\n        viewBox='0 0 20 10'\n        xmlns='http://www.w3.org/2000/svg'\n        style={{ color: 'blue' }}\n      >\n        <circle cx='5' cy='5' r='4' fill='none' stroke='currentColor' />\n      </svg>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support currentColor when color is set on parent element', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 32,\n          fontWeight: 600,\n          color: 'red',\n        }}\n      >\n        <svg\n          width='75'\n          viewBox='0 0 75 65'\n          fill='currentColor'\n          style={{ margin: '0 75px' }}\n        >\n          <path d='M37.59.25l36.95 64H.64l36.95-64z'></path>\n        </svg>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render svg prefer size props rather than viewBox', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100px',\n          height: '100px',\n          background: '#8250df',\n          display: 'flex',\n        }}\n      >\n        <svg\n          xmlns='http://www.w3.org/2000/svg'\n          width='60'\n          viewBox='0 0 100 100'\n        >\n          <circle cx='50' cy='50' r='50' fill='red' />\n        </svg>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support currentColor when used on svg nodes', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          color: 'red',\n        }}\n      >\n        <svg\n          width='75'\n          viewBox='0 0 75 65'\n          fill='#000'\n          style={{ margin: '0 75px' }}\n        >\n          <path\n            stroke='currentColor'\n            d='M37.59.25l36.95 64H.64l36.95-64z'\n          ></path>\n        </svg>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should render svg without viewBox', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100px',\n          height: '100px',\n          background: '#1a73e8',\n          display: 'flex',\n        }}\n      >\n        <svg xmlns='http://www.w3.org/2000/svg' width='30' height='30'>\n          <circle cx='50' cy='50' r='50' fill='red' />\n        </svg>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  // TODO wait for @resvg/resvg-js to support mask-type\n  it('should respect style on svg node', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 32,\n          fontWeight: 600,\n        }}\n      >\n        <svg xmlns='http://www.w3.org/2000/svg' width='30' height='30'>\n          <circle cx='50' cy='50' r='50' style={{ fill: 'gold' }} />\n        </svg>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/tab-size.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('tab-size', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it(\"Tab renders as space when white-space is not 'pre' or 'pre-wrap'\", async () => {\n    const tab = String.fromCodePoint(0x09)\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          backgroundColor: '#fff',\n        }}\n      >\n        <div style={{ display: 'flex' }}>{tab}one tab</div>\n        <div style={{ display: 'flex', tabSize: 8 }}>{tab}one tab</div>\n        <div style={{ display: 'flex' }}>1{tab}one tab</div>\n        <div style={{ display: 'flex' }}>1111{tab}one tab</div>\n        <div style={{ display: 'flex' }}>\n          {tab}\n          {tab}two tabs\n        </div>\n        <div style={{ display: 'flex' }}>\n          1{tab}\n          {tab}two tabs\n        </div>\n        <div style={{ display: 'flex' }}>\n          1{tab}\n          {tab}\n          {tab}three tabs\n        </div>\n      </div>,\n      {\n        width: 150,\n        height: 150,\n        fonts,\n      }\n    )\n\n    expect(toImage(svg, 150)).toMatchImageSnapshot()\n  })\n\n  it(\"Tabs render correctly with default tab-size of 8 when white-space is 'pre'\", async () => {\n    const tab = String.fromCodePoint(0x09)\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          backgroundColor: '#fff',\n          whiteSpace: 'pre',\n        }}\n      >\n        <div style={{ display: 'flex' }}>{tab}one tab</div>\n        <div style={{ display: 'flex', tabSize: 8 }}>{tab}one tab</div>\n        <div style={{ display: 'flex' }}>1{tab}one tab</div>\n        <div style={{ display: 'flex' }}>1111{tab}one tab</div>\n        <div style={{ display: 'flex' }}>\n          {tab}\n          {tab}two tabs\n        </div>\n        <div style={{ display: 'flex' }}>\n          1{tab}\n          {tab}two tabs\n        </div>\n        <div style={{ display: 'flex' }}>\n          1{tab}\n          {tab}\n          {tab}three tabs\n        </div>\n      </div>,\n      {\n        width: 150,\n        height: 150,\n        fonts,\n      }\n    )\n\n    expect(toImage(svg, 150)).toMatchImageSnapshot()\n  })\n\n  it('Tabs render correctly when tab-size is a number', async () => {\n    const tab = String.fromCodePoint(0x09)\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          backgroundColor: '#fff',\n          whiteSpace: 'pre',\n          tabSize: 4,\n        }}\n      >\n        <div style={{ display: 'flex' }}>{tab}one tab</div>\n        <div style={{ display: 'flex', tabSize: 8 }}>{tab}one tab</div>\n        <div style={{ display: 'flex' }}>1{tab}one tab</div>\n        <div style={{ display: 'flex' }}>1111{tab}one tab</div>\n        <div style={{ display: 'flex' }}>\n          {tab}\n          {tab}two tabs\n        </div>\n        <div style={{ display: 'flex' }}>\n          1{tab}\n          {tab}two tabs\n        </div>\n        <div style={{ display: 'flex' }}>\n          1{tab}\n          {tab}\n          {tab}three tabs\n        </div>\n      </div>,\n      {\n        width: 150,\n        height: 150,\n        fonts,\n      }\n    )\n\n    expect(toImage(svg, 150)).toMatchImageSnapshot()\n  })\n\n  it('Tabs render correctly when tab-size is a string', async () => {\n    const tab = String.fromCodePoint(0x09)\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          backgroundColor: '#fff',\n          whiteSpace: 'pre',\n          tabSize: '10px',\n        }}\n      >\n        <div style={{ display: 'flex' }}>{tab}one tab</div>\n        <div style={{ display: 'flex', tabSize: '1rem' }}>{tab}one tab</div>\n        <div style={{ display: 'flex' }}>1{tab}one tab</div>\n        <div style={{ display: 'flex' }}>1111{tab}one tab</div>\n        <div style={{ display: 'flex' }}>\n          {tab}\n          {tab}two tabs\n        </div>\n        <div style={{ display: 'flex' }}>\n          1{tab}\n          {tab}two tabs\n        </div>\n        <div style={{ display: 'flex' }}>\n          1{tab}\n          {tab}\n          {tab}three tabs\n        </div>\n      </div>,\n      {\n        width: 150,\n        height: 150,\n        fonts,\n      }\n    )\n\n    expect(toImage(svg, 150)).toMatchImageSnapshot()\n  })\n\n  it(\"Tabs render correctly with default tab-size of 8 when white-space is 'pre-wrap'\", async () => {\n    const tab = String.fromCodePoint(0x09)\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          backgroundColor: '#fff',\n          whiteSpace: 'pre-wrap',\n        }}\n      >\n        <div style={{ display: 'flex' }}>{tab}one tab</div>\n        <div style={{ display: 'flex', tabSize: 8 }}>{tab}one tab</div>\n        <div style={{ display: 'flex' }}>1{tab}one tab</div>\n        <div style={{ display: 'flex' }}>1111{tab}one tab</div>\n        <div style={{ display: 'flex' }}>\n          {tab}\n          {tab}two tabs\n        </div>\n        <div style={{ display: 'flex' }}>\n          1{tab}\n          {tab}two tabs\n        </div>\n        <div style={{ display: 'flex' }}>\n          1{tab}\n          {tab}\n          {tab}three tabs\n        </div>\n      </div>,\n      {\n        width: 150,\n        height: 150,\n        fonts,\n      }\n    )\n\n    expect(toImage(svg, 150)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/text-align.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Text Align', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('Should work correctly when `text-align: left`', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            maxWidth: '190px',\n            textAlign: 'left',\n            alignItems: 'center',\n            justifyContent: 'center',\n            backgroundColor: '#ff6c2f',\n            color: 'white',\n            letterSpacing: '1px',\n          }}\n        >\n          God kisses the finite in his love and man the infinite\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should work correctly when `text-align: center`', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            maxWidth: '190px',\n            textAlign: 'center',\n            alignItems: 'center',\n            justifyContent: 'center',\n            backgroundColor: '#ff6c2f',\n            color: 'white',\n            letterSpacing: '1px',\n          }}\n        >\n          God kisses the finite in his love and man the infinite\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should work correctly when `text-align: right`', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            maxWidth: '190px',\n            textAlign: 'right',\n            alignItems: 'center',\n            justifyContent: 'center',\n            backgroundColor: '#ff6c2f',\n            color: 'white',\n            letterSpacing: '1px',\n          }}\n        >\n          God kisses the finite in his love and man the infinite\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should work correctly when `text-align: end`', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            maxWidth: '190px',\n            textAlign: 'end',\n            alignItems: 'center',\n            justifyContent: 'center',\n            backgroundColor: '#ff6c2f',\n            color: 'white',\n            letterSpacing: '1px',\n          }}\n        >\n          God kisses the finite in his love and man the infinite\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n  it('Should work correctly when `text-align: justify`', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            maxWidth: '190px',\n            textAlign: 'justify',\n            alignItems: 'center',\n            justifyContent: 'center',\n            backgroundColor: '#ff6c2f',\n            color: 'white',\n            letterSpacing: '1px',\n          }}\n        >\n          God kisses the finite in his love and man the infinite\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/text-decoration.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, loadDynamicAsset, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Text Decoration', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('Should work correctly when `text-decoration-line: line-through` and `text-align: right`', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            maxWidth: '190px',\n            backgroundColor: '#91a8d0',\n            textDecorationLine: 'line-through',\n            color: 'white',\n            textAlign: 'center',\n          }}\n        >\n          你好! It doesn’t 안녕! exist, it never has. I’m nostalgic for a place\n          that never existed.\n        </div>\n      </div>,\n      {\n        width: 200,\n        height: 200,\n        fonts,\n        loadAdditionalAsset: (languageCode: string, segment: string) => {\n          return loadDynamicAsset(languageCode, segment) as any\n        },\n      }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should work correctly when `text-decoration-line: underline` and `text-align: right`', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            maxWidth: '190px',\n            backgroundColor: '#91a8d0',\n            textDecorationLine: 'underline',\n            color: 'white',\n            textAlign: 'right',\n          }}\n        >\n          你好! It doesn’t 안녕! exist, it never has. I’m nostalgic for a place\n          that never existed.\n        </div>\n      </div>,\n      {\n        width: 200,\n        height: 200,\n        fonts,\n        loadAdditionalAsset: (languageCode: string, segment: string) => {\n          return loadDynamicAsset(languageCode, segment) as any\n        },\n      }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should work correctly when `text-decoration-style: dotted`', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            maxWidth: '190px',\n            backgroundColor: '#91a8d0',\n            textDecorationLine: 'underline',\n            textDecorationStyle: 'dotted',\n            color: 'white',\n          }}\n        >\n          It doesn’t exist, it never has. I’m nostalgic for a place that never\n          existed.\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should work correctly when `text-decoration-style: dashed`', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            maxWidth: '190px',\n            backgroundColor: '#91a8d0',\n            textDecorationLine: 'underline',\n            textDecorationStyle: 'dashed',\n            color: 'white',\n          }}\n        >\n          It doesn’t exist, it never has. I’m nostalgic for a place that never\n          existed.\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should work correctly with `text-decoration` and `transform`', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          padding: 10,\n          backgroundColor: '#fff',\n          fontSize: 32,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            transform: 'translate(5px, 5px)',\n            padding: 10,\n            textDecoration: 'underline',\n          }}\n        >\n          lynn\n        </div>\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n        loadAdditionalAsset: (languageCode: string, segment: string) => {\n          return loadDynamicAsset(languageCode, segment) as any\n        },\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('Should work correctly when `text-decoration-style: double`', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            backgroundColor: '#91a8d0',\n            textDecoration: 'underline double',\n            color: 'white',\n          }}\n        >\n          It doesn’t exist, it never has. I’m nostalgic for a place that never\n          existed.\n        </div>\n        <div\n          style={{\n            backgroundColor: '#000',\n            textDecoration: 'line-through double',\n            color: 'white',\n          }}\n        >\n          It doesn’t exist, it never has. I’m nostalgic for a place that never\n          existed.\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should skip ink by default when `text-decoration-line: underline`', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 40,\n          color: '#000',\n          textDecorationLine: 'underline',\n        }}\n      >\n        abgpqapa\n      </div>,\n      { width: 260, height: 120, fonts }\n    )\n\n    expect(toImage(svg, 260)).toMatchImageSnapshot()\n  })\n\n  it('Should render continuous line when `text-decoration-skip-ink: none`', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 40,\n          color: '#000',\n          textDecorationLine: 'underline',\n          textDecorationSkipInk: 'none',\n        }}\n      >\n        abgpqapa\n      </div>,\n      { width: 260, height: 120, fonts }\n    )\n\n    expect(toImage(svg, 260)).toMatchImageSnapshot()\n  })\n\n  it('Should skip ink correctly with complex descenders', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 40,\n          color: '#000',\n          textDecorationLine: 'underline',\n        }}\n      >\n        agayaqapajaya;a,a|a\n      </div>,\n      { width: 360, height: 160, fonts }\n    )\n\n    expect(toImage(svg, 360)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/text-indent.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('Text Indent', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('Should work correctly with positive pixel indent', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            maxWidth: '190px',\n            textIndent: '40px',\n            backgroundColor: '#ff6c2f',\n            color: 'white',\n          }}\n        >\n          This is a multiline text that should have the first line indented by\n          40 pixels\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should work correctly with negative indent (hanging indent)', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            maxWidth: '190px',\n            textIndent: '-20px',\n            paddingLeft: '20px',\n            backgroundColor: '#ff6c2f',\n            color: 'white',\n          }}\n        >\n          This is a multiline text with hanging indent for the first line\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should work correctly with percentage value', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            width: '190px',\n            textIndent: '20%',\n            backgroundColor: '#ff6c2f',\n            color: 'white',\n          }}\n        >\n          This text has a first line indented by 20% of container width\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should work correctly with em units', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            maxWidth: '190px',\n            textIndent: '2em',\n            backgroundColor: '#ff6c2f',\n            color: 'white',\n          }}\n        >\n          This text has a first line indented by 2em (relative to font size)\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should work correctly with text-align: center', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            maxWidth: '190px',\n            textIndent: '30px',\n            textAlign: 'center',\n            backgroundColor: '#ff6c2f',\n            color: 'white',\n          }}\n        >\n          This text is centered and has an indent on the first line\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should work correctly with text-align: right', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            maxWidth: '190px',\n            textIndent: '30px',\n            textAlign: 'right',\n            backgroundColor: '#ff6c2f',\n            color: 'white',\n          }}\n        >\n          This text is right-aligned and has an indent on the first line\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should work correctly with text-align: justify', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            maxWidth: '190px',\n            textIndent: '30px',\n            textAlign: 'justify',\n            backgroundColor: '#ff6c2f',\n            color: 'white',\n          }}\n        >\n          This text is justified and has an indent on the first line only\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should work correctly with single line text', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            width: '190px',\n            textIndent: '40px',\n            backgroundColor: '#ff6c2f',\n            color: 'white',\n          }}\n        >\n          Single line\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should inherit from parent', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n          textIndent: '30px',\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            maxWidth: '190px',\n            backgroundColor: '#ff6c2f',\n            color: 'white',\n          }}\n        >\n          This text inherits the text-indent from the parent element\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should override inherited value', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n          textIndent: '30px',\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            maxWidth: '190px',\n            textIndent: '10px',\n            backgroundColor: '#ff6c2f',\n            color: 'white',\n          }}\n        >\n          This text overrides the inherited text-indent with a smaller value\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  it('Should work correctly with zero indent', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          height: '100%',\n          width: '100%',\n          display: 'flex',\n          flexDirection: 'column',\n          alignItems: 'center',\n          justifyContent: 'center',\n          backgroundColor: '#fff',\n          fontSize: 20,\n          fontWeight: 600,\n        }}\n      >\n        <div\n          style={{\n            display: 'flex',\n            maxWidth: '190px',\n            textIndent: '0px',\n            backgroundColor: '#ff6c2f',\n            color: 'white',\n          }}\n        >\n          This text has no indent on the first line baseline test\n        </div>\n      </div>,\n      { width: 200, height: 200, fonts, embedFont: true }\n    )\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/text-wrap.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('text-wrap', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should wrap normally with text-wrap: wrap', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 100,\n          height: 100,\n          fontSize: 18,\n          color: 'red',\n          // @ts-ignore: This isn't a valid CSS property supported by browsers yet.\n          textWrap: 'wrap',\n        }}\n      >\n        {'a a a a a a a a'}\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should wrap balancedly with text-wrap: balance', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 100,\n          height: 100,\n          fontSize: 18,\n          color: 'red',\n          // @ts-ignore: This isn't a valid CSS property supported by browsers yet.\n          textWrap: 'balance',\n        }}\n      >\n        {'a a a a a a a a'}\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/transform.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('transform', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  describe('translate', () => {\n    it('should translate shape', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 10,\n            height: 10,\n            backgroundColor: 'red',\n            transform: 'translate(10px,20px)',\n          }}\n        />,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should translate shape in x-axis', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 10,\n            height: 10,\n            backgroundColor: 'red',\n            transform: 'translateX(10px)',\n          }}\n        />,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should translate shape in y-axis', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 10,\n            height: 10,\n            backgroundColor: 'red',\n            transform: 'translateY(10px)',\n          }}\n        />,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should support %', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            width: 50,\n            height: 10,\n            backgroundColor: 'red',\n            transform: 'translate(100%,100%)',\n          }}\n        >\n          <div\n            style={{\n              width: 50,\n              height: 10,\n              backgroundColor: 'blue',\n              transform: 'translate(-100%,100%) rotate(90deg)',\n            }}\n          />\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('rotate', () => {\n    it('should rotate shape', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 10,\n            height: 10,\n            backgroundColor: 'red',\n            transform: 'rotate(30deg)',\n          }}\n        />,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n    it('should rotate text with overflow', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            transform: 'rotate(40deg)',\n            width: '200px',\n            height: '20px',\n            overflow: 'hidden',\n            backgroundColor: 'red',\n          }}\n        >\n          Hello, World Hello, World\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('scale', () => {\n    it('should scale shape', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 10,\n            height: 10,\n            backgroundColor: 'red',\n            transform: 'scale(1.5)',\n          }}\n        />,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should scale shape in two directions', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 10,\n            height: 10,\n            backgroundColor: 'red',\n            transform: 'scale(2, 3)',\n          }}\n        />,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('multiple transforms', () => {\n    it('should support translate rotate and scale', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 10,\n            height: 10,\n            backgroundColor: 'red',\n            transform: 'rotate(45deg) scale(2, 0.2) translate(50px, 50px)',\n          }}\n        />,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('behavior with parent overflow', () => {\n    it('should not inherit parent clip-path', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            display: 'flex',\n            width: 20,\n            height: 20,\n            overflow: 'hidden',\n          }}\n        >\n          <div\n            style={{\n              width: 15,\n              height: 15,\n              backgroundColor: 'red',\n              transform: 'rotate(45deg) translate(15px, 5px)',\n            }}\n          />\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n})\n"
  },
  {
    "path": "test/typesetting.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('typesetting', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should wrap normally', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          fontSize: 18,\n          color: 'black',\n          wordBreak: 'break-word',\n        }}\n      >\n        A quick brown fox jumps over the lazy dog.\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should wrap normally for special characters', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '100%',\n          height: '100%',\n          fontSize: 18,\n          color: 'black',\n          wordBreak: 'break-word',\n        }}\n      >\n        {`@A #quick ?brown :fox !jumps -over ~the %lazy ^dog.`}\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/units.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\nimport { splitEffects } from '../src/utils.js'\n\ndescribe('Units', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should support %', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '30%',\n          height: '10%',\n          background: 'red',\n        }}\n      ></div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support em', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '2em',\n          height: '3em',\n          background: 'red',\n          fontSize: 12,\n        }}\n      ></div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support vh and vw', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '10vw',\n          height: '80vh',\n          background: 'red',\n        }}\n      ></div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support rem', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '2rem',\n          height: '3rem',\n          background: 'red',\n          fontSize: 12,\n        }}\n      ></div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support px and numbers', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: '20px',\n          height: 30,\n          background: 'red',\n          fontSize: 12,\n        }}\n      ></div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support rgb syntaxs', async () => {\n    const svg = await satori(\n      <div style={{ display: 'flex' }}>\n        <div\n          style={{\n            width: 10,\n            height: 10,\n            background: 'rgb(100%,0%,0%)',\n          }}\n        ></div>\n        <div\n          style={{\n            width: 10,\n            height: 10,\n            background: 'rgb(255 0 0 / 50%)',\n          }}\n        ></div>\n        <div\n          style={{\n            width: 10,\n            height: 10,\n            background: 'rgb(255, 0, 0, 0.5)',\n          }}\n        ></div>\n      </div>,\n      {\n        width: 100,\n        height: 100,\n        fonts,\n      }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should support split multiple effect', () => {\n    const tests = {\n      'url(https:a.png), linear-gradient(blue, red)': [\n        'url(https:a.png)',\n        'linear-gradient(blue, red)',\n      ],\n      'rgba(0,0,0,.7)': ['rgba(0,0,0,.7)'],\n      '1px 1px 2px black, 0 0 1em blue': ['1px 1px 2px black', '0 0 1em blue'],\n      '2px 2px red, 4px 4px #4bf542, 6px 6px rgba(186, 147, 17, 30%)': [\n        '2px 2px red',\n        '4px 4px #4bf542',\n        '6px 6px rgba(186, 147, 17, 30%)',\n      ],\n    }\n\n    for (const [k, v] of Object.entries(tests)) {\n      expect(splitEffects(k, ',')).toEqual(v)\n    }\n\n    ;[' ', /\\s{1}/].forEach((v) => {\n      expect(\n        splitEffects(\n          'drop-shadow(4px 4px 10px blue) blur(4px) saturate(150%)',\n          v\n        )\n      ).toEqual([\n        'drop-shadow(4px 4px 10px blue)',\n        'blur(4px)',\n        'saturate(150%)',\n      ])\n    })\n  })\n})\n"
  },
  {
    "path": "test/utils.tsx",
    "content": "import { beforeAll, expect } from 'vitest'\nimport { join } from 'path'\nimport { Resvg } from '@resvg/resvg-js'\nimport { toMatchImageSnapshot } from 'jest-image-snapshot'\nimport { readFile } from 'node:fs/promises'\n\nimport { type SatoriOptions } from '../src/index.js'\n\nexport async function getDynamicAsset(text: string): Promise<Buffer> {\n  const fontPath = join(process.cwd(), 'test', 'assets', text)\n  return await readFile(fontPath)\n}\n\nexport async function loadDynamicAsset(code: string, text: string) {\n  return [\n    {\n      name: `satori_${code}_fallback_${text}`,\n      data: await getDynamicAsset(text),\n      weight: 400,\n      style: 'normal',\n      lang: code === 'unknown' ? undefined : code.split('|')[0],\n    },\n  ]\n}\n\nexport function initFonts(callback: (fonts: SatoriOptions['fonts']) => void) {\n  beforeAll(async () => {\n    const fontPath = join(process.cwd(), 'test', 'assets', 'Roboto-Regular.ttf')\n    const fontData = await readFile(fontPath)\n    callback([\n      {\n        name: 'Roboto',\n        data: fontData,\n        weight: 400,\n        style: 'normal',\n      },\n    ])\n  })\n}\n\nexport function toImage(svg: string, width = 100) {\n  const resvg = new Resvg(svg, {\n    fitTo: {\n      mode: 'width',\n      value: width,\n    },\n    font: {\n      // As system fallback font\n      fontFiles: [\n        join(process.cwd(), 'test', 'assets', 'playfair-display.ttf'),\n      ],\n      loadSystemFonts: false,\n      defaultFontFamily: 'Playfair Display',\n    },\n  })\n  const pngData = resvg.render()\n  return pngData.asPng()\n}\n\ndeclare global {\n  namespace jest {\n    interface Matchers<R> {\n      toMatchImageSnapshot(): R\n    }\n  }\n}\n\nexpect.extend({ toMatchImageSnapshot })\n"
  },
  {
    "path": "test/webkit-text-stroke.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('webkit-text-stroke', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  it('should work basic text stroke', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          width: 100,\n          height: 100,\n          fontSize: 30,\n          background: '#ebebeb',\n          color: '#ffffff',\n          WebkitTextStroke: '4px #000000',\n        }}\n      >\n        Hello, world\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should work nested text stroke', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          flexWrap: 'wrap',\n          width: 100,\n          height: 100,\n          fontSize: 30,\n          background: '#ebebeb',\n          color: '#ffffff',\n          WebkitTextStroke: '4px #000000',\n        }}\n      >\n        Hello, <span style={{ WebkitTextStrokeColor: '#ff0000' }}>world</span>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n\n  it('should work nested and complex text stroke', async () => {\n    const svg = await satori(\n      <div\n        style={{\n          display: 'flex',\n          flexWrap: 'wrap',\n          width: 100,\n          height: 100,\n          fontSize: 30,\n          background: '#ebebeb',\n          color: '#ffffff',\n          WebkitTextStroke: '4px #000000',\n        }}\n      >\n        Hello,\n        <span style={{ WebkitTextStrokeColor: '#f00' }}>w</span>\n        <span style={{ WebkitTextStrokeColor: '#ff0' }}>o</span>\n        <span style={{ WebkitTextStrokeColor: '#0f0' }}>r</span>\n        <span style={{ WebkitTextStrokeColor: '#0ff' }}>l</span>\n        <span style={{ WebkitTextStrokeColor: '#00f' }}>d</span>\n        <span\n          style={{\n            WebkitTextStrokeColor: '#f0f',\n            WebkitTextStrokeWidth: '6px',\n          }}\n        >\n          !\n        </span>\n      </div>,\n      { width: 100, height: 100, fonts }\n    )\n    expect(toImage(svg, 100)).toMatchImageSnapshot()\n  })\n})\n"
  },
  {
    "path": "test/white-space.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('white-space', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  describe('normal', () => {\n    it('should not render extra spaces with `white-space: normal`', async () => {\n      const EnSpace = String.fromCodePoint(Number('0x2002'))\n\n      const svg = await satori(\n        <div\n          style={{\n            whiteSpace: 'normal',\n            display: 'flex',\n            flexDirection: 'column',\n          }}\n        >\n          <div>{'hello'}</div>\n          <div>{' hello '}</div>\n          <div>{EnSpace + 'hello'}</div>\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should not render extra line breaks with `white-space: normal`', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            whiteSpace: 'normal',\n          }}\n        >\n          {' hello \\n world'}\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should wrap automatically with `white-space: normal`', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            whiteSpace: 'normal',\n          }}\n        >\n          hello, world\n        </div>,\n        {\n          width: 20,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 20)).toMatchImageSnapshot()\n    })\n\n    it('Should have line break before fast.!!!!!!!!!!!!!!!!!', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            backgroundColor: '#fff',\n          }}\n        >\n          Taking a look at Vercels new library to generate dynamic OpenGraph\n          images on the fly it is fast.!!!!!!!!!!!!!!!!!\n        </div>,\n        {\n          width: 340,\n          height: 60,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 400)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('pre', () => {\n    it('should always preserve extra spaces with `white-space: pre`', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            whiteSpace: 'pre',\n          }}\n        >\n          {'     hello '}\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should always preserve extra line breaks with `white-space: pre`', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            whiteSpace: 'pre',\n          }}\n        >\n          {' hello \\n world '}\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n          embedFont: false,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render line breaks correctly without separators', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            whiteSpace: 'pre',\n            color: 'red',\n          }}\n        >\n          {'hello\\nworld'}\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n          embedFont: false,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should not wrap with `white-space: pre`', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            whiteSpace: 'pre',\n          }}\n        >\n          hello, world\n        </div>,\n        {\n          width: 20,\n          height: 100,\n          fonts,\n          embedFont: false,\n        }\n      )\n      expect(toImage(svg, 20)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('with `white-space: pre-wrap`', () => {\n    it('should always preserve extra spaces with `white-space: pre-wrap`', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            whiteSpace: 'pre-wrap',\n          }}\n        >\n          {' hello '}\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should always preserve extra line breaks with `white-space: pre-wrap`', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            whiteSpace: 'pre-wrap',\n          }}\n        >\n          {' hello \\n world'}\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n          embedFont: false,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should automatically wrap with `white-space: pre-wrap`', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            whiteSpace: 'pre-wrap',\n          }}\n        >\n          hello, world\n        </div>,\n        {\n          width: 20,\n          height: 100,\n          fonts,\n          embedFont: false,\n        }\n      )\n      expect(toImage(svg, 20)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('with `white-space: pre-line`', () => {\n    it('should always collapse spaces and preserve line breaks with `white-space: pre-line`', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            whiteSpace: 'pre-line',\n          }}\n        >\n          {'  hello \\n world'}\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n          embedFont: false,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('with `white-space: nowrap`', () => {\n    it('should not wrap with `white-space: nowrap` and swallow extra spaces', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            whiteSpace: 'nowrap',\n          }}\n        >\n          {` hello, world `}\n        </div>,\n        {\n          width: 20,\n          height: 100,\n          fonts,\n          embedFont: false,\n        }\n      )\n      expect(toImage(svg, 20)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('with `\\\\n` in content', () => {\n    it('should render `\\\\n` as a whitespace', async () => {\n      const svg = await satori(<div style={{}}>{`hello\\nworld`}</div>, {\n        width: 100,\n        height: 100,\n        fonts,\n      })\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render `\\\\n` as a line break with `pre`', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            whiteSpace: 'pre',\n          }}\n        >\n          {`hello\\nworld`}\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should render consecutive line breaks with `pre`', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            whiteSpace: 'pre',\n          }}\n        >\n          {`hello\\n\\nworld`}\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n})\n"
  },
  {
    "path": "test/word-break.test.tsx",
    "content": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, loadDynamicAsset, toImage } from './utils.js'\nimport satori from '../src/index.js'\n\ndescribe('word-break', () => {\n  let fonts\n  initFonts((f) => (fonts = f))\n\n  describe('normal', () => {\n    it('should not break word if possible to wrap', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 100,\n            height: 100,\n            fontSize: 24,\n            color: 'red',\n            wordBreak: 'normal',\n          }}\n        >\n          {'aaaaaa hello'}\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should not break long word', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 100,\n            height: 100,\n            fontSize: 24,\n            color: 'red',\n            wordBreak: 'normal',\n          }}\n        >\n          {'aaaaaaaaaaa hello'}\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  it('should support non-breaking space', async () => {\n    const text = `She weighs around blah 50\\u00a0kg`\n    const svg = await satori(\n      <div\n        style={{\n          color: 'red',\n          display: 'flex',\n          flexDirection: 'column',\n          width: '100%',\n          wordBreak: 'normal',\n        }}\n      >\n        <span>{text}</span>\n        <span>{text.replaceAll('\\u00A0', ' ')}</span>\n      </div>,\n      {\n        width: 200,\n        height: 100,\n        fonts,\n      }\n    )\n\n    expect(toImage(svg, 200)).toMatchImageSnapshot()\n  })\n\n  describe('break-all', () => {\n    it('should always break words eagerly', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 100,\n            height: 100,\n            fontSize: 24,\n            color: 'red',\n            wordBreak: 'break-all',\n          }}\n        >\n          {'a fascinating world'}\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n  })\n\n  describe('break-word', () => {\n    it('should try to wrap words if possible', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 100,\n            height: 100,\n            fontSize: 24,\n            color: 'red',\n            wordBreak: 'break-word',\n          }}\n        >\n          {'aaaaaa world'}\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should break words if cannot fit into one line', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 100,\n            height: 100,\n            fontSize: 24,\n            color: 'red',\n            wordBreak: 'break-word',\n          }}\n        >\n          {'fascinating world'}\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should wrap first and then break long words', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            width: 100,\n            height: 100,\n            fontSize: 24,\n            color: 'red',\n            wordBreak: 'break-word',\n          }}\n        >\n          {'a fascinating world'}\n        </div>,\n        {\n          width: 100,\n          height: 100,\n          fonts,\n        }\n      )\n\n      expect(toImage(svg, 100)).toMatchImageSnapshot()\n    })\n\n    it('should not break CJK with word-break: keep-all', async () => {\n      const svg = await satori(\n        <div\n          style={{\n            height: '100%',\n            width: '100%',\n            backgroundColor: '#fff',\n            display: 'flex',\n          }}\n        >\n          <div\n            style={{\n              width: '100%',\n              wordBreak: 'keep-all',\n            }}\n          >\n            Hello! 你好! 안녕! こんにちは! Χαίρετε! Hallå!\n          </div>\n        </div>,\n        {\n          width: 200,\n          height: 200,\n          fonts,\n          loadAdditionalAsset: (languageCode: string, segment: string) => {\n            return loadDynamicAsset(languageCode, segment) as any\n          },\n        }\n      )\n\n      expect(toImage(svg, 200)).toMatchImageSnapshot()\n    })\n  })\n})\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"module\": \"node16\",\n    \"moduleResolution\": \"node16\",\n    \"esModuleInterop\": true,\n    \"target\": \"ES2021\",\n    \"lib\": [\"esnext\", \"dom\"],\n    \"baseUrl\": \".\",\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src/*\", \"test/*\"]\n}\n"
  },
  {
    "path": "tsup.config.ts",
    "content": "import { defineConfig } from 'tsup'\nimport { join } from 'path'\nimport { replace } from 'esbuild-plugin-replace'\n\nconst isStandaloneBuild = !!process.env.SATORI_STANDALONE\n\nexport default defineConfig({\n  entry: {\n    [isStandaloneBuild ? 'standalone' : 'index']: 'src/index.ts',\n    'jsx/index': 'src/jsx/index.ts',\n    'jsx/jsx-runtime': 'src/jsx/jsx-runtime.ts',\n  },\n  splitting: false,\n  sourcemap: true,\n  target: 'node16',\n  dts: process.env.NODE_ENV !== 'development' && {\n    resolve: ['twrnc', './tw-config', './types'],\n  },\n  minify: process.env.NODE_ENV !== 'development',\n  format: ['esm', 'cjs'],\n  noExternal: ['twrnc', 'emoji-regex-xs', 'yoga-layout'],\n  esbuildOptions(options) {\n    options.tsconfig = 'tsconfig.json'\n    options.legalComments = 'external'\n  },\n  env: isStandaloneBuild\n    ? {\n        SATORI_STANDALONE: '1',\n      }\n    : {},\n  esbuildPlugins: [\n    {\n      name: 'optimize tailwind',\n      setup(build) {\n        // Get rid of chalk\n        // https://github.com/tailwindlabs/tailwindcss/blob/b8cda161dd0993083dcef1e2a03988c70be0ce93/src/util/log.js\n        build.onResolve({ filter: /\\/log$/ }, (args) => {\n          if (args.importer.includes('/tailwindcss/')) {\n            return {\n              path: join(__dirname, 'src', 'vendor', 'twrnc', 'log.js'),\n            }\n          }\n        })\n\n        // Get rid of picocolors\n        // https://github.com/tailwindlabs/tailwindcss/blob/bf4494104953b13a5f326b250d7028074815e77e/src/featureFlags.js\n        build.onResolve({ filter: /^picocolors$/ }, () => {\n          return {\n            path: join(__dirname, 'src', 'vendor', 'twrnc', 'picocolors.js'),\n          }\n        })\n\n        // Get rid of util-deprecate/node.js\n        build.onResolve({ filter: /util-deprecate/ }, () => {\n          return {\n            path: join(__dirname, 'src', 'vendor', 'twrnc', 'deprecate.js'),\n          }\n        })\n      },\n    },\n    // We don't like `Function`.\n    // https://github.com/tailwindlabs/tailwindcss/blob/bf4494104953b13a5f326b250d7028074815e77e/src/util/getAllConfigs.js#L8\n    replace({\n      'preset instanceof Function': 'typeof preset === \"function\"',\n    }),\n  ],\n})\n"
  },
  {
    "path": "turbo.json",
    "content": "{\n  \"$schema\": \"https://turbo.build/schema.json\",\n  \"pipeline\": {\n    \"dev\": {\n      \"cache\": false\n    }\n  }\n}\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import path from 'path'\nimport { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n  test: {\n    coverage: {\n      reporter: ['text', 'json', 'html'],\n    },\n  },\n})\n"
  },
  {
    "path": "vitest.jsx-runtime.config.ts",
    "content": "import { defineConfig, mergeConfig } from 'vitest/config'\nimport vitestConfig from './vitest.config'\n\nexport default mergeConfig(\n  vitestConfig,\n  defineConfig({\n    esbuild: {\n      jsx: 'automatic',\n      jsxImportSource: new URL('./src/jsx', import.meta.url).href,\n    },\n  })\n)\n"
  }
]