]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/blob
9d9348a5f0b00cf257fb7174f6b1fd0a695b396f
[pkg/ggml/sources/llama.cpp] /
1 /**
2 * Rehype plugin to enhance code blocks with wrapper, header, and action buttons.
3 *
4 * Wraps <pre><code> elements with a container that includes:
5 * - Language label
6 * - Copy button
7 * - Preview button (for HTML code blocks)
8 *
9 * This operates directly on the HAST tree for better performance,
10 * avoiding the need to stringify and re-parse HTML.
11 */
12
13 import type { Plugin } from 'unified';
14 import type { Root, Element, ElementContent } from 'hast';
15 import { visit } from 'unist-util-visit';
16 import {
17 CODE_BLOCK_SCROLL_CONTAINER_CLASS,
18 CODE_BLOCK_WRAPPER_CLASS,
19 CODE_BLOCK_HEADER_CLASS,
20 CODE_BLOCK_ACTIONS_CLASS,
21 CODE_LANGUAGE_CLASS,
22 COPY_CODE_BTN_CLASS,
23 PREVIEW_CODE_BTN_CLASS,
24 RELATIVE_CLASS
25 } from '$lib/constants';
26
27 declare global {
28 interface Window {
29 idxCodeBlock?: number;
30 }
31 }
32
33 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>`;
34
35 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>`;
36
37 function createIconElement(svg: string): Element {
38 return {
39 type: 'element',
40 tagName: 'span',
41 properties: {},
42 children: [{ type: 'raw', value: svg } as unknown as ElementContent]
43 };
44 }
45
46 function createButton(className: string, title: string, iconSvg: string, codeId: string): Element {
47 return {
48 type: 'element',
49 tagName: 'button',
50 properties: {
51 className: [className],
52 'data-code-id': codeId,
53 title,
54 type: 'button'
55 },
56 children: [createIconElement(iconSvg)]
57 };
58 }
59
60 function createCopyButton(codeId: string): Element {
61 return createButton(COPY_CODE_BTN_CLASS, 'Copy code', COPY_ICON_SVG, codeId);
62 }
63
64 function createPreviewButton(codeId: string): Element {
65 return createButton(PREVIEW_CODE_BTN_CLASS, 'Preview code', PREVIEW_ICON_SVG, codeId);
66 }
67
68 function createHeader(language: string, codeId: string): Element {
69 const actions: Element[] = [createCopyButton(codeId)];
70
71 if (language.toLowerCase() === 'html') {
72 actions.push(createPreviewButton(codeId));
73 }
74
75 return {
76 type: 'element',
77 tagName: 'div',
78 properties: { className: [CODE_BLOCK_HEADER_CLASS] },
79 children: [
80 {
81 type: 'element',
82 tagName: 'span',
83 properties: { className: [CODE_LANGUAGE_CLASS] },
84 children: [{ type: 'text', value: language }]
85 },
86 {
87 type: 'element',
88 tagName: 'div',
89 properties: { className: [CODE_BLOCK_ACTIONS_CLASS] },
90 children: actions
91 }
92 ]
93 };
94 }
95
96 function createScrollContainer(preElement: Element): Element {
97 return {
98 type: 'element',
99 tagName: 'div',
100 properties: { className: [CODE_BLOCK_SCROLL_CONTAINER_CLASS] },
101 children: [preElement]
102 };
103 }
104
105 function createWrapper(header: Element, preElement: Element): Element {
106 return {
107 type: 'element',
108 tagName: 'div',
109 properties: { className: [CODE_BLOCK_WRAPPER_CLASS, RELATIVE_CLASS] },
110 children: [header, createScrollContainer(preElement)]
111 };
112 }
113
114 function extractLanguage(codeElement: Element): string {
115 const className = codeElement.properties?.className;
116 if (!Array.isArray(className)) return 'text';
117
118 for (const cls of className) {
119 if (typeof cls === 'string' && cls.startsWith('language-')) {
120 return cls.replace('language-', '');
121 }
122 }
123
124 return 'text';
125 }
126
127 /**
128 * Generates a unique code block ID using a global counter.
129 */
130 function generateCodeId(): string {
131 if (typeof window !== 'undefined') {
132 return `code-${(window.idxCodeBlock = (window.idxCodeBlock ?? 0) + 1)}`;
133 }
134 // Fallback for SSR - use timestamp + random
135 return `code-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
136 }
137
138 /**
139 * Rehype plugin to enhance code blocks with wrapper, header, and action buttons.
140 * This plugin wraps <pre><code> elements with a container that includes:
141 * - Language label
142 * - Copy button
143 * - Preview button (for HTML code blocks)
144 */
145 export const rehypeEnhanceCodeBlocks: Plugin<[], Root> = () => {
146 return (tree: Root) => {
147 visit(tree, 'element', (node: Element, index, parent) => {
148 if (node.tagName !== 'pre' || !parent || index === undefined) return;
149
150 const codeElement = node.children.find(
151 (child): child is Element => child.type === 'element' && child.tagName === 'code'
152 );
153
154 if (!codeElement) return;
155
156 const language = extractLanguage(codeElement);
157 const codeId = generateCodeId();
158
159 codeElement.properties = {
160 ...codeElement.properties,
161 'data-code-id': codeId
162 };
163
164 const header = createHeader(language, codeId);
165 const wrapper = createWrapper(header, node);
166
167 // Replace pre with wrapper in parent
168 (parent.children as ElementContent[])[index] = wrapper;
169 });
170 };
171 };