diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index f2939706..2baf95f5 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -8,7 +8,15 @@ module.exports = {
'prettier'
],
plugins: ['svelte3', 'tailwindcss', '@typescript-eslint'],
- ignorePatterns: ['docs/*', '*.cjs', '*.md', 'snapshots.js', 'svelte.config.js', 'package.json'],
+ ignorePatterns: [
+ 'docs/*',
+ '*.cjs',
+ '*.md',
+ 'snapshots.js',
+ 'svelte.config.js',
+ 'package.json',
+ 'tsconfig.json'
+ ],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
diff --git a/cypress/integration/actions.spec.ts b/cypress/integration/actions.spec.ts
index 6a2a8268..bcef5906 100644
--- a/cypress/integration/actions.spec.ts
+++ b/cypress/integration/actions.spec.ts
@@ -17,4 +17,10 @@ describe('Check actions', () => {
});
});
});
+
+ it('should load gists from URL', () => {
+ cy.get('#gist').type('https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a');
+ cy.contains('Load Gist').click();
+ cy.contains('Go shopping!!');
+ });
});
diff --git a/cypress/integration/loadSite.spec.ts b/cypress/integration/loadSite.spec.ts
index 10b8efa8..82c39f91 100644
--- a/cypress/integration/loadSite.spec.ts
+++ b/cypress/integration/loadSite.spec.ts
@@ -32,6 +32,33 @@ describe('Site Loads', () => {
cy.contains('classDiagram');
});
+ it('should load diagram from gist', () => {
+ cy.visit(`/edit?gist=https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a`);
+ 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('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 prevent setting the "securityLevel" option via URL', () => {
const b64State = toBase64(
`{"code":"graph TD\\nA[\\"
\\"]","mermaid":"{\\"securityLevel\\": \\"loose\\", \\"theme\\": \\"forest\\"}","updateEditor":true,"autoSync":true,"updateDiagram":true}`,
diff --git a/cypress/snapshots.js b/cypress/snapshots.js
index 0162dd5c..dc023fad 100644
--- a/cypress/snapshots.js
+++ b/cypress/snapshots.js
@@ -4,7 +4,16 @@ module.exports = {
"1": "{\"code\":\"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"updateEditor\":true,\"autoSync\":true,\"updateDiagram\":true}"
},
"Check Redirect from old URL": {
- "1": "{\"code\":\"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"updateEditor\":false,\"autoSync\":true,\"updateDiagram\":false}"
+ "1": "{\"code\":\"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"updateEditor\":false,\"autoSync\":true,\"updateDiagram\":true}"
+ },
+ "should load diagram from gist": {
+ "1": "{\"code\":\"graph TD\\n A[Party] -->|Get money| B(Go shopping!!)\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"forest\\\",\\n \\\"test\\\": \\\"hello world\\\"\\n}\",\"updateEditor\":false,\"autoSync\":true,\"updateDiagram\":true,\"loader\":{\"type\":\"gist\",\"config\":{\"url\":\"https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a\"}}}"
+ },
+ "should load diagram from gist revision": {
+ "1": "{\"code\":\"graph TD\\n A[Party] -->|Get money| B(Go shopping)\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"forest\\\",\\n \\\"test\\\": \\\"hello\\\"\\n}\",\"updateEditor\":false,\"autoSync\":true,\"updateDiagram\":true,\"loader\":{\"type\":\"gist\",\"config\":{\"url\":\"https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/ec9b4ab0e41e4ff6287326cd3cb47affd7851e19\"}}}"
+ },
+ "should load diagram from raw files": {
+ "1": "{\"code\":\"graph TD\\n A[Party] -->|Get money| B(Go shopping!!)\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"forest\\\",\\n \\\"test\\\": \\\"hello world\\\"\\n}\",\"updateEditor\":false,\"autoSync\":true,\"updateDiagram\":true,\"loader\":{\"type\":\"files\",\"config\":{\"codeURL\":\"https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/code.mmd\",\"configURL\":\"https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/config.json\"}}}"
}
},
"__version": "8.2.0",
diff --git a/src/app.postcss b/src/app.postcss
index ce6711bb..edf491bf 100644
--- a/src/app.postcss
+++ b/src/app.postcss
@@ -12,3 +12,7 @@
.action-btn {
@apply rounded p-2 bg-indigo-400 shadow flex-auto text-white hover:bg-indigo-500;
}
+
+.input {
+ @apply flex-1 border-indigo-400 border-solid border-2 rounded;
+}
diff --git a/src/lib/components/actions.svelte b/src/lib/components/actions.svelte
index 9fd61467..110fd2b3 100644
--- a/src/lib/components/actions.svelte
+++ b/src/lib/components/actions.svelte
@@ -105,11 +105,26 @@
);
};
- const onCopyMarkdown = (event: Event) => {
- event.target.select();
+ const onCopyMarkdown = () => {
+ document.getElementById('markdown').select();
document.execCommand('Copy');
};
+ let gistURL = '';
+ codeStore.subscribe((state) => {
+ if (state.loader?.type === 'gist') {
+ // @ts-ignore Gist will have url
+ gistURL = state.loader.config.url;
+ }
+ });
+
+ const loadGist = () => {
+ if (!gistURL) {
+ alert('Please enter a Gist URL first');
+ }
+ window.location.href = `${window.location.pathname}?gist=${gistURL}`;
+ };
+
let iUrl: string;
let svgUrl: string;
let mdCode: string;
@@ -162,8 +177,22 @@
-
-
+
+
+
+
+
+
+
diff --git a/src/lib/components/card/tabs.svelte b/src/lib/components/card/tabs.svelte
index fc11b338..dc479b6f 100644
--- a/src/lib/components/card/tabs.svelte
+++ b/src/lib/components/card/tabs.svelte
@@ -7,7 +7,7 @@
export let title: string;
export let isOpen = false;
- let activeTabID: string = tabs[0]?.id;
+ $: activeTabID = tabs[0]?.id;
const dispatch = createEventDispatcher();
const toggleTabs = (tab: Tab) => {
diff --git a/src/lib/components/history/history.svelte b/src/lib/components/history/history.svelte
index 1010d40d..96350cfd 100644
--- a/src/lib/components/history/history.svelte
+++ b/src/lib/components/history/history.svelte
@@ -3,29 +3,30 @@
import { codeStore, getStateString } from '$lib/util/state';
import {
addHistoryEntry,
- autoHistoryMode,
+ historyModeStore,
clearHistoryData,
getPreviousState,
- historyStore
+ historyStore,
+ loaderHistoryStore
} from './history';
import { notify, prompt } from '$lib/util/notify';
import { onMount } from 'svelte';
import moment from 'moment';
- import type { State, Tab } from '$lib/types';
+ import type { HistoryType, State, Tab } from '$lib/types';
const HISTORY_SAVE_INTERVAL = 60000;
const tabSelectHandler = (message: CustomEvent) => {
- autoHistoryMode.set('timeline' === message.detail.id);
+ historyModeStore.set(message.detail.id as HistoryType);
};
- const tabs: Tab[] = [
+ let tabs: Tab[] = [
{
- id: 'saved',
+ id: 'manual',
title: 'Saved',
icon: 'far fa-bookmark'
},
{
- id: 'timeline',
+ id: 'auto',
title: 'Timeline',
icon: 'fas fa-history'
}
@@ -38,7 +39,7 @@
addHistoryEntry({
state: $codeStore,
time: Date.now(),
- auto
+ type: auto ? 'auto' : 'manual'
});
} else if (!auto) {
notify('State already saved.');
@@ -62,12 +63,26 @@
};
onMount(() => {
- autoHistoryMode.set(false);
+ historyModeStore.set('manual');
setInterval(() => {
saveHistory(true);
}, HISTORY_SAVE_INTERVAL);
});
+ loaderHistoryStore.subscribe((entries) => {
+ if (entries.length > 0 && tabs.length === 2) {
+ tabs = [
+ {
+ id: 'loader',
+ title: 'Revisions',
+ icon: 'fab fa-git-alt'
+ },
+ ...tabs
+ ];
+ historyModeStore.set('loader');
+ }
+ });
+
let isOpen = true;
@@ -78,20 +93,30 @@
class="btn"
on:click|stopPropagation={() => saveHistory()}
title="Save current state">
-
+ {#if $historyModeStore !== 'loader'}
+
+ {/if}
{#if $historyStore.length > 0}
- {#each $historyStore as { state, time, name }}
+ {#each $historyStore as { state, time, name, url, type }}
-
-
{name}
+ {#if url}
+
{name}
+ {:else}
+
{name}
+ {/if}
{relativeTime(time)}
@@ -99,9 +124,11 @@
-
+ {#if type !== 'loader'}
+
+ {/if}
diff --git a/src/lib/components/history/history.ts b/src/lib/components/history/history.ts
index 4b7677a6..8dc0809b 100644
--- a/src/lib/components/history/history.ts
+++ b/src/lib/components/history/history.ts
@@ -1,12 +1,12 @@
import { derived, Readable, Writable, writable, get } from 'svelte/store';
import { persist, localStorage } from '@macfja/svelte-persistent-store';
import { generateSlug } from 'random-word-slugs';
-import type { HistoryEntry } from '../../types';
+import type { HistoryEntry, HistoryType } from '$lib/types';
const MAX_AUTO_HISTORY_LENGTH = 30;
-export const autoHistoryMode: Writable = persist(
- writable(true),
+export const historyModeStore: Writable = persist(
+ writable('manual'),
localStorage(),
'autoHistoryMode'
);
@@ -23,17 +23,30 @@ const manualHistoryStore: Writable = persist(
'manualHistoryStore'
);
+export const loaderHistoryStore: Writable = writable([] as HistoryEntry[]);
+
export const historyStore: Readable = derived(
- [autoHistoryMode, autoHistoryStore, manualHistoryStore],
- ([autoMode, autoHistories, manualHistories], set) => {
- set(autoMode ? autoHistories : manualHistories);
+ [historyModeStore, autoHistoryStore, manualHistoryStore, loaderHistoryStore],
+ ([historyMode, autoHistories, manualHistories, loadedHistories], set) => {
+ if (historyMode === 'auto') {
+ set(autoHistories);
+ } else if (historyMode === 'manual') {
+ set(manualHistories);
+ } else if (historyMode === 'loader') {
+ set(loadedHistories);
+ } else {
+ set(autoHistories);
+ }
}
);
export const addHistoryEntry = (entry: HistoryEntry): void => {
+ if (entry.type === 'loader') {
+ loaderHistoryStore.update((entries) => [entry, ...entries]);
+ return;
+ }
entry.name = generateSlug(2);
-
- if (!entry.auto) {
+ if (entry.type !== 'auto') {
manualHistoryStore.update((entries) => [entry, ...entries]);
return;
}
@@ -46,9 +59,12 @@ export const addHistoryEntry = (entry: HistoryEntry): void => {
};
export const clearHistoryData = (time?: number): void => {
- (get(autoHistoryMode) ? autoHistoryStore : manualHistoryStore).update((entries) =>
- entries.filter((entry) => time && entry.time != time)
- );
+ (get(historyModeStore) === 'auto' ? autoHistoryStore : manualHistoryStore).update((entries) => {
+ if (get(historyModeStore) !== 'loader') {
+ entries = entries.filter((entry) => time && entry.time != time);
+ }
+ return entries;
+ });
};
export const getPreviousState = (auto: boolean): string => {
diff --git a/src/lib/types.d.ts b/src/lib/types.d.ts
index 3f121d93..e0a139b3 100644
--- a/src/lib/types.d.ts
+++ b/src/lib/types.d.ts
@@ -29,11 +29,32 @@ export interface State {
updateEditor: boolean;
updateDiagram: boolean;
autoSync: boolean;
+ loader?: LoaderConfig;
}
+export interface GistLoaderConfig {
+ url: string;
+}
+
+export interface LoadingState {
+ loading: boolean;
+ message?: string;
+}
+export interface FileLoaderConfig {
+ codeURL: string;
+ configURL?: string;
+}
+export interface LoaderConfig {
+ type: 'gist' | 'files';
+ config: GistLoaderConfig | FileLoaderConfig;
+}
+export type HistoryType = 'auto' | 'manual' | 'loader';
export interface HistoryEntry {
state: State;
time: number;
name?: string;
- auto: boolean;
+ type: HistoryType;
+ url?: string;
}
+
+type Loader = (url: string) => Promise;
diff --git a/src/lib/util/fileLoaders/gist.ts b/src/lib/util/fileLoaders/gist.ts
new file mode 100644
index 00000000..cd9a2a75
--- /dev/null
+++ b/src/lib/util/fileLoaders/gist.ts
@@ -0,0 +1,105 @@
+/* eslint-disable @typescript-eslint/no-unsafe-assignment */
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import type { State } from '$lib/types';
+import { defaultState } from '../state';
+import { addHistoryEntry } from '../../components/history/history';
+
+const codeFileName = 'code.mmd';
+const configFileName = 'config.json';
+
+const isValidGist = (files: any): boolean => {
+ return codeFileName in files;
+};
+
+const getFileContent = async (file: any): Promise => {
+ if (file.truncated) {
+ return await (await fetch(file.raw_url)).text();
+ }
+ return file.content;
+};
+
+interface GistData {
+ code: string;
+ config?: string;
+ author: string;
+ time: number;
+ version: string;
+ url: string;
+}
+
+const getGistData = async (gistURL: string): Promise => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const [_, __, gistID, revisionID] = gistURL.split('github.com').pop().split('/');
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const { html_url, files, history } = await (
+ await fetch(`https://api.github.com/gists/${gistID}${revisionID ? '/' + revisionID : ''}`)
+ ).json();
+ if (isValidGist(files)) {
+ const code = await getFileContent(files[codeFileName]);
+ let config: string;
+ if (configFileName in files) {
+ config = await getFileContent(files[configFileName]);
+ }
+ const currentItem = history[0];
+ return {
+ url: `${html_url}/${currentItem.version}`,
+ code,
+ config,
+ author: currentItem.user.login,
+ time: new Date(currentItem.committed_at).getTime(),
+ version: (currentItem.version as string).slice(-7)
+ };
+ } else {
+ throw 'Invalid gist provided';
+ }
+};
+
+const getStateFromGist = (gist: GistData, gistURL: string = gist.url): State => {
+ const state: State = {
+ ...defaultState,
+ code: gist.code,
+ loader: {
+ type: 'gist',
+ config: {
+ url: gistURL
+ }
+ }
+ };
+ gist.config && (state.mermaid = gist.config);
+ return state;
+};
+
+export const loadGistData = async (gistURL: string): Promise => {
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const [_, __, gistID, revisionID] = gistURL.split('github.com').pop().split('/');
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const { history } = await (
+ await fetch(`https://api.github.com/gists/${gistID}${revisionID ? '/' + revisionID : ''}`)
+ ).json();
+ const gistHistory: GistData[] = [];
+ for (const entry of history) {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const data: GistData = await getGistData(entry.url).catch(() => undefined);
+ data && gistHistory.push(data);
+ }
+ if (gistHistory.length === 0) {
+ throw 'Invalid gist provided';
+ }
+ gistHistory.reverse();
+ const state = getStateFromGist(gistHistory.slice(-1).pop(), gistURL);
+ for (const gist of gistHistory) {
+ addHistoryEntry({
+ state: getStateFromGist(gist),
+ time: gist.time,
+ type: 'loader',
+ url: gist.url,
+ name: `${gist.author} v${gist.version}`
+ });
+ }
+ return state;
+ } catch (err) {
+ console.error(err);
+ }
+};
diff --git a/src/lib/util/fileLoaders/loader.ts b/src/lib/util/fileLoaders/loader.ts
new file mode 100644
index 00000000..02b73aa2
--- /dev/null
+++ b/src/lib/util/fileLoaders/loader.ts
@@ -0,0 +1,58 @@
+import { loadGistData } from './gist';
+import { updateCodeStore, defaultState } from '../state';
+import type { Loader, State } from '$lib/types';
+const loaders: Record = {
+ gist: loadGistData
+};
+
+export const loadDataFromUrl = async (): Promise => {
+ const searchParams = new URLSearchParams(window.location.search);
+ let state: State = defaultState;
+ let code: string, config: string;
+ let loaded = false;
+ const codeURL: string = searchParams.get('code');
+ const configURL: string = searchParams.get('config');
+
+ if (codeURL) {
+ code = await (await fetch(codeURL)).text();
+ loaded = true;
+ }
+ if (configURL) {
+ config = await (await fetch(configURL)).text();
+ } else {
+ config = defaultState.mermaid;
+ }
+ if (!code) {
+ for (const [key, value] of searchParams.entries()) {
+ if (key in loaders) {
+ try {
+ state = await loaders[key](value);
+ loaded = true;
+ break;
+ } catch (err) {
+ console.error(err);
+ }
+ }
+ }
+ } else {
+ state = {
+ code,
+ mermaid: config,
+ loader: {
+ type: 'files',
+ config: {
+ codeURL,
+ configURL
+ }
+ }
+ } as State;
+ }
+ loaded &&
+ updateCodeStore({
+ ...state,
+ autoSync: true,
+ updateDiagram: true,
+ updateEditor: true
+ });
+ // window.location.search = '';
+};
diff --git a/src/lib/util/loading.ts b/src/lib/util/loading.ts
new file mode 100644
index 00000000..84205ebf
--- /dev/null
+++ b/src/lib/util/loading.ts
@@ -0,0 +1,19 @@
+import { writable, Writable } from 'svelte/store';
+import type { LoadingState } from '$lib/types';
+
+const defaultLoading: LoadingState = {
+ loading: false
+};
+
+export const loadingStateStore: Writable = writable(defaultLoading);
+export const initLoading = async (message: string, task: Promise): Promise => {
+ loadingStateStore.set({
+ loading: true,
+ message
+ });
+ const result: T = await task;
+ loadingStateStore.set({
+ loading: false
+ });
+ return result;
+};
diff --git a/src/lib/util/state.ts b/src/lib/util/state.ts
index 3669b6ac..643ea9d8 100644
--- a/src/lib/util/state.ts
+++ b/src/lib/util/state.ts
@@ -4,7 +4,7 @@ import { persist, localStorage } from '@macfja/svelte-persistent-store';
import type { State } from '$lib/types';
import { saveStatistcs } from './stats';
-const defaultState: State = {
+export const defaultState: State = {
code: `graph TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
diff --git a/src/lib/util/util.ts b/src/lib/util/util.ts
index 482dab1c..84e02992 100644
--- a/src/lib/util/util.ts
+++ b/src/lib/util/util.ts
@@ -1,6 +1,8 @@
import type { State } from '$lib/types';
import { initURLSubscription, loadState, updateCodeStore } from './state';
import { analytics, initAnalytics } from './stats';
+import { loadDataFromUrl } from './fileLoaders/loader';
+import { initLoading } from './loading';
export const loadStateFromURL = (): void => {
loadState(window.location.hash.slice(1));
@@ -14,6 +16,7 @@ export const syncDiagram = (): void => {
export const initHandler = async (): Promise => {
loadStateFromURL();
+ await initLoading('Loading Gist...', loadDataFromUrl().catch(console.error));
syncDiagram();
initURLSubscription();
await initAnalytics();
diff --git a/src/routes/__layout.svelte b/src/routes/__layout.svelte
index f17b2f6e..92cf8996 100644
--- a/src/routes/__layout.svelte
+++ b/src/routes/__layout.svelte
@@ -2,6 +2,7 @@
import '../app.postcss';
import { base } from '$app/paths';
import { onMount } from 'svelte';
+ import { loadingStateStore } from '$lib/util/loading';
// This can be removed once https://github.com/sveltejs/kit/issues/1612 is fixed.
// Then move it into src and vite will bundle it automatically.
onMount(() => {
@@ -23,3 +24,44 @@
+
+{#if $loadingStateStore.loading}
+
+
+
+
{$loadingStateStore.message}
+
+
+{/if}
+
+
diff --git a/tsconfig.json b/tsconfig.json
index 975f193c..e82fd297 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,7 +2,7 @@
"compilerOptions": {
"moduleResolution": "node",
"module": "es2020",
- "lib": ["es2020", "ESNext"],
+ "lib": ["es2020", "ESNext", "DOM.Iterable"],
"target": "es2019",
/**
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript