summaryrefslogtreecommitdiff
path: root/llama.cpp/tools/server/webui/src/lib/markdown/enhance-code-blocks.ts
diff options
context:
space:
mode:
Diffstat (limited to 'llama.cpp/tools/server/webui/src/lib/markdown/enhance-code-blocks.ts')
-rw-r--r--llama.cpp/tools/server/webui/src/lib/markdown/enhance-code-blocks.ts162
1 files changed, 162 insertions, 0 deletions
diff --git a/llama.cpp/tools/server/webui/src/lib/markdown/enhance-code-blocks.ts b/llama.cpp/tools/server/webui/src/lib/markdown/enhance-code-blocks.ts
new file mode 100644
index 0000000..6f0e03e
--- /dev/null
+++ b/llama.cpp/tools/server/webui/src/lib/markdown/enhance-code-blocks.ts
@@ -0,0 +1,162 @@
+/**
+ * Rehype plugin to enhance code blocks with wrapper, header, and action buttons.
+ *
+ * Wraps <pre><code> elements with a container that includes:
+ * - Language label
+ * - Copy button
+ * - Preview button (for HTML code blocks)
+ *
+ * This operates directly on the HAST tree for better performance,
+ * avoiding the need to stringify and re-parse HTML.
+ */
+
+import type { Plugin } from 'unified';
+import type { Root, Element, ElementContent } from 'hast';
+import { visit } from 'unist-util-visit';
+
+declare global {
+ interface Window {
+ idxCodeBlock?: number;
+ }
+}
+
+const COPY_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy-icon lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
+
+const PREVIEW_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye lucide-eye-icon"><path d="M2.062 12.345a1 1 0 0 1 0-.69C3.5 7.73 7.36 5 12 5s8.5 2.73 9.938 6.655a1 1 0 0 1 0 .69C20.5 16.27 16.64 19 12 19s-8.5-2.73-9.938-6.655"/><circle cx="12" cy="12" r="3"/></svg>`;
+
+/**
+ * Creates an SVG element node from raw SVG string.
+ * Since we can't parse HTML in HAST directly, we use the raw property.
+ */
+function createRawHtmlElement(html: string): Element {
+ return {
+ type: 'element',
+ tagName: 'span',
+ properties: {},
+ children: [{ type: 'raw', value: html } as unknown as ElementContent]
+ };
+}
+
+function createCopyButton(codeId: string): Element {
+ return {
+ type: 'element',
+ tagName: 'button',
+ properties: {
+ className: ['copy-code-btn'],
+ 'data-code-id': codeId,
+ title: 'Copy code',
+ type: 'button'
+ },
+ children: [createRawHtmlElement(COPY_ICON_SVG)]
+ };
+}
+
+function createPreviewButton(codeId: string): Element {
+ return {
+ type: 'element',
+ tagName: 'button',
+ properties: {
+ className: ['preview-code-btn'],
+ 'data-code-id': codeId,
+ title: 'Preview code',
+ type: 'button'
+ },
+ children: [createRawHtmlElement(PREVIEW_ICON_SVG)]
+ };
+}
+
+function createHeader(language: string, codeId: string): Element {
+ const actions: Element[] = [createCopyButton(codeId)];
+
+ if (language.toLowerCase() === 'html') {
+ actions.push(createPreviewButton(codeId));
+ }
+
+ return {
+ type: 'element',
+ tagName: 'div',
+ properties: { className: ['code-block-header'] },
+ children: [
+ {
+ type: 'element',
+ tagName: 'span',
+ properties: { className: ['code-language'] },
+ children: [{ type: 'text', value: language }]
+ },
+ {
+ type: 'element',
+ tagName: 'div',
+ properties: { className: ['code-block-actions'] },
+ children: actions
+ }
+ ]
+ };
+}
+
+function createWrapper(header: Element, preElement: Element): Element {
+ return {
+ type: 'element',
+ tagName: 'div',
+ properties: { className: ['code-block-wrapper'] },
+ children: [header, preElement]
+ };
+}
+
+function extractLanguage(codeElement: Element): string {
+ const className = codeElement.properties?.className;
+ if (!Array.isArray(className)) return 'text';
+
+ for (const cls of className) {
+ if (typeof cls === 'string' && cls.startsWith('language-')) {
+ return cls.replace('language-', '');
+ }
+ }
+
+ return 'text';
+}
+
+/**
+ * Generates a unique code block ID using a global counter.
+ */
+function generateCodeId(): string {
+ if (typeof window !== 'undefined') {
+ return `code-${(window.idxCodeBlock = (window.idxCodeBlock ?? 0) + 1)}`;
+ }
+ // Fallback for SSR - use timestamp + random
+ return `code-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
+}
+
+/**
+ * Rehype plugin to enhance code blocks with wrapper, header, and action buttons.
+ * This plugin wraps <pre><code> elements with a container that includes:
+ * - Language label
+ * - Copy button
+ * - Preview button (for HTML code blocks)
+ */
+export const rehypeEnhanceCodeBlocks: Plugin<[], Root> = () => {
+ return (tree: Root) => {
+ visit(tree, 'element', (node: Element, index, parent) => {
+ if (node.tagName !== 'pre' || !parent || index === undefined) return;
+
+ const codeElement = node.children.find(
+ (child): child is Element => child.type === 'element' && child.tagName === 'code'
+ );
+
+ if (!codeElement) return;
+
+ const language = extractLanguage(codeElement);
+ const codeId = generateCodeId();
+
+ codeElement.properties = {
+ ...codeElement.properties,
+ 'data-code-id': codeId
+ };
+
+ const header = createHeader(language, codeId);
+ const wrapper = createWrapper(header, node);
+
+ // Replace pre with wrapper in parent
+ (parent.children as ElementContent[])[index] = wrapper;
+ });
+ };
+};