Skip to content

Commit

Permalink
feat(canvas): markmap
Browse files Browse the repository at this point in the history
  • Loading branch information
pnd280 committed Jan 13, 2025
1 parent e1dc5c6 commit da97509
Show file tree
Hide file tree
Showing 35 changed files with 931 additions and 435 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"i18next": "^23.16.8",
"immer": "^10.1.1",
"jquery": "^3.7.1",
"js-base64": "^3.7.7",
"lodash": "^4.17.21",
"lz-string": "^1.5.0",
"pako": "^2.1.0",
Expand Down Expand Up @@ -112,7 +113,9 @@
"glob": "^11.0.1",
"gulp": "^4.0.2",
"gulp-zip": "^6.1.0",
"js-base64": "^3.7.7",
"markmap-lib": "^0.18.8",
"markmap-render": "^0.18.8",
"markmap-view": "^0.18.8",
"mermaid": "^11.4.1",
"minimatch": "^10.0.1",
"nanoid": "^5.0.9",
Expand Down
722 changes: 343 additions & 379 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/components/CodeHighlighter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useColorSchemeStore } from "@/data/color-scheme-store";
const INTERPRETED_LANGUAGES: Record<string, string> = {
html: "markup",
react: "jsx",
markmap: "markdown",
};

const CodeHighlighter = memo(function CodeHighlighter({
Expand Down
8 changes: 7 additions & 1 deletion src/data/plugins/plugins-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const CORE_PLUGINS = [
"domObserver",
"reactVdom",
"mermaidRenderer",
"markmapRenderer",
] as const;

export type CorePluginId = (typeof CORE_PLUGINS)[number];
Expand Down Expand Up @@ -181,7 +182,12 @@ export const PLUGINS_METADATA: CplxPluginMetadata = {
"Visualize and interact with generated content side by side. Similar to claude.ai's artifacts. Very experimental",
tags: ["new", "experimental", "desktopOnly", "ui"],
dependentPlugins: ["thread:betterCodeBlocks"],
dependentCorePlugins: ["spaRouter", "domObserver", "mermaidRenderer"],
dependentCorePlugins: [
"spaRouter",
"domObserver",
"mermaidRenderer",
"markmapRenderer",
],
},
"thread:exportThread": {
id: "thread:exportThread",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { PluginsStatesService } from "@/services/plugins-states/plugins-states";
import { getThemeCss } from "@/utils/pplx-theme-loader-utils";
import { injectMainWorldScript, insertCss } from "@/utils/utils";

import markmapRendererPlugin from "@/features/plugins/_core/markmap-renderer/index.main?script&module";
import mermaidRendererPlugin from "@/features/plugins/_core/mermaid-renderer/index.main?script&module";
import networkInterceptPlugin from "@/features/plugins/_core/network-intercept/index.main?script&module";
import reactVdomPlugin from "@/features/plugins/_core/react-vdom/index.main?script&module";
Expand Down Expand Up @@ -54,6 +55,12 @@ export async function initCorePlugins() {
inject: shouldEnableCorePlugin("mermaidRenderer"),
});

injectMainWorldScript({
url: chrome.runtime.getURL(markmapRendererPlugin),
head: true,
inject: shouldEnableCorePlugin("markmapRenderer"),
});

if (shouldEnableCorePlugin("webSocket")) {
InternalWebSocketManager.getInstance().handShake();
}
Expand Down
5 changes: 3 additions & 2 deletions src/entrypoints/webext-bridge-imports.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { BackgroundEvents as BackgroundEventHandlers } from "@/entrypoints/background/listeners";
import type { MarkmapRendererEvents as MarkmapRendererEventHandlers } from "@/features/plugins/_core/markmap-renderer/listeners.main";
import type { MermaidRendererEvents as MermaidRendererEventHandlers } from "@/features/plugins/_core/mermaid-renderer/listeners.main";
import type { InterceptorsEvents as NetworkInterceptInterceptorsEventHandlers } from "@/features/plugins/_core/network-intercept/listeners";
import type { ReactVdomEvents as ReactVdomEventHandlers } from "@/features/plugins/_core/react-vdom/listeners.main";
import type { DispatchEvents as SpaRouterDispatchEventHandlers } from "@/features/plugins/_core/spa-router/listeners";
import type { CsUtilEvents as SpaRouterCsUtilEventHandlers } from "@/features/plugins/_core/spa-router/listeners.main";

export type AllEventHandlers = BackgroundEventHandlers &
NetworkInterceptInterceptorsEventHandlers &
SpaRouterCsUtilEventHandlers &
SpaRouterDispatchEventHandlers &
ReactVdomEventHandlers &
MermaidRendererEventHandlers;
MermaidRendererEventHandlers &
MarkmapRendererEventHandlers;
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export default function CanvasPluginDetails() {
<div className="tw-mx-auto tw-flex tw-w-max tw-flex-col tw-items-center tw-gap-1">
<InlineCode className="tw-w-max">✅ markdown</InlineCode>
<InlineCode className="tw-w-max">✅ mermaid</InlineCode>
<InlineCode className="tw-w-max">✅ markmap</InlineCode>
<InlineCode className="tw-w-max">✅ plantuml</InlineCode>
<InlineCode className="tw-w-max">✅ html</InlineCode>
<InlineCode className="tw-w-max">❌ react</InlineCode>
Expand All @@ -93,6 +94,7 @@ export default function CanvasPluginDetails() {
<div className="tw-mx-auto tw-flex tw-w-max tw-flex-col tw-items-center tw-gap-1">
<InlineCode className="tw-w-max">✅ markdown</InlineCode>
<InlineCode className="tw-w-max">✅ mermaid</InlineCode>
<InlineCode className="tw-w-max">✅ markmap</InlineCode>
<InlineCode className="tw-w-max">✅ plantuml</InlineCode>
<InlineCode className="tw-w-max">✅ html</InlineCode>
<InlineCode className="tw-w-max">✅ react</InlineCode>
Expand Down
169 changes: 169 additions & 0 deletions src/features/plugins/_core/markmap-renderer/index.main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import throttle from "lodash/throttle";
import type { Markmap } from "markmap-view";

import { setupMarkmapRendererListeners } from "@/features/plugins/_core/markmap-renderer/listeners.main";
import { injectMainWorldScriptBlock } from "@/utils/utils";

export class MarkmapRenderer {
private static instance: MarkmapRenderer | null = null;
private importPromise: Promise<void> | null = null;

private constructor() {}

private currentMarkmapSvgNode: HTMLElement | null = null;
private currentMarkmapInstance: Markmap | null = null;

static getInstance() {
if (!MarkmapRenderer.instance) {
MarkmapRenderer.instance = new MarkmapRenderer();
}
return MarkmapRenderer.instance;
}

initialize() {
setupMarkmapRendererListeners();
$(() => this.importMarkmap());
}

private async importMarkmap(): Promise<void> {
if (!this.importPromise) {
const scriptContent = `
import * as markmapLib from "https://cdn.jsdelivr.net/npm/markmap-lib/+esm";
import * as markmapView from "https://cdn.jsdelivr.net/npm/markmap-view/+esm";
import * as markmapRender from "https://cdn.jsdelivr.net/npm/markmap-render/+esm";
window.markmapLib = markmapLib;
window.markmapView = markmapView;
window.markmapRender = markmapRender;
const transformer = new markmapLib.Transformer();
window.markmapTransformer = transformer;
const { scripts, styles } = transformer.getAssets();
markmapView.loadCSS(styles);
markmapView.loadJS(scripts, { getMarkmap: () => markmapView });
`;

this.importPromise = injectMainWorldScriptBlock({
scriptContent,
waitForExecution: true,
}).catch((error) => {
console.error("Failed to import Markmap:", error);
throw error;
});
}

return this.importPromise;
}

isInitialized() {
return this.importPromise?.then(() => true).catch(() => false);
}

handleRenderRequest = throttle(
async ({
content,
selector,
}: {
content: string;
selector: string;
}): Promise<{ success: boolean; error?: string }> => {
const $target = $(selector);

if ($target.length === 0) {
console.warn("No elements found for rendering Markmap canvas");
return {
success: false,
error: "No elements found for rendering Markmap canvas",
};
}

try {
await MarkmapRenderer.waitForInitialization();

const transformer = window.markmapTransformer!;

const { root } = transformer.transform(content);

if (!document.body.contains(this.currentMarkmapSvgNode)) {
const $target = $(selector);

this.currentMarkmapSvgNode = $target[0];

const { Markmap } = window.markmapView!;

this.currentMarkmapInstance = Markmap.create(
selector,
{
duration: 50,
autoFit: true,
},
root,
);
} else {
this.currentMarkmapInstance?.setData(root);
this.currentMarkmapInstance?.fit();
}

return {
success: true,
};
} catch (error) {
return {
success: false,
error: (error as any).str,
};
}
},
500,
{
leading: true,
trailing: true,
},
);

private static getInteractiveHtml({ content }: { content: string }) {
const fillTemplate = window.markmapRender!.fillTemplate;

const { root, features } = window.markmapTransformer!.transform(content);
const assets = window.markmapTransformer!.getUsedAssets(features);

const html = fillTemplate(root, assets);

return html;
}

static openAsInteractiveHtml({ content }: { content: string }) {
const html = MarkmapRenderer.getInteractiveHtml({ content });

const blob = new Blob([html], { type: "text/html" });
const url = URL.createObjectURL(blob);

window.open(url, "_blank");
}

static downloadAsInteractiveHtml({
content,
title,
}: {
content: string;
title: string;
}) {
const html = MarkmapRenderer.getInteractiveHtml({ content });

const blob = new Blob([html], { type: "text/html" });

const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${title}.html`;
a.click();
}

static async waitForInitialization() {
while (!MarkmapRenderer.getInstance().isInitialized()) {
await sleep(100);
}
}
}

MarkmapRenderer.getInstance().initialize();
51 changes: 51 additions & 0 deletions src/features/plugins/_core/markmap-renderer/listeners.main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { onMessage } from "webext-bridge/window";

import { MarkmapRenderer } from "@/features/plugins/_core/markmap-renderer/index.main";

export type MarkmapRendererEvents = {
"markmapRenderer:isInitialized": () => boolean;
"markmapRenderer:render": (params: { content: string; selector: string }) => {
success: boolean;
error?: string;
};
"markmapRenderer:openAsInteractiveHtml": (params: {
content: string;
}) => void;
"markmapRenderer:downloadAsInteractiveHtml": (params: {
content: string;
title: string;
}) => void;
};

export function setupMarkmapRendererListeners() {
onMessage(
"markmapRenderer:isInitialized",
() => MarkmapRenderer.getInstance()?.isInitialized() ?? false,
);

onMessage("markmapRenderer:render", ({ data: { content, selector } }) => {
return MarkmapRenderer.getInstance().handleRenderRequest({
content,
selector,
});
});

onMessage(
"markmapRenderer:openAsInteractiveHtml",
({ data: { content } }) => {
return MarkmapRenderer.openAsInteractiveHtml({
content,
});
},
);

onMessage(
"markmapRenderer:downloadAsInteractiveHtml",
({ data: { content, title } }) => {
return MarkmapRenderer.downloadAsInteractiveHtml({
content,
title,
});
},
);
}
Loading

0 comments on commit da97509

Please sign in to comment.