Full Code of vercel/satori for AI

main eab60eeecca1 cached
154 files
778.7 KB
229.5k tokens
367 symbols
1 requests
Download .txt
Showing preview only (820K chars total). Download the full file or copy to clipboard to get everything.
Repository: vercel/satori
Branch: main
Commit: eab60eeecca1
Files: 154
Total size: 778.7 KB

Directory structure:
gitextract_raqdfr0j/

├── .eslintrc.json
├── .github/
│   ├── CODEOWNERS
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── config.yml
│   │   └── feature_request.md
│   └── workflows/
│       ├── ci.yml
│       └── pr.yml
├── .gitignore
├── .husky/
│   └── pre-commit
├── .npmrc
├── .prettierignore
├── .prettierrc
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── package.json
├── patches/
│   └── yoga-layout@3.2.1.patch
├── playground/
│   ├── LICENSE
│   ├── cards/
│   │   ├── playground-data.ts
│   │   └── preview-tabs.ts
│   ├── components/
│   │   ├── introduction.module.css
│   │   ├── introduction.tsx
│   │   ├── panel-resize-handle.module.css
│   │   ├── panel-resize-handle.tsx
│   │   └── resvg_worker.ts
│   ├── decs.d.ts
│   ├── index.d.ts
│   ├── next-env.d.ts
│   ├── package.json
│   ├── pages/
│   │   ├── _app.tsx
│   │   ├── _document.tsx
│   │   ├── api/
│   │   │   └── font.ts
│   │   └── index.tsx
│   ├── styles.css
│   ├── tsconfig.json
│   └── utils/
│       ├── font.ts
│       └── twemoji.ts
├── pnpm-workspace.yaml
├── release.config.cjs
├── src/
│   ├── builder/
│   │   ├── background-image.ts
│   │   ├── border-radius.ts
│   │   ├── border.ts
│   │   ├── clip-path.ts
│   │   ├── content-mask.ts
│   │   ├── gradient/
│   │   │   ├── linear.ts
│   │   │   ├── radial.ts
│   │   │   └── utils.ts
│   │   ├── mask-image.ts
│   │   ├── overflow.ts
│   │   ├── rect.ts
│   │   ├── shadow.ts
│   │   ├── svg.ts
│   │   ├── text-decoration.ts
│   │   ├── text.ts
│   │   └── transform.ts
│   ├── font.ts
│   ├── handler/
│   │   ├── compute.ts
│   │   ├── expand.ts
│   │   ├── image.ts
│   │   ├── inheritable.ts
│   │   ├── preprocess.ts
│   │   ├── presets.ts
│   │   ├── tailwind.ts
│   │   └── variables.ts
│   ├── index.ts
│   ├── jsx/
│   │   ├── index.ts
│   │   ├── intrinsic-elements.ts
│   │   ├── jsx-runtime.ts
│   │   └── types.ts
│   ├── language.ts
│   ├── layout.ts
│   ├── parser/
│   │   ├── mask.ts
│   │   └── shape.ts
│   ├── satori.ts
│   ├── text/
│   │   ├── characters.ts
│   │   ├── index.ts
│   │   ├── measurer.ts
│   │   └── processor.ts
│   ├── transform-origin.ts
│   ├── types.d.ts
│   ├── utils.ts
│   ├── vendor/
│   │   ├── parse-css-dimension/
│   │   │   ├── LICENSE
│   │   │   ├── index.js
│   │   │   ├── package.json
│   │   │   └── src.js
│   │   └── twrnc/
│   │       ├── deprecate.js
│   │       ├── log.js
│   │       └── picocolors.js
│   ├── yoga.bundled.ts
│   ├── yoga.external.ts
│   └── yoga.ts
├── test/
│   ├── assets/
│   │   ├── Χαίρετ
│   │   ├── こんにちは
│   │   ├── 你好
│   │   └── 안녕
│   ├── background-clip.test.tsx
│   ├── basic.test.tsx
│   ├── benchmark/
│   │   ├── Geist-Black.otf
│   │   ├── Geist-Bold.otf
│   │   ├── Geist-Medium.otf
│   │   ├── Geist-Regular.otf
│   │   ├── Geist-SemiBold.otf
│   │   └── index.ts
│   ├── border.test.tsx
│   ├── box-sizing.test.tsx
│   ├── clip-path.test.tsx
│   ├── color-models.test.tsx
│   ├── css-variables.test.tsx
│   ├── display-contents.test.tsx
│   ├── display.test.tsx
│   ├── dynamic-size.test.tsx
│   ├── embed-font.test.tsx
│   ├── emoji.test.tsx
│   ├── error.test.tsx
│   ├── event.test.tsx
│   ├── flexbox-advanced.test.tsx
│   ├── font.test.tsx
│   ├── gap.test.tsx
│   ├── gradient.test.tsx
│   ├── image.test.tsx
│   ├── jsx-runtime.test.tsx
│   ├── language.test.tsx
│   ├── layout.test.tsx
│   ├── letter-spacing.test.tsx
│   ├── line-clamp.test.tsx
│   ├── line-height.test.tsx
│   ├── margin.test.tsx
│   ├── mask-image.test.tsx
│   ├── opacity.test.tsx
│   ├── overflow.test.tsx
│   ├── padding.test.tsx
│   ├── pixel-font.test.tsx
│   ├── position.test.tsx
│   ├── react.test.tsx
│   ├── shadow.test.tsx
│   ├── svg.test.tsx
│   ├── tab-size.test.tsx
│   ├── text-align.test.tsx
│   ├── text-decoration.test.tsx
│   ├── text-indent.test.tsx
│   ├── text-wrap.test.tsx
│   ├── transform.test.tsx
│   ├── typesetting.test.tsx
│   ├── units.test.tsx
│   ├── utils.tsx
│   ├── webkit-text-stroke.test.tsx
│   ├── white-space.test.tsx
│   └── word-break.test.tsx
├── tsconfig.json
├── tsup.config.ts
├── turbo.json
├── vitest.config.ts
├── vitest.jsx-runtime.config.ts
└── yoga.wasm

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintrc.json
================================================
{
  "env": {
    "browser": true,
    "es2021": true,
    "node": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:react/jsx-runtime",
    "plugin:@typescript-eslint/recommended"
  ],
  "overrides": [
    {
      "files": ["src/**/*.js", "src/**/*.ts"],
      "rules": {
        "prefer-const": "off"
      }
    },
    {
      "files": ["test/**/*.ts`", "test/**/*.tsx"],
      "rules": {
        "react/jsx-key": "off"
      }
    }
  ],
  "parser": "@typescript-eslint/parser",
  "plugins": ["react", "react-hooks", "@typescript-eslint"],
  "rules": {
    "no-inner-declarations": 0,
    "no-useless-escape": 1,
    "@typescript-eslint/ban-ts-comment": 1,
    "@typescript-eslint/no-extra-semi": 0,
    "@typescript-eslint/no-shadow": 2,
    "@typescript-eslint/ban-types": 0,
    "@typescript-eslint/no-namespace": 0,
    "react-hooks/rules-of-hooks": 2,
    "react-hooks/exhaustive-deps": 1,
    "react/prop-types": 0
  },
  "ignorePatterns": ["dist/", "node_modules", "vendor"]
}


================================================
FILE: .github/CODEOWNERS
================================================
* @shuding


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a bug report for Satori
---

# Bug report

## Description / Observed Behavior

What kind of issues did you encounter with Satori?

## Expected Behavior

How did you expect Satori to behave here?

## Reproduction

Create a shareable reproduction link for the issue using https://og-playground.vercel.app.

## Additional Context

Satori version, and any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
  - name: Question & Ideas
    url: https://github.com/vercel/satori/discussions
    about: Ask questions and share your thoughts with other community members


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Request a new feature for Satori
---

# Feature Request

## Description

What do you want to add to Satori, and why?

## Additional Context

You can add a shareable link using https://og-playground.vercel.app, or any other context that helps explaining this feature request here.


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  push:
    branches: ['main']
  pull_request:
    branches: ['main']

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
  integrity:
    # prevents this action from running on forks
    if: github.repository_owner == 'vercel'
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: [ 20 ]
    steps:
    - name: Checkout
      uses: actions/checkout@v3
    - name: Use pnpm
      run: corepack enable pnpm && pnpm --version
    - name: Use Node.js ${{ matrix.node }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node }}
        cache: 'pnpm'
    - run: pnpm install
    - run: pnpm ci-check

  test:
    # prevents this action from running on forks
    if: github.repository_owner == 'vercel'
    name: Node.js ${{ matrix.node }} on ${{ matrix.os }}
    timeout-minutes: 5
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest]
        node: [18, 20]
    runs-on: ${{ matrix.os }}
    permissions:
      contents: write # to be able to publish a GitHub release
      issues: write # to be able to comment on released issues
      pull-requests: write # to be able to comment on released pull requests
      id-token: write # to enable use of OIDC for npm provenance
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Use pnpm
        run: corepack enable pnpm && pnpm --version
      - name: Use Node.js ${{ matrix.node }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node }}
          cache: 'pnpm'
      - run: pnpm install
      - run: pnpm build
      - run: pnpm test
      - name: Maybe Release
        if: matrix.os == 'ubuntu-latest' && matrix.node == 20 && github.event_name == 'push' && github.ref == 'refs/heads/main'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN_ELEVATED }}
          NPM_CONFIG_PROVENANCE: 'true'
        run: pnpm dlx semantic-release@24.2.3


================================================
FILE: .github/workflows/pr.yml
================================================
name: PR
on:
  pull_request:
    types: [opened, edited, synchronize]
  pull_request_target:
    types: [opened, edited, synchronize]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: amannn/action-semantic-pull-request@0b14f54ac155d88e12522156e52cb6e397745cfd
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
node_modules
.DS_Store
.vercel
.vscode
.next
.idea
.turbo
dist
.pnpm-debug.log
__diff_output__
.eslintcache
coverage

playground/public/yoga.wasm
playground/tsconfig.tsbuildinfo

# Vendor files
# yoga.wasm


================================================
FILE: .husky/pre-commit
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm lint-staged


================================================
FILE: .npmrc
================================================
shell-emulator=true
provenance=true


================================================
FILE: .prettierignore
================================================
.github/
node_modules
**/.next/**
**/_next/**
**/dist/**
src/vendor/
pnpm-lock.yaml
*.md
coverage/

================================================
FILE: .prettierrc
================================================
{
  "tabWidth": 2,
  "useTabs": false,
  "singleQuote": true,
  "jsxSingleQuote": true,
  "semi": false
}


================================================
FILE: CONTRIBUTING.md
================================================
# Satori Contribution Guidelines

Thank you for reading this guide and we appreciate any contribution.

## Ask a Question

You 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.

## Report a Bug

Whenever 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.

If 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.

## Request for a New Feature

For 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.

If possible, you can add another additional context like how this feature can be implemented technically, what other alternative solutions we can have, etc.

## Local Development

This project uses [pnpm](https://pnpm.io). To install dependencies, run:

```bash
pnpm install
```

To start the playground together with Satori locally, run:

```bash
pnpm dev:playground
```

And visit localhost:3000.

To 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).

## Adding Tests

Satori uses [Vitest](https://vitest.dev) to test and generate snapshots. To start and live-watch the tests, run:

```bash
pnpm dev:test
```

It will update snapshot images as well.

You can also use `pnpm test` to only run the test.


================================================
FILE: LICENSE
================================================
Mozilla Public License Version 2.0
==================================

1. Definitions
--------------

1.1. "Contributor"
    means each individual or legal entity that creates, contributes to
    the creation of, or owns Covered Software.

1.2. "Contributor Version"
    means the combination of the Contributions of others (if any) used
    by a Contributor and that particular Contributor's Contribution.

1.3. "Contribution"
    means Covered Software of a particular Contributor.

1.4. "Covered Software"
    means Source Code Form to which the initial Contributor has attached
    the notice in Exhibit A, the Executable Form of such Source Code
    Form, and Modifications of such Source Code Form, in each case
    including portions thereof.

1.5. "Incompatible With Secondary Licenses"
    means

    (a) that the initial Contributor has attached the notice described
        in Exhibit B to the Covered Software; or

    (b) that the Covered Software was made available under the terms of
        version 1.1 or earlier of the License, but not also under the
        terms of a Secondary License.

1.6. "Executable Form"
    means any form of the work other than Source Code Form.

1.7. "Larger Work"
    means a work that combines Covered Software with other material, in
    a separate file or files, that is not Covered Software.

1.8. "License"
    means this document.

1.9. "Licensable"
    means having the right to grant, to the maximum extent possible,
    whether at the time of the initial grant or subsequently, any and
    all of the rights conveyed by this License.

1.10. "Modifications"
    means any of the following:

    (a) any file in Source Code Form that results from an addition to,
        deletion from, or modification of the contents of Covered
        Software; or

    (b) any new file in Source Code Form that contains any Covered
        Software.

1.11. "Patent Claims" of a Contributor
    means any patent claim(s), including without limitation, method,
    process, and apparatus claims, in any patent Licensable by such
    Contributor that would be infringed, but for the grant of the
    License, by the making, using, selling, offering for sale, having
    made, import, or transfer of either its Contributions or its
    Contributor Version.

1.12. "Secondary License"
    means either the GNU General Public License, Version 2.0, the GNU
    Lesser General Public License, Version 2.1, the GNU Affero General
    Public License, Version 3.0, or any later versions of those
    licenses.

1.13. "Source Code Form"
    means the form of the work preferred for making modifications.

1.14. "You" (or "Your")
    means an individual or a legal entity exercising rights under this
    License. For legal entities, "You" includes any entity that
    controls, is controlled by, or is under common control with You. For
    purposes of this definition, "control" means (a) the power, direct
    or indirect, to cause the direction or management of such entity,
    whether by contract or otherwise, or (b) ownership of more than
    fifty percent (50%) of the outstanding shares or beneficial
    ownership of such entity.

2. License Grants and Conditions
--------------------------------

2.1. Grants

Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:

(a) under intellectual property rights (other than patent or trademark)
    Licensable by such Contributor to use, reproduce, make available,
    modify, display, perform, distribute, and otherwise exploit its
    Contributions, either on an unmodified basis, with Modifications, or
    as part of a Larger Work; and

(b) under Patent Claims of such Contributor to make, use, sell, offer
    for sale, have made, import, and otherwise transfer either its
    Contributions or its Contributor Version.

2.2. Effective Date

The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.

2.3. Limitations on Grant Scope

The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:

(a) for any code that a Contributor has removed from Covered Software;
    or

(b) for infringements caused by: (i) Your and any other third party's
    modifications of Covered Software, or (ii) the combination of its
    Contributions with other software (except as part of its Contributor
    Version); or

(c) under Patent Claims infringed by Covered Software in the absence of
    its Contributions.

This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).

2.4. Subsequent Licenses

No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).

2.5. Representation

Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.

2.6. Fair Use

This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.

2.7. Conditions

Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.

3. Responsibilities
-------------------

3.1. Distribution of Source Form

All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.

3.2. Distribution of Executable Form

If You distribute Covered Software in Executable Form then:

(a) such Covered Software must also be made available in Source Code
    Form, as described in Section 3.1, and You must inform recipients of
    the Executable Form how they can obtain a copy of such Source Code
    Form by reasonable means in a timely manner, at a charge no more
    than the cost of distribution to the recipient; and

(b) You may distribute such Executable Form under the terms of this
    License, or sublicense it under different terms, provided that the
    license for the Executable Form does not attempt to limit or alter
    the recipients' rights in the Source Code Form under this License.

3.3. Distribution of a Larger Work

You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).

3.4. Notices

You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.

3.5. Application of Additional Terms

You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.

4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------

If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.

5. Termination
--------------

5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.

5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.

5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.

************************************************************************
*                                                                      *
*  6. Disclaimer of Warranty                                           *
*  -------------------------                                           *
*                                                                      *
*  Covered Software is provided under this License on an "as is"       *
*  basis, without warranty of any kind, either expressed, implied, or  *
*  statutory, including, without limitation, warranties that the       *
*  Covered Software is free of defects, merchantable, fit for a        *
*  particular purpose or non-infringing. The entire risk as to the     *
*  quality and performance of the Covered Software is with You.        *
*  Should any Covered Software prove defective in any respect, You     *
*  (not any Contributor) assume the cost of any necessary servicing,   *
*  repair, or correction. This disclaimer of warranty constitutes an   *
*  essential part of this License. No use of any Covered Software is   *
*  authorized under this License except under this disclaimer.         *
*                                                                      *
************************************************************************

************************************************************************
*                                                                      *
*  7. Limitation of Liability                                          *
*  --------------------------                                          *
*                                                                      *
*  Under no circumstances and under no legal theory, whether tort      *
*  (including negligence), contract, or otherwise, shall any           *
*  Contributor, or anyone who distributes Covered Software as          *
*  permitted above, be liable to You for any direct, indirect,         *
*  special, incidental, or consequential damages of any character      *
*  including, without limitation, damages for lost profits, loss of    *
*  goodwill, work stoppage, computer failure or malfunction, or any    *
*  and all other commercial damages or losses, even if such party      *
*  shall have been informed of the possibility of such damages. This   *
*  limitation of liability shall not apply to liability for death or   *
*  personal injury resulting from such party's negligence to the       *
*  extent applicable law prohibits such limitation. Some               *
*  jurisdictions do not allow the exclusion or limitation of           *
*  incidental or consequential damages, so this exclusion and          *
*  limitation may not apply to You.                                    *
*                                                                      *
************************************************************************

8. Litigation
-------------

Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.

9. Miscellaneous
----------------

This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.

10. Versions of the License
---------------------------

10.1. New Versions

Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.

10.2. Effect of New Versions

You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.

10.3. Modified Versions

If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).

10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses

If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.

Exhibit A - Source Code Form License Notice
-------------------------------------------

  This Source Code Form is subject to the terms of the Mozilla Public
  License, v. 2.0. If a copy of the MPL was not distributed with this
  file, You can obtain one at http://mozilla.org/MPL/2.0/.

If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.

You may add additional accurate notices of copyright ownership.

Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------

  This Source Code Form is "Incompatible With Secondary Licenses", as
  defined by the Mozilla Public License, v. 2.0.


================================================
FILE: README.md
================================================
![Satori](.github/card.png)

**Satori**: Enlightened library to convert HTML and CSS to SVG.

> **Note**
>
> 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)
>
> 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)

## Overview

Satori supports the JSX syntax, which makes it very straightforward to use. Here’s an overview of the basic usage:

```jsx
// api.jsx
import satori from 'satori'

const svg = await satori(
  <div style={{ color: 'black' }}>hello, world</div>,
  {
    width: 600,
    height: 400,
    fonts: [
      {
        name: 'Roboto',
        // Use `fs` (Node.js only) or `fetch` to read the font as Buffer/ArrayBuffer and provide `data` here.
        data: robotoArrayBuffer,
        weight: 400,
        style: 'normal',
      },
    ],
  },
)
```

Satori will render the element into a 600×400 SVG, and return the SVG string:

```js
'<svg ...><path d="..." fill="black"></path></svg>'
```

Under 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.

<br/>

## Documentation

### JSX

Satori only accepts JSX elements that are pure and stateless. You can use a subset of HTML
elements (see section below), or custom React components, but React APIs such as `useState`, `useEffect`, `dangerouslySetInnerHTML` are not supported.

#### Experimental: builtin JSX support

Satori 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.

```tsx
/** @jsxRuntime automatic */
/** @jsxImportSource satori/jsx */

import satori from 'satori';
import { FC, JSXNode } from 'satori/jsx';

const MyComponent: FC<{ children: JSXNode }> = ({ children }) => (
  <div style={{ color: 'black' }}>{children}</div>
)

const svg = await satori(
  <MyComponent>hello, world</MyComponent>,
  options,
)
```

#### Use without JSX

If 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:

```js
await satori(
  {
    type: 'div',
    props: {
      children: 'hello, world',
      style: { color: 'black' },
    },
  },
  options
)
```

### HTML Elements

Satori 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.

For 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>`.

Also, 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).

You can find the list of supported HTML elements and their preset styles [here](https://github.com/vercel/satori/blob/main/src/handler/presets.ts).

#### Images

You can use `<img>` to embed images. However, `width`, and `height` attributes are recommended to set:

```jsx
await satori(
  <img src="https://picsum.photos/200/300" width={200} height={300} />,
  options
)
```

When using `background-image`, the image will be stretched to fit the element by default if you don't specify the size.

If 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:

```jsx
await satori(
  <img src="data:image/png;base64,..." width={200} height={300} />,
  // Or src={arrayBuffer}, src={buffer}
  options
)
```

### CSS

Satori 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:

<table>
<thead>
<tr>
  <th>Property</th>
  <th>Property Expanded</th>
  <th>Supported Values</th>
  <th>Example</th>
</tr>
</thead>
<tbody>

<tr>
<td colspan="2"><b>CSS Variables</b></td>
<td>Supported, including <code>--var-name</code> declaration and <code>var(--var-name)</code> usage with fallback values</td>
<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>
</tr>

<tr>
<td colspan="2"><code>display</code></td>
<td><code>flex</code>, <code>contents</code>, <code>none</code>, default to <code>flex</code></td>
<td></td>
</tr>

<tr>
<td colspan="2"><code>position</code></td>
<td><code>relative</code>, <code>static</code> and <code>absolute</code>, default to <code>relative</code></td>
<td></td>
</tr>

<tr>
<td colspan="2"><code>color</code></td>
<td>Supported</td>
<td></td>
</tr>

<tr><td rowspan="5"><code>margin</code></td></tr>
<tr><td><code>marginTop</code></td><td>Supported</td><td></td></tr>
<tr><td><code>marginRight</code></td><td>Supported</td><td></td></tr>
<tr><td><code>marginBottom</code></td><td>Supported</td><td></td></tr>
<tr><td><code>marginLeft</code></td><td>Supported</td><td></td></tr>

<tr><td rowspan="5">Position</td></tr>
<tr><td><code>top</code></td><td>Supported</td><td></td></tr>
<tr><td><code>right</code></td><td>Supported</td><td></td></tr>
<tr><td><code>bottom</code></td><td>Supported</td><td></td></tr>
<tr><td><code>left</code></td><td>Supported</td><td></td></tr>

<tr><td rowspan="3">Size</td></tr>
<tr><td><code>width</code></td><td>Supported</td><td></td></tr>
<tr><td><code>height</code></td><td>Supported</td><td></td></tr>

<tr><td rowspan="5">Min & max size</td></tr>
<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>
<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>
<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>
<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>

<tr><td rowspan="5"><code>border</code></td></tr>
<tr><td>Width (<code>borderWidth</code>, <code>borderTopWidth</code>, ...)</td><td>Supported</td><td></td></tr>
<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>
<tr><td>Color (<code>borderColor</code>, <code>borderTopColor</code>, ...)</td><td>Supported</td><td></td></tr>
<tr><td>
  Shorthand (<code>border</code>, <code>borderTop</code>, ...)</td><td>Supported, i.e. <code>1px solid gray</code><br/>
</td><td></td></tr>

<tr><td rowspan="6"><code>borderRadius</code></td></tr>
<tr><td><code>borderTopLeftRadius</code></td><td>Supported</td><td></td></tr>
<tr><td><code>borderTopRightRadius</code></td><td>Supported</td><td></td></tr>
<tr><td><code>borderBottomLeftRadius</code></td><td>Supported</td><td></td></tr>
<tr><td><code>borderBottomRightRadius</code></td><td>Supported</td><td></td></tr>
<tr><td>Shorthand</td><td>Supported, i.e. <code>5px</code>, <code>50% / 5px</code></td><td></td></tr>

<tr><td rowspan="11">Flex</td></tr>
<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>
<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>
<tr><td><code>flexGrow</code></td><td>Supported</td><td></td></tr>
<tr><td><code>flexShrink</code></td><td>Supported</td><td></td></tr>
<tr><td><code>flexBasis</code></td><td>Supported except for <code>auto</code></td><td></td></tr>
<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>
<tr><td><code>alignContent</code></td><td>Supported</td><td></td></tr>
<tr><td><code>alignSelf</code></td><td>Supported</td><td></td></tr>
<tr><td><code>justifyContent</code></td><td>Supported</td><td></td></tr>
<tr><td><code>gap</code></td><td>Supported</td><td></td></tr>

<tr><td rowspan="5">Font</td></tr>
<tr><td><code>fontFamily</code></td><td>Supported</td><td></td></tr>
<tr><td><code>fontSize</code></td><td>Supported</td><td></td></tr>
<tr><td><code>fontWeight</code></td><td>Supported</td><td></td></tr>
<tr><td><code>fontStyle</code></td><td>Supported</td><td></td></tr>

<tr><td rowspan="13">Text</td></tr>
<tr><td><code>tabSize</code></td><td>Supported</td><td></td></tr>
<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>
<tr><td><code>textIndent</code></td><td>Supported, including negative values (hanging indent)</td><td></td></tr>
<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>
<tr><td><code>textOverflow</code></td><td><code>clip</code>, <code>ellipsis</code>, defaults to <code>clip</code></td><td></td></tr>
<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>
<tr><td><code>textShadow</code></td><td>Supported</td><td></td></tr>
<tr><td><code>lineHeight</code></td><td>Supported</td><td></td></tr>
<tr><td><code>letterSpacing</code></td><td>Supported</td><td></td></tr>
<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>
<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>
<tr><td><code>textWrap</code></td><td><code>wrap</code>, <code>balance</code>, defaults to <code>wrap</code></td><td></td></tr>

<tr><td rowspan="7">Background</td></tr>
<tr><td><code>backgroundColor</code></td><td>Supported, single value</td><td></td></tr>
<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>
<tr><td><code>backgroundPosition</code></td><td>Support single value</td><td></td></tr>
<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>
<tr><td><code>backgroundClip</code></td><td><code>border-box</code>, <code>text</code></td><td></td></tr>
<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>

<tr><td rowspan="5"><code>transform</code></td></tr>
<tr><td>Translate (<code>translate</code>, <code>translateX</code>, <code>translateY</code>)</td><td>Supported</td><td></td></tr>
<tr><td>Rotate</td><td>Supported</td><td></td></tr>
<tr><td>Scale (<code>scale</code>, <code>scaleX</code>, <code>scaleY</code>)</td><td>Supported</td><td></td></tr>
<tr><td>Skew (<code>skew</code>, <code>skewX</code>, <code>skewY</code>)</td><td>Supported</td><td></td></tr>

<tr>
<td colspan="2"><code>transformOrigin</code></td>
<td>Support one-value and two-value syntax (both relative and absolute values)</td>
<td></td>
</tr>

<tr>
<td colspan="2"><code>objectFit</code></td>
<td>Supported</td>
<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>
</tr>

<tr>
<td colspan="2"><code>objectPosition</code></td>
<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>
<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>
</tr>

<tr>
<td colspan="2"><code>opacity</code></td>
<td>Supported</td>
<td></td>
</tr>

<tr>
<td colspan="2"><code>boxSizing</code></td>
<td>Supported</td>
<td></td>
</tr>

<tr>
<td colspan="2"><code>boxShadow</code></td>
<td>Supported</td>
<td></td>
</tr>

<tr>
<td colspan="2"><code>overflow</code></td>
<td><code>visible</code> and <code>hidden</code>, default to <code>visible</code></td>
<td></td>
</tr>

<tr>
<td colspan="2"><code>filter</code></td>
<td>Supported</td>
<td></td>
</tr>

<tr>
<td colspan="2"><code>clipPath</code></td>
<td>Supported</td>
<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>
</tr>

<tr>
<td colspan="2"><code>lineClamp</code></td>
<td>Supported</td>
<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>
</tr>

<tr><td rowspan="5">Mask</td></tr>
<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>
<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>
<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>
<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>

<tr>
<td rowspan="2"><code>WebkitTextStroke</code>
<td><code>WebkitTextStrokeWidth</code></td>
<td>Supported</td>
<td></td>
</tr>
<tr>
<td><code>WebkitTextStrokeColor</code></td>
<td>Supported</td>
<td></td>
</tr>

</tbody>
</table>

Note:

1. Three-dimensional transforms are not supported.
2. There is no `z-index` support in SVG. Elements that come later in the document will be painted on top.
3. `calc` isn't supported.
4. `currentColor` support is only available for the `color` property.
5. CSS variables (custom properties) are supported, including inheritance, fallback values, and nested variables.

### Language and Typography

Advanced typography features such as kerning, ligatures and other OpenType features are not currently supported.

RTL languages are not supported either.

#### Fonts

Satori 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):

```js
await satori(
  <div style={{ fontFamily: 'Inter' }}>Hello</div>,
  {
    width: 600,
    height: 400,
    fonts: [
      {
        name: 'Inter',
        data: inter,
        weight: 400,
        style: 'normal',
      },
      {
        name: 'Inter',
        data: interBold,
        weight: 700,
        style: 'normal',
      },
    ],
  }
)
```

Multiple fonts can be passed to Satori and used in `fontFamily`.

> [!TIP]
> 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)

#### Emojis

To render custom images for specific graphemes, you can use `graphemeImages` option to map the grapheme to an image source:

```jsx
await satori(
  <div>Next.js is 🤯!</div>,
  {
    ...,
    graphemeImages: {
      '🤯': 'https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/1f92f.svg',
    },
  }
)
```

The image will be resized to the current font-size (both width and height) as a square.

#### Locales

Satori supports rendering text in different locales. You can specify the supported locales via the `lang` attribute:

```jsx
await satori(
  <div lang="ja-JP">骨</div>
)
```

Same 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.

Supported locales are exported as the `Locale` enum type.

#### Dynamically Load Emojis and Fonts

Satori 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:

```jsx
await satori(
  <div>👋 你好</div>,
  {
    // `code` will be the detected language code, `emoji` if it's an Emoji, or `unknown` if not able to tell.
    // `segment` will be the content to render.
    loadAdditionalAsset: async (code: string, segment: string) => {
      if (code === 'emoji') {
        // if segment is an emoji
        return `data:image/svg+xml;base64,...`
      }

      // if segment is normal text
      return loadFontFromSystem(code)
    }
  }
)
```

### Runtime Support

Satori 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.

If there is a limitation on dynamically loading WASM (e.g. Cloudflare Workers), you can use the Standalone Build which is mentioned below.

#### Standalone Build of Satori

Satori's standalone build doesn't include Yoga's WASM binary by default, and you need to load it manually before using Satori.

First, 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:

```jsx
import satori, { init } from 'satori/standalone'

const res = await fetch('https://unpkg.com/satori/yoga.wasm')
const yogaWasm = await res.arrayBuffer()

await init(yogaWasm)

// Now you can use satori as usual
const svg = await satori(...)
```

Of course, you can also load the `yoga.wasm` file from your local disk via `fs.readFile` in Node.js or other methods.

### Font Embedding

By 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.

You can turn off this behavior by setting `embedFont` to `false`, and Satori will use `<text>` instead:

```jsx
const svg = await satori(
  <div style={{ color: 'black' }}>hello, world</div>,
  {
    ...,
    embedFont: false,
  },
)
```

### Pixel Grid Rounding

Set `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.

```jsx
const svg = await satori(
  <div style={{ color: 'black' }}>hello, world</div>,
  {
    ...,
    pointScaleFactor: 2,
  },
)
```

### Debug

To draw the bounding box for debugging, you can pass `debug: true` as an option:

```jsx
const svg = await satori(
  <div style={{ color: 'black' }}>hello, world</div>,
  {
    ...,
    debug: true,
  },
)
```

<br/>

## Contribute

You 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.

<br/>

## Author

- Shu Ding ([@shuding](https://twitter.com/shuding))

---

<a aria-label="Vercel logo" href="https://vercel.com">
  <img src="https://badgen.net/badge/icon/Made%20by%20Vercel?icon=zeit&label&color=black&labelColor=black">
</a>


================================================
FILE: package.json
================================================
{
  "name": "satori",
  "version": "0.0.0-development",
  "description": "Enlightened library to convert HTML and CSS to SVG.",
  "module": "./dist/index.js",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "typesVersions": {
    "*": {
      "wasm": [
        "./dist/index.d.ts"
      ]
    }
  },
  "type": "module",
  "license": "MPL-2.0",
  "files": [
    "dist/**",
    "yoga.wasm"
  ],
  "imports": {
    "#satori/jsx/jsx-runtime": "./src/jsx/jsx-runtime.ts",
    "#satori/jsx/jsx-dev-runtime": "./src/jsx/jsx-runtime.ts"
  },
  "exports": {
    "./package.json": "./package.json",
    "./yoga.wasm": "./yoga.wasm",
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./standalone": {
      "import": {
        "types": "./dist/standalone.d.ts",
        "default": "./dist/standalone.js"
      },
      "require": {
        "types": "./dist/standalone.d.cts",
        "default": "./dist/standalone.cjs"
      }
    },
    "./jsx": {
      "types": "./dist/jsx/index.d.ts",
      "import": "./dist/jsx/index.js",
      "require": "./dist/jsx/index.cjs"
    },
    "./jsx/jsx-runtime": {
      "types": "./dist/jsx/jsx-runtime.d.ts",
      "import": "./dist/jsx/jsx-runtime.js",
      "require": "./dist/jsx/jsx-runtime.cjs"
    },
    "./jsx/jsx-dev-runtime": {
      "types": "./dist/jsx/jsx-runtime.d.ts",
      "import": "./dist/jsx/jsx-runtime.js",
      "require": "./dist/jsx/jsx-runtime.cjs"
    }
  },
  "scripts": {
    "prepare": "husky install",
    "dev": "pnpm run dev:default",
    "dev:default": "NODE_ENV=development tsup src/index.ts --watch --ignore-watch playground",
    "dev:playground": "turbo dev --filter=satori-playground...",
    "build": "pnpm run build:default && pnpm run build:standalone",
    "build:default": "NODE_ENV=production tsup",
    "build:standalone": "NODE_ENV=production SATORI_STANDALONE=1 tsup",
    "test": "NODE_ENV=test vitest run",
    "test:ui": "NODE_ENV=test vitest --ui --coverage.enabled",
    "test-type": "tsc -p tsconfig.json --noEmit && tsc -p playground/tsconfig.json --noEmit",
    "dev:test": "NODE_ENV=test vitest --update",
    "lint": "eslint . --ext .js,.jsx,.ts,.tsx --quiet --cache",
    "lint:fix": "pnpm lint --fix",
    "prettier-check": "prettier --check .",
    "prettier-fix": "prettier --write --list-different . --cache",
    "ci-check": "concurrently \"pnpm prettier-check\" \"pnpm test-type\" \"pnpm lint\"",
    "benchmark": "node --experimental-strip-types test/benchmark/index.ts"
  },
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix --cache",
      "prettier --write --cache"
    ]
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/vercel/satori.git"
  },
  "keywords": [
    "HTML",
    "JSX",
    "SVG",
    "converter",
    "renderer"
  ],
  "author": "Shu Ding <g@shud.in>",
  "bugs": {
    "url": "https://github.com/vercel/satori/issues"
  },
  "homepage": "https://github.com/vercel/satori#readme",
  "devDependencies": {
    "@resvg/resvg-js": "^2.1.0",
    "@types/node": "^16",
    "@types/opentype.js": "^1.3.3",
    "@types/react": "^17.0.38",
    "@typescript-eslint/eslint-plugin": "^5.40.0",
    "@typescript-eslint/parser": "^5.40.0",
    "@vitest/coverage-v8": "^0.32.0",
    "@vitest/ui": "^0.32.0",
    "concurrently": "^7.3.0",
    "esbuild-plugin-replace": "^1.2.0",
    "eslint": "^8.25.0",
    "eslint-plugin-react": "^7.31.10",
    "eslint-plugin-react-hooks": "^4.6.0",
    "husky": "8.0.3",
    "jest-image-snapshot": "^6.1.0",
    "lint-staged": "13.1.0",
    "mitata": "^1.0.34",
    "prettier": "^2.7.1",
    "react": "^17.0.2",
    "sharp": "^0.34.3",
    "tsup": "^7.1.0",
    "turbo": "^1.6.3",
    "twrnc": "^3.4.0",
    "typescript": "^5",
    "vitest": "^0.32.0"
  },
  "dependencies": {
    "@shuding/opentype.js": "1.4.0-beta.0",
    "css-background-parser": "^0.1.0",
    "css-box-shadow": "1.0.0-3",
    "css-gradient-parser": "^0.0.17",
    "css-to-react-native": "^3.0.0",
    "emoji-regex-xs": "^2.0.1",
    "escape-html": "^1.0.3",
    "linebreak": "^1.1.0",
    "parse-css-color": "^0.2.1",
    "postcss-value-parser": "^4.2.0",
    "yoga-layout": "^3.2.1"
  },
  "packageManager": "pnpm@8.7.0",
  "engines": {
    "node": ">=16"
  },
  "pnpm": {
    "patchedDependencies": {
      "yoga-layout@3.2.1": "patches/yoga-layout@3.2.1.patch"
    }
  }
}


================================================
FILE: patches/yoga-layout@3.2.1.patch
================================================
diff --git a/dist/binaries/yoga-wasm-esm.js b/dist/binaries/yoga-wasm-esm.js
new file mode 100644
index 0000000000000000000000000000000000000000..bf3970ef4d20ce2ee6b9b1e1e571ec3191c2d4a1
--- /dev/null
+++ b/dist/binaries/yoga-wasm-esm.js
@@ -0,0 +1,67 @@
+let _scriptDir = ''
+
+export default function (loadYoga) {
+loadYoga = loadYoga || {};
+
+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);
+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}
+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;
+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}
+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)}}
+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}
+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;
+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={};
+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]}
+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={};
+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)}
+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);}}
+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(){}
+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}}
+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=[]}
+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)}
+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"),
+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}
+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)}
+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}
+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;
+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([", "]));}
+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]=
+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}
+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);}}
+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}
+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}
+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}
+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};
+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};
+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};
+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"]()};
+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})};
+mb=h.UnboundTypeError=Da("UnboundTypeError");
+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;
+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 "+
+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===
+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,
+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,
+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);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,
+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,
+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[]})},
+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,
+{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};
+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=
+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>=
+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=
+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===
+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,
+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,
+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=
+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}};
+(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,
+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{}})();
+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)};
+h.dynCall_jiji=function(){return(h.dynCall_jiji=h.asm.L).apply(null,arguments)};var Kb;G=function Lb(){Kb||Mb();Kb||(G=Lb)};
+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())}}
+if(h.preInit)for("function"==typeof h.preInit&&(h.preInit=[h.preInit]);0<h.preInit.length;)h.preInit.pop()();Mb();
+
+  return loadYoga.ready
+}
diff --git a/dist/src/load.js b/dist/src/load.js
index efd1554faeebc6d6d792878f0ec63e4122ff3d96..232f122ef8fb2ba1c43844f88114ba46de2d5245 100644
--- a/dist/src/load.js
+++ b/dist/src/load.js
@@ -7,11 +7,14 @@
  * @format
  */
 
-// @ts-ignore untyped from Emscripten
-import loadYogaImpl from '../binaries/yoga-wasm-base64-esm.js';
 import wrapAssembly from "./wrapAssembly.js";
-export async function loadYoga() {
-  return wrapAssembly(await loadYogaImpl());
+export async function loadYoga(wasmOptions) {
+  const { default: loadYogaImpl } =
+    process.env.SATORI_STANDALONE === '1'
+      ? await import('../binaries/yoga-wasm-esm.js')
+      : await import('../binaries/yoga-wasm-base64-esm.js')
+
+  return wrapAssembly(await loadYogaImpl(wasmOptions));
 }
 export * from "./generated/YGEnums.js";
 //# sourceMappingURL=load.js.map


================================================
FILE: playground/LICENSE
================================================
Mozilla Public License Version 2.0
==================================

1. Definitions
--------------

1.1. "Contributor"
    means each individual or legal entity that creates, contributes to
    the creation of, or owns Covered Software.

1.2. "Contributor Version"
    means the combination of the Contributions of others (if any) used
    by a Contributor and that particular Contributor's Contribution.

1.3. "Contribution"
    means Covered Software of a particular Contributor.

1.4. "Covered Software"
    means Source Code Form to which the initial Contributor has attached
    the notice in Exhibit A, the Executable Form of such Source Code
    Form, and Modifications of such Source Code Form, in each case
    including portions thereof.

1.5. "Incompatible With Secondary Licenses"
    means

    (a) that the initial Contributor has attached the notice described
        in Exhibit B to the Covered Software; or

    (b) that the Covered Software was made available under the terms of
        version 1.1 or earlier of the License, but not also under the
        terms of a Secondary License.

1.6. "Executable Form"
    means any form of the work other than Source Code Form.

1.7. "Larger Work"
    means a work that combines Covered Software with other material, in
    a separate file or files, that is not Covered Software.

1.8. "License"
    means this document.

1.9. "Licensable"
    means having the right to grant, to the maximum extent possible,
    whether at the time of the initial grant or subsequently, any and
    all of the rights conveyed by this License.

1.10. "Modifications"
    means any of the following:

    (a) any file in Source Code Form that results from an addition to,
        deletion from, or modification of the contents of Covered
        Software; or

    (b) any new file in Source Code Form that contains any Covered
        Software.

1.11. "Patent Claims" of a Contributor
    means any patent claim(s), including without limitation, method,
    process, and apparatus claims, in any patent Licensable by such
    Contributor that would be infringed, but for the grant of the
    License, by the making, using, selling, offering for sale, having
    made, import, or transfer of either its Contributions or its
    Contributor Version.

1.12. "Secondary License"
    means either the GNU General Public License, Version 2.0, the GNU
    Lesser General Public License, Version 2.1, the GNU Affero General
    Public License, Version 3.0, or any later versions of those
    licenses.

1.13. "Source Code Form"
    means the form of the work preferred for making modifications.

1.14. "You" (or "Your")
    means an individual or a legal entity exercising rights under this
    License. For legal entities, "You" includes any entity that
    controls, is controlled by, or is under common control with You. For
    purposes of this definition, "control" means (a) the power, direct
    or indirect, to cause the direction or management of such entity,
    whether by contract or otherwise, or (b) ownership of more than
    fifty percent (50%) of the outstanding shares or beneficial
    ownership of such entity.

2. License Grants and Conditions
--------------------------------

2.1. Grants

Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:

(a) under intellectual property rights (other than patent or trademark)
    Licensable by such Contributor to use, reproduce, make available,
    modify, display, perform, distribute, and otherwise exploit its
    Contributions, either on an unmodified basis, with Modifications, or
    as part of a Larger Work; and

(b) under Patent Claims of such Contributor to make, use, sell, offer
    for sale, have made, import, and otherwise transfer either its
    Contributions or its Contributor Version.

2.2. Effective Date

The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.

2.3. Limitations on Grant Scope

The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:

(a) for any code that a Contributor has removed from Covered Software;
    or

(b) for infringements caused by: (i) Your and any other third party's
    modifications of Covered Software, or (ii) the combination of its
    Contributions with other software (except as part of its Contributor
    Version); or

(c) under Patent Claims infringed by Covered Software in the absence of
    its Contributions.

This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).

2.4. Subsequent Licenses

No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).

2.5. Representation

Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.

2.6. Fair Use

This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.

2.7. Conditions

Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.

3. Responsibilities
-------------------

3.1. Distribution of Source Form

All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.

3.2. Distribution of Executable Form

If You distribute Covered Software in Executable Form then:

(a) such Covered Software must also be made available in Source Code
    Form, as described in Section 3.1, and You must inform recipients of
    the Executable Form how they can obtain a copy of such Source Code
    Form by reasonable means in a timely manner, at a charge no more
    than the cost of distribution to the recipient; and

(b) You may distribute such Executable Form under the terms of this
    License, or sublicense it under different terms, provided that the
    license for the Executable Form does not attempt to limit or alter
    the recipients' rights in the Source Code Form under this License.

3.3. Distribution of a Larger Work

You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).

3.4. Notices

You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.

3.5. Application of Additional Terms

You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.

4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------

If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.

5. Termination
--------------

5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.

5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.

5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.

************************************************************************
*                                                                      *
*  6. Disclaimer of Warranty                                           *
*  -------------------------                                           *
*                                                                      *
*  Covered Software is provided under this License on an "as is"       *
*  basis, without warranty of any kind, either expressed, implied, or  *
*  statutory, including, without limitation, warranties that the       *
*  Covered Software is free of defects, merchantable, fit for a        *
*  particular purpose or non-infringing. The entire risk as to the     *
*  quality and performance of the Covered Software is with You.        *
*  Should any Covered Software prove defective in any respect, You     *
*  (not any Contributor) assume the cost of any necessary servicing,   *
*  repair, or correction. This disclaimer of warranty constitutes an   *
*  essential part of this License. No use of any Covered Software is   *
*  authorized under this License except under this disclaimer.         *
*                                                                      *
************************************************************************

************************************************************************
*                                                                      *
*  7. Limitation of Liability                                          *
*  --------------------------                                          *
*                                                                      *
*  Under no circumstances and under no legal theory, whether tort      *
*  (including negligence), contract, or otherwise, shall any           *
*  Contributor, or anyone who distributes Covered Software as          *
*  permitted above, be liable to You for any direct, indirect,         *
*  special, incidental, or consequential damages of any character      *
*  including, without limitation, damages for lost profits, loss of    *
*  goodwill, work stoppage, computer failure or malfunction, or any    *
*  and all other commercial damages or losses, even if such party      *
*  shall have been informed of the possibility of such damages. This   *
*  limitation of liability shall not apply to liability for death or   *
*  personal injury resulting from such party's negligence to the       *
*  extent applicable law prohibits such limitation. Some               *
*  jurisdictions do not allow the exclusion or limitation of           *
*  incidental or consequential damages, so this exclusion and          *
*  limitation may not apply to You.                                    *
*                                                                      *
************************************************************************

8. Litigation
-------------

Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.

9. Miscellaneous
----------------

This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.

10. Versions of the License
---------------------------

10.1. New Versions

Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.

10.2. Effect of New Versions

You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.

10.3. Modified Versions

If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).

10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses

If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.

Exhibit A - Source Code Form License Notice
-------------------------------------------

  This Source Code Form is subject to the terms of the Mozilla Public
  License, v. 2.0. If a copy of the MPL was not distributed with this
  file, You can obtain one at http://mozilla.org/MPL/2.0/.

If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.

You may add additional accurate notices of copyright ownership.

Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------

  This Source Code Form is "Incompatible With Secondary Licenses", as
  defined by the Mozilla Public License, v. 2.0.


================================================
FILE: playground/cards/playground-data.ts
================================================
export type Tabs = {
  [x: string]: string
}

const playgroundTabs: Tabs = {
  helloworld: `<div
  style={{
    height: '100%',
    width: '100%',
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#fff',
    fontSize: 32,
    fontWeight: 600,
  }}
>
  <svg
    width="75"
    viewBox="0 0 75 65"
    fill="#000"
    style={{ margin: '0 75px' }}
  >
    <path d="M37.59.25l36.95 64H.64l36.95-64z"></path>
  </svg>
  <div style={{ marginTop: 40 }}>Hello, World</div>
</div>
`,
  Vercel: `<div
  style={{
    height: '100%',
    width: '100%',
    display: 'flex',
    textAlign: 'center',
    alignItems: 'center',
    justifyContent: 'center',
    flexDirection: 'column',
    flexWrap: 'nowrap',
    backgroundColor: 'white',
    backgroundImage: 'radial-gradient(circle at 25px 25px, lightgray 2%, transparent 0%), radial-gradient(circle at 75px 75px, lightgray 2%, transparent 0%)',
    backgroundSize: '100px 100px',
  }}
>
  <div
    style={{
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
    }}
  >
    <svg
      height={80}
      viewBox="0 0 75 65"
      fill="black"
      style={{ margin: '0 75px' }}
    >
      <path d="M37.59.25l36.95 64H.64l36.95-64z"></path>
    </svg>
  </div>
  <div
    style={{
      display: 'flex',
      fontSize: 40,
      fontStyle: 'normal',
      color: 'black',
      marginTop: 30,
      lineHeight: 1.8,
      whiteSpace: 'pre-wrap',
    }}
  >
    <b>Vercel Edge Network</b>
  </div>
</div>
`,
  rauchg: `<div
  style={{
    display: 'flex',
    height: '100%',
    width: '100%',
    alignItems: 'center',
    justifyContent: 'center',
    letterSpacing: '-.02em',
    fontWeight: 700,
    background: 'white',
  }}
>
  <div
    style={{
      left: 42,
      top: 42,
      position: 'absolute',
      display: 'flex',
      alignItems: 'center',
    }}
  >
    <span
      style={{
        width: 24,
        height: 24,
        background: 'black',
      }}
    />
    <span
      style={{
        marginLeft: 8,
        fontSize: 20,
      }}
    >
      rauchg.com
    </span>
  </div>
  <div
    style={{
      display: 'flex',
      flexWrap: 'wrap',
      justifyContent: 'center',
      padding: '20px 50px',
      margin: '0 42px',
      fontSize: 40,
      width: 'auto',
      maxWidth: 550,
      textAlign: 'center',
      backgroundColor: 'black',
      color: 'white',
      lineHeight: 1.4,
    }}
  >
    Making the Web. Faster.
  </div>
</div>
`,
  'Tailwind (experimental)': `// Modified based on https://tailwindui.com/components/marketing/sections/cta-sections

<div tw="flex flex-col w-full h-full items-center justify-center bg-white">
  <div tw="bg-gray-50 flex w-full">
    <div tw="flex flex-col md:flex-row w-full py-12 px-4 md:items-center justify-between p-8">
      <h2 tw="flex flex-col text-3xl sm:text-4xl font-bold tracking-tight text-gray-900 text-left">
        <span>Ready to dive in?</span>
        <span tw="text-indigo-600">Start your free trial today.</span>
      </h2>
      <div tw="mt-8 flex md:mt-0">
        <div tw="flex rounded-md shadow">
          <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>
        </div>
        <div tw="ml-3 flex rounded-md shadow">
          <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>
        </div>
      </div>
    </div>
  </div>
</div>`,
  Gradients: `<div
  style={{
    display: 'flex',
    height: '100%',
    width: '100%',
    alignItems: 'center',
    justifyContent: 'center',
    flexDirection: 'column',
    backgroundImage: 'linear-gradient(to bottom, #dbf4ff, #fff1f1)',
    fontSize: 60,
    letterSpacing: -2,
    fontWeight: 700,
    textAlign: 'center',
  }}
  >
  <div
    style={{
      backgroundImage: 'linear-gradient(90deg, rgb(0, 124, 240), rgb(0, 223, 216))',
      backgroundClip: 'text',
      '-webkit-background-clip': 'text',
      color: 'transparent',
    }}
  >
    Develop
  </div>
  <div
    style={{
      backgroundImage: 'linear-gradient(90deg, rgb(121, 40, 202), rgb(255, 0, 128))',
      backgroundClip: 'text',
      '-webkit-background-clip': 'text',
      color: 'transparent',
    }}
  >
    Preview
  </div>
  <div
    style={{
      backgroundImage: 'linear-gradient(90deg, rgb(255, 77, 77), rgb(249, 203, 40))',
      backgroundClip: 'text',
      '-webkit-background-clip': 'text',
      color: 'transparent',
    }}
  >
    Ship
  </div>
</div>
`,
  'Color Models': `<div
  style={{
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'flex-start',
    fontSize: 24,
    fontWeight: 600,
    textAlign: 'left',
    padding: 70,
    color: 'red',
    backgroundImage: 'linear-gradient(to right, #334d50, #cbcaa5)',
    height: '100%',
    width: '100%'
  }}
>

  <div style={{ display: 'flex', flexDirection: 'column' }}>
    <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '15px', color: '#fff' }}>
      #fff
      <div style={{ fontWeight: 100 }}>hexadecimal</div>
    </div>
    <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '15px', color: '#ffffff70' }}>
      #ffffff70
      <div style={{ fontWeight: 100 }}>hexadecimal + transparency</div>
    </div>
    <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '15px', color: 'rgb(45, 45, 45)' }}>
      rgb(45, 45, 45)
      <div style={{ fontWeight: 100 }}>rgb</div>
    </div>
    <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '15px', color: 'rgb(45, 45, 45, 0.3)' }}>
      rgb(45, 45, 45, 0.3)
      <div style={{ fontWeight: 100 }}>rgba</div>
    </div>
  </div>

  <div style={{ display: 'flex', flexDirection: 'column' }}>
    <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '15px', color: 'hsl(186, 22%, 26%)' }}>
      hsl(186, 22%, 26%)
      <div style={{ fontWeight: 100 }}>hsl</div>
    </div>
    <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '15px', color: 'hsla(186, 22%, 26%, 40%)' }}>
      hsla(186, 22%, 26%, 40%)
      <div style={{ fontWeight: 100 }}>hsla</div>
    </div>
    <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '15px', color: 'white' }}>
      "white"
      <div style={{ fontWeight: 100 }}>predefined color names</div>
    </div>
    <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '15px', color: 'currentcolor' }}>
      should be red
      <div style={{ fontWeight: 100 }}>"currentcolor"</div>
    </div>
  </div>
</div>`,
  Advanced: `// Fallback fonts and Emoji are dynamically loaded
// from Google Fonts and CDNs in this demo.

// You can also return a function component in the playground.
() => {
  function Label({ children }) {
    return <label style={{
      fontSize: 15,
      fontWeight: 600,
      textTransform: 'uppercase',
      letterSpacing: 1,
      margin: '25px 0 10px',
      color: 'gray',
    }}>
      {children}
    </label>
  }

  return (
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        height: '100%',
        width: '100%',
        padding: '10px 20px',
        justifyContent: 'center',
        fontFamily: 'Inter, "Material Icons"',
        fontSize: 28,
        backgroundColor: 'white',
      }}
      >
      <Label>Language & Font subsets</Label>
      <div>
        Hello! 你好! 안녕! こんにちは! Χαίρετε! Hallå!
      </div>
      <Label>Emoji</Label>
      <div>
        👋 😄 🎉 🎄 🦋
      </div>
      <Label>Icon font</Label>
      <div>
          &#xe766; &#xeb9b; &#xf089;
      </div>
      <Label>Lang attribute</Label>
      <div style={{ display: 'flex' }}>
        <span lang="ja-JP">
          骨茶
        </span>/
        <span lang="zh-CN">
          骨茶
        </span>/
        <span lang="zh-TW">
          骨茶
        </span>/
        <span lang="zh-HK">
          骨茶
        </span>
      </div>
    </div>
  )
}  
`,
}

export default playgroundTabs


================================================
FILE: playground/cards/preview-tabs.ts
================================================
const previewTabs = [
  'SVG (Satori)',
  'PNG (Satori + resvg-js)', // https://github.com/yisibl/resvg-js
  'PDF (Satori + PDFKit)',
  'HTML (Native)',
]

export default previewTabs


================================================
FILE: playground/components/introduction.module.css
================================================
.container {
  position: fixed;
  left: 20px;
  bottom: 20px;
  width: 700px;
  min-height: 200px;
  max-width: calc(100vw - 40px);
  max-height: calc(100vh - 40px);
  margin: auto;
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  padding: 20px;
  color: #ddd;
  font-size: 14px;
  backdrop-filter: blur(24px);
  background-color: rgb(0 0 0 / 88%);
  box-shadow: 0 20px 40px #0000005c, 0 0 0 1px #868686;
  z-index: 4;
  opacity: 0;
  line-height: 1.6;
  letter-spacing: -0.01rem;
  word-spacing: -0.12rem;
  animation: fadein 0.4s ease 0.4s forwards;
}

.container p {
  margin: 0;
  margin-bottom: 1em;
}

.container code {
  background-color: #4a4a4a;
  padding: 0 4px;
  border-radius: 4px;
}

.container button {
  appearance: none;
  border: none;
  background: white;
  border-radius: 5px;
  padding: 8px 12px;
  width: 120px;
  align-self: flex-start;
  font-family: inherit;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;
}

.container button:hover {
  background: #ccc;
}

@keyframes fadein {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}


================================================
FILE: playground/components/introduction.tsx
================================================
import React from 'react'
import styles from './introduction.module.css'

interface IProps {
  onClose: React.MouseEventHandler<HTMLButtonElement>
}

export default function Introduction({ onClose }: IProps) {
  return (
    <div className={styles.container}>
      <p>👋 Welcome to the Vercel OG Image playground!</p>
      <p style={{ flex: 1 }}>
        You can use this tool to test and preview OG image cards generated with{' '}
        <code>@vercel/og</code>. To learn more about how to add it to your
        project, please read{' '}
        <a
          href='https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation'
          target='_blank'
          rel='noreferrer'
        >
          our documentation
        </a>{' '}
        or the{' '}
        <a
          href='https://vercel.com/blog/introducing-vercel-og-image-generation-fast-dynamic-social-card-images'
          target='_blank'
          rel='noreferrer'
        >
          announcement post
        </a>
        .
      </p>
      <button onClick={onClose}>Okay!</button>
    </div>
  )
}


================================================
FILE: playground/components/panel-resize-handle.module.css
================================================
.handle {
  flex: 0 0 1.5em;
  position: relative;
  outline: none;
  --background-color: #efefef;
}
.handle:hover {
  --background-color: #dbdbdb;
}
.handle[data-resize-handle-active] {
  --background-color: #f5f5f5;
}

.handle > div {
  position: absolute;
  top: 0.25em;
  bottom: 0.25em;
  left: 0.25em;
  right: 0.25em;
  background-color: var(--background-color);
  border: 1px solid #0000000f;
  border-radius: 4px;
  transition: background-color 0.2s linear;
}

.handle[data-panel-group-direction='horizontal'] > div {
  top: 0;
  bottom: 0;
}

.handle[data-panel-group-direction='vertical'] > div {
  left: 0;
  right: 0;
}

.handle svg {
  width: 1em;
  height: 1em;
  position: absolute;
  left: calc(50% - 0.5rem);
  top: calc(50% - 0.5rem);
}

.handle[data-panel-group-direction='horizontal'] svg {
  transform: rotate(90deg);
}


================================================
FILE: playground/components/panel-resize-handle.tsx
================================================
import React from 'react'
import { PanelResizeHandle as PanelResizeHandleImpl } from 'react-resizable-panels'

import styles from './panel-resize-handle.module.css'

export default function PanelResizeHandle() {
  return (
    <PanelResizeHandleImpl className={styles.handle}>
      <div>
        <svg
          xmlns='http://www.w3.org/2000/svg'
          fill='none'
          viewBox='0 0 24 24'
          strokeWidth={1.5}
          stroke='#adadad'
        >
          <path
            strokeLinecap='round'
            strokeLinejoin='round'
            d='M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5'
          />
        </svg>
      </div>
    </PanelResizeHandleImpl>
  )
}


================================================
FILE: playground/components/resvg_worker.ts
================================================
import * as resvg from '@resvg/resvg-wasm'

const wasmPath = new URL('@resvg/resvg-wasm/index_bg.wasm', import.meta.url)
fetch(wasmPath).then((res) => resvg.initWasm(res))

self.onmessage = (e) => {
  const { svg, width, _id } = e.data

  const renderer = new resvg.Resvg(svg, {
    fitTo: {
      mode: 'width',
      value: width,
    },
  })
  const image = renderer.render()
  const pngBuffer = image.asPng()
  const url = URL.createObjectURL(new Blob([pngBuffer], { type: 'image/png' }))
  self.postMessage({ _id, url })
}


================================================
FILE: playground/decs.d.ts
================================================
declare module 'pdfkit/js/pdfkit.standalone'
declare module 'satori'


================================================
FILE: playground/index.d.ts
================================================
export {}

declare global {
  interface Window {
    __resource: any
  }
}


================================================
FILE: playground/next-env.d.ts
================================================
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.


================================================
FILE: playground/package.json
================================================
{
  "private": true,
  "name": "satori-playground",
  "license": "MPL-2.0",
  "scripts": {
    "dev": "next",
    "debug": "node --inspect node_modules/next/dist/bin/next",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "@babel/runtime": "^7.19.0",
    "@monaco-editor/react": "^4.4.5",
    "@resvg/resvg-wasm": "^2.3.1",
    "blob-stream": "^0.1.3",
    "copy-to-clipboard": "^3.3.2",
    "fflate": "^0.7.3",
    "intl-segmenter-polyfill": "^0.4.4",
    "js-base64": "^3.7.2",
    "next": "^12.2.5",
    "pdfkit": "^0.13.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-hot-toast": "^2.3.0",
    "react-live": "^2.4.1",
    "react-resizable-panels": "^0.0.30",
    "satori": "workspace:*",
    "svg-to-pdfkit": "^0.1.8"
  },
  "devDependencies": {
    "@types/blob-stream": "^0.1.30",
    "@types/pdfkit": "^0.12.7",
    "@types/react-dom": "^18.0.6",
    "@types/svg-to-pdfkit": "^0.1.0",
    "regenerator": "link:@babel/runtime/regenerator"
  }
}


================================================
FILE: playground/pages/_app.tsx
================================================
import React from 'react'
import Head from 'next/head'
import { AppProps } from 'next/app'

import '../styles.css'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Head>
        <title>Vercel OG Image Playground</title>
        <meta
          name='viewport'
          content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no'
        />
        <meta name='theme-color' content='#fff' />
        <meta name='title' content='Vercel OG Image Playground' />
        <meta
          name='description'
          content='Generate Open Graph images with Vercel’s Edge Function.'
        />
        <meta property='og:type' content='website' />
        <meta property='og:url' content='https://og-playground.vercel.app/' />
        <meta property='og:title' content='Vercel OG Image Playground' />
        <meta
          property='og:description'
          content='Generate Open Graph images with Vercel’s Edge Function.'
        />
        <meta
          property='og:image'
          content='https://og-playground.vercel.app/og.png'
        />
        <meta property='twitter:card' content='summary_large_image' />
        <meta
          property='twitter:url'
          content='https://og-playground.vercel.app/'
        />
        <meta property='twitter:title' content='Vercel OG Image Playground' />
        <meta
          property='twitter:description'
          content='Generate Open Graph images with Vercel’s Edge Function.'
        />
        <meta
          property='twitter:image'
          content='https://og-playground.vercel.app/og.png'
        />
        <link
          rel='preload'
          href='/inter-latin-ext-400-normal.woff'
          as='fetch'
          crossOrigin='anonymous'
        />
        <link
          rel='preload'
          href='/inter-latin-ext-700-normal.woff'
          as='fetch'
          crossOrigin='anonymous'
        />
        <link
          rel='preload'
          href='/material-icons-base-400-normal.woff'
          as='fetch'
          crossOrigin='anonymous'
        />
        <link
          rel='preload'
          href='/iaw-mono-var.woff2'
          as='fetch'
          crossOrigin='anonymous'
        />
        <link rel='shortcut icon' href='/favicon.ico' type='image/x-icon' />
        <link rel='icon' href='/favicon.ico' type='image/x-icon' />
      </Head>
      <Component {...pageProps} />
    </>
  )
}


================================================
FILE: playground/pages/_document.tsx
================================================
import React from 'react'
import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
  return (
    <Html lang='en'>
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}


================================================
FILE: playground/pages/api/font.ts
================================================
import type { NextRequest } from 'next/server'
import { FontDetector, languageFontMap } from '../../utils/font'

export const config = {
  runtime: 'experimental-edge',
}

const detector = new FontDetector()

// Our own encoding of multiple fonts and their code, so we can fetch them in one request. The structure is:
// [1 byte = X, length of language code][X bytes of language code string][4 bytes = Y, length of font][Y bytes of font data]
// Note that:
// - The language code can't be longer than 255 characters.
// - The language code can't contain non-ASCII characters.
// - The font data can't be longer than 4GB.
// When there are multiple fonts, they are concatenated together.
function encodeFontInfoAsArrayBuffer(code: string, fontData: ArrayBuffer) {
  // 1 byte per char
  const buffer = new ArrayBuffer(1 + code.length + 4 + fontData.byteLength)
  const bufferView = new Uint8Array(buffer)
  // 1 byte for the length of the language code
  bufferView[0] = code.length
  // X bytes for the language code
  for (let i = 0; i < code.length; i++) {
    bufferView[i + 1] = code.charCodeAt(i)
  }

  // 4 bytes for the length of the font data
  new DataView(buffer).setUint32(1 + code.length, fontData.byteLength, false)

  // Y bytes for the font data
  bufferView.set(new Uint8Array(fontData), 1 + code.length + 4)

  return buffer
}

export default async function loadGoogleFont(req: NextRequest) {
  if (req.nextUrl.pathname !== '/api/font') return

  const { searchParams } = new URL(req.url)

  const fonts = searchParams.getAll('fonts')
  const text = searchParams.get('text')

  if (!fonts || fonts.length === 0 || !text) return

  const textByFont = await detector.detect(text, fonts)

  const _fonts = Object.keys(textByFont)

  const encodedFontBuffers: ArrayBuffer[] = []
  let fontBufferByteLength = 0
  ;(
    await Promise.all(_fonts.map((font) => fetchFont(textByFont[font], font)))
  ).forEach((fontData, i) => {
    if (fontData) {
      // TODO: We should be able to directly get the language code here :)
      const langCode = Object.entries(languageFontMap).find(
        ([, v]) => v === _fonts[i]
      )?.[0]

      if (langCode) {
        const buffer = encodeFontInfoAsArrayBuffer(langCode, fontData)
        encodedFontBuffers.push(buffer)
        fontBufferByteLength += buffer.byteLength
      }
    }
  })

  const responseBuffer = new ArrayBuffer(fontBufferByteLength)
  const responseBufferView = new Uint8Array(responseBuffer)
  let offset = 0
  encodedFontBuffers.forEach((buffer) => {
    responseBufferView.set(new Uint8Array(buffer), offset)
    offset += buffer.byteLength
  })

  return new Response(responseBuffer, {
    headers: {
      'Content-Type': 'font/woff',
      'Cache-Control': 'public, max-age=31536000, immutable',
    },
  })
}

async function fetchFont(
  text: string,
  font: string
): Promise<ArrayBuffer | null> {
  const API = `https://fonts.googleapis.com/css2?family=${font}&text=${encodeURIComponent(
    text
  )}`

  const css = await (
    await fetch(API, {
      headers: {
        // Make sure it returns TTF.
        'User-Agent':
          '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',
      },
    })
  ).text()

  const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/)

  if (!resource) return null

  const res = await fetch(resource[1])

  return res.arrayBuffer()
}


================================================
FILE: playground/pages/index.tsx
================================================
import React from 'react'
import satori from 'satori'
import { LiveProvider, LiveContext, withLive } from 'react-live'
import { useEffect, useState, useRef, useContext, useCallback } from 'react'
import { createPortal } from 'react-dom'
import Editor, { useMonaco } from '@monaco-editor/react'
import toast, { Toaster } from 'react-hot-toast'
import copy from 'copy-to-clipboard'
import packageJson from 'satori/package.json'
import * as fflate from 'fflate'
import { Base64 } from 'js-base64'
import PDFDocument from 'pdfkit/js/pdfkit.standalone'
import SVGtoPDF from 'svg-to-pdfkit'
import blobStream from 'blob-stream'
import { createIntlSegmenterPolyfill } from 'intl-segmenter-polyfill'
import { Panel, PanelGroup } from 'react-resizable-panels'

import { loadEmoji, getIconCode, apis } from '../utils/twemoji'
import Introduction from '../components/introduction'
import PanelResizeHandle from '../components/panel-resize-handle'
import { languageFontMap } from '../utils/font'

import playgroundTabs, { Tabs } from '../cards/playground-data'
import previewTabs from '../cards/preview-tabs'

const cardNames = Object.keys(playgroundTabs)
const editedCards: Tabs = { ...playgroundTabs }

async function init() {
  if (typeof window === 'undefined') return []

  const [font, fontBold, fontIcon, Segmenter] =
    window.__resource ||
    (window.__resource = await Promise.all([
      fetch('/inter-latin-ext-400-normal.woff').then((res) =>
        res.arrayBuffer()
      ),
      fetch('/inter-latin-ext-700-normal.woff').then((res) =>
        res.arrayBuffer()
      ),
      fetch('/material-icons-base-400-normal.woff').then((res) =>
        res.arrayBuffer()
      ),
      !globalThis.Intl || !globalThis.Intl.Segmenter
        ? createIntlSegmenterPolyfill(
            fetch(
              new URL(
                'intl-segmenter-polyfill/dist/break_iterator.wasm',
                import.meta.url
              )
            )
          )
        : null,
    ]))

  if (Segmenter) {
    globalThis.Intl = globalThis.Intl || {}
    //@ts-expect-error
    globalThis.Intl.Segmenter = Segmenter
  }

  return [
    {
      name: 'Inter',
      data: font,
      weight: 400,
      style: 'normal',
    },
    {
      name: 'Inter',
      data: fontBold,
      weight: 700,
      style: 'normal',
    },
    {
      name: 'Material Icons',
      data: fontIcon,
      weight: 400,
      style: 'normal',
    },
  ]
}

function withCache(fn: Function) {
  const cache = new Map()
  return async (...args: string[]) => {
    const key = args.join(':')
    if (cache.has(key)) return cache.get(key)
    const result = await fn(...args)
    cache.set(key, result)
    return result
  }
}

type LanguageCode = keyof typeof languageFontMap | 'emoji'

const loadDynamicAsset = withCache(
  async (emojiType: keyof typeof apis, _code: string, text: string) => {
    if (_code === 'emoji') {
      // It's an emoji, load the image.
      return (
        `data:image/svg+xml;base64,` +
        btoa(await loadEmoji(emojiType, getIconCode(text)))
      )
    }

    const codes = _code.split('|')

    // Try to load from Google Fonts.
    const names = codes
      .map((code) => languageFontMap[code as keyof typeof languageFontMap])
      .filter(Boolean)

    if (names.length === 0) return []

    const params = new URLSearchParams()
    for (const name of names.flat()) {
      params.append('fonts', name)
    }
    params.set('text', text)

    try {
      const response = await fetch(`/api/font?${params.toString()}`)

      if (response.status === 200) {
        const data = await response.arrayBuffer()
        const fonts: any[] = []

        // Decode the encoded font format.
        const decodeFontInfoFromArrayBuffer = (buffer: ArrayBuffer) => {
          let offset = 0
          const bufferView = new Uint8Array(buffer)

          while (offset < bufferView.length) {
            // 1 byte for font name length.
            const languageCodeLength = bufferView[offset]
            offset += 1
            let languageCode = ''
            for (let i = 0; i < languageCodeLength; i++) {
              languageCode += String.fromCharCode(bufferView[offset + i])
            }
            offset += languageCodeLength

            // 4 bytes for font data length.
            const fontDataLength = new DataView(buffer).getUint32(offset, false)
            offset += 4
            const fontData = buffer.slice(offset, offset + fontDataLength)
            offset += fontDataLength

            fonts.push({
              name: `satori_${languageCode}_fallback_${text}`,
              data: fontData,
              weight: 400,
              style: 'normal',
              lang: languageCode === 'unknown' ? undefined : languageCode,
            })
          }
        }

        decodeFontInfoFromArrayBuffer(data)

        return fonts
      }
    } catch (e) {
      console.error('Failed to load dynamic font for', text, '. Error:', e)
    }
  }
)

// https://raw.githubusercontent.com/n3r4zzurr0/svg-spinners/main/svg/90-ring.svg
const spinner = (
  <svg
    width='24'
    height='24'
    viewBox='0 0 24 24'
    xmlns='http://www.w3.org/2000/svg'
    style={{
      position: 'absolute',
      top: 0,
      bottom: 0,
      left: 0,
      right: 0,
      margin: 'auto',
      fill: 'white',
      zIndex: 1,
    }}
  >
    <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'>
      <animateTransform
        attributeName='transform'
        type='rotate'
        dur='0.75s'
        values='0 12 12;360 12 12'
        repeatCount='indefinite'
      />
    </path>
  </svg>
)

function initResvgWorker() {
  if (typeof window === 'undefined') return

  const worker = new Worker(
    new URL('../components/resvg_worker.ts', import.meta.url)
  )

  const pending = new Map()
  worker.onmessage = (e) => {
    const { _id, url } = e.data
    const resolve = pending.get(_id)
    if (resolve) {
      resolve(url)
      pending.delete(_id)
    }
  }

  return async (msg: object) => {
    const _id = Math.random()
    worker.postMessage({
      ...msg,
      _id,
    })
    return new Promise((resolve) => {
      pending.set(_id, resolve)
    })
  }
}

const loadFonts = init()
const renderPNG = initResvgWorker()

interface ITabs {
  options: string[]
  onChange: (value: string) => void
  children: React.ReactNode
}

function Tabs({ options, onChange, children }: ITabs) {
  const [active, setActive] = useState(options[0])

  return (
    <div className='tabs'>
      <div className='tabs-container'>
        {options.map((option) => (
          <div
            title={option}
            className={'tab' + (active === option ? ' active' : '')}
            key={option}
            onClick={() => {
              setActive(option)
              onChange(option)
            }}
          >
            {option}
          </div>
        ))}
      </div>
      {children}
    </div>
  )
}

function LiveEditor({ id }: { id: string }) {
  const { onChange } = useContext(LiveContext) as unknown as {
    onChange: (val: string) => void
  }

  const monaco = useMonaco()
  useEffect(() => {
    if (monaco) {
      monaco.editor.defineTheme('IDLE', {
        base: 'vs',
        inherit: false,
        rules: [
          {
            background: 'FFFFFF',
            token: '',
          },
          {
            token: 'delimiter',
            foreground: '999999',
          },
          {
            token: 'aaa',
            foreground: '00ff00',
          },
          {
            foreground: '919191',
            token: 'comment',
          },
          {
            foreground: '00a33f',
            token: 'string',
          },
          {
            foreground: '3b54bf',
            token: 'number',
          },
          {
            foreground: 'a535ae',
            token: 'constant.language',
          },
          {
            foreground: 'ff5600',
            token: 'keyword',
          },
          {
            foreground: 'ff5600',
            token: 'storage',
          },
          {
            foreground: '21439c',
            token: 'entity.name.type',
          },
          {
            foreground: '21439c',
            token: 'entity.name.function',
          },
          {
            foreground: 'a535ae',
            token: 'support.function',
          },
          {
            foreground: 'a535ae',
            token: 'support.constant',
          },
          {
            foreground: 'a535ae',
            token: 'support.type',
          },
          {
            foreground: 'a535ae',
            token: 'support.class',
          },
          {
            foreground: 'a535ae',
            token: 'support.variable',
          },
          {
            foreground: '000000',
            background: '990000',
            token: 'invalid',
          },
          {
            foreground: '990000',
            token: 'constant.other.placeholder.py',
          },
        ],
        colors: {
          'editor.foreground': '#000000',
          'editor.background': '#FFFFFF',
          'editor.selectionBackground': '#BAD6FD',
          'editor.lineHighlightBackground': '#00000012',
          'editorCursor.foreground': '#000000',
          'editorWhitespace.foreground': '#BFBFBF',
        },
      })
      monaco.editor.setTheme('IDLE')
    }
  }, [monaco])

  const ref = useRef<HTMLDivElement>(null)

  return (
    <div ref={ref} style={{ height: '100%', position: 'relative' }}>
      <div style={{ position: 'absolute' }}>
        <Editor
          height='100%'
          theme='IDLE'
          defaultLanguage='javascript'
          value={editedCards[id]}
          onChange={(newCode) => {
            // We also update the code in memory so switching tabs will preserve the
            // edited code (until refreshing).
            editedCards[id] = newCode ?? ''
            onChange(newCode ?? '')
          }}
          onMount={async (editor, _monaco) => {
            if (ref.current) {
              const relayout = ([e]: any) => {
                editor.layout({
                  width: e.borderBoxSize[0].inlineSize,
                  height: e.borderBoxSize[0].blockSize,
                })
              }
              const resizeObserver = new ResizeObserver(relayout)
              resizeObserver.observe(ref.current)
            }
          }}
          options={{
            fontFamily: 'iaw-mono-var',
            fontSize: 14,
            wordWrap: 'on',
            tabSize: 2,
            minimap: {
              enabled: false,
            },
            smoothScrolling: true,
            cursorSmoothCaretAnimation: 'on',
            contextmenu: false,
            automaticLayout: true,
          }}
        />
      </div>
    </div>
  )
}

// For sharing & resuming.
const currentOptions = {}
let overrideOptions: any = null

const LiveSatori = withLive(function ({
  live,
}: {
  live?: { element: React.ComponentType; error: string }
}) {
  const [options, setOptions] = useState<object | null>(null)
  const [debug, setDebug] = useState(false)
  const [fontEmbed, setFontEmbed] = useState(true)
  const [emojiType, setEmojiType] = useState('twemoji')
  const [objectURL, setObjectURL] = useState<string>('')
  const [renderType, setRenderType] = useState('svg')
  const [renderError, setRenderError] = useState(null)
  const [width, setWidth] = useState(400 * 2)
  const [height, setHeight] = useState(200 * 2)
  const [iframeNode, setIframeNode] = useState<HTMLElement | undefined>()
  const previewContainerRef = useRef<HTMLDivElement>(null)
  const [scaleRatio, setScaleRatio] = useState(1)
  const [loadingResources, setLoadingResources] = useState(true)
  const updateIframeRef = useCallback(
    (node: HTMLIFrameElement) => {
      if (node) {
        if (node.contentWindow?.document) {
          /* Force tailwindcss to create stylesheets on first render */
          const forceUpdate = () => {
            return setTimeout(() => {
              const div = doc.createElement('div')
              div.classList.add('hidden')
              doc.body.appendChild(div)
              setTimeout(() => {
                doc.body.removeChild(div)
              }, 300)
            }, 200)
          }
          const doc = node.contentWindow.document
          const script = doc.createElement('script')
          script.src = 'https://cdn.tailwindcss.com'
          doc.head.appendChild(script)
          script.addEventListener('load', () => {
            const configScript = doc.createElement('script')
            configScript.text = `
            tailwind.config = {
              plugins: [{
                handler({ addBase }) {
                  addBase({
                    'html': {
                      'line-height': 1.2,
                    }
                  })
                }
              }]
            }
          `
            doc.head.appendChild(configScript)
          })
          const updateClass = () => {
            Array.from(doc.querySelectorAll('[tw]')).forEach((v) => {
              const tw = v.getAttribute('tw')
              if (tw) {
                v.setAttribute('class', tw)
                v.removeAttribute('tw')
              }
            })
          }
          forceUpdate()
          const observer = new MutationObserver(updateClass)
          observer.observe(doc.body, { childList: true, subtree: true })
          setIframeNode(doc.body)
        }
      }
    },
    [setIframeNode]
  ) // eslint-disable-line]
  useEffect(() => {
    if (overrideOptions) {
      setWidth(Math.min(overrideOptions.width || 800, 2000))
      setHeight(Math.min(overrideOptions.height || 800, 2000))
      setDebug(!!overrideOptions.debug)
      setEmojiType(overrideOptions.emojiType || 'twemoji')
      setFontEmbed(!!overrideOptions.fontEmbed)
    }
  }, [overrideOptions])

  const sizeRef = useRef([width, height])
  sizeRef.current = [width, height]

  function updateScaleRatio() {
    if (!previewContainerRef.current) return

    const [w, h] = sizeRef.current
    const containerWidth = previewContainerRef.current.clientWidth
    const containerHeight = previewContainerRef.current.clientHeight
    setScaleRatio(
      Math.min(1, Math.min(containerWidth / w, containerHeight / h))
    )
  }

  useEffect(() => {
    ;(async () => {
      setOptions({
        fonts: await loadFonts,
      })
      setLoadingResources(false)
    })()
  }, [])

  useEffect(() => {
    if (!previewContainerRef.current) return

    const observer = new ResizeObserver(updateScaleRatio)
    observer.observe(previewContainerRef.current)

    return () => {
      observer.disconnect()
    }
  }, [])

  useEffect(() => {
    updateScaleRatio()
  }, [width, height])

  const [result, setResult] = useState('')
  const [renderedTimeSpent, setRenderTime] = useState<number>(0)

  useEffect(() => {
    let cancelled = false

    ;(async () => {
      // We leave a small buffer here to debounce if it's PNG.
      if (renderType === 'png') {
        await new Promise((resolve) => setTimeout(resolve, 15))
      }
      if (cancelled) return

      let _result = ''
      let _renderedTimeSpent = 0

      if (live?.element && options) {
        const start = (
          typeof performance !== 'undefined' ? performance : Date
        ).now()
        if (renderType !== 'html') {
          try {
            _result = await satori(live.element.prototype.render(), {
              ...options,
              embedFont: fontEmbed,
              width,
              height,
              debug,
              loadAdditionalAsset: (code: string, text: string) =>
                loadDynamicAsset(emojiType, code, text),
            })
            if (renderType === 'png') {
              const url = (await renderPNG?.({
                svg: _result,
                width,
              })) as string

              if (!cancelled) {
                setObjectURL(url)

                // After rendering the PNG @1x quickly, we render the PNG @2x for
                // the playground only to make it look less blurry.
                // We only do that for images that are not too big (1200^2).
                if (width * height <= 1440000) {
                  setTimeout(async () => {
                    if (cancelled) return
                    const _url = (await renderPNG?.({
                      svg: _result,
                      width: width * 2,
                    })) as string

                    if (cancelled) return
                    setObjectURL(_url)
                  }, 20)
                }
              }
            }
            if (renderType === 'pdf') {
              const doc = new PDFDocument({
                compress: false,
                size: [width, height],
              })
              SVGtoPDF(doc, _result, 0, 0, {
                width,
                height,
                preserveAspectRatio: `xMidYMid meet`,
              })
              const stream = doc.pipe(blobStream())
              stream.on('finish', () => {
                const blob = stream.toBlob('application/pdf')
                setObjectURL(URL.createObjectURL(blob))
              })
              doc.end()
            }
            setRenderError(null)
          } catch (e: any) {
            console.error(e)
            setRenderError(e.message)
            return null
          }
        } else {
          setRenderError(null)
        }
        _renderedTimeSpent =
          (typeof performance !== 'undefined' ? performance : Date).now() -
          start
      }

      Object.assign(currentOptions, {
        width,
        height,
        debug,
        emojiType,
        fontEmbed,
      })
      setResult(_result)
      setRenderTime(_renderedTimeSpent)
    })()

    return () => {
      cancelled = true
    }
  }, [
    live?.element,
    options,
    width,
    height,
    debug,
    emojiType,
    fontEmbed,
    renderType,
  ])

  return (
    <>
      <Panel>
        <Tabs
          options={previewTabs}
          onChange={(text) => {
            const _renderType = text.split(' ')[0].toLowerCase()
            // 'svg' | 'png' | 'html' | 'pdf'
            setRenderType(_renderType)
          }}
        >
          <div className='preview-card'>
            {live?.error || renderError ? (
              <div className='error'>
                <pre>{live?.error || renderError}</pre>
              </div>
            ) : null}
            {loadingResources ? spinner : null}
            <div
              className='result-container'
              ref={previewContainerRef}
              dangerouslySetInnerHTML={
                renderType !== 'svg'
                  ? undefined
                  : {
                      __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>`,
                    }
              }
            >
              {renderType === 'html' ? (
                <iframe
                  key='html'
                  ref={updateIframeRef}
                  width={width}
                  height={height}
                  style={{
                    transform: `scale(${scaleRatio})`,
                  }}
                >
                  {iframeNode &&
                    createPortal(
                      <>
                        <style
                          dangerouslySetInnerHTML={{
                            __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}`,
                          }}
                        />
                        {live?.element ? <live.element /> : null}
                      </>,
                      iframeNode
                    )}
                </iframe>
              ) : renderType === 'png' && objectURL ? (
                <img
                  src={objectURL}
                  width={width}
                  height={height}
                  style={{
                    maxHeight: '100%',
                    maxWidth: '100%',
                    objectFit: 'contain',
                  }}
                  alt='Preview'
                />
              ) : renderType === 'pdf' && objectURL ? (
                <iframe
                  key='pdf'
                  width={width}
                  height={height}
                  src={
                    objectURL +
                    '#toolbar=0&navpanes=0&scrollbar=0&statusbar=0&messages=0&scrollbar=0'
                  }
                  style={{
                    transform: `scale(${scaleRatio})`,
                  }}
                />
              ) : null}
            </div>
            <footer>
              <span className='ellipsis'>
                {renderType === 'html'
                  ? '[HTML] Rendered.'
                  : `[${renderType.toUpperCase()}] Generated in `}
              </span>
              <span className='data'>
                {renderType === 'html'
                  ? ''
                  : `${~~(renderedTimeSpent * 100) / 100}ms.`}
                {renderType === 'pdf' || renderType === 'png' ? (
                  <>
                    {' '}
                    <a href={objectURL ?? ''} target='_blank' rel='noreferrer'>
                      (View in New Tab ↗)
                    </a>
                  </>
                ) : (
                  ''
                )}
              </span>
              <span>{`[${width}×${height}]`}</span>
            </footer>
          </div>
        </Tabs>
      </Panel>
      <PanelResizeHandle />
      <Panel>
        <div className='controller'>
          <h2 className='title'>Configurations</h2>
          <div className='content'>
            <div className='control'>
              <label htmlFor='width'>Container Width</label>
              <div>
                <input
                  type='range'
                  value={width}
                  onChange={(e) => setWidth(Number(e.target.value))}
                  min={100}
                  max={1200}
                  step={1}
                />
                <input
                  id='width'
                  type='number'
                  value={width}
                  onChange={(e) => setWidth(Number(e.target.value))}
                  min={100}
                  max={1200}
                  step={1}
                />
                px
              </div>
            </div>
            <div className='control'>
              <label htmlFor='height'>Container Height</label>
              <div>
                <input
                  type='range'
                  value={height}
                  onChange={(e) => setHeight(Number(e.target.value))}
                  min={100}
                  max={1200}
                  step={1}
                />
                <input
                  id='height'
                  type='number'
                  value={height}
                  onChange={(e) => setHeight(Number(e.target.value))}
                  min={100}
                  max={1200}
                  step={1}
                />
                px
              </div>
            </div>
            <div className='control'>
              <label htmlFor='reset'>Size</label>
              <button
                id='reset'
                onClick={() => {
                  setWidth(800)
                  setHeight(400)
                }}
              >
                Reset
              </button>
              <button
                type='button'
                onClick={() => {
                  setWidth(1200)
                  setHeight(600)
                }}
              >
                2:1
              </button>
              <button
                type='button'
                onClick={() => {
                  setWidth(1200)
                  setHeight(630)
                }}
              >
                1.9:1
              </button>
            </div>
            <div className='control'>
              <label htmlFor='debug'>Debug Mode</label>
              <input
                id='debug'
                type='checkbox'
                checked={debug}
                onChange={() => setDebug(!debug)}
              />
            </div>
            <div className='control'>
              <label htmlFor='font'>Embed Font</label>
              <input
                id='font'
                type='checkbox'
                checked={fontEmbed}
                onChange={() => setFontEmbed(!fontEmbed)}
              />
            </div>
            <div className='control'>
              <label htmlFor='emoji'>Emoji Provider</label>
              <select
                id='emoji'
                onChange={(e) => setEmojiType(e.target.value)}
                value={emojiType}
              >
                <option value='twemoji'>Twemoji</option>
                <option value='fluent'>Fluent Emoji</option>
                <option value='fluentFlat'>Fluent Emoji Flat</option>
                <option value='noto'>Noto Emoji</option>
                <option value='blobmoji'>Blobmoji</option>
                <option value='openmoji'>OpenMoji</option>
              </select>
            </div>
            <div className='control'>
              <label htmlFor='export'>Export</label>
              <a
                className={!result || renderType === 'html' ? 'disabled' : ''}
                href={
                  result
                    ? `data:image/svg+xml;charset=utf-8,${encodeURIComponent(
                        result
                      )}`
                    : undefined
                }
                target={result ? '_blank' : ''}
                download={result ? 'image.svg' : false}
                rel='noreferrer'
              >
                Export SVG
              </a>
              <a
                className={!result || renderType === 'html' ? 'disabled' : ''}
                href='#'
                onClick={(e) => {
                  e.preventDefault()
                  if (!result) return false
                  window.open?.('')?.document.write(result)
                }}
              >
                (View in New Tab ↗)
              </a>
            </div>
            <div className='control'>
              <label>Satori Version</label>
              <a
                href='https://github.com/vercel/satori'
                target='_blank'
                rel='noreferrer'
              >
                {packageJson.version}
              </a>
            </div>
          </div>
        </div>
      </Panel>
    </>
  )
})

function ResetCode({ activeCard }: { activeCard: string }) {
  const { onChange } = useContext(LiveContext) as unknown as {
    onChange: (val: string) => void
  }

  useEffect(() => {
    const params = new URL(String(document.location)).searchParams
    const shared = params.get('share')
    // we just need change editedCards on mounted
    if (shared) {
      try {
        const decompressedData = fflate.strFromU8(
          fflate.decompressSync(Base64.toUint8Array(shared))
        )
        let card
        let tab
        try {
          const decoded = JSON.parse(decompressedData)
          card = decoded.code
          overrideOptions = decoded.options
          tab = decoded.tab || 'helloworld'
        } catch (e) {
          card = decompressedData
        }

        editedCards[tab] = card
        onChange(editedCards[tab])
      } catch (e) {
        console.error('Failed to parse shared card:', e)
      }
    }
  }, [])

  return (
    <button
      onClick={() => {
        editedCards[activeCard] = playgroundTabs[activeCard]
        onChange(editedCards[activeCard])
        window.history.replaceState(null, '', '/')
        toast.success('Content reset')
      }}
    >
      Reset
    </button>
  )
}

export default function Playground() {
  const [activeCard, setActiveCard] = useState<string>('helloworld')
  const [showIntroduction, setShowIntroduction] = useState(false)
  const [isMobileView, setIsMobileView] = useState(false)

  // set isMobileView to true if the screen is less than 600px wide
  useEffect(() => {
    const handleResize = () => {
      setIsMobileView(window.innerWidth < 600)
    }
    handleResize()
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  useEffect(() => {
    try {
      const hasVisited = localStorage.getItem('_vercel_og_playground_visited')
      if (hasVisited) return
    } catch (e) {
      console.error(e)
    }

    setShowIntroduction(true)
  }, [])

  const [hydrated, setHydrated] = useState(false)
  useEffect(() => {
    setHydrated(true)
  }, [])

  const editorPanel = (
    <Panel>
      <Tabs
        options={cardNames}
        onChange={(name: string) => {
          setActiveCard(name)
        }}
      >
        <div className='editor'>
          <div className='editor-controls'>
            <ResetCode activeCard={activeCard} />
            <button
              onClick={() => {
                const code = editedCards[activeCard]
                const compressed = Base64.fromUint8Array(
                  fflate.deflateSync(
                    fflate.strToU8(
                      JSON.stringify({
                        code,
                        options: currentOptions,
                        tab: activeCard,
                      })
                    )
                  ),
                  true
                )

                window.history.replaceState(null, '', '?share=' + compressed)
                copy(window.location.href)
                toast.success('Copied to clipboard')
              }}
            >
              Share
            </button>
          </div>
          <div className='monaco-container'>
            <LiveEditor key={activeCard} id={activeCard} />
          </div>
        </div>
      </Tabs>
    </Panel>
  )

  const previewPanel = (
    <Panel>
      <PanelGroup direction='vertical'>
        <LiveSatori />
      </PanelGroup>
    </Panel>
  )

  return (
    <>
      {showIntroduction ? (
        <Introduction
          onClose={() => {
            setShowIntroduction(false)
            localStorage.setItem('_vercel_og_playground_visited', '1')
          }}
        />
      ) : null}
      <Toaster
        toastOptions={{
          style: {
            fontSize: 13,
            borderRadius: 6,
            padding: '2px 4px 2px 12px',
          },
        }}
      />
      <nav>
        <h1>
          <svg viewBox='0 0 75 65' fill='#000' height='12'>
            <title>Vercel</title>
            <path d='M37.59.25l36.95 64H.64l36.95-64z'></path>
          </svg>
          OG Image Playground
        </h1>
        <ul>
          <li>
            <a href='https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation'>
              Docs
            </a>
          </li>
          <li>
            <a href='https://nextjs.org/discord'>Discord</a>
          </li>
          <li>
            <a href='https://github.com/vercel/satori'>GitHub</a>
          </li>
        </ul>
      </nav>
      <div className='container'>
        <LiveProvider code={editedCards[activeCard]}>
          {hydrated ? (
            <PanelGroup
              autoSaveId='og-playground'
              direction={isMobileView ? 'vertical' : 'horizontal'}
            >
              {isMobileView ? previewPanel : editorPanel}
              <PanelResizeHandle />
              {isMobileView ? editorPanel : previewPanel}
            </PanelGroup>
          ) : null}
        </LiveProvider>
      </div>
    </>
  )
}


================================================
FILE: playground/styles.css
================================================
/* Fonts for the demo */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  src: url(/inter-latin-ext-700-normal.woff) format('woff2');
}

/* UI */

@font-face {
  font-family: 'iaw-mono-var';
  font-weight: 100 700;
  font-style: normal;
  font-named-instance: 'Regular';
  font-display: block;
  src: url('/iaw-mono-var.woff2') format('woff2');
}

:root {
  --font: iaw-mono-var, SF Mono, SFMono-Regular, ui-monospace, Menlo, Consolas,
    monospace;
}

* {
  box-sizing: border-box;
}

html,
body,
#__next {
  height: 100%;
}

body {
  font-family: var(--font);
  font-variant: common-ligatures contextual;
  letter-spacing: -0.015em;
  margin: 0;
  background: fixed 0 0 /20px 20px radial-gradient(#d1d1d1 1px, transparent 0),
    fixed 10px 10px /20px 20px radial-gradient(#d1d1d1 1px, transparent 0);
  --border: 1px solid #d7d7d7;
  --border-inactive: 1px solid #e4e4e4;
}

nav {
  display: flex;
  position: sticky;
  top: 0;
  height: 40px;
  align-items: center;
  padding: 0 15px;
  background: white;
  box-shadow: 0 0 24px rgb(0 0 0 / 10%), 0 0 0 1px rgb(0 0 0 / 4%);
  font-size: 0.9rem;
  z-index: 2;
}

h1 {
  flex: 1;
  margin: 0;
  font-size: 0.9rem;
  font-weight: 500;
  color: #444;
  display: flex;
  align-items: center;
  gap: 0.3rem;
  letter-spacing: -0.015rem;
  word-spacing: -0.09rem;
  cursor: default;
}

nav ul {
  display: flex;
  list-style: none;
  margin: 0;
  gap: 10px;
  padding: 0;
}

a {
  color: inherit;
}

iframe {
  position: absolute;
  border: none;
  appearance: none;
}

textarea,
pre,
code {
  font-family: var(--font) !important;
}
.editor {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: white;
  border-radius: 12px;
  border-top-left-radius: 0;
  border: var(--border);
  overflow: auto;
}
.editor .editor-controls {
  padding: 4px 8px;
  gap: 8px;
  display: flex;
  justify-content: flex-end;
}
.editor .editor-controls button {
  font-family: var(--font);
  font-size: 12px;
  user-select: none;
  padding: 0px 4px;
  height: 20px;
}
.editor .monaco-container {
  flex: 1;
}

.container {
  display: flex;
  width: 100%;
  height: calc(100% - 40px);
  margin: 0;
  padding: 10px;
  overflow: auto;
  gap: 10px;
}

.content-wrapper svg {
  width: 100%;
  height: 100%;
}

.tabs {
  display: flex;
  flex-direction: column;
  height: 100%;
  width: 100%;
}

.container > .tabs {
  height: 100%;
  display: flex;
  flex-direction: column;
  flex: 1 1 50%;
  max-width: 50%;
}

.preview {
  flex: 1 1 50%;
  max-width: calc(50% - 5px);
  align-self: flex-start;
}

.result-container {
  position: relative;
  display: flex;
  height: 100%;
  width: 100%;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.preview-card {
  flex: 1;
  position: relative;
  border-radius: 12px;
  border-top-left-radius: 0;
  background: #111;
  border: var(--border);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.preview-card footer {
  font-size: 12px;
  display: flex;
  align-items: center;
  padding: 3px 10px 4px;
  height: 24px;
  color: #444;
  border-top: 1px solid rgb(0 0 0 / 8%);
  background: #fafafa;
  white-space: nowrap;
}
.preview-card footer .ellipsis {
  white-space: pre;
  overflow: hidden;
  text-overflow: ellipsis;
}
.preview-card footer .data {
  flex: 1;
}

.error {
  position: absolute;
  width: 100%;
  height: calc(100% - 24px);
  padding: 10px 20px;
  overflow: auto;
  color: #ff3737;
  font-size: 13px;
  z-index: 1;
  background: white;
}
.error pre {
  margin: 0;
  white-space: pre-wrap;
}

.preview {
  height: 100%;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.tabs-container {
  display: inline-flex;
  position: relative;
  font-size: 12px;
  margin-bottom: -1px;
  letter-spacing: -0.02em;
  z-index: 1;
  user-select: none;
  white-space: nowrap;
  max-width: calc(100% - 15px);
}

.tab {
  color: #a5a5a5;
  background: #efefef;
  height: 22px;
  border-top-left-radius: 6px;
  border-top-right-radius: 6px;
  border: var(--border-inactive);
  border-bottom: var(--border);
  padding: 2px 10px 4px;
  cursor: default;
  text-overflow: ellipsis;
  overflow: hidden;
}
.tab:hover {
  background: #fafafa;
}

.tab.active {
  color: #111;
  font-weight: 600;
  border: var(--border);
  border-bottom: none;
  background: white;
}

.controller {
  flex: 1;
  height: 100%;
  border-radius: 12px;
  background: white;
  border: var(--border);
  overflow: hidden;
}

.controller h2.title {
  font-size: 12px;
  letter-spacing: -0.02em;
  font-weight: 500;
  color: #444;
  margin: 0;
  padding: 8px 10px;
  text-transform: uppercase;
  background: #fafafa;
  border-bottom: 1px solid rgb(0 0 0 / 8%);
  user-select: none;
}
.controller .content {
  display: flex;
  padding: 8px 10px;
  height: calc(100% - 34px);
  overflow: auto;
  flex-direction: column;
}
.controller .control {
  font-size: 12px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.controller .control > div {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px;
}
.controller .control:not(:last-child) {
  padding-bottom: 8px;
  margin-bottom: 8px;
  border-bottom: 1px solid rgb(0 0 0 / 8%);
}
.controller .control label {
  flex: 0 0 140px;
  user-select: none;
}
.controller input {
  font-family: var(--font);
  outline: none;
}
.controller input[type='number'] {
  appearance: none;
  border: 1px solid rgb(0 0 0 / 20%);
  border-radius: 4px;
}
.controller input[type='number']:hover {
  border: 1px solid rgb(0 0 0 / 30%);
}
.controller input[type='number']:focus {
  border: 1px solid rgb(0 0 0 / 40%);
}
.controller a.disabled {
  color: #a5a5a5;
  cursor: not-allowed;
}
.controller .copyright {
  flex: 1;
}

@media screen and (max-width: 999px) {
  .container {
    flex-direction: column-reverse;
  }
  .preview {
    height: unset;
    width: 100%;
    max-height: calc((50vw - 15px) * 9 / 16 + 44px);
    max-width: 100%;
    flex-direction: row;
  }
  .container > .tabs {
    max-width: unset;
    max-height: calc(100% - (50vw - 15px) * 9 / 16 - 54px);
  }
  .preview > .tabs {
    width: calc(50% - 5px);
  }
  .controller .control label {
    flex: 0 0 120px;
  }
}

@media screen and (max-width: 599px) {
  .preview > .tabs,
  .controller {
    width: 100%;
  }
  .container {
    gap: 0;
  }
  .preview {
    flex-direction: column;
    max-height: calc((100vw - 20px) * 9 / 16 + 214px - 5px);
    padding-bottom: 10px;
  }
  .container > .tabs {
    max-height: calc(100% - (100vw - 20px) * 9 / 16 - 214px + 5px);
  }
}


================================================
FILE: playground/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es2015",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "incremental": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "include": ["decs.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}


================================================
FILE: playground/utils/font.ts
================================================
type UnicodeRange = Array<number | number[]>

export class FontDetector {
  private rangesByLang: {
    [font: string]: UnicodeRange
  } = {}

  public async detect(
    text: string,
    fonts: string[]
  ): Promise<{
    [lang: string]: string
  }> {
    await this.load(fonts)

    const result: {
      [lang: string]: string
    } = {}

    for (const segment of text) {
      const lang = this.detectSegment(segment, fonts)
      if (lang) {
        result[lang] = result[lang] || ''
        result[lang] += segment
      }
    }

    return result
  }

  private detectSegment(segment: string, fonts: string[]): string | null {
    for (const font of fonts) {
      const range = this.rangesByLang[font]
      if (range && checkSegmentInRange(segment, range)) {
        return font
      }
    }

    return null
  }

  private async load(fonts: string[]): Promise<void> {
    let params = ''

    const existingLang = Object.keys(this.rangesByLang)
    const langNeedsToLoad = fonts.filter((font) => !existingLang.includes(font))

    if (langNeedsToLoad.length === 0) {
      return
    }

    for (const font of langNeedsToLoad) {
      params += `family=${font}&`
    }
    params += 'display=swap'

    const API = `https://fonts.googleapis.com/css2?${params}`

    const fontFace = await (
      await fetch(API, {
        headers: {
          // Make sure it returns TTF.
          'User-Agent':
            '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',
        },
      })
    ).text()

    this.addDetectors(fontFace)
  }

  private addDetectors(input: string) {
    const regex = /font-family:\s*'(.+?)';.+?unicode-range:\s*(.+?);/gms
    const matches = input.matchAll(regex)

    for (const [, _lang, range] of matches) {
      const lang = _lang.replaceAll(' ', '+')
      if (!this.rangesByLang[lang]) {
        this.rangesByLang[lang] = []
      }

      this.rangesByLang[lang].push(...convert(range))
    }
  }
}

function convert(input: string): UnicodeRange {
  return input.split(', ').map((range) => {
    range = range.replaceAll('U+', '')
    const [start, end] = range.split('-').map((hex) => parseInt(hex, 16))

    if (isNaN(end)) {
      return start
    }

    return [start, end]
  })
}

function checkSegmentInRange(segment: string, range: UnicodeRange): boolean {
  const codePoint = segment.codePointAt(0)

  if (!codePoint) return false

  return range.some((val) => {
    if (typeof val === 'number') {
      return codePoint === val
    } else {
      const [start, end] = val
      return start <= codePoint && codePoint <= end
    }
  })
}

// @TODO: Support font style and weights, and make this option extensible rather
// than built-in.
// @TODO: Cover most languages with Noto Sans.
export const languageFontMap = {
  'ja-JP': 'Noto+Sans+JP',
  'ko-KR': 'Noto+Sans+KR',
  'zh-CN': 'Noto+Sans+SC',
  'zh-TW': 'Noto+Sans+TC',
  'zh-HK': 'Noto+Sans+HK',
  'th-TH': 'Noto+Sans+Thai',
  'bn-IN': 'Noto+Sans+Bengali',
  'ar-AR': 'Noto+Sans+Arabic',
  'ta-IN': 'Noto+Sans+Tamil',
  'ml-IN': 'Noto+Sans+Malayalam',
  'he-IL': 'Noto+Sans+Hebrew',
  'te-IN': 'Noto+Sans+Telugu',
  devanagari: 'Noto+Sans+Devanagari',
  kannada: 'Noto+Sans+Kannada',
  symbol: ['Noto+Sans+Symbols', 'Noto+Sans+Symbols+2'],
  math: 'Noto+Sans+Math',
  unknown: 'Noto+Sans',
}


================================================
FILE: playground/utils/twemoji.ts
================================================
/**
 * Modified version of https://unpkg.com/twemoji@13.1.0/dist/twemoji.esm.js.
 */

/*! Copyright Twitter Inc. and other contributors. Licensed under MIT */

const U200D = String.fromCharCode(8205)
const UFE0Fg = /\uFE0F/g

export function getIconCode(char: string) {
  return toCodePoint(char.indexOf(U200D) < 0 ? char.replace(UFE0Fg, '') : char)
}

function toCodePoint(unicodeSurrogates: string) {
  const r = []
  let c = 0,
    p = 0,
    i = 0

  while (i < unicodeSurrogates.length) {
    c = unicodeSurrogates.charCodeAt(i++)
    if (p) {
      r.push((65536 + ((p - 55296) << 10) + (c - 56320)).toString(16))
      p = 0
    } else if (55296 <= c && c <= 56319) {
      p = c
    } else {
      r.push(c.toString(16))
    }
  }
  return r.join('-')
}

export const apis = {
  twemoji: (code: string) =>
    'https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/' +
    code.toLowerCase() +
    '.svg',
  openmoji: 'https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/',
  blobmoji: 'https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/',
  noto: 'https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/',
  fluent: (code: string) =>
    'https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/' +
    code.toLowerCase() +
    '_color.svg',
  fluentFlat: (code: string) =>
    'https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/' +
    code.toLowerCase() +
    '_flat.svg',
}

const emojiCache: Record<string, Promise<any>> = {}

export function loadEmoji(type: keyof typeof apis, code: string) {
  const key = type + ':' + code
  if (key in emojiCache) return emojiCache[key]

  if (!type || !apis[type]) {
    type = 'twemoji'
  }

  const api = apis[type]
  if (typeof api === 'function') {
    return (emojiCache[key] = fetch(api(code)).then((r) => r.text()))
  }
  return (emojiCache[key] = fetch(`${api}${code.toUpperCase()}.svg`).then((r) =>
    r.text()
  ))
}


================================================
FILE: pnpm-workspace.yaml
================================================
packages:
  - 'playground'
  - '.'


================================================
FILE: release.config.cjs
================================================
module.exports = {
  branches: ['main'],
  tagFormat: '${version}',
}


================================================
FILE: src/builder/background-image.ts
================================================
import CssDimension from '../vendor/parse-css-dimension/index.js'
import { buildXMLString } from '../utils.js'

import { resolveImageData } from '../handler/image.js'
import { buildLinearGradient } from './gradient/linear.js'
import { buildRadialGradient } from './gradient/radial.js'
import cssColorParse from 'parse-css-color'

interface Background {
  attachment?: string
  color?: string
  clip: string
  image: string
  origin?: string
  position: string
  size: string
  repeat: string
}

function toAbsoluteValue(v: string | number, base: number) {
  if (typeof v === 'string' && v.endsWith('%')) {
    return (base * parseFloat(v)) / 100
  }
  return +v
}

function calculateKeywordSize(
  keyword: string,
  containerWidth: number,
  containerHeight: number,
  imageWidth: number,
  imageHeight: number
): [number, number] {
  if (!imageWidth || !imageHeight) {
    return [containerWidth, containerHeight]
  }

  if (keyword === 'cover') {
    // Scale to cover the container (use max scale to ensure it covers)
    const scaleX = containerWidth / imageWidth
    const scaleY = containerHeight / imageHeight
    const scale = Math.max(scaleX, scaleY)
    return [imageWidth * scale, imageHeight * scale]
  }

  if (keyword === 'contain') {
    // Scale to fit within the container (use min scale to ensure it fits)
    const scaleX = containerWidth / imageWidth
    const scaleY = containerHeight / imageHeight
    const scale = Math.min(scaleX, scaleY)
    return [imageWidth * scale, imageHeight * scale]
  }

  // For 'auto' or other values, handle auto
  if (keyword === 'auto' || keyword.includes('auto')) {
    const parts = keyword.split(' ')
    const widthPart = parts[0] || 'auto'
    const heightPart = parts[1] || parts[0] || 'auto'

    let finalWidth = imageWidth
    let finalHeight = imageHeight

    if (widthPart === 'auto' && heightPart !== 'auto') {
      // Width is auto, height is specified
      const parsedHeight = toAbsoluteValue(heightPart, containerHeight)
      finalHeight = parsedHeight
      finalWidth = (imageWidth / imageHeight) * parsedHeight
    } else if (heightPart === 'auto' && widthPart !== 'auto') {
      // Height is auto, width is specified
      const parsedWidth = toAbsoluteValue(widthPart, containerWidth)
      finalWidth = parsedWidth
      finalHeight = (imageHeight / imageWidth) * parsedWidth
    }
    // If both are auto, use intrinsic dimensions

    return [finalWidth, finalHeight]
  }

  return [containerWidth, containerHeight]
}

function parseLengthPairs(
  str: string,
  {
    x,
    y,
    defaultX,
    defaultY,
  }: {
    x: number
    y: number
    defaultX: number | string
    defaultY: number | string
  }
) {
  return (
    str
      ? str
          .split(' ')
          .map((value) => {
            try {
              const parsed = new CssDimension(value)
              return parsed.type === 'length' || parsed.type === 'number'
                ? parsed.value
                : parsed.value + parsed.unit
            } catch (e) {
              return null
            }
          })
          .filter((v) => v !== null)
      : [defaultX, defaultY]
  ).map((v, index) => toAbsoluteValue(v, [x, y][index]))
}

export default async function backgroundImage(
  {
    id,
    width,
    height,
    left,
    top,
  }: { id: string; width: number; height: number; left: number; top: number },
  { image, size, position, repeat }: Background,
  inheritableStyle: Record<string, number | string>,
  from?: 'background' | 'mask'
): Promise<string[]> {
  // Default to `repeat`.
  repeat = repeat || 'repeat'
  from = from || 'background'

  const repeatX = repeat === 'repeat-x' || repeat === 'repeat'
  const repeatY = repeat === 'repeat-y' || repeat === 'repeat'

  // Check if size is a keyword (cover, contain, auto) that needs to be calculated later
  const isKeywordSize =
    size &&
    (size === 'cover' ||
      size === 'contain' ||
      size === 'auto' ||
      size.includes('auto'))

  // For gradients, keyword sizes (cover, contain, auto) resolve to the
  // container dimensions since gradients have no intrinsic size.
  // For url() images, keyword sizes are calculated later using the image's
  // intrinsic dimensions.
  const isGradient =
    image.startsWith('linear-gradient(') ||
    image.startsWith('repeating-linear-gradient(') ||
    image.startsWith('radial-gradient(') ||
    image.startsWith('repeating-radial-gradient(')

  const dimensions =
    isKeywordSize && isGradient
      ? [width, height] // Gradients have no intrinsic size; keyword sizes resolve to container
      : isKeywordSize
      ? [0, 0] // Will be calculated later when we have image dimensions
      : parseLengthPairs(size, {
          x: width,
          y: height,
          defaultX: width,
          defaultY: height,
        })
  const offsets = parseLengthPairs(position, {
    x: width,
    y: height,
    defaultX: 0,
    defaultY: 0,
  })

  if (
    image.startsWith('linear-gradient(') ||
    image.startsWith('repeating-linear-gradient(')
  ) {
    return buildLinearGradient(
      { id, width, height, repeatX, repeatY },
      image,
      dimensions,
      offsets,
      inheritableStyle,
      from
    )
  }

  if (
    image.startsWith('radial-gradient(') ||
    image.startsWith('repeating-radial-gradient(')
  ) {
    return buildRadialGradient(
      { id, width, height, repeatX, repeatY },
      image,
      dimensions,
      offsets,
      inheritableStyle,
      from
    )
  }

  if (image.startsWith('url(')) {
    const [src, imageWidth, imageHeight] = await resolveImageData(
      image.slice(4, -1)
    )

    let resolvedWidth: number
    let resolvedHeight: number

    if (isKeywordSize) {
      // Calculate dimensions based on keyword (cover, contain, auto)
      const [calcWidth, calcHeight] = calculateKeywordSize(
        size,
        width,
        height,
        imageWidth,
        imageHeight
      )
      resolvedWidth = calcWidth
      resolvedHeight = calcHeight
    } else {
      // Use the previously parsed dimensions
      const dimensionsWithoutFallback = parseLengthPairs(size, {
        x: width,
        y: height,
        defaultX: 0,
        defaultY: 0,
      })
      resolvedWidth =
        from === 'mask'
          ? imageWidth || dimensionsWithoutFallback[0]
          : dimensionsWithoutFallback[0] || imageWidth
      resolvedHeight =
        from === 'mask'
          ? imageHeight || dimensionsWithoutFallback[1]
          : dimensionsWithoutFallback[1] || imageHeight
    }

    return [
      `satori_bi${id}`,
      buildXMLString(
        'pattern',
        {
          id: `satori_bi${id}`,
          patternContentUnits: 'userSpaceOnUse',
          patternUnits: 'userSpaceOnUse',
          x: offsets[0] + left,
          y: offsets[1] + top,
          width: repeatX ? resolvedWidth : '100%',
          height: repeatY ? resolvedHeight : '100%',
        },
        buildXMLString('image', {
          x: 0,
          y: 0,
          width: resolvedWidth,
          height: resolvedHeight,
          preserveAspectRatio: 'none',
          href: src,
        })
      ),
    ]
  }

  if (cssColorParse(image)) {
    const colorObj = cssColorParse(image)
    const [r, g, b, a] = colorObj.values
    const alpha = a !== undefined ? a : 1
    const color = `rgba(${r},${g},${b},${alpha})`

    return [
      `satori_bi${id}`,
      buildXMLString(
        'pattern',
        {
          id: `satori_bi${id}`,
          patternContentUnits: 'userSpaceOnUse',
          patternUnits: 'userSpaceOnUse',
          x: left,
          y: top,
          width: width,
          height: height,
        },
        buildXMLString('rect', {
          x: 0,
          y: 0,
          width: width,
          height: height,
          fill: color,
        })
      ),
    ]
  }

  throw new Error(`Invalid background image: "${image}"`)
}


================================================
FILE: src/builder/border-radius.ts
================================================
/**
 * CSS border radius to SVG path.
 */

// TODO: Support the `border-radius: 10px / 20px` syntax.
// https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius

import { buildXMLString, lengthToNumber } from '../utils.js'

// Getting the intersection of a 45deg ray with the elliptical arc x^2/rx^2 + y^2/ry^2 = 1.
// Reference:
// https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
function svgArcCenterOffset([rx, ry]: number[]) {
  if (Math.round(rx * 1000) === 0 && Math.round(ry * 1000) === 0) {
    return 0
  }
  return Math.round(((rx * ry) / Math.sqrt(rx * rx + ry * ry)) * 1000) / 1000
}

function resolveSize(a: number, b: number, limit: number) {
  if (limit < a + b) {
    if (limit / 2 < a && limit / 2 < b) {
      a = b = limit / 2
    } else if (limit / 2 < a) {
      a = limit - b
    } else if (limit / 2 < b) {
      b = limit - a
    }
  }
  return [a, b]
}

function makeSmaller(arr: [number, number]) {
  arr[0] = arr[1] = Math.min(arr[0], arr[1])
}

// Each corner can have 2 values, the first is the horizontal radius, the second is the vertical radius.
function resolveRadius(
  v: number | string | undefined,
  width: number,
  height: number,
  fontSize: number,
  style: any
): [boolean, undefined | [number, number]] {
  if (typeof v === 'string') {
    const sides = v.split(' ').map((s) => s.trim())
    const singleValue = !sides[1] && !sides[0].endsWith('%')
    sides[1] = sides[1] || sides[0]
    return [
      singleValue,
      [
        Math.min(lengthToNumber(sides[0], fontSize, width, style, true), width),
        Math.min(
          lengthToNumber(sides[1], fontSize, height, style, true),
          height
        ),
      ],
    ]
  }
  if (typeof v === 'number') {
    return [true, [Math.min(v, width), Math.min(v, height)]]
  }
  return [true, undefined]
}

const radiusZeroOrNull = (_radius?: [number, number]) =>
  _radius && _radius[0] !== 0 && _radius[1] !== 0

export function getBorderRadiusClipPath(
  {
    id,
    borderRadiusPath,
    borderType,
    left,
    top,
    width,
    height,
  }: {
    id: string
    borderRadiusPath?: string
    borderType?: 'rect' | 'path'
    left: number
    top: number
    width: number
    height: number
  },
  style: Record<string, number | string>
) {
  const rectClipId = `satori_brc-${id}`
  const defs = buildXMLString(
    'clipPath',
    {
      id: rectClipId,
    },
    buildXMLString(borderType, {
      x: left,
      y: top,
      width,
      height,
      d: borderRadiusPath ? borderRadiusPath : undefined,
    })
  )

  return [defs, rectClipId]
}

export default function radius(
  {
    left,
    top,
    width,
    height,
  }: {
    left: number
    top: number
    width: number
    height: number
  },
  style: Record<string, any>,
  partialSides?: boolean[]
) {
  let {
    borderTopLeftRadius,
    borderTopRightRadius,
    borderBottomLeftRadius,
    borderBottomRightRadius,
    fontSize,
  } = style

  let singleAbsValueTopLeftCorner
  let singleAbsValueTopRightCorner
  let singleAbsValueBottomLeftCorner
  let singleAbsValueBottomRightCorner
  ;[singleAbsValueTopLeftCorner, borderTopLeftRadius] = resolveRadius(
    borderTopLeftRadius,
    width,
    height,
    fontSize,
    style
  )
  ;[singleAbsValueTopRightCorner, borderTopRightRadius] = resolveRadius(
    borderTopRightRadius,
    width,
    height,
    fontSize,
    style
  )
  ;[singleAbsValueBottomLeftCorner, borderBottomLeftRadius] = resolveRadius(
    borderBottomLeftRadius,
    width,
    height,
    fontSize,
    style
  )
  ;[singleAbsValueBottomRightCorner, borderBottomRightRadius] = resolveRadius(
    borderBottomRightRadius,
    width,
    height,
    fontSize,
    style
  )

  if (
    !partialSides &&
    !radiusZeroOrNull(borderTopLeftRadius) &&
    !radiusZeroOrNull(borderTopRightRadius) &&
    !radiusZeroOrNull(borderBottomLeftRadius) &&
    !radiusZeroOrNull(borderBottomRightRadius)
  ) {
    return ''
  }
  borderTopLeftRadius ||= [0, 0]
  borderTopRightRadius ||= [0, 0]
  borderBottomLeftRadius ||= [0, 0]
  borderBottomRightRadius ||= [0, 0]

  // Limit the radius sizes of each edge to make sure they will never overlap.

  // Top
  ;[borderTopLeftRadius[0], borderTopRightRadius[0]] = resolveSize(
    borderTopLeftRadius[0],
    borderTopRightRadius[0],
    width
  )
  // Bottom
  ;[borderBottomLeftRadius[0], borderBottomRightRadius[0]] = resolveSize(
    borderBottomLeftRadius[0],
    borderBottomRightRadius[0],
    width
  )
  // Left
  ;[borderTopLeftRadius[1], borderBottomLeftRadius[1]] = resolveSize(
    borderTopLeftRadius[1],
    borderBottomLeftRadius[1],
    height
  )
  // Right
  ;[borderTopRightRadius[1], borderBottomRightRadius[1]] = resolveSize(
    borderTopRightRadius[1],
    borderBottomRightRadius[1],
    height
  )

  // If the specified border radius is a single value (e.g. 10px or 10em), we take
  // the minimum of the resolved horizontal and vertical radius and apply to both.
  if (singleAbsValueTopLeftCorner) {
    makeSmaller(borderTopLeftRadius)
  }
  if (singleAbsValueTopRightCorner) {
    makeSmaller(borderTopRightRadius)
  }
  if (singleAbsValueBottomLeftCorner) {
    makeSmaller(borderBottomLeftRadius)
  }
  if (singleAbsValueBottomRightCorner) {
    makeSmaller(borderBottomRightRadius)
  }

  type Arc = [[number, number], [number, number]]
  const p: Arc[] = []
  p[0] = [borderTopRightRadius, borderTopRightRadius]
  p[1] = [
    borderBottomRightRadius,
    [-borderBottomRightRadius[0], borderBottomRightRadius[1]],
  ]
  p[2] = [
    borderBottomLeftRadius,
    [-borderBottomLeftRadius[0], -borderBottomLeftRadius[1]],
  ]
  p[3] = [
    borderTopLeftRadius,
    [borderTopLeftRadius[0], -borderTopLeftRadius[1]],
  ]

  const T = `h${width - borderTopLeftRadius[0] - borderTopRightRadius[0]} a${
    p[0][0]
  } 0 0 1 ${p[0][1]}`
  const R = `v${
    height - borderTopRightRadius[1] - borderBottomRightRadius[1]
  } a${p[1][0]} 0 0 1 ${p[1][1]}`
  const B = `h${
    borderBottomRightRadius[0] + borderBottomLeftRadius[0] - width
  } a${p[2][0]} 0 0 1 ${p[2][1]}`
  const L = `v${borderBottomLeftRadius[1] + borderTopLeftRadius[1] - height} a${
    p[3][0]
  } 0 0 1 ${p[3][1]}`

  if (partialSides) {
    // "However it is not defined what these transitions look like or what function maps from this ratio to a point on the curve."
    // https://w3c.github.io/csswg-drafts/css-backgrounds-3/#corner-transitions
    let start = partialSides.indexOf(false)

    if (!partialSides.includes(true)) throw new Error('Invalid `partialSides`.')

    if (start === -1) {
      start = 0
    } else {
      while (!partialSides[start]) {
        start = (start + 1) % 4
      }
    }

    function getArc(i: number) {
      const c0 = svgArcCenterOffset(
        [
          borderTopLeftRadius,
          borderTopRightRadius,
          borderBottomRightRadius,
          borderBottomLeftRadius,
        ][i]
      )
      return i === 0
        ? [
            [
              left + borderTopLeftRadius[0] - c0,
              top + borderTopLeftRadius[1] - c0,
            ],
            [left + borderTopLeftRadius[0], top],
          ]
        : i === 1
        ? [
            [
              left + width - borderTopRightRadius[0] + c0,
              top + borderTopRightRadius[1] - c0,
            ],
            [left + width, top + borderTopRightRadius[1]],
          ]
        : i === 2
        ? [
            [
              left + width - borderBottomRightRadius[0] + c0,
              top + height - borderBottomRightRadius[1] + c0,
            ],
            [left + width - borderBottomRightRadius[0], top + height],
          ]
        : [
            [
              left + borderBottomLeftRadius[0] - c0,
              top + height - borderBottomLeftRadius[1] + c0,
            ],
            [left, top + height - borderBottomLeftRadius[1]],
          ]
    }

    let result = ''

    const arc0 = getArc(start)

    let l = `M${arc0[0]} A${p[(start + 3) % 4][0]} 0 0 1 ${arc0[1]}`

    let len = 0
    for (; len < 4 && partialSides[(start + len) % 4]; len++) {
      result += l + ' '
      l = [T, R, B, L][(start + len) % 4]
    }
    const end = (start + len) % 4

    // For the last segment, we skip the full arc and add the half arc.
    result += l.split(' ')[0]

    const arc1 = getArc(end)
    result += ` A${p[(end + 3) % 4][0]} 0 0 1 ${arc1[0]}`

    return result
  }

  // Generate the path
  return `M${left + borderTopLeftRadius[0]},${top} ${T} ${R} ${B} ${L}`
}


================================================
FILE: src/builder/border.ts
================================================
import { buildXMLString } from '../utils.js'
import radius from './border-radius.js'

function compareBorderDirections(a: string, b: string, style: any) {
  return (
    style[a + 'Width'] === style[b + 'Width'] &&
    style[a + 'Style'] === style[b + 'Style'] &&
    style[a + 'Color'] === style[b + 'Color']
  )
}

export function getBorderClipPath(
  {
    id,
    // Can be `overflow: hidden` from parent containers.
    currentClipPathId,
    borderPath,
    borderType,
    left,
    top,
    width,
    height,
  }: {
    id: string
    currentClipPathId?: string | number
    borderPath?: string
    borderType?: 'rect' | 'path'
    left: number
    top: number
    width: number
    height: number
  },
  style: Record<string, number | string>
) {
  const hasBorder =
    style.borderTopWidth ||
    style.borderRightWidth ||
    style.borderBottomWidth ||
    style.borderLeftWidth

  if (!hasBorder) return null

  // In SVG, stroke is always centered on the path and there is no
  // existing property to make it behave like CSS border. So here we
  // 2x the border width and introduce another clip path to clip the
  // overflowed part.
  const rectClipId = `satori_bc-${id}`
  const defs = buildXMLString(
    'clipPath',
    {
      id: rectClipId,
      'clip-path': currentClipPathId ? `url(#${currentClipPathId})` : undefined,
    },
    buildXMLString(borderType, {
      x: left,
      y: top,
      width,
      height,
      d: borderPath ? borderPath : undefined,
    })
  )

  return [defs, rectClipId]
}

export default function border(
  {
    left,
    top,
    width,
    height,
    props,
    asContentMask,
    maskBorderOnly,
  }: {
    left: number
    top: number
    width: number
    height: number
    props: any
    asContentMask?: boolean
    maskBorderOnly?: boolean
  },
  style: Record<string, number | string>
) {
  const directions = ['borderTop', 'borderRight', 'borderBottom', 'borderLeft']

  // No border
  if (
    !asContentMask &&
    !directions.some((direction) => style[direction + 'Width'])
  )
    return ''

  let fullBorder = ''

  let start = 0
  while (
    start > 0 &&
    compareBorderDirections(
      directions[start],
      directions[(start + 3) % 4],
      style
    )
  ) {
    start = (start + 3) % 4
  }

  let partialSides = [false, false, false, false]
  let currentStyle = []
  for (let _i = 0; _i < 4; _i++) {
    const i = (start + _i) % 4
    const ni = (start + _i + 1) % 4

    const d = directions[i]
    const nd = directions[ni]

    partialSides[i] = true
    currentStyle = [
      style[d + 'Width'],
      style[d + 'Style'],
      style[d + 'Color'],
      d,
    ]

    if (!compareBorderDirections(d, nd, style)) {
      const w =
        (currentStyle[0] || 0) +
        (asContentMask && !maskBorderOnly
          ? style[d.replace('border', 'padding')] || 0
          : 0)
      if (w) {
        fullBorder += buildXMLString('path', {
          width,
          height,
          ...props,
          fill: 'none',
          stroke: asContentMask ? '#000' : currentStyle[2],
          'stroke-width': w * 2,
          'stroke-dasharray':
            !asContentMask && currentStyle[1] === 'dashed'
              ? w * 2 + ' ' + w
              : undefined,
          d: radius(
            { left, top, width, height },
            style as Record<string, number>,
            partialSides
          ),
        })
      }
      partialSides = [false, false, false, false]
    }
  }

  if (partialSides.some(Boolean)) {
    const w =
      (currentStyle[0] || 0) +
      (asContentMask && !maskBorderOnly
        ? style[currentStyle[3].replace('border', 'padding')] || 0
        : 0)
    if (w) {
      fullBorder += buildXMLString('path', {
        width,
        height,
        ...props,
        fill: 'none',
        stroke: asContentMask ? '#000' : currentStyle[2],
        'stroke-width': w * 2,
        'stroke-dasharray':
          !asContentMask && currentStyle[1] === 'dashed'
            ? w * 2 + ' ' + w
            : undefined,
        d: radius(
          { left, top, width, height },
          style as Record<string, number>,
          partialSides
        ),
      })
    }
  }

  return fullBorder
}


================================================
FILE: src/builder/clip-path.ts
================================================
import { buildXMLString } from '../utils.js'
import { createShapeParser } from '../parser/shape.js'

export function genClipPathId(id: string) {
  return `satori_cp-${id}`
}
export function genClipPath(id: string) {
  return `url(#${genClipPathId(id)})`
}

export function buildClipPath(
  v: {
    left: number
    top: number
    width: number
    height: number
    path: string
    matrix: string | undefined
    id: string
    currentClipPath: string | string
    src?: string
  },
  style: Record<string, string | number>,
  inheritedStyle: Record<string, string | number>
) {
  if (style.clipPath === 'none') return ''

  const parser = createShapeParser(v, style, inheritedStyle)
  const clipPath = style.clipPath as string

  let tmp: { type: string; [p: string]: string | number } = { type: '' }

  for (const k of Object.keys(parser)) {
    tmp = parser[k](clipPath)
    if (tmp) break
  }

  if (tmp) {
    const { type, ...rest } = tmp
    return buildXMLString(
      'clipPath',
      {
        id: genClipPathId(v.id),
        'clip-path': v.currentClipPath,
        transform: `translate(${v.left}, ${v.top})`,
      },
      buildXMLString(type, rest)
    )
  }
  return ''
}


================================================
FILE: src/builder/content-mask.ts
================================================
/**
 * When there is border radius, the content area should be clipped by the
 * inner path of border + padding. This applies to <img> element as well as any
 * child element inside a `overflow: hidden` container.
 */

import { buildXMLString } from '../utils.js'
import border from './border.js'

export default function contentMask(
  {
    id,
    left,
    top,
    width,
    height,
    matrix,
    borderOnly,
  }: {
    id: string
    left: number
    top: number
    width: number
    height: number
    matrix: string | undefined
    borderOnly?: boolean
  },
  style: Record<string, number | string>
) {
  const offsetLeft =
    ((style.borderLeftWidth as number) || 0) +
    (borderOnly ? 0 : (style.paddingLeft as number) || 0)
  const offsetTop =
    ((style.borderTopWidth as number) || 0) +
    (borderOnly ? 0 : (style.paddingTop as number) || 0)
  const offsetRight =
    ((style.borderRightWidth as number) || 0) +
    (borderOnly ? 0 : (style.paddingRight as number) || 0)
  const offsetBottom =
    ((style.borderBottomWidth as number) || 0) +
    (borderOnly ? 0 : (style.paddingBottom as number) || 0)

  const contentArea = {
    x: left + offsetLeft,
    y: top + offsetTop,
    width: width - offsetLeft - offsetRight,
    height: height - offsetTop - offsetBottom,
  }

  const _contentMask = buildXMLString(
    'mask',
    { id },
    buildXMLString('rect', {
      ...contentArea,
      fill: '#fff',
      // add transformation matrix to mask if overflow is hidden AND a
      // transformation style is defined, otherwise children will be clipped
      // incorrectly
      transform:
        style.overflow === 'hidden' && style.transform && matrix
          ? matrix
          : undefined,
      mask: style._inheritedMaskId
        ? `url(#${style._inheritedMaskId})`
        : undefined,
    }) +
      border(
        {
          left,
          top,
          width,
          height,
          props: {
            transform: matrix ? matrix : undefined,
          },
          asContentMask: true,
          maskBorderOnly: borderOnly,
        },
        style
      )
  )

  return _contentMask
}


================================================
FILE: src/builder/gradient/linear.ts
================================================
import { parseLinearGradient, ColorStop } from 'css-gradient-parser'
import { normalizeStops } from './utils.js'
import { buildXMLString, calcDegree, lengthToNumber } from '../../utils.js'

export function buildLinearGradient(
  {
    id,
    width,
    height,
    repeatX,
    repeatY,
  }: {
    id: string
    width: number
    height: number
    repeatX: boolean
    repeatY: boolean
  },
  image: string,
  dimensions: number[],
  offsets: number[],
  inheritableStyle: Record<string, number | string>,
  from?: 'background' | 'mask'
) {
  const parsed = parseLinearGradient(image)
  const [imageWidth, imageHeight] = dimensions
  const repeating = image.startsWith('repeating')

  // Calculate the direction.
  let points, length, xys

  if (parsed.orientation.type === 'directional') {
    points = resolveXYFromDirection(parsed.orientation.value)

    length = Math.sqrt(
      Math.pow((points.x2 - points.x1) * imageWidth, 2) +
        Math.pow((points.y2 - points.y1) * imageHeight, 2)
    )
  } else if (parsed.orientation.type === 'angular') {
    const { length: l, ...p } = calcNormalPoint(
      (calcDegree(
        `${parsed.orientation.value.value}${parsed.orientation.value.unit}`
      ) /
        180) *
        Math.PI,
      imageWidth,
      imageHeight
    )

    length = l
    points = p
  }

  xys = repeating
    ? calcPercentage(parsed.stops, length, points, inheritableStyle)
    : points

  const stops = normalizeStops(
    repeating ? resolveRepeatingCycle(parsed.stops, length) : length,
    parsed.stops,
    inheritableStyle,
    repeating,
    from
  )

  const gradientId = `satori_bi${id}`
  const patternId = `satori_pattern_${id}`

  const defs = buildXMLString(
    'pattern',
    {
      id: patternId,
      x: offsets[0] / width,
      y: offsets[1] / height,
      width: repeatX ? imageWidth / width : '1',
      height: repeatY ? imageHeight / height : '1',
      patternUnits: 'objectBoundingBox',
    },
    buildXMLString(
      'linearGradient',
      {
        id: gradientId,
        ...xys,
        spreadMethod: repeating ? 'repeat' : 'pad',
      },
      stops
        .map((stop) =>
          buildXMLString('stop', {
            offset: (stop.offset ?? 0) * 100 + '%',
            'stop-color': stop.color,
          })
        )
        .join('')
    ) +
      buildXMLString('rect', {
        x: 0,
        y: 0,
        width: imageWidth,
        height: imageHeight,
        fill: `url(#${gradientId})`,
      })
  )
  return [patternId, defs]
}

function resolveRepeatingCycle(stops: ColorStop[], length: number) {
  const last = stops[stops.length - 1]
  const { offset } = last
  if (!offset) return length

  if (offset.unit === '%') return (Number(offset.value) / 100) * length

  return Number(offset.value)
}

function resolveXYFromDirection(dir: string) {
  let x1 = 0,
    y1 = 0,
    x2 = 0,
    y2 = 0

  if (dir.includes('top')) {
    y1 = 1
  } else if (dir.includes('bottom')) {
    y2 = 1
  }

  if (dir.includes('left')) {
    x1 = 1
  } else if (dir.includes('right')) {
    x2 = 1
  }

  if (!x1 && !x2 && !y1 && !y2) {
    y1 = 1
  }

  return { x1, y1, x2, y2 }
}

/**
 * calc start point and end point of linear gradient
 */
function calcNormalPoint(v: number, w: number, h: number) {
  const r = Math.pow(h / w, 2)

  // make sure angle is 0 <= angle <= 360
  v = ((v % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2)

  let x1, y1, x2, y2, length, tmp, a, b

  const dfs = (angle: number) => {
    if (angle === 0) {
      x1 = 0
      y1 = h
      x2 = 0
      y2 = 0
      length = h
      return
    } else if (angle === Math.PI / 2) {
      x1 = 0
      y1 = 0
      x2 = w
      y2 = 0
      length = w
      return
    }
    if (angle > 0 && angle < Math.PI / 2) {
      x1 =
        ((r * w) / 2 / Math.tan(angle) - h / 2) /
        (Math.tan(angle) + r / Math.tan(angle))
      y1 = Math.tan(angle) * x1 + h
      x2 = Math.abs(w / 2 - x1) + w / 2
      y2 = h / 2 - Math.abs(y1 - h / 2)
      length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
      // y = -1 / tan * x = h / 2 +1 /
Download .txt
gitextract_raqdfr0j/

├── .eslintrc.json
├── .github/
│   ├── CODEOWNERS
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── config.yml
│   │   └── feature_request.md
│   └── workflows/
│       ├── ci.yml
│       └── pr.yml
├── .gitignore
├── .husky/
│   └── pre-commit
├── .npmrc
├── .prettierignore
├── .prettierrc
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── package.json
├── patches/
│   └── yoga-layout@3.2.1.patch
├── playground/
│   ├── LICENSE
│   ├── cards/
│   │   ├── playground-data.ts
│   │   └── preview-tabs.ts
│   ├── components/
│   │   ├── introduction.module.css
│   │   ├── introduction.tsx
│   │   ├── panel-resize-handle.module.css
│   │   ├── panel-resize-handle.tsx
│   │   └── resvg_worker.ts
│   ├── decs.d.ts
│   ├── index.d.ts
│   ├── next-env.d.ts
│   ├── package.json
│   ├── pages/
│   │   ├── _app.tsx
│   │   ├── _document.tsx
│   │   ├── api/
│   │   │   └── font.ts
│   │   └── index.tsx
│   ├── styles.css
│   ├── tsconfig.json
│   └── utils/
│       ├── font.ts
│       └── twemoji.ts
├── pnpm-workspace.yaml
├── release.config.cjs
├── src/
│   ├── builder/
│   │   ├── background-image.ts
│   │   ├── border-radius.ts
│   │   ├── border.ts
│   │   ├── clip-path.ts
│   │   ├── content-mask.ts
│   │   ├── gradient/
│   │   │   ├── linear.ts
│   │   │   ├── radial.ts
│   │   │   └── utils.ts
│   │   ├── mask-image.ts
│   │   ├── overflow.ts
│   │   ├── rect.ts
│   │   ├── shadow.ts
│   │   ├── svg.ts
│   │   ├── text-decoration.ts
│   │   ├── text.ts
│   │   └── transform.ts
│   ├── font.ts
│   ├── handler/
│   │   ├── compute.ts
│   │   ├── expand.ts
│   │   ├── image.ts
│   │   ├── inheritable.ts
│   │   ├── preprocess.ts
│   │   ├── presets.ts
│   │   ├── tailwind.ts
│   │   └── variables.ts
│   ├── index.ts
│   ├── jsx/
│   │   ├── index.ts
│   │   ├── intrinsic-elements.ts
│   │   ├── jsx-runtime.ts
│   │   └── types.ts
│   ├── language.ts
│   ├── layout.ts
│   ├── parser/
│   │   ├── mask.ts
│   │   └── shape.ts
│   ├── satori.ts
│   ├── text/
│   │   ├── characters.ts
│   │   ├── index.ts
│   │   ├── measurer.ts
│   │   └── processor.ts
│   ├── transform-origin.ts
│   ├── types.d.ts
│   ├── utils.ts
│   ├── vendor/
│   │   ├── parse-css-dimension/
│   │   │   ├── LICENSE
│   │   │   ├── index.js
│   │   │   ├── package.json
│   │   │   └── src.js
│   │   └── twrnc/
│   │       ├── deprecate.js
│   │       ├── log.js
│   │       └── picocolors.js
│   ├── yoga.bundled.ts
│   ├── yoga.external.ts
│   └── yoga.ts
├── test/
│   ├── assets/
│   │   ├── Χαίρετ
│   │   ├── こんにちは
│   │   ├── 你好
│   │   └── 안녕
│   ├── background-clip.test.tsx
│   ├── basic.test.tsx
│   ├── benchmark/
│   │   ├── Geist-Black.otf
│   │   ├── Geist-Bold.otf
│   │   ├── Geist-Medium.otf
│   │   ├── Geist-Regular.otf
│   │   ├── Geist-SemiBold.otf
│   │   └── index.ts
│   ├── border.test.tsx
│   ├── box-sizing.test.tsx
│   ├── clip-path.test.tsx
│   ├── color-models.test.tsx
│   ├── css-variables.test.tsx
│   ├── display-contents.test.tsx
│   ├── display.test.tsx
│   ├── dynamic-size.test.tsx
│   ├── embed-font.test.tsx
│   ├── emoji.test.tsx
│   ├── error.test.tsx
│   ├── event.test.tsx
│   ├── flexbox-advanced.test.tsx
│   ├── font.test.tsx
│   ├── gap.test.tsx
│   ├── gradient.test.tsx
│   ├── image.test.tsx
│   ├── jsx-runtime.test.tsx
│   ├── language.test.tsx
│   ├── layout.test.tsx
│   ├── letter-spacing.test.tsx
│   ├── line-clamp.test.tsx
│   ├── line-height.test.tsx
│   ├── margin.test.tsx
│   ├── mask-image.test.tsx
│   ├── opacity.test.tsx
│   ├── overflow.test.tsx
│   ├── padding.test.tsx
│   ├── pixel-font.test.tsx
│   ├── position.test.tsx
│   ├── react.test.tsx
│   ├── shadow.test.tsx
│   ├── svg.test.tsx
│   ├── tab-size.test.tsx
│   ├── text-align.test.tsx
│   ├── text-decoration.test.tsx
│   ├── text-indent.test.tsx
│   ├── text-wrap.test.tsx
│   ├── transform.test.tsx
│   ├── typesetting.test.tsx
│   ├── units.test.tsx
│   ├── utils.tsx
│   ├── webkit-text-stroke.test.tsx
│   ├── white-space.test.tsx
│   └── word-break.test.tsx
├── tsconfig.json
├── tsup.config.ts
├── turbo.json
├── vitest.config.ts
├── vitest.jsx-runtime.config.ts
└── yoga.wasm
Download .txt
SYMBOL INDEX (367 symbols across 64 files)

FILE: playground/cards/playground-data.ts
  type Tabs (line 1) | type Tabs = {

FILE: playground/components/introduction.tsx
  type IProps (line 4) | interface IProps {
  function Introduction (line 8) | function Introduction({ onClose }: IProps) {

FILE: playground/components/panel-resize-handle.tsx
  function PanelResizeHandle (line 6) | function PanelResizeHandle() {

FILE: playground/index.d.ts
  type Window (line 4) | interface Window {

FILE: playground/pages/_app.tsx
  function App (line 7) | function App({ Component, pageProps }: AppProps) {

FILE: playground/pages/_document.tsx
  function Document (line 4) | function Document() {

FILE: playground/pages/api/font.ts
  function encodeFontInfoAsArrayBuffer (line 17) | function encodeFontInfoAsArrayBuffer(code: string, fontData: ArrayBuffer) {
  function loadGoogleFont (line 37) | async function loadGoogleFont(req: NextRequest) {
  function fetchFont (line 86) | async function fetchFont(

FILE: playground/pages/index.tsx
  function init (line 29) | async function init() {
  function withCache (line 84) | function withCache(fn: Function) {
  type LanguageCode (line 95) | type LanguageCode = keyof typeof languageFontMap | 'emoji'
  function initResvgWorker (line 200) | function initResvgWorker() {
  type ITabs (line 232) | interface ITabs {
  function Tabs (line 238) | function Tabs({ options, onChange, children }: ITabs) {
  function LiveEditor (line 263) | function LiveEditor({ id }: { id: string }) {
  function updateScaleRatio (line 498) | function updateScaleRatio() {
  function ResetCode (line 911) | function ResetCode({ activeCard }: { activeCard: string }) {
  function Playground (line 958) | function Playground() {

FILE: playground/utils/font.ts
  type UnicodeRange (line 1) | type UnicodeRange = Array<number | number[]>
  class FontDetector (line 3) | class FontDetector {
    method detect (line 8) | public async detect(
    method detectSegment (line 31) | private detectSegment(segment: string, fonts: string[]): string | null {
    method load (line 42) | private async load(fonts: string[]): Promise<void> {
    method addDetectors (line 72) | private addDetectors(input: string) {
  function convert (line 87) | function convert(input: string): UnicodeRange {
  function checkSegmentInRange (line 100) | function checkSegmentInRange(segment: string, range: UnicodeRange): bool...

FILE: playground/utils/twemoji.ts
  constant U200D (line 7) | const U200D = String.fromCharCode(8205)
  function getIconCode (line 10) | function getIconCode(char: string) {
  function toCodePoint (line 14) | function toCodePoint(unicodeSurrogates: string) {
  function loadEmoji (line 54) | function loadEmoji(type: keyof typeof apis, code: string) {

FILE: src/builder/background-image.ts
  type Background (line 9) | interface Background {
  function toAbsoluteValue (line 20) | function toAbsoluteValue(v: string | number, base: number) {
  function calculateKeywordSize (line 27) | function calculateKeywordSize(
  function parseLengthPairs (line 82) | function parseLengthPairs(
  function backgroundImage (line 115) | async function backgroundImage(

FILE: src/builder/border-radius.ts
  function svgArcCenterOffset (line 13) | function svgArcCenterOffset([rx, ry]: number[]) {
  function resolveSize (line 20) | function resolveSize(a: number, b: number, limit: number) {
  function makeSmaller (line 33) | function makeSmaller(arr: [number, number]) {
  function resolveRadius (line 38) | function resolveRadius(
  function getBorderRadiusClipPath (line 69) | function getBorderRadiusClipPath(
  function radius (line 107) | function radius(

FILE: src/builder/border.ts
  function compareBorderDirections (line 4) | function compareBorderDirections(a: string, b: string, style: any) {
  function getBorderClipPath (line 12) | function getBorderClipPath(
  function border (line 66) | function border(

FILE: src/builder/clip-path.ts
  function genClipPathId (line 4) | function genClipPathId(id: string) {
  function genClipPath (line 7) | function genClipPath(id: string) {
  function buildClipPath (line 11) | function buildClipPath(

FILE: src/builder/content-mask.ts
  function contentMask (line 10) | function contentMask(

FILE: src/builder/gradient/linear.ts
  function buildLinearGradient (line 5) | function buildLinearGradient(
  function resolveRepeatingCycle (line 106) | function resolveRepeatingCycle(stops: ColorStop[], length: number) {
  function resolveXYFromDirection (line 116) | function resolveXYFromDirection(dir: string) {
  function calcNormalPoint (line 144) | function calcNormalPoint(v: number, w: number, h: number) {
  function calcPercentage (line 222) | function calcPercentage(

FILE: src/builder/gradient/radial.ts
  function buildRadialGradient (line 10) | function buildRadialGradient(
  type PositionKeyWord (line 154) | type PositionKeyWord = 'center' | 'left' | 'right' | 'top' | 'bottom'
  function calcColorStopTotalLength (line 156) | function calcColorStopTotalLength(
  function calcRadialGradient (line 176) | function calcRadialGradient(
  function calcPos (line 220) | function calcPos(
  type Shape (line 240) | type Shape = 'circle' | 'ellipse'
  function calcRadialGradientProps (line 242) | function calcRadialGradientProps(
  function calcRadius (line 280) | function calcRadius(
  function patchSpread (line 390) | function patchSpread(
  function f2r (line 425) | function f2r(fx: number, fy: number) {
  function isSizeAllLength (line 449) | function isSizeAllLength(v: RadialPropertyValue[]): v is Array<{

FILE: src/builder/gradient/utils.ts
  type Stop (line 5) | interface Stop {
  function normalizeStops (line 10) | function normalizeStops(

FILE: src/builder/mask-image.ts
  function buildMaskImage (line 7) | async function buildMaskImage(

FILE: src/builder/overflow.ts
  function overflow (line 9) | function overflow(

FILE: src/builder/rect.ts
  function parseObjectPosition (line 19) | function parseObjectPosition(
  function rect (line 102) | async function rect(

FILE: src/builder/shadow.ts
  function shiftPath (line 7) | function shiftPath(path: string, dx: number, dy: number) {
  constant SCALE (line 23) | const SCALE = 1.1
  function buildDropShadow (line 25) | function buildDropShadow(
  function boxShadow (line 135) | function boxShadow(

FILE: src/builder/svg.ts
  function svg (line 3) | function svg({

FILE: src/builder/text-decoration.ts
  function buildSkipInkSegments (line 4) | function buildSkipInkSegments(
  function buildDecoration (line 59) | function buildDecoration(

FILE: src/builder/text.ts
  function container (line 6) | function container(
  function buildText (line 46) | function buildText(

FILE: src/builder/transform.ts
  function resolveTransforms (line 7) | function resolveTransforms(transforms: any[], width: number, height: num...
  function transform (line 77) | function transform(

FILE: src/font.ts
  type Weight (line 7) | type Weight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900
  type WeightName (line 8) | type WeightName = 'normal' | 'bold'
  type FontWeight (line 9) | type FontWeight = Weight | WeightName
  type FontStyle (line 10) | type FontStyle = 'normal' | 'italic'
  constant SUFFIX_WHEN_LANG_NOT_SET (line 11) | const SUFFIX_WHEN_LANG_NOT_SET = 'unknown'
  type FontOptions (line 13) | interface FontOptions {
  type GlyphBox (line 21) | type GlyphBox = {
  type SkipInkBand (line 27) | type SkipInkBand = {
  type FontEngine (line 32) | type FontEngine = {
  type BandPoint (line 55) | type BandPoint = [number, number]
  type LineSegment (line 57) | type LineSegment = {
  function flattenPath (line 62) | function flattenPath(commands: opentype.Path['commands']): LineSegment[] {
  function evaluateBezier (line 113) | function evaluateBezier(points: BandPoint[], t: number): BandPoint {
  function computeBandBox (line 130) | function computeBandBox(
  function computeBoundingBox (line 218) | function computeBoundingBox(
  function compareFont (line 245) | function compareFont(
  class FontLoader (line 297) | class FontLoader {
    method constructor (line 300) | constructor(fontOptions: FontOptions[]) {
    method get (line 305) | private get({
    method addFonts (line 342) | public addFonts(fontOptions: FontOptions[]) {
    method getEngine (line 398) | public getEngine(
    method patchFontFallbackResolver (line 610) | private patchFontFallbackResolver(
    method measure (line 667) | private measure(
    method getSVG (line 690) | private getSVG(
  function getLangFromFontName (line 775) | function getLangFromFontName(name: string): Locale | undefined {

FILE: src/handler/compute.ts
  type SatoriElement (line 20) | type SatoriElement = keyof typeof presets
  function compute (line 22) | async function compute(

FILE: src/handler/expand.ts
  function handleFallbackColor (line 40) | function handleFallbackColor(
  function purify (line 55) | function purify(name: string, value?: string | number) {
  function handleSpecialCase (line 63) | function handleSpecialCase(
  function getErrorHint (line 234) | function getErrorHint(name: string) {
  constant RGB_SLASH (line 241) | const RGB_SLASH = /rgb\((\d+)\s+(\d+)\s+(\d+)\s*\/\s*([\.\d]+)\)/
  function normalizeColor (line 242) | function normalizeColor(value: string | object) {
  type MainStyle (line 263) | type MainStyle = {
  type OtherStyle (line 309) | type OtherStyle = Exclude<Record<PropertyKey, string | number>, keyof Ma...
  type SerializedStyle (line 311) | type SerializedStyle = Partial<MainStyle & OtherStyle>
  function expand (line 313) | function expand(
  function calcBaseFontSize (line 518) | function calcBaseFontSize(
  function refineHSL (line 540) | function refineHSL(color: string) {
  function getCurrentColor (line 553) | function getCurrentColor(
  function convertCurrentColorToActualValue (line 564) | function convertCurrentColorToActualValue(
  function preprocess (line 571) | function preprocess(

FILE: src/handler/image.ts
  constant AVIF (line 10) | const AVIF = 'image/avif'
  constant WEBP (line 11) | const WEBP = 'image/webp'
  constant APNG (line 12) | const APNG = 'image/apng'
  constant PNG (line 13) | const PNG = 'image/png'
  constant JPEG (line 14) | const JPEG = 'image/jpeg'
  constant GIF (line 15) | const GIF = 'image/gif'
  constant SVG (line 16) | const SVG = 'image/svg+xml'
  function parseJPEG (line 18) | function parseJPEG(buf: ArrayBuffer) {
  function parseGIF (line 48) | function parseGIF(buf: ArrayBuffer) {
  function parsePNG (line 56) | function parsePNG(buf: ArrayBuffer) {
  type ResolvedImageData (line 63) | type ResolvedImageData = [string, number?, number?] | readonly []
  constant ALLOWED_IMAGE_TYPES (line 67) | const ALLOWED_IMAGE_TYPES = [PNG, APNG, JPEG, GIF, SVG]
  constant SVG_ATTRS_REGEX (line 70) | const SVG_ATTRS_REGEX = /<svg[^>]*>/i
  constant VIEWBOX_REGEX (line 71) | const VIEWBOX_REGEX = /viewBox=['"]([^'"]+)['"]/
  constant WIDTH_REGEX (line 72) | const WIDTH_REGEX = /width=['"](\d*\.?\d+)['"]/
  constant HEIGHT_REGEX (line 73) | const HEIGHT_REGEX = /height=['"](\d*\.?\d+)['"]/
  function arrayBufferToBase64 (line 75) | function arrayBufferToBase64(buffer) {
  function base64ToArrayBuffer (line 88) | function base64ToArrayBuffer(base64: string): ArrayBuffer {
  function parseSvgImageSize (line 98) | function parseSvgImageSize(src: string, data: string) {
  function arrayBufferToDataUri (line 131) | function arrayBufferToDataUri(data: ArrayBuffer) {
  function resolveImageData (line 158) | async function resolveImageData(
  function detectContentType (line 294) | function detectContentType(buffer: Uint8Array) {
  function detectAPNG (line 331) | function detectAPNG(bytes: Uint8Array) {

FILE: src/handler/inheritable.ts
  function inheritable (line 43) | function inheritable(style: SerializedStyle): SerializedStyle {

FILE: src/handler/preprocess.ts
  constant ATTRIBUTE_MAPPING (line 7) | const ATTRIBUTE_MAPPING = {
  function translateSVGNodeToSVGString (line 97) | function translateSVGNodeToSVGString(
  function preProcessNode (line 148) | async function preProcessNode(node: ReactNode) {
  function SVGNodeToImage (line 188) | async function SVGNodeToImage(

FILE: src/handler/presets.ts
  constant DEFAULT_DISPLAY (line 9) | const DEFAULT_DISPLAY = 'flex'

FILE: src/handler/tailwind.ts
  type TwPlugin (line 5) | type TwPlugin = TwConfig['plugins'][number]
  function createTw (line 40) | function createTw(config?: TwConfig) {
  function getTw (line 51) | function getTw({

FILE: src/handler/variables.ts
  type CSSVariables (line 11) | type CSSVariables = Record<string, string>
  function extractCustomProperties (line 17) | function extractCustomProperties(
  function mergeVariables (line 42) | function mergeVariables(
  function resolveVariables (line 54) | function resolveVariables(
  function extractVarArgs (line 148) | function extractVarArgs(
  function replaceNode (line 189) | function replaceNode(node: valueParser.Node, value: string) {
  function resolveStyleVariables (line 201) | function resolveStyleVariables(

FILE: src/jsx/index.ts
  function createElement (line 15) | function createElement<P extends {}>(

FILE: src/jsx/intrinsic-elements.ts
  type CSSProperties (line 20) | interface CSSProperties {
  type Booleanish (line 24) | type Booleanish = 'true' | 'false' | boolean
  type CrossOrigin (line 25) | type CrossOrigin = 'anonymous' | 'use-credentials' | '' | undefined
  type SVGProps (line 27) | interface SVGProps<T> extends SVGAttributes<T> {}
  type SVGLineElementAttributes (line 29) | interface SVGLineElementAttributes<T> extends SVGProps<T> {}
  type SVGTextElementAttributes (line 30) | interface SVGTextElementAttributes<T> extends SVGProps<T> {}
  type DOMAttributes (line 32) | interface DOMAttributes<T> {
  type AriaRole (line 36) | type AriaRole =
  type AriaAttributes (line 108) | interface AriaAttributes {
  type HTMLAttributes (line 362) | interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
  type DetailedHTMLProps (line 477) | type DetailedHTMLProps<E extends HTMLAttributes<T>, T> = E
  type HTMLElementType (line 479) | type HTMLElementType =
  type SVGElementType (line 599) | type SVGElementType =
  type HTMLAttributeReferrerPolicy (line 656) | type HTMLAttributeReferrerPolicy =
  type HTMLAttributeAnchorTarget (line 667) | type HTMLAttributeAnchorTarget =
  type AnchorHTMLAttributes (line 674) | interface AnchorHTMLAttributes<T> extends HTMLAttributes<T> {
  type AudioHTMLAttributes (line 685) | interface AudioHTMLAttributes<T> extends MediaHTMLAttributes<T> {}
  type AreaHTMLAttributes (line 687) | interface AreaHTMLAttributes<T> extends HTMLAttributes<T> {
  type BaseHTMLAttributes (line 699) | interface BaseHTMLAttributes<T> extends HTMLAttributes<T> {
  type BlockquoteHTMLAttributes (line 704) | interface BlockquoteHTMLAttributes<T> extends HTMLAttributes<T> {
  type ButtonHTMLAttributes (line 708) | interface ButtonHTMLAttributes<T> extends HTMLAttributes<T> {
  type CanvasHTMLAttributes (line 724) | interface CanvasHTMLAttributes<T> extends HTMLAttributes<T> {
  type ColHTMLAttributes (line 729) | interface ColHTMLAttributes<T> extends HTMLAttributes<T> {
  type ColgroupHTMLAttributes (line 734) | interface ColgroupHTMLAttributes<T> extends HTMLAttributes<T> {
  type DataHTMLAttributes (line 738) | interface DataHTMLAttributes<T> extends HTMLAttributes<T> {
  type DetailsHTMLAttributes (line 742) | interface DetailsHTMLAttributes<T> extends HTMLAttributes<T> {
  type DelHTMLAttributes (line 747) | interface DelHTMLAttributes<T> extends HTMLAttributes<T> {
  type DialogHTMLAttributes (line 752) | interface DialogHTMLAttributes<T> extends HTMLAttributes<T> {
  type EmbedHTMLAttributes (line 756) | interface EmbedHTMLAttributes<T> extends HTMLAttributes<T> {
  type FieldsetHTMLAttributes (line 763) | interface FieldsetHTMLAttributes<T> extends HTMLAttributes<T> {
  type FormHTMLAttributes (line 769) | interface FormHTMLAttributes<T> extends HTMLAttributes<T> {
  type HtmlHTMLAttributes (line 780) | interface HtmlHTMLAttributes<T> extends HTMLAttributes<T> {
  type IframeHTMLAttributes (line 784) | interface IframeHTMLAttributes<T> extends HTMLAttributes<T> {
  type ImgHTMLAttributes (line 807) | interface ImgHTMLAttributes<T> extends HTMLAttributes<T> {
  type InsHTMLAttributes (line 822) | interface InsHTMLAttributes<T> extends HTMLAttributes<T> {
  type HTMLInputTypeAttribute (line 827) | type HTMLInputTypeAttribute =
  type AutoFillAddressKind (line 852) | type AutoFillAddressKind = 'billing' | 'shipping'
  type AutoFillBase (line 853) | type AutoFillBase = '' | 'off' | 'on'
  type AutoFillContactField (line 854) | type AutoFillContactField =
  type AutoFillContactKind (line 864) | type AutoFillContactKind = 'home' | 'mobile' | 'work'
  type AutoFillCredentialField (line 865) | type AutoFillCredentialField = 'webauthn'
  type AutoFillNormalField (line 866) | type AutoFillNormalField =
  type OptionalPrefixToken (line 903) | type OptionalPrefixToken<T extends string> = `${T} ` | ''
  type OptionalPostfixToken (line 904) | type OptionalPostfixToken<T extends string> = ` ${T}` | ''
  type AutoFillField (line 905) | type AutoFillField =
  type AutoFillSection (line 908) | type AutoFillSection = `section-${string}`
  type AutoFill (line 909) | type AutoFill =
  type HTMLInputAutoCompleteAttribute (line 912) | type HTMLInputAutoCompleteAttribute = AutoFill | (string & {})
  type InputHTMLAttributes (line 914) | interface InputHTMLAttributes<T> extends HTMLAttributes<T> {
  type KeygenHTMLAttributes (line 950) | interface KeygenHTMLAttributes<T> extends HTMLAttributes<T> {
  type LabelHTMLAttributes (line 959) | interface LabelHTMLAttributes<T> extends HTMLAttributes<T> {
  type LiHTMLAttributes (line 964) | interface LiHTMLAttributes<T> extends HTMLAttributes<T> {
  type LinkHTMLAttributes (line 968) | interface LinkHTMLAttributes<T> extends HTMLAttributes<T> {
  type MapHTMLAttributes (line 988) | interface MapHTMLAttributes<T> extends HTMLAttributes<T> {
  type MenuHTMLAttributes (line 992) | interface MenuHTMLAttributes<T> extends HTMLAttributes<T> {
  type MediaHTMLAttributes (line 996) | interface MediaHTMLAttributes<T> extends HTMLAttributes<T> {
  type MetaHTMLAttributes (line 1009) | interface MetaHTMLAttributes<T> extends HTMLAttributes<T> {
  type MeterHTMLAttributes (line 1017) | interface MeterHTMLAttributes<T> extends HTMLAttributes<T> {
  type QuoteHTMLAttributes (line 1027) | interface QuoteHTMLAttributes<T> extends HTMLAttributes<T> {
  type ObjectHTMLAttributes (line 1031) | interface ObjectHTMLAttributes<T> extends HTMLAttributes<T> {
  type OlHTMLAttributes (line 1043) | interface OlHTMLAttributes<T> extends HTMLAttributes<T> {
  type OptgroupHTMLAttributes (line 1049) | interface OptgroupHTMLAttributes<T> extends HTMLAttributes<T> {
  type OptionHTMLAttributes (line 1054) | interface OptionHTMLAttributes<T> extends HTMLAttributes<T> {
  type OutputHTMLAttributes (line 1061) | interface OutputHTMLAttributes<T> extends HTMLAttributes<T> {
  type ParamHTMLAttributes (line 1067) | interface ParamHTMLAttributes<T> extends HTMLAttributes<T> {
  type ProgressHTMLAttributes (line 1072) | interface ProgressHTMLAttributes<T> extends HTMLAttributes<T> {
  type SlotHTMLAttributes (line 1077) | interface SlotHTMLAttributes<T> extends HTMLAttributes<T> {
  type ScriptHTMLAttributes (line 1081) | interface ScriptHTMLAttributes<T> extends HTMLAttributes<T> {
  type SelectHTMLAttributes (line 1095) | interface SelectHTMLAttributes<T> extends HTMLAttributes<T> {
  type SourceHTMLAttributes (line 1106) | interface SourceHTMLAttributes<T> extends HTMLAttributes<T> {
  type StyleHTMLAttributes (line 1116) | interface StyleHTMLAttributes<T> extends HTMLAttributes<T> {
  type TableHTMLAttributes (line 1127) | interface TableHTMLAttributes<T> extends HTMLAttributes<T> {
  type TextareaHTMLAttributes (line 1139) | interface TextareaHTMLAttributes<T> extends HTMLAttributes<T> {
  type TdHTMLAttributes (line 1156) | interface TdHTMLAttributes<T> extends HTMLAttributes<T> {
  type ThHTMLAttributes (line 1168) | interface ThHTMLAttributes<T> extends HTMLAttributes<T> {
  type TimeHTMLAttributes (line 1177) | interface TimeHTMLAttributes<T> extends HTMLAttributes<T> {
  type TrackHTMLAttributes (line 1181) | interface TrackHTMLAttributes<T> extends HTMLAttributes<T> {
  type VideoHTMLAttributes (line 1189) | interface VideoHTMLAttributes<T> extends MediaHTMLAttributes<T> {
  type SVGAttributes (line 1206) | interface SVGAttributes<T> extends AriaAttributes, DOMAttributes<T> {
  type WebViewHTMLAttributes (line 1496) | interface WebViewHTMLAttributes<T> extends HTMLAttributes<T> {
  type DefinedIntrinsicElements (line 1520) | interface DefinedIntrinsicElements {

FILE: src/jsx/jsx-runtime.ts
  type ElementClass (line 23) | type ElementClass = never
  type ElementType (line 25) | type ElementType = string | FC<any>
  type Element (line 27) | type Element = JSXElement<any, any>
  type ElementAttributesProperty (line 29) | interface ElementAttributesProperty {
  type ElementChildrenAttribute (line 33) | interface ElementChildrenAttribute {
  type IntrinsicElements (line 38) | interface IntrinsicElements extends DefinedIntrinsicElements {}
  type IntrinsicAttributes (line 40) | interface IntrinsicAttributes {
  function jsx (line 46) | function jsx(

FILE: src/jsx/types.ts
  type JSXKey (line 9) | type JSXKey = string | number | bigint
  type JSXElement (line 26) | interface JSXElement<
  type AwaitedJSXNode (line 35) | type AwaitedJSXNode =
  type JSXNode (line 75) | type JSXNode =
  type FC (line 113) | type FC<P = {}> = (props: P) => JSXNode | Promise<JSXNode>

FILE: src/language.ts
  type SpecialCodeKey (line 47) | type SpecialCodeKey = keyof typeof specialCode
  type CodeKey (line 48) | type CodeKey = keyof typeof specialCode | keyof typeof code
  type Locale (line 49) | type Locale = keyof typeof code
  type LangCode (line 50) | type LangCode = CodeKey | 'unknown'
  function isValidLocale (line 53) | function isValidLocale(x: any): x is Locale {
  function detectLanguageCode (line 57) | function detectLanguageCode(
  function normalizeLocale (line 86) | function normalizeLocale(locale?: string): Locale | undefined {

FILE: src/layout.ts
  type LayoutContext (line 24) | interface LayoutContext {
  type SatoriNode (line 40) | interface SatoriNode {

FILE: src/parser/mask.ts
  function getMaskProperty (line 4) | function getMaskProperty(style: Record<string, string | number>, name: s...
  type MaskProperty (line 9) | interface MaskProperty {
  function parseMask (line 18) | function parseMask(

FILE: src/parser/shape.ts
  function createShapeParser (line 13) | function createShapeParser(
  function resolveFillRule (line 207) | function resolveFillRule(str: string) {
  function resolvePosition (line 214) | function resolvePosition(position: string, xDelta: number, yDelta: numbe...

FILE: src/satori.ts
  type SatoriOptions (line 18) | type SatoriOptions = (
  function satori (line 44) | async function satori(
  function getRootNode (line 196) | function getRootNode(
  function convertToLanguageCodes (line 209) | function convertToLanguageCodes(
  function unique (line 242) | function unique<T>(arr: T[]): T[] {

FILE: src/text/characters.ts
  function stringFromCode (line 1) | function stringFromCode(code: string): string {

FILE: src/text/index.ts
  function shouldSkipWhenFindingMissingFont (line 28) | function shouldSkipWhenFindingMissingFont(word: string): boolean {
  function isFullyTransparent (line 32) | function isFullyTransparent(color: string): boolean {
  function isOpaqueWhite (line 38) | function isOpaqueWhite(color: string): boolean {
  function isImage (line 121) | function isImage(s: string): boolean {
  function flow (line 198) | function flow(width: number) {
  type DecorationLine (line 532) | type DecorationLine = {
  function calcEllipsis (line 641) | function calcEllipsis(baseWidth: number, _text: string) {
  function createTextContainerNode (line 940) | function createTextContainerNode(Yoga: TYoga, textAlign: string): YogaNo...
  function detectTabs (line 964) | function detectTabs(text: string):

FILE: src/text/measurer.ts
  function genMeasurer (line 4) | function genMeasurer(

FILE: src/text/processor.ts
  function preprocess (line 6) | function preprocess(
  function processTextTransform (line 49) | function processTextTransform(
  function processTextOverflow (line 76) | function processTextOverflow(
  function processWordBreak (line 114) | function processWordBreak(
  function processWhiteSpace (line 128) | function processWhiteSpace(
  function parseLineClamp (line 157) | function parseLineClamp(input: number | string): [number?, string?] {

FILE: src/transform-origin.ts
  type ParsedTransformOrigin (line 8) | interface ParsedTransformOrigin {
  type ParsedUnit (line 19) | interface ParsedUnit {
  function parseUnit (line 26) | function parseUnit(word: string, baseFontSize: number): ParsedUnit {
  function handleWord (line 46) | function handleWord(
  function parseTransformOrigin (line 77) | function parseTransformOrigin(

FILE: src/utils.ts
  function isReactElement (line 6) | function isReactElement(node: ReactNode): node is ReactElement {
  function isClass (line 19) | function isClass(f: Function) {
  function isForwardRefComponent (line 23) | function isForwardRefComponent(type: any) {
  function isReactComponent (line 27) | function isReactComponent(type: any) {
  function hasDangerouslySetInnerHTMLProp (line 31) | function hasDangerouslySetInnerHTMLProp(props: any) {
  function normalizeChildren (line 35) | function normalizeChildren(children: any) {
  function lengthToNumber (line 65) | function lengthToNumber(
  function calcDegree (line 116) | function calcDegree(deg: string) {
  function multiply (line 132) | function multiply(m1: number[], m2: number[]) {
  function v (line 143) | function v(
  constant MAX_SEGMENT_CACHE_SIZE (line 176) | const MAX_SEGMENT_CACHE_SIZE = 500
  function segment (line 178) | function segment(
  function buildXMLString (line 243) | function buildXMLString(
  function createLRU (line 262) | function createLRU<T>(max = 20) {
  function parseViewBox (line 294) | function parseViewBox(viewBox?: string | null | undefined) {
  function toString (line 298) | function toString(x: unknown): string {
  function isString (line 302) | function isString(x: unknown): x is string {
  function isNumber (line 306) | function isNumber(x: unknown): x is number {
  function isUndefined (line 310) | function isUndefined(x: unknown): x is undefined {
  function asPointPercentageLength (line 314) | function asPointPercentageLength(
  function asPointAutoPercentageLength (line 342) | function asPointAutoPercentageLength(
  function splitByBreakOpportunities (line 373) | function splitByBreakOpportunities(
  function splitEffects (line 418) | function splitEffects(

FILE: src/vendor/parse-css-dimension/index.js
  function s (line 1) | function s(t){if(/\.\D?$/.test(t))throw new Error("The dot should be fol...
  function U (line 1) | function U(t){return new s(t)}
  function x (line 1) | function x(t){var r=t.match(/\./g);return r?r.length:0}
  function o (line 1) | function o(t){var r=parseFloat(t);if(isNaN(r))throw new Error("Invalid n...
  function O (line 1) | function O(t){var r=t.match(/\D+$/),n=r&&r[0];if(n&&E.indexOf(n)===-1)th...
  function i (line 1) | function i(t,r){return Object.fromEntries(t.map(n=>[n,r]))}
  function F (line 1) | function F(t){return D[t]||"length"}

FILE: src/vendor/parse-css-dimension/src.js
  function CssDimension (line 7) | function CssDimension(value) {
  function factory (line 47) | function factory(value) {
  function countDots (line 51) | function countDots(value) {
  function tryParseFloat (line 56) | function tryParseFloat(value) {
  function parseUnit (line 72) | function parseUnit(value) {
  function createLookups (line 88) | function createLookups(list, value) {
  function getTypeFromUnit (line 92) | function getTypeFromUnit(unit) {

FILE: src/vendor/twrnc/log.js
  method info (line 2) | info(key, messages) {
  method warn (line 5) | warn(key, messages) {
  method risk (line 8) | risk(key, messages) {

FILE: src/yoga.bundled.ts
  function getYoga (line 5) | function getYoga() {

FILE: src/yoga.external.ts
  type InitInput (line 18) | type InitInput =
  function loadWasm (line 28) | async function loadWasm(
  function init (line 79) | function init(input: InitInput) {
  function getYoga (line 95) | function getYoga() {

FILE: src/yoga.ts
  function init (line 7) | function init(input: InitInput) {
  function getYoga (line 15) | function getYoga() {

FILE: test/basic.test.tsx
  function MyComponent (line 102) | function MyComponent() {
  function MyAsyncComponent (line 125) | async function MyAsyncComponent() {

FILE: test/benchmark/index.ts
  function generateSVG (line 55) | async function generateSVG() {
  function generatePNGWithResvg (line 254) | function generatePNGWithResvg(svg: string) {
  function generatePNGWithSharp (line 270) | async function generatePNGWithSharp(svg: string) {

FILE: test/css-variables.test.tsx
  type CSSProperties (line 7) | interface CSSProperties {

FILE: test/image.test.tsx
  constant PNG_SAMPLE (line 6) | const PNG_SAMPLE =
  function dataUriToArrayBuffer (line 9) | function dataUriToArrayBuffer(dataUri: string): ArrayBuffer {
  constant PNG_SAMPLE_ARRAYBUFFER (line 19) | const PNG_SAMPLE_ARRAYBUFFER = dataUriToArrayBuffer(PNG_SAMPLE)

FILE: test/jsx-runtime.test.tsx
  function MyComponent (line 15) | function MyComponent() {
  function MyAsyncComponent (line 39) | async function MyAsyncComponent() {

FILE: test/mask-image.test.tsx
  constant PNG_SAMPLE (line 6) | const PNG_SAMPLE =

FILE: test/utils.tsx
  function getDynamicAsset (line 9) | async function getDynamicAsset(text: string): Promise<Buffer> {
  function loadDynamicAsset (line 14) | async function loadDynamicAsset(code: string, text: string) {
  function initFonts (line 26) | function initFonts(callback: (fonts: SatoriOptions['fonts']) => void) {
  function toImage (line 41) | function toImage(svg: string, width = 100) {
  type Matchers (line 62) | interface Matchers<R> {

FILE: tsup.config.ts
  method esbuildOptions (line 22) | esbuildOptions(options) {
  method setup (line 34) | setup(build) {
Condensed preview — 154 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (833K chars).
[
  {
    "path": ".eslintrc.json",
    "chars": 1032,
    "preview": "{\n  \"env\": {\n    \"browser\": true,\n    \"es2021\": true,\n    \"node\": true\n  },\n  \"extends\": [\n    \"eslint:recommended\",\n   "
  },
  {
    "path": ".github/CODEOWNERS",
    "chars": 11,
    "preview": "* @shuding\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 425,
    "preview": "---\nname: Bug report\nabout: Create a bug report for Satori\n---\n\n# Bug report\n\n## Description / Observed Behavior\n\nWhat k"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 202,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: Question & Ideas\n    url: https://github.com/vercel/satori/discussi"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 313,
    "preview": "---\nname: Feature request\nabout: Request a new feature for Satori\n---\n\n# Feature Request\n\n## Description\n\nWhat do you wa"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 2073,
    "preview": "name: CI\n\non:\n  push:\n    branches: ['main']\n  pull_request:\n    branches: ['main']\n\nconcurrency:\n  group: ${{ github.wo"
  },
  {
    "path": ".github/workflows/pr.yml",
    "chars": 343,
    "preview": "name: PR\non:\n  pull_request:\n    types: [opened, edited, synchronize]\n  pull_request_target:\n    types: [opened, edited,"
  },
  {
    "path": ".gitignore",
    "chars": 206,
    "preview": "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\npl"
  },
  {
    "path": ".husky/pre-commit",
    "chars": 70,
    "preview": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\npnpm lint-staged\n"
  },
  {
    "path": ".npmrc",
    "chars": 36,
    "preview": "shell-emulator=true\nprovenance=true\n"
  },
  {
    "path": ".prettierignore",
    "chars": 98,
    "preview": ".github/\nnode_modules\n**/.next/**\n**/_next/**\n**/dist/**\nsrc/vendor/\npnpm-lock.yaml\n*.md\ncoverage/"
  },
  {
    "path": ".prettierrc",
    "chars": 106,
    "preview": "{\n  \"tabWidth\": 2,\n  \"useTabs\": false,\n  \"singleQuote\": true,\n  \"jsxSingleQuote\": true,\n  \"semi\": false\n}\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1888,
    "preview": "# Satori Contribution Guidelines\n\nThank you for reading this guide and we appreciate any contribution.\n\n## Ask a Questio"
  },
  {
    "path": "LICENSE",
    "chars": 16725,
    "preview": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\""
  },
  {
    "path": "README.md",
    "chars": 30413,
    "preview": "![Satori](.github/card.png)\n\n**Satori**: Enlightened library to convert HTML and CSS to SVG.\n\n> **Note**\n>\n> To use Sato"
  },
  {
    "path": "package.json",
    "chars": 4375,
    "preview": "{\n  \"name\": \"satori\",\n  \"version\": \"0.0.0-development\",\n  \"description\": \"Enlightened library to convert HTML and CSS to"
  },
  {
    "path": "patches/yoga-layout@3.2.1.patch",
    "chars": 26359,
    "preview": "diff --git a/dist/binaries/yoga-wasm-esm.js b/dist/binaries/yoga-wasm-esm.js\nnew file mode 100644\nindex 0000000000000000"
  },
  {
    "path": "playground/LICENSE",
    "chars": 16725,
    "preview": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\""
  },
  {
    "path": "playground/cards/playground-data.ts",
    "chars": 8130,
    "preview": "export type Tabs = {\n  [x: string]: string\n}\n\nconst playgroundTabs: Tabs = {\n  helloworld: `<div\n  style={{\n    height: "
  },
  {
    "path": "playground/cards/preview-tabs.ts",
    "chars": 183,
    "preview": "const previewTabs = [\n  'SVG (Satori)',\n  'PNG (Satori + resvg-js)', // https://github.com/yisibl/resvg-js\n  'PDF (Sator"
  },
  {
    "path": "playground/components/introduction.module.css",
    "chars": 1131,
    "preview": ".container {\n  position: fixed;\n  left: 20px;\n  bottom: 20px;\n  width: 700px;\n  min-height: 200px;\n  max-width: calc(100"
  },
  {
    "path": "playground/components/introduction.tsx",
    "chars": 1089,
    "preview": "import React from 'react'\nimport styles from './introduction.module.css'\n\ninterface IProps {\n  onClose: React.MouseEvent"
  },
  {
    "path": "playground/components/panel-resize-handle.module.css",
    "chars": 842,
    "preview": ".handle {\n  flex: 0 0 1.5em;\n  position: relative;\n  outline: none;\n  --background-color: #efefef;\n}\n.handle:hover {\n  -"
  },
  {
    "path": "playground/components/panel-resize-handle.tsx",
    "chars": 686,
    "preview": "import React from 'react'\nimport { PanelResizeHandle as PanelResizeHandleImpl } from 'react-resizable-panels'\n\nimport st"
  },
  {
    "path": "playground/components/resvg_worker.ts",
    "chars": 528,
    "preview": "import * as resvg from '@resvg/resvg-wasm'\n\nconst wasmPath = new URL('@resvg/resvg-wasm/index_bg.wasm', import.meta.url)"
  },
  {
    "path": "playground/decs.d.ts",
    "chars": 69,
    "preview": "declare module 'pdfkit/js/pdfkit.standalone'\ndeclare module 'satori'\n"
  },
  {
    "path": "playground/index.d.ts",
    "chars": 75,
    "preview": "export {}\n\ndeclare global {\n  interface Window {\n    __resource: any\n  }\n}\n"
  },
  {
    "path": "playground/next-env.d.ts",
    "chars": 201,
    "preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edite"
  },
  {
    "path": "playground/package.json",
    "chars": 1003,
    "preview": "{\n  \"private\": true,\n  \"name\": \"satori-playground\",\n  \"license\": \"MPL-2.0\",\n  \"scripts\": {\n    \"dev\": \"next\",\n    \"debug"
  },
  {
    "path": "playground/pages/_app.tsx",
    "chars": 2451,
    "preview": "import React from 'react'\nimport Head from 'next/head'\nimport { AppProps } from 'next/app'\n\nimport '../styles.css'\n\nexpo"
  },
  {
    "path": "playground/pages/_document.tsx",
    "chars": 257,
    "preview": "import React from 'react'\nimport { Html, Head, Main, NextScript } from 'next/document'\n\nexport default function Document"
  },
  {
    "path": "playground/pages/api/font.ts",
    "chars": 3462,
    "preview": "import type { NextRequest } from 'next/server'\nimport { FontDetector, languageFontMap } from '../../utils/font'\n\nexport "
  },
  {
    "path": "playground/pages/index.tsx",
    "chars": 32081,
    "preview": "import React from 'react'\nimport satori from 'satori'\nimport { LiveProvider, LiveContext, withLive } from 'react-live'\ni"
  },
  {
    "path": "playground/styles.css",
    "chars": 6524,
    "preview": "/* Fonts for the demo */\n@font-face {\n  font-family: 'Inter';\n  font-style: normal;\n  font-weight: 700;\n  src: url(/inte"
  },
  {
    "path": "playground/tsconfig.json",
    "chars": 508,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es2015\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    "
  },
  {
    "path": "playground/utils/font.ts",
    "chars": 3370,
    "preview": "type UnicodeRange = Array<number | number[]>\n\nexport class FontDetector {\n  private rangesByLang: {\n    [font: string]: "
  },
  {
    "path": "playground/utils/twemoji.ts",
    "chars": 1933,
    "preview": "/**\n * Modified version of https://unpkg.com/twemoji@13.1.0/dist/twemoji.esm.js.\n */\n\n/*! Copyright Twitter Inc. and oth"
  },
  {
    "path": "pnpm-workspace.yaml",
    "chars": 35,
    "preview": "packages:\n  - 'playground'\n  - '.'\n"
  },
  {
    "path": "release.config.cjs",
    "chars": 70,
    "preview": "module.exports = {\n  branches: ['main'],\n  tagFormat: '${version}',\n}\n"
  },
  {
    "path": "src/builder/background-image.ts",
    "chars": 7898,
    "preview": "import CssDimension from '../vendor/parse-css-dimension/index.js'\nimport { buildXMLString } from '../utils.js'\n\nimport {"
  },
  {
    "path": "src/builder/border-radius.ts",
    "chars": 8509,
    "preview": "/**\n * CSS border radius to SVG path.\n */\n\n// TODO: Support the `border-radius: 10px / 20px` syntax.\n// https://develope"
  },
  {
    "path": "src/builder/border.ts",
    "chars": 4212,
    "preview": "import { buildXMLString } from '../utils.js'\nimport radius from './border-radius.js'\n\nfunction compareBorderDirections(a"
  },
  {
    "path": "src/builder/clip-path.ts",
    "chars": 1194,
    "preview": "import { buildXMLString } from '../utils.js'\nimport { createShapeParser } from '../parser/shape.js'\n\nexport function gen"
  },
  {
    "path": "src/builder/content-mask.ts",
    "chars": 2138,
    "preview": "/**\n * When there is border radius, the content area should be clipped by the\n * inner path of border + padding. This ap"
  },
  {
    "path": "src/builder/gradient/linear.ts",
    "chars": 6274,
    "preview": "import { parseLinearGradient, ColorStop } from 'css-gradient-parser'\nimport { normalizeStops } from './utils.js'\nimport "
  },
  {
    "path": "src/builder/gradient/radial.ts",
    "chars": 10569,
    "preview": "import {\n  parseRadialGradient,\n  RadialResult,\n  RadialPropertyValue,\n  ColorStop,\n} from 'css-gradient-parser'\nimport "
  },
  {
    "path": "src/builder/gradient/utils.ts",
    "chars": 3001,
    "preview": "import { lengthToNumber } from '../../utils.js'\nimport cssColorParse from 'parse-css-color'\nimport type { ColorStop } fr"
  },
  {
    "path": "src/builder/mask-image.ts",
    "chars": 1211,
    "preview": "import { buildXMLString } from '../utils.js'\nimport buildBackgroundImage from './background-image.js'\nimport type { Mask"
  },
  {
    "path": "src/builder/overflow.ts",
    "chars": 1995,
    "preview": "/**\n * Generate clip path for the given element.\n */\n\nimport { buildXMLString } from '../utils.js'\nimport mask from './c"
  },
  {
    "path": "src/builder/rect.ts",
    "chars": 14478,
    "preview": "import type { ParsedTransformOrigin } from '../transform-origin.js'\n\nimport backgroundImage from './background-image.js'"
  },
  {
    "path": "src/builder/shadow.ts",
    "chars": 9197,
    "preview": "// @TODO: It seems that SVG filters are pretty expensive for resvg, PNG\n// generation time 10x'd when adding this filter"
  },
  {
    "path": "src/builder/svg.ts",
    "chars": 345,
    "preview": "import { buildXMLString } from '../utils.js'\n\nexport default function svg({\n  width,\n  height,\n  content,\n}: {\n  width: "
  },
  {
    "path": "src/builder/text-decoration.ts",
    "chars": 4029,
    "preview": "import { buildXMLString } from '../utils.js'\nimport type { GlyphBox } from '../font.js'\n\nfunction buildSkipInkSegments(\n"
  },
  {
    "path": "src/builder/text.ts",
    "chars": 3749,
    "preview": "import escapeHTML from 'escape-html'\nimport type { ParsedTransformOrigin } from '../transform-origin.js'\nimport transfor"
  },
  {
    "path": "src/builder/transform.ts",
    "chars": 3657,
    "preview": "import { multiply } from '../utils.js'\nimport type { ParsedTransformOrigin } from '../transform-origin.js'\n\nconst baseMa"
  },
  {
    "path": "src/font.ts",
    "chars": 20748,
    "preview": "/**\n * This class handles everything related to fonts.\n */\nimport opentype from '@shuding/opentype.js'\nimport { Locale, "
  },
  {
    "path": "src/handler/compute.ts",
    "chars": 11719,
    "preview": "/**\n * Handler to update the Yoga node properties with the given element type and\n * style. Each supported element has i"
  },
  {
    "path": "src/handler/expand.ts",
    "chars": 15361,
    "preview": "/**\n * This module expands the CSS properties to get rid of shorthands, as well as\n * cleaning up some properties.\n */\n\n"
  },
  {
    "path": "src/handler/image.ts",
    "chars": 9462,
    "preview": "/**\n * This module is used to fetch image from the given URL and resolve it as\n * base64 inlined data URI, so the toolch"
  },
  {
    "path": "src/handler/inheritable.ts",
    "chars": 1197,
    "preview": "import { SerializedStyle } from './expand.js'\n\nconst list = new Set([\n  'color',\n  'font',\n  'fontFamily',\n  'fontSize',"
  },
  {
    "path": "src/handler/preprocess.ts",
    "chars": 6908,
    "preview": "import type { ReactElement, ReactNode } from 'react'\nimport { resolveImageData, cache } from './image.js'\nimport { isRea"
  },
  {
    "path": "src/handler/presets.ts",
    "chars": 2697,
    "preview": "/**\n * Pre-defined styles for elements. Here we hand pick some from Chromium's\n * default styles:\n * https://chromium.go"
  },
  {
    "path": "src/handler/tailwind.ts",
    "chars": 1476,
    "preview": "import type { TwConfig } from 'twrnc'\n\nimport * as twrnc from 'twrnc/create'\n\ntype TwPlugin = TwConfig['plugins'][number"
  },
  {
    "path": "src/handler/variables.ts",
    "chars": 5808,
    "preview": "/**\n * This module handles CSS custom properties (CSS variables) including:\n * - Extracting custom properties from style"
  },
  {
    "path": "src/index.ts",
    "chars": 237,
    "preview": "export type {\n  FontOptions as Font,\n  Weight as FontWeight,\n  FontStyle,\n} from './font.js'\nexport type { Locale } from"
  },
  {
    "path": "src/jsx/index.ts",
    "chars": 1061,
    "preview": "import { jsx } from './jsx-runtime.js'\nimport type { JSXNode, JSXElement, JSXKey, FC } from './types.js'\n\nexport type * "
  },
  {
    "path": "src/jsx/intrinsic-elements.ts",
    "chars": 57429,
    "preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\n/* eslint-disable @typescript-eslint/no-empty-interface */\n/**\n "
  },
  {
    "path": "src/jsx/jsx-runtime.ts",
    "chars": 2495,
    "preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\n/* eslint-disable @typescript-eslint/no-empty-interface */\n/**\n "
  },
  {
    "path": "src/jsx/types.ts",
    "chars": 2495,
    "preview": "/**\n * @file\n * These types are adapted from React v19.1\n *\n * @see {@link https://github.com/DefinitelyTyped/Definitely"
  },
  {
    "path": "src/language.ts",
    "chars": 2855,
    "preview": "// This function guesses the human language (writing system) of the given\n// JavaScript string, using the Unicode Alias "
  },
  {
    "path": "src/layout.ts",
    "chars": 9105,
    "preview": "/**\n * This module is used to calculate the layout of the current sub-tree.\n */\n\nimport type { ReactNode } from 'react'\n"
  },
  {
    "path": "src/parser/mask.ts",
    "chars": 1075,
    "preview": "import { getPropertyName } from 'css-to-react-native'\nimport { splitEffects } from '../utils.js'\n\nfunction getMaskProper"
  },
  {
    "path": "src/parser/shape.ts",
    "chars": 5431,
    "preview": "import { lengthToNumber } from '../utils.js'\nimport { default as buildBorderRadius } from '../builder/border-radius.js'\n"
  },
  {
    "path": "src/satori.ts",
    "chars": 7464,
    "preview": "import type { ReactNode } from 'react'\nimport type { TwConfig } from 'twrnc'\nimport type { SatoriNode } from './layout.j"
  },
  {
    "path": "src/text/characters.ts",
    "chars": 286,
    "preview": "export function stringFromCode(code: string): string {\n  code = code.replace('U+', '0x')\n\n  return String.fromCodePoint("
  },
  {
    "path": "src/text/index.ts",
    "chars": 29507,
    "preview": "/**\n * This module calculates the layout of a text string. Currently the only\n * supported inline node is text. All othe"
  },
  {
    "path": "src/text/measurer.ts",
    "chars": 1194,
    "preview": "import { FontEngine } from '../font.js'\nimport { segment } from '../utils.js'\n\nexport function genMeasurer(\n  engine: Fo"
  },
  {
    "path": "src/text/processor.ts",
    "chars": 4280,
    "preview": "import { Locale } from '../language.js'\nimport { isNumber, segment, splitByBreakOpportunities } from '../utils.js'\nimpor"
  },
  {
    "path": "src/transform-origin.ts",
    "chars": 2861,
    "preview": "import valueParser from 'postcss-value-parser'\n\nimport CssDimension from './vendor/parse-css-dimension/index.js'\n\n/**\n *"
  },
  {
    "path": "src/types.d.ts",
    "chars": 62,
    "preview": "declare module '@shuding/opentype.js' {\n  export = opentype\n}\n"
  },
  {
    "path": "src/utils.ts",
    "chars": 10867,
    "preview": "import type { ReactNode, ReactElement } from 'react'\nimport LineBreaker from 'linebreak'\n\nimport CssDimension from './ve"
  },
  {
    "path": "src/vendor/parse-css-dimension/LICENSE",
    "chars": 1074,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2015 Jed Mao\n\nPermission is hereby granted, free of charge, to any person obtaining"
  },
  {
    "path": "src/vendor/parse-css-dimension/index.js",
    "chars": 1456,
    "preview": "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\","
  },
  {
    "path": "src/vendor/parse-css-dimension/package.json",
    "chars": 453,
    "preview": "{\n  \"name\": \"parse-css-dimension\",\n  \"version\": \"0.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \""
  },
  {
    "path": "src/vendor/parse-css-dimension/src.js",
    "chars": 2133,
    "preview": "var cssLengthUnits = require('css-length-units')\nvar cssAngleUnits = require('css-angle-units')\nvar cssResolutionUnits ="
  },
  {
    "path": "src/vendor/twrnc/deprecate.js",
    "chars": 136,
    "preview": "module.exports = function deprecate(fn, message) {\n  return function (...args) {\n    console.warn(message)\n    return fn"
  },
  {
    "path": "src/vendor/twrnc/log.js",
    "chars": 311,
    "preview": "export default {\n  info(key, messages) {\n    console.info(...(Array.isArray(key) ? [key] : [messages, key]))\n  },\n  warn"
  },
  {
    "path": "src/vendor/twrnc/picocolors.js",
    "chars": 39,
    "preview": "export default {\n  yellow: (s) => s,\n}\n"
  },
  {
    "path": "src/yoga.bundled.ts",
    "chars": 151,
    "preview": "import { loadYoga } from 'yoga-layout/load'\n\n// Always preload Yoga.\nconst loadingYoga = loadYoga()\nexport function getY"
  },
  {
    "path": "src/yoga.external.ts",
    "chars": 2712,
    "preview": "import { loadYoga as loadYogaUntyped, type Yoga } from 'yoga-layout/load'\n\nconst loadYoga = loadYogaUntyped as (options:"
  },
  {
    "path": "src/yoga.ts",
    "chars": 750,
    "preview": "import { type Yoga } from 'yoga-layout/load'\nimport { type Node } from 'yoga-layout'\nimport { type InitInput } from './y"
  },
  {
    "path": "test/background-clip.test.tsx",
    "chars": 3027,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/basic.test.tsx",
    "chars": 5130,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/benchmark/index.ts",
    "chars": 8863,
    "preview": "import { run, bench, summary } from 'mitata'\nimport { join } from 'path'\nimport { readFileSync } from 'fs'\n\nimport { Res"
  },
  {
    "path": "test/border.test.tsx",
    "chars": 7587,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/box-sizing.test.tsx",
    "chars": 1710,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { toImage } from './utils.js'\nimport satori from '../src/index.js'"
  },
  {
    "path": "test/clip-path.test.tsx",
    "chars": 3343,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/color-models.test.tsx",
    "chars": 8390,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/css-variables.test.tsx",
    "chars": 7429,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/display-contents.test.tsx",
    "chars": 4612,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/display.test.tsx",
    "chars": 956,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { toImage } from './utils.js'\nimport satori from '../src/index.js'"
  },
  {
    "path": "test/dynamic-size.test.tsx",
    "chars": 839,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/embed-font.test.tsx",
    "chars": 2157,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts } from './utils.js'\nimport satori from '../src/index.j"
  },
  {
    "path": "test/emoji.test.tsx",
    "chars": 29157,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/error.test.tsx",
    "chars": 3238,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts } from './utils.js'\nimport satori from '../src/index.j"
  },
  {
    "path": "test/event.test.tsx",
    "chars": 1442,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts } from './utils.js'\nimport satori from '../src/index.j"
  },
  {
    "path": "test/flexbox-advanced.test.tsx",
    "chars": 18023,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/font.test.tsx",
    "chars": 4013,
    "preview": "import { join } from 'node:path'\nimport { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './ut"
  },
  {
    "path": "test/gap.test.tsx",
    "chars": 2257,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { toImage } from './utils.js'\nimport satori from '../src/index.js'"
  },
  {
    "path": "test/gradient.test.tsx",
    "chars": 18128,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/image.test.tsx",
    "chars": 46091,
    "preview": "import { it, describe, expect, beforeEach, afterEach } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nim"
  },
  {
    "path": "test/jsx-runtime.test.tsx",
    "chars": 1993,
    "preview": "// TODO: use `#satori/jsx` as import source after upgradine vitest.\n/** @jsxRuntime automatic */\n/** @jsxImportSource .."
  },
  {
    "path": "test/language.test.tsx",
    "chars": 3654,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/layout.test.tsx",
    "chars": 543,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/letter-spacing.test.tsx",
    "chars": 8143,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/line-clamp.test.tsx",
    "chars": 5127,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/line-height.test.tsx",
    "chars": 858,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/margin.test.tsx",
    "chars": 12807,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/mask-image.test.tsx",
    "chars": 8981,
    "preview": "import { it, describe, expect, beforeEach, afterEach } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nim"
  },
  {
    "path": "test/opacity.test.tsx",
    "chars": 9693,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/overflow.test.tsx",
    "chars": 3930,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/padding.test.tsx",
    "chars": 11666,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/pixel-font.test.tsx",
    "chars": 2536,
    "preview": "import { it, describe, expect } from 'vitest'\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../src"
  },
  {
    "path": "test/position.test.tsx",
    "chars": 2947,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/react.test.tsx",
    "chars": 719,
    "preview": "import { forwardRef } from 'react'\nimport { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './"
  },
  {
    "path": "test/shadow.test.tsx",
    "chars": 8113,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/svg.test.tsx",
    "chars": 8977,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/tab-size.test.tsx",
    "chars": 5750,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/text-align.test.tsx",
    "chars": 4965,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/text-decoration.test.tsx",
    "chars": 7972,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, loadDynamicAsset, toImage } from './utils.js'\nimport "
  },
  {
    "path": "test/text-indent.test.tsx",
    "chars": 9667,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/text-wrap.test.tsx",
    "chars": 1270,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/transform.test.tsx",
    "chars": 5218,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/typesetting.test.tsx",
    "chars": 1166,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/units.test.tsx",
    "chars": 3677,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/utils.tsx",
    "chars": 1695,
    "preview": "import { beforeAll, expect } from 'vitest'\nimport { join } from 'path'\nimport { Resvg } from '@resvg/resvg-js'\nimport { "
  },
  {
    "path": "test/webkit-text-stroke.test.tsx",
    "chars": 2218,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/white-space.test.tsx",
    "chars": 7273,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, toImage } from './utils.js'\nimport satori from '../sr"
  },
  {
    "path": "test/word-break.test.tsx",
    "chars": 4622,
    "preview": "import { it, describe, expect } from 'vitest'\n\nimport { initFonts, loadDynamicAsset, toImage } from './utils.js'\nimport "
  },
  {
    "path": "tsconfig.json",
    "chars": 275,
    "preview": "{\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"module\": \"node16\",\n    \"moduleResolution\": \"node16\",\n    \"esModule"
  },
  {
    "path": "tsup.config.ts",
    "chars": 2170,
    "preview": "import { defineConfig } from 'tsup'\nimport { join } from 'path'\nimport { replace } from 'esbuild-plugin-replace'\n\nconst "
  },
  {
    "path": "turbo.json",
    "chars": 112,
    "preview": "{\n  \"$schema\": \"https://turbo.build/schema.json\",\n  \"pipeline\": {\n    \"dev\": {\n      \"cache\": false\n    }\n  }\n}\n"
  },
  {
    "path": "vitest.config.ts",
    "chars": 183,
    "preview": "import path from 'path'\nimport { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n  test: {\n    covera"
  },
  {
    "path": "vitest.jsx-runtime.config.ts",
    "chars": 283,
    "preview": "import { defineConfig, mergeConfig } from 'vitest/config'\nimport vitestConfig from './vitest.config'\n\nexport default mer"
  }
]

// ... and 10 more files (download for full content)

About this extraction

This page contains the full source code of the vercel/satori GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 154 files (778.7 KB), approximately 229.5k tokens, and a symbol index with 367 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!