+
diff --git a/src/lib/components/view.svelte b/src/lib/components/view.svelte
index e50e1580..8cf21549 100644
--- a/src/lib/components/view.svelte
+++ b/src/lib/components/view.svelte
@@ -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 @@
{$stateStore.error}
{/if}
+{#if outOfSync}
+
+ Diagram out of sync.
+ Press (Sync button) or {cmdKey} + Enter to sync.
+
+{/if}
diff --git a/src/lib/util/env.ts b/src/lib/util/env.ts
index dd40b8dc..128761f2 100644
--- a/src/lib/util/env.ts
+++ b/src/lib/util/env.ts
@@ -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';
diff --git a/src/lib/util/state.ts b/src/lib/util/state.ts
index 989269fa..9237c44f 100644
--- a/src/lib/util/state.ts
+++ b/src/lib/util/state.ts
@@ -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);
});
};
diff --git a/src/lib/util/stats.test.ts b/src/lib/util/stats.test.ts
new file mode 100644
index 00000000..09db84f5
--- /dev/null
+++ b/src/lib/util/stats.test.ts
@@ -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);
+ });
+});
diff --git a/src/lib/util/stats.ts b/src/lib/util/stats.ts
index 6df0afcf..9b4b9209 100644
--- a/src/lib/util/stats.ts
+++ b/src/lib/util/stats.ts
@@ -1,25 +1,17 @@
import { browser } from '$app/environment';
import type { AnalyticsInstance } from 'analytics';
-
export let analytics: AnalyticsInstance;
export const initAnalytics = async (): Promise
=> {
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 => {
}
};
-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 => {
- 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 = {};
+// 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]);
};
diff --git a/src/lib/util/theme.ts b/src/lib/util/theme.ts
index 24ae78fd..4221ee06 100644
--- a/src/lib/util/theme.ts
+++ b/src/lib/util/theme.ts
@@ -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 });
};
diff --git a/src/routes/edit/+page.svelte b/src/routes/edit/+page.svelte
index e6a14305..19293e86 100644
--- a/src/routes/edit/+page.svelte
+++ b/src/routes/edit/+page.svelte
@@ -130,7 +130,9 @@
{/if}
@@ -158,6 +160,7 @@