diff --git a/.vscode/settings.json b/.vscode/settings.json index 70dc2f2f..47967284 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "editor.formatOnSave": true, - "cSpell.words": ["pako", "Serde", "serdes"], + "cSpell.words": ["asyncable", "pako", "Serde", "serdes"], "vitest.commandLine": "yarn test:unit", "vitest.enable": true, "testing.autoRun.mode": "rerun", diff --git a/cypress/e2e/diagramUpdate.spec.ts b/cypress/e2e/diagramUpdate.spec.ts index 533a1adc..477fed29 100644 --- a/cypress/e2e/diagramUpdate.spec.ts +++ b/cypress/e2e/diagramUpdate.spec.ts @@ -10,7 +10,7 @@ describe('Auto sync tests', () => { 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'); + getEditor({ bottom: true, newline: true }).type(' C --> Test'); cy.get('#view').should('have.class', 'outOfSync'); cy.get('#view').should('contain.text', 'Diagram out of sync.'); cy.getLocalStorage('codeStore').snapshot(); @@ -71,7 +71,7 @@ describe('Auto sync tests', () => { }); }); -describe.only('Pan and Zoom', () => { +describe('Pan and Zoom', () => { beforeEach(() => { cy.clearLocalStorage(); cy.visit('/'); diff --git a/cypress/e2e/loadSite.spec.ts b/cypress/e2e/loadSite.spec.ts index 841b4024..479c6a12 100644 --- a/cypress/e2e/loadSite.spec.ts +++ b/cypress/e2e/loadSite.spec.ts @@ -20,39 +20,41 @@ describe('Site Loads', () => { it('should load sample diagrams when clicked', () => { cy.contains('Sample Diagrams').click(); - cy.contains('Pie Chart').click(); + cy.contains('Pie').click(); cy.contains('pie title Pets adopted by volunteers'); - cy.contains('Class Diagram').click(); + cy.contains('Class').click(); cy.contains('classDiagram'); }); - it('should load diagram from gist', () => { - cy.visit(`/edit?gist=https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a`); - cy.contains('History').click(); - cy.contains('Go shopping!!'); - cy.contains('Revisions'); - cy.contains('sidharthv96 v8f8f1e2'); - cy.contains('sidharthv96 v7851e19'); - cy.getLocalStorage('codeStore').snapshot(); - }); + 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 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(); + 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. @@ -71,7 +73,7 @@ describe('Site Loads', () => { // cy.get('#view').contains(`src='https://via.placeholder.com/64'`); // }); - it('should allow persisting "securityLevel" using confirm dialogue', () => { + it.only('should allow persisting "securityLevel" using confirm dialogue', () => { const b64State = toBase64( `{"code":"graph TD\\nA[\\"\\"]","mermaid":"{\\"securityLevel\\": \\"loose\\", \\"theme\\": \\"forest\\"}","autoSync":true,"updateDiagram":true}`, true diff --git a/package.json b/package.json index e6bb31c3..b9e6997b 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "license": "MIT", "scripts": { "dev": "vite dev", + "dev:force": "MERMAID_LOCAL=true yarn dev --force", "dev:test": "yarn dev", "build": "vite build", "preview": "vite preview", @@ -26,7 +27,6 @@ "@sveltejs/kit": "1.0.0-next.516", "@testing-library/jest-dom": "5.16.5", "@testing-library/svelte": "3.2.2", - "@types/mermaid": "9.1.0", "@types/pako": "2.0.0", "@types/uuid": "8.3.4", "@typescript-eslint/eslint-plugin": "5.40.1", @@ -57,6 +57,7 @@ "prettier": "2.7.1", "prettier-plugin-svelte": "2.8.0", "svelte": "3.52.0", + "svelte-asyncable": "^2.1.0", "svelte-preprocess": "4.10.7", "tailwindcss": "3.1.8", "tslib": "2.4.0", @@ -69,7 +70,7 @@ "analytics-plugin-plausible": "0.0.6", "daisyui": "2.31.0", "js-base64": "3.7.2", - "mermaid": "9.1.7", + "mermaid": "9.2.0-rc7", "moment": "2.29.4", "monaco-editor": "0.34.1", "monaco-mermaid": "1.0.6", @@ -91,4 +92,4 @@ "engines": { "node": ">=16.7" } -} +} \ No newline at end of file diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 00000000..571447df --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,14 @@ +/// + +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; +} diff --git a/src/lib/components/actions.svelte b/src/lib/components/actions.svelte index 48784ef1..9f8a4f56 100644 --- a/src/lib/components/actions.svelte +++ b/src/lib/components/actions.svelte @@ -1,12 +1,13 @@ -
- {#each Object.keys(samples) as sample} - +
+ {#each diagramOrder as sample} + {/each}
diff --git a/src/lib/components/view.svelte b/src/lib/components/view.svelte index 8cf21549..b9986c6a 100644 --- a/src/lib/components/view.svelte +++ b/src/lib/components/view.svelte @@ -1,11 +1,11 @@ -{#if error && $stateStore.error instanceof Error} -
{$stateStore.error}
-{/if} +{#await $stateStore then state} + {#if error && state.error instanceof Error} +
{state.error}
+ {/if} +{/await} {#if outOfSync}
diff --git a/src/lib/util/env.ts b/src/lib/util/env.ts index 128761f2..f5738f65 100644 --- a/src/lib/util/env.ts +++ b/src/lib/util/env.ts @@ -1,4 +1,9 @@ -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 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 +}; diff --git a/src/lib/util/mermaid.ts b/src/lib/util/mermaid.ts new file mode 100644 index 00000000..1b95b5f2 --- /dev/null +++ b/src/lib/util/mermaid.ts @@ -0,0 +1,41 @@ +import mermaid from 'mermaid'; +// We need to export MermaidConfig and all related types from mermaid. +import type { MermaidConfig } from 'mermaid/dist/config.type'; +import { env } from './env'; +const { mermaidBaseURL, mermaidCDNUrl, useLocalMermaid } = env; + +const getDiagramURL = (name: string, version: string): string => { + if (useLocalMermaid) { + return `${mermaidBaseURL}/${name}-detector.esm.mjs`; + } + return `${mermaidCDNUrl}/${name}@${version}/dist/${name}-detector.esm.mjs`; +}; + +const initialize = mermaid.initializeAsync({ + // logLevel: 0, + lazyLoadedDiagrams: [getDiagramURL('mermaid-mindmap', '9.2.0-rc4')], + loadExternalDiagramsAtStartup: true +}); + +export const init = async () => { + await initialize; +}; + +export const render = async ( + config: MermaidConfig, + code: string, + id: string, + callback: Parameters[2] +): Promise => { + // Should be able to call this multiple times without any issues. + mermaid.initialize(config); + // console.log('Rendering', code); + // mermaid.mermaidAPI.render(id, code, callback); + await init(); + await mermaid.mermaidAPI.renderAsync(id, code, callback); +}; + +export const parse = async (code: string): Promise => { + await init(); + return mermaid.parseAsync(code); +}; diff --git a/src/lib/util/state.ts b/src/lib/util/state.ts index 9237c44f..d678d54f 100644 --- a/src/lib/util/state.ts +++ b/src/lib/util/state.ts @@ -1,13 +1,13 @@ -import { writable, get, derived } from 'svelte/store'; +import { writable, get, type Readable } from 'svelte/store'; import { persist, localStorage } from './persist'; import { saveStatistics, countLines } from './stats'; import { serializeState, deserializeState } from './serde'; -import { cmdKey } from './util'; -import mermaid from 'mermaid'; +import { asyncable, syncable } from 'svelte-asyncable'; +import { cmdKey, errorDebug } from './util'; +import { parse } from './mermaid'; -import type { Readable } from 'svelte/store'; import type { MarkerData, State, ValidatedState } from '$lib/types'; - +let count = 0; export const defaultState: State = { code: `graph TD A[Christmas] -->|Get money| B(Go shopping) @@ -41,42 +41,96 @@ const urlParseFailedState = `graph TD // inputStateStore handles all updates and is shared externally when exporting via URL, History, etc. export const inputStateStore = persist(writable(defaultState), localStorage(), 'codeStore'); -// All internal reads should be done via stateStore, but it should not be persisted/shared externally. -export const stateStore: Readable = derived([inputStateStore], ([state]) => { - const processed: ValidatedState = { +export let currentState: ValidatedState = (() => { + const state = get(inputStateStore); + return { ...state, - serialized: '', + serialized: serializeState(state), errorMarkers: [], error: undefined, editorMode: state.editorMode ?? 'code' }; +})(); - // No changes should be done to fields part of `state`. - try { - processed.serialized = serializeState(state); - mermaid.parse(state.code); - JSON.parse(state.mermaid); - } catch (e) { - processed.error = e; - console.error(e); - if (e.hash) { - try { - const marker: MarkerData = { - severity: 8, // Error - startLineNumber: e.hash.loc.first_line, - startColumn: e.hash.loc.first_column, - endLineNumber: e.hash.loc.last_line, - endColumn: (e.hash.loc.last_column as number) + 1, - message: e.str - }; - processed.errorMarkers = [marker]; - } catch (err) { - console.error('Error without line helper', err); +// All internal reads should be done via stateStore, but it should not be persisted/shared externally. +export const stateStore = asyncable( + async (state: State) => { + const processed: ValidatedState = { + ...state, + serialized: '', + errorMarkers: [], + error: undefined, + editorMode: state.editorMode ?? 'code' + }; + + console.log('asyncable', state); + // No changes should be done to fields part of `state`. + try { + processed.serialized = serializeState(state); + await parse(state.code); + JSON.parse(state.mermaid); + } catch (e) { + processed.error = e; + errorDebug(); + console.error(e); + if (e.hash) { + try { + const marker: MarkerData = { + severity: 8, // Error + startLineNumber: e.hash.loc.first_line, + startColumn: e.hash.loc.first_column, + endLineNumber: e.hash.loc.last_line, + endColumn: (e.hash.loc.last_column as number) + 1, + message: e.str + }; + processed.errorMarkers = [marker]; + } catch (err) { + console.error('Error without line helper', err); + } } } - } - return processed; -}); + currentState = processed; + return processed; + }, + undefined, + [inputStateStore] +); + +// export const stateStore: Readable = derived([inputStateStore], ([state]) => { +// const processed: ValidatedState = { +// ...state, +// serialized: '', +// errorMarkers: [], +// error: undefined, +// editorMode: state.editorMode ?? 'code' +// }; + +// // No changes should be done to fields part of `state`. +// try { +// processed.serialized = serializeState(state); +// mermaid.parse(state.code); +// JSON.parse(state.mermaid); +// } catch (e) { +// processed.error = e; +// console.error(e); +// if (e.hash) { +// try { +// const marker: MarkerData = { +// severity: 8, // Error +// startLineNumber: e.hash.loc.first_line, +// startColumn: e.hash.loc.first_column, +// endLineNumber: e.hash.loc.last_line, +// endColumn: (e.hash.loc.last_column as number) + 1, +// message: e.str +// }; +// processed.errorMarkers = [marker]; +// } catch (err) { +// console.error('Error without line helper', err); +// } +// } +// } +// return processed; +// }); export const loadState = (data: string): void => { let state: State; @@ -103,7 +157,7 @@ export const loadState = (data: string): void => { state.mermaid = defaultState.mermaid; } } - updateCodeStore({ ...state }); + updateCodeStore(state); }; export const updateCodeStore = (newState: Partial): void => { @@ -114,17 +168,18 @@ export const updateCodeStore = (newState: Partial): void => { }; let prompted = false; -export const updateCode = ( +export const updateCode = async ( code: string, { updateDiagram = false, resetPanZoom = false }: { updateDiagram?: boolean; resetPanZoom?: boolean } = {} -): void => { +): Promise => { + console.log('updateCode', code); const lines = countLines(code); saveStatistics(code); - - if (lines > 50 && !prompted && get(stateStore).autoSync) { + errorDebug(); + if (lines > 50 && !prompted && (await get(stateStore)).autoSync) { const turnOff = confirm( `Long diagram detected. Turn off Auto Sync? Use ${cmdKey} + Enter or click the sync logo to manually sync.` ); @@ -146,6 +201,7 @@ export const updateCode = ( }; export const updateConfig = (config: string): void => { + console.log('updateConfig', config); inputStateStore.update((state) => { return { ...state, mermaid: config }; }); @@ -164,7 +220,8 @@ export const toggleDarkTheme = (dark: boolean): void => { let urlDebounce: number; export const initURLSubscription = (): void => { - stateStore.subscribe(({ serialized }) => { + stateStore.subscribe(async (state) => { + const { serialized } = await state; clearTimeout(urlDebounce); urlDebounce = window.setTimeout(() => { history.replaceState(undefined, undefined, `#${serialized}`); diff --git a/src/lib/util/util.ts b/src/lib/util/util.ts index d3995a2f..7b5456b4 100644 --- a/src/lib/util/util.ts +++ b/src/lib/util/util.ts @@ -26,3 +26,13 @@ export const initHandler = async (): Promise => { export const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; export const cmdKey = isMac ? 'Cmd' : 'Ctrl'; + +let count = 0; +export const errorDebug = (limit = 100) => { + count += 1; + if (count > limit) { + console.log(count, limit); + // eslint-disable-next-line no-debugger + debugger; + } +}; diff --git a/src/routes/edit/+page.svelte b/src/routes/edit/+page.svelte index 19293e86..fc219c6e 100644 --- a/src/routes/edit/+page.svelte +++ b/src/routes/edit/+page.svelte @@ -54,7 +54,8 @@ }; let docURL = docURLBase; let activeTabID = 'code'; - stateStore.subscribe(({ code, editorMode }: ValidatedState) => { + stateStore.subscribe(async (state) => { + const { code, editorMode } = await state; activeTabID = editorMode; const codeTypeMatch = /([\S]+)[\s\n]/.exec(code); if (codeTypeMatch && codeTypeMatch.length > 1) { @@ -84,28 +85,30 @@ onMount(async () => { await initHandler(); - const resizer = document.getElementById('resizeHandler'); - const element = document.getElementById('editorPane'); - const resize = (e: { pageX: number }) => { - const newWidth = e.pageX - element.getBoundingClientRect().left; - if (newWidth > 50) { - element.style.width = `${newWidth}px`; - } - }; - const stopResize = () => { - window.removeEventListener('mousemove', resize); - }; - resizer.addEventListener('mousedown', (e) => { - e.preventDefault(); - window.addEventListener('mousemove', resize); - window.addEventListener('mouseup', stopResize); - }); + // const resizer = document.getElementById('resizeHandler'); + // const element = document.getElementById('editorPane'); + // const resize = (e: { pageX: number }) => { + // const newWidth = e.pageX - element.getBoundingClientRect().left; + // if (newWidth > 50) { + // element.style.width = `${newWidth}px`; + // } + // }; + + // const stopResize = () => { + // window.removeEventListener('mousemove', resize); + // }; + // resizer.addEventListener('mousedown', (e) => { + // e.preventDefault(); + // window.addEventListener('mousemove', resize); + // window.addEventListener('mouseup', stopResize); + // }); });
+