|
130 | 130 |
|
131 | 131 | <script setup> |
132 | 132 | import { computed, ref } from 'vue' |
133 | | -import { marked } from 'marked' |
134 | | -import DOMPurify from 'dompurify' |
135 | 133 | import { XCircle, AlertTriangle, Info, CheckCircle, Circle, ChevronDown } from 'lucide-vue-next' |
| 134 | +import { formatAnnouncementMessage } from '@/utils/markdown' |
136 | 135 |
|
137 | 136 | // Props |
138 | 137 | const props = defineProps({ |
@@ -237,55 +236,6 @@ const getTypeClasses = (type) => { |
237 | 236 | return typeConfigs[type] || typeConfigs.none |
238 | 237 | } |
239 | 238 |
|
240 | | -const escapeHtml = (value) => { |
241 | | - if (value === null || value === undefined) { |
242 | | - return '' |
243 | | - } |
244 | | -
|
245 | | - return String(value) |
246 | | - .replace(/&/g, '&') |
247 | | - .replace(/</g, '<') |
248 | | - .replace(/>/g, '>') |
249 | | - .replace(/"/g, '"') |
250 | | - .replace(/'/g, ''') |
251 | | -} |
252 | | -
|
253 | | -const renderer = new marked.Renderer() |
254 | | -
|
255 | | -renderer.link = (tokenOrHref, title, text) => { |
256 | | - const tokenObject = typeof tokenOrHref === 'object' && tokenOrHref !== null |
257 | | - ? tokenOrHref |
258 | | - : null |
259 | | -
|
260 | | - const href = tokenObject ? tokenObject.href : tokenOrHref |
261 | | - const resolvedTitle = tokenObject ? tokenObject.title : title |
262 | | - const resolvedText = tokenObject ? tokenObject.text : text |
263 | | -
|
264 | | - const url = escapeHtml(href || '') |
265 | | - const titleAttribute = resolvedTitle ? ` title="${escapeHtml(resolvedTitle)}"` : '' |
266 | | - const linkText = resolvedText || '' |
267 | | -
|
268 | | - return `<a href="${url}" target="_blank" rel="noopener noreferrer"${titleAttribute}>${linkText}</a>` |
269 | | -} |
270 | | -
|
271 | | -marked.use({ |
272 | | - renderer, |
273 | | - breaks: true, |
274 | | - gfm: true, |
275 | | - headerIds: false, |
276 | | - mangle: false |
277 | | -}) |
278 | | -
|
279 | | -const formatAnnouncementMessage = (message) => { |
280 | | - if (!message) { |
281 | | - return '' |
282 | | - } |
283 | | -
|
284 | | - const markdown = String(message) |
285 | | - const html = marked.parse(markdown) |
286 | | - return DOMPurify.sanitize(html, { ADD_ATTR: ['target', 'rel'] }) |
287 | | -} |
288 | | -
|
289 | 239 | const formatDate = (dateString) => { |
290 | 240 | const date = new Date(dateString) |
291 | 241 | const today = new Date() |
@@ -349,22 +299,4 @@ const formatFullTimestamp = (timestamp) => { |
349 | 299 | margin-left: 1.5rem; |
350 | 300 | } |
351 | 301 | } |
352 | | -
|
353 | | -.announcement-content :deep(a) { |
354 | | - color: #1d4ed8; |
355 | | - text-decoration: underline; |
356 | | - font-weight: 500; |
357 | | -} |
358 | | -
|
359 | | -.announcement-content :deep(a:hover) { |
360 | | - color: #1e40af; |
361 | | -} |
362 | | -
|
363 | | -.dark .announcement-content :deep(a) { |
364 | | - color: #60a5fa; |
365 | | -} |
366 | | -
|
367 | | -.dark .announcement-content :deep(a:hover) { |
368 | | - color: #93c5fd; |
369 | | -} |
370 | 302 | </style> |
0 commit comments