@farming-labs/theme 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,187 @@
1
+ import { highlight } from "sugar-high";
2
+
3
+ //#region src/ai-markdown.ts
4
+ const codeBlockTokenBoundary = String.fromCharCode(0);
5
+ function buildCodeBlock(lang, code) {
6
+ const highlighted = highlight(code.replace(/\n$/, "")).replace(/<\/span>\n<span/g, "</span><span");
7
+ return `<div class="fd-ai-code-block"><div class="fd-ai-code-header">${lang ? `<div class="fd-ai-code-lang">${escapeHtml(lang)}</div>` : ""}<button class="fd-ai-code-copy" onclick="(function(btn){var code=btn.closest('.fd-ai-code-block').querySelector('code').textContent;navigator.clipboard.writeText(code).then(function(){btn.textContent='Copied!';setTimeout(function(){btn.textContent='Copy'},1500)})})(this)">Copy</button></div><pre><code>${highlighted}</code></pre></div>`;
8
+ }
9
+ function getCodeFenceOpening(line) {
10
+ const match = /^(?: {0,3})(`{3,}|~{3,})(.*)$/.exec(line);
11
+ if (!match) return null;
12
+ const marker = match[1];
13
+ const rawInfo = match[2] ?? "";
14
+ if (!marker) return null;
15
+ if (marker.startsWith("`") && rawInfo.includes("`")) return null;
16
+ return {
17
+ markerChar: marker[0],
18
+ markerLength: marker.length,
19
+ lang: extractCodeFenceLanguage(rawInfo)
20
+ };
21
+ }
22
+ function isCodeFenceClosing(line, fence) {
23
+ const trimmed = line.trim();
24
+ if (trimmed.length < fence.markerLength) return false;
25
+ for (const char of trimmed) if (char !== fence.markerChar) return false;
26
+ return true;
27
+ }
28
+ function extractCodeFenceLanguage(rawInfo) {
29
+ return (rawInfo.trim().split(/\s+/, 1)[0] ?? "").replace(/^\{\.?/, "").replace(/^\./, "").replace(/[},].*$/, "");
30
+ }
31
+ function replaceFencedCodeBlocks(text, codeBlocks, tokenBoundary) {
32
+ const lines = text.split("\n");
33
+ const output = [];
34
+ let i = 0;
35
+ while (i < lines.length) {
36
+ const fence = getCodeFenceOpening(lines[i]);
37
+ if (!fence) {
38
+ output.push(lines[i]);
39
+ i += 1;
40
+ continue;
41
+ }
42
+ const codeLines = [];
43
+ i += 1;
44
+ while (i < lines.length && !isCodeFenceClosing(lines[i], fence)) {
45
+ codeLines.push(lines[i]);
46
+ i += 1;
47
+ }
48
+ if (i < lines.length) i += 1;
49
+ codeBlocks.push(buildCodeBlock(fence.lang, codeLines.join("\n")));
50
+ output.push(`${tokenBoundary}CB${codeBlocks.length - 1}${tokenBoundary}`);
51
+ }
52
+ return output.join("\n");
53
+ }
54
+ function renderAIResponseMarkdown(text) {
55
+ const codeBlocks = [];
56
+ return renderMarkdownBlocks(replaceFencedCodeBlocks(text, codeBlocks, codeBlockTokenBoundary), codeBlocks);
57
+ }
58
+ function renderMarkdownBlocks(text, codeBlocks) {
59
+ const lines = text.split("\n");
60
+ const output = [];
61
+ let i = 0;
62
+ while (i < lines.length) {
63
+ const trimmed = lines[i].trim();
64
+ if (!trimmed) {
65
+ i++;
66
+ continue;
67
+ }
68
+ const codeBlockIndex = getCodeBlockTokenIndex(trimmed);
69
+ if (codeBlockIndex !== null) {
70
+ output.push(codeBlocks[codeBlockIndex] ?? "");
71
+ i++;
72
+ continue;
73
+ }
74
+ if (isHorizontalRule(trimmed)) {
75
+ output.push("<hr class=\"fd-ai-hr\" />");
76
+ i++;
77
+ continue;
78
+ }
79
+ const heading = getHeading(trimmed);
80
+ if (heading) {
81
+ output.push(`<h${heading.level}>${renderInlineMarkdown(heading.text)}</h${heading.level}>`);
82
+ i++;
83
+ continue;
84
+ }
85
+ if (isTableRow(lines[i]) && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
86
+ const tableLines = [lines[i]];
87
+ i++;
88
+ i++;
89
+ while (i < lines.length && isTableRow(lines[i])) {
90
+ tableLines.push(lines[i]);
91
+ i++;
92
+ }
93
+ output.push(renderTable(tableLines));
94
+ continue;
95
+ }
96
+ const unorderedItems = collectListItems(lines, i, "unordered");
97
+ if (unorderedItems) {
98
+ output.push(`<ul>${unorderedItems.items.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join("")}</ul>`);
99
+ i = unorderedItems.nextIndex;
100
+ continue;
101
+ }
102
+ const orderedItems = collectListItems(lines, i, "ordered");
103
+ if (orderedItems) {
104
+ output.push(`<ol>${orderedItems.items.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join("")}</ol>`);
105
+ i = orderedItems.nextIndex;
106
+ continue;
107
+ }
108
+ const paragraphLines = [];
109
+ while (i < lines.length && lines[i].trim() && !isMarkdownBlockStart(lines, i)) {
110
+ paragraphLines.push(lines[i].trim());
111
+ i++;
112
+ }
113
+ output.push(`<p>${paragraphLines.map((paragraphLine) => renderInlineMarkdown(paragraphLine)).join("<br>")}</p>`);
114
+ }
115
+ return output.join("");
116
+ }
117
+ function getCodeBlockTokenIndex(line) {
118
+ const prefix = `${codeBlockTokenBoundary}CB`;
119
+ if (!line.startsWith(prefix) || !line.endsWith(codeBlockTokenBoundary)) return null;
120
+ const rawIndex = line.slice(prefix.length, -1);
121
+ if (!/^\d+$/.test(rawIndex)) return null;
122
+ return Number(rawIndex);
123
+ }
124
+ function getHeading(line) {
125
+ const match = /^(#{1,4})\s+(.+)$/.exec(line);
126
+ if (!match) return null;
127
+ return {
128
+ level: match[1].length + 1,
129
+ text: match[2]
130
+ };
131
+ }
132
+ function collectListItems(lines, startIndex, type) {
133
+ const pattern = type === "ordered" ? /^(?: {0,3})\d+\.\s+(.+)$/ : /^(?: {0,3})[-*+]\s+(.+)$/;
134
+ const items = [];
135
+ let i = startIndex;
136
+ while (i < lines.length) {
137
+ const match = pattern.exec(lines[i]);
138
+ if (!match) break;
139
+ items.push(match[1]);
140
+ i++;
141
+ }
142
+ return items.length > 0 ? {
143
+ items,
144
+ nextIndex: i
145
+ } : null;
146
+ }
147
+ function isMarkdownBlockStart(lines, index) {
148
+ const line = lines[index];
149
+ const trimmed = line.trim();
150
+ return getCodeBlockTokenIndex(trimmed) !== null || isHorizontalRule(trimmed) || Boolean(getHeading(trimmed)) || isTableRow(line) && index + 1 < lines.length && isTableSeparator(lines[index + 1]) || Boolean(collectListItems(lines, index, "unordered")) || Boolean(collectListItems(lines, index, "ordered"));
151
+ }
152
+ function renderInlineMarkdown(text) {
153
+ const inlineCodeTokens = [];
154
+ let result = escapeHtml(text).replace(/`([^`]+)`/g, (_match, code) => {
155
+ inlineCodeTokens.push(`<code>${code}</code>`);
156
+ return `${codeBlockTokenBoundary}IC${inlineCodeTokens.length - 1}${codeBlockTokenBoundary}`;
157
+ });
158
+ result = result.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>").replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "<em>$1</em>").replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, href) => {
159
+ return `<a href="${escapeAttribute(href)}">${label}</a>`;
160
+ });
161
+ return result.replace(new RegExp(`${codeBlockTokenBoundary}IC(\\d+)${codeBlockTokenBoundary}`, "g"), (_match, idx) => inlineCodeTokens[Number(idx)] ?? "");
162
+ }
163
+ function escapeHtml(s) {
164
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
165
+ }
166
+ function escapeAttribute(s) {
167
+ return escapeHtml(s).replace(/"/g, "&quot;");
168
+ }
169
+ function isHorizontalRule(line) {
170
+ return /^(?:-{3,}|\*{3,}|_{3,})$/.test(line);
171
+ }
172
+ function isTableRow(line) {
173
+ const trimmed = line.trim();
174
+ return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.includes("|");
175
+ }
176
+ function isTableSeparator(line) {
177
+ return /^\|[\s:]*-+[\s:]*(\|[\s:]*-+[\s:]*)*\|$/.test(line.trim());
178
+ }
179
+ function renderTable(rows) {
180
+ const parseRow = (row) => row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
181
+ return `<table>${`<thead><tr>${parseRow(rows[0]).map((c) => `<th>${renderInlineMarkdown(c)}</th>`).join("")}</tr></thead>`}<tbody>${rows.slice(1).map((row) => {
182
+ return `<tr>${parseRow(row).map((c) => `<td>${renderInlineMarkdown(c)}</td>`).join("")}</tr>`;
183
+ }).join("")}</tbody></table>`;
184
+ }
185
+
186
+ //#endregion
187
+ export { renderAIResponseMarkdown };
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
 
3
3
  import { emitClientAnalyticsEvent } from "./client-analytics.mjs";
4
+ import { renderAIResponseMarkdown } from "./ai-markdown.mjs";
4
5
  import { useCallback, useEffect, useRef, useState } from "react";
5
6
  import { createPortal } from "react-dom";
6
7
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
- import { highlight } from "sugar-high";
8
8
 
9
9
  //#region src/ai-search-dialog.tsx
10
10
  /**
@@ -282,61 +282,6 @@ async function copyTextToClipboard(text) {
282
282
  return fallbackCopyText(text);
283
283
  }
284
284
  }
285
- function buildCodeBlock(lang, code) {
286
- const highlighted = highlight(code.replace(/\n$/, "")).replace(/<\/span>\n<span/g, "</span><span");
287
- return `<div class="fd-ai-code-block"><div class="fd-ai-code-header">${lang ? `<div class="fd-ai-code-lang">${escapeHtml(lang)}</div>` : ""}<button class="fd-ai-code-copy" onclick="(function(btn){var code=btn.closest('.fd-ai-code-block').querySelector('code').textContent;navigator.clipboard.writeText(code).then(function(){btn.textContent='Copied!';setTimeout(function(){btn.textContent='Copy'},1500)})})(this)">Copy</button></div><pre><code>${highlighted}</code></pre></div>`;
288
- }
289
- function renderMarkdown(text) {
290
- const codeBlockTokenBoundary = String.fromCharCode(0);
291
- const codeBlockTokenPattern = new RegExp(`${codeBlockTokenBoundary}CB(\\d+)${codeBlockTokenBoundary}`, "g");
292
- const codeBlocks = [];
293
- let processed = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
294
- codeBlocks.push(buildCodeBlock(lang, code));
295
- return `\x00CB${codeBlocks.length - 1}\x00`;
296
- });
297
- processed = processed.replace(/```(\w*)\n([\s\S]*)$/, (_match, lang, code) => {
298
- codeBlocks.push(buildCodeBlock(lang, code));
299
- return `\x00CB${codeBlocks.length - 1}\x00`;
300
- });
301
- const lines = processed.split("\n");
302
- const output = [];
303
- let i = 0;
304
- while (i < lines.length) {
305
- if (isTableRow(lines[i]) && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
306
- const tableLines = [lines[i]];
307
- i++;
308
- i++;
309
- while (i < lines.length && isTableRow(lines[i])) {
310
- tableLines.push(lines[i]);
311
- i++;
312
- }
313
- output.push(renderTable(tableLines));
314
- continue;
315
- }
316
- output.push(lines[i]);
317
- i++;
318
- }
319
- let result = output.join("\n");
320
- result = result.replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>").replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "<em>$1</em>").replace(/\[([^\]]+)\]\(([^)]+)\)/g, "<a href=\"$2\">$1</a>").replace(/^### (.*$)/gm, "<h4>$1</h4>").replace(/^## (.*$)/gm, "<h3>$1</h3>").replace(/^# (.*$)/gm, "<h2>$1</h2>").replace(/^[-*] (.*$)/gm, "<div style=\"display:flex;gap:8px;padding:2px 0\"><span style=\"opacity:0.5\">•</span><span>$1</span></div>").replace(/^(\d+)\. (.*$)/gm, "<div style=\"display:flex;gap:8px;padding:2px 0\"><span style=\"opacity:0.5\">$1.</span><span>$2</span></div>").replace(/\n\n/g, "<div style=\"height:8px\"></div>").replace(/\n/g, "<br>");
321
- result = result.replace(codeBlockTokenPattern, (_m, idx) => codeBlocks[Number(idx)]);
322
- return result;
323
- }
324
- function escapeHtml(s) {
325
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
326
- }
327
- function isTableRow(line) {
328
- const trimmed = line.trim();
329
- return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.includes("|");
330
- }
331
- function isTableSeparator(line) {
332
- return /^\|[\s:]*-+[\s:]*(\|[\s:]*-+[\s:]*)*\|$/.test(line.trim());
333
- }
334
- function renderTable(rows) {
335
- const parseRow = (row) => row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
336
- return `<table>${`<thead><tr>${parseRow(rows[0]).map((c) => `<th>${c}</th>`).join("")}</tr></thead>`}<tbody>${rows.slice(1).map((row) => {
337
- return `<tr>${parseRow(row).map((c) => `<td>${c}</td>`).join("")}</tr>`;
338
- }).join("")}</tbody></table>`;
339
- }
340
285
  function SearchIcon() {
341
286
  return /* @__PURE__ */ jsxs("svg", {
342
287
  width: "16",
@@ -936,7 +881,7 @@ function AIChat({ api, requestMode, requestHeaders, requestStream = true, messag
936
881
  className: "fd-ai-bubble-ai",
937
882
  children: msg.content ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
938
883
  className: isStreaming && i === messages.length - 1 ? "fd-ai-streaming" : void 0,
939
- dangerouslySetInnerHTML: { __html: renderMarkdown(msg.content) }
884
+ dangerouslySetInnerHTML: { __html: renderAIResponseMarkdown(msg.content) }
940
885
  }), feedbackEnabled && !msg.isError && !isStreaming && /* @__PURE__ */ jsx(AIFeedbackControls, {
941
886
  value: msg.feedback,
942
887
  onCopy: () => handleCopyMessage(msg, i),
@@ -1685,7 +1630,7 @@ function FullModalAIChat({ api, requestMode, requestHeaders, requestStream, isOp
1685
1630
  className: "fd-ai-fm-msg-content",
1686
1631
  children: msg.content ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
1687
1632
  className: isStreaming && i === messages.length - 1 ? "fd-ai-streaming" : void 0,
1688
- dangerouslySetInnerHTML: { __html: renderMarkdown(msg.content) }
1633
+ dangerouslySetInnerHTML: { __html: renderAIResponseMarkdown(msg.content) }
1689
1634
  }), msg.role === "assistant" && feedbackEnabled && !msg.isError && !isStreaming && /* @__PURE__ */ jsx(AIFeedbackControls, {
1690
1635
  value: msg.feedback,
1691
1636
  onCopy: () => handleCopyMessage(msg, i),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/theme",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
5
5
  "keywords": [
6
6
  "docs",
@@ -145,7 +145,7 @@
145
145
  "tsdown": "^0.20.3",
146
146
  "typescript": "^5.9.3",
147
147
  "vitest": "^4.1.8",
148
- "@farming-labs/docs": "0.2.2"
148
+ "@farming-labs/docs": "0.2.4"
149
149
  },
150
150
  "peerDependencies": {
151
151
  "@farming-labs/docs": ">=0.0.1",
package/styles/ai.css CHANGED
@@ -924,6 +924,35 @@
924
924
  color: inherit;
925
925
  }
926
926
 
927
+ .fd-ai-bubble-ai h5 {
928
+ font-size: 0.875rem;
929
+ font-weight: 600;
930
+ margin: 8px 0 4px;
931
+ line-height: 1.4;
932
+ color: inherit;
933
+ }
934
+
935
+ .fd-ai-bubble-ai ul,
936
+ .fd-ai-bubble-ai ol {
937
+ margin: 6px 0 8px;
938
+ padding-left: 1.25rem;
939
+ }
940
+
941
+ .fd-ai-bubble-ai li {
942
+ margin: 3px 0;
943
+ padding-left: 2px;
944
+ }
945
+
946
+ .fd-ai-bubble-ai li::marker {
947
+ color: var(--color-fd-muted-foreground, #71717a);
948
+ }
949
+
950
+ .fd-ai-bubble-ai .fd-ai-hr {
951
+ border: 0;
952
+ border-top: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.1));
953
+ margin: 12px 0;
954
+ }
955
+
927
956
  .fd-ai-bubble-ai strong {
928
957
  font-weight: 600;
929
958
  color: var(--color-fd-foreground, #e4e4e7);
@@ -1251,6 +1280,35 @@
1251
1280
  color: var(--color-fd-foreground, #e4e4e7);
1252
1281
  }
1253
1282
 
1283
+ .fd-ai-fm-msg-content h5 {
1284
+ font-size: 0.9375rem;
1285
+ font-weight: 600;
1286
+ margin: 10px 0 4px;
1287
+ line-height: 1.4;
1288
+ color: var(--color-fd-foreground, #e4e4e7);
1289
+ }
1290
+
1291
+ .fd-ai-fm-msg-content ul,
1292
+ .fd-ai-fm-msg-content ol {
1293
+ margin: 8px 0 10px;
1294
+ padding-left: 1.35rem;
1295
+ }
1296
+
1297
+ .fd-ai-fm-msg-content li {
1298
+ margin: 4px 0;
1299
+ padding-left: 2px;
1300
+ }
1301
+
1302
+ .fd-ai-fm-msg-content li::marker {
1303
+ color: var(--color-fd-muted-foreground, #71717a);
1304
+ }
1305
+
1306
+ .fd-ai-fm-msg-content .fd-ai-hr {
1307
+ border: 0;
1308
+ border-top: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.1));
1309
+ margin: 16px 0;
1310
+ }
1311
+
1254
1312
  .fd-ai-fm-msg-content strong {
1255
1313
  font-weight: 600;
1256
1314
  color: var(--color-fd-foreground, #e4e4e7);