@farming-labs/theme 0.2.3 → 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.
- package/dist/ai-markdown.mjs +99 -10
- package/package.json +2 -2
- package/styles/ai.css +58 -0
package/dist/ai-markdown.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { highlight } from "sugar-high";
|
|
2
2
|
|
|
3
3
|
//#region src/ai-markdown.ts
|
|
4
|
+
const codeBlockTokenBoundary = String.fromCharCode(0);
|
|
4
5
|
function buildCodeBlock(lang, code) {
|
|
5
6
|
const highlighted = highlight(code.replace(/\n$/, "")).replace(/<\/span>\n<span/g, "</span><span");
|
|
6
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>`;
|
|
@@ -51,13 +52,36 @@ function replaceFencedCodeBlocks(text, codeBlocks, tokenBoundary) {
|
|
|
51
52
|
return output.join("\n");
|
|
52
53
|
}
|
|
53
54
|
function renderAIResponseMarkdown(text) {
|
|
54
|
-
const codeBlockTokenBoundary = String.fromCharCode(0);
|
|
55
|
-
const codeBlockTokenPattern = new RegExp(`${codeBlockTokenBoundary}CB(\\d+)${codeBlockTokenBoundary}`, "g");
|
|
56
55
|
const codeBlocks = [];
|
|
57
|
-
|
|
56
|
+
return renderMarkdownBlocks(replaceFencedCodeBlocks(text, codeBlocks, codeBlockTokenBoundary), codeBlocks);
|
|
57
|
+
}
|
|
58
|
+
function renderMarkdownBlocks(text, codeBlocks) {
|
|
59
|
+
const lines = text.split("\n");
|
|
58
60
|
const output = [];
|
|
59
61
|
let i = 0;
|
|
60
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
|
+
}
|
|
61
85
|
if (isTableRow(lines[i]) && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
|
|
62
86
|
const tableLines = [lines[i]];
|
|
63
87
|
i++;
|
|
@@ -69,17 +93,82 @@ function renderAIResponseMarkdown(text) {
|
|
|
69
93
|
output.push(renderTable(tableLines));
|
|
70
94
|
continue;
|
|
71
95
|
}
|
|
72
|
-
|
|
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]);
|
|
73
140
|
i++;
|
|
74
141
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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)] ?? "");
|
|
79
162
|
}
|
|
80
163
|
function escapeHtml(s) {
|
|
81
164
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
82
165
|
}
|
|
166
|
+
function escapeAttribute(s) {
|
|
167
|
+
return escapeHtml(s).replace(/"/g, """);
|
|
168
|
+
}
|
|
169
|
+
function isHorizontalRule(line) {
|
|
170
|
+
return /^(?:-{3,}|\*{3,}|_{3,})$/.test(line);
|
|
171
|
+
}
|
|
83
172
|
function isTableRow(line) {
|
|
84
173
|
const trimmed = line.trim();
|
|
85
174
|
return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.includes("|");
|
|
@@ -89,8 +178,8 @@ function isTableSeparator(line) {
|
|
|
89
178
|
}
|
|
90
179
|
function renderTable(rows) {
|
|
91
180
|
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>`;
|
|
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>`;
|
|
94
183
|
}).join("")}</tbody></table>`;
|
|
95
184
|
}
|
|
96
185
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farming-labs/theme",
|
|
3
|
-
"version": "0.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.
|
|
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);
|