Merge pull request #1911 from mermaid-js/release-promotion
Release live editor
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildRedirectUrl } from './redirect';
|
||||
|
||||
const mockLocation = (url: string): Location => {
|
||||
const parsed = new URL(url);
|
||||
return { hash: parsed.hash, search: parsed.search } as Location;
|
||||
};
|
||||
|
||||
describe('buildRedirectUrl', () => {
|
||||
it('should redirect to /edit by default when hash is empty', () => {
|
||||
expect(buildRedirectUrl(mockLocation('https://mermaid.live/'))).toBe('/edit');
|
||||
});
|
||||
|
||||
it('should preserve search params', () => {
|
||||
expect(buildRedirectUrl(mockLocation('https://mermaid.live/?utm_source=github'))).toBe(
|
||||
'/edit?utm_source=github'
|
||||
);
|
||||
});
|
||||
|
||||
it('should extract route and fragment from old hash format', () => {
|
||||
expect(buildRedirectUrl(mockLocation('https://mermaid.live/#/edit/pako:abc123'))).toBe(
|
||||
'/edit#pako:abc123'
|
||||
);
|
||||
});
|
||||
|
||||
it('should place search params before the hash fragment', () => {
|
||||
expect(
|
||||
buildRedirectUrl(mockLocation('https://mermaid.live/?utm_source=twitter#/edit/pako:abc123'))
|
||||
).toBe('/edit?utm_source=twitter#pako:abc123');
|
||||
});
|
||||
|
||||
it('should handle hash with view route', () => {
|
||||
expect(buildRedirectUrl(mockLocation('https://mermaid.live/#/view/pako:xyz'))).toBe(
|
||||
'/view#pako:xyz'
|
||||
);
|
||||
});
|
||||
|
||||
it('should default to edit when hash has no route', () => {
|
||||
expect(buildRedirectUrl(mockLocation('https://mermaid.live/#somethingelse'))).toBe('/edit');
|
||||
});
|
||||
|
||||
it('should handle multiple search params with hash', () => {
|
||||
expect(
|
||||
buildRedirectUrl(
|
||||
mockLocation(
|
||||
'https://mermaid.live/?utm_source=gh&utm_medium=link&utm_campaign=test#/edit/pako:data'
|
||||
)
|
||||
)
|
||||
).toBe('/edit?utm_source=gh&utm_medium=link&utm_campaign=test#pako:data');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Build the redirect URL for legacy root-path links.
|
||||
* Extracts the route and fragment from the old hash-based URL format,
|
||||
* and ensures search params (e.g. UTM) come before the hash fragment.
|
||||
*/
|
||||
export const buildRedirectUrl = (location: Location): string => {
|
||||
const parts = location.hash.split('/');
|
||||
let path = 'edit';
|
||||
let fragment = '';
|
||||
if (parts.length > 2) {
|
||||
path = parts[1];
|
||||
fragment = `#${parts[2]}`;
|
||||
}
|
||||
return `/${path}${location.search}${fragment}`;
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { getAnalyticsSafeUrl } from './stats';
|
||||
|
||||
describe('getAnalyticsUrl', () => {
|
||||
beforeEach(() => {
|
||||
// Reset location to a clean state before each test
|
||||
window.history.replaceState(null, '', '/');
|
||||
});
|
||||
|
||||
it('should return origin and pathname for a simple URL', () => {
|
||||
window.history.replaceState(null, '', '/edit');
|
||||
const url = getAnalyticsSafeUrl();
|
||||
expect(url).toBe(`${window.location.origin}/edit`);
|
||||
});
|
||||
|
||||
it('should include search/query params (UTM parameters)', () => {
|
||||
window.history.replaceState(null, '', '/edit?utm_source=github&utm_medium=docs');
|
||||
const url = getAnalyticsSafeUrl();
|
||||
expect(url).toBe(`${window.location.origin}/edit?utm_source=github&utm_medium=docs`);
|
||||
});
|
||||
|
||||
it('should never include the hash', () => {
|
||||
window.history.replaceState(null, '', '/edit#pako:someDiagramData');
|
||||
// replaceState doesn't set hash, so set it via location
|
||||
window.location.hash = '#pako:someDiagramData';
|
||||
const url = getAnalyticsSafeUrl();
|
||||
expect(url).not.toContain('#');
|
||||
expect(url).not.toContain('pako:');
|
||||
expect(url).toBe(`${window.location.origin}/edit`);
|
||||
});
|
||||
|
||||
it('should include search params but exclude hash when both are present', () => {
|
||||
window.history.replaceState(null, '', '/edit?utm_campaign=launch');
|
||||
window.location.hash = '#pako:diagramDataHere';
|
||||
const url = getAnalyticsSafeUrl();
|
||||
expect(url).not.toContain('#');
|
||||
expect(url).not.toContain('pako:');
|
||||
expect(url).toContain('utm_campaign=launch');
|
||||
expect(url).toBe(`${window.location.origin}/edit?utm_campaign=launch`);
|
||||
});
|
||||
|
||||
it('should return just origin for root path with no params', () => {
|
||||
window.history.replaceState(null, '', '/');
|
||||
const url = getAnalyticsSafeUrl();
|
||||
expect(url).toBe(`${window.location.origin}/`);
|
||||
});
|
||||
});
|
||||
+10
-5
@@ -23,6 +23,15 @@ export const initAnalytics = async (): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the current page URL for analytics tracking.
|
||||
* Includes origin, pathname, and search (for UTM params),
|
||||
* but never the hash (which contains diagram data).
|
||||
*/
|
||||
export const getAnalyticsSafeUrl = (): string => {
|
||||
return window.location.origin + window.location.pathname + window.location.search;
|
||||
};
|
||||
|
||||
export const countLines = (code: string): number => {
|
||||
return (code.match(/\n/g)?.length ?? 0) + 1;
|
||||
};
|
||||
@@ -111,11 +120,7 @@ export const logEvent = (
|
||||
if (timeouts.has(key)) {
|
||||
clearTimeout(timeouts.get(key));
|
||||
} else {
|
||||
plausible.trackEvent(
|
||||
name,
|
||||
{ props: data },
|
||||
{ url: window.location.origin + window.location.pathname }
|
||||
);
|
||||
plausible.trackEvent(name, { props: data }, { url: getAnalyticsSafeUrl() });
|
||||
}
|
||||
timeouts.set(
|
||||
key,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { initLoading } from './loading';
|
||||
import { isOnMermaidAI } from './migration/domainMigration';
|
||||
import { applyMigrations } from './migrations';
|
||||
import { initURLSubscription, loadState, updateCodeStore, verifyState } from './state';
|
||||
import { initAnalytics, plausible } from './stats';
|
||||
import { getAnalyticsSafeUrl, initAnalytics, plausible } from './stats';
|
||||
|
||||
export const getDomain = (url?: string): string => {
|
||||
if (!url) return '';
|
||||
@@ -30,7 +30,9 @@ export const initHandler = async (): Promise<void> => {
|
||||
syncDiagram();
|
||||
initURLSubscription();
|
||||
await initAnalytics();
|
||||
plausible?.trackPageview({ url: window.location.origin + window.location.pathname });
|
||||
plausible?.trackPageview({
|
||||
url: getAnalyticsSafeUrl()
|
||||
});
|
||||
verifyState();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { buildRedirectUrl } from '$lib/util/redirect';
|
||||
import { onMount } from 'svelte';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
onMount(async () => {
|
||||
// Handle old live editor links and redirect to new version
|
||||
const hash = window.location.hash.split('/');
|
||||
let newURL = 'edit';
|
||||
if (hash.length > 2) {
|
||||
newURL = `${hash[1]}#${hash[2]}`;
|
||||
}
|
||||
await goto(`${base}/${newURL}`, {
|
||||
await goto(buildRedirectUrl(window.location), {
|
||||
replaceState: true
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user