|
18 | 18 | </div> |
19 | 19 | </div> |
20 | 20 | </router-link> |
21 | | - <p |
22 | | - v-if="instructors[0].bio" |
23 | | - class="text-p-sm text-ink-gray-7 leading-6 mt-4 line-clamp-3" |
24 | | - > |
25 | | - {{ instructors[0].bio }} |
26 | | - </p> |
| 21 | + <div |
| 22 | + v-if="hasBio(instructors[0].bio)" |
| 23 | + v-html="renderBio(instructors[0].bio)" |
| 24 | + class="ProseMirror prose prose-sm max-w-none text-p-sm text-ink-gray-7 leading-6 mt-4 line-clamp-3" |
| 25 | + ></div> |
27 | 26 | </template> |
28 | 27 |
|
29 | 28 | <template v-else> |
|
39 | 38 | </div> |
40 | 39 | </div> |
41 | 40 | </router-link> |
42 | | - <p |
43 | | - v-if="focused?.bio" |
44 | | - class="text-p-sm text-ink-gray-7 leading-6 mt-4 line-clamp-3" |
45 | | - > |
46 | | - {{ focused.bio }} |
47 | | - </p> |
| 41 | + <div |
| 42 | + v-if="hasBio(focused?.bio)" |
| 43 | + v-html="renderBio(focused?.bio)" |
| 44 | + class="ProseMirror prose prose-sm max-w-none text-p-sm text-ink-gray-7 leading-6 mt-4 line-clamp-3" |
| 45 | + ></div> |
48 | 46 |
|
49 | 47 | <div class="mt-4 pt-4 border-t border-outline-gray-2"> |
50 | 48 | <div |
|
83 | 81 |
|
84 | 82 | <script setup lang="ts"> |
85 | 83 | import { computed, ref, watch } from 'vue' |
| 84 | +import DOMPurify from 'dompurify' |
86 | 85 | import UserAvatar from '@/components/UserAvatar.vue' |
| 86 | +import { decodeEntities, htmlToText } from '@/utils' |
87 | 87 | import type { CourseInstructorInfo } from '@/types/api' |
88 | 88 |
|
89 | 89 | const props = defineProps<{ |
@@ -142,6 +142,30 @@ const headerLabel = computed<string>(() => { |
142 | 142 | return __('Taught by a team of {0}').format(String(n)) |
143 | 143 | }) |
144 | 144 |
|
| 145 | +function hasBio(bio?: string | null): boolean { |
| 146 | + if (!bio) return false |
| 147 | + return htmlToText(bio).trim().length > 0 || /<img\b/i.test(bio) |
| 148 | +} |
| 149 | +
|
| 150 | +function renderBio(bio?: string | null): string { |
| 151 | + return DOMPurify.sanitize(decodeEntities(bio || ''), { |
| 152 | + ALLOWED_TAGS: [ |
| 153 | + 'b', |
| 154 | + 'i', |
| 155 | + 'em', |
| 156 | + 'strong', |
| 157 | + 'a', |
| 158 | + 'p', |
| 159 | + 'br', |
| 160 | + 'ul', |
| 161 | + 'ol', |
| 162 | + 'li', |
| 163 | + 'img', |
| 164 | + ], |
| 165 | + ALLOWED_ATTR: ['href', 'target', 'rel', 'src'], |
| 166 | + }) |
| 167 | +} |
| 168 | +
|
145 | 169 | function profileLink(instructor: CourseInstructorInfo) { |
146 | 170 | return { name: 'Profile', params: { username: instructor.username } } |
147 | 171 | } |
|
0 commit comments