Merge branch 'develop' into sidv/tsMermaid
* develop: (40 commits) Add tabindex chore: A11y changes chore(deps): update all non-major dependencies fix: favicon size Update Browserslist chore: Update favicon Update Browserslist chore(deps): update all non-major dependencies Update Browserslist Fix an issue where the image is jpg format on mermaid.ink chore(deps): update all non-major dependencies chore(deps): update all non-major dependencies chore: Remove stability days from renovate Update Browserslist chore(deps): update all non-major dependencies Update Browserslist chore(deps): update all non-major dependencies Update Browserslist chore(deps): update all non-major dependencies Update Browserslist ...
This commit is contained in:
+2
-1
@@ -44,6 +44,7 @@ module.exports = {
|
||||
],
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'es/no-regexp-lookbehind-assertions': 'error'
|
||||
'es/no-regexp-lookbehind-assertions': 'error',
|
||||
curly: ['error', 'all']
|
||||
}
|
||||
};
|
||||
|
||||
@@ -48,4 +48,3 @@ jobs:
|
||||
parallel: true
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
MERMAID_DISABLE_DEBOUNCE: 'true'
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
name: Update Browserslist
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
branches-ignore:
|
||||
- 'develop'
|
||||
- 'master'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
Vendored
+2
-1
@@ -4,5 +4,6 @@
|
||||
"vitest.commandLine": "yarn test:unit",
|
||||
"vitest.enable": true,
|
||||
"testing.autoRun.mode": "rerun",
|
||||
"svelte.enable-ts-plugin": true
|
||||
"svelte.enable-ts-plugin": true,
|
||||
"githubPullRequests.ignoredPullRequestBranches": ["develop"]
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
# Stop : press ctrl + c
|
||||
# or
|
||||
# docker stop mermaid-live-editor
|
||||
FROM node:18.9.0 as mermaid-live-editor-builder
|
||||
FROM node:18.11.0 as mermaid-live-editor-builder
|
||||
COPY --chown=node:node . /home
|
||||
WORKDIR /home
|
||||
RUN yarn install
|
||||
|
||||
@@ -9,8 +9,10 @@ describe('Auto sync tests', () => {
|
||||
it('should dim diagram when code is edited', () => {
|
||||
cy.contains('Auto sync').click();
|
||||
cy.get('#view').should('not.have.class', 'outOfSync');
|
||||
cy.get('#view').should('not.contain.text', 'Diagram out of sync.');
|
||||
getEditor().type(' C --> Test');
|
||||
cy.get('#view').should('have.class', 'outOfSync');
|
||||
cy.get('#view').should('contain.text', 'Diagram out of sync.');
|
||||
cy.getLocalStorage('codeStore').snapshot();
|
||||
});
|
||||
|
||||
@@ -19,7 +21,9 @@ describe('Auto sync tests', () => {
|
||||
cy.get('#view').should('not.have.class', 'outOfSync');
|
||||
getEditor().type(' C --> Test');
|
||||
cy.get('#view').should('have.class', 'outOfSync');
|
||||
cy.get('#view').should('contain.text', 'Diagram out of sync.');
|
||||
getEditor().type(`${cmd}{enter}`);
|
||||
cy.get('#view').should('not.contain.text', 'Diagram out of sync.');
|
||||
cy.get('#view').should('not.have.class', 'outOfSync');
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ module.exports = {
|
||||
"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": "10.7.0",
|
||||
"__version": "10.10.0",
|
||||
"Auto sync tests": {
|
||||
"should dim diagram when code is edited": {
|
||||
"1": "{\"code\":\"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\\n C --> Test\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"autoSync\":false,\"updateDiagram\":false}"
|
||||
|
||||
+23
-24
@@ -5,7 +5,7 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"dev:test": "MERMAID_DISABLE_DEBOUNCE=true yarn dev",
|
||||
"dev:test": "yarn dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "prettier --check --cache --plugin-search-dir=. .;eslint --ignore-path .gitignore .",
|
||||
@@ -22,56 +22,55 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/snapshot": "2.1.7",
|
||||
"@sveltejs/adapter-static": "1.0.0-next.42",
|
||||
"@sveltejs/kit": "1.0.0-next.480",
|
||||
"@sveltejs/adapter-static": "1.0.0-next.44",
|
||||
"@sveltejs/kit": "1.0.0-next.516",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/svelte": "3.2.1",
|
||||
"@testing-library/svelte": "3.2.2",
|
||||
"@types/pako": "2.0.0",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.36.2",
|
||||
"@typescript-eslint/parser": "5.36.2",
|
||||
"@vitest/ui": "0.23.1",
|
||||
"autoprefixer": "10.4.8",
|
||||
"@typescript-eslint/eslint-plugin": "5.40.1",
|
||||
"@typescript-eslint/parser": "5.40.1",
|
||||
"@vitest/ui": "0.24.3",
|
||||
"autoprefixer": "10.4.12",
|
||||
"c8": "7.12.0",
|
||||
"chai": "4.3.6",
|
||||
"cssnano": "5.1.13",
|
||||
"cy-verify-downloads": "0.1.8",
|
||||
"cypress": "10.7.0",
|
||||
"cy-verify-downloads": "0.1.11",
|
||||
"cypress": "10.10.0",
|
||||
"cypress-localstorage-commands": "2.2.1",
|
||||
"eslint": "8.23.0",
|
||||
"eslint": "8.25.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-cypress": "2.12.1",
|
||||
"eslint-plugin-es": "4.1.0",
|
||||
"eslint-plugin-postcss-modules": "2.0.0",
|
||||
"eslint-plugin-svelte3": "4.0.0",
|
||||
"eslint-plugin-tailwindcss": "3.6.1",
|
||||
"eslint-plugin-vitest": "0.0.8",
|
||||
"eslint-plugin-tailwindcss": "3.6.2",
|
||||
"eslint-plugin-vitest": "0.0.11",
|
||||
"esserializer": "1.3.2",
|
||||
"husky": "8.0.1",
|
||||
"jsdom": "20.0.0",
|
||||
"jsdom": "20.0.1",
|
||||
"lint-staged": "13.0.3",
|
||||
"node-html-parser": "6.0.0",
|
||||
"postcss": "8.4.16",
|
||||
"node-html-parser": "6.1.1",
|
||||
"postcss": "8.4.18",
|
||||
"postcss-load-config": "4.0.1",
|
||||
"prettier": "2.7.1",
|
||||
"prettier-plugin-svelte": "2.7.0",
|
||||
"svelte": "3.50.1",
|
||||
"prettier-plugin-svelte": "2.8.0",
|
||||
"svelte": "3.52.0",
|
||||
"svelte-preprocess": "4.10.7",
|
||||
"tailwindcss": "3.1.8",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.8.3",
|
||||
"vite": "3.1.0",
|
||||
"vitest": "0.23.1"
|
||||
"typescript": "4.8.4",
|
||||
"vite": "3.1.8",
|
||||
"vitest": "0.24.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@analytics/google-analytics": "1.0.3",
|
||||
"analytics": "0.8.1",
|
||||
"analytics-plugin-plausible": "0.0.6",
|
||||
"daisyui": "2.25.0",
|
||||
"daisyui": "2.31.0",
|
||||
"js-base64": "3.7.2",
|
||||
"mermaid": "9.2.0-rc1",
|
||||
"moment": "2.29.4",
|
||||
"monaco-editor": "0.34.0",
|
||||
"monaco-editor": "0.34.1",
|
||||
"monaco-mermaid": "1.0.6",
|
||||
"pako": "2.0.4",
|
||||
"random-word-slugs": "0.1.6",
|
||||
|
||||
@@ -14,10 +14,6 @@
|
||||
{
|
||||
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchDatasources": ["npm"],
|
||||
"stabilityDays": 3
|
||||
}
|
||||
],
|
||||
"dependencyDashboard": true,
|
||||
|
||||
+11
-9
@@ -11,7 +11,9 @@
|
||||
<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.png" />
|
||||
<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"
|
||||
@@ -21,30 +23,30 @@
|
||||
referrerpolicy="no-referrer" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/editor/editor.main.min.css"
|
||||
integrity="sha512-iQEIc0rsSDujsfjtD+lfyJ1W23Bh/lbgriubKDAym6VlEIDRj9rrbSIyJRyshOrl8s0yRcQ0+gyrZfSLyjJGWQ=="
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs/editor/editor.main.min.css"
|
||||
integrity="sha512-GzcoZD7y5zvBofYtImXPZaPVhoY7xLPt+ysmbPb/vU+quSKFkcngxaSrxuwprDZL4MALUqGFmnqCxQZqMozv1Q=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer" />
|
||||
<script>
|
||||
var require = {
|
||||
paths: {
|
||||
vs: "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs",
|
||||
},
|
||||
vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/loader.min.js"
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs/loader.min.js"
|
||||
integrity="sha512-6bIYsGqvLpAiEBXPdRQeFf5cueeBECtAKJjIHer3BhBZNTV3WLcLA8Tm3pDfxUwTMIS+kAZwTUvJ1IrMdX8C5w=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"></script>
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/editor/editor.main.nls.min.js"
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs/editor/editor.main.nls.min.js"
|
||||
integrity="sha512-CCv+DKWw+yZhxf4Z+ExT6HC5G+3S45TeMTYcJyYbdrv4BpK2vyALJ4FoVR/KGWDIPu7w4tNCOC9MJQIkYPR5FA=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"></script>
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/editor/editor.main.js"
|
||||
integrity="sha512-TTPQbVI87mnVMV+1KbkKJ8vdQ4QqqbKyuTtJ9wQD8CqnwQLSQgXH7MWOQ88VO7pRzxWhqI1vYDeEV651sAH4ig=="
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs/editor/editor.main.js"
|
||||
integrity="sha512-BtSZPhzoyN8kq1axY6cgOFPSgLJgFwvAZ3WxeDGxEFXeFcFuK2s7Hr+zF75npVHASUY1dxudAfIVzwmgdR89Bw=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"></script>
|
||||
%sveltekit.head%
|
||||
|
||||
@@ -115,19 +115,19 @@
|
||||
|
||||
const onCopyClipboard = (event: Event) => {
|
||||
exportImage(event, clipboardCopy);
|
||||
void logEvent('copyClipboard');
|
||||
logEvent('copyClipboard');
|
||||
};
|
||||
|
||||
const onDownloadPNG = (event: Event) => {
|
||||
exportImage(event, downloadImage);
|
||||
void logEvent('download', {
|
||||
logEvent('download', {
|
||||
type: 'png'
|
||||
});
|
||||
};
|
||||
|
||||
const onDownloadSVG = () => {
|
||||
simulateDownload(getFileName('svg'), `data:image/svg+xml;base64,${getBase64SVG()}`);
|
||||
void logEvent('download', {
|
||||
logEvent('download', {
|
||||
type: 'svg'
|
||||
});
|
||||
};
|
||||
@@ -135,7 +135,7 @@
|
||||
const onCopyMarkdown = () => {
|
||||
(document.getElementById('markdown') as HTMLInputElement).select();
|
||||
document.execCommand('Copy');
|
||||
void logEvent('copyMarkdown');
|
||||
logEvent('copyMarkdown');
|
||||
};
|
||||
|
||||
let gistURL = '';
|
||||
@@ -151,7 +151,7 @@
|
||||
alert('Please enter a Gist URL first');
|
||||
}
|
||||
window.location.href = `${window.location.pathname}?gist=${gistURL}`;
|
||||
void logEvent('loadGist');
|
||||
logEvent('loadGist');
|
||||
};
|
||||
|
||||
let iUrl: string;
|
||||
@@ -166,7 +166,7 @@
|
||||
isNetlify = true;
|
||||
}
|
||||
stateStore.subscribe(({ code, serialized }) => {
|
||||
iUrl = `${rendererUrl}/img/${serialized}`;
|
||||
iUrl = `${rendererUrl}/img/${serialized}?type=png`;
|
||||
svgUrl = `${rendererUrl}/svg/${serialized}`;
|
||||
krokiUrl = `${krokiRendererUrl}/mermaid/svg/${pakoSerde.serialize(code)}`;
|
||||
mdCode = `[](${window.location.protocol}//${window.location.host}${window.location.pathname}#${serialized})`;
|
||||
@@ -180,26 +180,24 @@
|
||||
><i class="far fa-copy mr-2" /> Copy Image to clipboard
|
||||
</button>
|
||||
{/if}
|
||||
<button id="downloadPNG" class="action-btn flex-auto" on:click={onDownloadPNG}>
|
||||
<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-auto" on:click={onDownloadSVG}>
|
||||
<button id="downloadSVG" class="action-btn flex-grow" on:click={onDownloadSVG}>
|
||||
<i class="fas fa-download mr-2" /> SVG
|
||||
</button>
|
||||
<a target="_blank" href={iUrl}>
|
||||
<button class="action-btn flex-auto">
|
||||
<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" href={svgUrl}>
|
||||
<button class="action-btn flex-auto">
|
||||
<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" href={krokiUrl}>
|
||||
<button class="action-btn flex-auto">
|
||||
<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>
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
<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: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' : ''}">
|
||||
|
||||
@@ -19,7 +19,10 @@
|
||||
</script>
|
||||
|
||||
<div class="flex cursor-default">
|
||||
<span class="mr-2 font-semibold" on:click|stopPropagation={() => (isOpen = !isOpen)}>
|
||||
<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}
|
||||
@@ -29,7 +32,8 @@
|
||||
{#each tabs as tab}
|
||||
<div
|
||||
class="tab tab-lifted {activeTabID === tab.id ? 'tab-active' : 'text-primary-content'}"
|
||||
on:click|stopPropagation={() => toggleTabs(tab)}>
|
||||
on:click|stopPropagation={() => toggleTabs(tab)}
|
||||
on:keypress|stopPropagation={() => toggleTabs(tab)}>
|
||||
<i class="mr-1 {tab.icon}" />
|
||||
{tab.title}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import { onMount } from 'svelte';
|
||||
import initEditor from 'monaco-mermaid';
|
||||
import { logEvent } from '$lib/util/stats';
|
||||
import { debounceEnabled } from '$lib/util/env';
|
||||
|
||||
let divEl: HTMLDivElement = null;
|
||||
let editor: monaco.editor.IStandaloneCodeEditor;
|
||||
@@ -37,10 +36,8 @@
|
||||
Monaco?.editor.setModelLanguage(editor.getModel(), language);
|
||||
}
|
||||
|
||||
// Display errors if present
|
||||
if (errorMarkers.length > 0) {
|
||||
Monaco?.editor.setModelMarkers(editor.getModel(), 'test', errorMarkers);
|
||||
}
|
||||
// Display/clear errors
|
||||
Monaco?.editor.setModelMarkers(editor.getModel(), 'mermaid', errorMarkers);
|
||||
});
|
||||
|
||||
themeStore.subscribe(({ isDark }) => {
|
||||
@@ -55,21 +52,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
// Debounce state updates to avoid performance issues
|
||||
let debounce: { [key: string]: number } = {};
|
||||
const updateHandler = (newText: string) => {
|
||||
text = newText;
|
||||
const mode = $stateStore.editorMode;
|
||||
if (debounceEnabled) {
|
||||
clearTimeout(debounce[mode]);
|
||||
debounce[mode] = window.setTimeout(() => {
|
||||
handleUpdate(text, mode);
|
||||
}, 300);
|
||||
} else {
|
||||
handleUpdate(text, mode);
|
||||
}
|
||||
};
|
||||
|
||||
const loadMonaco = async () => {
|
||||
let i = 0;
|
||||
while (i++ < 500) {
|
||||
@@ -89,7 +71,8 @@
|
||||
initEditor(Monaco);
|
||||
editor = Monaco.editor.create(divEl, editorOptions);
|
||||
editor.onDidChangeModelContent(() => {
|
||||
updateHandler(editor.getValue());
|
||||
text = editor.getValue();
|
||||
handleUpdate(text, $stateStore.editorMode);
|
||||
});
|
||||
editor.addAction({
|
||||
id: 'mermaid-render-diagram',
|
||||
@@ -97,7 +80,7 @@
|
||||
keybindings: [Monaco.KeyMod.CtrlCmd | Monaco.KeyCode.Enter],
|
||||
run: function () {
|
||||
syncDiagram();
|
||||
void logEvent('renderDiagram', {
|
||||
logEvent('renderDiagram', {
|
||||
method: 'keyboadShortcut'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="Open revision in new tab"
|
||||
class="hover:underline text-blue-500">{name}</a>
|
||||
{:else}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="navbar mb-2 shadow-lg bg-primary">
|
||||
<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>
|
||||
@@ -61,7 +61,7 @@
|
||||
<ul class="lg:flex items-center justify-between text-base pt-4 lg:pt-0">
|
||||
{#each links as { title, href, icon }}
|
||||
<li>
|
||||
<a class="btn btn-ghost" target="_blank" {href}>
|
||||
<a class="btn btn-ghost" target="_blank" rel="noreferrer" {href}>
|
||||
{#if icon}
|
||||
<i class={icon} />
|
||||
{/if}
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
updateDiagram: true,
|
||||
resetPanZoom: true
|
||||
});
|
||||
void logEvent('loadSampleDiagram', { diagramType });
|
||||
logEvent('loadSampleDiagram', { diagramType });
|
||||
};
|
||||
|
||||
// Adding in this array will add an icon to the preset menu
|
||||
|
||||
@@ -2,31 +2,32 @@
|
||||
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'
|
||||
'🌝 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"
|
||||
@@ -48,10 +49,14 @@
|
||||
</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={theme.includes($themeStore.theme) ? 'bordered' : ''}>
|
||||
<span class="btn btn-ghost justify-start" on:click={() => setTheme(theme)}>{theme}</span>
|
||||
<span
|
||||
class="btn btn-ghost justify-start"
|
||||
on:click={() => setTheme(theme)}
|
||||
on:keypress={() => setTheme(theme)}>{theme}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
import { onMount } from 'svelte';
|
||||
import mermaid from 'mermaid';
|
||||
import panzoom from 'svg-pan-zoom';
|
||||
import type { State } from '$lib/types';
|
||||
import type { State, ValidatedState } from '$lib/types';
|
||||
import { logEvent } from '$lib/util/stats';
|
||||
import { cmdKey } from '$lib/util/util';
|
||||
|
||||
let code = '';
|
||||
let config = '';
|
||||
@@ -16,16 +17,12 @@
|
||||
let manualUpdate = true;
|
||||
let panZoomEnabled = $stateStore.panZoom;
|
||||
let pzoom: SvgPanZoom.Instance;
|
||||
let debounce: number;
|
||||
|
||||
const handlePanZoomChange = () => {
|
||||
const pan = pzoom.getPan();
|
||||
const zoom = pzoom.getZoom();
|
||||
clearTimeout(debounce);
|
||||
debounce = window.setTimeout(() => {
|
||||
updateCodeStore({ pan, zoom });
|
||||
void logEvent('panZoom');
|
||||
}, 200);
|
||||
updateCodeStore({ pan, zoom });
|
||||
logEvent('panZoom');
|
||||
};
|
||||
|
||||
const handlePanZoom = (state: State) => {
|
||||
@@ -53,50 +50,53 @@
|
||||
});
|
||||
};
|
||||
|
||||
const handleStateChange = (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;
|
||||
if (code === state.code && config === state.mermaid && panZoomEnabled === state.panZoom) {
|
||||
// Do not render if there is no change in Code/Config/PanZoom
|
||||
return;
|
||||
}
|
||||
code = state.code;
|
||||
config = state.mermaid;
|
||||
panZoomEnabled = state.panZoom;
|
||||
const scroll = view.parentElement.scrollTop;
|
||||
delete container.dataset.processed;
|
||||
mermaid.initialize(Object.assign({}, JSON.parse(state.mermaid)));
|
||||
mermaid.render('graph-div', code, (svgCode) => {
|
||||
if (svgCode.length > 0) {
|
||||
handlePanZoom(state);
|
||||
container.innerHTML = svgCode;
|
||||
const graphDiv = document.getElementById('graph-div');
|
||||
graphDiv.setAttribute('height', '100%');
|
||||
graphDiv.style.maxWidth = '100%';
|
||||
}
|
||||
});
|
||||
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) => {
|
||||
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;
|
||||
if (code === state.code && config === state.mermaid && panZoomEnabled === state.panZoom) {
|
||||
// Do not render if there is no change in Code/Config/PanZoom
|
||||
return;
|
||||
}
|
||||
code = state.code;
|
||||
config = state.mermaid;
|
||||
panZoomEnabled = state.panZoom;
|
||||
const scroll = view.parentElement.scrollTop;
|
||||
delete container.dataset.processed;
|
||||
mermaid.initialize(Object.assign({}, JSON.parse(state.mermaid)));
|
||||
mermaid.render('graph-div', code, (svgCode) => {
|
||||
if (svgCode.length > 0) {
|
||||
handlePanZoom(state);
|
||||
container.innerHTML = svgCode;
|
||||
const graphDiv = document.getElementById('graph-div');
|
||||
graphDiv.setAttribute('height', '100%');
|
||||
graphDiv.style.maxWidth = '100%';
|
||||
}
|
||||
});
|
||||
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;
|
||||
}
|
||||
handleStateChange(state);
|
||||
});
|
||||
window.addEventListener('resize', () => {
|
||||
if ($stateStore.panZoom && pzoom) {
|
||||
@@ -110,6 +110,12 @@
|
||||
<div class="p-2 text-red-600" id="errorContainer">{$stateStore.error}</div>
|
||||
{/if}
|
||||
|
||||
{#if outOfSync}
|
||||
<div class="absolute w-full p-2 z-10 text-yellow-600 bg-base-100 bg-opacity-80 text-center">
|
||||
Diagram out of sync. <br />
|
||||
Press <i class="fas fa-sync" /> (Sync button) or <kbd>{cmdKey} + Enter</kbd> to sync.
|
||||
</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>
|
||||
|
||||
@@ -2,4 +2,3 @@ export const rendererUrl: string =
|
||||
(import.meta.env.MERMAID_RENDERER_URL as string) ?? 'https://mermaid.ink';
|
||||
export const krokiRendererUrl: string =
|
||||
(import.meta.env.MERMAID_KROKI_RENDERER_URL as string) ?? 'https://kroki.io';
|
||||
export const debounceEnabled: boolean = import.meta.env.MERMAID_DISABLE_DEBOUNCE !== 'true';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { writable, get, derived } from 'svelte/store';
|
||||
import { persist, localStorage } from './persist';
|
||||
import { saveStatistics } from './stats';
|
||||
import { saveStatistics, countLines } from './stats';
|
||||
import { serializeState, deserializeState } from './serde';
|
||||
import { cmdKey } from './util';
|
||||
import mermaid from 'mermaid';
|
||||
@@ -121,8 +121,8 @@ export const updateCode = (
|
||||
resetPanZoom = false
|
||||
}: { updateDiagram?: boolean; resetPanZoom?: boolean } = {}
|
||||
): void => {
|
||||
const lines = countLines(code);
|
||||
saveStatistics(code);
|
||||
const lines = (code.match(/\n/g) || '').length + 1;
|
||||
|
||||
if (lines > 50 && !prompted && get(stateStore).autoSync) {
|
||||
const turnOff = confirm(
|
||||
@@ -162,9 +162,13 @@ export const toggleDarkTheme = (dark: boolean): void => {
|
||||
});
|
||||
};
|
||||
|
||||
let urlDebounce: number;
|
||||
export const initURLSubscription = (): void => {
|
||||
stateStore.subscribe(({ serialized }) => {
|
||||
history.replaceState(undefined, undefined, `#${serialized}`);
|
||||
clearTimeout(urlDebounce);
|
||||
urlDebounce = window.setTimeout(() => {
|
||||
history.replaceState(undefined, undefined, `#${serialized}`);
|
||||
}, 250);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { detectType } from './stats';
|
||||
describe('diagram detection', () => {
|
||||
it('should detect diagrams correctly', () => {
|
||||
expect(
|
||||
detectType(`%%{{
|
||||
graph`)
|
||||
).toBe('graph');
|
||||
expect(detectType(`gitGraph`)).toBe('gitGraph');
|
||||
expect(
|
||||
detectType(`%%{{
|
||||
|
||||
|
||||
flowChart
|
||||
graph`)
|
||||
).toBe('flowChart');
|
||||
expect(detectType(`loki -> thor`)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
+64
-28
@@ -1,25 +1,17 @@
|
||||
import { browser } from '$app/environment';
|
||||
import type { AnalyticsInstance } from 'analytics';
|
||||
|
||||
export let analytics: AnalyticsInstance;
|
||||
|
||||
export const initAnalytics = async (): Promise<void> => {
|
||||
if (browser && !analytics) {
|
||||
try {
|
||||
const { Analytics } = await import('analytics');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const googleAnalytics = (await import('@analytics/google-analytics')).default;
|
||||
const plausible = (await import('analytics-plugin-plausible')).default;
|
||||
const [{ Analytics }, { default: plausible }] = await Promise.all([
|
||||
import('analytics'),
|
||||
import('analytics-plugin-plausible')
|
||||
]);
|
||||
analytics = Analytics({
|
||||
app: 'mermaid-live-editor',
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
plugins: [
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
googleAnalytics({
|
||||
measurementIds: ['UA-153180559-1']
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
plausible({
|
||||
domain: 'mermaid.live',
|
||||
hashMode: false,
|
||||
@@ -35,28 +27,72 @@ export const initAnalytics = async (): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
const detectType = (text: string): string => {
|
||||
return text
|
||||
export const detectType = (text: string): string => {
|
||||
const possibleDiagramTypes = [
|
||||
'classDiagram',
|
||||
'erDiagram',
|
||||
'flowChart',
|
||||
'gantt',
|
||||
'gitGraph',
|
||||
'graph',
|
||||
'journey',
|
||||
'pie',
|
||||
'stateDiagram'
|
||||
];
|
||||
const firstLine = text
|
||||
.replace(/^\s*%%.*\n/g, '\n')
|
||||
.trimStart()
|
||||
.split(' ')[0];
|
||||
.split(' ')[0]
|
||||
.toLowerCase();
|
||||
const detectedDiagram = possibleDiagramTypes.find((d) => firstLine.includes(d.toLowerCase()));
|
||||
return detectedDiagram;
|
||||
};
|
||||
|
||||
export const countLines = (code: string): number => {
|
||||
return (code.match(/\n/g) || '').length + 1;
|
||||
};
|
||||
|
||||
// manual debounce
|
||||
let timeout: number;
|
||||
export const saveStatistics = (graph: string): void => {
|
||||
if (analytics) {
|
||||
clearTimeout(timeout);
|
||||
// Only save statistics after a 5 sec delay
|
||||
timeout = window.setTimeout(() => {
|
||||
const graphType = detectType(graph);
|
||||
console.debug(`ga: send event: render ${graphType}`);
|
||||
void logEvent('render', { graphType });
|
||||
}, 5000);
|
||||
const graphType = detectType(graph);
|
||||
if (!graphType) {
|
||||
return;
|
||||
}
|
||||
const length = countLines(graph);
|
||||
logEvent('render', { graphType, length });
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const logEvent = async (name: string, data?: any): Promise<void> => {
|
||||
await analytics?.track(name, data);
|
||||
const minutesToMilliSeconds = (minutes: number): number => {
|
||||
return minutes * 60_000;
|
||||
};
|
||||
|
||||
const defaultDelay = minutesToMilliSeconds(1);
|
||||
const delaysPerEvent = {
|
||||
render: minutesToMilliSeconds(5),
|
||||
panZoom: minutesToMilliSeconds(10),
|
||||
copyClipboard: defaultDelay,
|
||||
download: defaultDelay,
|
||||
copyMarkdown: defaultDelay,
|
||||
loadGist: defaultDelay,
|
||||
loadSampleDiagram: defaultDelay,
|
||||
renderDiagram: defaultDelay,
|
||||
history: defaultDelay,
|
||||
migration: defaultDelay,
|
||||
themeChange: defaultDelay
|
||||
};
|
||||
export type AnalyticsEvent = keyof typeof delaysPerEvent;
|
||||
const timeouts: Record<string, number> = {};
|
||||
// manual debounce to reduce the number of events sent to analytics
|
||||
export const logEvent = (name: AnalyticsEvent, data?: unknown): void => {
|
||||
if (!analytics) {
|
||||
return;
|
||||
}
|
||||
const key = data ? JSON.stringify({ name, data }) : name;
|
||||
if (timeouts[key] === undefined) {
|
||||
void analytics.track(name, data);
|
||||
} else {
|
||||
clearTimeout(timeouts[key]);
|
||||
}
|
||||
timeouts[key] = window.setTimeout(() => {
|
||||
delete timeouts[key];
|
||||
}, delaysPerEvent[name]);
|
||||
};
|
||||
|
||||
@@ -34,5 +34,5 @@ export const setTheme = (theme: string): void => {
|
||||
const isDark = darkThemes.includes(theme);
|
||||
console.log('Setting theme', theme);
|
||||
themeStore.set({ theme, isDark });
|
||||
void logEvent('themeChange', { theme, isDark });
|
||||
logEvent('themeChange', { theme, isDark });
|
||||
};
|
||||
|
||||
@@ -130,7 +130,9 @@
|
||||
{/if}
|
||||
|
||||
<button class="btn btn-secondary btn-xs" title="View documentation">
|
||||
<a target="_blank" href={docURL} data-cy="docs"><i class="fas fa-book mr-1" />Docs</a>
|
||||
<a target="_blank" rel="noreferrer" href={docURL} data-cy="docs">
|
||||
<i class="fas fa-book mr-1" />Docs
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -158,6 +160,7 @@
|
||||
<a
|
||||
href={`${base}/view#${$stateStore.serialized}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="btn btn-secondary btn-xs"
|
||||
title="View diagram in new page"
|
||||
><i class="fas fa-external-link-alt mr-1" />Full screen</a>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 491 491" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path d="M490.16,84.61C490.16,37.912 452.248,0 405.55,0L84.61,0C37.912,0 0,37.912 0,84.61L0,405.55C0,452.248 37.912,490.16 84.61,490.16L405.55,490.16C452.248,490.16 490.16,452.248 490.16,405.55L490.16,84.61Z" style="fill:rgb(255,54,112);"/>
|
||||
<path d="M407.48,111.18C335.587,108.103 269.573,152.338 245.08,220C220.587,152.338 154.573,108.103 82.68,111.18C80.285,168.229 107.577,222.632 154.74,254.82C178.908,271.419 193.35,298.951 193.27,328.27L193.27,379.13L296.9,379.13L296.9,328.27C296.816,298.953 311.255,271.42 335.42,254.82C382.596,222.644 409.892,168.233 407.48,111.18Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 47 KiB |
@@ -3,12 +3,7 @@
|
||||
"name": "Mermaid Live Editor",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "/icon-512.png",
|
||||
"src": "/favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user