From b52f2c42205751893f8279d7c4bd623b82dec76e Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Thu, 20 Jan 2022 18:08:53 +0530 Subject: [PATCH] Serdes --- .vscode/settings.json | 3 ++ cypress/integration/loadSite.spec.ts | 8 +++- src/lib/components/actions.svelte | 7 ++-- src/lib/util/serde.ts | 57 ++++++++++++++++++++++++++++ src/lib/util/state.ts | 35 +++-------------- src/routes/edit.svelte | 6 +-- 6 files changed, 78 insertions(+), 38 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/lib/util/serde.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..1295ee63 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSave": true +} diff --git a/cypress/integration/loadSite.spec.ts b/cypress/integration/loadSite.spec.ts index 6043f8e3..aa939cc4 100644 --- a/cypress/integration/loadSite.spec.ts +++ b/cypress/integration/loadSite.spec.ts @@ -103,15 +103,19 @@ describe('Site Loads', () => { '/edit#eyJjb2RlIjoiY2xhc3NEaWFncmFtXG4gICAgQW5pbWFsIDx8LS0gRHVja1xuICAgIEFuaW1hbCA8fC0tIEZpc2hcbiAgICBBbmltYWwgPHwtLSBaZWJyYVxuICAgIEFuaW1hbCA6ICtpbnQgYWdlXG4gICAgQW5pbWFsIDogK1N0cmluZyBnZW5kZXJcbiAgICBBbmltYWw6ICtpc01hbW1hbCgpXG4gICAgQW5pbWFsOiArbWF0ZSgpXG4gICAgY2xhc3MgRHVja3tcbiAgICAgICtTdHJpbmcgYmVha0NvbG9yXG4gICAgICArc3dpbSgpXG4gICAgICArcXVhY2soKVxuICAgIH1cbiAgICBjbGFzcyBGaXNoe1xuICAgICAgLWludCBzaXplSW5GZWV0XG4gICAgICAtY2FuRWF0KClcbiAgICB9XG4gICAgY2xhc3MgWmVicmF7XG4gICAgICArYm9vbCBpc193aWxkXG4gICAgICArcnVuKClcbiAgICB9XG4gICAgICAgICAgICAiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGFya1wiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ' ); cy.contains('Animal'); + cy.visit( + '/edit/#base64:eyJjb2RlIjoiZ3JhcGggVERcbiAgICBBW05ldyBZZWFyXSAtLT58R2V0IG1vbmV5fCBCKEdvIHNob3BwaW5nKVxuICAgIEIgLS0-IEN7TGV0IG1lIHRoaW5rfVxuICAgIEMgLS0-fE9uZXwgRFtMYXB0b3BdXG4gICAgQyAtLT58VHdvfCBFW2lQaG9uZV1cbiAgICBDIC0tPnxUaHJlZXwgRltmYTpmYS1jYXIgQ2FyXSIsIm1lcm1haWQiOiJ7XG4gIFwidGhlbWVcIjogXCJkZWZhdWx0XCJcbn0iLCJ1cGRhdGVFZGl0b3IiOmZhbHNlLCJhdXRvU3luYyI6dHJ1ZSwidXBkYXRlRGlhZ3JhbSI6ZmFsc2V9' + ); + cy.contains('New Year'); }); it('should load compressed URL', () => { cy.visit( - '/edit#eNpVkM2KwkAQhF-l6dMK5gVyEDRxvYi7sF6WjIcm0zqDzg_jBJEk725Hd2G3Tw31VVFUj23QjCWeEkUD-1p5kFs2O77BN1M6QFEshg1ncMHzfYDV2ybA1YQYrT_NXvhqgqDqtxPGkI315_ElVU__h-cB6mZLMYd4-Kvsb2GAdWM_jcT_V0xicb03RyqPVLSUoJI-OEfHyZHV0rqfDAqzYccKS3k1pbNC5Ufhuqgp81rbHBJKxuXKc6Quh6-7b7HMqeNfqLYkC7gfanwAlW1ZvQ' + '/edit#pako:eNpVkM2KwkAQhF-l6dMK5gVyEDRxvYi7sF6WjIcm0zqDzg_jBJEk725Hd2G3Tw31VVFUj23QjCWeEkUD-1p5kFs2O77BN1M6QFEshg1ncMHzfYDV2ybA1YQYrT_NXvhqgqDqtxPGkI315_ElVU__h-cB6mZLMYd4-Kvsb2GAdWM_jcT_V0xicb03RyqPVLSUoJI-OEfHyZHV0rqfDAqzYccKS3k1pbNC5Ufhuqgp81rbHBJKxuXKc6Quh6-7b7HMqeNfqLYkC7gfanwAlW1ZvQ' ); cy.contains('New Year'); cy.visit( - '/edit#eNptkU1PwzAMhv9K5BOI9Q9EXBDbJA477YYqITcxndV8QD40weh_Jy1rGR0-OY_tV2_sEyivCSQogzGuGduAtnaixINji0bcf1WVWGfVXdMtx8M1faYm4B8sxR27JLClJd6nwK4VLTlN4bI4jMQd2pLe3C4KFhNNcLQ92jv9ADGLNoTdozc-zIV4ZDsNlud7RtVN7_5Sb_jYrFcN3iN_0pPbEqUZK3QbTP_Ojyv4NdR4bwTHlyMbPcOQ3WJ2CliBpWCRdbnLqFJDOpClGmRJNYauhtr1pS-_6bKMjebkA8hXNJFWgDn5_YdTIFPINDWdb3vu6r8BaWOZRQ' + '/edit#pako:eNptkU1PwzAMhv9K5BOI9Q9EXBDbJA477YYqITcxndV8QD40weh_Jy1rGR0-OY_tV2_sEyivCSQogzGuGduAtnaixINji0bcf1WVWGfVXdMtx8M1faYm4B8sxR27JLClJd6nwK4VLTlN4bI4jMQd2pLe3C4KFhNNcLQ92jv9ADGLNoTdozc-zIV4ZDsNlud7RtVN7_5Sb_jYrFcN3iN_0pPbEqUZK3QbTP_Ojyv4NdR4bwTHlyMbPcOQ3WJ2CliBpWCRdbnLqFJDOpClGmRJNYauhtr1pS-_6bKMjebkA8hXNJFWgDn5_YdTIFPINDWdb3vu6r8BaWOZRQ' ); cy.reload(); cy.contains('Animal'); diff --git a/src/lib/components/actions.svelte b/src/lib/components/actions.svelte index 13d13ec4..4b527d85 100644 --- a/src/lib/components/actions.svelte +++ b/src/lib/components/actions.svelte @@ -3,7 +3,8 @@ import Card from '$lib/components/card/card.svelte'; import { krokiRendererUrl, rendererUrl } from '$lib/util/env'; - import { compressedState, codeStore, compressString } from '$lib/util/state'; + import { pakoSerde } from '$lib/util/serde'; + import { serializedState, codeStore } from '$lib/util/state'; import { toBase64 } from 'js-base64'; import moment from 'moment'; @@ -138,10 +139,10 @@ if (browser && ['mermaid.live', 'netlify'].some((path) => window.location.host.includes(path))) { isNetlify = true; } - compressedState.subscribe((encodedState: string) => { + serializedState.subscribe((encodedState: string) => { iUrl = `${rendererUrl}/img/${encodedState}`; svgUrl = `${rendererUrl}/svg/${encodedState}`; - krokiUrl = `${krokiRendererUrl}/mermaid/svg/${compressString($codeStore.code)}`; + krokiUrl = `${krokiRendererUrl}/mermaid/svg/${pakoSerde.serialize($codeStore.code)}`; mdCode = `[![](${iUrl})](${window.location.protocol}//${window.location.host}${window.location.pathname}#${encodedState})`; }); diff --git a/src/lib/util/serde.ts b/src/lib/util/serde.ts new file mode 100644 index 00000000..d70ea3c1 --- /dev/null +++ b/src/lib/util/serde.ts @@ -0,0 +1,57 @@ +import { deflate, inflate } from 'pako'; +import { toUint8Array, fromUint8Array, toBase64, fromBase64 } from 'js-base64'; +import type { State } from '$lib/types'; + +interface Serde { + serialize: (state: string) => string; + deserialize: (state: string) => string; +} + +enum SerdeType { + Base64 = 'base64', + Pako = 'pako' +} + +const base64Serde: Serde = { + serialize: (state: string): string => { + return toBase64(state, true); + }, + deserialize: (state: string): string => { + return fromBase64(state); + } +}; + +export const pakoSerde: Serde = { + serialize: (state: string): string => { + const data = new TextEncoder().encode(state); + const compressed = deflate(data, { level: 9 }); + return fromUint8Array(compressed, true); + }, + deserialize: (state: string): string => { + const data = toUint8Array(state); + return inflate(data, { to: 'string' }); + } +}; +const serdes: Record = { + [SerdeType.Base64]: base64Serde, + [SerdeType.Pako]: pakoSerde +}; + +export const serializeState = (state: State): string => { + const json = JSON.stringify(state); + const defaultSerde = SerdeType.Pako; + const serialized = serdes[defaultSerde].serialize(json); + return `${defaultSerde}:${serialized}`; +}; + +export const deserializeState = (state: string): State => { + let type: string, serialized: string; + if (state.includes(':')) { + [type, serialized] = state.split(':'); + } else { + type = SerdeType.Base64; + serialized = state; + } + const json = serdes[type].deserialize(serialized); + return JSON.parse(json); +}; diff --git a/src/lib/util/state.ts b/src/lib/util/state.ts index a124eb2e..fb190049 100644 --- a/src/lib/util/state.ts +++ b/src/lib/util/state.ts @@ -1,10 +1,9 @@ import { writable, get, derived } from 'svelte/store'; import type { Readable } from 'svelte/store'; -import { fromBase64, toUint8Array, fromUint8Array } from 'js-base64'; import { persist, localStorage } from '@macfja/svelte-persistent-store'; import type { State } from '$lib/types'; import { saveStatistics } from './stats'; -import pako from 'pako'; +import { serializeState, deserializeState } from './serde'; export const defaultState: State = { code: `graph TD @@ -37,40 +36,16 @@ const urlParseFailedState = `graph TD 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"`; -const textEncode = (str: string) => { - return new TextEncoder().encode(str); -}; - -export const compressString = (source: string): string => { - const data = textEncode(source); - const compressed = pako.deflate(data, { level: 9 }); - const result = fromUint8Array(compressed, true); - return result; -}; - -const decompressString = (source: string): string => { - const data = toUint8Array(source); - const decompressed = pako.inflate(data, { to: 'string' }); - return decompressed; -}; - export const codeStore = persist(writable(defaultState), localStorage(), 'codeStore'); -export const compressedState: Readable = derived([codeStore], ([code], set) => { - set(compressString(JSON.stringify(code))); +export const serializedState: Readable = derived([codeStore], ([code], set) => { + set(serializeState(code)); }); export const loadState = (data: string): void => { let state: State; console.log('Loading', data); try { - let stateStr: string; - if (data.startsWith('eyJ')) { - stateStr = fromBase64(data); - } else { - stateStr = decompressString(data); - } - console.log(`Trying to load state: ${stateStr}`); - state = JSON.parse(stateStr); + state = deserializeState(data); const mermaidConfig = typeof state.mermaid === 'string' ? JSON.parse(state.mermaid) : state.mermaid; if ( @@ -140,7 +115,7 @@ export const toggleDarkTheme = (dark: boolean): void => { }; export const initURLSubscription = (): void => { - compressedState.subscribe((state: string) => { + serializedState.subscribe((state: string) => { history.replaceState(undefined, undefined, `#${state}`); }); }; diff --git a/src/routes/edit.svelte b/src/routes/edit.svelte index 5e6e3f0e..aa21f82a 100644 --- a/src/routes/edit.svelte +++ b/src/routes/edit.svelte @@ -6,7 +6,7 @@ import View from '$lib/components/view.svelte'; import Card from '$lib/components/card/card.svelte'; import History from '$lib/components/history/history.svelte'; - import { updateCode, updateConfig, codeStore, compressedState } from '$lib/util/state'; + import { updateCode, updateConfig, codeStore, serializedState } from '$lib/util/state'; import { initHandler, syncDiagram } from '$lib/util/util'; import { errorStore } from '$lib/util/error'; import { onMount } from 'svelte'; @@ -15,7 +15,7 @@ import type { EditorUpdateEvent, State, Tab } from '$lib/types'; import { base } from '$app/paths'; - compressedState; // Weird fix for error > compressedState is not defined. Treeshaking? + serializedState; // Weird fix for error > serializedState is not defined. Treeshaking? let selectedMode = 'code'; const languageMap = { code: 'mermaid', @@ -96,7 +96,7 @@ }; const viewDiagram = () => { - window.open(`${base}/view#${$compressedState}`, '_blank').focus(); + window.open(`${base}/view#${$serializedState}`, '_blank').focus(); }; onMount(async () => {