File size: 5,518 Bytes
31dd200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
/**

 * 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';
import {
	CODE_BLOCK_SCROLL_CONTAINER_CLASS,
	CODE_BLOCK_WRAPPER_CLASS,
	CODE_BLOCK_HEADER_CLASS,
	CODE_BLOCK_ACTIONS_CLASS,
	CODE_LANGUAGE_CLASS,
	COPY_CODE_BTN_CLASS,
	PREVIEW_CODE_BTN_CLASS,
	RELATIVE_CLASS
} from '$lib/constants/code-blocks';

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_CLASS],
			'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_CLASS],
			'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_CLASS] },
		children: [
			{
				type: 'element',
				tagName: 'span',
				properties: { className: [CODE_LANGUAGE_CLASS] },
				children: [{ type: 'text', value: language }]
			},
			{
				type: 'element',
				tagName: 'div',
				properties: { className: [CODE_BLOCK_ACTIONS_CLASS] },
				children: actions
			}
		]
	};
}

function createScrollContainer(preElement: Element): Element {
	return {
		type: 'element',
		tagName: 'div',
		properties: { className: [CODE_BLOCK_SCROLL_CONTAINER_CLASS] },
		children: [preElement]
	};
}

function createWrapper(header: Element, preElement: Element): Element {
	return {
		type: 'element',
		tagName: 'div',
		properties: { className: [CODE_BLOCK_WRAPPER_CLASS, RELATIVE_CLASS] },
		children: [header, createScrollContainer(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;
		});
	};
};