Merge pull request #1090 from mermaid-js/sidv/capitalizeSvelteComponents

chore: Capitalize components to be inline with svelte standards
This commit is contained in:
Sidharth Vinod
2022-11-15 20:15:21 +05:30
committed by GitHub
34 changed files with 284 additions and 187 deletions
+14 -1
View File
@@ -5,6 +5,7 @@ module.exports = {
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
// 'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/strict',
'prettier'
],
plugins: ['svelte3', 'tailwindcss', '@typescript-eslint', 'es', 'vitest'],
@@ -19,7 +20,19 @@ module.exports = {
'package.json',
'tsconfig.json'
],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
overrides: [
{ files: ['*.svelte'], processor: 'svelte3/svelte3' },
{
files: ['*.ts'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/strict',
'prettier'
]
}
],
settings: {
'svelte3/typescript': () => require('typescript')
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"editor.formatOnSave": true,
"cSpell.words": ["asyncable", "mindmap", "pako", "Serde", "serdes"],
"cSpell.words": ["asyncable", "KROKI", "mindmap", "pako", "Serde", "serdes", "tailwindcss"],
"vitest.commandLine": "yarn test:unit",
"vitest.enable": true,
"testing.autoRun.mode": "rerun",
+2
View File
@@ -17,6 +17,7 @@ export const verifyFileSize = (
) => {
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
@@ -31,6 +32,7 @@ export const verifyFileSnapshot = (
) => {
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
+5 -5
View File
@@ -1,11 +1,11 @@
/// <reference types="vite/client" />
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;
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...
}
+2 -1
View File
@@ -9,7 +9,8 @@ declare global {
namespace jest {
interface Matchers<R = void>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
// eslint-disable-next-line no-undef
extends TestingLibraryMatchers<typeof expect.stringContaining, R> {}
}
}
@@ -1,6 +1,6 @@
<script lang="ts">
import { browser } from '$app/environment';
import Card from '$lib/components/card/card.svelte';
import Card from '$lib/components/Card/Card.svelte';
import { env } from '$lib/util/env';
import { pakoSerde } from '$lib/util/serde';
import { stateStore } from '$lib/util/state';
@@ -15,8 +15,8 @@
`mermaid-diagram-${moment().format('YYYY-MM-DD-HHmmss')}.${ext}`;
const getBase64SVG = (svg?: HTMLElement, width?: number, height?: number): string => {
svg?.setAttribute('height', `${height}px`);
svg?.setAttribute('width', `${width}px`); // Workaround https://stackoverflow.com/questions/28690643/firefox-error-rendering-an-svg-image-to-html5-canvas-with-drawimage
height && svg?.setAttribute('height', `${height}px`);
width && svg?.setAttribute('width', `${width}px`); // Workaround https://stackoverflow.com/questions/28690643/firefox-error-rendering-an-svg-image-to-html5-canvas-with-drawimage
if (!svg) {
svg = getSvgEl();
}
@@ -28,7 +28,10 @@
const exportImage = (event: Event, exporter: Exporter) => {
const canvas: HTMLCanvasElement = document.createElement('canvas');
const svg: HTMLElement = document.querySelector('#container svg');
const svg: HTMLElement | null = document.querySelector('#container svg');
if (!svg) {
throw new Error('svg not found');
}
const box: DOMRect = svg.getBoundingClientRect();
canvas.width = box.width;
canvas.height = box.height;
@@ -43,6 +46,9 @@
}
const context = canvas.getContext('2d');
if (!context) {
throw new Error('context not found');
}
context.fillStyle = 'white';
context.fillRect(0, 0, canvas.width, canvas.height);
@@ -56,12 +62,12 @@
const getSvgEl = () => {
const svgEl: HTMLElement = document
.querySelector('#container svg')
.querySelector('#container svg')!
.cloneNode(true) as HTMLElement;
svgEl.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
const fontAwesomeCdnUrl = Array.from(document.head.getElementsByTagName('link'))
.map((l) => l.href)
.find((h) => h && h.includes('font-awesome'));
.find((h) => h.includes('font-awesome'));
if (fontAwesomeCdnUrl == null) {
return svgEl;
}
@@ -99,10 +105,10 @@
context.drawImage(image, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => {
try {
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1004/files
if (!blob) {
throw new Error('blob is empty');
}
void navigator.clipboard.write([
/* eslint-disable no-undef */
// @ts-ignore: https://github.com/microsoft/TypeScript/issues/43821
new ClipboardItem({
[blob.type]: blob
})
@@ -142,7 +148,7 @@
let gistURL = '';
stateStore.subscribe(({ loader }) => {
if (loader?.type === 'gist') {
// @ts-ignore Gist will have url
// @ts-expect-error Gist will have url
gistURL = loader.config.url;
}
});
@@ -166,8 +172,7 @@
if (browser && ['mermaid.live', 'netlify'].some((path) => window.location.host.includes(path))) {
isNetlify = true;
}
stateStore.subscribe(async (state) => {
const { code, serialized } = await state;
stateStore.subscribe(({ code, serialized }) => {
iUrl = `${rendererUrl}/img/${serialized}?type=png`;
svgUrl = `${rendererUrl}/svg/${serialized}`;
krokiUrl = `${krokiRendererUrl}/mermaid/svg/${pakoSerde.serialize(code)}`;
@@ -1,11 +1,11 @@
<script lang="ts">
import type { Tab } from '$lib/types';
import { slide } from 'svelte/transition';
import Tabs from './tabs.svelte';
import Tabs from './Tabs.svelte';
export let isCloseable = true;
export let isOpen = true;
export let tabs: Tab[] = [];
export let activeTabID: string = '';
export let activeTabID = '';
export let title: string;
$: isOpen = isCloseable ? isOpen : true;
$: isTabsShown = isOpen && tabs.length > 0;
@@ -0,0 +1,3 @@
// Vitest Snapshot v1
exports[`card.svelte > mounts 1`] = `"<div><div class=\\"card rounded overflow-hidden m-2 flex-grow flex flex-col shadow-2xl\\"><div class=\\"bg-primary p-2 pb-0 flex-none cursor-pointer\\"><div class=\\"flex justify-between\\"><div class=\\"flex cursor-default s-lTROw8sJayZ1\\"><span class=\\"mr-2 font-semibold s-lTROw8sJayZ1\\"><i class=\\"fas fa-chevron-right icon s-lTROw8sJayZ1 isOpen\\"></i> TabTest</span> <ul class=\\"tabs s-lTROw8sJayZ1\\"><div class=\\"tab tab-lifted tab-active s-lTROw8sJayZ1\\"><i class=\\"mr-1 fab fa-git-alt s-lTROw8sJayZ1\\"></i> title1 </div><div class=\\"tab tab-lifted text-primary-content s-lTROw8sJayZ1\\"><i class=\\"mr-1 far fa-bookmark s-lTROw8sJayZ1\\"></i> title2 </div></ul></div><!--<Tabs>--> <div class=\\"flex gap-x-4 items-center -mt-2\\"></div></div></div> <div class=\\"card-body p-0 flex-grow overflow-auto text-base-content\\"></div></div><!--<Card>--></div>"`;
@@ -1,6 +1,6 @@
import { cleanup, render } from '@testing-library/svelte';
import { describe, expect, it, afterEach } from 'vitest';
import Card from './card.svelte';
import Card from './Card.svelte';
describe('card.svelte', () => {
// TODO: @testing-library/svelte claims to add this automatically but it doesn't work without explicit afterEach
@@ -10,8 +10,8 @@ describe('card.svelte', () => {
const { container } = render(Card, {
title: 'TabTest',
tabs: [
{ id: 't1', title: 'title1' },
{ id: 't2', title: 'title2' }
{ id: 't1', title: 'title1', icon: 'fab fa-git-alt' },
{ id: 't2', title: 'title2', icon: 'far fa-bookmark' }
]
});
expect(container).toBeTruthy();
@@ -8,9 +8,9 @@
import initEditor from 'monaco-mermaid';
import { logEvent } from '$lib/util/stats';
let divEl: HTMLDivElement = null;
let editor: monaco.editor.IStandaloneCodeEditor;
let Monaco: typeof monaco;
let divEl: HTMLDivElement | undefined = undefined;
let editor: monaco.editor.IStandaloneCodeEditor | undefined;
let Monaco: typeof monaco | undefined;
let editorOptions: monaco.editor.IStandaloneEditorConstructionOptions = {
minimap: {
enabled: false
@@ -22,7 +22,9 @@
stateStore.subscribe(({ errorMarkers, editorMode, code, mermaid }) => {
console.log('editor store subscription', { code, mermaid });
if (!editor) return;
if (!editor || !Monaco) {
return;
}
// Update editor text if it's different
const newText = editorMode === 'code' ? code : mermaid;
@@ -34,12 +36,17 @@
// Update editor mode if it's different
const language = editorMode === 'code' ? 'mermaid' : 'json';
if (editor.getModel().getLanguageId() !== language) {
Monaco?.editor.setModelLanguage(editor.getModel(), language);
const model = editor.getModel();
if (!model) {
console.error("editor model doesn't exist");
return;
}
if (model.getLanguageId() !== language) {
Monaco.editor.setModelLanguage(model, language);
}
// Display/clear errors
Monaco?.editor.setModelMarkers(editor.getModel(), 'mermaid', errorMarkers);
Monaco.editor.setModelMarkers(model, 'mermaid', errorMarkers);
});
themeStore.subscribe(({ isDark }) => {
@@ -60,7 +67,7 @@
// errorDebug();
let i = 0;
while (i++ < 500) {
// @ts-ignore : This is a hack to handle a svelte-kit error when importing monaco.
// @ts-expect-error : This is a hack to handle a svelte-kit error when importing monaco.
Monaco = window.monaco;
if (Monaco !== undefined) {
return;
@@ -72,13 +79,20 @@
onMount(async () => {
await loadMonaco(); // Fix https://github.com/mermaid-js/mermaid-live-editor/issues/175
if (!Monaco) {
throw new Error('Monaco failed to load');
}
if (!divEl) {
throw new Error('divEl is undefined');
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
initEditor(Monaco);
errorDebug(100);
editor = Monaco.editor.create(divEl, editorOptions);
editor.onDidChangeModelContent(({ isFlush, changes }) => {
const newText = editor.getValue();
const newText = editor?.getValue();
console.log('editor onDidChangeModelContent', { text, newText, isFlush, changes });
if (text === newText || isFlush) {
if (!newText || text === newText || isFlush) {
return;
}
text = newText;
@@ -95,19 +109,21 @@
});
}
});
Monaco?.editor.setTheme($themeStore.isDark ? 'mermaid-dark' : 'mermaid');
Monaco.editor.setTheme($themeStore.isDark ? 'mermaid-dark' : 'mermaid');
const resizeObserver = new ResizeObserver((entries) => {
editor.layout({
editor!.layout({
height: entries[0].contentRect.height,
width: entries[0].contentRect.width
});
});
resizeObserver.observe(divEl.parentElement);
if (divEl.parentElement) {
resizeObserver.observe(divEl.parentElement);
}
console.log(`editor mounted`);
return () => {
console.log(`editor disposed`);
editor.dispose();
editor?.dispose();
};
});
</script>
@@ -1,5 +1,5 @@
<script lang="ts">
import Card from '$lib/components/card/card.svelte';
import Card from '$lib/components/Card/Card.svelte';
import { inputStateStore, getStateString } from '$lib/util/state';
import {
addHistoryEntry,
@@ -14,7 +14,7 @@
import { onMount } from 'svelte';
import { get } from 'svelte/store';
import moment from 'moment';
import type { HistoryType, State, Tab } from '$lib/types';
import type { HistoryEntry, HistoryType, State, Tab } from '$lib/types';
import { logEvent } from '$lib/util/stats';
const HISTORY_SAVE_INTERVAL = 60000;
@@ -60,7 +60,7 @@
}
const reader = new FileReader();
reader.onload = (e) => {
const data = JSON.parse(e.target.result as string);
const data: HistoryEntry[] = JSON.parse(e.target.result as string);
restoreHistory(data);
};
reader.readAsText(file);
@@ -7,7 +7,7 @@ import {
historyModeStore,
historyStore
} from './history';
import { defaultState } from '../../util/state';
import { defaultState } from '$lib/util/state';
import { get } from 'svelte/store';
describe('history', () => {
@@ -21,9 +21,9 @@ describe('history', () => {
type: 'manual'
});
const [manualEntry]: HistoryEntry[] = JSON.parse(
window.localStorage.getItem('manualHistoryStore')
);
const [manualEntry] = JSON.parse(
window.localStorage.getItem('manualHistoryStore') ?? '[]'
) as HistoryEntry[];
expect(manualEntry.time).toBe(12345);
expect(manualEntry.type).toBe('manual');
@@ -36,7 +36,9 @@ describe('history', () => {
type: 'auto'
});
const [autoEntry]: HistoryEntry[] = JSON.parse(window.localStorage.getItem('autoHistoryStore'));
const [autoEntry] = JSON.parse(
window.localStorage.getItem('autoHistoryStore') ?? '[]'
) as HistoryEntry[];
expect(autoEntry.time).toBe(54321);
expect(autoEntry.type).toBe('auto');
@@ -101,19 +103,23 @@ describe('history migration', () => {
'autoHistoryStore',
'[{"state":{"code":"graph TD\\n A[New Year] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":false},"time":0,"type":"auto","name":"barking-dog"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":true},"time":0,"type":"manual","name":"needy-mosquito"}]'
);
let manualHistoryStore: HistoryEntry[] = JSON.parse(
window.localStorage.getItem('manualHistoryStore')
);
let autoHistoryStore: HistoryEntry[] = JSON.parse(
window.localStorage.getItem('autoHistoryStore')
);
let manualHistoryStore = JSON.parse(
window.localStorage.getItem('manualHistoryStore') ?? '[]'
) as HistoryEntry[];
let autoHistoryStore = JSON.parse(
window.localStorage.getItem('autoHistoryStore') ?? '[]'
) as HistoryEntry[];
expect(manualHistoryStore.every(({ id }) => id !== undefined)).toBe(false);
expect(autoHistoryStore.every(({ id }) => id !== undefined)).toBe(false);
injectHistoryIDs();
manualHistoryStore = JSON.parse(window.localStorage.getItem('manualHistoryStore'));
autoHistoryStore = JSON.parse(window.localStorage.getItem('autoHistoryStore'));
manualHistoryStore = JSON.parse(
window.localStorage.getItem('manualHistoryStore') ?? '[]'
) as HistoryEntry[];
autoHistoryStore = JSON.parse(
window.localStorage.getItem('autoHistoryStore') ?? '[]'
) as HistoryEntry[];
expect(manualHistoryStore.every(({ id }) => id !== undefined)).toBe(true);
expect(autoHistoryStore.every(({ id }) => id !== undefined)).toBe(true);
});
@@ -1,13 +1,13 @@
<script context="module">
<script context="module" lang="ts">
import { version } from 'mermaid/package.json';
import { analytics } from '$lib/util/stats';
analytics?.track('version', {
void analytics?.track('version', {
mermaidVersion: version
});
</script>
<script lang="ts">
import Theme from './theme.svelte';
import Theme from './Theme.svelte';
interface Link {
title: string;
@@ -1,6 +1,6 @@
<script lang="ts">
import { updateCode } from '$lib/util/state';
import Card from '$lib/components/card/card.svelte';
import Card from '$lib/components/Card/Card.svelte';
import { logEvent } from '$lib/util/stats';
const samples = {
@@ -104,7 +104,8 @@
Mermaid`
};
const loadSampleDiagram = (diagramType: string): void => {
type SampleTypes = keyof typeof samples;
const loadSampleDiagram = (diagramType: SampleTypes): void => {
updateCode(samples[diagramType], {
updateDiagram: true,
resetPanZoom: true
@@ -113,8 +114,8 @@
};
// Adding in this array will add an icon to the preset menu
const newDiagrams: Array<keyof typeof samples> = ['Mindmap'];
const diagramOrder: Array<keyof typeof samples> = [
const newDiagrams: SampleTypes[] = ['Mindmap'];
const diagramOrder: SampleTypes[] = [
'Sequence',
'Flow',
'Class',
@@ -1,4 +1,4 @@
<script>
<script lang="ts">
import { setTheme, themeStore } from '$lib/util/theme';
const themes = [
@@ -52,7 +52,7 @@
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<ul tabindex="0" class="p-4 menu compact">
{#each themes as theme}
<li class={theme.includes($themeStore.theme) ? 'bordered' : ''}>
<li class:bordered={$themeStore.theme !== undefined && theme.includes($themeStore.theme)}>
<span
class="btn btn-ghost justify-start"
on:click={() => setTheme(theme)}
@@ -6,6 +6,7 @@
import { logEvent } from '$lib/util/stats';
import { AsyncQueue, cmdKey } from '$lib/util/util';
import { render as renderDiagram } from '$lib/util/mermaid';
import type { MermaidConfig } from 'mermaid';
let code = '';
let config = '';
@@ -16,9 +17,12 @@
let hide = false;
let manualUpdate = true;
let panZoomEnabled = $stateStore.panZoom;
let pzoom: SvgPanZoom.Instance;
let pzoom: typeof panzoom | undefined;
const handlePanZoomChange = () => {
if (!pzoom) {
return;
}
const pan = pzoom.getPan();
const zoom = pzoom.getZoom();
updateCodeStore({ pan, zoom });
@@ -32,8 +36,11 @@
hide = true;
pzoom?.destroy();
pzoom = undefined;
Promise.resolve().then(() => {
void Promise.resolve().then(() => {
const graphDiv = document.getElementById('graph-div');
if (!graphDiv) {
return;
}
pzoom = panzoom(graphDiv, {
onPan: handlePanZoomChange,
onZoom: handlePanZoomChange,
@@ -70,10 +77,10 @@
code = state.code;
config = state.mermaid;
panZoomEnabled = state.panZoom;
const scroll = view.parentElement.scrollTop;
const scroll = view.parentElement!.scrollTop;
delete container.dataset.processed;
await renderDiagram(
Object.assign({}, JSON.parse(state.mermaid)),
Object.assign({}, JSON.parse(state.mermaid)) as MermaidConfig,
code,
'graph-div',
(svgCode, bindFunctions) => {
@@ -82,6 +89,9 @@
container.innerHTML = svgCode;
// console.log(container.innerHTML);
const graphDiv = document.getElementById('graph-div');
if (!graphDiv) {
throw new Error('graph-div not found');
}
graphDiv.setAttribute('height', '100%');
graphDiv.style.maxWidth = '100%';
if (bindFunctions) {
@@ -90,7 +100,7 @@
}
}
);
view.parentElement.scrollTop = scroll;
view.parentElement!.scrollTop = scroll;
error = false;
} else if (manualUpdate) {
manualUpdate = false;
@@ -106,8 +116,8 @@
const q = new AsyncQueue(handleStateChange);
onMount(() => {
stateStore.subscribe(async (state) => {
await q.process(state);
stateStore.subscribe((state) => {
void q.process(state);
});
window.addEventListener('resize', () => {
if ($stateStore.panZoom && pzoom) {
@@ -1,3 +0,0 @@
// Vitest Snapshot v1
exports[`card.svelte > mounts 1`] = `"<div><div class=\\"card rounded overflow-hidden m-2 flex-grow flex flex-col shadow-2xl\\"><div class=\\"bg-primary p-2 pb-0 flex-none cursor-pointer\\"><div class=\\"flex justify-between\\"><div class=\\"flex cursor-default s-_wx1E_JHsCoF\\"><span class=\\"mr-2 font-semibold s-_wx1E_JHsCoF\\"><i class=\\"fas fa-chevron-right icon s-_wx1E_JHsCoF isOpen\\"></i> TabTest</span> <ul class=\\"tabs s-_wx1E_JHsCoF\\"><div class=\\"tab tab-lifted tab-active s-_wx1E_JHsCoF\\"><i class=\\"mr-1 undefined s-_wx1E_JHsCoF\\"></i> title1 </div><div class=\\"tab tab-lifted text-primary-content s-_wx1E_JHsCoF\\"><i class=\\"mr-1 undefined s-_wx1E_JHsCoF\\"></i> title2 </div></ul></div><!--<Tabs>--> <div class=\\"flex gap-x-4 items-center -mt-2\\"></div></div></div> <div class=\\"card-body p-0 flex-grow overflow-auto text-base-content\\"></div></div><!--<Card>--></div>"`;
+14 -4
View File
@@ -73,14 +73,24 @@ export type HistoryEntry = { id: string; state: State; time: number; url?: strin
}
);
export interface DocConfig {
[key: string]: {
export type DocConfig = Record<
string,
{
code: string;
config?: string;
};
}
}
>;
export type EditorMode = 'code' | 'config';
export type Loader = (url: string) => Promise<State>;
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
export interface ErrorHash {
loc: {
first_line: number;
last_line: number;
first_column: number;
last_column: number;
};
}
+57 -38
View File
@@ -3,17 +3,23 @@
/* 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';
import { defaultState } from '$lib/util/state';
import { addHistoryEntry } from '$lib/components/History/history';
const codeFileName = 'code.mmd';
const configFileName = 'config.json';
interface GithubFile {
truncated: boolean;
raw_url: string;
content: string;
}
const isValidGist = (files: any): boolean => {
return codeFileName in files;
};
const getFileContent = async (file: any): Promise<string> => {
const getFileContent = async (file: GithubFile): Promise<string> => {
if (file.truncated) {
return await (await fetch(file.raw_url)).text();
}
@@ -29,16 +35,26 @@ interface GistData {
url: string;
}
interface GistResponse {
files: Record<string, GithubFile>;
html_url: string;
history: { url: string; committed_at: string; version: string; user: { login: string } }[];
}
const getGistData = async (gistURL: string): Promise<GistData> => {
const path = gistURL.split('github.com').pop();
if (!path) {
throw new Error('Invalid GitHub URL' + gistURL);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, __, gistID, revisionID] = gistURL.split('github.com').pop().split('/');
const [_, __, gistID, revisionID] = path.split('/');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { html_url, files, history } = await (
const { html_url, files, history }: GistResponse = await (
await fetch(`https://api.github.com/gists/${gistID}${revisionID ? '/' + revisionID : ''}`)
).json();
if (isValidGist(files)) {
const code = await getFileContent(files[codeFileName]);
let config: string;
let config = '{}';
if (configFileName in files) {
config = await getFileContent(files[configFileName]);
}
@@ -50,10 +66,10 @@ const getGistData = async (gistURL: string): Promise<GistData> => {
config,
author: currentItem.user.login,
time: new Date(currentItem.committed_at).getTime(),
version: (currentItem.version as string).slice(-7)
version: currentItem.version.slice(-7)
};
} else {
throw 'Invalid gist provided';
throw new Error('Invalid gist provided');
}
};
@@ -73,35 +89,38 @@ const getStateFromGist = (gist: GistData, gistURL: string = gist.url): State =>
};
export const loadGistData = async (gistURL: string): Promise<State> => {
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);
const path = gistURL.split('github.com').pop();
if (!path) {
throw new Error('Invalid GitHub URL' + gistURL);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, __, gistID, revisionID] = path.split('/');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { history }: GistResponse = await (
await fetch(`https://api.github.com/gists/${gistID}${revisionID ? '/' + revisionID : ''}`)
).json();
const gistHistory: GistData[] = [];
for (const entry of history) {
const data: GistData | undefined = await getGistData(entry.url).catch(() => undefined);
data && gistHistory.push(data);
}
if (gistHistory.length === 0) {
throw new Error('Invalid gist provided');
}
gistHistory.reverse();
const entry = gistHistory.slice(-1).pop();
if (!entry) {
throw new Error('Invalid gist provided');
}
const state = getStateFromGist(entry, 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;
};
+7 -6
View File
@@ -8,10 +8,11 @@ const loaders: Record<string, Loader> = {
export const loadDataFromUrl = async (): Promise<void> => {
const searchParams = new URLSearchParams(window.location.search);
let state: Partial<State> = defaultState;
let code: string, config: string;
let code: string | undefined = undefined;
let config: string | undefined = undefined;
let loaded = false;
const codeURL: string = searchParams.get('code');
const configURL: string = searchParams.get('config');
const codeURL: string | undefined = searchParams.get('code') ?? undefined;
const configURL: string | undefined = searchParams.get('config') ?? undefined;
if (codeURL) {
code = await (await fetch(codeURL)).text();
@@ -23,9 +24,6 @@ export const loadDataFromUrl = async (): Promise<void> => {
config = defaultState.mermaid;
}
if (!code) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
for (const [key, value] of searchParams.entries()) {
if (key in loaders) {
try {
@@ -38,6 +36,9 @@ export const loadDataFromUrl = async (): Promise<void> => {
}
}
} else {
if (!codeURL) {
throw new Error('Code URL is not defined');
}
state = {
code,
mermaid: config,
+1 -1
View File
@@ -23,5 +23,5 @@ export const render = async (
export const parse = async (code: string): Promise<boolean> => {
await init();
return (await mermaid.parseAsync(code)) as boolean;
return await mermaid.parseAsync(code);
};
+10 -6
View File
@@ -16,18 +16,22 @@ describe('migrations', () => {
it('should migrate from v0 to v1', async () => {
const { applyMigrations } = await import('./migrations');
let manualHistoryStore: HistoryEntry[] = JSON.parse(
window.localStorage.getItem('manualHistoryStore')
);
window.localStorage.getItem('manualHistoryStore') ?? '[]'
) as HistoryEntry[];
let autoHistoryStore: HistoryEntry[] = JSON.parse(
window.localStorage.getItem('autoHistoryStore')
);
window.localStorage.getItem('autoHistoryStore') ?? '[]'
) as HistoryEntry[];
expect(manualHistoryStore.every(({ id }) => id !== undefined)).toBe(false);
expect(autoHistoryStore.every(({ id }) => id !== undefined)).toBe(false);
applyMigrations();
manualHistoryStore = JSON.parse(window.localStorage.getItem('manualHistoryStore'));
autoHistoryStore = JSON.parse(window.localStorage.getItem('autoHistoryStore'));
manualHistoryStore = JSON.parse(
window.localStorage.getItem('manualHistoryStore') ?? '[]'
) as HistoryEntry[];
autoHistoryStore = JSON.parse(
window.localStorage.getItem('autoHistoryStore') ?? '[]'
) as HistoryEntry[];
expect(manualHistoryStore.every(({ id }) => id !== undefined)).toBe(true);
expect(autoHistoryStore.every(({ id }) => id !== undefined)).toBe(true);
});
+2 -2
View File
@@ -1,13 +1,13 @@
import { writable, get, type Writable } from 'svelte/store';
import { persist, localStorage } from '$lib/util/persist';
import { injectHistoryIDs } from '$lib/components/history/history';
import { injectHistoryIDs } from '$lib/components/History/history';
import { logEvent } from './stats';
interface MigrationState {
version: number;
}
const migrations: { [key: string]: () => void } = {
const migrations: Record<string, () => void> = {
injectHistoryIDs
};
+16 -22
View File
@@ -39,16 +39,16 @@ let noWarnings = false;
/**
* List of storages where the warning have already been displayed.
*/
const alreadyWarnFor: Array<string> = [];
const alreadyWarnFor: string[] = [];
/**
* Add a log to indicate that the requested Storage have not been found.
* @param {string} storageName
*/
const warnStorageNotFound = (storageName) => {
const isProduction = typeof process !== 'undefined' && process.env?.NODE_ENV === 'production';
const warnStorageNotFound = (storageName: string) => {
const isProduction = typeof process !== 'undefined' && process.env.NODE_ENV === 'production';
if (!noWarnings && alreadyWarnFor.indexOf(storageName) === -1 && !isProduction) {
if (!noWarnings && !alreadyWarnFor.includes(storageName) && !isProduction) {
let message = `Unable to find the ${storageName}. No data will be persisted.`;
if (typeof window === 'undefined') {
message +=
@@ -60,17 +60,8 @@ const warnStorageNotFound = (storageName) => {
}
};
const allowedClasses = [];
/**
* Add a class to the allowed list of classes to be serialized
* @param classDef The class to add to the list
*/
export const addSerializableClass = (classDef: () => unknown): void => {
allowedClasses.push(classDef);
};
const serialize = (value: unknown): string => ESSerializer.serialize(value);
const deserialize = (value: string): unknown => {
const deserialize = (value?: string | null): unknown => {
// @TODO: to remove in the next major
if (value === 'undefined') {
return undefined;
@@ -78,7 +69,7 @@ const deserialize = (value: string): unknown => {
if (value !== null && value !== undefined) {
try {
return ESSerializer.deserialize(value, allowedClasses);
return ESSerializer.deserialize(value);
} catch (e) {
// Do nothing
// use the value "as is"
@@ -162,7 +153,7 @@ export function persist<T>(
store.set(initialValue);
}
if ((storage as SelfUpdateStorageInterface<T>).addListener) {
if ('addListener' in storage) {
(storage as SelfUpdateStorageInterface<T>).addListener(key, (newValue) => {
store.set(newValue);
});
@@ -184,7 +175,7 @@ function getBrowserStorage(
browserStorage: Storage,
listenExternalChanges = false
): SelfUpdateStorageInterface<any> {
const listeners: Array<{ key: string; listener: (newValue: any) => void }> = [];
const listeners: { key: string; listener: (newValue: any) => void }[] = [];
const listenerFunction = (event: StorageEvent) => {
const eventKey = event.key;
if (event.storageArea === browserStorage) {
@@ -196,12 +187,14 @@ function getBrowserStorage(
}
};
const connect = () => {
if (listenExternalChanges && typeof window !== 'undefined' && window?.addEventListener) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (listenExternalChanges && typeof window !== 'undefined' && window.addEventListener) {
window.addEventListener('storage', listenerFunction);
}
};
const disconnect = () => {
if (listenExternalChanges && typeof window !== 'undefined' && window?.removeEventListener) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (listenExternalChanges && typeof window !== 'undefined' && window.removeEventListener) {
window.removeEventListener('storage', listenerFunction);
}
};
@@ -237,10 +230,11 @@ function getBrowserStorage(
/**
* Storage implementation that use the browser local storage
* @param {boolean} listenExternalChanges - Update the store if the localStorage is updated from another page
* @param listenExternalChanges - Update the store if the localStorage is updated from another page
*/
export function localStorage<T>(listenExternalChanges = false): StorageInterface<T> {
if (typeof window !== 'undefined' && window?.localStorage) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (typeof window !== 'undefined' && window.localStorage) {
return getBrowserStorage(window.localStorage, listenExternalChanges);
}
warnStorageNotFound('window.localStorage');
@@ -250,7 +244,7 @@ export function localStorage<T>(listenExternalChanges = false): StorageInterface
/**
* Storage implementation that do nothing
*/
export function noopStorage(): StorageInterface<any> {
export function noopStorage<T>(): StorageInterface<T> {
return {
getValue(): null {
return null;
+1 -1
View File
@@ -36,7 +36,7 @@ const serdes: { [key in SerdeType]: Serde } = {
};
export const serializeState = (state: State, serde: SerdeType = 'pako'): string => {
if (serdes[serde] === undefined) {
if (!(serde in serdes)) {
throw new Error(`Unknown serde type: ${serde}`);
}
const json = JSON.stringify(state);
+21 -12
View File
@@ -5,7 +5,9 @@ import { serializeState, deserializeState } from './serde';
import { cmdKey, errorDebug, AsyncQueue } from './util';
import { parse } from './mermaid';
import type { MarkerData, State, ValidatedState } from '$lib/types';
import type { ErrorHash, MarkerData, State, ValidatedState } from '$lib/types';
import type { MermaidConfig } from 'mermaid';
export const defaultState: State = {
code: `graph TD
A[Christmas] -->|Get money| B(Go shopping)
@@ -50,7 +52,7 @@ export const currentState: ValidatedState = (() => {
};
})();
let q: AsyncQueue<State>;
let q: AsyncQueue<State> | undefined;
const processState = async (state: State) => {
const processed: ValidatedState = {
@@ -69,14 +71,19 @@ const processState = async (state: State) => {
processed.error = e;
errorDebug();
console.error(e);
if (e.hash) {
if ('hash' in e) {
const {
loc: { first_line, last_line, first_column, last_column }
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
} = e.hash as ErrorHash;
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,
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: e.str
};
processed.errorMarkers = [marker];
@@ -99,7 +106,7 @@ export const stateStore: Readable<ValidatedState> = derived(
set(newState);
});
}
q.process(state);
void q.process(state);
},
currentState
);
@@ -109,8 +116,10 @@ export const loadState = (data: string): void => {
console.log(`Loading '${data}'`);
try {
state = deserializeState(data);
const mermaidConfig: { [key: string]: string } =
typeof state.mermaid === 'string' ? JSON.parse(state.mermaid) : state.mermaid;
const mermaidConfig: MermaidConfig =
typeof state.mermaid === 'string'
? (JSON.parse(state.mermaid) as MermaidConfig)
: state.mermaid;
if (
mermaidConfig.securityLevel &&
mermaidConfig.securityLevel !== 'strict' &&
@@ -180,7 +189,7 @@ export const updateConfig = (config: string): void => {
export const toggleDarkTheme = (dark: boolean): void => {
inputStateStore.update((state) => {
const config = JSON.parse(state.mermaid);
const config = JSON.parse(state.mermaid) as MermaidConfig;
if (!config.theme || ['dark', 'default'].includes(config.theme)) {
config.theme = dark ? 'dark' : 'default';
}
@@ -194,7 +203,7 @@ export const initURLSubscription = (): void => {
stateStore.subscribe(({ serialized }) => {
clearTimeout(urlDebounce);
urlDebounce = window.setTimeout(() => {
history.replaceState(undefined, undefined, `#${serialized}`);
history.replaceState(undefined, '', `#${serialized}`);
}, 250);
});
};
+10 -9
View File
@@ -1,6 +1,6 @@
import { browser } from '$app/environment';
import type { AnalyticsInstance } from 'analytics';
export let analytics: AnalyticsInstance;
export let analytics: AnalyticsInstance | undefined;
export const initAnalytics = async (): Promise<void> => {
if (browser && !analytics) {
@@ -27,7 +27,7 @@ export const initAnalytics = async (): Promise<void> => {
}
};
export const detectType = (text: string): string => {
export const detectType = (text: string): string | undefined => {
const possibleDiagramTypes = [
'classDiagram',
'erDiagram',
@@ -49,7 +49,7 @@ export const detectType = (text: string): string => {
};
export const countLines = (code: string): number => {
return (code.match(/\n/g) || '').length + 1;
return (code.match(/\n/g)?.length ?? 0) + 1;
};
export const saveStatistics = (graph: string): void => {
@@ -80,19 +80,20 @@ const delaysPerEvent = {
themeChange: defaultDelay
};
export type AnalyticsEvent = keyof typeof delaysPerEvent;
const timeouts: Record<string, number> = {};
const timeouts: Map<string, number> = new Map<string, number>();
// manual debounce to reduce the number of events sent to analytics
export const logEvent = (name: AnalyticsEvent, data?: unknown): void => {
if (!analytics) {
return;
}
const key = data ? JSON.stringify({ name, data }) : name;
if (timeouts[key] === undefined) {
if (!timeouts.has(key)) {
void analytics.track(name, data);
} else {
clearTimeout(timeouts[key]);
clearTimeout(timeouts.get(key));
}
timeouts[key] = window.setTimeout(() => {
delete timeouts[key];
}, delaysPerEvent[name]);
timeouts.set(
key,
window.setTimeout(() => timeouts.delete(key), delaysPerEvent[name])
);
};
+2 -2
View File
@@ -23,10 +23,10 @@ export const initHandler = async (): Promise<void> => {
syncDiagram();
initURLSubscription();
await initAnalytics();
analytics?.page();
await analytics?.page();
};
export const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
export const isMac = navigator.platform.toUpperCase().includes('MAC');
export const cmdKey = isMac ? 'Cmd' : 'Ctrl';
let count = 0;
+2 -2
View File
@@ -10,8 +10,8 @@
// 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(() => {
window.addEventListener('hashchange', async (ev) => {
await initHandler();
window.addEventListener('hashchange', () => {
void initHandler();
});
if ('serviceWorker' in navigator) {
navigator.serviceWorker
+11 -7
View File
@@ -1,11 +1,11 @@
<script lang="ts">
import Editor from '$lib/components/editor.svelte';
import Navbar from '$lib/components/navbar.svelte';
import Preset from '$lib/components/preset.svelte';
import Actions from '$lib/components/actions.svelte';
import View from '$lib/components/view.svelte';
import Card from '$lib/components/card/card.svelte';
import History from '$lib/components/history/history.svelte';
import Editor from '$lib/components/Editor.svelte';
import Navbar from '$lib/components/Navbar.svelte';
import Preset from '$lib/components/Preset.svelte';
import Actions from '$lib/components/Actions.svelte';
import View from '$lib/components/View.svelte';
import Card from '$lib/components/Card/Card.svelte';
import History from '$lib/components/History/History.svelte';
import { inputStateStore, stateStore, updateCodeStore } from '$lib/util/state';
import { cmdKey, initHandler, syncDiagram } from '$lib/util/util';
import { onMount } from 'svelte';
@@ -86,6 +86,10 @@
await initHandler();
const resizer = document.getElementById('resizeHandler');
const element = document.getElementById('editorPane');
if (!resizer || !element) {
console.debug('Failed to find resize handler or editor pane', { resizer, element });
return;
}
const resize = (e: { pageX: number }) => {
const newWidth = e.pageX - element.getBoundingClientRect().left;
if (newWidth > 50) {
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import View from '$lib/components/view.svelte';
import View from '$lib/components/View.svelte';
import { initHandler } from '$lib/util/util';
import { onMount } from 'svelte';
onMount(initHandler);
+1
View File
@@ -12,6 +12,7 @@
"compilerOptions": {
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"strictNullChecks": true,
"types": ["vitest/importMeta"]
},
"extends": "./.svelte-kit/tsconfig.json"