<!--
{% set project_name='excalidraw-brute-export-cli' %}
{% set last_stable_release=shell("node -p \"require('./package.json').version\"",
                                 include_args=False) | trim %}
{% set last_unstable_release=last_stable_release %}
{% set node_versions=shell("node -p \"require('./package.json').engines.node\"",
                                 include_args=False) | trim %}
{% set tested_node_versions=shell('python -m yq -c \'.jobs["build-and-test"]["strategy"]["matrix"]["node-version"]\' .github/workflows/build-and-test.yml',
                                 include_args=False) | trim %}
{% set badge_color='0A1E1E' %}
-->

# <div align="center">![Excalidraw Brute Export CLI][1]</div>

<div align="center">

<!-- Icons from https://lucide.dev/icons/users -->
<!-- Icons from https://lucide.dev/icons/laptop-minimal -->

![**Audience:** Developers][4] ![**Platform:** Linux][5]

</div>

<p align="center">
  <strong>
    <a href="#-features">🎇Features</a> &nbsp;&bull;&nbsp;
    <a href="#-installation">🏠Installation</a> &nbsp;&bull;&nbsp;
    <a href="#-usage">🚜Usage</a> &nbsp;&bull;&nbsp;
    <a href="#-command-line-options">💻CLI</a>
  </strong>
</p>
<p align="center">
  <strong>
    <a href="#-requirements">✅Requirements</a>
    &nbsp;&bull;&nbsp;
    <a href="#-docker-image">🐳Docker</a>
    &nbsp;&bull;&nbsp;
    <a href="#-gotchas-and-limitations">🚸Gotchas</a>
  </strong>
</p>

<div align="center">

![Top language][6] [![GitHub License][7]][8] [![npm - version][9]][10]

[![Node Version][24]][25]

**Export Excalidraw diagrams to SVG or PNG using a headless browser, using the
exact same export process as Excalidraw itself**

</div>

---

<div align="center">

|                   | Status                      | Stable                    | Unstable                  |                    |
| ----------------- | --------------------------- | ------------------------- | ------------------------- | ------------------ |
| **[Master][11]**  | [![Build and Test][13]][15] | [![since tagged][16]][18] |                           | ![last commit][22] |
| **[Develop][12]** | [![Build and Test][14]][15] | [![since tagged][17]][19] | [![since tagged][20]][21] | ![last commit][23] |

</div>

<img src=".github/demo.gif" alt="Demo" width="100%">

- ❔ What: Uses [🎭 playwright][2] to run a headless firefox browser to export
  [Excalidraw][3] diagrams to svg/png files. Using a browser bypasses certain
  bugs that happen with other projects that attempt to export by emulating the
  DOM (without a browser).
- Why:
  - To allow automated export of Excalidraw diagrams to svg/png files via the
    command line.
  - Currently, Excalidraw can only be exported by a human clicking on the
    "Export image" button.
  - Addresses/mitigates
    [excalidraw/excalidraw#1261](https://github.com/excalidraw/excalidraw/issues/1261)
    `excalidraw CLI #1261` which is an open feature request on the Excalidraw
    project.
  - Addresses/mitigates
    [JRJurman/excalidraw-to-svg#6](https://github.com/JRJurman/excalidraw-to-svg/issues/6)
    `Error rendering edge-labels. #6`.
  - Addresses/mitigates
    [Timmmm/excalidraw_export#6](https://github.com/Timmmm/excalidraw_export/issues/6)
    `Error rendering edge-labels. #6`.
- 🤝 Related Projects
  - ⭐⭐⭐⭐⭐
    [JRJurman/excalidraw-to-svg](https://github.com/JRJurman/excalidraw-to-svg)
    uses jsdom to simulate the DOM, then runs Excalidraw+react in nodejs, loads
    the diagram files and exports them.
    - Comparison: `JRJurman/excalidraw-to-svg` is faster and more efficient than
      {{project_name}}. However, excalidraw-brute-export-cli is a "brute force"
      approach to exporting Excalidraw diagrams, and in _some ways_ might be
      more reliable.
  - ⭐⭐⭐⭐⭐
    [Timmmm/excalidraw_export](https://github.com/Timmmm/excalidraw_export)
    similar to `JRJurman/excalidraw-to-svg` but simplifies the code and also
    embeds SVG fonts.
    - Comparison: `Timmmm/excalidraw_export` is faster and more efficient than
      {{project_name}}. However, excalidraw-brute-export-cli is a "brute force"
      approach to exporting Excalidraw diagrams, and in _some ways_ might be
      more reliable.

## 🎇 Features

- Export Excalidraw diagrams to SVG or PNG using a headless browser, using the
  exact same export process as Excalidraw itself.
- Can point to any excalidraw instance.
- Ability to change timeouts.
- Debugging: Ability to take screenshots at each step.

## 🏠 Installation

```bash
# Install globally from npm registry.
npm install -g excalidraw-brute-export-cli

# Or install globally, direct from GitHub:
npm install -g https://github.com/realazthat/excalidraw-brute-export-cli.git#v{{last_stable_release}}

# Might prompt for root.
npx playwright install-deps
npx playwright install firefox
```

## 🚜 Usage

Example:

<!--{{snippet('./examples/simple_example.sh',
              start='# SNIPPET_START',
              end='\n# SNIPPET_END',
              backtickify='bash',
              decomentify='nl')|trim}}-->

<!--{{ shell('cat ./.github/simple_example.log',
               start=': ECHO_SNIPPET_START',
               end='^.*: ECHO_SNIPPET_END',
               regex='MULTILINE',
               rich='README.simple_example.log.svg',
               rich_alt='Output of `bash ./examples/simple_example.sh`',
               rich_bg_color='black',
               rich_term='xterm-256color',
               rich_cols=160,
               include_args=False,
               decomentify='nl') }}-->

And the resulting image (svg):

<img src="./examples/simple_example_output.svg" alt="Simple Excalidraw Diagram as a SVG" width="400" />

## 💻 Command Line Options

<!--{{ shell('npx excalidraw-brute-export-cli --help',
             rich='README.help.generated.svg',
             rich_alt='Output of `npx excalidraw-brute-export-cli --help`',
             rich_bg_color='black',
             rich_cols=120,
             decomentify=True) }}-->

## 🐳 Running Excalidraw locally

- <https://excalidraw.com> is a moving target as it gets updated. You can
  alternatively run your own excalidraw at a specific git tag, and point to it
  with the `--url` option, for more reproducible results.

  - Unfortunately, as of `2024/05/05` the Excalidraw project doesn't regularly
    update its docker image (See
    <https://hub.docker.com/r/excalidraw/excalidraw/tags>,
    [excalidraw/issues/7893](https://github.com/excalidraw/excalidraw/issues/7893),
    [excalidraw/issues/7403](https://github.com/excalidraw/excalidraw/issues/7403)).
  - Unfortunately, as of `2024/05/05` the Excalidraw project's Dockerfile is
    frequently broken (See
    [excalidraw/issues/7582](https://github.com/excalidraw/excalidraw/issues/7582),
    [excalidraw/pull/7806](https://github.com/excalidraw/excalidraw/pull/7806),
    [excalidraw/pull/7430](https://github.com/excalidraw/excalidraw/pull/7430),
    [excalidraw/pull/7502](https://github.com/excalidraw/excalidraw/pull/7502)).
    - According to
      [excalidraw/issues/7582#issuecomment-1900651112](https://github.com/excalidraw/excalidraw/issues/7582#issuecomment-1900651112)
      `v0.15.0` (from `2023/04/18`) is the last tag that builds.

- Here is how to run your own Excalidraw instance at Excalidraw tag
  `{{rawsnippet('./.github/.excalidraw-tag')|trim}}`:

```bash
# First build the image. Do this once.
git clone https://github.com/excalidraw/excalidraw.git
cd excalidraw
git checkout "{{rawsnippet('./.github/.excalidraw-tag')|trim}}"
docker build -t "my-excalidraw-image:{{rawsnippet('./.github/.excalidraw-tag')|trim}}" .

# Now we'll run Excalidraw in a container instance.

# Delete the old instance if it exists.
docker rm -f "your-instance-name" || true

# Replace 59876 with your desired port.
docker run -dit --name "your-instance-name" -p 59876:80 "my-excalidraw-image:{{rawsnippet('./.github/.excalidraw-tag')|trim}}"

# Visit your instance at http://localhost:59876

# Use the --url option to point to your instance.
npx excalidraw-brute-export-cli \
  ...
  --url "http://localhost:59876" \
  ...

# Or, set `EXCALIDRAW_BRUTE_EXPORT_CLI_URL` in your environment and leave out
# the --url option.
```

## ✅ Requirements

- Tested with latest version of <https://excalidraw.com> as of `2024/05/05`, and
  Excalidraw tag `{{rawsnippet('./.github/.excalidraw-tag')|trim}}` for more
  consistent output, and testing.
- Supported Node versions: `{{ node_versions }}` (See
  {{path('./package.json', link='md')}}). These versions were chosen from
  current supported and upcoming versions of node, from
  [Node.js: Previous Releases](https://nodejs.org/en/about/previous-releases).
- Tested Node versions on GitHub Actions: `{{ tested_node_versions }}`.

### Tested on

- WSL2 Ubuntu 20.04, Node `{{rawsnippet('./.nvmrc')|trim}}` using Excalidraw at
  tag `{{rawsnippet('./.github/.excalidraw-tag')|trim}}`.

## 🐳 Docker Image

Docker images are published to [ghcr.io/realazthat/{{project_name}}][49] at each
tag.

<!--{{snippet('./examples/simple-remote-docker_example-noautorun.sh',
              start='# SNIPPET_START',
              end='\n# SNIPPET_END',
              backtickify='bash',
              decomentify='nl')|trim}}-->

If you want to build the image yourself, you can use the Dockerfile in the
repository.

<!--{{snippet('./examples/simple-local-docker_example.sh',
              start='# SNIPPET_START',
              end='\n# SNIPPET_END',
              backtickify='bash',
              decomentify='nl')|trim}}-->

## 🚸 Gotchas and Limitations

- Sometimes playwright times out.
  - Mitigations:
    - Increase the timeout with the `--timeout` option.
    - Run the command again.
  - If this is a persistent problem, please open an issue
    [here](https://github.com/realazthat/excalidraw-brute-export-cli/issues/new)
    and upload the diagram (zip it if necessary).

## 🤏 Versioning

We use SemVer for versioning. For the versions available, see the tags on this
repository.

## 🔑 License

This project is licensed under the MIT License - see the
{{path('./LICENSE.md', link='md')}} file for details.

## 🫡 Contributions

### Development environment: Linux-like

- For running `pre.sh` (Linux-like environment).

  - From {{path('./.github/dependencies.yml', link='md')}}, which is used for
    the GH Action to do a fresh install of everything:

    {{shell('python -m yq --yaml-output  \'.dev\' .github/dependencies.yml',
    include_args=False,
    backtickify='yaml',
    indented=4
    )}}

  - Requires `pyenv`, or an exact matching version of python as in
    {{path('scripts/.python-version', link='md')}} (which is currently
    `{{ rawsnippet('scripts/.python-version') }}`).
  - `jq`, ([installation](https://jqlang.github.io/jq/)) required for
    [yq](https://github.com/kislyuk/yq), which is itself required for our
    {{path('./README.md', link='md')}} generation, which uses `tomlq` (from the
    [yq](https://github.com/kislyuk/yq) package) to include version strings from
    {{path('./scripts/pyproject.toml', link='md')}}.
  - act (to run the GH Action locally):
    - Requires nodejs.
    - Requires Go.
    - docker.
  - Generate animation:
    - docker
  - Tests
    - docker, to serve Excalidraw.

### Commit Process

1. (Optionally) Fork the `develop` branch.
2. If the {{path('.github/demo.gif', link='md')}} will change, run
   `bash ./scripts/generate-animation.sh`, this will generate a new
   {{path('.github/demo.gif', link='md')}}.
   - Sanity-check the animation visually!
   - Unfortunately, every run will make a unique gif, please don't stage this
     file unless it changes due to some feature change or somesuch.
3. Stage your files: e.g `git add path/to/file.py`.
4. `bash ./scripts/pre.sh`, this will format, lint, and test the code.
5. `git status` check if anything changed (generated
   {{path('README.md', link='md')}} for example), if so, `git add` the changes,
   and go back to the previous step.
6. `git commit -m "..."`.
7. Make a PR to `develop` (or push to develop if you have the rights).

## 🔄🚀 Release Process

These instructions are for maintainers of the project.

1. In the `develop` branch, run `bash {{path('./scripts/pre.sh')}}` to ensure
   everything is in order.
2. In the `develop` branch, bump the version in
   {{path('package.json', link='md')}}, following semantic versioning
   principles. Run `bash ./scripts/pre.sh` to ensure everything is in order.
   - If anything got generated (e.g README or terminal output images), you will
     have to stage those.
3. In the `develop` branch, commit these changes with a message like
   `"Prepare release X.Y.Z"`. (See the contributions section
   [above](#commit-process)).
4. Merge the `develop` branch into the `master` branch:
   `git checkout master && git merge develop --no-ff`.
5. `master` branch: Tag the release: Create a git tag for the release with
   `git tag -a vX.Y.Z -m "Version X.Y.Z"`.
6. Publish to NPM: Publish the release to NPM with
   `bash ./scripts/deploy-to-npm.sh`.
7. Push to GitHub: Push the commit and tags to GitHub with
   `git push && git push --tags`.
8. The `--no-ff` option adds a commit to the master branch for the merge, so
   refork the develop branch from the master branch:
   `git checkout develop && git merge master`.
9. Push the develop branch to GitHub: `git push origin develop`.

[1]: ./.github/logo-exported.svg
[2]: https://playwright.dev/
[3]: https://excalidraw.com/

<!-- Logo from https://lucide.dev/icons/users -->

[4]:
  https://img.shields.io/badge/Audience-Developers|Users-{{badge_color}}?style=plastic&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXVzZXJzIj48cGF0aCBkPSJNMTYgMjF2LTJhNCA0IDAgMCAwLTQtNEg2YTQgNCAwIDAgMC00IDR2MiIvPjxjaXJjbGUgY3g9IjkiIGN5PSI3IiByPSI0Ii8+PHBhdGggZD0iTTIyIDIxdi0yYTQgNCAwIDAgMC0zLTMuODciLz48cGF0aCBkPSJNMTYgMy4xM2E0IDQgMCAwIDEgMCA3Ljc1Ii8+PC9zdmc+

<!-- Logo from https://lucide.dev/icons/laptop-minimal -->

[5]:
  https://img.shields.io/badge/Platform-Node-{{badge_color}}?style=plastic&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWxhcHRvcC1taW5pbWFsIj48cmVjdCB3aWR0aD0iMTgiIGhlaWdodD0iMTIiIHg9IjMiIHk9IjQiIHJ4PSIyIiByeT0iMiIvPjxsaW5lIHgxPSIyIiB4Mj0iMjIiIHkxPSIyMCIgeTI9IjIwIi8+PC9zdmc+
[6]:
  https://img.shields.io/github/languages/top/realazthat/{{project_name}}.svg?&cacheSeconds=28800&style=plastic&color={{badge_color}}
[7]:
  https://img.shields.io/github/license/realazthat/{{project_name}}?style=plastic&color={{badge_color}}
[8]: ./LICENSE.md
[9]:
  https://img.shields.io/npm/v/{{project_name}}?style=plastic&color={{badge_color}}
[10]: https://www.npmjs.com/package/{{project_name}}
[11]: https://github.com/realazthat/{{project_name}}/tree/master
[12]: https://github.com/realazthat/{{project_name}}/tree/develop
[13]:
  https://img.shields.io/github/actions/workflow/status/realazthat/{{project_name}}/build-and-test.yml?branch=master&style=plastic
[14]:
  https://img.shields.io/github/actions/workflow/status/realazthat/{{project_name}}/build-and-test.yml?branch=develop&style=plastic
[15]:
  https://github.com/realazthat/{{project_name}}/actions/workflows/build-and-test.yml
[16]:
  https://img.shields.io/github/commits-since/realazthat/{{project_name}}/v{{last_stable_release}}/master?style=plastic
[17]:
  https://img.shields.io/github/commits-since/realazthat/{{project_name}}/v{{last_stable_release}}/develop?style=plastic
[18]:
  https://github.com/realazthat/{{project_name}}/compare/v{{last_stable_release}}...master
[19]:
  https://github.com/realazthat/{{project_name}}/compare/v{{last_stable_release}}...develop
[20]:
  https://img.shields.io/github/commits-since/realazthat/{{project_name}}/v{{last_unstable_release}}/develop?style=plastic
[21]:
  https://github.com/realazthat/{{project_name}}/compare/v{{last_unstable_release}}...develop
[22]:
  https://img.shields.io/github/last-commit/realazthat/{{project_name}}/master?style=plastic
[23]:
  https://img.shields.io/github/last-commit/realazthat/{{project_name}}/develop?style=plastic
[24]:
  https://img.shields.io/node/v/excalidraw-brute-export-cli?style=plastic&color={{badge_color}}
[25]: https://www.npmjs.com/package/excalidraw-brute-export-cli
