@farming-labs/theme 0.2.1 → 0.2.3
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.
- package/dist/ai-markdown.mjs +98 -0
- package/dist/ai-search-dialog.mjs +3 -58
- package/dist/docs-api.mjs +1 -1
- package/dist/docs-cloud-ai-client.mjs +10 -7
- package/package.json +2 -2
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { highlight } from "sugar-high";
|
|
2
|
+
|
|
3
|
+
//#region src/ai-markdown.ts
|
|
4
|
+
function buildCodeBlock(lang, code) {
|
|
5
|
+
const highlighted = highlight(code.replace(/\n$/, "")).replace(/<\/span>\n<span/g, "</span><span");
|
|
6
|
+
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>`;
|
|
7
|
+
}
|
|
8
|
+
function getCodeFenceOpening(line) {
|
|
9
|
+
const match = /^(?: {0,3})(`{3,}|~{3,})(.*)$/.exec(line);
|
|
10
|
+
if (!match) return null;
|
|
11
|
+
const marker = match[1];
|
|
12
|
+
const rawInfo = match[2] ?? "";
|
|
13
|
+
if (!marker) return null;
|
|
14
|
+
if (marker.startsWith("`") && rawInfo.includes("`")) return null;
|
|
15
|
+
return {
|
|
16
|
+
markerChar: marker[0],
|
|
17
|
+
markerLength: marker.length,
|
|
18
|
+
lang: extractCodeFenceLanguage(rawInfo)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function isCodeFenceClosing(line, fence) {
|
|
22
|
+
const trimmed = line.trim();
|
|
23
|
+
if (trimmed.length < fence.markerLength) return false;
|
|
24
|
+
for (const char of trimmed) if (char !== fence.markerChar) return false;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
function extractCodeFenceLanguage(rawInfo) {
|
|
28
|
+
return (rawInfo.trim().split(/\s+/, 1)[0] ?? "").replace(/^\{\.?/, "").replace(/^\./, "").replace(/[},].*$/, "");
|
|
29
|
+
}
|
|
30
|
+
function replaceFencedCodeBlocks(text, codeBlocks, tokenBoundary) {
|
|
31
|
+
const lines = text.split("\n");
|
|
32
|
+
const output = [];
|
|
33
|
+
let i = 0;
|
|
34
|
+
while (i < lines.length) {
|
|
35
|
+
const fence = getCodeFenceOpening(lines[i]);
|
|
36
|
+
if (!fence) {
|
|
37
|
+
output.push(lines[i]);
|
|
38
|
+
i += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const codeLines = [];
|
|
42
|
+
i += 1;
|
|
43
|
+
while (i < lines.length && !isCodeFenceClosing(lines[i], fence)) {
|
|
44
|
+
codeLines.push(lines[i]);
|
|
45
|
+
i += 1;
|
|
46
|
+
}
|
|
47
|
+
if (i < lines.length) i += 1;
|
|
48
|
+
codeBlocks.push(buildCodeBlock(fence.lang, codeLines.join("\n")));
|
|
49
|
+
output.push(`${tokenBoundary}CB${codeBlocks.length - 1}${tokenBoundary}`);
|
|
50
|
+
}
|
|
51
|
+
return output.join("\n");
|
|
52
|
+
}
|
|
53
|
+
function renderAIResponseMarkdown(text) {
|
|
54
|
+
const codeBlockTokenBoundary = String.fromCharCode(0);
|
|
55
|
+
const codeBlockTokenPattern = new RegExp(`${codeBlockTokenBoundary}CB(\\d+)${codeBlockTokenBoundary}`, "g");
|
|
56
|
+
const codeBlocks = [];
|
|
57
|
+
const lines = replaceFencedCodeBlocks(text, codeBlocks, codeBlockTokenBoundary).split("\n");
|
|
58
|
+
const output = [];
|
|
59
|
+
let i = 0;
|
|
60
|
+
while (i < lines.length) {
|
|
61
|
+
if (isTableRow(lines[i]) && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
|
|
62
|
+
const tableLines = [lines[i]];
|
|
63
|
+
i++;
|
|
64
|
+
i++;
|
|
65
|
+
while (i < lines.length && isTableRow(lines[i])) {
|
|
66
|
+
tableLines.push(lines[i]);
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
output.push(renderTable(tableLines));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
output.push(lines[i]);
|
|
73
|
+
i++;
|
|
74
|
+
}
|
|
75
|
+
let result = output.join("\n");
|
|
76
|
+
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>");
|
|
77
|
+
result = result.replace(codeBlockTokenPattern, (_m, idx) => codeBlocks[Number(idx)]);
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
function escapeHtml(s) {
|
|
81
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
82
|
+
}
|
|
83
|
+
function isTableRow(line) {
|
|
84
|
+
const trimmed = line.trim();
|
|
85
|
+
return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.includes("|");
|
|
86
|
+
}
|
|
87
|
+
function isTableSeparator(line) {
|
|
88
|
+
return /^\|[\s:]*-+[\s:]*(\|[\s:]*-+[\s:]*)*\|$/.test(line.trim());
|
|
89
|
+
}
|
|
90
|
+
function renderTable(rows) {
|
|
91
|
+
const parseRow = (row) => row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
92
|
+
return `<table>${`<thead><tr>${parseRow(rows[0]).map((c) => `<th>${c}</th>`).join("")}</tr></thead>`}<tbody>${rows.slice(1).map((row) => {
|
|
93
|
+
return `<tr>${parseRow(row).map((c) => `<td>${c}</td>`).join("")}</tr>`;
|
|
94
|
+
}).join("")}</tbody></table>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
//#endregion
|
|
98
|
+
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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:
|
|
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:
|
|
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/dist/docs-api.mjs
CHANGED
|
@@ -35,7 +35,7 @@ const FILE_EXTS = [
|
|
|
35
35
|
"js"
|
|
36
36
|
];
|
|
37
37
|
const DEFAULT_DOCS_API_ROUTE = "/api/docs";
|
|
38
|
-
const DEFAULT_DOCS_CLOUD_API_BASE_URL = "https://
|
|
38
|
+
const DEFAULT_DOCS_CLOUD_API_BASE_URL = "https://api.farming-labs.dev";
|
|
39
39
|
const DEFAULT_DOCS_CLOUD_API_KEY_ENV = "DOCS_CLOUD_API_KEY";
|
|
40
40
|
const DEFAULT_AGENT_SPEC_ROUTE = "/api/docs/agent/spec";
|
|
41
41
|
const DEFAULT_AGENT_SPEC_WELL_KNOWN_ROUTE = "/.well-known/agent";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//#region src/docs-cloud-ai-client.ts
|
|
2
2
|
const DEFAULT_DOCS_API_ROUTE = "/api/docs";
|
|
3
|
-
const DEFAULT_DOCS_CLOUD_API_BASE_URL = "https://
|
|
3
|
+
const DEFAULT_DOCS_CLOUD_API_BASE_URL = "https://api.farming-labs.dev";
|
|
4
|
+
const DEFAULT_PUBLIC_DOCS_CLOUD_PROJECT_ID_ENV = "NEXT_PUBLIC_DOCS_CLOUD_PROJECT_ID";
|
|
4
5
|
const DEFAULT_PUBLIC_DOCS_CLOUD_API_KEY_ENV = "NEXT_PUBLIC_DOCS_CLOUD_API_KEY";
|
|
5
6
|
function readRuntimeEnv(name) {
|
|
6
7
|
const value = process.env[name]?.trim();
|
|
@@ -10,13 +11,15 @@ function resolveDocsCloudApiBaseUrl() {
|
|
|
10
11
|
return (readRuntimeEnv("NEXT_PUBLIC_DOCS_CLOUD_URL") ?? readRuntimeEnv("DOCS_CLOUD_API_URL") ?? DEFAULT_DOCS_CLOUD_API_BASE_URL).replace(/\/+$/, "");
|
|
11
12
|
}
|
|
12
13
|
function resolveDocsCloudProjectId() {
|
|
13
|
-
return readRuntimeEnv(
|
|
14
|
+
return readRuntimeEnv(DEFAULT_PUBLIC_DOCS_CLOUD_PROJECT_ID_ENV);
|
|
14
15
|
}
|
|
15
|
-
function
|
|
16
|
+
function resolveDocsCloudApiKey(config) {
|
|
16
17
|
const configuredEnv = config.cloud?.apiKey?.env?.trim();
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
if (configuredEnv && isPublicRuntimeEnvName(configuredEnv)) return readRuntimeEnv(configuredEnv);
|
|
19
|
+
return readRuntimeEnv(DEFAULT_PUBLIC_DOCS_CLOUD_API_KEY_ENV);
|
|
20
|
+
}
|
|
21
|
+
function isPublicRuntimeEnvName(name) {
|
|
22
|
+
return name.startsWith("NEXT_PUBLIC_");
|
|
20
23
|
}
|
|
21
24
|
function resolveDocsCloudAIStream(config) {
|
|
22
25
|
const aiConfig = config.ai;
|
|
@@ -26,7 +29,7 @@ function resolveDocsCloudAIStream(config) {
|
|
|
26
29
|
function resolveDocsCloudAIClientRequest(config, fallbackApi = DEFAULT_DOCS_API_ROUTE) {
|
|
27
30
|
if (config.ai?.provider !== "docs-cloud") return { api: fallbackApi };
|
|
28
31
|
const projectId = resolveDocsCloudProjectId();
|
|
29
|
-
const apiKey =
|
|
32
|
+
const apiKey = resolveDocsCloudApiKey(config);
|
|
30
33
|
const requestStream = resolveDocsCloudAIStream(config);
|
|
31
34
|
if (!projectId || !apiKey) return {
|
|
32
35
|
api: fallbackApi,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farming-labs/theme",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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.
|
|
148
|
+
"@farming-labs/docs": "0.2.3"
|
|
149
149
|
},
|
|
150
150
|
"peerDependencies": {
|
|
151
151
|
"@farming-labs/docs": ">=0.0.1",
|