Compare commits

..
9 Commits
Author SHA1 Message Date
github-actions 9a59fc4fe5 Release Beta 2021-01-14 12:45:04 +00:00
Sidharth Vinod 82e17f41aa Beta Deploy 2021-01-14 18:13:51 +05:30
github-actions 84c6a85545 Release 2021-01-14 12:29:56 +00:00
Sidharth Vinod 3bd5a640ec Try add 2021-01-14 17:58:49 +05:30
Sidharth Vinod 1cb42e0a88 Test build 2021-01-14 17:56:31 +05:30
Sidharth Vinod da53dab766 CI Test 2021-01-14 17:54:12 +05:30
Sidharth Vinod 9396ed376b Fix git 2021-01-14 17:50:46 +05:30
Sidharth Vinod 4bc257bace Fix install 2021-01-14 17:48:56 +05:30
Sidharth Vinod 395ea5b7f1 Beta CI Test 2021-01-14 17:47:36 +05:30
138 changed files with 12005 additions and 10488 deletions
-5
View File
@@ -1,5 +0,0 @@
**/node_modules
**/.git
**/.svelte-kit
**/dist
**/docs
-8
View File
@@ -1,8 +0,0 @@
docs/**
.svelte-kit/**
static/**
build/**
node_modules/**
coverage/**
__snapshots__/**
snapshots.js
-100
View File
@@ -1,100 +0,0 @@
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',
'prettier'
],
plugins: [
'svelte3',
'tailwindcss',
'@typescript-eslint',
'es',
'vitest',
'no-only-tests',
'unicorn'
],
ignorePatterns: [
'docs/*',
'*.cjs',
'*.js',
'*.md',
'snapshots.js',
'svelte.config.js',
'renovate.json',
'package.json',
'tsconfig.json'
],
overrides: [
{ files: ['*.svelte'], processor: 'svelte3/svelte3' },
{
files: ['*.ts'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/strict',
'prettier'
]
}
],
settings: {
'svelte3/typescript': () => require('typescript')
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
extraFileExtensions: ['.svelte'],
allowAutomaticSingleRunInference: true
},
env: {
browser: true,
es2020: 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',
curly: ['error', 'all'],
'no-only-tests/no-only-tests': 'error',
'unicorn/no-null': 'off',
'unicorn/filename-case': [
'error',
{
case: 'camelCase'
}
],
'unicorn/prevent-abbreviations': [
'error',
{
allowList: {
ctx: true,
db: true,
doc: true,
env: true,
fn: true,
i: true,
param: true,
req: true,
res: true,
str: true,
searchParams: true,
temp: true,
ImportMetaEnv: true
}
}
]
}
};
-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]
-41
View File
@@ -1,41 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Link to Live Editor:
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
-19
View File
@@ -1,19 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
-7
View File
@@ -1,7 +0,0 @@
name: 'Mermaid CodeQL config'
paths:
- src
paths-ignore:
- node_modules
- '**/*.spec.js'
-23
View File
@@ -1,23 +0,0 @@
version: 2
updates:
- package-ecosystem: docker
directory: /
target-branch: develop
schedule:
interval: weekly
day: friday
time: '07:00'
- package-ecosystem: github-actions
directory: /
target-branch: develop
schedule:
interval: weekly
day: friday
time: '07:00'
- package-ecosystem: npm
directory: /
target-branch: develop
schedule:
interval: weekly
day: friday
time: '07:00'
+34
View File
@@ -0,0 +1,34 @@
name: Beta Deploy
on:
push:
branches:
- none
# - citest
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout docs repo
uses: actions/checkout@v2
with:
repository: mermaid-js/docs
path: docs
- name: Checkout
uses: actions/checkout@v2
with:
path: main
- uses: actions/setup-node@v2
with:
node-version: '12'
- run: |
cd main
npm i -g yarn
yarn install
yarn release-beta
git config user.name github-actions
git config user.email github-actions@github.com
+29 -30
View File
@@ -9,14 +9,14 @@
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: 'CodeQL'
name: "CodeQL"
on:
push:
branches: [master]
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
branches: [ master ]
schedule:
- cron: '36 1 * * 2'
@@ -28,41 +28,40 @@ jobs:
strategy:
fail-fast: false
matrix:
language: ['javascript']
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
config-file: ./.github/codeql/config.yml
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
# - name: Autobuild
# uses: github/codeql-action/autobuild@v1
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+28
View File
@@ -0,0 +1,28 @@
name: Deploy Live Editor Beta
on:
push:
branches:
- master
- citest
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v2
with:
node-version: '12'
- name: Checkout
uses: actions/checkout@v2
- name: Build & Deploy
run: |
npm i -g yarn
yarn install
yarn release-beta
git config user.name github-actions
git config user.email github-actions@github.com
git add ./docs/*
git commit -m "Release Beta"
git push
+14 -25
View File
@@ -2,37 +2,26 @@ name: Deploy Live Editor
on:
push:
branches:
- master
pull_request:
branches:
- master
- develop
- release
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/setup-node@v2
with:
node-version: 18
cache: yarn
node-version: '12'
- name: Checkout
uses: actions/checkout@v2
- name: Build & Deploy
run: |
export DEPLOY=true
[ "$GITHUB_EVENT_NAME" != "pull_request" ] && rm -rf docs/_app/
npm i -g yarn
yarn install
version=$(yarn version --patch --no-git-tag-version | grep "New version" | cut -d':' -f 2)
yarn build
yarn run lint
cd ..
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
if: ${{ github.ref == 'refs/heads/master' }}
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs
keep_files: true
yarn release
git config user.name github-actions
git config user.email github-actions@github.com
git add ./docs/*
git commit -m "Release"
git push
-69
View File
@@ -1,69 +0,0 @@
name: Docker
on:
push:
# Publish `master` as Docker `latest` image.
branches:
- master
# Publish `v1.2.3` tags as releases.
tags:
- v*
# Run tests for all PRs to master and develop.
pull_request:
branches:
- master
- develop
env:
IMAGE_NAME: mermaid-live-editor
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run tests
run: |
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@v3
- 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
-20
View File
@@ -1,20 +0,0 @@
name: Mark stale issues and pull requests
on:
schedule:
- cron: '0 0 * * 4'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v8
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 90 days with no activity. Remove stale label or comment or this will be closed in 30 days'
stale-pr-message: 'This pr is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 30 days'
days-before-stale: 90
days-before-close: 30
days-before-pr-close: -1
-51
View File
@@ -1,51 +0,0 @@
name: Tests
on:
pull_request:
branches:
- master
- develop
jobs:
cypress-run:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# run 3 copies of the current job in parallel
containers: [1, 2, 3]
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/cache@v3
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-
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'yarn'
# Install NPM dependencies, cache them correctly
# and run all Cypress tests
- name: Cypress run
uses: cypress-io/github-action@v3
with:
build: yarn build
start: yarn preview
wait-on: 'http://localhost:3000'
record: true
headless: true
parallel: true
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
CYPRESS_CI: true
-36
View File
@@ -1,36 +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: yarn-and-build-cache
with:
path: |
build
node_modules
key: ${{ runner.os }}-node_modules-build-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-node_modules-build-
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'yarn'
- name: Lint & Test
run: |
yarn install
yarn lint
yarn test:unit
-32
View File
@@ -1,32 +0,0 @@
name: Update Browserslist database
on:
schedule:
- cron: '0 2 1,15 * *'
permissions:
contents: write
pull-requests: write
jobs:
update-browserslist-database:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
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'
-8
View File
@@ -4,11 +4,3 @@ coverage/
build/
yarn-error.log
.npmrc
.DS_Store
/.svelte-kit
/build
/functions
/snapshots.js
/cypress/downloads
/cypress/videos
/cypress/screenshots
-1
View File
@@ -1 +0,0 @@
_
-4
View File
@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn pre-commit
View File
-1
View File
@@ -1 +0,0 @@
engine-strict=true
BIN
View File
Binary file not shown.
+1 -8
View File
@@ -1,8 +1 @@
docs/**
.svelte-kit/**
static/**
build/**
node_modules/**
coverage/**
__snapshots__/**
snapshots.js
docs
+2 -6
View File
@@ -1,7 +1,3 @@
{
"singleQuote": true,
"svelteSortOrder": "options-scripts-markup-styles",
"bracketSameLine": true,
"trailingComma": "none",
"printWidth": 100
}
"singleQuote": true
}
-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}"
}
]
}
-27
View File
@@ -1,27 +0,0 @@
{
"editor.formatOnSave": true,
"cSpell.blockCheckingWhenLineLengthGreaterThan": 150,
"cSpell.words": [
"asyncable",
"Browserslist",
"ckppp",
"corg",
"cssnano",
"daisyui",
"esserializer",
"KROKI",
"localstorage",
"mermaidchart",
"mindmap",
"pako",
"Serde",
"serdes",
"tailwindcss",
"uparrow"
],
"vitest.commandLine": "yarn test:unit",
"vitest.enable": true,
"testing.autoRun.mode": "rerun",
"svelte.enable-ts-plugin": true,
"githubPullRequests.ignoredPullRequestBranches": ["develop"]
}
+5 -4
View File
@@ -1,17 +1,18 @@
# 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:8080 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:18.17.1 as mermaid-live-editor-builder
FROM node:13.6.0-alpine as mermaid-live-editor-builder
COPY --chown=node:node . /home
WORKDIR /home
RUN yarn install
RUN yarn build
FROM nginxinc/nginx-unprivileged:alpine as mermaid-live-editor-runner
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
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
@@ -1,8 +0,0 @@
FROM node:18.17.1
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
+30 -43
View File
@@ -1,6 +1,6 @@
[![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)
[![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)
# Contributors are welcome!
# Contributors are welcome
If you want to speed up the progress for mermaid-live-editor, join the slack channel and contact knsv.
@@ -8,6 +8,7 @@ If you want to speed up the progress for mermaid-live-editor, join the slack cha
Edit, preview and share mermaid charts/diagrams.
## Features
- Edit and preview flowcharts, sequence diagrams, gantt diagrams in real time.
@@ -15,60 +16,46 @@ Edit, preview and share mermaid charts/diagrams.
- Get a link to a viewer of the diagram so that you can share it with others.
- Get a link to edit the diagram so that someone else can tweak it and send a new link back
## Live demo
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
```
### To configure renderer URL
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 Environment variable MERMAID_KROKI_RENDERER_URL to your Kroki instance.
Default is `https://kroki.io`
### Development
```bash
docker compose up --build
```
Then open http://localhost:3000
You can try out a live version [here](https://mermaid-js.github.io/mermaid-live-editor/).
## Setup
Below link will help you making a copy of the repository in your local system.
This project is set up using [Yarn](https://classic.yarnpkg.com/en/docs/getting-started):
https://docs.github.com/en/get-started/quickstart/fork-a-repo
```
yarn install
```
## Requirements
or in develop branch to use the beta version of mermaid use
```
yarn install --update-checksums
```
This together with a .npmrc file:
```
registry=https://registry.npmjs.com/
@mermaid-js:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken="XXXXXXXXX"
```
- [volta](https://volta.sh/) to manage node versions.
- [Node.js](https://nodejs.org/en/). `volta install node`
- [yarn](https://yarnpkg.com/) package manager. `volta install yarn`
## Development
```sh
yarn install
yarn dev -- --open
```
yarn dev
open http://localhost:8080
```
This app is created with Svelte Kit.
This app is created with Svelte + svelte-spa-router.
## Release
When a PR is created targeting master, it will be built and deployed by Netlify.
The URL will be indicated in a Comment in the PR.
Once the PR is merged, it will automatically be released.
```
yarn release
```
-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.
+2 -9
View File
@@ -4,18 +4,11 @@
# git clone git@github.com:mermaid-js/docs.git
# git clone https://github.com/mermaid-js/docs.git
set -e
rm -rf docs
yarn release
pushd .
if [ ! -d ../docs ]; then
echo "Clone https://github.com/mermaid-js/docs to parent folder before continuing."
exit
fi
rm -rf ../docs/mermaid-live-editor-beta/*
cp -r docs/* ../docs/mermaid-live-editor-beta
cd ../docs
git add .
git commit -m "Beta updated with latest changes"
git commit * -m "Beta updated with latest changes"
git push
popd
-6
View File
@@ -1,6 +0,0 @@
#!/bin/bash
pushd ../docs
# find ./ -type f -exec sed -i 's/service-worker.js/mermaid-live-editor\/service-worker.js/g' {} \;
find ./ -type f -exec sed -i 's/manifest.json/\/mermaid-live-editor\/manifest.json/g' {} \;
find ./ -type f -exec sed -i 's/favicon.png/\/mermaid-live-editor\/favicon.png/g' {} \;
popd
-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.');
}
-35
View File
@@ -1,35 +0,0 @@
import { defineConfig } from 'cypress';
import fs from 'fs';
import { isFileExist, findFiles } from 'cy-verify-downloads';
export default defineConfig({
projectId: '2ckppp',
viewportWidth: 1440,
viewportHeight: 768,
snapshotFileName: './cypress/snapshots.js',
defaultCommandTimeout: 5000,
requestTimeout: 5000,
retries: {
runMode: 2,
openMode: 0
},
e2e: {
setupNodeEvents(on, config) {
on('task', {
isFileExist,
findFiles,
deleteFile(path) {
fs.rmSync(path);
return null;
},
readFileMaybe(filename) {
if (fs.existsSync(filename)) {
return fs.readFileSync(filename, 'utf8');
}
return null;
}
});
},
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.spec.ts'
}
});
-10
View File
@@ -1,10 +0,0 @@
{
"plugins": ["cypress"],
"extends": ["plugin:cypress/recommended"],
"rules": {
"jest/expect-expect": "off"
},
"env": {
"cypress/globals": true
}
}
-49
View File
@@ -1,49 +0,0 @@
import { typeInEditor, verifyFileSizeGreaterThan } from './util';
describe('Check actions', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/edit');
cy.contains('Actions').click();
});
it('should update markdown code', () => {
cy.get('#markdown')
.invoke('val')
.then((oldText) => {
typeInEditor('C --> HistoryTest', { bottom: true, newline: true });
cy.get('#markdown')
.invoke('val')
.then((newText) => {
expect(oldText).to.not.eq(newText);
});
});
});
it.skip('should load gists from URL', () => {
cy.get('#gist').type('https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a');
cy.contains('Load Gist').click();
cy.contains('Go shopping!!');
});
it('should download png and svg', () => {
cy.clock(new Date(2022, 0, 1).getTime());
cy.get(`#downloadPNG`).click();
verifyFileSizeGreaterThan('diagram', 'png', 34_000);
cy.get(`#downloadSVG`).click();
verifyFileSizeGreaterThan('diagram', 'svg', 10_000);
// Verify downloaded file is different for different diagrams
cy.contains('Sample Diagrams').click();
cy.contains('ER').click();
cy.get(`#downloadPNG`).click();
verifyFileSizeGreaterThan('diagram', 'png', 40_000);
cy.get(`#downloadSVG`).click();
verifyFileSizeGreaterThan('diagram', 'svg', 11_000);
cy.clock().invoke('restore');
});
});
-97
View File
@@ -1,97 +0,0 @@
import { typeInEditor, cmd } from './util';
describe('Auto sync tests', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/');
cy.url().should('contain', '/edit#pako');
cy.window().should('have.property', 'editorLoaded', true);
});
it('should dim diagram when code is edited', () => {
cy.contains('Auto sync').click();
cy.get('#view').should('not.have.class', 'outOfSync');
cy.get('#errorContainer').should('not.exist');
typeInEditor(' C --> Test', { bottom: true });
cy.get('#view').should('have.class', 'outOfSync');
cy.get('#errorContainer').should('contain.text', 'Diagram out of sync.');
cy.getLocalStorage('codeStore').snapshot();
});
it('should update diagram when shortcut is used', () => {
cy.contains('Auto sync').click();
cy.get('#view').should('not.have.class', 'outOfSync');
typeInEditor(' C --> Test');
cy.get('#view').should('have.class', 'outOfSync');
cy.get('#errorContainer').should('contain.text', 'Diagram out of sync.');
typeInEditor(`${cmd}{enter}`);
cy.get('#errorContainer').should('not.exist');
cy.get('#view').should('not.have.class', 'outOfSync');
});
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');
typeInEditor(' 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();
typeInEditor('ing');
cy.get('#view').should('not.have.class', 'outOfSync');
cy.getLocalStorage('codeStore').snapshot();
});
it('supports commenting code out/in', () => {
cy.get('#editor').contains('Car').click();
cy.get('#editor').get('textarea').type(`${cmd}/`, { force: true });
cy.get('#view').contains('Car').should('not.exist');
typeInEditor(`{uparrow}${cmd}/`);
cy.get('#view').contains('Car').should('exist');
});
it('supports editing code when code is incorrect', () => {
cy.visit(
'/edit#pako:eNpljjEKwzAMRa8SNOcEnlt6gK5eVFvYJsgOqkwpIXevg9smEE1PnyfxF3DFExgISW-CczQ2D21cYU7a-SGYXRwyvTp9jUhuKlVP-eHy7zA-leQsMEmg_QOM0BLG5FujZVMsaCQmC6ahR5ks2Lw2r84ela4-aREwKpVGwKrl_s7ut3fnkjAIcg_XDzuaUhs'
);
cy.get('#errorContainer').should('not.exist');
typeInEditor(`branch test`, { bottom: true, newline: true });
cy.get('#editor').contains('branch test').should('exist');
cy.get('#errorContainer')
.contains(
'Error: Trying to checkout branch which is not yet created. (Help try using "branch master")'
)
.should('exist');
});
it('should update diagram after entire text is removed', () => {
// https://github.com/mermaid-js/mermaid-live-editor/issues/1102
typeInEditor(`${cmd} a {backspace}`);
typeInEditor('graph LR');
typeInEditor(' {enter} A-->Car');
cy.get('#view').contains('Car').should('exist');
});
});
describe('Pan and Zoom', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/');
});
it('should toggle pan and zoom', () => {
cy.get('#svg-pan-zoom-reset-pan-zoom').should('not.exist');
cy.contains('Pan & Zoom').click();
cy.get('#svg-pan-zoom-reset-pan-zoom').should('exist');
cy.contains('Pan & Zoom').click();
cy.get('#svg-pan-zoom-reset-pan-zoom').should('not.exist');
});
});
-36
View File
@@ -1,36 +0,0 @@
describe('Editor docs tests', () => {
beforeEach(() => {
cy.on('uncaught:exception', () => {
return false;
});
cy.clearLocalStorage();
cy.visit('/edit');
cy.contains('Sample Diagrams').click();
});
it('Test default loading', () => {
cy.get(`[data-cy=docs][href^="https://mermaid-js.github.io/mermaid"]`).should('exist');
});
it('Test to see if the correct URL loads when changing from one diagram to other', () => {
cy.contains('Flow').click();
cy.get(`[data-cy=docs][href$="/syntax/flowchart"]`).should('exist');
cy.contains('Config').click();
cy.get(`[data-cy=docs][href$="/syntax/flowchart?id=configuration"]`).should('exist');
cy.contains('Sequence').click();
cy.get(`[data-cy=docs][href$="/syntax/sequenceDiagram?id=configuration"]`).should('exist');
cy.contains('Code').click();
cy.get(`[data-cy=docs][href$="/syntax/sequenceDiagram"]`).should('exist');
});
it("Test to check URLs for a case where config URL doesn't exist", () => {
cy.contains('State').click();
cy.get(`[data-cy=docs][href$="/syntax/stateDiagram"]`).should('exist');
cy.contains('Config').click();
cy.get(`[data-cy=docs][href$="/syntax/stateDiagram"]`).should('exist');
});
});
-103
View File
@@ -1,103 +0,0 @@
import { typeInEditor, verifyFileSnapshot } from './util';
describe('Save History', () => {
beforeEach(() => {
cy.clock(new Date(2022, 0, 1).getTime());
cy.clearLocalStorage();
cy.visit('/edit');
cy.contains('History').click();
});
afterEach(() => {
cy.clock().invoke('restore');
});
it('should load history from localstorage', () => {
cy.setLocalStorage(
'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","id":"d7ea820e-21dd-418a-b984-fd58acde09df","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","id":"b749ffc6-522b-4a44-86cf-7c1ffc3146b3","name":"helpful-ocean"}]'
);
cy.setLocalStorage(
'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","id":"69ea820e-522b-4a44-86cf-fd58acde09df","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","id":"x749ffc6-21dd-418a-b984-7c1ffc3146b3","name":"needy-mosquito"}]'
);
cy.reload();
cy.contains('History').click();
cy.get('#historyList').find('li').should('have.length', 2);
cy.get('#historyList').find('No items in History').should('not.exist');
cy.get('#historyList').contains('helpful-ocean');
cy.get('#historyList').contains('hollow-art');
cy.contains('Restore').click();
cy.contains('Halloween');
cy.contains('Timeline').click();
cy.get('#historyList').find('li').should('have.length', 2);
cy.get('#historyList').find('No items in History').should('not.exist');
cy.get('#historyList').contains('needy-mosquito');
cy.get('#historyList').contains('barking-dog');
cy.contains('Restore').click();
cy.contains('New Year');
});
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);
typeInEditor(' 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();
typeInEditor(' 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();
typeInEditor(' 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');
});
// TODO: Fix #639
xit('should auto save history', () => {
typeInEditor(' C --> HistoryTest');
cy.tick(70_000);
cy.contains('Timeline').click();
cy.get('#historyList').find('li').should('have.length', 1);
cy.get('#editor').type('ing');
cy.tick(70_000);
cy.get('#historyList').find('li').should('have.length', 2);
for (let i = 0; i < 31; i++) {
cy.get('#editor').type('.');
cy.tick(70_000);
}
cy.get('#historyList').find('li').should('have.length', 30);
});
it('should download history', () => {
cy.get('#saveHistory').click();
cy.get(`#downloadHistory`).click();
verifyFileSnapshot('history', 'json', 'A[Christmas] -->|Get money| B(Go shopping)');
});
});
-136
View File
@@ -1,136 +0,0 @@
import { toBase64 } from 'js-base64';
describe('Site Loads', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/');
cy.url().should('include', '/edit#pako');
});
it('Check Home page load', () => {
cy.url().should('include', '/edit');
cy.contains('History').click();
cy.getLocalStorage('codeStore').snapshot();
});
it('should keep code after reload', () => {
cy.get('#editor').contains('Car');
cy.reload();
cy.get('#editor').contains('Car');
});
it('Check Redirect from old URL', () => {
cy.visit(
'/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZylcbiAgICBCIC0tPiBDe0xldCBtZSB0aGlua31cbiAgICBDIC0tPnxPbmV8IERbTGFwdG9wXVxuICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdXG4gICAgQyAtLT58VGhyZWV8IEZbZmE6ZmEtY2FyIENhcl0iLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ'
);
cy.url().should('include', '/edit#pako:eNp');
});
it('should load sample diagrams when clicked', () => {
cy.contains('Sample Diagrams').click();
cy.contains('Pie').click();
cy.contains('pie title Pets adopted by volunteers');
cy.contains('Class').click();
cy.contains('classDiagram');
});
(Cypress.env('CI') === 'true' ? describe : describe.skip)('github', () => {
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\\"}","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.skip('should allow persisting "securityLevel" using confirm dialogue', () => {
const b64State = toBase64(
`{"code":"graph TD\\nA[\\"<img src='https://dummyimage.com/64' width=64/>\\"]","mermaid":"{\\"securityLevel\\": \\"loose\\", \\"theme\\": \\"forest\\"}","autoSync":true,"updateDiagram":true}`,
true
);
cy.on('window:confirm', () => false);
cy.visit(`/edit#${b64State}`);
cy.get('#editor').type(' ');
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.reload(true);
cy.contains('Please Click here to Raise an issue in github.');
});
});
describe('Verify types of URLs', () => {
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.contains('Animal');
});
describe('Uncompressed URLs', () => {
it('should load URL without specifier', () => {
cy.visit(
'/edit/#eyJjb2RlIjoiZ3JhcGhcbiAgICBUZXN0TGFiZWwiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGVmYXVsdFwiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ'
);
cy.contains('TestLabel');
cy.visit(
'/edit#eyJjb2RlIjoiY2xhc3NEaWFncmFtXG4gICAgQW5pbWFsIDx8LS0gRHVja1xuICAgIEFuaW1hbCA8fC0tIEZpc2hcbiAgICBBbmltYWwgPHwtLSBaZWJyYVxuICAgIEFuaW1hbCA6ICtpbnQgYWdlXG4gICAgQW5pbWFsIDogK1N0cmluZyBnZW5kZXJcbiAgICBBbmltYWw6ICtpc01hbW1hbCgpXG4gICAgQW5pbWFsOiArbWF0ZSgpXG4gICAgY2xhc3MgRHVja3tcbiAgICAgICtTdHJpbmcgYmVha0NvbG9yXG4gICAgICArc3dpbSgpXG4gICAgICArcXVhY2soKVxuICAgIH1cbiAgICBjbGFzcyBGaXNoe1xuICAgICAgLWludCBzaXplSW5GZWV0XG4gICAgICAtY2FuRWF0KClcbiAgICB9XG4gICAgY2xhc3MgWmVicmF7XG4gICAgICArYm9vbCBpc193aWxkXG4gICAgICArcnVuKClcbiAgICB9XG4gICAgICAgICAgICAiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGFya1wiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ'
);
cy.contains('Animal');
});
it('should load URL with "base64" specifier', () => {
cy.visit(
'/edit/#base64:eyJjb2RlIjoiZ3JhcGhcbiAgICBUZXN0TGFiZWwiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGVmYXVsdFwiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ'
);
cy.contains('TestLabel');
});
});
});
-56
View File
@@ -1,56 +0,0 @@
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('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', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/edit', {
onBeforeLoad(win) {
cy.stub(win, 'matchMedia')
.callThrough()
.withArgs('(prefers-color-scheme: dark)')
.returns({
matches: true
});
}
});
cy.contains('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();
});
});
});
-60
View File
@@ -1,60 +0,0 @@
export const cmd = `{${Cypress.platform === 'darwin' ? 'meta' : 'ctrl'}}`;
interface EditorOptions {
bottom?: boolean;
newline?: boolean;
}
export const typeInEditor = (
text: string,
{ bottom = true, newline = false }: EditorOptions = {}
) => {
cy.window().should('have.property', 'editorLoaded', true);
cy.get('#editor').click();
cy.get('#editor').within(($editor) => {
if (bottom) {
cy.get('textarea').type('{pageDown}', { force: true });
}
if (newline) {
cy.get('textarea').type('{enter}', { force: true });
}
cy.get('textarea').type(text, { force: true });
});
};
const downloadsFolder = Cypress.config('downloadsFolder');
export const verifyFileSizeGreaterThan = (
fileType: 'history' | 'diagram',
extension: string,
size: number
) => {
const fileName = `mermaid-${fileType}-2022-01-01-000000.${extension}`;
const filePath = `${downloadsFolder}/${fileName}`;
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
cy.verifyDownload(fileName);
cy.readFile(filePath, null, {
log: false
}).then((buffer: ArrayBuffer) => {
expect(buffer.byteLength).to.be.gt(size);
expect(buffer.byteLength).to.be.lt(size * 1.3);
});
cy.task('deleteFile', filePath);
};
export const verifyFileSnapshot = (
fileType: 'history' | 'diagram',
extension: string,
content: string
) => {
const fileName = `mermaid-${fileType}-2022-01-01-000000.${extension}`;
const filePath = `${downloadsFolder}/${fileName}`;
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
cy.verifyDownload(fileName);
cy.readFile(filePath, null, {
log: false
}).then((buffer: ArrayBuffer) =>
expect(new TextDecoder('utf8').decode(buffer)).to.contain(content)
);
cy.task('deleteFile', filePath);
};
-5
View File
@@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
-52
View File
@@ -1,52 +0,0 @@
module.exports = {
"Site Loads": {
"Check Home page load": {
"1": "{\"code\":\"flowchart 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}\",\"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}\",\"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}\",\"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}\",\"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}\",\"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": "12.17.4",
"Auto sync tests": {
"should dim diagram when code is edited": {
"1": "{\"code\":\"flowchart 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}\",\"autoSync\":false,\"updateDiagram\":false}"
},
"should not dim diagram when code is in sync": {
"1": "{\"code\":\"flowchart 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}\",\"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
@@ -1,38 +0,0 @@
// ***********************************************
// 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';
-28
View File
@@ -1,28 +0,0 @@
// ***********************************************************
// 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';
require('cy-verify-downloads').addCustomCommand();
// 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
@@ -1,7 +0,0 @@
{
"compilerOptions": {
"allowJs": true,
"types": ["cypress", "cypress-localstorage-commands", "cy-verify-downloads", "node"]
},
"include": ["**/*.ts"]
}
-11
View File
@@ -1,11 +0,0 @@
version: '3.7'
services:
mermaid:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- ./src:/app/src
ports:
- 3000:3000
- 24678:24678
+2481
View File
File diff suppressed because it is too large Load Diff
+419
View File
File diff suppressed because one or more lines are too long
+2481
View File
File diff suppressed because it is too large Load Diff
+419
View File
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

+134
View File
@@ -0,0 +1,134 @@
@media (prefers-color-scheme: light) {
html, body {
position: relative;
width: 100%;
height: 100%;
}
#dgraph-div {
display: none;
}
body {
color: #333;
margin: 0;
padding: 8px;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}
a {
color: rgb(0,100,200);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:visited {
color: rgb(0,80,160);
}
label {
display: block;
}
input, button, select, textarea {
font-family: inherit;
font-size: inherit;
padding: 0.4em;
margin: 0 0 0.5em 0;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
}
input:disabled {
color: #ccc;
}
input[type="range"] {
height: 0;
}
button {
background-color: #f4f4f4;
outline: none;
}
button:active {
background-color: #ddd;
}
button:focus {
border-color: #666;
}
}
body {
background-color: #3f3f3f;
color: #c8c8c8;
margin: 0;
padding: 8px;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
}}
@media (prefers-color-scheme: dark) {
html, body {
position: relative;
width: 100%;
height: 100%;
a {
color: rgb(0,100,200);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:visited {
color: rgb(0,80,160);
}
label {
display: block;
}
input, button, select, textarea {
font-family: inherit;
font-size: inherit;
padding: 0.4em;
margin: 0 0 0.5em 0;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 2px;
}
input:disabled {
color: #ccc;
}
input[type="range"] {
height: 0;
}
button {
background-color: #f4f4f4;
outline: none;
}
button:active {
background-color: #ddd;
}
button:focus {
border-color: #666;
}
}
+45
View File
@@ -0,0 +1,45 @@
<!doctype html>
<html>
<head>
<meta charset='utf8'>
<meta name='viewport' content='width=device-width'>
<title>Mermaid live editor</title>
<link rel='icon' type='image/png' href='favicon.png'>
<link rel='stylesheet' href='styles.css'>
<link rel='stylesheet' href='bundle.css'>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<!-- Google Analytics -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-153180559-1', 'auto');
</script>
<!-- End Google Analytics -->
<style>
.myInlineDecoration {
/* cursor: pointer; */
/* text-decoration: underline; */
font-weight: bold;
font-style: oblique;
background: #ffcccc;
}
#dgraph-div {
display: none;
}
</style>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>Online FlowChart &amp; Diagrams Editor - Mermaid Live Editor</title>
<meta name="og:image" content="https://mermaid-js.github.io/images/header.png">
<link rel="canonical" href="https://mermaid-js.github.io/mermaid-live-editor/">
<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.">
</head>
<body>
<script src='bundle.js'></script>
</body>
</html>
+43
View File
@@ -0,0 +1,43 @@
html {
padding: 0;
width: 100%;
height: 100%;
}
body {
padding: 32px;
font-family: 'trebuchet ms', verdana, arial;
position: relative;
width: 100%;
height: 100%;
box-sizing: border-box;
margin: 0;
}
/* @media (prefers-color-scheme: dark) {
body {
background-color: #3f3f3f;
color: #c8c8c8;
}
a, label {
color: #d8d8d8;
}
} */
.mermaid {
/* font-family: 'trebuchet ms', verdana, arial; */
}
.marketing-links {
font-size: 16px;
}
.ant-card {
margin-bottom: 16px;
}
i {
font-size: 10px !important;
}
.mermaidTooltip {
position: absolute;
}
-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;
}
}
+37 -86
View File
@@ -1,96 +1,47 @@
{
"name": "mermaid-live-editor",
"version": "2.0.67",
"type": "module",
"version": "1.2.0",
"license": "MIT",
"scripts": {
"dev": "vite dev",
"dev:force": "MERMAID_LOCAL=true yarn dev --force",
"dev:test": "yarn dev",
"build": "vite build",
"preview": "vite preview",
"lint": "prettier --check --cache --plugin-search-dir=. .;eslint --ignore-path .gitignore .",
"lint:fix": "prettier --write --cache --plugin-search-dir=. .;eslint --fix --ignore-path .gitignore .",
"format": "prettier --write --cache --plugin-search-dir=. .",
"pre-commit": "lint-staged",
"postinstall": "husky install && svelte-kit sync && (git config blame.ignoreRevsFile .git-blame-ignore-revs || true)",
"test:unit": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"test:browser": "cypress run",
"test": "test:unit && test:browser",
"cy": "cypress open"
},
"devDependencies": {
"@cypress/snapshot": "2.1.7",
"@sveltejs/adapter-static": "2.0.3",
"@sveltejs/kit": "1.25.0",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/svelte": "3.2.2",
"@types/pako": "2.0.0",
"@types/uuid": "9.0.4",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"@vitest/ui": "^0.34.0",
"autoprefixer": "^10.4.14",
"c8": "7.14.0",
"chai": "^4.3.7",
"cssnano": "^6.0.0",
"cy-verify-downloads": "0.2.0",
"cypress": "12.17.4",
"cypress-localstorage-commands": "2.2.4",
"eslint": "8.49.0",
"eslint-config-prettier": "8.10.0",
"eslint-plugin-cypress": "2.14.0",
"eslint-plugin-es": "4.1.0",
"eslint-plugin-no-only-tests": "^3.1.0",
"eslint-plugin-postcss-modules": "2.0.0",
"eslint-plugin-svelte3": "4.0.0",
"eslint-plugin-tailwindcss": "3.13.0",
"eslint-plugin-unicorn": "^46.0.0",
"eslint-plugin-vitest": "^0.3.0",
"esserializer": "1.3.11",
"font-awesome": "^4.7.0",
"husky": "^8.0.3",
"jsdom": "21.1.2",
"lint-staged": "13.3.0",
"node-html-parser": "^6.1.5",
"postcss": "^8.4.21",
"postcss-load-config": "4.0.1",
"prettier": "2.8.8",
"prettier-plugin-svelte": "^2.10.0",
"svelte": "3.59.2",
"svelte-preprocess": "5.0.4",
"tailwindcss": "^3.3.1",
"tslib": "^2.5.0",
"typescript": "5.2.2",
"vite": "^4.3.9",
"vitest": "^0.34.0"
"cross-env": "^5.2.0",
"css-loader": "^2.1.1",
"husky": "^4.3.0",
"lint-staged": "^10.5.2",
"mini-css-extract-plugin": "^0.6.0",
"prettier": "^2.2.1",
"prettier-plugin-svelte": "^1.4.1",
"serve": "^11.0.0",
"style-loader": "^0.23.1",
"svelte": "^3.0.0",
"svelte-loader": "2.13.3",
"svelte-watch-resize": "^1.0.3",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.0",
"webpack-dev-server": "^3.3.1"
},
"scripts": {
"release": "cross-env NODE_ENV=production webpack -p",
"release-beta": "yarn release --output-path ./docs/beta",
"build": "cross-env NODE_ENV=production webpack",
"dev": "webpack-dev-server --content-base docs",
"pretty": "prettier --write ./src/*"
},
"dependencies": {
"analytics": "0.8.9",
"analytics-plugin-plausible": "0.0.6",
"daisyui": "2.52.0",
"dayjs": "^1.11.7",
"js-base64": "3.7.5",
"mermaid": "10.5.0",
"monaco-editor": "0.43.0",
"pako": "2.1.0",
"random-word-slugs": "0.1.7",
"svg-pan-zoom": "3.6.1",
"uuid": "9.0.1"
"mermaid": "8.8.4",
"js-base64": "^2.5.1",
"json-lint": "^0.1.0",
"jsonlint": "^1.6.3",
"moment": "^2.24.0",
"monaco-editor": "0.18.1",
"svelte-routing": "^1.4.0",
"svelte-spa-router": "^1.3.0"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{ts,svelte,js,css,md,json}": [
"prettier --plugin-search-dir=. --write",
"eslint --ignore-path .gitignore "
]
},
"volta": {
"node": "18.17.1",
"yarn": "1.22.19"
},
"engines": {
"node": ">=16.7"
"*.{ts,svelte,js,css,md}": "prettier --plugin-search-dir=. --write"
}
}
-23
View File
@@ -1,23 +0,0 @@
const tailwindcss = require('tailwindcss');
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');
const mode = process.env.NODE_ENV;
const dev = mode === 'development';
module.exports = {
plugins: [
// Some plugins, like postcss-nested, need to run before Tailwind
tailwindcss,
// But others, like autoprefixer, need to run after
autoprefixer,
!dev &&
cssnano({
preset: 'default'
})
]
};
@@ -1,17 +1,13 @@
## :bookmark_tabs: Summary
Brief description about the content of your PR:
Resolves #<your issue id here>
## :straight_ruler: Design Decisions
Describe the way your implementation works or what design decisions you made if applicable:
### :clipboard: Tasks
Make sure you
- [ ] :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
- [ ] :book: have read the [contribution guidelines](https://github.com/mermaid-js/mermaid/blob/develop/CONTRIBUTING.md)
- [ ] :computer: have added unit/e2e tests (if appropriate)
- [ ] :bookmark: targeted `master` branch
-28
View File
@@ -1,28 +0,0 @@
{
"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
}
+31
View File
@@ -0,0 +1,31 @@
<script>
import Router from 'svelte-spa-router';
// Used for SSR. A falsy value is ignored by the Router.
import Edit from './routes/Edit.svelte';
import View from './routes/View.svelte';
const routes = {
// Exact path
'/': Edit,
// Using named parameters, with last being optional
'/edit/:data': Edit,
// Wildcard parameter
'/view/:data': View,
// Catch-all
// This is optional, but if present it must be the last
'*': Edit,
};
</script>
<style>
</style>
<svelte:head>
<link
href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
rel="stylesheet" />
</svelte:head>
<Router {routes} />
-29
View File
@@ -1,29 +0,0 @@
<!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="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="%sveltekit.assets%/favicon.svg" />
<link rel="mask-icon" href="%sveltekit.assets%/favicon.svg" color="#000000" />
<meta name="theme-color" content="#6366F1" />
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"
integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA=="
crossorigin="anonymous"
referrerpolicy="no-referrer" />
%sveltekit.head%
</head>
<body>
<div id="svelte">%sveltekit.body%</div>
</body>
</html>
-10
View File
@@ -1,10 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.input {
@apply flex-1 border-primary border-solid border-2 rounded;
}
.action-btn {
@apply btn btn-primary;
}
+3
View File
@@ -0,0 +1,3 @@
import { writable } from 'svelte/store';
export const codeErrorStore = writable(undefined);
+55
View File
@@ -0,0 +1,55 @@
import { writable, get } from 'svelte/store';
// import mermaid from '@mermaid-js/mermaid';
import mermaid from '@mermaid';
import { Base64 } from 'js-base64';
import { push, pop, replace } from 'svelte-spa-router';
export const codeStore = writable({});
export const fromUrl = (data) => {
let code;
let state;
const isDarkMode =
window.matchMedia('(prefers-color-scheme: dark)').matches && false;
try {
let stateStr = Base64.decode(data);
console.log('state from url', stateStr);
state = JSON.parse(stateStr);
if (state.code === undefined) {
// not valid json
// state = { code: '', mermaid: { theme: themeFromUrl } }
}
code = state.code;
} catch (e) {
// console.error('Init error', e);
code = `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]
`;
state = { code, mermaid: { theme: isDarkMode ? 'dark' : 'default' } };
}
codeStore.set(state);
};
export const updateCodeStore = (newState) => {
codeStore.set(newState);
replace('/edit/' + Base64.encodeURI(JSON.stringify(newState)));
};
export const updateCode = (code, updateEditor) => {
const state = get(codeStore);
state.code = code;
state.updateEditor = updateEditor;
codeStore.set(state);
};
export const updateConfig = (config) => {
const state = get(codeStore);
state.mermaid = config;
codeStore.set(state);
};
const unsubscribe = codeStore.subscribe((state) => {
replace('/edit/' + Base64.encodeURI(JSON.stringify(state)));
});
+36
View File
@@ -0,0 +1,36 @@
<script>
export let title = '';
export let noPadding = false;
let contentClass = 'padding';
if (noPadding) {
contentClass = '';
}
</script>
<style>
#card {
border: 1px solid lightgray;
margin-bottom: 16px;
}
#title {
font-family: 'Playfair Display', serif;
font-weight: 400;
border-bottom: 1px solid lightgray;
font-size: 1.25rem;
background-color: #1e60ab;
color: #eaebef;
}
#content {
padding-top: 8px;
}
.padding {
padding: 8px;
}
</style>
<div id="card">
<div id="title" class="padding">{title}</div>
<div id="content" class={contentClass}>
<slot />
</div>
</div>
+114
View File
@@ -0,0 +1,114 @@
<script>
import {
codeStore,
updateCodeStore,
updateConfig,
updateCode,
} from '../code-store.js';
import { configErrorStore } from '../config-error-store.js';
import { onMount } from 'svelte';
import { push, pop, replace } from 'svelte-spa-router';
import { Base64 } from 'js-base64';
// import mermaid from '@mermaid-js/mermaid';
import mermaid from '@mermaid';
import Error from './Error.svelte';
import { getResizeHandler, initEditor } from './editor-utils';
import { watchResize } from 'svelte-watch-resize';
import 'monaco-editor/esm/vs/editor/browser/controller/coreCommands.js';
import 'monaco-editor/esm/vs/editor/contrib/find/findController.js';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js';
export let conf = '';
export let code = '';
export let error = false;
let edit;
let editorElem = null;
let decorations = [];
const decArr = [];
let resizeHandler = () => {};
let oldConf = {};
const handleConfUpdate = (conf) => {
try {
console.log(conf);
JSON.parse(conf);
// let newState = { code, mermaid: JSON.parse(conf) };
// oldConf = newState.mermaid;
updateConfig(JSON.parse(conf));
configErrorStore.set(undefined);
// const model = edit.getModel();
// // model.setValue(conf);
// // model.dispose();
} catch (e) {
console.log('Error in parsed', e);
configErrorStore.set(e);
const str = JSON.stringify({ code, mermaid: oldConf });
replace('/edit/' + Base64.encodeURI(str));
}
};
const unsubscribe = codeStore.subscribe((state) => {
if (editorElem === null) {
console.log('Starting stuff', document.getElementById('editor-conf'));
editorElem = document.getElementById('editor-conf');
}
if (!conf && state) {
conf = JSON.stringify(state.mermaid, null, 2);
}
if (state) {
code = state.code;
}
if (!edit && conf && editorElem !== null) {
edit = monaco.editor.create(editorElem, {
value: [conf].join('\n'),
theme: 'myCoolTheme',
language: 'JSON',
});
resizeHandler = getResizeHandler(edit);
let decorations = [];
edit.onDidChangeModelContent(function (e) {
const conf = edit.getValue();
handleConfUpdate(conf);
});
handleConfUpdate(conf);
}
});
initEditor(monaco);
const unsubscribeError = configErrorStore.subscribe((_error) => {
// console.log('Error ideintified' + _error.toString());
if (_error) {
error = _error.toString();
} else {
error = false;
}
});
onMount(async () => {
console.log('Mounting config');
self.MonacoEnvironment = {
getWorkerUrl: function (moduleId, label) {
return './editor.worker.bundle.js';
},
};
});
</script>
<style>
#editor-conf {
width: 100%;
height: 200px;
max-height: 300px;
flex: 1;
}
</style>
<div id="editor-container">
<div id="editor-conf" use:watchResize={resizeHandler} />
{#if error}
<Error errorText={error} />
{/if}
</div>
+136
View File
@@ -0,0 +1,136 @@
<script>
import { codeStore, updateCodeStore } from '../code-store.js';
import { codeErrorStore } from '../code-error-store.js';
import { onMount } from 'svelte';
import { push, pop, replace } from 'svelte-spa-router';
import { Base64 } from 'js-base64';
// import mermaid from '@mermaid-js/mermaid';
import mermaid from '@mermaid';
import Error from './Error.svelte';
import { getResizeHandler, initEditor } from './editor-utils';
import 'monaco-editor/esm/vs/editor/browser/controller/coreCommands.js';
import 'monaco-editor/esm/vs/editor/contrib/find/findController.js';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js';
import { watchResize } from 'svelte-watch-resize';
export let code = '';
const isDarkMode =
window.matchMedia('(prefers-color-scheme: dark)').matches && false;
export let conf = { theme: isDarkMode ? 'dark' : 'default' };
let edit;
export let error = false;
let decorations = [];
const decArr = [];
let editorElem = null;
let resizeHandler = () => {};
const handleCodeUpdate = (code) => {
try {
mermaid.parse(code);
let newState = { code, mermaid: conf, updateEditor: false };
updateCodeStore(newState);
decArr.forEach((decor) => {
edit.deltaDecorations(decor, []);
});
codeErrorStore.set(undefined);
const model = edit.getModel();
} catch (e) {
if (e) {
codeErrorStore.set(e);
console.log('Error in parsed', e.hash);
const str = JSON.stringify({ code: code, mermaid: conf });
replace('/edit/' + Base64.encodeURI(str));
const l = e.hash.line;
decArr.push(
edit.deltaDecorations(
[],
[
{
range: new monaco.Range(
e.hash.loc.first_line,
e.hash.loc.last_line,
e.hash.loc.first_column,
e.hash.loc.last_column
),
options: { inlineClassName: 'myInlineDecoration' },
},
]
)
);
}
}
};
const unsubscribe = codeStore.subscribe((state) => {
console.log('Code change');
if (editorElem === null) {
editorElem = document.getElementById('editor');
}
if (!code && state) {
code = state.code;
}
if (state) {
conf = state.mermaid;
}
if (!edit && code && editorElem !== null) {
edit = monaco.editor.create(editorElem, {
value: [code].join('\n'),
theme: 'myCoolTheme',
language: 'mermaid',
});
resizeHandler = getResizeHandler(edit);
let decorations = [];
edit.onDidChangeModelContent(function (e) {
const code = edit.getValue();
handleCodeUpdate(code);
});
handleCodeUpdate(code);
}
if (state && state.updateEditor && edit && code && editorElem !== null) {
edit.setValue(state.code);
handleCodeUpdate(state.code);
}
});
const unsubscribeError = codeErrorStore.subscribe((_error) => {
if (_error) {
error = true;
} else {
error = false;
}
});
initEditor(monaco);
onMount(async () => {
// editorElem = document.querySelector('#editor')
self.MonacoEnvironment = {
getWorkerUrl: function (moduleId, label) {
return './editor.worker.bundle.js';
},
};
});
// export let name;
// export let params = {};
</script>
<style>
#editor-container {
}
#editor {
width: 100%;
height: 400px;
max-height: 300px;
flex: 1;
}
</style>
<div id="editor-container">
<div id="editor" use:watchResize={resizeHandler} />
{#if error}
<Error errorText="Syntax Error" />
{/if}
</div>
+49
View File
@@ -0,0 +1,49 @@
<script>
import { codeErrorStore } from '../code-error-store.js';
import { onMount } from 'svelte';
onMount(async () => {});
// export let code = '';
export let classes = '';
// export let error = {};
// export let token = '';
// export let expected = '';
// export let params = {}
export let errorText = '';
// const unsubscribeError = codeErrorStore.subscribe( _error => {
// if(typeof _error === 'undefined') {
// classes = 'invisible';
// error = {};
// token='';
// expected='';
// } else {
// classes = 'visible';
// error = _error;
// console.log('error: ', _error);
// token = error.hash.token;
// expected = error.hash.expected.join(' ');;
// }
// });
</script>
<style>
#error {
background: darkred;
color: white;
flex: 1;
padding: 10px;
/* position: absolute; */
}
.invisible {
display: none;
}
.visible {
display: block;
}
</style>
<div id="error" class={classes}>{errorText}</div>
+228
View File
@@ -0,0 +1,228 @@
<script>
import { link } from 'svelte-spa-router';
import { Base64 } from 'js-base64';
import moment from 'moment';
import { codeStore } from '../code-store.js';
const getBase64SVG = () => {
const container = document.getElementById('container');
const svg = container.innerHTML.replaceAll('<br>', '<br/>');
return Base64.encode(svg);
};
const exportImage = (event, exporter) => {
const canvas = document.createElement('canvas');
const svg = document.querySelector('#container svg');
const box = svg.getBoundingClientRect();
canvas.width = box.width;
canvas.height = box.height;
if (imagemodeselected === 'width') {
const ratio = box.height / box.width;
canvas.width = userimagewidth;
canvas.height = userimagewidth * ratio;
} else if (imagemodeselected === 'height') {
const ratio = box.width / box.height;
canvas.width = userimageheight * ratio;
canvas.height = userimageheight;
}
const context = canvas.getContext('2d');
context.fillStyle = 'white';
context.fillRect(0, 0, canvas.width, canvas.height);
const image = new Image();
image.onload = exporter(canvas, context, image);
console.log('SVG', getBase64SVG());
image.src = `data:image/svg+xml;base64,${getBase64SVG()}`;
event.stopPropagation();
event.preventDefault();
};
const downloadImage = (canvas, context, image) => {
return () => {
context.drawImage(image, 0, 0, canvas.width, canvas.height);
const a = document.createElement('a');
a.download = `mermaid-diagram-${moment().format('YYYYMMDDHHmmss')}.png`;
a.href = canvas
.toDataURL('image/png')
.replace('image/png', 'image/octet-stream');
a.click();
};
};
const clipboardCopy = (canvas, context, image) => {
return () => {
context.drawImage(image, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => {
try {
navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob,
}),
]);
} catch (error) {
console.error(error);
}
});
};
};
const onCopyClipboard = (event) => {
exportImage(event, clipboardCopy);
};
const onDownloadPNG = (event) => {
exportImage(event, downloadImage);
};
const onDownloadSVG = (event) => {
console.log('event', event.target);
event.target.href = `data:image/svg+xml;base64,${getBase64SVG()}`;
event.target.download = `mermaid-diagram-${moment().format(
'YYYYMMDDHHmmss'
)}.svg`;
console.log('event', event);
};
const onCopyMarkdown = (event) => {
event.target.select();
document.execCommand('Copy');
};
let url = '/mermaid-live-editor/#/view';
let b64Code;
let iUrl;
let svgUrl;
let mdCode;
let imagemodeselected = 'auto';
let userimagewidth = 1920;
let userimageheight = 1080;
const unsubscribe = codeStore.subscribe((state) => {
b64Code = Base64.encodeURI(JSON.stringify(state));
url = `/mermaid-live-editor/#/view/${b64Code}`;
iUrl = `https://mermaid.ink/img/${b64Code}`;
svgUrl = `https://mermaid.ink/svg/${b64Code}`;
mdCode = `[![](${iUrl})](${window.location.protocol}//${window.location.host}${window.location.pathname}#/edit/${b64Code})`;
});
</script>
<style>
#links {
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid lightgray;
}
#markdown {
padding: 7px;
font-family: monospace;
font-size: 14px;
width: 95%;
margin: 1rem 0;
border: 1px solid lightgray;
}
label[for='markdown'] {
cursor: pointer;
margin: 0 auto;
}
/*
@media (prefers-color-scheme: dark) {
label[for="markdown"] {
color: #d8d8d8;
}
} */
.button-style {
background-color: #a2d9e2;
color: #33a2c4;
border-radius: 0.25rem;
padding: 0.5rem;
border: 1px solid #a2d9e2;
margin: 0.25rem;
}
.button-style:hover {
background-color: #fff;
color: #33a2c4;
border: 1px solid #33a2c4;
}
.button-style:focus {
outline: none;
}
.link-style {
text-decoration: none;
color: #33a2c4;
}
#copy-section {
padding-top: 1rem;
text-align: center;
}
</style>
<div id="links">
<button class="button-style">
<a class="link-style" href={url} download="" on:click={onCopyClipboard}>
Copy Image
</a>
</button>
<button class="button-style">
<a class="link-style" href={url} download="" on:click={onDownloadPNG}>
Download PNG
</a>
</button>
<button class="button-style">
<a class="link-style" href={url} use:link>Link to view</a>
</button>
<button class="button-style">
<a class="link-style" href={url} download="" on:click={onDownloadSVG}>
Download SVG
</a>
</button>
<button class="button-style">
<a class="link-style" href={iUrl}>Link to Image</a>
</button>
<button class="button-style">
<a class="link-style" href={svgUrl}>Link to SVG</a>
</button>
(markdown is base64 encoded for these urls)
</div>
<div id="copy-section">
<label for="markdown" class="button-style">Copy Markdown</label>
<br />
<input id="markdown" type="text" value={mdCode} on:click={onCopyMarkdown} />
</div>
<p>
<label>PNG size:</label>
<input
type="radio"
value="auto"
id="autosize"
bind:group={imagemodeselected} />
<label for="autosize">auto</label>
<input
type="radio"
value="width"
id="width-active"
bind:group={imagemodeselected} />
<label for="width">width</label>
<input
id="width"
type="number"
min="3"
max="10000"
bind:value={userimagewidth}
disabled={imagemodeselected !== 'width'} />
<input
type="radio"
value="height"
id="height-active"
bind:group={imagemodeselected} />
<label for="height">height</label>
<input
id="height"
type="number"
min="3"
max="10000"
bind:value={userimageheight}
disabled={imagemodeselected !== 'height'} /><br />
</p>
+31
View File
@@ -0,0 +1,31 @@
<script>
export let noPadding = false;
let contentClass = 'padding';
if (noPadding) {
contentClass = '';
}
</script>
<style>
#tag {
height: auto;
margin: 0 8px 0 0;
padding: 0 7px;
font-size: 12px;
line-height: 20px;
white-space: nowrap;
border: 1px solid #d9d9d9;
border-radius: 4px;
cursor: default;
font-size: smaller;
/* padding: 8px; */
color: #52c41a;
background: #f6ffed;
border-color: #b7eb8f;
margin-left: 8px;
}
</style>
<div id="tag">
<slot />
</div>
+124
View File
@@ -0,0 +1,124 @@
<script>
import { codeStore } from '../code-store.js';
import { codeErrorStore } from '../code-error-store.js';
import { configErrorStore } from '../config-error-store.js';
import { onMount } from 'svelte';
// import mermaid from '@mermaid-js/mermaid';
import mermaid from '@mermaid';
const detectType = (text) => {
text = text.replace(/^\s*%%.*\n/g, '\n');
console.debug('Detecting diagram type based on the text ' + text);
if (text.match(/^\s*sequenceDiagram/)) {
return 'sequence';
}
if (text.match(/^\s*gantt/)) {
return 'gantt';
}
if (text.match(/^\s*classDiagram/)) {
return 'class';
}
if (text.match(/^\s*stateDiagram/)) {
return 'state';
}
if (text.match(/^\s*gitGraph/)) {
return 'git';
}
if (text.match(/^\s*flowchart/)) {
return 'flowchart';
}
if (text.match(/^\s*info/)) {
return 'info';
}
if (text.match(/^\s*pie/)) {
return 'pie';
}
return 'flowchart';
};
// manual debounce
let timeout;
const saveStatistcs = (graphType) => {
clearTimeout(timeout);
// Only save statistcs after a 5 sec delay
timeout = setTimeout(function () {
console.log('ga:', 'send', 'event', 'render', graphType, graphType);
ga('send', 'event', graphType, 'render', 'render');
}, 5000);
};
let element;
let container;
onMount(async () => {
element = document.querySelector('graph-div');
});
let insertSvg = function (svgCode, bindFunctions) {
// element.innerHTML = svgCode;
};
export let code = '';
export let configClasses = '';
export let codeClasses = '';
const unsubscribe = codeStore.subscribe((state) => {
try {
if (container && state) {
code = state.code;
// mermaid.parse(code)
// Replacing special characters '<' and '>' with encoded '&lt;' and '&gt;'
let _code = code;
_code = _code.replace(/</g, '&lt;');
_code = _code.replace(/>/g, '&gt;');
container.innerHTML = _code;
saveStatistcs(detectType(code));
delete container.dataset.processed;
mermaid.initialize(Object.assign({}, state.mermaid));
mermaid.init(undefined, container);
if (code) mermaid.render('graph-div', code, insertSvg);
}
} catch (e) {
console.log('view fail', e);
}
});
const unsubscribeError = codeErrorStore.subscribe((_error) => {
if (typeof _error === 'undefined') {
codeClasses = '';
} else {
codeClasses = 'error';
console.log('code error: ', _error);
}
});
const unsubscribeConfigError = configErrorStore.subscribe((_error) => {
if (typeof _error === 'undefined') {
configClasses = '';
} else {
configClasses = 'error';
console.log('conf error: ', _error);
}
});
</script>
<style>
#view {
border: 1px solor darkred;
flex: 1;
}
#container {
overflow-x: auto;
}
.error {
opacity: 0.5;
}
</style>
<div id="view" class="{codeClasses} {configClasses}">
<div id="container" bind:this={container} />
</div>
+86
View File
@@ -0,0 +1,86 @@
export const initEditor = (monaco) => {
monaco.languages.register({ id: 'mermaid' });
// Register a tokens provider for the language
monaco.languages.setMonarchTokensProvider('mermaid', {
typeKeywords: [
'graph',
'stateDiagram',
'sequenceDiagram',
'classDiagram',
'pie',
'flowchart',
'gantt',
],
keywords: ['patricipant', 'as'],
arrows: ['---', '===', '-->', '==>'],
tokenizer: {
root: [
[/[{}]/, 'delimiter.bracket'],
[
/[a-z_$][\w$]*/,
{ cases: { '@typeKeywords': 'keyword', '@keywords': 'keyword' } },
],
[/[-=>ox]+/, { cases: { '@arrows': 'transition' } }],
[/[\[\{\(}]+.+?[\)\]\}]+/, 'string'],
[/\".*\"/, 'string'],
],
},
whitespace: [
[/[ \t\r\n]+/, 'white'],
[/\%\%.*$/, 'comment'],
],
});
monaco.editor.defineTheme('myCoolTheme', {
base: 'vs',
inherit: false,
rules: [
{ token: 'keyword', foreground: '880000', fontStyle: 'bold' },
{ token: 'custom-error', foreground: 'ff0000', fontStyle: 'bold' },
{ token: 'string', foreground: 'AA8500' },
{ token: 'transition', foreground: '008800', fontStyle: 'bold' },
{ token: 'delimiter.bracket', foreground: '000000', fontStyle: 'bold' },
],
});
// Register a completion item provider for the new language
monaco.languages.registerCompletionItemProvider('mermaid', {
provideCompletionItems: () => {
var suggestions = [
{
label: 'simpleText',
kind: monaco.languages.CompletionItemKind.Text,
insertText: 'simpleText',
},
{
label: 'testing',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'testing(${1:condition})',
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
},
{
label: 'ifelse',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: [
'if (${1:condition}) {',
'\t$0',
'} else {',
'\t',
'}',
].join('\n'),
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'If-Else Statement',
},
];
return { suggestions: suggestions };
},
});
};
export const getResizeHandler = (editor) => {
return (node) => editor && editor.layout();
};
+3
View File
@@ -0,0 +1,3 @@
import { writable } from 'svelte/store';
export const configErrorStore = writable(undefined);
+168
View File
@@ -0,0 +1,168 @@
// (1) Desired editor features:
import 'monaco-editor/esm/vs/editor/browser/controller/coreCommands.js';
// import 'monaco-editor/esm/vs/editor/browser/widget/codeEditorWidget.js';
// import 'monaco-editor/esm/vs/editor/browser/widget/diffEditorWidget.js';
// import 'monaco-editor/esm/vs/editor/browser/widget/diffNavigator.js';
// import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/bracketMatching.js';
// import 'monaco-editor/esm/vs/editor/contrib/caretOperations/caretOperations.js';
// import 'monaco-editor/esm/vs/editor/contrib/caretOperations/transpose.js';
// import 'monaco-editor/esm/vs/editor/contrib/clipboard/clipboard.js';
// import 'monaco-editor/esm/vs/editor/contrib/codelens/codelensController.js';
// import 'monaco-editor/esm/vs/editor/contrib/colorPicker/colorDetector.js';
// import 'monaco-editor/esm/vs/editor/contrib/comment/comment.js';
// import 'monaco-editor/esm/vs/editor/contrib/contextmenu/contextmenu.js';
// import 'monaco-editor/esm/vs/editor/contrib/cursorUndo/cursorUndo.js';
// import 'monaco-editor/esm/vs/editor/contrib/dnd/dnd.js';
import 'monaco-editor/esm/vs/editor/contrib/find/findController.js';
// import 'monaco-editor/esm/vs/editor/contrib/folding/folding.js';
// import 'monaco-editor/esm/vs/editor/contrib/format/formatActions.js';
// import 'monaco-editor/esm/vs/editor/contrib/goToDeclaration/goToDeclarationCommands.js';
// import 'monaco-editor/esm/vs/editor/contrib/goToDeclaration/goToDeclarationMouse.js';
// import 'monaco-editor/esm/vs/editor/contrib/gotoError/gotoError.js';
// import 'monaco-editor/esm/vs/editor/contrib/hover/hover.js';
// import 'monaco-editor/esm/vs/editor/contrib/inPlaceReplace/inPlaceReplace.js';
// import 'monaco-editor/esm/vs/editor/contrib/linesOperations/linesOperations.js';
// import 'monaco-editor/esm/vs/editor/contrib/links/links.js';
// import 'monaco-editor/esm/vs/editor/contrib/multicursor/multicursor.js';
// import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints.js';
// import 'monaco-editor/esm/vs/editor/contrib/quickFix/quickFixCommands.js';
// import 'monaco-editor/esm/vs/editor/contrib/referenceSearch/referenceSearch.js';
// import 'monaco-editor/esm/vs/editor/contrib/rename/rename.js';
// import 'monaco-editor/esm/vs/editor/contrib/smartSelect/smartSelect.js';
// import 'monaco-editor/esm/vs/editor/contrib/snippet/snippetController2.js';
// import 'monaco-editor/esm/vs/editor/contrib/suggest/suggestController.js';
// import 'monaco-editor/esm/vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode.js';
// import 'monaco-editor/esm/vs/editor/contrib/wordHighlighter/wordHighlighter.js';
// import 'monaco-editor/esm/vs/editor/contrib/wordOperations/wordOperations.js';
// import 'monaco-editor/esm/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.js';
// import 'monaco-editor/esm/vs/editor/standalone/browser/inspectTokens/inspectTokens.js';
// import 'monaco-editor/esm/vs/editor/standalone/browser/iPadShowKeyboard/iPadShowKeyboard.js';
// import 'monaco-editor/esm/vs/editor/standalone/browser/quickOpen/quickOutline.js';
// import 'monaco-editor/esm/vs/editor/standalone/browser/quickOpen/gotoLine.js';
// import 'monaco-editor/esm/vs/editor/standalone/browser/quickOpen/quickCommand.js';
// import 'monaco-editor/esm/vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast.js';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js';
// (2) Desired languages:
// import 'monaco-editor/esm/vs/language/typescript/monaco.contribution';
// import 'monaco-editor/esm/vs/language/css/monaco.contribution';
// import 'monaco-editor/esm/vs/language/json/monaco.contribution';
// import 'monaco-editor/esm/vs/language/html/monaco.contribution';
// import 'monaco-editor/esm/vs/basic-languages/bat/bat.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/coffee/coffee.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/cpp/cpp.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/csharp/csharp.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/csp/csp.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/css/css.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/dockerfile/dockerfile.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/fsharp/fsharp.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/go/go.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/handlebars/handlebars.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/html/html.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/ini/ini.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/java/java.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/less/less.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/lua/lua.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/msdax/msdax.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/mysql/mysql.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/objective-c/objective-c.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/pgsql/pgsql.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/php/php.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/postiats/postiats.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/powershell/powershell.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/pug/pug.contribution.js';
import 'monaco-editor/esm/vs/basic-languages/python/python.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/r/r.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/razor/razor.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/redis/redis.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/redshift/redshift.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/ruby/ruby.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/sb/sb.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/scss/scss.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/solidity/solidity.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/swift/swift.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/vb/vb.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/xml/xml.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js';
// import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution';
// import 'monaco-editor/esm/vs/basic-languages/typescript/typescript.contribution';
monaco.languages.register({ id: 'mermaid' });
// Register a tokens provider for the language
monaco.languages.setMonarchTokensProvider('mermaid', {
tokenizer: {
root: [
[/\[graph.*/, 'custom-error'],
[/\[notice.*/, 'custom-notice'],
[/\[info.*/, 'custom-info'],
[/\[[a-zA-Z 0-9:]+\]/, 'custom-date'],
],
},
});
self.MonacoEnvironment = {
getWorkerUrl: function (moduleId, label) {
return './editor.worker.bundle.js';
},
};
// Define a new theme that contains only rules that match this language
monaco.editor.defineTheme('myCoolTheme', {
base: 'vs',
inherit: false,
rules: [
{ token: 'custom-info', foreground: '808080' },
{ token: 'custom-error', foreground: 'ff0000', fontStyle: 'bold' },
{ token: 'custom-notice', foreground: 'FFA500' },
{ token: 'custom-date', foreground: '008800' },
],
});
// Register a completion item provider for the new language
monaco.languages.registerCompletionItemProvider('mySpecialLanguage', {
provideCompletionItems: () => {
var suggestions = [
{
label: 'simpleText',
kind: monaco.languages.CompletionItemKind.Text,
insertText: 'simpleText',
},
{
label: 'testing',
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: 'testing(${1:condition})',
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
},
{
label: 'ifelse',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: [
'if (${1:condition}) {',
'\t$0',
'} else {',
'\t',
'}',
].join('\n'),
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'If-Else Statement',
},
];
return { suggestions: suggestions };
},
});
const edit = monaco.editor.create(document.getElementById('editor'), {
value: [code].join('\n'),
theme: 'myCoolTheme',
// value: getCode(),
//language: 'mySpecialLanguage'
language: 'mermaid',
});
edit.onDidChangeModelContent(function (e) {
codeStore.set(edit.getValue());
});
-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_CDN_URL?: string;
readonly MERMAID_BASE_URL?: string;
readonly MERMAID_LOCAL?: boolean;
// more env variables...
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
-16
View File
@@ -1,16 +0,0 @@
/* eslint-disable @typescript-eslint/no-empty-interface */
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable @typescript-eslint/no-unused-vars */
/// <reference types="@sveltejs/kit" />
import type { TestingLibraryMatchers } from '@testing-library/jest-dom/matchers';
declare global {
namespace jest {
interface Matchers<R = void>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line no-undef
extends TestingLibraryMatchers<typeof expect.stringContaining, R> {}
}
}
-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;
};
-265
View File
@@ -1,265 +0,0 @@
<script lang="ts">
import { browser } from '$app/environment';
import Card from '$lib/components/Card/Card.svelte';
import { env } from '$lib/util/env';
import { pakoSerde } from '$lib/util/serde';
import { stateStore } from '$lib/util/state';
import { logEvent } from '$lib/util/stats';
import { toBase64 } from 'js-base64';
import dayjs from 'dayjs';
const { krokiRendererUrl, rendererUrl } = env;
type Exporter = (context: CanvasRenderingContext2D, image: HTMLImageElement) => () => void;
const getFileName = (ext: string) =>
`mermaid-diagram-${dayjs().format('YYYY-MM-DD-HHmmss')}.${ext}`;
const getBase64SVG = (svg?: HTMLElement, width?: number, height?: number): string => {
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
if (!svg) {
svg = getSvgEl();
}
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 | null = document.querySelector('#container svg');
if (!svg) {
throw new Error('svg not found');
}
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');
if (!context) {
throw new Error('context not found');
}
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 getSvgEl = () => {
const svgEl: HTMLElement = document
.querySelector('#container svg')!
.cloneNode(true) as HTMLElement;
svgEl.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
const fontAwesomeCdnUrl = Array.from(document.head.getElementsByTagName('link'))
.map((l) => l.href)
.find((h) => h.includes('font-awesome'));
if (fontAwesomeCdnUrl == null) {
return svgEl;
}
const styleEl = document.createElement('style');
styleEl.innerText = `@import url("${fontAwesomeCdnUrl}");'`;
svgEl.prepend(styleEl);
return svgEl;
};
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(
getFileName('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 {
if (!blob) {
throw new Error('blob is empty');
}
void navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob
})
]);
} catch (error) {
console.error(error);
}
});
};
};
const onCopyClipboard = (event: Event) => {
exportImage(event, clipboardCopy);
logEvent('copyClipboard');
};
const onDownloadPNG = (event: Event) => {
exportImage(event, downloadImage);
logEvent('download', {
type: 'png'
});
};
const onDownloadSVG = () => {
simulateDownload(getFileName('svg'), `data:image/svg+xml;base64,${getBase64SVG()}`);
logEvent('download', {
type: 'svg'
});
};
const onCopyMarkdown = () => {
(document.getElementById('markdown') as HTMLInputElement).select();
document.execCommand('Copy');
logEvent('copyMarkdown');
};
let gistURL = '';
stateStore.subscribe(({ loader }) => {
if (loader?.type === 'gist') {
// @ts-expect-error Gist will have url
gistURL = loader.config.url;
}
});
const loadGist = () => {
if (!gistURL) {
alert('Please enter a Gist URL first');
}
window.location.href = `${window.location.pathname}?gist=${gistURL}`;
logEvent('loadGist');
};
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;
}
stateStore.subscribe(({ code, serialized }) => {
iUrl = `${rendererUrl}/img/${serialized}?type=png`;
svgUrl = `${rendererUrl}/svg/${serialized}`;
krokiUrl = `${krokiRendererUrl}/mermaid/svg/${pakoSerde.serialize(code)}`;
mdCode = `[![](${iUrl})](${window.location.protocol}//${window.location.host}${window.location.pathname}#${serialized})`;
});
</script>
<Card title="Actions" isOpen={false}>
<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 id="downloadPNG" class="action-btn flex-grow" on:click={onDownloadPNG}>
<i class="fas fa-download mr-2" /> PNG
</button>
<button id="downloadSVG" class="action-btn flex-grow" on:click={onDownloadSVG}>
<i class="fas fa-download mr-2" /> SVG
</button>
<a target="_blank" rel="noreferrer" class="flex-grow" href={iUrl}>
<button class="action-btn w-full">
<i class="fas fa-external-link-alt mr-2" /> PNG
</button>
</a>
<a target="_blank" rel="noreferrer" class="flex-grow" href={svgUrl}>
<button class="action-btn w-full">
<i class="fas fa-external-link-alt mr-2" /> SVG
</button>
</a>
<a target="_blank" rel="noreferrer" class="flex-grow" href={krokiUrl}>
<button class="action-btn w-full">
<i class="fas fa-external-link-alt mr-2" /> Kroki
</button>
</a>
<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>
-31
View File
@@ -1,31 +0,0 @@
<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 activeTabID = '';
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)}
on:keypress={() => (isOpen = !isOpen)}>
<div class="flex justify-between">
<Tabs on:select {tabs} bind:isOpen {title} {isCloseable} {activeTabID} />
<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>
-52
View File
@@ -1,52 +0,0 @@
<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;
export let activeTabID: string;
if (!activeTabID && tabs.length > 0) {
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)}
on:keypress|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 {activeTabID === tab.id ? 'tab-active' : 'text-primary-content'}"
on:click|stopPropagation={() => toggleTabs(tab)}
on:keypress|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>
-23
View File
@@ -1,23 +0,0 @@
import { cleanup, render } from '@testing-library/svelte';
import { describe, expect, it, afterEach } from 'vitest';
import Card from './Card.svelte';
describe('card.svelte', () => {
// TODO: @testing-library/svelte claims to add this automatically but it doesn't work without explicit afterEach
afterEach(() => cleanup());
it('mounts', () => {
const { container } = render(Card, {
title: 'TabTest',
tabs: [
{ id: 't1', title: 'title1', icon: 'fab fa-git-alt' },
{ id: 't2', title: 'title2', icon: 'far fa-bookmark' }
]
});
expect(container).toBeTruthy();
expect(container).toHaveTextContent('TabTest');
expect(container).toHaveTextContent('title1');
expect(container).toHaveTextContent('title2');
expect(container).not.toHaveTextContent('title3');
});
});
-128
View File
@@ -1,128 +0,0 @@
<script lang="ts">
import type { EditorMode } from '$lib/types';
import { stateStore, updateCode, updateConfig } from '$lib/util/state';
import { themeStore } from '$lib/util/theme';
import { errorDebug, syncDiagram } from '$lib/util/util';
import * as monaco from 'monaco-editor';
import monacoJsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import monacoEditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import { onMount } from 'svelte';
import { initEditor } from '$lib/util/monacoExtra';
import { logEvent } from '$lib/util/stats';
let divEl: HTMLDivElement | undefined = undefined;
let editor: monaco.editor.IStandaloneCodeEditor | undefined;
let editorOptions: monaco.editor.IStandaloneEditorConstructionOptions = {
minimap: {
enabled: false
},
theme: 'mermaid',
overviewRulerLanes: 0
};
let text = '';
stateStore.subscribe(({ errorMarkers, editorMode, code, mermaid }) => {
// console.log('editor store subscription', { code, mermaid });
if (!editor) {
return;
}
// Update editor text if it's different
const newText = editorMode === 'code' ? code : mermaid;
if (newText !== text) {
// console.log('updating editor text', newText);
editor.setScrollTop(0);
editor.setValue(newText);
text = newText;
}
// Update editor mode if it's different
const language = editorMode === 'code' ? 'mermaid' : 'json';
const model = editor.getModel();
if (!model) {
console.error("editor model doesn't exist");
return;
}
if (model.getLanguageId() !== language) {
monaco.editor.setModelLanguage(model, language);
}
// Display/clear errors
monaco.editor.setModelMarkers(model, 'mermaid', errorMarkers);
});
themeStore.subscribe(({ isDark }) => {
editor && monaco.editor.setTheme(isDark ? 'mermaid-dark' : 'mermaid');
});
const handleUpdate = (text: string, mode: EditorMode) => {
// console.log('editor HandleUpdate', { text, mode });
if (mode === 'code') {
updateCode(text);
} else {
updateConfig(text);
}
};
onMount(async () => {
self.MonacoEnvironment = {
getWorker(_, label) {
if (label === 'json') {
return new monacoJsonWorker();
}
return new monacoEditorWorker();
}
};
if (!divEl) {
throw new Error('divEl is undefined');
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
initEditor(monaco);
errorDebug(100);
editor = monaco.editor.create(divEl, editorOptions);
editor.onDidChangeModelContent(({ isFlush, changes }) => {
const newText = editor?.getValue();
// console.log('editor onDidChangeModelContent', { text, newText, isFlush, changes });
if (!newText || text === newText || isFlush) {
return;
}
text = newText;
handleUpdate(text, $stateStore.editorMode);
});
editor.addAction({
id: 'mermaid-render-diagram',
label: 'Render Diagram',
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
run: function () {
syncDiagram();
logEvent('renderDiagram', {
method: 'keyboardShortcut'
});
}
});
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
});
});
if (divEl.parentElement) {
resizeObserver.observe(divEl.parentElement);
}
// @ts-ignore
if (window.Cypress) {
// @ts-ignore
window.editorLoaded = true;
}
return () => {
// console.log(`editor disposed`);
editor?.dispose();
};
});
</script>
<div bind:this={divEl} id="editor" class="overflow-hidden" />
-194
View File
@@ -1,194 +0,0 @@
<script lang="ts">
import Card from '$lib/components/Card/Card.svelte';
import { inputStateStore, getStateString } from '$lib/util/state';
import {
addHistoryEntry,
historyModeStore,
clearHistoryData,
getPreviousState,
historyStore,
loaderHistoryStore,
restoreHistory
} from './history';
import { notify, prompt } from '$lib/util/notify';
import { onMount } from 'svelte';
import { get } from 'svelte/store';
import dayjs from 'dayjs';
import dayjsRelativeTime from 'dayjs/plugin/relativeTime';
import type { HistoryEntry, HistoryType, State, Tab } from '$lib/types';
import { logEvent } from '$lib/util/stats';
dayjs.extend(dayjsRelativeTime);
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 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', ({ target }: Event) => {
const file = (<HTMLInputElement>target).files[0];
if (!file) {
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const data: HistoryEntry[] = JSON.parse(e.target.result as string);
restoreHistory(data);
};
reader.readAsText(file);
});
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 });
};
const relativeTime = (time: number) => {
const t = new Date(time);
return `${new Date(t).toLocaleString()} (${dayjs(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="uploadHistory"
class="btn btn-xs btn-secondary w-12"
on:click|stopPropagation={() => uploadHistory()}
title="Upload history"><i class="fa fa-upload" /></button>
{#if $historyStore.length > 0}
<button
id="downloadHistory"
class="btn btn-xs btn-secondary w-12"
on:click|stopPropagation={() => downloadHistory()}
title="Download history"><i class="fa fa-download" /></button>
{/if}
|
<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 { id, 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={() => restoreHistoryItem(state)}
><i class="fas fa-undo mr-1" />Restore</button>
{#if type !== 'loader'}
<button class="btn btn-error" on:click={() => clearHistory(id)}
><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>
-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 { derived, writable, get } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store';
import { persist, localStorage } from '$lib/util/persist';
import { generateSlug } from 'random-word-slugs';
import type { HistoryEntry, HistoryType, Optional } from '$lib/types';
import { v4 as uuidV4 } from 'uuid';
import { logEvent } from '$lib/util/stats';
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;
};
-96
View File
@@ -1,96 +0,0 @@
<script context="module" lang="ts">
import { version } from 'mermaid/package.json';
import { analytics } from '$lib/util/stats';
void analytics?.track('version', {
mermaidVersion: version
});
</script>
<script lang="ts">
import Theme from './Theme.svelte';
interface Link {
href: string;
title?: string;
icon?: string;
img?: string;
}
const links: Link[] = [
{
title: 'Documentation',
href: 'https://mermaid.js.org/intro/n00b-gettingStarted.html'
},
{
title: 'Tutorial',
href: 'https://mermaid.js.org/config/Tutorials.html'
},
{
title: 'Mermaid',
href: 'https://github.com/mermaid-js/mermaid'
},
{
title: 'CLI',
href: 'https://github.com/mermaid-js/mermaid-cli'
},
{
href: 'https://github.com/mermaid-js/mermaid-live-editor',
icon: 'fab fa-github fa-lg'
},
{
href: 'https://mermaidchart.com',
img: '/mermaidchart-logo.svg'
}
];
</script>
<div class="navbar shadow-lg bg-primary p-0">
<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, img }}
<li>
<a class="btn btn-ghost" target="_blank" {href}>
{#if icon}
<i class={icon} />
{:else if img}
<img src={img} alt={title} />
{/if}
{#if title}
{title}
{/if}
</a>
</li>
{/each}
</ul>
</div>
</div>
<style>
#menu-toggle:checked + #menu {
display: block;
}
.navbar {
z-index: 10000;
}
img {
width: 1.5rem;
height: 1.5rem;
}
</style>
-160
View File
@@ -1,160 +0,0 @@
<script lang="ts">
import { updateCode } from '$lib/util/state';
import Card from '$lib/components/Card/Card.svelte';
import { logEvent } from '$lib/util/stats';
const samples = {
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]`,
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!`,
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()
}`,
State: `stateDiagram-v2
[*] --> Still
Still --> [*]
Still --> Moving
Moving --> Still
Moving --> Crash
Crash --> [*]`,
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`,
Pie: `pie title Pets adopted by volunteers
"Dogs" : 386
"Cats" : 85
"Rats" : 15`,
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"`,
'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`,
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 effectivness<br/>and features
On Automatic creation
Uses
Creative techniques
Strategic planning
Argument mapping
Tools
Pen and paper
Mermaid`,
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]`
};
type SampleTypes = keyof typeof samples;
const loadSampleDiagram = (diagramType: SampleTypes): void => {
updateCode(samples[diagramType], {
updateDiagram: true,
resetPanZoom: true
});
logEvent('loadSampleDiagram', { diagramType });
};
// Adding in this array will add an icon to the preset menu
const newDiagrams: SampleTypes[] = ['Mindmap', 'QuadrantChart'];
const diagramOrder: SampleTypes[] = [
'Sequence',
'Flow',
'Class',
'State',
'ER',
'Gantt',
'User Journey',
'Git',
'Pie',
'Mindmap',
'QuadrantChart'
];
</script>
<Card title="Sample Diagrams" isOpen={false}>
<div class="flex flex-wrap p-2 gap-2">
{#each diagramOrder as sample}
<button
class="btn btn-sm btn-primary w-28 normal-case flex-grow"
on:click={() => loadSampleDiagram(sample)}>
{sample}
{#if newDiagrams.includes(sample)}
<span class="ml-2 fa fa-heart" />
{/if}
</button>
{/each}
</div>
</Card>
-64
View File
@@ -1,64 +0,0 @@
<script lang="ts">
import { setTheme, themeStore } from '$lib/util/theme';
const themes = [
'🌝 light',
'🌚 dark',
'🧁 cupcake',
'🐝 bumblebee',
'✳️ emerald',
'🏢 corporate',
'🌃 synthwave',
'👴 retro',
'🤖 cyberpunk',
'🌸 valentine',
'🎃 halloween',
'🌷 garden',
'🌲 forest',
'🐟 aqua',
'👓 lofi',
'🖍 pastel',
'🧚‍♀️ fantasy',
'📝 wireframe',
'🏴 black',
'💎 luxury',
'🧛‍♂️ dracula'
];
</script>
<div class="hidden lg:block dropdown">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div tabindex="0" class="btn btn-ghost">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block w-6 h-6 stroke-current md:mr-2"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" /></svg>
<span class="hidden md:inline">Theme</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1792 1792"
class="inline-block w-4 h-4 ml-1 fill-current"
><path
d="M1395 736q0 13-10 23l-466 466q-10 10-23 10t-23-10l-466-466q-10-10-10-23t10-23l50-50q10-10 23-10t23 10l393 393 393-393q10-10 23-10t23 10l50 50q10 10 10 23z" /></svg>
</div>
<div
class="mt-14 overflow-y-auto shadow-2xl top-px dropdown-content h-96 w-56 bg-base-200 text-base-content">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<ul tabindex="0" class="p-4 menu compact">
{#each themes as theme}
<li class:bordered={$themeStore.theme !== undefined && theme.includes($themeStore.theme)}>
<span
class="btn btn-ghost justify-start"
on:click={() => setTheme(theme)}
on:keypress={() => setTheme(theme)}>{theme}</span>
</li>
{/each}
</ul>
</div>
</div>
-164
View File
@@ -1,164 +0,0 @@
<script lang="ts">
import { inputStateStore, stateStore, updateCodeStore } from '$lib/util/state';
import { onMount } from 'svelte';
import panzoom from 'svg-pan-zoom';
import type { State, ValidatedState } from '$lib/types';
import { logEvent } from '$lib/util/stats';
import { cmdKey } from '$lib/util/util';
import { render as renderDiagram } from '$lib/util/mermaid';
import type { MermaidConfig } from 'mermaid';
let code = '';
let config = '';
let container: HTMLDivElement;
let view: HTMLDivElement;
let error = false;
let outOfSync = false;
let hide = false;
let manualUpdate = true;
let panZoomEnabled = $stateStore.panZoom;
let pzoom: typeof panzoom | undefined;
const handlePanZoomChange = () => {
if (!pzoom) {
return;
}
const pan = pzoom.getPan();
const zoom = pzoom.getZoom();
updateCodeStore({ pan, zoom });
logEvent('panZoom');
};
const handlePanZoom = (state: State) => {
if (!state.panZoom) {
return;
}
hide = true;
pzoom?.destroy();
pzoom = undefined;
void Promise.resolve().then(() => {
const graphDiv = document.getElementById('graph-div');
if (!graphDiv) {
return;
}
pzoom = panzoom(graphDiv, {
onPan: handlePanZoomChange,
onZoom: handlePanZoomChange,
controlIconsEnabled: true,
fit: true,
center: true
});
const { pan, zoom } = state;
if (pan !== undefined && zoom !== undefined && Number.isFinite(zoom)) {
pzoom.zoom(zoom);
pzoom.pan(pan);
}
hide = false;
});
};
const handleStateChange = async (state: ValidatedState) => {
if (state.error !== undefined) {
error = true;
return;
}
error = false;
try {
if (container && state && (state.updateDiagram || state.autoSync)) {
if (!state.autoSync) {
$inputStateStore.updateDiagram = false;
}
outOfSync = false;
manualUpdate = true;
// Do not render if there is no change in Code/Config/PanZoom
if (code === state.code && config === state.mermaid && panZoomEnabled === state.panZoom) {
return;
}
code = state.code;
config = state.mermaid;
panZoomEnabled = state.panZoom;
const scroll = view.parentElement!.scrollTop;
delete container.dataset.processed;
const { svg, bindFunctions } = await renderDiagram(
Object.assign({}, JSON.parse(state.mermaid)) as MermaidConfig,
code,
'graph-div'
);
if (svg.length > 0) {
handlePanZoom(state);
container.innerHTML = svg;
console.log({ svg });
const graphDiv = document.getElementById('graph-div');
if (!graphDiv) {
throw new Error('graph-div not found');
}
graphDiv.setAttribute('height', '100%');
graphDiv.style.maxWidth = '100%';
if (bindFunctions) {
bindFunctions(graphDiv);
}
}
view.parentElement!.scrollTop = scroll;
error = false;
} else if (manualUpdate) {
manualUpdate = false;
} else if (code !== state.code || config !== state.mermaid) {
outOfSync = true;
}
} catch (e) {
console.error('view fail', e);
error = true;
}
};
onMount(() => {
stateStore.subscribe((state) => {
void handleStateChange(state);
});
window.addEventListener('resize', () => {
if ($stateStore.panZoom && pzoom) {
pzoom.resize();
}
});
});
</script>
{#if (error && $stateStore.error instanceof Error) || outOfSync}
<div
class="absolute w-full p-2 z-10 font-mono {error
? 'text-red-600'
: 'text-yellow-600'} bg-base-100 bg-opacity-80 text-left"
id="errorContainer">
{#if error}
{@html $stateStore.error?.toString().replace(/\n/g, '<br />')}
{:else}
Diagram out of sync. <br />
Press <i class="fas fa-sync" /> (Sync button) or <kbd>{cmdKey} + Enter</kbd> to sync.
{/if}
</div>
{/if}
<div id="view" bind:this={view} class="p-2 h-full" class:error class:outOfSync>
<div id="container" bind:this={container} class="h-full overflow-auto" class:hide />
</div>
<style>
#view {
flex: 1;
}
#container {
transition: visibility 0.3s;
}
.error,
.outOfSync {
opacity: 0.5;
}
.hide {
visibility: hidden;
}
</style>
-88
View File
@@ -1,88 +0,0 @@
export interface MarkerData {
severity: number;
message: string;
source?: string;
startLineNumber: number;
startColumn: number;
endLineNumber: number;
endColumn: number;
}
export interface TabEvents {
select: Tab;
}
export interface Tab {
id: string;
title: string;
icon: string;
}
export interface State {
code: string;
mermaid: string;
updateDiagram: boolean;
autoSync: boolean;
editorMode?: EditorMode;
panZoom?: boolean;
pan?: { x: number; y: number };
zoom?: number;
loader?: LoaderConfig;
}
export interface ValidatedState extends State {
editorMode: EditorMode;
error?: Error;
errorMarkers: MarkerData[];
serialized: string;
}
export interface GistLoaderConfig {
url: string;
}
export interface LoadingState {
loading: boolean;
message?: string;
}
export interface FileLoaderConfig {
codeURL: string;
configURL?: string;
}
export interface LoaderConfig {
type: 'gist' | 'files';
config: GistLoaderConfig | FileLoaderConfig;
}
export type HistoryType = 'auto' | 'manual' | 'loader';
export type HistoryEntry = { id: string; state: State; time: number; url?: string } & (
| {
type: 'loader';
name: string;
}
| {
type: HistoryType;
name?: string;
}
);
export type DocumentationConfig = Record<
string,
{
code: string;
config?: string;
}
>;
export type EditorMode = 'code' | 'config';
export type Loader = (url: string) => Promise<State>;
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
export interface ErrorHash {
loc: {
first_line: number;
last_line: number;
first_column: number;
last_column: number;
};
}
-9
View File
@@ -1,9 +0,0 @@
export const env = {
rendererUrl: import.meta.env.MERMAID_RENDERER_URL ?? 'https://mermaid.ink',
krokiRendererUrl: import.meta.env.MERMAID_KROKI_RENDERER_URL ?? 'https://kroki.io',
mermaidCDNUrl: import.meta.env.MERMAID_CDN_URL ?? 'https://unpkg.com/@mermaid-js',
mermaidBaseURL: import.meta.env.MERMAID_BASE_URL ?? 'http://localhost:9000',
useLocalMermaid: import.meta.env.MERMAID_LOCAL ?? false,
isDev: import.meta.env.DEV,
baseURL: import.meta.env.BASE_URL
};
-127
View File
@@ -1,127 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { State } from '$lib/types';
import { defaultState } from '$lib/util/state';
import { addHistoryEntry } from '$lib/components/History/history';
import { fetchJSON, fetchText } from '$lib/util/util';
const codeFileName = 'code.mmd';
const configFileName = 'config.json';
interface GithubFile {
truncated: boolean;
raw_url: string;
content: string;
}
const isValidGist = (files: any): boolean => {
return codeFileName in files;
};
const getFileContent = async (file: GithubFile): Promise<string> => {
if (file.truncated) {
return await fetchText(file.raw_url);
}
return file.content;
};
interface GistData {
code: string;
config?: string;
author: string;
time: number;
version: string;
url: string;
}
interface GistResponse {
files: Record<string, GithubFile>;
html_url: string;
history: { url: string; committed_at: string; version: string; user: { login: string } }[];
}
const getGistData = async (gistURL: string): Promise<GistData> => {
const path = gistURL.split('github.com').pop();
if (!path) {
throw new Error('Invalid GitHub URL' + gistURL);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, __, gistID, revisionID] = path.split('/');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { html_url, files, history }: GistResponse = await fetchJSON(
`https://api.github.com/gists/${gistID}${revisionID ? '/' + revisionID : ''}`
);
if (isValidGist(files)) {
const code = await getFileContent(files[codeFileName]);
let config = '{}';
if (configFileName in files) {
config = await getFileContent(files[configFileName]);
}
const currentItem = history[0];
return {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
url: `${html_url}/${currentItem.version}`,
code,
config,
author: currentItem.user.login,
time: new Date(currentItem.committed_at).getTime(),
version: currentItem.version.slice(-7)
};
} else {
throw new Error('Invalid gist provided');
}
};
const getStateFromGist = (gist: GistData, gistURL: string = gist.url): State => {
const state: State = {
...defaultState,
code: gist.code,
loader: {
type: 'gist',
config: {
url: gistURL
}
}
};
gist.config && (state.mermaid = gist.config);
return state;
};
export const loadGistData = async (gistURL: string): Promise<State> => {
const path = gistURL.split('github.com').pop();
if (!path) {
throw new Error('Invalid GitHub URL' + gistURL);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, __, gistID, revisionID] = path.split('/');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { history }: GistResponse = await fetchJSON(
`https://api.github.com/gists/${gistID}${revisionID ? '/' + revisionID : ''}`
);
const gistHistory: GistData[] = [];
for (const entry of history) {
const data: GistData | undefined = await getGistData(entry.url).catch();
data && gistHistory.push(data);
}
if (gistHistory.length === 0) {
throw new Error('Invalid gist provided');
}
gistHistory.reverse();
const entry = gistHistory.slice(-1).pop();
if (!entry) {
throw new Error('Invalid gist provided');
}
const state = getStateFromGist(entry, gistURL);
for (const gist of gistHistory) {
addHistoryEntry({
state: getStateFromGist(gist),
time: gist.time,
type: 'loader',
url: gist.url,
name: `${gist.author} v${gist.version}`
});
}
return state;
};
-58
View File
@@ -1,58 +0,0 @@
import { loadGistData } from './gist';
import { updateCodeStore, defaultState } from '$lib/util/state';
import { fetchText } from '$lib/util/util';
import type { Loader, State } from '$lib/types';
const loaders: Record<string, Loader> = {
gist: loadGistData
};
export const loadDataFromUrl = async (): Promise<void> => {
const searchParams = new URLSearchParams(window.location.search);
let state: Partial<State> = defaultState;
let loaded = false;
const codeURL: string | undefined = searchParams.get('code') ?? undefined;
const configURL: string | undefined = searchParams.get('config') ?? undefined;
let code: string | undefined;
const config = configURL ? await fetchText(configURL) : defaultState.mermaid;
if (codeURL) {
code = await fetchText(codeURL);
loaded = true;
}
if (code) {
if (!codeURL) {
throw new Error('Code URL is not defined');
}
state = {
code,
mermaid: config,
loader: {
type: 'files',
config: {
codeURL,
configURL
}
}
};
} else {
for (const [key, value] of searchParams.entries()) {
if (key in loaders) {
try {
state = await loaders[key](value);
loaded = true;
break;
} catch (error) {
console.error(error);
}
}
}
}
loaded &&
updateCodeStore({
...state,
autoSync: true,
updateDiagram: true
});
};
-20
View File
@@ -1,20 +0,0 @@
import { writable } from 'svelte/store';
import type { Writable } from 'svelte/store';
import type { LoadingState } from '$lib/types';
const defaultLoading: LoadingState = {
loading: false
};
export const loadingStateStore: Writable<LoadingState> = writable(defaultLoading);
export const initLoading = async <T>(message: string, task: Promise<T>): Promise<T> => {
loadingStateStore.set({
loading: true,
message
});
const result: T = await task;
loadingStateStore.set({
loading: false
});
return result;
};

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