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 type { MermaidConfig } from 'mermaid'; export const defaultState: State = { code: `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] `, mermaid: formatJSON({ theme: 'default' }), autoSync: true, updateDiagram: true }; const urlParseFailedState = `flowchart TD A[Loading URL failed. We can try to figure out why.] -->|Decode JSON| B(Please check the console to see the JSON and error details.) B --> C{Is the JSON correct?} C -->|Yes| D(Please Click here to Raise an issue in github.
Including the broken link in the issue
will speed up the fix.) C -->|No| E{Did someone
send you this link?} E -->|Yes| F[Ask them to send
you the complete link] E -->|No| G{Did you copy
the complete URL?} G --> |Yes| D G --> |"No :("| H(Try using the Timeline tab in History
from same browser you used to create the diagram.) click D href "https://github.com/mermaid-js/mermaid-live-editor/issues/new?assignees=&labels=bug&template=bug_report.md&title=Broken%20link" "Raise issue"`; // inputStateStore handles all updates and is shared externally when exporting via URL, History, etc. export const inputStateStore = persist(writable(defaultState), localStorage(), 'codeStore'); export const currentState: ValidatedState = (() => { const state = get(inputStateStore); return { ...state, serialized: serializeState(state), errorMarkers: [], error: undefined, editorMode: state.editorMode ?? 'code' }; })(); const processState = async (state: 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); await parse(state.code); JSON.parse(state.mermaid); } catch (error) { processed.error = error as Error; errorDebug(); console.error(error); if ('hash' in error) { try { const { loc: { first_line, last_line, first_column, last_column } // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access } = error.hash as ErrorHash; const marker: MarkerData = { severity: 8, // Error startLineNumber: first_line, startColumn: first_column, endLineNumber: last_line, endColumn: last_column + 1, // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment message: error.str }; processed.errorMarkers = [marker]; } catch (error) { console.error('Error without line helper', error); } } } return processed; }; // All internal reads should be done via stateStore, but it should not be persisted/shared externally. export const stateStore: Readable = derived( [inputStateStore], ([state], set) => { void processState(state).then(set); }, currentState ); export const loadState = (data: string): void => { let state: State; console.log(`Loading '${data}'`); try { state = deserializeState(data); const mermaidConfig: MermaidConfig = typeof state.mermaid === 'string' ? (JSON.parse(state.mermaid) as MermaidConfig) : state.mermaid; if ( mermaidConfig.securityLevel && mermaidConfig.securityLevel !== 'strict' && confirm( `Removing "securityLevel":"${mermaidConfig.securityLevel}" from the config for safety.\nClick Cancel if you trust the source of this Diagram.` ) ) { delete mermaidConfig.securityLevel; // Prevent setting overriding securityLevel when loading state to mitigate possible XSS attack } state.mermaid = formatJSON(mermaidConfig); } catch (error) { state = get(inputStateStore); if (data) { console.error('Init error', error); state.code = urlParseFailedState; state.mermaid = defaultState.mermaid; } } updateCodeStore(state); }; export const updateCodeStore = (newState: Partial): void => { inputStateStore.update((state) => { return { ...state, ...newState }; }); }; let prompted = false; export const updateCode = ( code: string, { updateDiagram = false, 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) { state.pan = undefined; state.zoom = undefined; } return { ...state, code, updateDiagram }; }); }; export const updateConfig = (config: string): void => { // console.log('updateConfig', config); inputStateStore.update((state) => { return { ...state, mermaid: config }; }); }; export const toggleDarkTheme = (dark: boolean): void => { inputStateStore.update((state) => { const config = JSON.parse(state.mermaid) as MermaidConfig; if (!config.theme || ['dark', 'default'].includes(config.theme)) { config.theme = dark ? 'dark' : 'default'; } return { ...state, mermaid: formatJSON(config) }; }); }; let urlDebounce: number; export const initURLSubscription = (): void => { stateStore.subscribe(({ serialized }) => { clearTimeout(urlDebounce); urlDebounce = window.setTimeout(() => { history.replaceState(undefined, '', `#${serialized}`); }, 250); }); }; export const getStateString = (): string => { return JSON.stringify(get(inputStateStore)); };