|
| 1 | +import { Marked } from "marked" |
| 2 | +import hljs from "highlight.js" |
| 3 | + |
| 4 | +export const marked = (() => { |
| 5 | + const aliases = { |
| 6 | + vue: 'html', |
| 7 | + } |
| 8 | + |
| 9 | + const ret = new Marked( |
| 10 | + markedHighlight({ |
| 11 | + langPrefix: 'hljs language-', |
| 12 | + highlight(code, lang, info) { |
| 13 | + if (aliases[lang]) { |
| 14 | + lang = aliases[lang] |
| 15 | + } |
| 16 | + const language = hljs.getLanguage(lang) ? lang : 'plaintext' |
| 17 | + return hljs.highlight(code, { language }).value |
| 18 | + } |
| 19 | + }) |
| 20 | + ) |
| 21 | + ret.use({ extensions:[thinkTag()] }) |
| 22 | + //ret.use({ extensions: [divExtension()] }) |
| 23 | + return ret |
| 24 | +})(); |
| 25 | + |
| 26 | +export function renderMarkdown(content) { |
| 27 | + if (content) { |
| 28 | + content = content |
| 29 | + .replaceAll(`\\[ \\boxed{`,'\n<span class="inline-block text-xl text-blue-500 bg-blue-50 dark:text-blue-400 dark:bg-blue-950 px-3 py-1 rounded">') |
| 30 | + .replaceAll('} \\]','</span>\n') |
| 31 | + } |
| 32 | + return marked.parse(content) |
| 33 | +} |
| 34 | + |
| 35 | +// export async function renderMarkdown(body) { |
| 36 | +// const rawHtml = marked.parse(body) |
| 37 | +// return <main dangerouslySetInnerHTML={{ __html: rawHtml }} /> |
| 38 | +// } |
| 39 | + |
| 40 | +export function markedHighlight(options) { |
| 41 | + if (typeof options === 'function') { |
| 42 | + options = { |
| 43 | + highlight: options |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + if (!options || typeof options.highlight !== 'function') { |
| 48 | + throw new Error('Must provide highlight function') |
| 49 | + } |
| 50 | + |
| 51 | + if (typeof options.langPrefix !== 'string') { |
| 52 | + options.langPrefix = 'language-' |
| 53 | + } |
| 54 | + |
| 55 | + return { |
| 56 | + async: !!options.async, |
| 57 | + walkTokens(token) { |
| 58 | + if (token.type !== 'code') { |
| 59 | + return |
| 60 | + } |
| 61 | + |
| 62 | + const lang = getLang(token.lang) |
| 63 | + |
| 64 | + if (options.async) { |
| 65 | + return Promise.resolve(options.highlight(token.text, lang, token.lang || '')).then(updateToken(token)) |
| 66 | + } |
| 67 | + |
| 68 | + const code = options.highlight(token.text, lang, token.lang || '') |
| 69 | + if (code instanceof Promise) { |
| 70 | + throw new Error('markedHighlight is not set to async but the highlight function is async. Set the async option to true on markedHighlight to await the async highlight function.') |
| 71 | + } |
| 72 | + updateToken(token)(code) |
| 73 | + }, |
| 74 | + renderer: { |
| 75 | + code(code, infoString) { |
| 76 | + const lang = getLang(infoString) |
| 77 | + let text = code.text |
| 78 | + const classAttr = lang |
| 79 | + ? ` class="${options.langPrefix}${escape(lang)}"` |
| 80 | + : ' class="hljs"'; |
| 81 | + text = text.replace(/\n$/, '') |
| 82 | + return `<pre><code${classAttr}>${code.escaped ? text : escape(text, true)}\n</code></pre>` |
| 83 | + } |
| 84 | + } |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +function getLang(lang) { |
| 89 | + return (lang || '').match(/\S*/)[0] |
| 90 | +} |
| 91 | + |
| 92 | +function updateToken(token) { |
| 93 | + return code => { |
| 94 | + if (typeof code === 'string' && code !== token.text) { |
| 95 | + token.escaped = true |
| 96 | + token.text = code |
| 97 | + } |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +// copied from marked helpers |
| 102 | +const escapeTest = /[&<>"']/ |
| 103 | +const escapeReplace = new RegExp(escapeTest.source, 'g') |
| 104 | +const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/ |
| 105 | +const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g') |
| 106 | +const escapeReplacements = { |
| 107 | + '&': '&', |
| 108 | + '<': '<', |
| 109 | + '>': '>', |
| 110 | + '"': '"', |
| 111 | + "'": ''' |
| 112 | +} |
| 113 | +const getEscapeReplacement = ch => escapeReplacements[ch] |
| 114 | +function escape(html, encode) { |
| 115 | + if (encode) { |
| 116 | + if (escapeTest.test(html)) { |
| 117 | + return html.replace(escapeReplace, getEscapeReplacement) |
| 118 | + } |
| 119 | + } else { |
| 120 | + if (escapeTestNoEncode.test(html)) { |
| 121 | + return html.replace(escapeReplaceNoEncode, getEscapeReplacement) |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | + return html |
| 126 | +} |
| 127 | + |
| 128 | +/** |
| 129 | + * Marked.js extension for rendering <think> tags as expandable, scrollable components |
| 130 | + * using Tailwind CSS |
| 131 | + */ |
| 132 | + |
| 133 | +// Extension for Marked.js to handle <think> tags |
| 134 | +function thinkTag() { |
| 135 | + globalThis.toggleThink = toggleThink |
| 136 | + return ({ |
| 137 | + name: 'thinkTag', |
| 138 | + level: 'block', |
| 139 | + start(src) { |
| 140 | + return src.match(/^<think>/)?.index; |
| 141 | + }, |
| 142 | + tokenizer(src) { |
| 143 | + const rule = /^<think>([\s\S]*?)<\/think>/ |
| 144 | + const match = rule.exec(src) |
| 145 | + if (match) { |
| 146 | + return { |
| 147 | + type: 'thinkTag', |
| 148 | + raw: match[0], |
| 149 | + content: match[1].trim(), |
| 150 | + } |
| 151 | + } |
| 152 | + return undefined |
| 153 | + }, |
| 154 | + renderer(token) { |
| 155 | + // Parse the markdown content inside the think tag |
| 156 | + const parsedContent = marked.parse(token.content) |
| 157 | + |
| 158 | + // Generate a unique ID for this think component |
| 159 | + const uniqueId = 'think-' + Math.random().toString(36).substring(2, 10) |
| 160 | + |
| 161 | + // Create the expandable, scrollable component with Tailwind CSS |
| 162 | + return ` |
| 163 | + <div class="my-4 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm"> |
| 164 | + <button type="button" |
| 165 | + id="${uniqueId}-toggle" |
| 166 | + class="flex justify-between items-center w-full py-2 px-4 text-left text-gray-700 dark:text-gray-300 font-medium hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none" |
| 167 | + onclick="toggleThink('${uniqueId}')"> |
| 168 | + <span>Thinking</span> |
| 169 | + <svg id="${uniqueId}-icon" class="h-5 w-5 text-gray-500 dark:text-gray-400 transform transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| 170 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> |
| 171 | + </svg> |
| 172 | + </button> |
| 173 | + <div |
| 174 | + id="${uniqueId}-content" |
| 175 | + class="hidden overflow-auto max-h-64 px-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900" |
| 176 | + style="max-height:16rem;"> |
| 177 | + ${parsedContent} |
| 178 | + </div> |
| 179 | + </div> |
| 180 | + ` |
| 181 | + } |
| 182 | + }) |
| 183 | +} |
| 184 | + |
| 185 | +// JavaScript function to toggle the visibility of the think content |
| 186 | +function toggleThink(id) { |
| 187 | + const content = document.getElementById(`${id}-content`) |
| 188 | + const icon = document.getElementById(`${id}-icon`) |
| 189 | + |
| 190 | + if (content.classList.contains('hidden')) { |
| 191 | + content.classList.remove('hidden') |
| 192 | + icon.classList.add('rotate-180') |
| 193 | + } else { |
| 194 | + content.classList.add('hidden') |
| 195 | + icon.classList.remove('rotate-180') |
| 196 | + } |
| 197 | +} |
0 commit comments