Skip to content

Commit 1e35120

Browse files
Reverted to edit 8b3959ef-92d3-4cfe-a9ed-1d26d2244f9d: "feat: add dynamic page titles based on selected conversation"
1 parent f544d3c commit 1e35120

File tree

1 file changed

+106
-61
lines changed

1 file changed

+106
-61
lines changed

src/utils/markdownUtils.ts

Lines changed: 106 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,120 @@
11
import { marked } from "marked";
2-
import hljs from "highlight.js";
32
import { markedHighlight } from "marked-highlight";
4-
import type { Link } from "marked";
5-
6-
marked.use(markedHighlight({
7-
highlight: (code, lang) => {
8-
if (lang && hljs.getLanguage(lang)) {
9-
try {
10-
return hljs.highlight(code, { language: lang }).value;
11-
} catch (err) {
12-
console.error("Error highlighting code:", err);
13-
}
14-
}
15-
return code; // Use the original code if language isn't found
16-
}
17-
}));
18-
19-
const renderer = new marked.Renderer();
20-
21-
// Store the original link renderer
22-
const originalLinkRenderer = renderer.link.bind(renderer);
23-
24-
// Customize the link renderer to add icons
25-
renderer.link = ({ href, title, text }: Link) => {
26-
if (!href) return text;
27-
28-
const linkHtml = originalLinkRenderer({ href, title, text });
29-
30-
let iconSvg = '';
31-
32-
if (href.includes('github.com')) {
33-
// GitHub icon
34-
iconSvg = '<svg class="inline-block ml-1 w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>';
35-
} else if (href.includes('wikipedia.org')) {
36-
// Simple "W" icon for Wikipedia
37-
iconSvg = '<span class="inline-flex items-center justify-center ml-1 w-4 h-4 text-xs font-bold bg-gray-200 dark:bg-gray-700 rounded-full">W</span>';
38-
} else {
39-
// Generic external link icon for other links
40-
iconSvg = '<svg class="inline-block ml-1 w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>';
3+
import hljs from "highlight.js";
4+
5+
marked.setOptions({
6+
gfm: true,
7+
breaks: true,
8+
silent: true,
9+
});
10+
11+
marked.use(
12+
markedHighlight({
13+
langPrefix: "hljs language-",
14+
highlight(code, lang, info) {
15+
lang = info.split(".").pop() || lang;
16+
if (lang == "shell") lang = "bash";
17+
if (lang == "result") lang = "markdown";
18+
const language = hljs.getLanguage(lang) ? lang : "plaintext";
19+
return hljs.highlight(code, { language }).value;
20+
},
21+
})
22+
);
23+
24+
export function processNestedCodeBlocks(content: string) {
25+
// If no code blocks or only one code block, return as-is
26+
if (content.split('```').length < 3) {
27+
const match = content.match(/```(\S*)/);
28+
return {
29+
processedContent: content,
30+
langtags: match ? [match[1]] : []
31+
};
4132
}
42-
43-
// Insert the icon after the link
44-
return linkHtml.slice(0, -4) + iconSvg + '</a>';
45-
};
4633

47-
export function processNestedCodeBlocks(content: string): { processedContent: string; langtags: string[] } {
34+
const lines = content.split('\n');
4835
const langtags: string[] = [];
49-
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
50-
let match;
51-
let lastIndex = 0;
52-
let processedContent = '';
53-
54-
while ((match = codeBlockRegex.exec(content)) !== null) {
55-
const [fullMatch, lang] = match;
56-
if (lang) langtags.push(lang);
57-
processedContent += content.slice(lastIndex, match.index) + fullMatch;
58-
lastIndex = match.index + fullMatch.length;
36+
const result: string[] = [];
37+
38+
for (let i = 0; i < lines.length; i++) {
39+
const line = lines[i];
40+
const strippedLine = line.trim();
41+
42+
if (strippedLine.startsWith('```')) {
43+
if (strippedLine !== '```') {
44+
// Start of a code block with a language
45+
const lang = strippedLine.slice(3);
46+
langtags.push(lang);
47+
}
48+
result.push(line);
49+
} else {
50+
result.push(line);
51+
}
5952
}
6053

61-
processedContent += content.slice(lastIndex);
62-
return { processedContent, langtags };
54+
return {
55+
processedContent: result.join('\n'),
56+
langtags: langtags.filter(Boolean)
57+
};
6358
}
6459

65-
export function transformThinkingTags(content: string): string {
60+
export function transformThinkingTags(content: string) {
61+
if (content.startsWith('`') && content.endsWith('`')) {
62+
return content;
63+
}
64+
6665
return content.replace(
6766
/<thinking>([\s\S]*?)<\/thinking>/g,
68-
'<details><summary>💭 Thinking</summary>\n\n$1\n\n</details>'
67+
(_match: string, thinkingContent: string) =>
68+
`<details><summary>💭 Thinking</summary>\n\n${thinkingContent}\n\n</details>`
69+
);
70+
}
71+
72+
export function parseMarkdownContent(content: string) {
73+
const processedContent = transformThinkingTags(content);
74+
const { processedContent: transformedContent, langtags } = processNestedCodeBlocks(processedContent);
75+
76+
let parsedResult = marked.parse(transformedContent, {
77+
async: false,
78+
});
79+
80+
parsedResult = parsedResult.replace(
81+
/<pre><code(?:\s+class="([^"]+)")?>([^]*?)<\/code><\/pre>/g,
82+
(_, classes = "", code) => {
83+
const langtag_fallback = ((classes || "").split(" ")[1] || "Code").replace("language-", "");
84+
const langtag = langtags?.shift() || langtag_fallback;
85+
const emoji = getCodeBlockEmoji(langtag);
86+
return `
87+
<details>
88+
<summary>${emoji} ${langtag}</summary>
89+
<pre><code class="${classes}">${code}</code></pre>
90+
</details>
91+
`;
92+
}
6993
);
94+
95+
return parsedResult;
96+
}
97+
98+
function getCodeBlockEmoji(langtag: string): string {
99+
if (isPath(langtag)) return "📄";
100+
if (isTool(langtag)) return "🛠️";
101+
if (isOutput(langtag)) return "📤";
102+
if (isWrite(langtag)) return "📝";
103+
return "💻";
70104
}
71105

72-
export function parseMarkdownContent(content: string): string {
73-
const transformedContent = transformThinkingTags(content);
74-
return marked(transformedContent, { renderer });
75-
}
106+
function isPath(langtag: string): boolean {
107+
return (langtag.includes("/") || langtag.includes("\\") || langtag.includes(".")) && langtag.split(" ").length === 1;
108+
}
109+
110+
function isTool(langtag: string): boolean {
111+
return ["ipython", "shell", "tmux"].includes(langtag.split(" ")[0].toLowerCase());
112+
}
113+
114+
function isOutput(langtag: string): boolean {
115+
return ["stdout", "stderr", "result"].includes(langtag.toLowerCase());
116+
}
117+
118+
function isWrite(langtag: string): boolean {
119+
return ["save", "patch", "append"].includes(langtag.split(" ")[0].toLowerCase());
120+
}

0 commit comments

Comments
 (0)