Merge pull request #1374 from mermaid-js/release-promotion

Release live editor
This commit is contained in:
Sidharth Vinod
2024-01-23 13:46:03 +05:30
committed by GitHub
18 changed files with 673 additions and 570 deletions
+13 -11
View File
@@ -1,6 +1,6 @@
import { defineConfig } from 'cypress';
import fs from 'fs';
import { isFileExist, findFiles } from 'cy-verify-downloads';
import path from 'path';
export default defineConfig({
projectId: '2ckppp',
viewportWidth: 1440,
@@ -15,17 +15,19 @@ export default defineConfig({
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');
readAndDeleteFile({ fileNamePattern, folder, mode }) {
const fileNameRegex = new RegExp(fileNamePattern);
const files = fs.readdirSync(folder);
const filename = files.find((file) => file.match(fileNameRegex));
const filePath = path.join(folder, filename);
try {
if (mode === 'size') {
return fs.statSync(filePath).size;
}
return fs.readFileSync(filePath, 'utf8');
} finally {
fs.rmSync(filePath);
}
return null;
}
});
},
-4
View File
@@ -26,8 +26,6 @@ describe('Check actions', () => {
});
it('should download png and svg', () => {
cy.clock(new Date(2022, 0, 1).getTime());
cy.get(`#downloadPNG`).click();
verifyFileSizeGreaterThan('diagram', 'png', 34_000);
@@ -43,7 +41,5 @@ describe('Check actions', () => {
cy.get(`#downloadSVG`).click();
verifyFileSizeGreaterThan('diagram', 'svg', 11_000);
cy.clock().invoke('restore');
});
});
+18
View File
@@ -50,6 +50,24 @@ describe('Auto sync tests', () => {
cy.getLocalStorage('codeStore').snapshot();
});
it('should automatically defer rendering when complex diagrams are edited', () => {
cy.get('#view').should('not.have.class', 'outOfSync');
typeInEditor(`
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i`);
cy.get('#view').should('have.class', 'outOfSync');
cy.get('#errorContainer').should('contain.text', 'It will be updated automatically.');
// The class should be removed automatically after 1 second.
cy.get('#view').should('not.have.class', 'outOfSync');
});
it('supports commenting code out/in', () => {
cy.get('#editor').contains('Car').click();
cy.get('#editor').get('textarea').type(`${cmd}/`, { force: true });
+15 -20
View File
@@ -29,17 +29,15 @@ export const verifyFileSizeGreaterThan = (
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.get('#view').should('not.have.class', 'outOfSync');
cy.task('readAndDeleteFile', {
folder: downloadsFolder,
fileNamePattern: `^mermaid-${fileType}-.*.${extension}$`,
mode: 'size'
}).then((fileSize: number) => {
expect(fileSize).to.be.gt(size);
expect(fileSize).to.be.lt(size * 1.3);
});
cy.task('deleteFile', filePath);
};
export const verifyFileSnapshot = (
@@ -47,14 +45,11 @@ export const verifyFileSnapshot = (
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);
cy.task('readAndDeleteFile', {
folder: downloadsFolder,
fileNamePattern: `^mermaid-${fileType}-.*.${extension}$`,
mode: 'content'
}).then((fileContent: number) => {
expect(fileContent).to.contain(content);
});
};
-1
View File
@@ -15,7 +15,6 @@
// Import commands.js using ES2015 syntax:
import './commands';
require('cy-verify-downloads').addCustomCommand();
// Alternatively you can use CommonJS syntax:
// require('./commands')
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"allowJs": true,
"types": ["cypress", "cypress-localstorage-commands", "cy-verify-downloads", "node"]
"types": ["cypress", "cypress-localstorage-commands", "node"]
},
"include": ["**/*.ts"]
}
+7 -7
View File
@@ -24,19 +24,19 @@
"devDependencies": {
"@cypress/snapshot": "2.1.7",
"@sveltejs/adapter-static": "3.0.1",
"@sveltejs/kit": "2.3.0",
"@sveltejs/kit": "2.4.3",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@testing-library/svelte": "4.0.5",
"@types/lodash-es": "^4.17.12",
"@types/pako": "2.0.3",
"@types/uuid": "9.0.7",
"@typescript-eslint/eslint-plugin": "6.18.1",
"@typescript-eslint/parser": "6.18.1",
"@typescript-eslint/eslint-plugin": "6.19.1",
"@typescript-eslint/parser": "6.19.1",
"@vitest/ui": "^1.1.3",
"autoprefixer": "^10.4.14",
"c8": "7.14.0",
"chai": "^4.3.7",
"cssnano": "^6.0.0",
"cy-verify-downloads": "0.2.2",
"cypress": "12.17.4",
"cypress-localstorage-commands": "2.2.5",
"eslint": "8.56.0",
@@ -50,7 +50,6 @@
"eslint-plugin-unicorn": "^50.0.1",
"eslint-plugin-vitest": "^0.3.20",
"esserializer": "^1.3.11",
"font-awesome": "^4.7.0",
"husky": "^8.0.3",
"jsdom": "^21.1.2",
"lint-staged": "^15.2.0",
@@ -72,8 +71,9 @@
"dependencies": {
"daisyui": "2.52.0",
"dayjs": "^1.11.7",
"js-base64": "3.7.5",
"mermaid": "10.6.1",
"js-base64": "3.7.6",
"lodash-es": "^4.17.21",
"mermaid": "10.7.0",
"monaco-editor": "0.45.0",
"pako": "2.1.0",
"plausible-tracker": "^0.3.8",
+2 -2
View File
@@ -17,8 +17,8 @@
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
integrity="sha512-Avb2QiuDEEvB4bZJYdft2mNjVShBftLdPG8FJ0V7irTLQ8Uo0qcPxh4Plq7G5tGm0rU+1SPhVotteLpBERwTkw=="
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA=="
crossorigin="anonymous"
referrerpolicy="no-referrer" />
%sveltekit.head%
+11 -6
View File
@@ -1,12 +1,13 @@
<script lang="ts">
import { browser } from '$app/environment';
import Card from '$lib/components/Card/Card.svelte';
import { waitForRender } from '$lib/util/autoSync';
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';
import { toBase64 } from 'js-base64';
const { krokiRendererUrl, rendererUrl } = env;
type Exporter = (context: CanvasRenderingContext2D, image: HTMLImageElement) => () => void;
@@ -30,7 +31,11 @@
return toBase64(svgString);
};
const exportImage = (event: Event, exporter: Exporter) => {
const exportImage = async (event: Event, exporter: Exporter) => {
await waitForRender();
if (document.querySelector('.outOfSync')) {
throw new Error('Diagram is out of sync');
}
const canvas: HTMLCanvasElement = document.createElement('canvas');
const svg = document.querySelector<HTMLElement>('#container svg');
if (!svg) {
@@ -122,13 +127,13 @@
};
};
const onCopyClipboard = (event: Event) => {
exportImage(event, clipboardCopy);
const onCopyClipboard = async (event: Event) => {
await exportImage(event, clipboardCopy);
logEvent('copyClipboard');
};
const onDownloadPNG = (event: Event) => {
exportImage(event, downloadImage);
const onDownloadPNG = async (event: Event) => {
await exportImage(event, downloadImage);
logEvent('download', {
type: 'png'
});
+1 -1
View File
@@ -88,7 +88,7 @@
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
initEditor(monaco);
errorDebug(100);
errorDebug();
editor = monaco.editor.create(divElement, editorOptions);
editor.onDidChangeModelContent(({ isFlush }) => {
const newText = editor?.getValue();
+19 -2
View File
@@ -3,10 +3,11 @@
import { onMount } from 'svelte';
import panzoom from 'svg-pan-zoom';
import type { State, ValidatedState } from '$lib/types';
import { logEvent } from '$lib/util/stats';
import { logEvent, saveStatistics } from '$lib/util/stats';
import { cmdKey } from '$lib/util/util';
import { render as renderDiagram } from '$lib/util/mermaid';
import type { MermaidConfig } from 'mermaid';
import { recordRenderTime, shouldRefreshView } from '$lib/util/autoSync';
let code = '';
let config = '';
@@ -59,6 +60,7 @@
};
const handleStateChange = async (state: ValidatedState) => {
const startTime = Date.now();
if (state.error !== undefined) {
error = true;
errorLines = state.error.toString().split('\n');
@@ -76,6 +78,12 @@
if (code === state.code && config === state.mermaid && panZoomEnabled === state.panZoom) {
return;
}
if (!shouldRefreshView()) {
outOfSync = true;
return;
}
code = state.code;
config = state.mermaid;
panZoomEnabled = state.panZoom;
@@ -114,6 +122,11 @@
console.error('view fail', error_);
error = true;
}
const timeTaken = Date.now() - startTime;
saveStatistics(code, timeTaken);
recordRenderTime(timeTaken, () => {
$inputStateStore.updateDiagram = true;
});
};
onMount(() => {
@@ -140,7 +153,11 @@
{/each}
{:else}
Diagram out of sync. <br />
Press <i class="fas fa-sync" /> (Sync button) or <kbd>{cmdKey} + Enter</kbd> to sync.
{#if $stateStore.autoSync}
It will be updated automatically.
{:else}
Press <i class="fas fa-sync" /> (Sync button) or <kbd>{cmdKey} + Enter</kbd> to sync.
{/if}
{/if}
</div>
{/if}
+49
View File
@@ -0,0 +1,49 @@
import debounce from 'lodash-es/debounce';
import { get } from 'svelte/store';
import { stateStore } from './state';
let shouldSync = true;
let updater: () => void;
let renderPromise: Promise<void> | undefined;
let resolveRenderPromise: (() => void) | undefined;
const renderDelay = 1000;
const slowRenderThreshold = 150;
const debouncedRender = debounce(() => {
shouldSync = true;
updater();
}, renderDelay);
export const recordRenderTime = (renderTimeMs: number, updaterFunction: () => void): void => {
resolveRenderPromise?.();
const { autoSync } = get(stateStore);
if (!autoSync) {
return;
}
updater = updaterFunction;
const isSlow = renderTimeMs > slowRenderThreshold;
if (!shouldSync) {
debouncedRender();
}
shouldSync = !isSlow;
};
export const shouldRefreshView = (): boolean => {
if (!renderPromise) {
renderPromise = new Promise((resolve) => {
resolveRenderPromise = () => {
renderPromise = undefined;
resolve();
};
});
}
if (!shouldSync) {
debouncedRender();
}
return shouldSync;
};
export const waitForRender = (): Promise<void> => {
return renderPromise ?? Promise.resolve();
};
+17 -7
View File
@@ -1,15 +1,25 @@
<script lang="ts">
const taglines = [
'Try diagramming with ChatGPT at Mermaid Chart',
"Try Mermaid's Visual Editor at Mermaid Chart",
'Enjoy live collaboration with teammates at Mermaid Chart'
];
const taglines = {
announcement_bar_ai_diagramming: 'Try diagramming with ChatGPT at Mermaid Chart',
announcement_bar_visual_editor: "Try Mermaid's Visual Editor at Mermaid Chart",
announcement_bar_live_collaboration: 'Enjoy live collaboration with teammates at Mermaid Chart'
};
const taglineKeys = Object.keys(taglines);
const taglineKey = taglineKeys[Math.floor(Math.random() * taglineKeys.length)];
const tagline = taglines[taglineKey];
const url =
'https://www.mermaidchart.com/?' +
new URLSearchParams({
utm_source: 'mermaid_live_editor',
utm_medium: taglineKey,
utm_campaign: 'promo_2024'
}).toString();
</script>
<a
href="https://www.mermaidchart.com/"
href={url}
target="_blank"
class="flex flex-grow justify-center gap-6 align-middle tracking-wide">
{taglines[Math.floor(Math.random() * taglines.length)]}
{tagline}
<button class="rounded bg-gray-800 p-1 px-4 text-sm font-light">Try it now</button>
</a>
+1 -1
View File
@@ -14,7 +14,7 @@ const promotions: Promotion[] = [
{
id: 'holiday-2023',
startDate: new Date('2023-11-27'),
endDate: new Date('2024-01-9'),
endDate: new Date('2024-01-09'),
component: Holiday2023
},
{
+11 -27
View File
@@ -1,12 +1,11 @@
import { writable, get, type Readable, derived } from 'svelte/store';
import { persist, localStorage } from './persist';
import { saveStatistics, countLines } from './stats';
import { serializeState, deserializeState } from './serde';
import { cmdKey, errorDebug, formatJSON } from './util';
import { parse } from './mermaid';
import type { ErrorHash, MarkerData, State, ValidatedState } from '$lib/types';
import { debounce } from 'lodash-es';
import type { MermaidConfig } from 'mermaid';
import { derived, get, writable, type Readable } from 'svelte/store';
import { parse } from './mermaid';
import { localStorage, persist } from './persist';
import { deserializeState, serializeState } from './serde';
import { errorDebug, formatJSON } from './util';
export const defaultState: State = {
code: `flowchart TD
@@ -134,7 +133,6 @@ export const updateCodeStore = (newState: Partial<State>): void => {
});
};
let prompted = false;
export const updateCode = (
code: string,
{
@@ -142,21 +140,7 @@ export const updateCode = (
resetPanZoom = false
}: { updateDiagram?: boolean; resetPanZoom?: boolean } = {}
): void => {
// console.log('updateCode', code);
const lines = countLines(code);
saveStatistics(code);
errorDebug();
if (lines > 50 && !prompted && get(stateStore).autoSync) {
const turnOff = confirm(
`Long diagram detected. Turn off Auto Sync? Use ${cmdKey} + Enter or click the sync logo to manually sync.`
);
prompted = true;
if (turnOff) {
updateCodeStore({
autoSync: false
});
}
}
inputStateStore.update((state) => {
if (resetPanZoom) {
@@ -185,13 +169,13 @@ export const toggleDarkTheme = (dark: boolean): void => {
});
};
let urlDebounce: number;
export const initURLSubscription = (): void => {
const updateHash = debounce((hash) => {
history.replaceState(undefined, '', `#${hash}`);
}, 250);
stateStore.subscribe(({ serialized }) => {
clearTimeout(urlDebounce);
urlDebounce = window.setTimeout(() => {
history.replaceState(undefined, '', `#${serialized}`);
}, 250);
updateHash(serialized);
});
};
+34 -22
View File
@@ -47,33 +47,45 @@ export const countLines = (code: string): number => {
return (code.match(/\n/g)?.length ?? 0) + 1;
};
export const saveStatistics = (graph: string): void => {
export const saveStatistics = (graph: string, renderTime: number): void => {
const graphType = detectType(graph);
if (!graphType) {
return;
}
const length = countLines(graph);
const lengthBucket =
length < 10
? '0-10'
: length < 25
? '10-25'
: length < 50
? '25-50'
: length < 100
? '50-100'
: length < 200
? '100-200'
: length < 500
? '200-500'
: length < 700
? '500-700'
: length < 1000
? '700-1000'
: length < 1500
? '1000-1500'
: '1500+';
logEvent('render', { graphType, length, lengthBucket });
const lengthBucket = getBucket(length);
const renderTimeMsBucket = getBucket(renderTime);
logEvent('render', { graphType, length, lengthBucket, renderTimeMsBucket });
};
const getBucket = (length: number): string => {
return length < 10
? '0-10'
: length < 25
? '10-25'
: length < 50
? '25-50'
: length < 100
? '50-100'
: length < 200
? '100-200'
: length < 500
? '200-500'
: length < 700
? '500-700'
: length < 1000
? '700-1000'
: length < 1500
? '1000-1500'
: length < 2500
? '1500-2500'
: length < 4500
? '2500-4500'
: length < 7000
? '4500-7000'
: length < 10_000
? '7000-10000'
: '10000+';
};
const minutesToMilliSeconds = (minutes: number): number => {
+1 -1
View File
@@ -28,7 +28,7 @@ export const isMac = navigator.platform.toUpperCase().includes('MAC');
export const cmdKey = isMac ? 'Cmd' : 'Ctrl';
let count = 0;
export const errorDebug = (limit = 100) => {
export const errorDebug = (limit = 1000) => {
count += 1;
if (count > limit) {
console.log(count, limit);
+473 -457
View File
File diff suppressed because it is too large Load Diff