Compare commits

..
Author SHA1 Message Date
Sidharth Vinod c5d9cc8996 Fix types 2022-01-21 13:45:56 +05:30
193 changed files with 7931 additions and 14954 deletions
+1 -10
View File
@@ -2,13 +2,4 @@
**/.git
**/.svelte-kit
**/dist
**/docs
**/.github
**/.husky
**/.vscode
Dockerfile
.dockerignore
docker-compose.yml
README.md
**/docs
-7
View File
@@ -1,7 +0,0 @@
MERMAID_DOMAIN=''
MERMAID_ANALYTICS_URL=''
MERMAID_RENDERER_URL='https://mermaid.ink'
MERMAID_KROKI_RENDERER_URL='https://kroki.io'
MERMAID_IS_ENABLED_MERMAID_CHART_LINKS=''
# cp .env .env.local to make local changes
-8
View File
@@ -1,8 +0,0 @@
docs/**
.svelte-kit/**
static/**
build/**
node_modules/**
coverage/**
__snapshots__/**
snapshots.js
+46 -119
View File
@@ -1,121 +1,48 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
// 'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/strict',
'plugin:unicorn/recommended',
'plugin:svelte/recommended',
'plugin:svelte/prettier',
'prettier'
],
plugins: [
'tailwindcss',
'@typescript-eslint',
'es',
'vitest',
'no-only-tests',
'sort-keys',
'unicorn'
],
ignorePatterns: [
'docs/*',
'*.cjs',
'*.js',
'*.md',
'snapshots.js',
'svelte.config.js',
'renovate.json',
'package.json',
'tsconfig.json'
],
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
},
{
files: ['*.ts'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/strict',
'prettier'
]
},
{
files: ['**/components/ui/**'],
rules: {
'unicorn/prefer-export-from': 'off',
'unicorn/prevent-abbreviations': 'off',
'unicorn/explicit-length-check': 'off',
'sort-keys/sort-keys-fix': 'off'
}
}
],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
tsconfigRootDir: __dirname,
project: './tsconfig.json',
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2020: true
},
rules: {
'sort-keys/sort-keys-fix': ['error', 'asc', { minKeys: 5 }],
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-ignore': 'allow-with-description'
}
],
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'es/no-regexp-lookbehind-assertions': 'error',
curly: ['error', 'all'],
'no-only-tests/no-only-tests': 'error',
'unicorn/no-null': 'off',
'unicorn/filename-case': [
'error',
{
case: 'camelCase'
}
],
'unicorn/filename-case': 'off',
'unicorn/prevent-abbreviations': [
'error',
{
allowList: {
args: true,
ctx: true,
db: true,
doc: true,
env: true,
fn: true,
i: true,
j: true,
k: true,
param: true,
Params: true,
params: true,
Props: true,
props: true,
req: true,
res: true,
str: true,
temp: true,
ImportMetaEnv: true
}
}
]
}
root: true,
parser: '@typescript-eslint/parser',
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'prettier'
],
plugins: ['svelte3', 'tailwindcss', '@typescript-eslint', 'es'],
ignorePatterns: [
'docs/*',
'*.cjs',
'*.md',
'snapshots.js',
'svelte.config.js',
'renovate.json',
'package.json',
'tsconfig.json'
],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2019,
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
rules: {
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-ignore': 'allow-with-description'
}
],
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'es/no-regexp-lookbehind-assertions': 'error'
}
};
-5
View File
@@ -1,5 +0,0 @@
# .git-blame-ignore-revs
# Prettier Pass
6fde9e22a3ba0b80879a1909fb233367ef20b738
# Tabs -> Spaces
0ac606b8fbf6f94215cbda1d4511e2dcd30e4655
-1
View File
@@ -1 +0,0 @@
github: [sidharthv96, knsv]
+1 -1
View File
@@ -12,6 +12,6 @@ Describe the way your implementation works or what design decisions you made if
Make sure you
- [ ] :book: have read the [contribution guidelines](https://mermaid.js.org/community/contributing.html)
- [ ] :book: have read the [contribution guidelines](https://github.com/mermaid-js/mermaid/blob/master/CONTRIBUTING.md)
- [ ] :computer: have added unit/e2e tests (if appropriate)
- [ ] :bookmark: targeted `develop` branch
+3 -3
View File
@@ -35,11 +35,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v1
with:
config-file: ./.github/codeql/config.yml
languages: ${{ matrix.language }}
@@ -65,4 +65,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v1
+46
View File
@@ -0,0 +1,46 @@
name: Cypress Tests
on:
pull_request:
branches:
- master
- develop
jobs:
cypress-run:
runs-on: ubuntu-latest
# Update after https://github.com/cypress-io/cypress-docker-images/issues/487
container: cypress/browsers:node14.16.0-chrome90-ff88
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/cache@v2
id: yarn-and-build-cache
with:
path: |
~/.cache/Cypress
build
node_modules
key: ${{ runner.os }}-node_modules-build-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-node_modules-build-
- name: Lint
run: |
yarn install
yarn run lint
# Install NPM dependencies, cache them correctly
# and run all Cypress tests
- name: Cypress run
uses: cypress-io/github-action@v2
with:
build: yarn build
start: yarn preview
wait-on: 'http://localhost:3000'
record: true
headless: true
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+29 -38
View File
@@ -11,52 +11,43 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/setup-node@v4
- uses: actions/cache@v2
id: yarn-cache
with:
node-version-file: '.node-version'
cache: pnpm
- name: Setup Pages
uses: actions/configure-pages@v5
with:
static_site_generator: sveltekit
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Build & Deploy
env:
MERMAID_DOMAIN: 'mermaid.live'
MERMAID_ANALYTICS_URL: 'https://p.mermaid.live'
MERMAID_RENDERER_URL: 'https://mermaid.ink'
MERMAID_KROKI_RENDERER_URL: 'https://kroki.io'
MERMAID_IS_ENABLED_MERMAID_CHART_LINKS: 'true'
run: |
npm i -g yarn
export DEPLOY=true
[ "$GITHUB_EVENT_NAME" == "pull_request" ] && export BETA=true && rm -rf docs/beta/
[ "$GITHUB_EVENT_NAME" != "pull_request" ] && rm -rf docs/_app/
pnpm install
pnpm build
yarn install
yarn run lint
version=$(yarn version --patch --no-git-tag-version | grep "New version" | cut -d':' -f 2)
yarn build
cd bin
./fix-path
cd ..
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
if: ${{ github.ref == 'refs/heads/master' }}
with:
path: ./docs
deploy:
if: ${{ github.ref == 'refs/heads/master' }}
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
permissions:
pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs
keep_files: true
+52 -31
View File
@@ -2,11 +2,13 @@ name: Docker
on:
push:
# Publish `master` as Docker `latest` image.
branches:
# Publish `master` as Docker `latest` image.
- master
# Publish `develop` as Docker `nightly` image.
- develop
# Publish `v1.2.3` tags as releases.
tags:
- v*
# Run tests for all PRs to master and develop.
pull_request:
@@ -14,35 +16,54 @@ on:
- master
- develop
env:
IMAGE_NAME: mermaid-live-editor
jobs:
docker:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get release version
- uses: actions/checkout@v2
- name: Run tests
run: |
RELEASE_VERSION=$([ "${{ github.ref_name }}" = "master" ] && echo "latest" || echo "nightly")
echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "${GITHUB_ENV}"
- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=${{ env.RELEASE_VERSION }}
- uses: docker/build-push-action@v5
with:
context: .
target: mermaid
push: ${{ github.event_name == 'push' }}
pull: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
docker build . --file Dockerfile
push:
# Ensure test job passes before pushing image.
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v2
- name: Build image
run: docker build . --file Dockerfile --tag $IMAGE_NAME
- name: Log into registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push image
run: |
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Use Docker `latest` tag convention
[ "$VERSION" == "master" ] && VERSION=latest
echo IMAGE_ID=$IMAGE_ID
echo VERSION=$VERSION
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
docker push $IMAGE_ID:$VERSION
-21
View File
@@ -1,21 +0,0 @@
name: Create release pull request
on:
push:
branches:
- develop
jobs:
productionPromotion:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: master
- name: Reset promotion branch
run: |
git fetch origin develop:develop
git reset --hard develop
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
branch: release-promotion
title: Release live editor
+17
View File
@@ -0,0 +1,17 @@
name: Mark stale issues and pull requests
on:
schedule:
- cron: '0 0 * * 4'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v4
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
exempt-issue-labels: 'retained'
exempt-pr-labels: 'retained'
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days'
stale-pr-message: 'This pr is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days'
-54
View File
@@ -1,54 +0,0 @@
name: Tests
on:
pull_request:
branches:
- master
- develop
jobs:
playwright:
name: 'Playwright Tests'
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.52.0-jammy
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/cache@v4
id: pnpm-and-build-cache
with:
path: |
build
node_modules
key: ${{ runner.os }}-node_modules-build-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-node_modules-build-
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.node-version'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Run Playwright tests
run: pnpm test:e2e
env:
CI: true
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
-42
View File
@@ -1,42 +0,0 @@
name: Unit Tests
on:
pull_request:
branches:
- master
- develop
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/cache@v3
id: pnpm-and-build-cache
with:
path: |
build
node_modules
key: ${{ runner.os }}-node_modules-build-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-node_modules-build-
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.node-version'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Lint
run: pnpm lint
- name: Run unit tests
run: pnpm test:unit
+10 -26
View File
@@ -1,32 +1,16 @@
name: Update Browserslist database
name: Update Browserslist
on:
schedule:
- cron: '0 2 1,15 * *'
permissions:
contents: write
pull-requests: write
workflow_dispatch:
jobs:
update-browserslist-database:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- uses: actions/checkout@v2
- run: npx browserslist@latest --update-db
- name: Commit changes
uses: EndBug/add-and-commit@v7
with:
fetch-depth: 0
- name: Configure git
run: |
git config --global user.email "action@github.com"
git config --global user.name "GitHub Action"
- name: Update Browserslist database and create PR if applies
uses: c2corg/browserslist-update-action@v2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: browserslist-update
base_branch: develop
commit_message: 'build: update Browserslist db'
title: 'build: update Browserslist db'
body: Auto-generated by [browserslist-update-action](https://github.com/c2corg/browserslist-update-action/)
labels: 'chores, github action'
author_name: ${{ github.actor }}
author_email: ${{ github.actor }}@users.noreply.github.com
message: 'Update Browserslist'
@@ -0,0 +1,20 @@
name: Update Monaco-editor
on:
workflow_dispatch:
pull_request:
types: [opened]
branches:
- 'dependabot/npm_and_yarn/develop/monaco-editor-**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: ./bin/update-monaco
- name: Commit changes
uses: EndBug/add-and-commit@v7
with:
author_name: ${{ github.actor }}
author_email: ${{ github.actor }}@users.noreply.github.com
message: 'Update Monaco-editor'
+7 -11
View File
@@ -1,17 +1,13 @@
.DS_Store
node_modules/
coverage/
.cache/
.env.local
build/
yarn-error.log
.npmrc
.DS_Store
/.svelte-kit
/build
/coverage
/docs
/functions
/node_modules
/snapshots.js
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/cypress/videos
/cypress/screenshots
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
pnpm pre-commit
yarn pre-commit
-1
View File
@@ -1 +0,0 @@
22.15.0
-1
View File
@@ -1,2 +1 @@
engine-strict=true
auto-install-peers=true
+1 -4
View File
@@ -3,7 +3,4 @@ docs/**
static/**
build/**
node_modules/**
coverage/**
__snapshots__/**
snapshots.js
pnpm-lock.yaml
snapshots.js
+6 -7
View File
@@ -1,9 +1,8 @@
{
"singleQuote": true,
"svelteSortOrder": "options-scripts-markup-styles",
"bracketSameLine": true,
"trailingComma": "none",
"printWidth": 100,
"tailwindConfig": "./tailwind.config.js",
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"]
"singleQuote": true,
"svelteSortOrder": "options-scripts-markup-styles",
"bracketSameLine": true,
"useTabs": true,
"trailingComma": "none",
"printWidth": 100
}
-15
View File
@@ -1,15 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}
+2 -48
View File
@@ -1,50 +1,4 @@
{
"editor.formatOnSave": true,
"cSpell.blockCheckingWhenLineLengthGreaterThan": 150,
"cSpell.words": [
"appinstalled",
"asyncable",
"Browserslist",
"ckppp",
"corg",
"cssnano",
"esserializer",
"fontawesome",
"fsegurai",
"gantt",
"gitgraph",
"KROKI",
"localstorage",
"mermaidchart",
"mindmap",
"Pageview",
"pako",
"panmove",
"panstart",
"panzoom",
"pinchmove",
"pinchstart",
"pzoom",
"roughjs",
"Serde",
"serdes",
"Stackable",
"tailwindcss",
"uparrow",
"webfonts",
"zenuml"
],
"vitest.commandLine": "pnpm test:unit",
"vitest.enable": true,
"testing.autoRun.mode": "rerun",
"svelte.enable-ts-plugin": true,
"githubPullRequests.ignoredPullRequestBranches": ["develop"],
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode"
},
"tailwindCSS.classAttributes": ["class", "className", ".*Classes"],
"tailwindCSS.experimental.classRegex": [
["([\"'`][^\"'`]*.*?[\"'`])", "[\"'`]([^\"'`]*).*?[\"'`]"]
],
"tailwindCSS.emmetCompletions": true
"editor.formatOnSave": true,
"cSpell.words": ["pako", "Serde", "serdes"]
}
-1
View File
@@ -1 +0,0 @@
mermaid.live
+15 -33
View File
@@ -1,34 +1,16 @@
FROM docker.io/library/node:22.15.0-alpine3.21 AS mermaid-live-editor-dependencies
# Two-stage docker container for mermaid-js/mermaid-live-editor
# Build : docker build -t mermaid-js/mermaid-live-editor .
# Run : docker run --name mermaid-live-editor --publish 8080:80 mermaid-js/mermaid-live-editor
# Start : docker start mermaid-live-editor
# Use webbrowser : http://localhost:8080
# Stop : press ctrl + c
# or
# docker stop mermaid-live-editor
FROM node:17.3.1 as mermaid-live-editor-builder
COPY --chown=node:node . /home
WORKDIR /home
RUN yarn install
RUN yarn build
RUN apk --no-cache add build-base git python3 && \
rm -rf /var/cache/apk/*
RUN corepack enable pnpm
WORKDIR /app
COPY ./package.json .
COPY ./pnpm-lock.yaml .
RUN pnpm install
FROM mermaid-live-editor-dependencies AS mermaid-live-editor-builder
ARG MERMAID_RENDERER_URL
ARG MERMAID_KROKI_RENDERER_URL
ARG MERMAID_ANALYTICS_URL
ARG MERMAID_DOMAIN
ARG MERMAID_IS_ENABLED_MERMAID_CHART_LINKS
COPY . ./
RUN pnpm build
FROM mermaid-live-editor-builder AS mermaid-dev
ENTRYPOINT ["pnpm", "dev"]
FROM nginx:1.28-alpine3.21 AS mermaid
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=mermaid-live-editor-builder /app/docs /usr/share/nginx/html
FROM nginx:alpine as mermaid-live-editor-runner
COPY --from=mermaid-live-editor-builder --chown=nginx:nginx /home/docs /usr/share/nginx/html
+8
View File
@@ -0,0 +1,8 @@
FROM node:17
WORKDIR /app
COPY package.json .
COPY yarn.lock .
RUN npm install
COPY . .
RUN ls
CMD ["yarn", "dev"]
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 - 2023 Knut Sveidqvist
Copyright (c) 2020 - 2021 Knut Sveidqvist
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+19 -67
View File
@@ -1,7 +1,10 @@
[![Join our Discord!](https://img.shields.io/static/v1?message=join%20chat&color=9cf&logo=discord&label=discord)](https://discord.gg/sKeNQX4Wtj)
[![Netlify Status](https://api.netlify.com/api/v1/badges/27fa023d-7c73-4a3f-9791-b3b657a47100/deploy-status)](https://app.netlify.com/sites/mermaidjs/deploys)
[![Mermaid Live Editor](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/detailed/2ckppp/master&style=flat&logo=cypress)](https://dashboard.cypress.io/projects/2ckppp/runs) [![Join our Slack!](https://img.shields.io/static/v1?message=join%20chat&color=9cf&logo=slack&label=slack)](https://join.slack.com/t/mermaid-talk/shared_invite/enQtNzc4NDIyNzk4OTAyLWVhYjQxOTI2OTg4YmE1ZmJkY2Y4MTU3ODliYmIwOTY3NDJlYjA0YjIyZTdkMDMyZTUwOGI0NjEzYmEwODcwOTE)[![Netlify Status](https://api.netlify.com/api/v1/badges/27fa023d-7c73-4a3f-9791-b3b657a47100/deploy-status)](https://app.netlify.com/sites/mermaidjs/deploys)
# Mermaid Live Editor
# Contributors are welcome!
If you want to speed up the progress for mermaid-live-editor, join the slack channel and contact knsv.
# mermaid-live-editor
Edit, preview and share mermaid charts/diagrams.
@@ -14,52 +17,25 @@ Edit, preview and share mermaid charts/diagrams.
## Live demo
You can try out a [live version](https://mermaid.live/).
# Contributors are welcome!
If you want to speed up the progress for mermaid-live-editor, join the Discord channel and contact knsv.
You can try out a live version [here](https://mermaid.live/).
## Docker
### Run published image
```bash
docker run --platform linux/amd64 --publish 8000:8080 ghcr.io/mermaid-js/mermaid-live-editor
docker run --publish 8000:80 ghcr.io/mermaid-js/mermaid-live-editor
```
### To configure renderer URL
When building set the MERMAID_RENDERER_URL build argument to the rendering
service.
Example:
Default is`https://mermaid.ink`.
Set to empty string to disable PNG and SVG links under Actions
When building, Set the Environment variable MERMAID_RENDERER_URL to the rendering service.
Default is `https://mermaid.ink`
### To configure Kroki Instance URL
When building set the MERMAID_KROKI_RENDERER_URL build argument to your Kroki
instance.
When building, Set the Environment variable MERMAID_KROKI_RENDERER_URL to your Kroki instance.
Default is `https://kroki.io`
Set to empty string to disable Kroki link under Actions
### To configure Analytics
When building set the MERMAID_ANALYTICS_URL build argument to your plausible instance, and MERMAID_DOMAIN to your domain.
Default is empty, disabling analytics.
### To enable Mermaid Chart links and promotion
When building set the MERMAID_IS_ENABLED_MERMAID_CHART_LINKS build argument to `true`
Default is empty, disabling button to save to Mermaid Chart and promotional banner.
### To update the Security modal
The modal shown on clicking the security link assumes analytics, renderer, Kroki
and Mermaid chart are enabled. You can update it by modifying `Privacy.svelte`
if you wish.
### Development
@@ -67,46 +43,22 @@ if you wish.
docker compose up --build
```
Then open http://localhost:3000
### Building and running images locally
#### Build
```bash
docker build -t mermaid-js/mermaid-live-editor .
```
#### Run
```bash
docker run --detach --name mermaid-live-editor --publish 8080:8080 mermaid-js/mermaid-live-editor
```
Visit: <http://localhost:8080>
#### Stop
```bash
docker stop mermaid-live-editor
```
Then open http://localhost:8000
## Setup
Below link will help you making a copy of the repository in your local system.
[Volta](https://volta.sh) is used for managing node and yarn versions.
https://docs.github.com/en/get-started/quickstart/fork-a-repo
This project is set up using [Yarn](https://classic.yarnpkg.com/en/docs/getting-started):
## Requirements
- [Node.js](https://nodejs.org/en/) current LTS version
- [pnpm](https://pnpm.io/) package manager. Install with `corepack enable pnpm`
```
yarn install
```
## Development
```sh
pnpm install
pnpm dev -- --open
```
yarn dev -- --open
```
This app is created with Svelte Kit.
-13
View File
@@ -1,13 +0,0 @@
# Security Policy
## Supported Versions
Live editor only has a single version, which will be maintained.
| Version | Supported |
| ------- | ------------------ |
| latest | :white_check_mark: |
## Reporting a Vulnerability
To report a vulnerability, please e-mail security@mermaid.live with a description of the issue, the steps you took to create the issue, affected versions, and if known, mitigations for the issue.
+1 -1
View File
@@ -6,7 +6,7 @@
# git clone https://github.com/mermaid-js/docs.git
set -e
rm -rf docs
pnpm release
yarn release
pushd .
if [ ! -d ../docs ]; then
echo "Clone https://github.com/mermaid-js/docs to parent folder before continuing."
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
monacoVersion="$(jq -r '.dependencies."monaco-editor"' package.json)"
monacoVersion="${monacoVersion:1}"
if [[ $(uname) == "Darwin" ]];
then
sed -i '' -E "s/monaco-editor\/[^/]*/monaco-editor\/$monacoVersion/g" ./src/app.html
else
sed -i'' -E "s/monaco-editor\/[^/]*/monaco-editor\/$monacoVersion/g" ./src/app.html
fi
-45
View File
@@ -1,45 +0,0 @@
import fs from 'fs';
import path from 'path';
import { parse } from 'node-html-parser';
import prettier from 'prettier';
// parse monaco version out of package.json
const packageJson = JSON.parse(fs.readFileSync('package.json'));
const monacoVersion = packageJson.dependencies['monaco-editor'].replace('^', '');
// fetch monaco sri info from cdnjs api
const cdnjsAPIResp = await fetch(
`https://api.cdnjs.com/libraries/monaco-editor/${monacoVersion}?fields=sri`
);
if (cdnjsAPIResp.ok) {
const respJson = await cdnjsAPIResp.json();
const htmlPath = path.join('src', 'app.html');
const appHtml = fs
.readFileSync(htmlPath, 'utf8')
// update monaco version of every asset in app.html
.replaceAll(/[0-9.]+\/min\/vs/g, `${monacoVersion}/min/vs`);
const root = parse(appHtml);
const updateIntegrity = (tag, attr) => {
for (const node of root
.getElementsByTagName(tag)
.filter((node) => node.getAttribute(attr)?.includes('monaco-editor'))) {
const file = node.getAttribute(attr).split(`${monacoVersion}/`)[1];
node.setAttribute('integrity', respJson.sri[file]);
}
};
updateIntegrity('script', 'src');
updateIntegrity('link', 'href');
fs.writeFileSync(
htmlPath,
prettier.format(root.toString(), {
singleQuote: false,
parser: 'html',
bracketSameLine: true,
useTabs: true
})
);
} else {
throw Error('Unable to fetch monaco sri data from cdnjs api.');
}
-17
View File
@@ -1,17 +0,0 @@
{
"$schema": "https://next.shadcn-svelte.com/schema.json",
"style": "new-york",
"tailwind": {
"config": "tailwind.config.js",
"css": "src/app.postcss",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks"
},
"typescript": true,
"registry": "https://next.shadcn-svelte.com/registry"
}
+11
View File
@@ -0,0 +1,11 @@
{
"projectId": "2ckppp",
"$schema": "https://on.cypress.io/cypress.schema.json",
"baseUrl": "http://localhost:3000",
"pluginsFile": "./cypress/plugins/index.cjs",
"viewportWidth": 1440,
"viewportHeight": 768,
"snapshotFileName": "./cypress/snapshots.js",
"defaultCommandTimeout": 16000,
"requestTimeout": 16000
}
+12
View File
@@ -0,0 +1,12 @@
{
"plugins": ["cypress", "mocha"],
"extends": ["plugin:cypress/recommended", "plugin:mocha/recommended"],
"rules": {
"jest/expect-expect": "off",
"mocha/no-mocha-arrows": "off"
},
"env": {
"cypress/globals": true,
"mocha": true
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
+24
View File
@@ -0,0 +1,24 @@
describe('Check actions', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/edit');
});
it('should update markdown code', () => {
cy.get('#markdown')
.invoke('val')
.then((oldText) => {
cy.get('#editor').click('bottom').type('{enter}C --> HistoryTest');
cy.get('#markdown')
.invoke('val')
.then((newText) => {
expect(oldText).to.not.eq(newText);
});
});
});
it('should load gists from URL', () => {
cy.get('#gist').type('https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a');
cy.contains('Load Gist').click();
cy.contains('Go shopping!!');
});
});
+33
View File
@@ -0,0 +1,33 @@
describe('Auto sync tests', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/');
});
it('should dim diagram when code is edited', () => {
cy.contains('Auto sync').click();
cy.get('#view').should('not.have.class', 'outOfSync');
cy.get('#editor').type(' C --> Test');
cy.get('#view').should('have.class', 'outOfSync');
cy.getLocalStorage('codeStore').snapshot();
});
it('should show/hide sync button with auto sync', () => {
cy.get('[data-cy=sync]').should('not.exist');
cy.contains('Auto sync').click();
cy.get('[data-cy=sync]').should('exist');
cy.get('#autoSync').check();
cy.get('[data-cy=sync]').should('not.exist');
});
it('should not dim diagram when code is in sync', () => {
cy.contains('Auto sync').click();
cy.get('#view').should('not.have.class', 'outOfSync');
cy.get('#editor').type(' C --> Test');
cy.get('#view').should('have.class', 'outOfSync');
cy.get('[data-cy=sync]').click();
cy.get('#view').should('not.have.class', 'outOfSync');
cy.get('#autoSync').check();
cy.get('#editor').type('ing');
cy.get('#view').should('not.have.class', 'outOfSync');
cy.getLocalStorage('codeStore').snapshot();
});
});
+63
View File
@@ -0,0 +1,63 @@
describe('Save History', () => {
beforeEach(() => {
cy.clock();
cy.clearLocalStorage();
cy.visit('/edit');
cy.contains('Actions').click();
cy.contains('History').click();
});
it('should save when clicked', () => {
cy.get('#historyList').find('li').should('have.length', 0);
cy.get('#historyList').contains('No items in History');
cy.get('#saveHistory').click();
cy.get('#historyList').find('No items in History').should('not.exist');
cy.get('#historyList').find('li').should('have.length', 1);
cy.get('#saveHistory').click();
cy.on('window:alert', (str) => {
expect(str).to.equal('State already saved.');
});
cy.on('window:confirm', () => true);
cy.get('#editor').type(' C --> HistoryTest');
cy.get('#saveHistory').click();
cy.get('#historyList').find('li').should('have.length', 2);
});
it('should be able to restore and delete', () => {
cy.get('#saveHistory').click();
cy.get('#editor').type(' C --> HistoryTest');
cy.get('#historyList').find('No items in History').should('not.exist');
cy.get('#historyList').find('li').should('have.length', 1);
cy.contains('HistoryTest');
cy.contains('Restore').click();
cy.contains('HistoryTest').should('not.exist');
cy.contains('Delete').click();
cy.get('#historyList').find('li').should('have.length', 0);
cy.get('#historyList').contains('No items in History');
cy.get('#saveHistory').click();
cy.get('#editor').type(' C --> HistoryTest');
cy.get('#saveHistory').click();
cy.get('#editor').type('ing');
cy.get('#clearHistory').click();
cy.on('window:alert', (str) => {
expect(str).to.equal('Clear all saved items?');
});
cy.on('window:confirm', () => true);
cy.get('#historyList').contains('No items in History');
});
it('should auto save history', () => {
cy.get('#editor').type(' C --> HistoryTest');
cy.tick(70000);
cy.contains('Timeline').click();
cy.get('#historyList').find('li').should('have.length', 1);
cy.get('#editor').type('ing');
cy.tick(70000);
cy.get('#historyList').find('li').should('have.length', 2);
for (let i = 0; i < 31; i++) {
cy.get('#editor').type('.');
cy.tick(70000);
}
cy.get('#historyList').find('li').should('have.length', 30);
});
});
+123
View File
@@ -0,0 +1,123 @@
import { toBase64 } from 'js-base64';
describe('Site Loads', () => {
beforeEach(() => {
cy.clearLocalStorage();
});
it('Check Home page load', () => {
cy.visit('/');
cy.url().should('include', '/edit');
cy.contains('History').click();
cy.getLocalStorage('codeStore').snapshot();
});
it('Check Redirect from old URL', () => {
cy.visit(
'/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZylcbiAgICBCIC0tPiBDe0xldCBtZSB0aGlua31cbiAgICBDIC0tPnxPbmV8IERbTGFwdG9wXVxuICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdXG4gICAgQyAtLT58VGhyZWV8IEZbZmE6ZmEtY2FyIENhcl0iLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ'
);
cy.url().should(
'include',
'/edit#eyJjb2RlIjoiZ3JhcGggVERcbiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZylcbiAgICBCIC0tPiBDe0xldCBtZSB0aGlua31cbiAgICBDIC0tPnxPbmV8IERbTGFwdG9wXVxuICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdXG4gICAgQyAtLT58VGhyZWV8IEZbZmE6ZmEtY2FyIENhcl0iLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ'
);
cy.contains('History').click();
cy.getLocalStorage('codeStore').snapshot();
});
it('should load sample diagrams when clicked', () => {
cy.contains('Sample Diagrams').click();
cy.contains('Pie Chart').click();
cy.contains('pie title Pets adopted by volunteers');
cy.contains('Class Diagram').click();
cy.contains('classDiagram');
});
it('should load diagram from gist', () => {
cy.visit(`/edit?gist=https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a`);
cy.contains('History').click();
cy.contains('Go shopping!!');
cy.contains('Revisions');
cy.contains('sidharthv96 v8f8f1e2');
cy.contains('sidharthv96 v7851e19');
cy.getLocalStorage('codeStore').snapshot();
});
it('should load diagram from gist revision', () => {
cy.visit(
'/edit?gist=https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/ec9b4ab0e41e4ff6287326cd3cb47affd7851e19'
);
cy.contains('History').click();
cy.contains('Party');
cy.contains('Revisions');
cy.contains('sidharthv96 v7851e19');
cy.getLocalStorage('codeStore').snapshot();
});
it('should load diagram from raw files', () => {
cy.visit(
'/edit?code=https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/code.mmd&config=https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/config.json'
);
cy.contains('Party');
cy.getLocalStorage('codeStore').snapshot();
});
// Disabled temporarily. Should be enabled after the issue is fixed in Mermaid.
// it('should prevent setting the "securityLevel" option via URL', () => {
// const b64State = toBase64(
// `{"code":"graph TD\\nA[\\"<img src='https://via.placeholder.com/64' width=64 />\\"]","mermaid":"{\\"securityLevel\\": \\"loose\\", \\"theme\\": \\"forest\\"}","updateEditor":true,"autoSync":true,"updateDiagram":true}`,
// true
// );
// cy.on('window:confirm', () => true);
// cy.visit(`/edit#${b64State}`);
// cy.contains('Config').click();
// cy.contains('forest');
// cy.contains('securityLevel').should('not.exist');
// cy.get('#view').find('img').should('not.exist');
// cy.get('#view').contains('<img');
// cy.get('#view').contains(`src='https://via.placeholder.com/64'`);
// });
it('should allow persisting "securityLevel" using confirm dialogue', () => {
const b64State = toBase64(
`{"code":"graph TD\\nA[\\"<img src='https://via.placeholder.com/64' width=64/>\\"]","mermaid":"{\\"securityLevel\\": \\"loose\\", \\"theme\\": \\"forest\\"}","updateEditor":true,"autoSync":true,"updateDiagram":true}`,
true
);
cy.on('window:confirm', () => false);
cy.visit(`/edit#${b64State}`);
cy.contains('Config').click();
cy.contains('forest');
cy.contains('securityLevel');
cy.get('#view').find('img').should('be.visible');
});
it('should show troubleshooting steps if loading fails', () => {
cy.visit('/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAg');
cy.contains('Please Click here to Raise an issue in github.');
});
it('should load uncompressed URL', () => {
cy.visit(
'/edit/#eyJjb2RlIjoiZ3JhcGggVERcbiAgICBBW05ldyBZZWFyXSAtLT58R2V0IG1vbmV5fCBCKEdvIHNob3BwaW5nKVxuICAgIEIgLS0-IEN7TGV0IG1lIHRoaW5rfVxuICAgIEMgLS0-fE9uZXwgRFtMYXB0b3BdXG4gICAgQyAtLT58VHdvfCBFW2lQaG9uZV1cbiAgICBDIC0tPnxUaHJlZXwgRltmYTpmYS1jYXIgQ2FyXSIsIm1lcm1haWQiOiJ7XG4gIFwidGhlbWVcIjogXCJkZWZhdWx0XCJcbn0iLCJ1cGRhdGVFZGl0b3IiOmZhbHNlLCJhdXRvU3luYyI6dHJ1ZSwidXBkYXRlRGlhZ3JhbSI6ZmFsc2V9'
);
cy.contains('New Year');
cy.visit(
'/edit#eyJjb2RlIjoiY2xhc3NEaWFncmFtXG4gICAgQW5pbWFsIDx8LS0gRHVja1xuICAgIEFuaW1hbCA8fC0tIEZpc2hcbiAgICBBbmltYWwgPHwtLSBaZWJyYVxuICAgIEFuaW1hbCA6ICtpbnQgYWdlXG4gICAgQW5pbWFsIDogK1N0cmluZyBnZW5kZXJcbiAgICBBbmltYWw6ICtpc01hbW1hbCgpXG4gICAgQW5pbWFsOiArbWF0ZSgpXG4gICAgY2xhc3MgRHVja3tcbiAgICAgICtTdHJpbmcgYmVha0NvbG9yXG4gICAgICArc3dpbSgpXG4gICAgICArcXVhY2soKVxuICAgIH1cbiAgICBjbGFzcyBGaXNoe1xuICAgICAgLWludCBzaXplSW5GZWV0XG4gICAgICAtY2FuRWF0KClcbiAgICB9XG4gICAgY2xhc3MgWmVicmF7XG4gICAgICArYm9vbCBpc193aWxkXG4gICAgICArcnVuKClcbiAgICB9XG4gICAgICAgICAgICAiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGFya1wiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ'
);
cy.contains('Animal');
cy.visit(
'/edit/#base64:eyJjb2RlIjoiZ3JhcGggVERcbiAgICBBW05ldyBZZWFyXSAtLT58R2V0IG1vbmV5fCBCKEdvIHNob3BwaW5nKVxuICAgIEIgLS0-IEN7TGV0IG1lIHRoaW5rfVxuICAgIEMgLS0-fE9uZXwgRFtMYXB0b3BdXG4gICAgQyAtLT58VHdvfCBFW2lQaG9uZV1cbiAgICBDIC0tPnxUaHJlZXwgRltmYTpmYS1jYXIgQ2FyXSIsIm1lcm1haWQiOiJ7XG4gIFwidGhlbWVcIjogXCJkZWZhdWx0XCJcbn0iLCJ1cGRhdGVFZGl0b3IiOmZhbHNlLCJhdXRvU3luYyI6dHJ1ZSwidXBkYXRlRGlhZ3JhbSI6ZmFsc2V9'
);
cy.contains('New Year');
});
it('should load compressed URL', () => {
cy.visit(
'/edit#pako:eNpVkM2KwkAQhF-l6dMK5gVyEDRxvYi7sF6WjIcm0zqDzg_jBJEk725Hd2G3Tw31VVFUj23QjCWeEkUD-1p5kFs2O77BN1M6QFEshg1ncMHzfYDV2ybA1YQYrT_NXvhqgqDqtxPGkI315_ElVU__h-cB6mZLMYd4-Kvsb2GAdWM_jcT_V0xicb03RyqPVLSUoJI-OEfHyZHV0rqfDAqzYccKS3k1pbNC5Ufhuqgp81rbHBJKxuXKc6Quh6-7b7HMqeNfqLYkC7gfanwAlW1ZvQ'
);
cy.contains('New Year');
cy.visit(
'/edit#pako:eNptkU1PwzAMhv9K5BOI9Q9EXBDbJA477YYqITcxndV8QD40weh_Jy1rGR0-OY_tV2_sEyivCSQogzGuGduAtnaixINji0bcf1WVWGfVXdMtx8M1faYm4B8sxR27JLClJd6nwK4VLTlN4bI4jMQd2pLe3C4KFhNNcLQ92jv9ADGLNoTdozc-zIV4ZDsNlud7RtVN7_5Sb_jYrFcN3iN_0pPbEqUZK3QbTP_Ojyv4NdR4bwTHlyMbPcOQ3WJ2CliBpWCRdbnLqFJDOpClGmRJNYauhtr1pS-_6bKMjebkA8hXNJFWgDn5_YdTIFPINDWdb3vu6r8BaWOZRQ'
);
cy.reload();
cy.contains('Animal');
});
});
+57
View File
@@ -0,0 +1,57 @@
describe('Test themes', () => {
describe('Test light themes', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/edit', {
onBeforeLoad(win) {
cy.stub(win, 'matchMedia')
.callThrough()
.withArgs('(prefers-color-scheme: dark)')
.returns({
matches: false
});
}
});
cy.contains('Change Theme').click();
});
it('should set light theme as default', () => {
cy.contains('light').parent().should('have.class', 'bordered');
cy.contains('dark').parent().should('not.have.class', 'bordered');
cy.getLocalStorage('themeStore').snapshot();
});
it('should change themes when clicked', () => {
cy.contains('light').parent().should('have.class', 'bordered');
cy.contains('cupcake').click();
cy.contains('cupcake').parent().should('have.class', 'bordered');
cy.contains('light').parent().should('not.have.class', 'bordered');
cy.contains('dark').parent().should('not.have.class', 'bordered');
cy.getLocalStorage('themeStore').snapshot();
});
});
describe('Test dark mode', () => {
// eslint-disable-next-line mocha/no-hooks-for-single-case
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/edit', {
onBeforeLoad(win) {
cy.stub(win, 'matchMedia')
.callThrough()
.withArgs('(prefers-color-scheme: dark)')
.returns({
matches: true
});
}
});
cy.contains('Change Theme').click();
});
it('should set dark theme as default', () => {
cy.contains('light').parent().should('not.have.class', 'bordered');
cy.contains('dark').parent().should('have.class', 'bordered');
cy.getLocalStorage('themeStore').snapshot();
});
});
});
+31
View File
@@ -0,0 +1,31 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
import * as fs from 'fs';
module.exports = (on, config) => {
on('task', {
readFileMaybe(filename) {
if (fs.existsSync(filename)) {
return fs.readFileSync(filename, 'utf8');
}
return null;
}
});
};
+52
View File
@@ -0,0 +1,52 @@
module.exports = {
"Site Loads": {
"Check Home page load": {
"1": "{\"code\":\"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"updateEditor\":true,\"autoSync\":true,\"updateDiagram\":true}"
},
"Check Redirect from old URL": {
"1": "{\"code\":\"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"updateEditor\":false,\"autoSync\":true,\"updateDiagram\":true}"
},
"should load diagram from gist": {
"1": "{\"code\":\"graph TD\\n A[Party] -->|Get money| B(Go shopping!!)\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"forest\\\",\\n \\\"test\\\": \\\"hello world\\\"\\n}\",\"updateEditor\":false,\"autoSync\":true,\"updateDiagram\":true,\"loader\":{\"type\":\"gist\",\"config\":{\"url\":\"https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a\"}}}"
},
"should load diagram from gist revision": {
"1": "{\"code\":\"graph TD\\n A[Party] -->|Get money| B(Go shopping)\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"forest\\\",\\n \\\"test\\\": \\\"hello\\\"\\n}\",\"updateEditor\":false,\"autoSync\":true,\"updateDiagram\":true,\"loader\":{\"type\":\"gist\",\"config\":{\"url\":\"https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/ec9b4ab0e41e4ff6287326cd3cb47affd7851e19\"}}}"
},
"should load diagram from raw files": {
"1": "{\"code\":\"graph TD\\n A[Party] -->|Get money| B(Go shopping!!)\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"forest\\\",\\n \\\"test\\\": \\\"hello world\\\"\\n}\",\"updateEditor\":false,\"autoSync\":true,\"updateDiagram\":true,\"loader\":{\"type\":\"files\",\"config\":{\"codeURL\":\"https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/code.mmd\",\"configURL\":\"https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/config.json\"}}}"
}
},
"__version": "9.2.0",
"Auto sync tests": {
"should dim diagram when code is edited": {
"1": "{\"code\":\"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\\n C --> Test\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"updateEditor\":false,\"autoSync\":false,\"updateDiagram\":false}"
},
"should not dim diagram when code is in sync": {
"1": "{\"code\":\"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\\n C --> Testing\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"updateEditor\":false,\"autoSync\":true,\"updateDiagram\":false}"
}
},
"Test themes": {
"should set light theme as default": {
"1": "{\"theme\":\"light\",\"isDark\":false}"
},
"should change themes when clicked": {
"1": "{\"theme\":\"cupcake\",\"isDark\":false}"
},
"should set dark theme as default": {
"1": "{\"theme\":\"dark\",\"isDark\":true}"
},
"Test light themes": {
"should set light theme as default": {
"1": "{\"theme\":\"light\",\"isDark\":false}"
},
"should change themes when clicked": {
"1": "{\"theme\":\"cupcake\",\"isDark\":false}"
}
},
"Test dark mode": {
"should set dark theme as default": {
"1": "{\"theme\":\"dark\",\"isDark\":true}"
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import { register } from '@cypress/snapshot';
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
register();
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
snapshot(): void;
}
}
}
import 'cypress-localstorage-commands';
+27
View File
@@ -0,0 +1,27 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')
Cypress.on('uncaught:exception', (err) => {
/* returning false here prevents Cypress from failing the test */
if (err.message.includes('ResizeObserver loop limit exceeded')) {
return false;
}
});
+7
View File
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"allowJs": true,
"types": ["cypress", "cypress-localstorage-commands"]
},
"include": ["**/*.ts"]
}
+2 -2
View File
@@ -3,9 +3,9 @@ services:
mermaid:
build:
context: .
target: mermaid-dev
dockerfile: Dockerfile.dev
volumes:
- ./src:/app/src
ports:
- 3000:3000
- 8080:8080
- 24678:24678
-12
View File
@@ -1,12 +0,0 @@
[build.environment]
MERMAID_ANALYTICS_URL = 'https://p.mermaid.live'
MERMAID_DOMAIN = 'mermaid.live'
MERMAID_RENDERER_URL = 'https://mermaid.ink'
MERMAID_KROKI_RENDERER_URL = 'https://kroki.io'
MERMAID_IS_ENABLED_MERMAID_CHART_LINKS ='true'
[[redirects]]
from = "/index.html"
to = "/edit"
status = 301
force = true
-9
View File
@@ -1,9 +0,0 @@
server {
listen 8080;
server_name mermaid;
location / {
root /usr/share/nginx/html;
# Fallback to index.html for other paths
try_files $uri /index.html;
}
}
+73 -129
View File
@@ -1,131 +1,75 @@
{
"name": "mermaid-live-editor",
"version": "2.0.67",
"type": "module",
"license": "MIT",
"scripts": {
"dev": "vite dev",
"dev:force": "MERMAID_LOCAL=true pnpm dev --force",
"dev:test": "pnpm dev",
"build": "vite build",
"preview": "vite preview",
"lint": "prettier --check --cache . && eslint --ignore-path .gitignore .",
"lint:fix": "prettier --write --cache . && eslint --fix --ignore-path .gitignore .",
"format": "prettier --write --cache .",
"pre-commit": "lint-staged",
"postinstall": "husky install && svelte-kit sync && (git config blame.ignoreRevsFile .git-blame-ignore-revs || true)",
"test:unit": "vitest",
"test:unit:ui": "vitest --ui",
"test:unit:coverage": "vitest run --coverage",
"test": "pnpm test:unit && pnpm test:e2e",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug"
},
"devDependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
"@iconify-json/material-symbols": "^1.2.20",
"@iconify-json/mdi": "^1.2.3",
"@playwright/test": "^1.52.0",
"@sveltejs/adapter-static": "3.0.8",
"@sveltejs/kit": "2.20.8",
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"@types/hammerjs": "^2.0.46",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.15.10",
"@types/pako": "2.0.3",
"@types/uuid": "9.0.8",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"@vitest/coverage-v8": "2.1.9",
"@vitest/ui": "^2.1.9",
"autoprefixer": "^10.4.21",
"bits-ui": "^1.4.6",
"c8": "7.14.0",
"chai": "^4.5.0",
"clsx": "^2.1.1",
"cssnano": "^6.1.2",
"eslint": "8.57.1",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-es": "^4.1.0",
"eslint-plugin-no-only-tests": "^3.3.0",
"eslint-plugin-postcss-modules": "^2.0.0",
"eslint-plugin-sort-keys": "^2.3.5",
"eslint-plugin-svelte": "^2.46.1",
"eslint-plugin-tailwindcss": "^3.18.0",
"eslint-plugin-unicorn": "^50.0.1",
"eslint-plugin-vitest": "^0.5.4",
"esserializer": "^1.3.11",
"husky": "^8.0.3",
"jsdom": "^25.0.1",
"lint-staged": "^15.5.1",
"lucide-svelte": "^0.507.0",
"node-html-parser": "^6.1.13",
"paneforge": "^1.0.0-next.5",
"postcss": "^8.5.3",
"postcss-load-config": "5.1.0",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.28.2",
"svelte-preprocess": "^6.0.3",
"svelte-sonner": "^0.3.28",
"tailwind-merge": "^3.2.0",
"tailwind-variants": "^0.3.1",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"unplugin-icons": "^22.1.0",
"vite": "^5.4.19",
"vitest": "^2.1.9",
"vitest-dom": "^0.1.1"
},
"dependencies": {
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.3.2",
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.11.0",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.36.7",
"@fontsource-variable/recursive": "^5.2.5",
"@fsegurai/codemirror-theme-vscode-dark": "^6.1.4",
"@fsegurai/codemirror-theme-vscode-light": "^6.1.4",
"@mermaid-js/layout-elk": "^0.1.7",
"@mermaid-js/mermaid-zenuml": "^0.2.0",
"codemirror": "^6.0.1",
"dayjs": "^1.11.13",
"hammerjs": "^2.0.8",
"js-base64": "3.7.7",
"lodash-es": "^4.17.21",
"mermaid": "^11.6.0",
"mode-watcher": "^0.5.1",
"monaco-editor": "0.52.2",
"pako": "2.1.0",
"plausible-tracker": "^0.3.9",
"random-word-slugs": "0.1.7",
"svg-pan-zoom": "3.6.2",
"svg2roughjs": "^3.2.1",
"uuid": "9.0.1"
},
"lint-staged": {
"*.{ts,svelte,js,css,md,json}": [
"prettier --plugin-search-dir=. --write",
"eslint --ignore-path .gitignore "
]
},
"engines": {
"node": ">=20.19.0"
},
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39",
"pnpm": {
"onlyBuiltDependencies": [
"deasync",
"esbuild",
"svelte-preprocess"
],
"ignoredBuiltDependencies": [
"vue-demi"
]
}
"name": "mermaid-live-editor",
"version": "2.0.67",
"scripts": {
"dev": "svelte-kit dev --host 0.0.0.0",
"build": "svelte-kit build",
"preview": "svelte-kit preview",
"lint": "prettier --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
"lint:fix": "prettier --write --plugin-search-dir=. . && eslint --fix --ignore-path .gitignore .",
"format": "prettier --write --plugin-search-dir=. .",
"pre-commit": "lint-staged",
"postinstall": "husky install",
"test": "cypress run",
"cy": "cypress open"
},
"devDependencies": {
"@cypress/snapshot": "^2.1.7",
"@sveltejs/adapter-static": "1.0.0-next.26",
"@sveltejs/kit": "1.0.0-next.232",
"@types/mermaid": "^8.2.7",
"@types/pako": "^1.0.3",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"autoprefixer": "^10.4.2",
"chai": "^4.3.4",
"cssnano": "^5.0.15",
"cypress": "9.3.1",
"cypress-localstorage-commands": "^1.6.1",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-es": "^4.1.0",
"eslint-plugin-mocha": "^10.0.3",
"eslint-plugin-postcss-modules": "^2.0.0",
"eslint-plugin-svelte3": "^3.4.0",
"eslint-plugin-tailwindcss": "^3.3.0",
"husky": "^7.0.4",
"lint-staged": "^12.2.1",
"mocha": "^9.1.4",
"node-html-parser": "^5.2.0",
"postcss": "^8.4.5",
"postcss-load-config": "^3.1.1",
"prettier": "~2.5.1",
"prettier-plugin-svelte": "^2.6.0",
"svelte": "^3.46.2",
"svelte-preprocess": "^4.10.2",
"tailwindcss": "^3.0.13",
"tslib": "^2.3.1",
"typescript": "^4.5.4"
},
"type": "module",
"dependencies": {
"@analytics/google-analytics": "^0.5.3",
"@macfja/svelte-persistent-store": "^1.2.0",
"analytics": "^0.7.21",
"daisyui": "1.24.3",
"js-base64": "^3.7.2",
"mermaid": "8.13.9",
"moment": "^2.29.1",
"monaco-editor": "^0.31.1",
"pako": "2.0.4",
"random-word-slugs": "^0.1.6"
},
"lint-staged": {
"*.{ts,svelte,js,css,md,json}": [
"prettier --plugin-search-dir=. --write",
"eslint --ignore-path .gitignore "
]
},
"volta": {
"node": "14.16.1",
"yarn": "1.22.10",
"npm": "7.11.2"
}
}
-28
View File
@@ -1,28 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
forbidOnly: !!process.env.CI,
fullyParallel: true,
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
}
],
reporter: process.env.CI ? [['github'], ['list']] : 'list',
retries: process.env.CI ? 2 : 0,
testDir: './tests',
use: {
baseURL: 'http://localhost:3000',
browserName: 'chromium',
permissions: ['clipboard-read', 'clipboard-write'],
trace: 'retain-on-failure',
viewport: { width: 1920, height: 1080 }
},
webServer: {
command: `pnpm ${process.env.CI ? 'preview' : 'dev'}`,
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI
},
workers: process.env.CI ? 3 : undefined
});
-7647
View File
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -6,18 +6,18 @@ const mode = process.env.NODE_ENV;
const dev = mode === 'development';
module.exports = {
plugins: [
// Some plugins, like postcss-nested, need to run before Tailwind
plugins: [
// Some plugins, like postcss-nested, need to run before Tailwind
tailwindcss,
tailwindcss,
// But others, like autoprefixer, need to run after
// But others, like autoprefixer, need to run after
autoprefixer,
autoprefixer,
!dev &&
cssnano({
preset: 'default'
})
]
!dev &&
cssnano({
preset: 'default'
})
]
};
+26 -27
View File
@@ -1,29 +1,28 @@
{
"extends": [
"config:base",
":rebaseStalePrs",
"group:allNonMajor",
"schedule:earlyMondays",
":automergeMinor",
":automergeTesters",
":automergeLinters",
":automergeTypes",
":automergePatch"
],
"packageRules": [
{
"matchPackagePatterns": ["^mermaid"],
"groupName": "Mermaid packages"
},
{
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
"automerge": true
}
],
"dependencyDashboard": true,
"major": {
"dependencyDashboardApproval": true
},
"dependencyDashboardAutoclose": true,
"rangeStrategy": "bump"
"extends": [
"config:base",
":rebaseStalePrs",
"group:allNonMajor",
"schedule:earlyMondays",
":automergeMinor",
":automergeTesters",
":automergeLinters",
":automergeTypes",
":automergePatch"
],
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
"automerge": true
},
{
"matchDatasources": ["npm"],
"stabilityDays": 3
}
],
"dependencyDashboard": true,
"major": {
"dependencyDashboardApproval": true
},
"dependencyDashboardAutoclose": true
}
View File
+45 -19
View File
@@ -1,21 +1,47 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Online FlowChart &amp; Diagrams Editor - Mermaid Live Editor</title>
<meta name="og:image" content="%sveltekit.assets%/favicon.svg" />
<link rel="canonical" href="https://mermaid.live" />
<meta
name="description"
content="Simplify documentation and avoid heavy tools. Open source Visio Alternative. Commonly used for explaining your code! Mermaid is a simple markdown-like script language for generating charts from text via javascript." />
<link rel="icon" type="image/png" href="%sveltekit.assets%/favicon.svg" />
<link rel="mask-icon" href="%sveltekit.assets%/favicon.svg" color="#000000" />
<meta name="theme-color" content="#ff3670" />
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
%sveltekit.head%
</head>
<body>
<div id="svelte">%sveltekit.body%</div>
</body>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Online FlowChart &amp; Diagrams Editor - Mermaid Live Editor</title>
<meta
name="og:image"
content="https://github.com/mermaid-js/mermaid/raw/develop/img/header.png" />
<link rel="canonical" href="https://mermaid.live" />
<meta
name="description"
content="Simplify documentation and avoid heavy tools. Open source Visio Alternative. Commonly used for explaining your code! Mermaid is a simple markdown-like script language for generating charts from text via javascript." />
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="manifest" href="manifest.json" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.31.1/min/vs/editor/editor.main.min.css"
crossorigin="anonymous"
referrerpolicy="no-referrer" />
<script>
var require = {
paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.31.1/min/vs' }
};
</script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.31.1/min/vs/loader.min.js"
crossorigin="anonymous"
referrerpolicy="no-referrer"></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.31.1/min/vs/editor/editor.main.nls.min.js"
crossorigin="anonymous"
referrerpolicy="no-referrer"></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.31.1/min/vs/editor/editor.main.js"
crossorigin="anonymous"
referrerpolicy="no-referrer"></script>
%svelte.head%
</head>
<body>
<div id="svelte">%svelte.body%</div>
</body>
</html>
+4 -62
View File
@@ -1,68 +1,10 @@
@import '@fontsource-variable/recursive/crsv.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 240 10% 91%;
--primary-foreground: 255 20% 15%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 228 24% 96%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 340 100% 44%;
--accent-foreground: 210 40% 98%;
--destructive: 0 72.22% 50.59%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.75rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 30%;
--primary-foreground: 222.2 47.4% 90%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 340 100% 44%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
.input {
@apply flex-1 border-primary border-solid border-2 rounded;
}
@layer base {
* {
@apply border-border;
}
body {
@apply h-screen w-screen overflow-hidden bg-background text-foreground;
}
}
body {
font-family: 'Recursive Variable', sans-serif;
}
.d {
@apply border border-red-500;
.action-btn {
@apply btn btn-primary;
}
-14
View File
@@ -1,14 +0,0 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly MERMAID_RENDERER_URL?: string;
readonly MERMAID_KROKI_RENDERER_URL?: string;
readonly MERMAID_ANALYTICS_URL?: string;
readonly MERMAID_DOMAIN?: string;
readonly MERMAID_IS_ENABLED_MERMAID_CHART_LINKS?: string;
// more env variables...
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
-6
View File
@@ -1,6 +0,0 @@
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
const response = await resolve(event, {});
return response;
};
+9
View File
@@ -0,0 +1,9 @@
import type { Handle } from '@sveltejs/kit/types/hooks';
export const handle: Handle = async ({ request, resolve }) => {
const response = await resolve(request, {
ssr: false
});
return response;
};
-341
View File
@@ -1,341 +0,0 @@
<script lang="ts">
import Card from '$/components/Card/Card.svelte';
import CopyButton from '$/components/CopyButton.svelte';
import CopyInput from '$/components/CopyInput.svelte';
import { Button } from '$/components/ui/button';
import { Input } from '$/components/ui/input';
import { Separator } from '$/components/ui/separator';
import * as ToggleGroup from '$/components/ui/toggle-group';
import { TID } from '$/constants';
import { env } from '$/util/env';
import { browser } from '$app/environment';
import { waitForRender } from '$lib/util/autoSync';
import { inputStateStore, stateStore, urlsStore } from '$lib/util/state';
import { logEvent } from '$lib/util/stats';
import { version as FAVersion } from '@fortawesome/fontawesome-free/package.json';
import dayjs from 'dayjs';
import { toBase64 } from 'js-base64';
import DownloadIcon from '~icons/material-symbols/download';
import ExternalLinkIcon from '~icons/material-symbols/open-in-new-rounded';
import WidthIcon from '~icons/material-symbols/width-rounded';
const fontAwesomeURLs = (() => {
const baseUrl = `https://cdnjs.cloudflare.com/ajax/libs/font-awesome/${FAVersion}`;
return {
css: `${baseUrl}/css/all.min.css`,
woff2: `${baseUrl}/webfonts/fa-solid-900.woff2`
};
})();
type Exporter = (context: CanvasRenderingContext2D, image: HTMLImageElement) => () => void;
const getFileName = (extension: string) =>
`mermaid-diagram-${dayjs().format('YYYY-MM-DD-HHmmss')}.${extension}`;
const getSvgElement = async () => {
try {
$inputStateStore.panZoom = false;
await new Promise((resolve) => setTimeout(resolve, 1000));
await waitForRender();
const svgElement = document.querySelector('#container svg');
if (!svgElement) {
throw new Error('svg not found');
}
svgElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
return {
svg: svgElement.cloneNode(true) as HTMLElement,
box: svgElement.querySelector('g')!.getBoundingClientRect()
};
} finally {
setTimeout(() => {
$inputStateStore.panZoom = true;
}, 10000);
}
};
const injectFontAwesome = async ({
svgString,
fontAwesomeEmbedMode
}: {
svgString: string;
fontAwesomeEmbedMode: 'raw' | 'url';
}) => {
if (fontAwesomeEmbedMode === 'url') {
return `<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="${fontAwesomeURLs.css}" type="text/css"?>
${svgString}`;
}
const [fontAwesomeCSS, fontBlob] = await Promise.all([
fetch(fontAwesomeURLs.css).then((response) => response.text()),
fetch(fontAwesomeURLs.woff2).then((res) => res.blob())
]);
const reader = new FileReader();
const fontBase64 = await new Promise<string>((resolve) => {
reader.onloadend = () => {
const base64 = reader.result as string;
// Remove the data URL prefix (data:application/octet-stream;base64,)
resolve(base64.split(',')[1]);
};
reader.readAsDataURL(fontBlob);
});
const styleString = `
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 900;
src: url(data:font/woff2;base64,${fontBase64}) format('woff2');
}
.fa {
font-family: 'Font Awesome 6 Free';
font-weight: 900;
// TODO: Make this dynamic from config
font-size: 16px;
width: 16px;
fill: black;
}
${fontAwesomeCSS}
`;
return svgString.replace('<style>', `<style>${styleString}`);
};
const getBase64SVG = async ({
svg,
box,
width,
height,
embedFontAwesome
}: {
svg?: HTMLElement;
box?: DOMRect;
width?: number;
height?: number;
embedFontAwesome?: boolean;
} = {}): Promise<string> => {
if (!svg || !box) {
({ svg, box } = await getSvgElement());
}
console.log(box);
// Prevents the SVG size of the interface from being changed
// svg = svg.cloneNode(true) as HTMLElement;
height && svg.setAttribute('height', `${height}px`);
width && svg.setAttribute('width', `${width}px`); // Workaround https://stackoverflow.com/questions/28690643/firefox-error-rendering-an-svg-image-to-html5-canvas-with-drawimage
svg.setAttribute('viewBox', `0 0 ${box.width} ${box.height}`);
const svgString = svg.outerHTML
.replaceAll('<br>', '<br/>')
.replaceAll(/<img([^>]*)>/g, (m, g: string) => `<img ${g} />`);
const svgStringWithFontAwesome = await injectFontAwesome({
svgString,
fontAwesomeEmbedMode: embedFontAwesome ? 'raw' : 'url'
});
console.log(svgStringWithFontAwesome);
return toBase64(svgStringWithFontAwesome);
};
const simulateDownload = (download: string, href: string): void => {
const a = document.createElement('a');
a.download = download;
a.href = href;
a.click();
a.remove();
};
const exportImage = async (event: Event, exporter: Exporter) => {
event.stopPropagation();
event.preventDefault();
const canvas = document.createElement('canvas');
const { svg, box } = await getSvgElement();
if (imageSizeMode === 'width') {
const ratio = box.height / box.width;
canvas.width = imageSize;
canvas.height = imageSize * ratio;
} else if (imageSizeMode === 'height') {
const ratio = box.width / box.height;
canvas.width = imageSize * ratio;
canvas.height = imageSize;
} else {
const multiplier = 2;
canvas.width = box.width * multiplier;
canvas.height = box.height * multiplier;
}
const context = canvas.getContext('2d');
if (!context) {
throw new Error('context not found');
}
context.fillStyle = `hsl(${window.getComputedStyle(document.body).getPropertyValue('--background')})`;
context.fillRect(0, 0, canvas.width, canvas.height);
const image = new Image();
image.addEventListener('load', () => {
exporter(context, image)();
});
image.src = `data:image/svg+xml;base64,${await getBase64SVG({ svg, box, height: canvas.height, width: canvas.width, embedFontAwesome: true })}`;
// Fallback to set panZoom to true after 2 seconds
// This is a workaround for the case when the image is not loaded
// setTimeout(() => {
// if (!$inputStateStore.panZoom) {
// }
// }, 2000);
};
const downloadImage: Exporter = (context, image) => {
return () => {
const { canvas } = context;
context.drawImage(image, 0, 0, canvas.width, canvas.height);
simulateDownload(
getFileName('png'),
canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream')
);
};
};
const isClipboardAvailable = (): boolean => {
return Object.prototype.hasOwnProperty.call(window, 'ClipboardItem');
};
const clipboardCopy: Exporter = (context, image) => {
return () => {
const { canvas } = context;
context.drawImage(image, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => {
try {
if (!blob) {
throw new Error('blob is empty');
}
void navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob
})
]);
} catch (error) {
console.error(error);
}
});
};
};
const onCopyClipboard = async (event: Event) => {
await exportImage(event, clipboardCopy);
logEvent('copyClipboard');
};
const onDownloadPNG = async (event: Event) => {
await exportImage(event, downloadImage);
logEvent('download', {
type: 'png'
});
};
const onDownloadSVG = async () => {
simulateDownload(getFileName('svg'), `data:image/svg+xml;base64,${await getBase64SVG()}`);
logEvent('download', {
type: 'svg'
});
};
let gistURL = $state('');
stateStore.subscribe(({ loader }) => {
if (loader?.type === 'gist') {
gistURL = loader.config.url;
}
});
const loadGist = () => {
if (!gistURL) {
return alert('Please enter a Gist URL first');
}
window.location.href = `${window.location.pathname}?gist=${gistURL}`;
logEvent('loadGist');
};
let imageSizeMode: 'auto' | 'width' | 'height' = $state('auto');
$effect(() => {
if (!imageSizeMode) {
imageSizeMode = 'auto';
}
});
let imageSize = $state(1080);
const isNetlify = browser && window.location.host.includes('netlify');
</script>
{#snippet dualActionButton(text: string, download: (event: Event) => unknown, url?: string)}
<div class="flex flex-grow gap-0.5">
<Button
class={['flex-grow', url && 'rounded-r-none']}
onclick={download}
data-testid="download-{text}">
<DownloadIcon />
{text}
</Button>
{#if url}
<Button class="rounded-l-none" href={url} target="_blank" rel="noreferrer noopener">
<ExternalLinkIcon />
</Button>
{/if}
</div>
{/snippet}
<Card title="Actions" isStackable icon={{ component: DownloadIcon, class: 'rotate-180' }}>
<div class="flex min-w-fit flex-col gap-2 p-2">
<div class="flex w-full items-center gap-2 whitespace-nowrap py-2">
PNG size
<ToggleGroup.Root type="single" variant="outline" bind:value={imageSizeMode}>
<ToggleGroup.Item value="auto">Auto</ToggleGroup.Item>
<ToggleGroup.Item value="width">Width</ToggleGroup.Item>
<ToggleGroup.Item value="height">Height</ToggleGroup.Item>
</ToggleGroup.Root>
{#if imageSizeMode !== 'auto'}
<WidthIcon
class={['size-6 shrink-0 transition-all', imageSizeMode === 'width' && 'rotate-90']} />
{/if}
<Input
type="number"
min="3"
max="10000"
disabled={imageSizeMode === 'auto'}
bind:value={imageSize} />
</div>
<div class="flex gap-2">
{@render dualActionButton('PNG', onDownloadPNG, $urlsStore.png)}
{@render dualActionButton('SVG', onDownloadSVG, $urlsStore.svg)}
{#if env.krokiRendererUrl}
<a target="_blank" rel="noreferrer" class="flex-grow" href={$urlsStore.kroki}>
<Button class="action-btn flex w-full items-center gap-2">
<ExternalLinkIcon /> Kroki
</Button>
</a>
{/if}
</div>
<Separator />
{#if isClipboardAvailable()}
<CopyButton onclick={onCopyClipboard} label="Copy Image" />
{/if}
{#if $urlsStore.mdCode}
<CopyInput value={$urlsStore.mdCode} label="Copy Markdown" testID={TID.copyMarkdown} />
{/if}
<div class="flex w-full items-center gap-2">
<Input type="url" bind:value={gistURL} placeholder="Enter Gist URL" />
<Button onclick={loadGist}>Load Gist</Button>
</div>
{#if isNetlify}
<div class="flex w-full items-center justify-center">
<a class="link text-sm text-gray-500 underline" href="https://netlify.com">
This site is powered by Netlify
</a>
</div>
{/if}
</div>
</Card>
-85
View File
@@ -1,85 +0,0 @@
<script lang="ts">
import type { Tab } from '$/types';
import type { Component, Snippet } from 'svelte';
import { quintOut } from 'svelte/easing';
import { slide } from 'svelte/transition';
import CollapseAllIcon from '~icons/material-symbols/collapse-all-rounded';
import Tabs from './Tabs.svelte';
interface Props {
isClosable?: boolean;
isOpen?: boolean;
isStackable?: boolean;
tabs?: Tab[];
activeTabID?: string;
title?: string;
icon?: {
component: Component;
class?: string;
};
onselect?: (tab: Tab) => void;
actions?: Snippet;
children: Snippet;
}
let {
isClosable = true,
isOpen = false,
isStackable = false,
tabs = [],
activeTabID = '',
title,
icon,
onselect,
actions,
children
}: Props = $props();
const toggleCardOpen = () => {
if (isClosable) {
isOpen = !isOpen;
}
};
let isTabsShown = $derived(isOpen && tabs.length > 0);
</script>
<div
class={[
'card flex h-fit flex-col overflow-hidden rounded-2xl border-2 border-muted',
isOpen && 'isOpen flex-grow',
isStackable ? 'flex-1 group-has-[.isOpen]:w-full group-has-[.isOpen]:flex-none' : 'w-full'
]}>
<div
role="toolbar"
tabindex="0"
class={[
'flex h-11 flex-none cursor-pointer items-center justify-between whitespace-nowrap bg-muted p-2',
isTabsShown && 'pb-1'
]}
onclick={toggleCardOpen}
onkeypress={toggleCardOpen}>
{#if icon || title}
<span role="menubar" tabindex="0" class="flex w-fit items-center gap-3">
{#if icon}
<icon.component class={icon.class} />
{/if}
{title}
</span>
{/if}
{#if isOpen && tabs && tabs.length > 0}
<Tabs {onselect} {tabs} {activeTabID} />
{/if}
{@render actions?.()}
{#if isOpen && isClosable}
<CollapseAllIcon />
{/if}
</div>
{#if isOpen}
<div class="flex-grow overflow-x-auto" transition:slide={{ easing: quintOut }}>
{@render children()}
</div>
{/if}
</div>
-52
View File
@@ -1,52 +0,0 @@
<script lang="ts">
import { Button } from '$/components/ui/button';
import { Separator } from '$/components/ui/separator';
import type { Tab } from '$lib/types';
import { fade } from 'svelte/transition';
let {
tabs,
activeTabID,
onselect
}: {
tabs: Tab[];
activeTabID: string;
onselect?: (tab: Tab) => void;
} = $props();
if (!activeTabID && tabs.length > 0) {
activeTabID = tabs[0].id;
}
const toggleTabs = (tab: Tab) => {
return (event: Event) => {
event.stopPropagation();
onselect?.(tab);
};
};
</script>
<div class="flex w-fit cursor-default items-center gap-2">
<ul class="flex gap-2 align-middle" transition:fade>
{#each tabs as tab, index}
<Button
role="tab"
variant="ghost"
class={[
'px-2',
activeTabID === tab.id && 'rounded-b-none border-b-2 border-b-primary-foreground/50'
]}
onclick={toggleTabs(tab)}
onkeypress={toggleTabs(tab)}>
<tab.icon />
{tab.title}
</Button>
{#if index < tabs.length - 1}
<div class="my-2">
<Separator orientation="vertical" class="w-0.5 bg-slate-300" />
</div>
{/if}
{/each}
</ul>
</div>
-41
View File
@@ -1,41 +0,0 @@
<script lang="ts">
import { Button } from '$/components/ui/button';
import type { InputType } from '$/types';
import { notify } from '$/util/notify';
import { scale } from 'svelte/transition';
import CheckIcon from '~icons/material-symbols/check-rounded';
import CopyIcon from '~icons/material-symbols/content-copy-outline-rounded';
let {
onclick,
label = 'Copy'
}: { onclick: (event?: Event) => Promise<unknown>; label?: string; type?: InputType } = $props();
let showCheckIcon = $state(false);
</script>
<Button
onclick={async (event) => {
try {
showCheckIcon = true;
setTimeout(() => {
showCheckIcon = false;
}, 1000);
await onclick(event);
} catch {
notify('Failed to copy');
}
}}>
<div class="grid">
{#key showCheckIcon}
<span transition:scale class="col-start-1 row-start-1">
{#if showCheckIcon}
<CheckIcon />
{:else}
<CopyIcon />
{/if}
</span>
{/key}
</div>
{label}
</Button>
-25
View File
@@ -1,25 +0,0 @@
<script lang="ts">
import CopyButton from '$/components/CopyButton.svelte';
import { Input } from '$/components/ui/input';
import type { InputType } from '$/types';
import { copyToClipboard } from '$/util/util';
let {
value,
label = 'Copy',
type = 'url',
testID
}: { value: string; label?: string; type?: InputType; testID?: string } = $props();
</script>
<div class="flex w-full items-center gap-2">
<Input
{type}
{value}
data-testid={testID}
onclick={(event) => {
event.currentTarget.setSelectionRange(0, event.currentTarget.value.length);
}} />
<CopyButton onclick={() => copyToClipboard(value)} {label} />
</div>
-118
View File
@@ -1,118 +0,0 @@
<script lang="ts">
import type { EditorProps } from '$/types';
import { stateStore } from '$/util/state';
import { initEditor } from '$lib/util/monacoExtra';
import { errorDebug } from '$lib/util/util';
import { mode } from 'mode-watcher';
import * as monaco from 'monaco-editor';
import monacoEditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import monacoJsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import { onMount } from 'svelte';
const { onUpdate }: EditorProps = $props();
let divElement: HTMLDivElement | undefined = $state();
let editor: monaco.editor.IStandaloneCodeEditor | undefined;
let editorOptions = {
minimap: {
enabled: false
},
overviewRulerLanes: 0
} satisfies monaco.editor.IStandaloneEditorConstructionOptions;
let currentText = '';
const jsonModel = monaco.editor.createModel(
'',
'json',
monaco.Uri.parse('internal://config.json')
);
const mermaidModel = monaco.editor.createModel(
'',
'mermaid',
monaco.Uri.parse('internal://mermaid.mmd')
);
onMount(() => {
self.MonacoEnvironment = {
getWorker(_, label) {
if (label === 'json') {
return new monacoJsonWorker();
}
return new monacoEditorWorker();
}
};
if (!divElement) {
throw new Error('divEl is undefined');
}
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
enableSchemaRequest: true,
schemas: [
{
fileMatch: ['config.json'],
uri: 'https://mermaid.js.org/schemas/config.schema.json'
}
]
});
initEditor(monaco);
errorDebug();
editor = monaco.editor.create(divElement, editorOptions);
editor.onDidChangeModelContent(({ isFlush }) => {
const newText = editor?.getValue();
if (!newText || currentText === newText || isFlush) {
return;
}
currentText = newText;
onUpdate(currentText);
});
const unsubscribeState = stateStore.subscribe(({ errorMarkers, editorMode, code, mermaid }) => {
if (!editor) {
return;
}
const model = editorMode === 'code' ? mermaidModel : jsonModel;
if (editor.getModel()?.id !== model.id) {
editor.setModel(model);
}
// Update editor text if it's different
const newText = editorMode === 'code' ? code : mermaid;
if (newText !== currentText) {
editor.setScrollTop(0);
editor.setValue(newText);
currentText = newText;
}
// Display/clear errors
monaco.editor.setModelMarkers(model, 'mermaid', errorMarkers);
});
const unsubscribeMode = mode.subscribe((mode) => {
editor && monaco.editor.setTheme(`mermaid${mode === 'dark' ? '-dark' : ''}`);
});
const resizeObserver = new ResizeObserver((entries) => {
editor?.layout({
height: entries[0].contentRect.height,
width: entries[0].contentRect.width
});
});
if (divElement.parentElement) {
resizeObserver.observe(divElement);
}
return () => {
unsubscribeState();
unsubscribeMode();
resizeObserver.disconnect();
editor?.dispose();
};
});
</script>
<div bind:this={divElement} id="editor" class="h-full flex-grow overflow-hidden"></div>
@@ -1,109 +0,0 @@
<script lang="ts">
import { Button } from '$/components/ui/button';
import { TID } from '$/constants';
import type { DocumentationConfig } from '$/types';
import { standardizeDiagramType } from '$/util/mermaid';
import { stateStore } from '$/util/state';
import BookIcon from '~icons/material-symbols/book-2-outline-rounded';
const docURLBase = 'https://mermaid.js.org';
const docMap = {
architecture: {
code: '/syntax/architecture.html'
},
block: {
code: '/syntax/block.html'
},
c4: {
code: '/syntax/c4.html'
},
class: {
code: '/syntax/classDiagram.html',
config: '/syntax/classDiagram.html#configuration'
},
er: {
code: '/syntax/entityRelationshipDiagram.html',
config: '/syntax/entityRelationshipDiagram.html#styling'
},
flowchart: {
code: '/syntax/flowchart.html',
config: '/syntax/flowchart.html#configuration'
},
gantt: {
code: '/syntax/gantt.html',
config: '/syntax/gantt.html#configuration'
},
gitGraph: {
code: '/syntax/gitgraph.html',
config: '/syntax/gitgraph.html#gitgraph-specific-configuration-options'
},
journey: {
code: '/syntax/userJourney.html'
},
kanban: {
code: '/syntax/kanban.html',
config: '/syntax/kanban.html#configuration-options'
},
mindmap: {
code: '/syntax/mindmap.html'
},
packet: {
code: '/syntax/packet.html',
config: '/config/schema-docs/config-defs-packet-diagram-config.html'
},
pie: {
code: '/syntax/pie.html',
config: '/syntax/pie.html#configuration'
},
quadrantChart: {
code: '/syntax/quadrantChart.html',
config: '/syntax/quadrantChart.html#chart-configurations'
},
requirement: {
code: '/syntax/requirementDiagram.html'
},
sankey: {
code: '/syntax/sankey.html',
config: '/syntax/sankey.html#configuration'
},
sequence: {
code: '/syntax/sequenceDiagram.html',
config: '/syntax/sequenceDiagram.html#configuration'
},
stateDiagram: {
code: '/syntax/stateDiagram.html'
},
timeline: {
code: '/syntax/timeline.html',
config: '/syntax/timeline.html#themes'
},
xychart: {
code: '/syntax/xyChart.html',
config: '/syntax/xyChart.html#chart-configurations'
},
zenuml: {
code: '/syntax/zenuml.html'
}
} as const satisfies DocumentationConfig;
const doc = $derived.by(() => {
const { editorMode, diagramType } = $stateStore;
if (!diagramType) {
return { key: '', url: docURLBase };
}
const key = standardizeDiagramType(diagramType);
const docConfig = docMap[key] ?? { code: '' };
const url = docURLBase + (docConfig[editorMode] ?? docConfig.code ?? '');
return { key, url };
});
</script>
<Button
variant="ghost"
data-testid={TID.diagramDocumentationButton}
href={doc.url}
target="_blank"
title="View documentation for {doc.key.replace('Diagram', '')} diagram">
<BookIcon />
Docs
</Button>
-38
View File
@@ -1,38 +0,0 @@
<script lang="ts">
import { Button } from '$/components/ui/button';
import * as Popover from '$/components/ui/popover';
import type { Component } from 'svelte';
interface Props {
links: { title: string; href: string }[];
icon?: Component;
class?: string;
}
let props: Props = $props();
</script>
<Popover.Root>
<Popover.Trigger class="flex items-center gap-0">
<Button variant="ghost" size="sm">
<props.icon class={props.class} />
</Button>
</Popover.Trigger>
<Popover.Content>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<ul tabindex="0" class="flex flex-col">
{#each props.links as { href, title }}
<li class="rounded-md p-2 hover:bg-muted">
<a
role="menuitem"
tabindex="0"
class="whitespace-nowrap underline"
target="_blank"
{href}>
{title}
</a>
</li>
{/each}
</ul>
</Popover.Content>
</Popover.Root>
-55
View File
@@ -1,55 +0,0 @@
<script lang="ts">
import DesktopEditor from '$/components/DesktopEditor.svelte';
import McWrapper from '$/components/McWrapper.svelte';
import MermaidChartIcon from '$/components/MermaidChartIcon.svelte';
import MobileEditor from '$/components/MobileEditor.svelte';
import { Button } from '$/components/ui/button';
import { TID } from '$/constants';
import { env } from '$/util/env';
import { stateStore, updateCode, updateConfig, urlsStore } from '$lib/util/state';
import ExclamationCircleIcon from '~icons/material-symbols/error-outline-rounded';
let { isMobile }: { isMobile: boolean } = $props();
const onUpdate = (text: string) => {
if ($stateStore.editorMode === 'code') {
updateCode(text);
} else {
updateConfig(text);
}
};
</script>
<div class="flex h-full flex-col">
{#if isMobile}
<MobileEditor {onUpdate} />
{:else}
<DesktopEditor {onUpdate} />
{/if}
{#if $stateStore.error instanceof Error}
<div class="flex flex-col text-sm" data-testid={TID.errorContainer}>
<div class="flex items-center justify-between gap-2 bg-slate-900 p-2 text-white">
<div class="flex w-fit items-center gap-2">
<ExclamationCircleIcon class="size-6 text-destructive" aria-hidden="true" />
<div class="flex flex-col">
<p>Syntax error</p>
{#if env.isEnabledMermaidChartLinks}
<p class="text-xs text-white/60">Create a free account to repair with AI</p>
{/if}
</div>
</div>
<McWrapper>
<Button
variant="accent"
size="sm"
href={$urlsStore.mermaidChart({ medium: 'ai_repair' }).save}>
<MermaidChartIcon />
AI Repair
</Button>
</McWrapper>
</div>
<output class="max-h-32 overflow-auto bg-muted p-2" name="mermaid-error" for="editor">
<pre>{$stateStore.error?.toString()}</pre>
</output>
</div>
{/if}
</div>
-13
View File
@@ -1,13 +0,0 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
children: Snippet;
}
let { children }: Props = $props();
</script>
<div class="flex h-12 items-center justify-between gap-2 rounded-2xl bg-muted p-3">
{@render children()}
</div>
-54
View File
@@ -1,54 +0,0 @@
<!--
@component
Exports a `waitForFontAwesomeToLoad` function that waits for FontAwesome to load.
Usage:
```svelte
<script lang="ts">
import FontAwesome from '$lib/client/components/FontAwesome';
let waitForFontAwesomeToLoad: FontAwesome["waitForFontAwesomeToLoad"];
</script>
<FontAwesome bind:waitForFontAwesomeToLoad />
```
-->
<script context="module" lang="ts">
/**
* Returns `true` if the code may contain FontAwesome icons, and we should
* wait for the fonts to load before rendering.
*/
export function mayContainFontAwesome(code: string) {
// taken from https://github.com/mermaid-js/mermaid/blob/7043892e871d0c413ec63dc1570a8ef738d15568/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js#L63
// Not ideal, since we're looking at unparsed code.
const regex = /fa[blrs]?:fa-[\w-]+/g;
return regex.test(code);
}
</script>
<script lang="ts">
// Vite will automatically take care of adding this to our `<head>`
let lazyLoadFontAwesomeCSS = import('./FontAwesomeCSS.svelte');
// eslint-disable-next-line unicorn/prefer-top-level-await
let fontsLoaded = (async () => {
await lazyLoadFontAwesomeCSS;
// Once the stylesheet has been parsed, the fonts will be in document.fonts
// TODO: Maybe we should scan the CSS style sheet only, and only load FontAwesome fonts
await Promise.allSettled(Array.from(document.fonts, (font) => font.load()));
})();
/**
* Wait for FontAwesome to load.
*
* @returns A promise that resolves when FontAwesome is
* loaded, or there was an error that we ignore.
*/
export async function waitForFontAwesomeToLoad() {
return await fontsLoaded;
}
</script>
{#await lazyLoadFontAwesomeCSS then FontAwesomeCSS}
<FontAwesomeCSS.default />
{/await}
-14
View File
@@ -1,14 +0,0 @@
<!--
@component
Imports the FontAwesome CSS file.
Needed, since the `@sveltejs/adapter-node` plugin doesn't seem to support
lazy-importing CSS, even though `@sveltejs/adapter-vercel` is fine with it!
However, making a `.svelte` file that imports the CSS file, and then
lazy-loading the `.svelte` file works.
-->
<script lang="ts">
import '@fortawesome/fontawesome-free/css/all.css';
</script>
-212
View File
@@ -1,212 +0,0 @@
<script lang="ts">
import Card from '$lib/components/Card/Card.svelte';
import type { HistoryEntry, HistoryType, State, Tab } from '$lib/types';
import { notify, prompt } from '$lib/util/notify';
import { getStateString, inputStateStore } from '$lib/util/state';
import { logEvent } from '$lib/util/stats';
import dayjs from 'dayjs';
import dayjsRelativeTime from 'dayjs/plugin/relativeTime';
import { onMount } from 'svelte';
import { get } from 'svelte/store';
import BookmarkIcon from '~icons/material-symbols/bookmark-outline-rounded';
import TrashAltIcon from '~icons/material-symbols/delete-outline-rounded';
import DownloadIcon from '~icons/material-symbols/download-rounded';
import SaveIcon from '~icons/material-symbols/save-outline-rounded';
import UndoIcon from '~icons/material-symbols/settings-backup-restore-rounded';
import UploadIcon from '~icons/material-symbols/upload-rounded';
import HistoryIcon from '~icons/mdi/clock-outline';
import GitAltIcon from '~icons/mdi/git';
import { Button } from '../ui/button';
import { Separator } from '../ui/separator';
import {
addHistoryEntry,
clearHistoryData,
getPreviousState,
historyModeStore,
historyStore,
loaderHistoryStore,
restoreHistory
} from './history';
dayjs.extend(dayjsRelativeTime);
const HISTORY_SAVE_INTERVAL = 60_000;
const tabSelectHandler = (tab: Tab) => {
historyModeStore.set(tab.id as HistoryType);
};
let tabs: Tab[] = $state([
{
id: 'manual',
title: 'Saved',
icon: BookmarkIcon
},
{
id: 'auto',
title: 'Timeline',
icon: HistoryIcon
}
]);
const downloadHistory = () => {
const data = get(historyStore);
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `mermaid-history-${dayjs().format('YYYY-MM-DD-HHmmss')}.json`;
a.click();
URL.revokeObjectURL(url);
logEvent('history', {
action: 'download'
});
};
const uploadHistory = () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json';
input.addEventListener('change', async ({ target }: Event) => {
const file = (target as HTMLInputElement)?.files?.[0];
if (!file) {
return;
}
const data: HistoryEntry[] = JSON.parse(await file.text());
restoreHistory(data);
});
input.click();
};
const saveHistory = (auto = false) => {
const currentState: string = getStateString();
const previousState: string = getPreviousState(auto);
if (previousState !== currentState) {
addHistoryEntry({
state: $inputStateStore,
time: Date.now(),
type: auto ? 'auto' : 'manual'
});
} else if (!auto) {
notify('State already saved.');
}
};
const clearHistory = (id?: string): void => {
if (!id && !prompt('Clear all saved items?')) {
return;
}
clearHistoryData(id);
};
const restoreHistoryItem = (state: State): void => {
inputStateStore.set({ ...state, updateDiagram: true });
};
onMount(() => {
historyModeStore.set('manual');
setInterval(() => {
saveHistory(true);
}, HISTORY_SAVE_INTERVAL);
});
loaderHistoryStore.subscribe((entries) => {
if (entries.length > 0 && tabs.length === 2) {
tabs = [
{
id: 'loader',
title: 'Revisions',
icon: GitAltIcon
},
...tabs
];
historyModeStore.set('loader');
}
});
</script>
<Card onselect={tabSelectHandler} isOpen isClosable={false} {tabs}>
{#snippet actions()}
<div class="flex items-center gap-2">
<Button
size="icon"
variant="ghost"
id="uploadHistory"
onclick={uploadHistory}
title="Upload history"><UploadIcon /></Button>
{#if $historyStore.length > 0}
<Button
id="downloadHistory"
size="icon"
variant="ghost"
onclick={downloadHistory}
title="Download history"><DownloadIcon /></Button>
{/if}
<Separator orientation="vertical" />
<Button
id="saveHistory"
size="icon"
variant="ghost"
onclick={() => saveHistory()}
title="Save current state"><SaveIcon /></Button>
{#if $historyModeStore !== 'loader'}
<Button
id="clearHistory"
size="icon"
variant="ghost"
class="hover:text-destructive"
onclick={() => clearHistory()}
title="Delete all saved states"><TrashAltIcon /></Button>
{/if}
</div>
{/snippet}
<ul class="flex h-full min-w-fit flex-col gap-2 overflow-auto p-2" id="historyList">
{#if $historyStore.length > 0}
{#each $historyStore as { id, state, time, name, url, type } (id)}
<li class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<div class="flex flex-col">
{#if url}
<a
href={url}
target="_blank"
title="Open revision in new tab"
class="text-blue-500 hover:underline">{name}</a>
{:else}
<span class="whitespace-nowrap">{name}</span>
{/if}
<span class="whitespace-nowrap text-xs text-primary-foreground/30">
{new Date(time).toLocaleString()}
</span>
</div>
<div class="flex items-center gap-2">
<span class="whitespace-nowrap text-sm text-primary-foreground/50">
{dayjs(time).fromNow()}
</span>
<Button size="icon" variant="ghost" onclick={() => restoreHistoryItem(state)}>
<UndoIcon />
</Button>
{#if type !== 'loader'}
<Button
size="icon"
variant="ghost"
class="hover:text-destructive"
onclick={() => clearHistory(id)}>
<TrashAltIcon />
</Button>
{/if}
</div>
</div>
<Separator />
</li>
{/each}
{:else}
<div class="m-2 text-center">
No items in History<br />
Click the Save button to save current state and restore it later.<br />
Timeline will automatically be saved every minute.
</div>
{/if}
</ul>
</Card>
-127
View File
@@ -1,127 +0,0 @@
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import type { HistoryEntry } from '$lib/types';
import { describe, it, expect } from 'vitest';
import {
addHistoryEntry,
injectHistoryIDs,
clearHistoryData,
historyModeStore,
historyStore
} from './history';
import { defaultState } from '$lib/util/state';
import { get } from 'svelte/store';
describe('history', () => {
it('should handle saving individual history entry', () => {
expect(window.localStorage.getItem('manualHistoryStore')).toBe('[]');
expect(window.localStorage.getItem('autoHistoryStore')).toBe('[]');
addHistoryEntry({
state: defaultState,
time: 12_345,
type: 'manual'
});
const [manualEntry] = JSON.parse(
window.localStorage.getItem('manualHistoryStore') ?? '[]'
) as HistoryEntry[];
expect(manualEntry.time).toBe(12_345);
expect(manualEntry.type).toBe('manual');
expect(manualEntry.name).not.toBeNull();
expect(manualEntry.state).not.toBeNull();
addHistoryEntry({
state: defaultState,
time: 54_321,
type: 'auto'
});
const [autoEntry] = JSON.parse(
window.localStorage.getItem('autoHistoryStore') ?? '[]'
) as HistoryEntry[];
expect(autoEntry.time).toBe(54_321);
expect(autoEntry.type).toBe('auto');
expect(autoEntry.name).not.toBeNull();
expect(autoEntry.state).not.toBeNull();
historyModeStore.set('manual');
clearHistoryData();
historyModeStore.set('auto');
clearHistoryData();
expect(window.localStorage.getItem('manualHistoryStore')).toBe('[]');
expect(window.localStorage.getItem('autoHistoryStore')).toBe('[]');
});
it('should clear history entries', () => {
addHistoryEntry({
state: defaultState,
time: 12_345,
type: 'manual'
});
addHistoryEntry({
state: { ...defaultState, code: 'graph TD\\n A[Christmas] -->|Get money| B(Go shopping)' },
time: 123_456,
type: 'manual'
});
historyModeStore.set('manual');
const store: HistoryEntry[] = get(historyStore);
expect(store.length).toBe(2);
clearHistoryData(store[1].id);
expect(get(historyStore).length).toBe(1);
clearHistoryData();
expect(get(historyStore).length).toBe(0);
historyModeStore.set('auto');
addHistoryEntry({
state: defaultState,
time: 54_321,
type: 'auto'
});
addHistoryEntry({
state: { ...defaultState, code: 'graph TD\\n A[Christmas] -->|Get money| B(Go shopping)' },
time: 654_321,
type: 'auto'
});
expect(get(historyStore).length).toBe(2);
clearHistoryData();
expect(get(historyStore).length).toBe(0);
// Test calling when history is empty
clearHistoryData();
expect(get(historyStore).length).toBe(0);
});
});
describe('history migration', () => {
it('should inject history IDs as migration', () => {
window.localStorage.setItem(
'manualHistoryStore',
'[{"state":{"code":"graph TD\\n A[Halloween] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":false},"time":0,"type":"manual","name":"hollow-art"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":true},"time":0,"type":"manual","name":"helpful-ocean"}]'
);
window.localStorage.setItem(
'autoHistoryStore',
'[{"state":{"code":"graph TD\\n A[New Year] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":false},"time":0,"type":"auto","name":"barking-dog"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":true},"time":0,"type":"manual","name":"needy-mosquito"}]'
);
let manualHistoryStore = JSON.parse(
window.localStorage.getItem('manualHistoryStore') ?? '[]'
) as HistoryEntry[];
let autoHistoryStore = JSON.parse(
window.localStorage.getItem('autoHistoryStore') ?? '[]'
) as HistoryEntry[];
expect(manualHistoryStore.every(({ id }) => id !== undefined)).toBe(false);
expect(autoHistoryStore.every(({ id }) => id !== undefined)).toBe(false);
injectHistoryIDs();
manualHistoryStore = JSON.parse(
window.localStorage.getItem('manualHistoryStore') ?? '[]'
) as HistoryEntry[];
autoHistoryStore = JSON.parse(
window.localStorage.getItem('autoHistoryStore') ?? '[]'
) as HistoryEntry[];
expect(manualHistoryStore.every(({ id }) => id !== undefined)).toBe(true);
expect(autoHistoryStore.every(({ id }) => id !== undefined)).toBe(true);
});
});
-152
View File
@@ -1,152 +0,0 @@
import type { HistoryEntry, HistoryType, Optional } from '$lib/types';
import { localStorage, persist } from '$lib/util/persist';
import { logEvent } from '$lib/util/stats';
import { generateSlug } from 'random-word-slugs';
import type { Readable, Writable } from 'svelte/store';
import { derived, get, writable } from 'svelte/store';
import { v4 as uuidV4 } from 'uuid';
const MAX_AUTO_HISTORY_LENGTH = 30;
export const historyModeStore: Writable<HistoryType> = persist(
writable('manual'),
localStorage(),
'autoHistoryMode'
);
const autoHistoryStore: Writable<HistoryEntry[]> = persist(
writable([]),
localStorage(),
'autoHistoryStore'
);
const manualHistoryStore: Writable<HistoryEntry[]> = persist(
writable([]),
localStorage(),
'manualHistoryStore'
);
export const loaderHistoryStore: Writable<HistoryEntry[]> = writable([]);
export const historyStore: Readable<HistoryEntry[]> = derived(
[historyModeStore, autoHistoryStore, manualHistoryStore, loaderHistoryStore],
([historyMode, autoHistories, manualHistories, loadedHistories], set) => {
switch (historyMode) {
case 'auto': {
set(autoHistories);
break;
}
case 'manual': {
set(manualHistories);
break;
}
case 'loader': {
set(loadedHistories);
break;
}
default: {
set(autoHistories);
}
}
}
);
export const addHistoryEntry = (entryToAdd: Optional<HistoryEntry, 'id'>): void => {
const entry: HistoryEntry = {
...entryToAdd,
id: uuidV4()
};
if (entry.type === 'loader') {
loaderHistoryStore.update((entries) => [entry, ...entries]);
return;
}
if (!entry.name) {
entry.name = generateSlug(2);
}
if (entry.type === 'auto') {
autoHistoryStore.update((entries) => {
if (entries.length >= MAX_AUTO_HISTORY_LENGTH) {
entries = entries.slice(0, MAX_AUTO_HISTORY_LENGTH - 1);
}
return [entry, ...entries];
});
}
manualHistoryStore.update((entries) => [entry, ...entries]);
logEvent('history', { action: 'save' });
};
export const clearHistoryData = (idToClear?: string): void => {
(get(historyModeStore) === 'auto' ? autoHistoryStore : manualHistoryStore).update((entries) => {
if (get(historyModeStore) !== 'loader') {
entries = entries.filter(({ id }) => idToClear && id != idToClear);
logEvent('history', { action: 'clear', type: idToClear ? 'single' : 'all' });
}
return entries;
});
};
export const getPreviousState = (auto: boolean): string => {
const entries = get(auto ? autoHistoryStore : manualHistoryStore);
if (entries.length > 0) {
return JSON.stringify(entries[0].state);
}
return '';
};
export const restoreHistory = (data: HistoryEntry[]) => {
const entries = data.filter((element) => validateEntry(element));
const invalidEntryCount = data.length - entries.length;
if (invalidEntryCount > 0) {
console.error(`${invalidEntryCount} invalid history entries were removed.`);
console.error(data);
}
if (entries.length > 0) {
let entryCount = 0;
(entries[0].type === 'auto' ? autoHistoryStore : manualHistoryStore).update((existing) => {
const existingIDs = new Set(existing.map(({ id }) => id));
const newEntries = entries.filter(({ id }) => !existingIDs.has(id));
entryCount = newEntries.length;
const combined = [...existing, ...newEntries];
combined.sort((a, b) => b.time - a.time);
return combined;
});
alert(
`${entryCount} entries restored. ${invalidEntryCount} invalid, ${
entries.length - entryCount
} duplicates.`
);
logEvent('history', {
action: 'restore',
success: entryCount,
invalid: invalidEntryCount,
duplicates: entries.length - entryCount
});
} else {
alert('No valid entries found.');
}
};
const setIDs = (entries: HistoryEntry[]) => {
for (const entry of entries) {
if (!entry.id) {
entry.id = uuidV4();
}
}
return entries;
};
export const injectHistoryIDs = (): void => {
autoHistoryStore.update(setIDs);
manualHistoryStore.update(setIDs);
};
const validateEntry = (entry: HistoryEntry): boolean => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return entry.type && entry.state && entry.time && true;
};
-87
View File
@@ -1,87 +0,0 @@
<script lang="ts">
import McWrapper from '$/components/McWrapper.svelte';
import ThemeIcon from '$/components/ThemeIcon.svelte';
import * as Popover from '$/components/ui/popover';
import { Switch } from '$/components/ui/switch';
import { urlsStore } from '$/util/state';
import { cn } from '$/utils';
import { mode, setMode } from 'mode-watcher';
import type { Component } from 'svelte';
import AddIcon from '~icons/material-symbols/add-2-rounded';
import BookIcon from '~icons/material-symbols/book-2-outline-rounded';
import PluginIcon from '~icons/material-symbols/extension-outline';
import HomeIcon from '~icons/material-symbols/house-outline-rounded';
import CommunityIcon from '~icons/material-symbols/person-play-outline-rounded';
import PlaygroundIcon from '~icons/material-symbols/shape-line-outline';
import MermaidChartIcon from './MermaidChartIcon.svelte';
const menuItems = $derived([
{ label: 'New Diagram', icon: AddIcon, href: $urlsStore.new },
{ label: 'Home', icon: HomeIcon, href: 'https://mermaid.js.org/' },
{ label: 'Documentation', icon: BookIcon, href: 'https://mermaid.js.org/intro/' },
{ label: 'Community', icon: CommunityIcon, href: 'https://discord.gg/sKeNQX4Wtj' }
]);
const mermaidChartMenuItems = $derived([
{
label: 'Edit in Playground',
icon: PlaygroundIcon,
href: $urlsStore.mermaidChart({ medium: 'main_menu' }).playground
},
{
label: 'Plugins',
icon: PluginIcon,
href: $urlsStore.mermaidChart({ medium: 'main_menu' }).plugins,
checkDiagramType: false
},
{
label: 'MermaidChart',
icon: MermaidChartIcon,
href: $urlsStore.mermaidChart({ medium: 'main_menu' }).home,
checkDiagramType: false
}
]);
</script>
{#snippet menuItem(options: { label: string; icon: Component; href: string; class?: string })}
<a
href={options.href}
target="_blank"
class={cn(
'flex items-center justify-start gap-2 border-b bg-muted p-2 px-3 hover:bg-background',
options.class
)}>
<options.icon class="size-5" />
{options.label}
</a>
{/snippet}
<Popover.Root>
<Popover.Trigger class="shrink-0">
<img class="size-6" src="/favicon.svg" alt="Mermaid Live Editor" />
</Popover.Trigger>
<Popover.Content align="start" class="flex flex-col overflow-hidden p-0" sideOffset={16}>
{#each menuItems as item}
{@render menuItem(item)}
{/each}
<div class="flex items-center justify-between border-b bg-muted px-3 py-2 hover:bg-background">
<span class="flex items-center gap-2">
<ThemeIcon />
Dark Mode
</span>
<Switch
checked={$mode === 'dark'}
onCheckedChange={(dark) => setMode(dark ? 'dark' : 'light')} />
</div>
{#each mermaidChartMenuItems as item}
<McWrapper side="right" shouldCheckDiagramType={item.checkDiagramType}>
{@render menuItem({
...item,
class: 'text-accent bg-background hover:bg-muted'
})}
</McWrapper>
{/each}
</Popover.Content>
</Popover.Root>
-50
View File
@@ -1,50 +0,0 @@
<script lang="ts">
import { env } from '$/util/env';
import { stateStore } from '$/util/state';
import * as Tooltip from '$lib/components/ui/tooltip';
import type { ComponentProps, Snippet } from 'svelte';
import ExternalLinkIcon from '~icons/material-symbols/open-in-new-rounded';
let {
children,
shouldCheckDiagramType = true,
side = 'bottom',
labelPrefix = 'Opens a new tab in'
}: {
children: Snippet;
shouldCheckDiagramType?: boolean;
side?: ComponentProps<typeof Tooltip.Content>['side'];
labelPrefix?: string;
} = $props();
let shouldDisableComponent = $derived(
shouldCheckDiagramType && $stateStore.diagramType === 'zenuml'
);
</script>
{#if env.isEnabledMermaidChartLinks}
<Tooltip.Provider>
<Tooltip.Root delayDuration={100}>
<Tooltip.Trigger>
<div class={[shouldDisableComponent && 'pointer-events-none cursor-not-allowed grayscale']}>
{@render children()}
</div>
</Tooltip.Trigger>
<Tooltip.Content {side}>
<div class="flex items-center gap-2">
{#if shouldDisableComponent}
<div class="text-muted-foreground">
This diagram type is not supported in MermaidChart.com
</div>
{:else}
<ExternalLinkIcon />
<span class="flex items-center gap-1">
{labelPrefix}
<div class="text-accent">MermaidChart.com</div>
</span>
{/if}
</div>
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>
{/if}
@@ -1,7 +0,0 @@
<script lang="ts">
import type { ClassValue } from 'svelte/elements';
let { class: className }: { class?: ClassValue } = $props();
</script>
<img class={['size-4', className]} src="/mermaidchart-logo.svg" alt="Mermaid Chart" />
-99
View File
@@ -1,99 +0,0 @@
<script lang="ts">
import type { EditorProps } from '$/types';
import { stateStore } from '$/util/state';
import { json, jsonLanguage } from '@codemirror/lang-json';
import { markdown } from '@codemirror/lang-markdown';
import { yamlFrontmatter } from '@codemirror/lang-yaml';
import { language } from '@codemirror/language';
import { Compartment, EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { vsCodeDark } from '@fsegurai/codemirror-theme-vscode-dark';
import { vsCodeLight } from '@fsegurai/codemirror-theme-vscode-light';
import { basicSetup } from 'codemirror';
import { mode } from 'mode-watcher';
import { onMount } from 'svelte';
let editorView: EditorView | undefined;
let editorContainer: HTMLDivElement;
let currentText = $state('');
const { onUpdate }: EditorProps = $props();
onMount(() => {
const themeCompartment = new Compartment();
const languageCompartment = new Compartment();
editorView = new EditorView({
state: EditorState.create({
doc: currentText,
extensions: [
basicSetup,
languageCompartment.of([]),
themeCompartment.of([]),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
const newText = update.state.doc.toString();
if (currentText === newText) {
return;
}
currentText = newText;
onUpdate(newText);
}
}),
EditorView.theme({
'&.cm-focused': {
outline: 'none'
},
'&.cm-editor': {
height: '100%'
},
'&.cm-scroller': {
overflow: 'auto'
}
})
]
}),
parent: editorContainer
});
const unsubscribeMode = mode.subscribe((mode) => {
editorView?.dispatch({
effects: themeCompartment.reconfigure(mode === 'dark' ? vsCodeDark : vsCodeLight)
});
});
const unsubscribeState = stateStore.subscribe(({ editorMode, code, mermaid }) => {
const text = editorMode === 'code' ? code : mermaid;
if (currentText === text || !editorView) {
return;
}
currentText = text;
editorView.dispatch({
changes: {
from: 0,
to: editorView.state.doc.length,
insert: text
}
});
const stateLanguage = editorView.state.facet(language);
const isStateJson = stateLanguage === jsonLanguage;
const isCodeJson = editorMode === 'config';
if (stateLanguage && isStateJson === isCodeJson) {
return;
}
editorView.dispatch({
effects: languageCompartment.reconfigure(
isCodeJson ? json() : yamlFrontmatter({ content: markdown() })
)
});
});
return () => {
unsubscribeMode();
unsubscribeState();
editorView?.destroy();
};
});
</script>
<div bind:this={editorContainer} class="size-full"></div>
-137
View File
@@ -1,137 +0,0 @@
<script lang="ts" module>
import { logEvent, plausible } from '$lib/util/stats';
import { version } from 'mermaid/package.json';
void logEvent('version', {
mermaidVersion: version
});
</script>
<script lang="ts">
import MainMenu from '$/components/MainMenu.svelte';
import McWrapper from '$/components/McWrapper.svelte';
import { Button } from '$/components/ui/button';
import { Separator } from '$/components/ui/separator';
import { Switch } from '$/components/ui/switch';
import { dismissPromotion, getActivePromotion } from '$lib/util/promos/promo';
import { urlsStore } from '$lib/util/state';
import { MCBaseURL } from '$lib/util/util';
import type { ComponentProps, Snippet } from 'svelte';
import CloseIcon from '~icons/material-symbols/close-rounded';
import GithubIcon from '~icons/mdi/github';
import DropdownNavMenu from './DropdownNavMenu.svelte';
interface Props {
mobileToggle?: Snippet;
children: Snippet;
}
let { children, mobileToggle }: Props = $props();
const isReferral = document.referrer.includes(MCBaseURL);
type Links = ComponentProps<typeof DropdownNavMenu>['links'];
const githubLinks: Links = [
{ title: 'Mermaid JS', href: 'https://github.com/mermaid-js/mermaid' },
{
title: 'Mermaid Live Editor',
href: 'https://github.com/mermaid-js/mermaid-live-editor'
},
{
title: 'Mermaid CLI',
href: 'https://github.com/mermaid-js/mermaid-cli'
}
];
let activePromotion = $state(getActivePromotion());
const trackBannerClick = () => {
if (!plausible || !activePromotion) {
return;
}
logEvent('bannerClick', {
promotion: activePromotion.id
});
};
</script>
{#if activePromotion}
<div class="top-bar z-10 flex h-fit w-full bg-primary">
<div
class="flex flex-grow"
role="button"
tabindex="0"
onclick={trackBannerClick}
onkeypress={trackBannerClick}>
<activePromotion.component {closeBanner} />
</div>
{#snippet closeBanner()}
<Button
title="Dismiss banner"
variant="ghost"
class="hover:bg-transparent hover:text-[#261A56]"
size="sm"
onclick={() => {
dismissPromotion(activePromotion?.id);
activePromotion = undefined;
}}>
<CloseIcon />
</Button>
{/snippet}
</div>
{/if}
<nav class="z-50 flex p-4 sm:p-6">
<div class="flex flex-1 items-center gap-4">
<MainMenu />
<div
id="switcher"
class="flex items-center justify-center gap-4 font-medium"
class:flex-row-reverse={isReferral}>
<a href="/" class="whitespace-nowrap text-accent">
{#if !isReferral && !mobileToggle}
Mermaid
{/if}
Live Editor
</a>
<McWrapper labelPrefix="Opens the current diagram in">
<div class="hidden items-center justify-center gap-4 md:flex">
<Separator orientation="vertical" />
<Switch
id="editorMode"
class="data-[state=checked]:bg-secondary"
checked={isReferral}
onclick={() => {
logEvent('playgroundToggle', { isReferred: isReferral });
// Wait for the event to be logged
setTimeout(() => {
window.open(
$urlsStore.mermaidChart({ medium: 'toggle' }).playground,
'_self',
// Do not send referrer header, if the user already came from playground
isReferral ? 'noreferrer' : ''
);
}, 100);
}} />
<a
href={$urlsStore.mermaidChart({ medium: 'toggle' }).playground}
class="whitespace-nowrap">
Playground <span class="hidden text-sm opacity-50 lg:inline"
>- more features, no account required</span>
</a>
</div>
</McWrapper>
</div>
</div>
<div
id="menu"
class="hidden flex-nowrap items-center justify-between gap-3 overflow-hidden md:flex">
<DropdownNavMenu icon={GithubIcon} links={githubLinks} />
<Separator orientation="vertical" />
{@render children()}
</div>
{@render mobileToggle?.()}
</nav>
-34
View File
@@ -1,34 +0,0 @@
<script lang="ts">
import FloatingToolbar from '$/components/FloatingToolbar.svelte';
import { Button } from '$/components/ui/button';
import { Separator } from '$/components/ui/separator';
import type { PanZoomState } from '$/util/panZoom';
import { urlsStore } from '$/util/state';
import ExpandIcon from '~icons/material-symbols/open-in-full-rounded';
import ArrowsToCircleIcon from '~icons/material-symbols/screenshot-frame-2';
import MagnifyingGlassPlusIcon from '~icons/material-symbols/zoom-in';
import MagnifyingGlassMinusIcon from '~icons/material-symbols/zoom-out';
let { panZoomState }: { panZoomState: PanZoomState } = $props();
</script>
<FloatingToolbar>
<Button variant="ghost" size="icon" title="Reset view" onclick={() => panZoomState.reset()}>
<ArrowsToCircleIcon />
</Button>
<Separator orientation="vertical" />
<Button
variant="ghost"
size="icon"
class="hidden sm:block"
onclick={() => panZoomState.zoomOut()}>
<MagnifyingGlassMinusIcon />
</Button>
<Button variant="ghost" size="icon" class="hidden sm:block" onclick={() => panZoomState.zoomIn()}>
<MagnifyingGlassPlusIcon />
</Button>
<Separator orientation="vertical" class="hidden sm:block" />
<Button variant="ghost" size="icon" title="Full Screen" href={$urlsStore.view} target="_blank">
<ExpandIcon />
</Button>
</FloatingToolbar>
-234
View File
@@ -1,234 +0,0 @@
<script lang="ts">
import Card from '$/components/Card/Card.svelte';
import { Button } from '$/components/ui/button';
import { updateCode } from '$lib/util/state';
import { logEvent } from '$lib/util/stats';
import ShapesIcon from '~icons/material-symbols/account-tree-outline-rounded';
const samples = {
Block: `block-beta
columns 3
doc>"Document"]:3
space down1<[" "]>(down) space
block:e:3
l["left"]
m("A wide one in the middle")
r["right"]
end
space down2<[" "]>(down) space
db[("DB")]:3
space:3
D space C
db --> D
C --> db
D --> C
style m fill:#d6d,stroke:#333,stroke-width:4px
`,
Class: `classDiagram
Animal <|-- Duck
Animal <|-- Fish
Animal <|-- Zebra
Animal : +int age
Animal : +String gender
Animal: +isMammal()
Animal: +mate()
class Duck{
+String beakColor
+swim()
+quack()
}
class Fish{
-int sizeInFeet
-canEat()
}
class Zebra{
+bool is_wild
+run()
}`,
ER: `erDiagram
CUSTOMER }|..|{ DELIVERY-ADDRESS : has
CUSTOMER ||--o{ ORDER : places
CUSTOMER ||--o{ INVOICE : "liable for"
DELIVERY-ADDRESS ||--o{ ORDER : receives
INVOICE ||--|{ ORDER : covers
ORDER ||--|{ ORDER-ITEM : includes
PRODUCT-CATEGORY ||--|{ PRODUCT : contains
PRODUCT ||--o{ ORDER-ITEM : "ordered in"`,
Flow: `flowchart TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
C -->|One| D[Laptop]
C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Car]`,
Gantt: `gantt
title A Gantt Diagram
dateFormat YYYY-MM-DD
section Section
A task :a1, 2014-01-01, 30d
Another task :after a1 , 20d
section Another
Task in sec :2014-01-12 , 12d
another task : 24d`,
Git: `gitGraph
commit
commit
branch develop
checkout develop
commit
commit
checkout main
merge develop
commit
commit`,
Mindmap: `mindmap
root((mindmap))
Origins
Long history
::icon(fa fa-book)
Popularisation
British popular psychology author Tony Buzan
Research
On effectiveness<br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
Pen and paper
Mermaid`,
Packet: `---
title: "TCP Packet"
---
packet-beta
0-15: "Source Port"
16-31: "Destination Port"
32-63: "Sequence Number"
64-95: "Acknowledgment Number"
96-99: "Data Offset"
100-105: "Reserved"
106: "URG"
107: "ACK"
108: "PSH"
109: "RST"
110: "SYN"
111: "FIN"
112-127: "Window"
128-143: "Checksum"
144-159: "Urgent Pointer"
160-191: "(Options and Padding)"
192-255: "Data (variable length)"
`,
Pie: `pie title Pets adopted by volunteers
"Dogs" : 386
"Cats" : 85
"Rats" : 15`,
QuadrantChart: `quadrantChart
title Reach and engagement of campaigns
x-axis Low Reach --> High Reach
y-axis Low Engagement --> High Engagement
quadrant-1 We should expand
quadrant-2 Need to promote
quadrant-3 Re-evaluate
quadrant-4 May be improved
Campaign A: [0.3, 0.6]
Campaign B: [0.45, 0.23]
Campaign C: [0.57, 0.69]
Campaign D: [0.78, 0.34]
Campaign E: [0.40, 0.34]
Campaign F: [0.35, 0.78]`,
Sequence: `sequenceDiagram
Alice->>+John: Hello John, how are you?
Alice->>+John: John, can you hear me?
John-->>-Alice: Hi Alice, I can hear you!
John-->>-Alice: I feel great!`,
State: `stateDiagram-v2
[*] --> Still
Still --> [*]
Still --> Moving
Moving --> Still
Moving --> Crash
Crash --> [*]`,
'User Journey': `journey
title My working day
section Go to work
Make tea: 5: Me
Go upstairs: 3: Me
Do work: 1: Me, Cat
section Go home
Go downstairs: 5: Me
Sit down: 3: Me`,
XYChart: `
xychart-beta
title "Sales Revenue"
x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
y-axis "Revenue (in $)" 4000 --> 11000
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
line [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]`,
ZenUML: `zenuml
title Order Service
@Actor Client #FFEBE6
@Boundary OrderController #0747A6
@EC2 <<BFF>> OrderService #E3FCEF
group BusinessService {
@Lambda PurchaseService
@AzureFunction InvoiceService
}
@Starter(Client)
// \`POST /orders\`
OrderController.post(payload) {
OrderService.create(payload) {
order = new Order(payload)
if(order != null) {
par {
PurchaseService.createPO(order)
InvoiceService.createInvoice(order)
}
}
}
}
`
};
type SampleTypes = keyof typeof samples;
const loadSampleDiagram = (diagramType: SampleTypes): void => {
updateCode(samples[diagramType], {
resetPanZoom: true,
updateDiagram: true
});
logEvent('loadSampleDiagram', { diagramType });
};
const diagramOrder: SampleTypes[] = [
'Flow',
'Sequence',
'Class',
'State',
'ER',
'Gantt',
'User Journey',
'Git',
'Pie',
'Mindmap',
'ZenUML',
'QuadrantChart',
'XYChart',
'Block',
'Packet'
];
</script>
<Card title="Sample Diagrams" isOpen isStackable icon={{ component: ShapesIcon }}>
<div class="flex h-fit max-h-52 flex-wrap gap-2 overflow-y-auto p-2">
{#each diagramOrder as sample}
<Button
size="sm"
class="w-fit min-w-20 flex-grow normal-case"
onclick={() => loadSampleDiagram(sample)}>
{sample}
</Button>
{/each}
</div>
</Card>
-53
View File
@@ -1,53 +0,0 @@
<script>
import * as Dialog from '$/components/ui/dialog';
import ShieldIcon from '~icons/material-symbols/shield-lock-outline-rounded';
</script>
<Dialog.Root>
<Dialog.Trigger>
<ShieldIcon />
</Dialog.Trigger>
<Dialog.Content class="max-h-full overflow-y-auto">
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2 text-xl">
<ShieldIcon class="size-8 text-green-700" />
Data security
</Dialog.Title>
</Dialog.Header>
<p class="text-xl font-semibold">
The content of the diagrams you create never leaves your browser.
</p>
<p>It's only stored in the URL, and your browser's local storage.</p>
<p>
Mermaid Live Editor is a fully open source, client side application, deployed transparently on <a
href="https://github.com/mermaid-js/mermaid-live-editor/deployments"
class="underline"
target="_blank">
GitHub Pages
</a>.
</p>
<p>
It will also work as a fully offline
<a href="https://web.dev/explore/progressive-web-apps" target="_blank">
Progressive Web App.
</a>
</p>
<p>
The only server we have is a self hosted version of the open source and privacy friendly
Plausible Analytics. We only collect anonymous data related to actions performed, like the
type of diagram rendered, number of times a feature was used, etc.
</p>
<p>
All the data we collect is anonymized and
<a href="https://p.mermaid.live/mermaid.live" class="underline" target="_blank">
available publicly.
</a>
</p>
<p>
Additional services like the external PNG/SVG/Kroki links and "Save to Mermaid Chart" feature
will share your diagram with the respective 3rd party service.
</p>
</Dialog.Content>
</Dialog.Root>
-48
View File
@@ -1,48 +0,0 @@
<script>
import { buttonVariants } from '$/components/ui/button';
import * as Dialog from '$/components/ui/dialog';
import { Separator } from '$/components/ui/separator';
import { env } from '$/util/env';
import { urlsStore } from '$/util/state';
import ShareIcon from '~icons/material-symbols/share';
import CopyInput from './CopyInput.svelte';
import MermaidChartIcon from './MermaidChartIcon.svelte';
</script>
<Dialog.Root>
<Dialog.Trigger class={buttonVariants({ size: 'sm' })}>Share</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2 text-xl">
<ShareIcon class="size-5" /> Shareable links
</Dialog.Title>
<Dialog.Description>Share your diagrams with others.</Dialog.Description>
</Dialog.Header>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<h2 class="flex items-center gap-2">
<img class="size-5" src="/favicon.svg" alt="Mermaid Live Editor" />
Mermaid Live Editor
</h2>
<CopyInput value={window.location.href} />
<Dialog.Description>
The content of the diagrams you create never leaves your browser.
</Dialog.Description>
</div>
{#if env.isEnabledMermaidChartLinks}
<Separator />
<div class="flex flex-col gap-2">
<h2 class="flex items-center gap-2">
<MermaidChartIcon class="size-5" />
Mermaid Chart Playground
</h2>
<CopyInput value={$urlsStore.mermaidChart({ medium: 'share' }).playground} />
<Dialog.Description>
Opens the Mermaid Chart Playground with Mermaid AI, Visual Editor, and more.
</Dialog.Description>
</div>
{/if}
</div>
</Dialog.Content>
</Dialog.Root>
@@ -1,21 +0,0 @@
<script lang="ts">
import FloatingToolbar from '$/components/FloatingToolbar.svelte';
import { Toggle } from '$/components/ui/toggle';
import { defaultState, inputStateStore } from '$/util/state';
import RoughIcon from '~icons/material-symbols/draw-outline-rounded';
import BackgroundIcon from '~icons/material-symbols/grid-4x4-rounded';
if ($inputStateStore.grid === undefined) {
// Handle cases where old states were saved without grid option
$inputStateStore.grid = defaultState.grid;
}
</script>
<FloatingToolbar>
<Toggle bind:pressed={$inputStateStore.rough} size="sm" title="Hand-Drawn">
<RoughIcon />
</Toggle>
<Toggle bind:pressed={$inputStateStore.grid} size="sm" title="Background Grid">
<BackgroundIcon />
</Toggle>
</FloatingToolbar>
-39
View File
@@ -1,39 +0,0 @@
<script lang="ts">
import { mode } from 'mode-watcher';
import { cubicInOut } from 'svelte/easing';
import { type TransitionConfig } from 'svelte/transition';
import MoonIcon from '~icons/material-symbols/dark-mode-outline-rounded';
import SunIcon from '~icons/material-symbols/light-mode-outline-rounded';
const spin = (
node: Element,
{ duration = 400, easing = cubicInOut, clockWise = true } = {}
): TransitionConfig => {
const style = getComputedStyle(node);
const opacity = +style.opacity;
const transform = style.transform === 'none' ? '' : style.transform;
return {
duration,
easing,
css: (t, u) => `
transform: ${transform} rotate(${u * 90 * (clockWise ? 1 : -1)}deg);
opacity: ${opacity * t}
`
};
};
</script>
<div class="inline-grid">
{#key $mode}
<div
in:spin={{ clockWise: true }}
out:spin={{ clockWise: false }}
class="col-start-1 row-start-1">
{#if $mode === 'dark'}
<MoonIcon />
{:else}
<SunIcon />
{/if}
</div>
{/key}
</div>
@@ -1,28 +0,0 @@
<script lang="ts">
import FloatingToolbar from '$/components/FloatingToolbar.svelte';
import Privacy from '$/components/Privacy.svelte';
import { Button } from '$/components/ui/button';
import { Separator } from '$/components/ui/separator';
import { TID } from '$/constants';
import { version } from 'mermaid/package.json';
import { mode, setMode } from 'mode-watcher';
import ThemeIcon from './ThemeIcon.svelte';
</script>
<FloatingToolbar>
<span class="text-sm font-semibold opacity-60">v{version}</span>
<Button variant="ghost" size="icon" title="Privacy & Security">
<Privacy />
</Button>
<Separator orientation="vertical" />
<Button
variant="ghost"
size="icon"
data-testid={TID.themeToggleButton}
title="Switch to {$mode === 'dark' ? 'light' : 'dark'} theme"
class="[&_svg]:size-5"
onclick={() => setMode($mode === 'dark' ? 'light' : 'dark')}>
<ThemeIcon />
</Button>
</FloatingToolbar>
-165
View File
@@ -1,165 +0,0 @@
<script lang="ts">
import type { State, ValidatedState } from '$/types';
import { recordRenderTime, shouldRefreshView } from '$/util/autoSync';
import { render as renderDiagram } from '$/util/mermaid';
import { PanZoomState } from '$/util/panZoom';
import { inputStateStore, stateStore, updateCodeStore } from '$/util/state';
import { logEvent, saveStatistics } from '$/util/stats';
import FontAwesome, { mayContainFontAwesome } from '$lib/components/FontAwesome.svelte';
import uniqueID from 'lodash-es/uniqueId';
import type { MermaidConfig } from 'mermaid';
import { mode } from 'mode-watcher';
import { onMount } from 'svelte';
import { Svg2Roughjs } from 'svg2roughjs';
let {
panZoomState = new PanZoomState(),
shouldShowGrid = true
}: { panZoomState?: PanZoomState; shouldShowGrid?: boolean } = $props();
let code = '';
let config = '';
let container: HTMLDivElement | undefined = $state();
let rough: boolean;
let view: HTMLDivElement | undefined = $state();
let error = $state(false);
let panZoom = true;
let manualUpdate = true;
let waitForFontAwesomeToLoad: FontAwesome['waitForFontAwesomeToLoad'] | undefined = $state();
// Set up panZoom state observer to update the store when pan/zoom changes
const setupPanZoomObserver = () => {
panZoomState.onPanZoomChange = (pan, zoom) => {
updateCodeStore({ pan, zoom });
logEvent('panZoom');
};
};
const handlePanZoom = (state: State, graphDiv: SVGSVGElement) => {
panZoomState.updateElement(graphDiv, state);
};
const handleStateChange = async (state: ValidatedState) => {
const startTime = Date.now();
if (state.error !== undefined) {
error = true;
return;
}
error = false;
let diagramType: string | undefined;
try {
if (container) {
manualUpdate = true;
// Do not render if there is no change in Code/Config/PanZoom
if (
code === state.code &&
config === state.mermaid &&
rough === state.rough &&
panZoom === state.panZoom
) {
return;
}
if (!shouldRefreshView()) {
return;
}
code = state.code;
config = state.mermaid;
rough = state.rough;
panZoom = state.panZoom ?? true;
if (mayContainFontAwesome(code)) {
await waitForFontAwesomeToLoad?.();
}
const scroll = view?.parentElement?.scrollTop;
delete container.dataset.processed;
const viewID = uniqueID('graph-');
const {
svg,
bindFunctions,
diagramType: detectedDiagramType
} = await renderDiagram(JSON.parse(state.mermaid) as MermaidConfig, code, viewID);
diagramType = detectedDiagramType;
if (svg.length > 0) {
container.innerHTML = svg;
let graphDiv = document.querySelector<SVGSVGElement>(`#${viewID}`);
if (!graphDiv) {
throw new Error('graph-div not found');
}
if (state.rough) {
const svg2roughjs = new Svg2Roughjs('#container');
svg2roughjs.svg = graphDiv;
await svg2roughjs.sketch();
graphDiv.remove();
const sketch = document.querySelector<SVGSVGElement>('#container > svg');
if (!sketch) {
throw new Error('sketch not found');
}
const height = sketch.getAttribute('height');
const width = sketch.getAttribute('width');
sketch.setAttribute('id', 'graph-div');
sketch.setAttribute('height', '100%');
sketch.setAttribute('width', '100%');
sketch.setAttribute('viewBox', `0 0 ${width} ${height}`);
sketch.style.maxWidth = '100%';
graphDiv = sketch;
} else {
graphDiv.setAttribute('height', '100%');
graphDiv.style.maxWidth = '100%';
if (bindFunctions) {
bindFunctions(graphDiv);
}
}
if (state.panZoom) {
handlePanZoom(state, graphDiv);
}
}
if (view?.parentElement && scroll) {
view.parentElement.scrollTop = scroll;
}
error = false;
} else if (manualUpdate) {
manualUpdate = false;
}
} catch (error_) {
console.error('view fail', error_);
error = true;
}
const renderTime = Date.now() - startTime;
saveStatistics({ code, diagramType, isRough: state.rough, renderTime });
recordRenderTime(renderTime, () => {
$inputStateStore.updateDiagram = true;
});
};
onMount(() => {
setupPanZoomObserver();
// Queue state changes to avoid race condition
let pendingStateChange = Promise.resolve();
stateStore.subscribe((state) => {
pendingStateChange = pendingStateChange.then(() => handleStateChange(state).catch(() => {}));
});
});
</script>
<FontAwesome bind:waitForFontAwesomeToLoad />
<div
id="view"
bind:this={view}
class={['h-full w-full', shouldShowGrid && `grid-bg-${$mode}`, error && 'opacity-50']}>
<div id="container" bind:this={container} class="h-full overflow-auto"></div>
</div>
<style>
.grid-bg-light {
background-size: 30px 30px;
background-image: radial-gradient(circle, #e4e4e48c 2px, #0000 2px);
}
.grid-bg-dark {
background-size: 30px 30px;
background-image: radial-gradient(circle, #46464646 2px, #0000 2px);
}
</style>
+226
View File
@@ -0,0 +1,226 @@
<script lang="ts">
import { browser } from '$app/env';
import Card from '$lib/components/card/card.svelte';
import { krokiRendererUrl, rendererUrl } from '$lib/util/env';
import { pakoSerde } from '$lib/util/serde';
import { serializedState, codeStore } from '$lib/util/state';
import { toBase64 } from 'js-base64';
import moment from 'moment';
type Exporter = (context: CanvasRenderingContext2D, image: HTMLImageElement) => () => void;
const getBase64SVG = (svg?: HTMLElement, width?: number, height?: number): string => {
svg?.setAttribute('height', `${height}px`);
svg?.setAttribute('width', `${width}px`); // Workaround https://stackoverflow.com/questions/28690643/firefox-error-rendering-an-svg-image-to-html5-canvas-with-drawimage
if (!svg) {
svg = document.querySelector('#container svg');
}
const svgString = svg.outerHTML
.replaceAll('<br>', '<br/>')
.replaceAll(/<img([^>]*)>/g, (m, g: string) => `<img ${g} />`);
return toBase64(svgString);
};
const exportImage = (event: Event, exporter: Exporter) => {
const canvas: HTMLCanvasElement = document.createElement('canvas');
const svg: HTMLElement = document.querySelector('#container svg');
const box: DOMRect = svg.getBoundingClientRect();
canvas.width = box.width;
canvas.height = box.height;
if (imagemodeselected === 'width') {
const ratio = box.height / box.width;
canvas.width = userimagesize;
canvas.height = userimagesize * ratio;
} else if (imagemodeselected === 'height') {
const ratio = box.width / box.height;
canvas.width = userimagesize * ratio;
canvas.height = userimagesize;
}
const context = canvas.getContext('2d');
context.fillStyle = 'white';
context.fillRect(0, 0, canvas.width, canvas.height);
const image = new Image();
image.onload = exporter(context, image);
image.src = `data:image/svg+xml;base64,${getBase64SVG(svg, canvas.width, canvas.height)}`;
event.stopPropagation();
event.preventDefault();
};
const simulateDownload = (download: string, href: string): void => {
const a = document.createElement('a');
a.download = download;
a.href = href;
a.click();
a.remove();
};
const downloadImage: Exporter = (context, image) => {
return () => {
const { canvas } = context;
context.drawImage(image, 0, 0, canvas.width, canvas.height);
simulateDownload(
`mermaid-diagram-${moment().format('YYYYMMDDHHmmss')}.png`,
canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream')
);
};
};
const isClipboardAvailable = (): boolean => {
return Object.prototype.hasOwnProperty.call(window, 'ClipboardItem') as boolean;
};
const clipboardCopy: Exporter = (context, image) => {
return () => {
const { canvas } = context;
context.drawImage(image, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => {
try {
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1004/files
void navigator.clipboard.write([
/* eslint-disable no-undef */
// @ts-ignore: https://github.com/microsoft/TypeScript/issues/43821
new ClipboardItem({
[blob.type]: blob
})
]);
} catch (error) {
console.error(error);
}
});
};
};
const onCopyClipboard = (event: Event) => {
exportImage(event, clipboardCopy);
};
const onDownloadPNG = (event: Event) => {
exportImage(event, downloadImage);
};
const onDownloadSVG = () => {
simulateDownload(
`mermaid-diagram-${moment().format('YYYYMMDDHHmmss')}.svg`,
`data:image/svg+xml;base64,${getBase64SVG()}`
);
};
const onCopyMarkdown = () => {
(document.getElementById('markdown') as HTMLInputElement).select();
document.execCommand('Copy');
};
let gistURL = '';
codeStore.subscribe((state) => {
if (state.loader?.type === 'gist') {
// @ts-ignore Gist will have url
gistURL = state.loader.config.url;
}
});
const loadGist = () => {
if (!gistURL) {
alert('Please enter a Gist URL first');
}
window.location.href = `${window.location.pathname}?gist=${gistURL}`;
};
let iUrl: string;
let svgUrl: string;
let krokiUrl: string;
let mdCode: string;
let imagemodeselected = 'auto';
let userimagesize = 1080;
let isNetlify = false;
if (browser && ['mermaid.live', 'netlify'].some((path) => window.location.host.includes(path))) {
isNetlify = true;
}
serializedState.subscribe((encodedState: string) => {
iUrl = `${rendererUrl}/img/${encodedState}`;
svgUrl = `${rendererUrl}/svg/${encodedState}`;
krokiUrl = `${krokiRendererUrl}/mermaid/svg/${pakoSerde.serialize($codeStore.code)}`;
mdCode = `[![](${iUrl})](${window.location.protocol}//${window.location.host}${window.location.pathname}#${encodedState})`;
});
</script>
<Card title="Actions" isOpen={true}>
<div class="flex flex-wrap gap-2 m-2">
{#if isClipboardAvailable()}
<button class="action-btn w-full" on:click={onCopyClipboard}
><i class="far fa-copy mr-2" /> Copy Image to clipboard
</button>
{/if}
<button class="action-btn flex-auto" on:click={onDownloadPNG}>
<i class="fas fa-download mr-2" /> PNG
</button>
<button class="action-btn flex-auto" on:click={onDownloadSVG}>
<i class="fas fa-download mr-2" /> SVG
</button>
<button class="action-btn flex-auto">
<a target="_blank" href={iUrl}><i class="fas fa-external-link-alt mr-2" /> PNG</a>
</button>
<button class="action-btn flex-auto">
<a target="_blank" href={svgUrl}><i class="fas fa-external-link-alt mr-2" /> SVG</a>
</button>
<button class="action-btn flex-auto">
<a target="_blank" href={krokiUrl}><i class="fas fa-external-link-alt mr-2" /> Kroki</a>
</button>
<div class="flex gap-2 items-center">
PNG size
<label for="autosize">
<input type="radio" value="auto" id="autosize" bind:group={imagemodeselected} /> Auto
</label>
<label for="width">
<input type="radio" value="width" id="width" bind:group={imagemodeselected} /> Width
</label>
<label for="height">
<input type="radio" value="height" id="height" bind:group={imagemodeselected} /> Height
</label>
{#if imagemodeselected !== 'auto'}
<input
id="height"
class="input"
type="number"
min="3"
max="10000"
bind:value={userimagesize} />
{/if}
</div>
<div class="w-full flex gap-2 items-center">
<input class="input" id="markdown" type="text" value={mdCode} on:click={onCopyMarkdown} />
<label for="markdown">
<button class="btn btn-primary btn-md flex-auto" on:click={onCopyMarkdown}>
Copy Markdown
</button>
</label>
</div>
<div class="w-full flex gap-2 items-center">
<input
class="input"
id="gist"
type="text"
bind:value={gistURL}
placeholder="Enter Gist URL" />
<label for="gist">
<button class="btn btn-primary btn-md flex-auto" on:click={loadGist}> Load Gist </button>
</label>
</div>
{#if isNetlify}
<div class="w-full flex items-center justify-center">
<a class="link underline text-gray-500 text-sm" href="https://netlify.com">
This site is powered by Netlify
</a>
</div>
{/if}
</div>
</Card>
+29
View File
@@ -0,0 +1,29 @@
<script lang="ts">
import type { Tab } from '$lib/types';
import { slide } from 'svelte/transition';
import Tabs from './tabs.svelte';
export let isCloseable = true;
export let isOpen = true;
export let tabs: Tab[] = [];
export let title: string;
$: isOpen = isCloseable ? isOpen : true;
$: isTabsShown = isOpen && tabs.length > 0;
</script>
<div class="card rounded overflow-hidden m-2 flex-grow flex flex-col shadow-2xl">
<div
class="bg-primary p-2 {isTabsShown ? 'pb-0' : ''} flex-none cursor-pointer"
on:click={() => (isOpen = !isOpen)}>
<div class="flex justify-between">
<Tabs on:select {tabs} bind:isOpen {title} {isCloseable} />
<div class="flex gap-x-4 items-center {isTabsShown ? '-mt-2' : ''}">
<slot name="actions" />
</div>
</div>
</div>
{#if isOpen}
<div class="card-body p-0 flex-grow overflow-auto text-base-content" transition:slide>
<slot />
</div>
{/if}
</div>
+46
View File
@@ -0,0 +1,46 @@
<script lang="ts">
import type { Tab, TabEvents } from '$lib/types';
import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
export let isCloseable = true;
export let tabs: Tab[] = [];
export let title: string;
export let isOpen = false;
$: activeTabID = tabs[0]?.id;
const dispatch = createEventDispatcher<TabEvents>();
const toggleTabs = (tab: Tab) => {
activeTabID = tab.id;
dispatch('select', tab);
};
</script>
<div class="flex cursor-default">
<span class="mr-2 font-semibold" on:click|stopPropagation={() => (isOpen = !isOpen)}>
{#if isCloseable}
<i class="fas fa-chevron-right icon" class:isOpen />
{/if}
{title}</span>
{#if isOpen && tabs}
<ul class="tabs" transition:fade>
{#each tabs as tab}
<div
class="tab tab-lifted text-primary-content {activeTabID === tab.id ? 'tab-active' : ''}"
on:click|stopPropagation={() => toggleTabs(tab)}>
<i class="mr-1 {tab.icon}" />
{tab.title}
</div>
{/each}
</ul>
{/if}
</div>
<style>
.icon {
transition-duration: 0.5s;
}
.isOpen {
transform: rotate(90deg);
}
</style>
+85
View File
@@ -0,0 +1,85 @@
<script lang="ts">
import type { EditorEvents } from '$lib/types';
import { codeStore } from '$lib/util/state';
import { themeStore } from '$lib/util/theme';
import type monaco from 'monaco-editor';
import { createEventDispatcher, onMount } from 'svelte';
import { initEditor } from './util';
let divEl: HTMLDivElement = null;
let editor: monaco.editor.IStandaloneCodeEditor;
let Monaco;
export let text: string;
export let language: string;
export let editorOptions: monaco.editor.IStandaloneEditorConstructionOptions = {
value: text,
language: language,
minimap: {
enabled: false
},
theme: 'mermaid',
overviewRulerLanes: 0
};
export let errorMarkers: monaco.editor.IMarkerData[] = [];
let oldText = text;
$: editor && Monaco?.editor.setModelLanguage(editor.getModel(), language);
$: {
if (text !== oldText) {
if ($codeStore.updateEditor) {
editor?.setValue(text);
}
oldText = text;
}
editor && Monaco?.editor.setModelMarkers(editor.getModel(), 'test', errorMarkers);
}
themeStore.subscribe(({ isDark }) => {
editor && Monaco?.editor.setTheme(isDark ? 'mermaid-dark' : 'mermaid');
});
const dispatch = createEventDispatcher<EditorEvents>();
const loadMonaco = async () => {
let i = 0;
while (i++ < 10) {
try {
// @ts-ignore : This is a hack to handle a svelte-kit error when importing monaco.
Monaco = monaco;
return;
} catch {
await new Promise((r) => setTimeout(r, 500));
}
}
alert('Loading Monaco Editor failed. Please try refreshing the page.');
};
onMount(async () => {
try {
// @ts-ignore : This is a hack to handle a svelte-kit error when importing monaco.
Monaco = monaco;
} catch {
await loadMonaco(); // Fix https://github.com/mermaid-js/mermaid-live-editor/issues/175
}
initEditor(Monaco);
editor = Monaco.editor.create(divEl, editorOptions);
editor.onDidChangeModelContent(() => {
text = editor.getValue();
dispatch('update', {
text
});
});
Monaco?.editor.setTheme($themeStore.isDark ? 'mermaid-dark' : 'mermaid');
const resizeObserver = new ResizeObserver((entries) => {
editor.layout({
height: entries[0].contentRect.height,
width: entries[0].contentRect.width
});
});
resizeObserver.observe(divEl.parentElement);
return () => {
editor.dispose();
};
});
</script>
<div bind:this={divEl} id="editor" class="overflow-hidden" />
+552
View File
@@ -0,0 +1,552 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const initEditor = (monacoEditor): void => {
monacoEditor.languages.register({ id: 'mermaid' });
const requirementDiagrams = [
'requirement',
'functionalRequirement',
'interfaceRequirement',
'performanceRequirement',
'physicalRequirement',
'designConstraint'
];
const keywords: {
[key: string]: {
typeKeywords: string[];
blockKeywords: string[];
keywords: string[];
};
} = {
flowchart: {
typeKeywords: ['flowchart', 'flowchart-v2', 'graph'],
blockKeywords: ['subgraph', 'end'],
keywords: [
'TB',
'TD',
'BT',
'RL',
'LR',
'click',
'call',
'href',
'_self',
'_blank',
'_parent',
'_top',
'linkStyle',
'style',
'classDef',
'class',
'direction',
'interpolate'
]
},
sequenceDiagram: {
typeKeywords: ['sequenceDiagram'],
blockKeywords: ['alt', 'par', 'and', 'loop', 'else', 'end', 'rect', 'opt', 'alt', 'rect'],
keywords: [
'participant',
'as',
'Note',
'note',
'right of',
'left of',
'over',
'activate',
'deactivate',
'autonumber',
'title',
'actor'
]
},
classDiagram: {
typeKeywords: ['classDiagram', 'classDiagram-v2'],
blockKeywords: ['class'],
keywords: [
'link',
'click',
'callback',
'call',
'href',
'cssClass',
'direction',
'TB',
'BT',
'RL',
'LR'
]
},
stateDiagram: {
typeKeywords: ['stateDiagram', 'stateDiagram-v2'],
blockKeywords: ['state', 'note', 'end'],
keywords: ['state', 'as', 'hide empty description', 'direction', 'TB', 'BT', 'RL', 'LR']
},
erDiagram: {
typeKeywords: ['erDiagram'],
blockKeywords: [],
keywords: []
},
journey: {
typeKeywords: ['journey'],
blockKeywords: ['section'],
keywords: ['title']
},
info: {
typeKeywords: ['info'],
blockKeywords: [],
keywords: ['showInfo']
},
gantt: {
typeKeywords: ['gantt'],
blockKeywords: [],
keywords: [
'title',
'dateFormat',
'axisFormat',
'todayMarker',
'section',
'excludes',
'inclusiveEndDates'
]
},
requirementDiagram: {
typeKeywords: ['requirement', 'requirementDiagram'],
blockKeywords: requirementDiagrams.concat('element'),
keywords: []
},
gitGraph: {
typeKeywords: ['gitGraph'],
blockKeywords: [],
keywords: ['commit', 'branch', 'merge', 'reset', 'checkout', 'LR', 'BT']
},
pie: {
typeKeywords: ['pie'],
blockKeywords: [],
keywords: ['title', 'showData']
}
};
// Register a tokens provider for the mermaid language
monacoEditor.languages.setMonarchTokensProvider('mermaid', {
...Object.entries(keywords)
.map((entry) =>
Object.fromEntries(
Object.entries(entry[1]).map((deepEntry) => [
entry[0] + deepEntry[0][0].toUpperCase() + deepEntry[0].slice(1),
deepEntry[1]
])
)
)
.reduce((overallKeywords, nextKeyword) => ({ ...overallKeywords, ...nextKeyword }), {}),
tokenizer: {
root: [
[/%%(?=.*%%$)/, { token: 'string', nextEmbedded: 'json' }],
[/%%$/, { token: 'string', nextEmbedded: '@pop' }],
[/^\s*gitGraph/m, 'typeKeyword', 'gitGraph'],
[/^\s*info/m, 'typeKeyword', 'info'],
[/^\s*pie/m, 'typeKeyword', 'pie'],
[/^\s*(flowchart|flowchart-v2|graph)/m, 'typeKeyword', 'flowchart'],
[/^\s*sequenceDiagram/, 'typeKeyword', 'sequenceDiagram'],
[/^\s*classDiagram(-v2)?/, 'typeKeyword', 'classDiagram'],
[/^\s*journey/, 'typeKeyword', 'journey'],
[/^\s*gantt/, 'typeKeyword', 'gantt'],
[/^\s*stateDiagram(-v2)?/, 'typeKeyword', 'stateDiagram'],
[/^\s*erDiagram/, 'typeKeyword', 'erDiagram'],
[/^\s*requirement(Diagram)?/, 'typeKeyword', 'requirementDiagram'],
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment']
],
gitGraph: [
[/option(?=s)/, { token: 'typeKeyword', next: 'optionsGitGraph' }],
[/(^\s*branch|reset|merge|checkout)(.*$)/, ['keyword', 'variable']],
[
/[a-zA-Z][\w$]*/,
{
cases: {
'@gitGraphBlockKeywords': 'typeKeyword',
'@gitGraphKeywords': 'keyword'
}
}
],
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'],
[/".*?"/, 'string'],
[/\^/, 'delimiter.bracket']
],
optionsGitGraph: [
[/s$/, { token: 'typeKeyword', nextEmbedded: 'json', matchOnlyAtLineStart: false }],
['end', { token: 'typeKeyword', next: '@pop', nextEmbedded: '@pop' }]
],
info: [
[
/[a-zA-Z][\w$]*/,
{
cases: {
'@infoBlockKeywords': 'typeKeyword',
'@infoKeywords': 'keyword'
}
}
]
],
pie: [
[/(title)(.*$)/, ['keyword', 'string']],
[
/[a-zA-Z][\w$]*/,
{
cases: {
'@pieBlockKeywords': 'typeKeyword',
'@pieKeywords': 'keyword'
}
}
],
[/".*?"/, 'string'],
[/\s*\d+/, 'number'],
[/:/, 'delimiter.bracket'],
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment']
],
flowchart: [
[/[ox]?(--+|==+)[ox]/, 'transition'],
[
/[a-zA-Z][\w$]*/,
{
cases: {
'@flowchartBlockKeywords': 'typeKeyword',
'@flowchartKeywords': 'keyword',
'@default': 'variable'
}
}
],
[/\|+.+?\|+/, 'string'],
[/\[+(\\.+?[\\/]|\/.+?[/\\])\]+/, 'string'],
[/[[>]+[^\]|[]+?\]+/, 'string'],
[/{+.+?}+/, 'string'],
[/\(+.+?\)+/, 'string'],
[/-\.+->?/, 'transition'],
[/(-[-.])([^->]+?)(-{3,}|-{2,}>|\.-+>)/, ['transition', 'string', 'transition']],
[/(==+)([^=]+?)(={3,}|={2,}>)/, ['transition', 'string', 'transition']],
[/<?(--+|==+)>|===+|---+/, 'transition'],
[/:::/, 'transition'],
[/[;&]/, 'delimiter.bracket'],
[/".*?"/, 'string'],
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment']
],
sequenceDiagram: [
[
/[a-zA-Z][\w$]*/,
{
cases: {
'@sequenceDiagramBlockKeywords': 'typeKeyword',
'@sequenceDiagramKeywords': 'keyword',
'@default': 'variable'
}
}
],
[/(--?>?>|--?[)x])[+-]?/, 'transition'],
[/(:)([^:\n]*?$)/, ['delimiter.bracket', 'string']],
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment']
],
classDiagram: [
[/(?!class\s)([a-zA-Z]+)(\s+[a-zA-Z]+)/, ['type', 'variable']],
[/(\*|<\|?|o)?(--|\.\.)(\*|\|?>|o)?/, 'transition'],
[/^\s*class\s(?!.*\{)/, 'keyword'],
[
/[a-zA-Z][\w$]*/,
{
cases: {
'@classDiagramBlockKeywords': 'typeKeyword',
'@classDiagramKeywords': 'keyword',
'@default': 'variable'
}
}
],
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'],
[/(<<)(.+?)(>>)/, ['delimiter.bracket', 'annotation', 'delimiter.bracket']],
[/".*?"/, 'string'],
[/:::/, 'transition'],
[/:|\+|-|#|~|\*\s*$|\$\s*$|\(|\)|{|}/, 'delimiter.bracket']
],
journey: [
[/(title)(.*)/, ['keyword', 'string']],
[/(section)(.*)/, ['typeKeyword', 'string']],
[
/[a-zA-Z][\w$]*/,
{
cases: {
'@journeyBlockKeywords': 'typeKeyword',
'@journeyKeywords': 'keyword',
'@default': 'variable'
}
}
],
[
/(^\s*.+?)(:)(.*?)(:)(.*?)([,$])/,
[
'string',
'delimiter.bracket',
'number',
'delimiter.bracket',
'variable',
'delimiter.bracket'
]
],
[/,/, 'delimiter.bracket'],
[/(^\s*.+?)(:)([^:]*?)$/, ['string', 'delimiter.bracket', 'variable']],
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment']
],
gantt: [
[/(title)(.*)/, ['keyword', 'string']],
[/(section)(.*)/, ['typeKeyword', 'string']],
[
/[a-zA-Z][\w$]*/,
{
cases: {
'@ganttBlockKeywords': 'typeKeyword',
'@ganttKeywords': 'keyword'
}
}
],
[/(^\s*.*?)(:)/, ['string', 'delimiter.bracket']],
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'],
[/:/, 'delimiter.bracket']
],
stateDiagram: [
[/note[^:]*$/, { token: 'typeKeyword', next: 'stateDiagramNote' }],
['hide empty description', 'keyword'],
[/^\s*state\s(?!.*\{)/, 'keyword'],
[/(<<)(fork|join|choice)(>>)/, 'annotation'],
[/(\[\[)(fork|join|choice)(]])/, ['delimiter.bracket', 'annotation', 'delimiter.bracket']],
[
/[a-zA-Z][\w$]*/,
{
cases: {
'@stateDiagramBlockKeywords': 'typeKeyword',
'@stateDiagramKeywords': 'keyword',
'@default': 'variable'
}
}
],
[/".*?"/, 'string'],
[/(:)([^:\n]*?$)/, ['delimiter.bracket', 'string']],
[/{|}/, 'delimiter.bracket'],
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'],
[/-->/, 'transition'],
[/\[.*?]/, 'string']
],
stateDiagramNote: [
[/^\s*end note$/, { token: 'typeKeyword', next: '@pop' }],
[/.*/, 'string']
],
erDiagram: [
[/[}|][o|](--|\.\.)[o|][{|]/, 'transition'],
[/".*?"/, 'string'],
[/(:)(.*?$)/, ['delimiter.bracket', 'string']],
[/:|{|}/, 'delimiter.bracket'],
[/([a-zA-Z]+)(\s+[a-zA-Z]+)/, ['type', 'variable']],
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'],
[/[a-zA-Z_-][\w$]*/, 'variable']
],
requirementDiagram: [
[/->|<-|-/, 'transition'],
[/(\d+\.)*\d+/, 'number'],
[
/[a-zA-Z_-][\w$]*/,
{
cases: {
'@requirementDiagramBlockKeywords': 'typeKeyword',
'@default': 'variable'
}
}
],
[/:|{|}|\//, 'delimiter.bracket'],
[/%%[^$]([^%]*(?!%%$)%?)*$/, 'comment'],
[/".*?"/, 'string']
]
}
});
monacoEditor.editor.defineTheme('mermaid-dark', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'typeKeyword', foreground: '9650c8', fontStyle: 'bold' },
{ token: 'transition', foreground: '008800', fontStyle: 'bold' }
],
colors: {}
});
monacoEditor.editor.defineTheme('mermaid', {
base: 'vs',
inherit: true,
rules: [
{ token: 'typeKeyword', foreground: '9650c8', fontStyle: 'bold' },
{ token: 'keyword', foreground: '649696' },
{ token: 'custom-error', foreground: 'ff0000', fontStyle: 'bold' },
{ token: 'string', foreground: 'AA8500' },
{ token: 'transition', foreground: '008800', fontStyle: 'bold' },
{ token: 'delimiter.bracket', foreground: '000000', fontStyle: 'bold' },
{ token: 'annotation', foreground: '4b4b96' },
{ token: 'number', foreground: '4b4b96' },
{ token: 'comment', foreground: '888c89' },
{ token: 'variable', foreground: 'A22889' },
{ token: 'type', foreground: '2BDEA8' }
],
colors: {}
});
// Register a completion item provider for the mermaid language
monacoEditor.languages.registerCompletionItemProvider('mermaid', {
provideCompletionItems: () => {
const suggestions = [
{
label: 'loop',
kind: monacoEditor.languages.CompletionItemKind.Snippet,
insertText: ['loop ${1:Loop text}', '\t$0', 'end'].join('\n'),
insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Sequence Diagram Loops'
},
{
label: 'alt',
kind: monacoEditor.languages.CompletionItemKind.Snippet,
insertText: ['alt ${1:Describing text}', '\t$0', 'else', '\t', 'end'].join('\n'),
insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Alternative Path'
},
{
label: 'opt',
kind: monacoEditor.languages.CompletionItemKind.Snippet,
insertText: ['opt ${1:Describing text}', '\t$0', 'end'].join('\n'),
insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Optional Path'
},
{
label: 'par',
kind: monacoEditor.languages.CompletionItemKind.Snippet,
insertText: [
'par ${1:[Action 1]}',
'\t$0',
'and ${2:[Action 2]}',
'\t',
'and ${3:[Action 3]}',
'\t',
'end'
].join('\n'),
insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Parallel Actions'
},
{
label: 'rect',
kind: monacoEditor.languages.CompletionItemKind.Snippet,
insertText: ['rect ${1:rgb(0, 255, 0)}', '\t$0', 'end'].join('\n'),
insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Background Color'
},
{
label: 'subgraph',
kind: monacoEditor.languages.CompletionItemKind.Snippet,
insertText: ['subgraph ${1:title}', '\t$0', 'end'].join('\n'),
insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Subgraph'
},
{
label: 'class',
kind: monacoEditor.languages.CompletionItemKind.Snippet,
insertText: ['class ${1:className} {', '\t$0', '}'].join('\n'),
insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Class'
},
{
label: 'state',
kind: monacoEditor.languages.CompletionItemKind.Snippet,
insertText: ['state ${1:stateName} {', '\t$0', '}'].join('\n'),
insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'State'
},
{
label: 'note',
kind: monacoEditor.languages.CompletionItemKind.Snippet,
insertText: ['note ${1:right of State1}', '\t$0', 'end note'].join('\n'),
insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'State'
},
{
label: 'section',
kind: monacoEditor.languages.CompletionItemKind.Snippet,
insertText: ['section ${1:Go to work}', '\t$0'].join('\n'),
insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'User-journey Section'
},
{
label: 'element',
kind: monacoEditor.languages.CompletionItemKind.Snippet,
insertText: ['element ${1:test_entity} {', '\t$0', '}'].join('\n'),
insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Requirement Diagram Element'
},
{
label: 'options',
kind: monacoEditor.languages.CompletionItemKind.Snippet,
insertText: ['options', '{', ' $0', '}', 'end'].join('\n'),
insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'Git Graph Options'
},
...requirementDiagrams.map((requirementDiagramType) => ({
label: requirementDiagramType,
kind: monacoEditor.languages.CompletionItemKind.Snippet,
insertText: [
requirementDiagramType + ' ${1:test_req} {',
'\tid: 1',
'\ttext: the test text.',
'\trisk: high',
'\tverifyMethod: test',
'}'
].join('\n'),
insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: requirementDiagramType
.split(/(?=[A-Z])/)
.map((part) => part[0].toUpperCase() + part.slice(1))
.join(' ')
})),
...[
...new Set(
Object.values(keywords)
.map((diagramKeywords) =>
Object.entries(diagramKeywords)
.filter((keywordType) => keywordType[0] !== 'annotations')
.map((entry) => entry[1])
)
.flat(2)
)
].map((keyword) => ({
label: keyword,
kind: monacoEditor.languages.CompletionItemKind.Keyword,
insertText: keyword
}))
];
return { suggestions: suggestions };
}
});
monacoEditor.languages.setLanguageConfiguration('mermaid', {
autoClosingPairs: [
{
open: '(',
close: ')'
},
{
open: '{',
close: '}'
},
{
open: '[',
close: ']'
}
],
brackets: [
['(', ')'],
['{', '}'],
['[', ']']
]
});
};
+142
View File
@@ -0,0 +1,142 @@
<script lang="ts">
import Card from '$lib/components/card/card.svelte';
import { codeStore, getStateString } from '$lib/util/state';
import {
addHistoryEntry,
historyModeStore,
clearHistoryData,
getPreviousState,
historyStore,
loaderHistoryStore
} from './history';
import { notify, prompt } from '$lib/util/notify';
import { onMount } from 'svelte';
import moment from 'moment';
import type { HistoryType, State, Tab } from '$lib/types';
const HISTORY_SAVE_INTERVAL = 60000;
const tabSelectHandler = (message: CustomEvent<Tab>) => {
historyModeStore.set(message.detail.id as HistoryType);
};
let tabs: Tab[] = [
{
id: 'manual',
title: 'Saved',
icon: 'far fa-bookmark'
},
{
id: 'auto',
title: 'Timeline',
icon: 'fas fa-history'
}
];
const saveHistory = (auto = false) => {
const currentState: string = getStateString();
const previousState: string = getPreviousState(auto);
if (previousState !== currentState) {
addHistoryEntry({
state: $codeStore,
time: Date.now(),
type: auto ? 'auto' : 'manual'
});
} else if (!auto) {
notify('State already saved.');
}
};
const clearHistory = (date?: number): void => {
if (!date && !prompt('Clear all saved items?')) {
return;
}
clearHistoryData(date);
};
const restoreHistory = (state: State): void => {
codeStore.set({ ...state, updateEditor: true, updateDiagram: true });
};
const relativeTime = (time: number) => {
const t = new Date(time);
return `${new Date(t).toLocaleString()} (${moment(t).fromNow()})`;
};
onMount(() => {
historyModeStore.set('manual');
setInterval(() => {
saveHistory(true);
}, HISTORY_SAVE_INTERVAL);
});
loaderHistoryStore.subscribe((entries) => {
if (entries.length > 0 && tabs.length === 2) {
tabs = [
{
id: 'loader',
title: 'Revisions',
icon: 'fab fa-git-alt'
},
...tabs
];
historyModeStore.set('loader');
}
});
let isOpen = false;
</script>
<Card on:select={tabSelectHandler} bind:isOpen {tabs} title="History">
<div slot="actions">
<button
id="saveHistory"
class="btn btn-xs btn-success w-12"
on:click|stopPropagation={() => saveHistory()}
title="Save current state"><i class="far fa-save" /></button>
{#if $historyModeStore !== 'loader'}
<button
id="clearHistory"
class="btn btn-xs btn-error w-12"
on:click|stopPropagation={() => clearHistory()}
title="Delete all saved states"><i class="fas fa-trash-alt" /></button>
{/if}
</div>
<ul class="p-2 space-y-2 overflow-auto h-56" id="historyList">
{#if $historyStore.length > 0}
{#each $historyStore as { state, time, name, url, type }}
<li class="rounded p-2 shadow flex-col">
<div class="flex">
<div class="flex-1">
<div class="flex flex-col text-base-content">
{#if url}
<a
href={url}
target="_blank"
title="Open revision in new tab"
class="hover:underline text-blue-500">{name}</a>
{:else}
<span>{name}</span>
{/if}
<span class="text-gray-400 text-sm">{relativeTime(time)}</span>
</div>
</div>
<div class="flex gap-2 content-center">
<button class="btn btn-success" on:click={() => restoreHistory(state)}
><i class="fas fa-undo mr-1" />Restore</button>
{#if type !== 'loader'}
<button class="btn btn-error" on:click={() => clearHistory(time)}
><i class="fas fa-trash-alt mr-1" />Delete</button>
{/if}
</div>
</div>
</li>
{/each}
{:else}
<div class="m-2">
No items in History<br />
Click the Save button to save current state and restore it later.<br />
Timeline will automatically be saved every minute.
</div>
{/if}
</ul>
</Card>
+77
View File
@@ -0,0 +1,77 @@
import { derived, writable, get } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store';
import { persist, localStorage } from '@macfja/svelte-persistent-store';
import { generateSlug } from 'random-word-slugs';
import type { HistoryEntry, HistoryType } from '$lib/types';
const MAX_AUTO_HISTORY_LENGTH = 30;
export const historyModeStore: Writable<HistoryType> = persist(
writable('manual'),
localStorage(),
'autoHistoryMode'
);
const autoHistoryStore: Writable<HistoryEntry[]> = persist(
writable([]),
localStorage(),
'autoHistoryStore'
);
const manualHistoryStore: Writable<HistoryEntry[]> = persist(
writable([]),
localStorage(),
'manualHistoryStore'
);
export const loaderHistoryStore: Writable<HistoryEntry[]> = writable([] as HistoryEntry[]);
export const historyStore: Readable<HistoryEntry[]> = derived(
[historyModeStore, autoHistoryStore, manualHistoryStore, loaderHistoryStore],
([historyMode, autoHistories, manualHistories, loadedHistories], set) => {
if (historyMode === 'auto') {
set(autoHistories);
} else if (historyMode === 'manual') {
set(manualHistories);
} else if (historyMode === 'loader') {
set(loadedHistories);
} else {
set(autoHistories);
}
}
);
export const addHistoryEntry = (entry: HistoryEntry): void => {
if (entry.type === 'loader') {
loaderHistoryStore.update((entries) => [entry, ...entries]);
return;
}
entry.name = generateSlug(2);
if (entry.type !== 'auto') {
manualHistoryStore.update((entries) => [entry, ...entries]);
return;
}
autoHistoryStore.update((entries) => {
if (entries.length === MAX_AUTO_HISTORY_LENGTH) {
entries.pop();
}
return [entry, ...entries];
});
};
export const clearHistoryData = (time?: number): void => {
(get(historyModeStore) === 'auto' ? autoHistoryStore : manualHistoryStore).update((entries) => {
if (get(historyModeStore) !== 'loader') {
entries = entries.filter((entry) => time && entry.time != time);
}
return entries;
});
};
export const getPreviousState = (auto: boolean): string => {
const entries = get(auto ? autoHistoryStore : manualHistoryStore);
if (entries.length > 0) {
return JSON.stringify(entries[0].state);
}
return '';
};
+82
View File
@@ -0,0 +1,82 @@
<script context="module">
import { version } from 'mermaid/package.json';
import { analytics } from '$lib/util/stats';
analytics?.track('version', {
mermaidVersion: version
});
</script>
<script lang="ts">
import Theme from './theme.svelte';
interface Link {
title: string;
href: string;
icon?: string;
}
const links: Link[] = [
{
title: 'Documentation',
href: 'https://mermaid-js.github.io/mermaid/#/n00b-gettingStarted'
},
{
title: 'Tutorial',
href: 'https://github.com/mermaid-js/mermaid/blob/develop/docs/Tutorials.md'
},
{
title: 'Mermaid',
href: 'https://github.com/mermaid-js/mermaid'
},
{
title: 'CLI',
href: 'https://github.com/mermaid-js/mermaid-cli'
},
{
title: '',
href: 'https://github.com/mermaid-js/mermaid-live-editor',
icon: 'fab fa-github fa-lg'
}
];
</script>
<div class="navbar mb-2 shadow-lg bg-primary">
<div class="flex-1 px-2 mx-2">
<span class="text-lg font-bold">
<a href="/">Mermaid<span class="text-xs font-thin">v{version}</span> Live Editor</a>
</span>
</div>
<label for="menu-toggle" class="pointer-cursor lg:hidden block"
><svg
class="fill-current "
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
><title>Menu</title><path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z" /></svg
></label>
<input class="hidden" type="checkbox" id="menu-toggle" />
<Theme />
<div class="hidden lg:flex lg:items-center lg:w-auto w-full" id="menu">
<ul class="lg:flex items-center justify-between text-base pt-4 lg:pt-0">
{#each links as { title, href, icon }}
<li>
<a class="btn btn-ghost" target="_blank" {href}>
{#if icon}
<i class={icon} />
{/if}
{title}</a>
</li>
{/each}
</ul>
</div>
</div>
<style>
#menu-toggle:checked + #menu {
display: block;
}
.navbar {
z-index: 10000;
}
</style>
+97
View File
@@ -0,0 +1,97 @@
<script lang="ts">
import { updateCode } from '$lib/util/state';
import Card from '$lib/components/card/card.svelte';
const samples = {
'Flow Chart': `graph TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
C -->|One| D[Laptop]
C -->|Two| E[iPhone]
C -->|Three| F[fa:fa-car Car]`,
'Sequence Diagram': `sequenceDiagram
Alice->>+John: Hello John, how are you?
Alice->>+John: John, can you hear me?
John-->>-Alice: Hi Alice, I can hear you!
John-->>-Alice: I feel great!
`,
'Class Diagram': `classDiagram
Animal <|-- Duck
Animal <|-- Fish
Animal <|-- Zebra
Animal : +int age
Animal : +String gender
Animal: +isMammal()
Animal: +mate()
class Duck{
+String beakColor
+swim()
+quack()
}
class Fish{
-int sizeInFeet
-canEat()
}
class Zebra{
+bool is_wild
+run()
}
`,
'State Diagram': `stateDiagram-v2
[*] --> Still
Still --> [*]
Still --> Moving
Moving --> Still
Moving --> Crash
Crash --> [*]
`,
'Gantt Chart': `gantt
title A Gantt Diagram
dateFormat YYYY-MM-DD
section Section
A task :a1, 2014-01-01, 30d
Another task :after a1 , 20d
section Another
Task in sec :2014-01-12 , 12d
another task : 24d
`,
'Pie Chart': `pie title Pets adopted by volunteers
"Dogs" : 386
"Cats" : 85
"Rats" : 15
`,
'ER Diagram': `erDiagram
CUSTOMER }|..|{ DELIVERY-ADDRESS : has
CUSTOMER ||--o{ ORDER : places
CUSTOMER ||--o{ INVOICE : "liable for"
DELIVERY-ADDRESS ||--o{ ORDER : receives
INVOICE ||--|{ ORDER : covers
ORDER ||--|{ ORDER-ITEM : includes
PRODUCT-CATEGORY ||--|{ PRODUCT : contains
PRODUCT ||--o{ ORDER-ITEM : "ordered in"
`,
'User Journey': ` journey
title My working day
section Go to work
Make tea: 5: Me
Go upstairs: 3: Me
Do work: 1: Me, Cat
section Go home
Go downstairs: 5: Me
Sit down: 3: Me
`
};
const loadSampleDiagram = (diagramType: string): void => {
updateCode(samples[diagramType], true, true);
};
</script>
<Card title="Sample Diagrams" isOpen={false}>
<div class="flex gap-2 flex-wrap p-2">
{#each Object.keys(samples) as sample}
<button class="btn btn-primary normal-case btn-sm" on:click={() => loadSampleDiagram(sample)}
>{sample}</button>
{/each}
</div>
</Card>

Some files were not shown because too many files have changed in this diff Show More