bolt.diy/app/utils/markdown.ts
Anirban Kar a199295ad8
Some checks are pending
Docker Publish / docker-build-publish (push) Waiting to run
Update Stable Branch / prepare-release (push) Waiting to run
feat: support for <think></think> tags to allow reasoning tokens formatted in UI (#1205)
2025-01-29 02:33:53 +05:30

145 lines
2.8 KiB
TypeScript

import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import type { PluggableList, Plugin } from 'unified';
import rehypeSanitize, { defaultSchema, type Options as RehypeSanitizeOptions } from 'rehype-sanitize';
import { SKIP, visit } from 'unist-util-visit';
import type { UnistNode, UnistParent } from 'node_modules/unist-util-visit/lib';
export const allowedHTMLElements = [
'a',
'b',
'blockquote',
'br',
'code',
'dd',
'del',
'details',
'div',
'dl',
'dt',
'em',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'hr',
'i',
'ins',
'kbd',
'li',
'ol',
'p',
'pre',
'q',
'rp',
'rt',
'ruby',
's',
'samp',
'source',
'span',
'strike',
'strong',
'sub',
'summary',
'sup',
'table',
'tbody',
'td',
'tfoot',
'th',
'thead',
'tr',
'ul',
'var',
'think',
];
// Add custom rehype plugin
function remarkThinkRawContent() {
return (tree: any) => {
visit(tree, (node: any) => {
if (node.type === 'html' && node.value && node.value.startsWith('<think>')) {
const cleanedContent = node.value.slice(7);
node.value = `<div class="__boltThought__">${cleanedContent}`;
return;
}
if (node.type === 'html' && node.value && node.value.startsWith('</think>')) {
const cleanedContent = node.value.slice(8);
node.value = `</div>${cleanedContent}`;
}
});
};
}
const rehypeSanitizeOptions: RehypeSanitizeOptions = {
...defaultSchema,
tagNames: allowedHTMLElements,
attributes: {
...defaultSchema.attributes,
div: [
...(defaultSchema.attributes?.div ?? []),
'data*',
['className', '__boltArtifact__', '__boltThought__'],
// ['className', '__boltThought__']
],
},
strip: [],
};
export function remarkPlugins(limitedMarkdown: boolean) {
const plugins: PluggableList = [remarkGfm];
if (limitedMarkdown) {
plugins.unshift(limitedMarkdownPlugin);
}
plugins.unshift(remarkThinkRawContent);
return plugins;
}
export function rehypePlugins(html: boolean) {
const plugins: PluggableList = [];
if (html) {
plugins.push(rehypeRaw, [rehypeSanitize, rehypeSanitizeOptions]);
}
return plugins;
}
const limitedMarkdownPlugin: Plugin = () => {
return (tree, file) => {
const contents = file.toString();
visit(tree, (node: UnistNode, index, parent: UnistParent) => {
if (
index == null ||
['paragraph', 'text', 'inlineCode', 'code', 'strong', 'emphasis'].includes(node.type) ||
!node.position
) {
return true;
}
let value = contents.slice(node.position.start.offset, node.position.end.offset);
if (node.type === 'heading') {
value = `\n${value}`;
}
parent.children[index] = {
type: 'text',
value,
} as any;
return [SKIP, index] as const;
});
};
};