11import { escapeHtml } from '../lib/escapeHtml.mjs'
22import { memoizePromise } from '../lib/memo.mjs'
3+ import { isSafeHtmlUrl } from '../lib/sanitizeHtml.mjs'
34
45const TITLE_DISPLAY_MAX = 120
56const DESC_DISPLAY_MAX = 200
@@ -98,14 +99,15 @@ export const unfurl = memoizePromise(url => url, fetchUnfurl, { max: 128, ttlMs:
9899export function renderEmbedCardHtml ( embed ) {
99100 if ( ! embed ?. url ) return ''
100101 const url = String ( embed . url )
102+ if ( ! isSafeHtmlUrl ( url ) ) return ''
101103 const title = truncateText ( embed . title || url , TITLE_DISPLAY_MAX )
102104 const description = truncateText ( embed . description || '' , DESC_DISPLAY_MAX )
103105 const siteName = truncateText ( embed . siteName || '' , 80 )
104106 const image = String ( embed . image || '' ) . trim ( )
105- const imageHtml = image
107+ const imageHtml = image && isSafeHtmlUrl ( image )
106108 ? `<img class="fount-embed-card-thumb" src="${ escapeHtml ( image ) } " alt="" loading="lazy" decoding="async" />`
107109 : ''
108- const noImageClass = image ? '' : ' fount-embed-card-no-image'
110+ const noImageClass = imageHtml ? '' : ' fount-embed-card-no-image'
109111 const descHtml = description
110112 ? `<div class="fount-embed-card-desc">${ escapeHtml ( description ) } </div>`
111113 : ''
@@ -132,6 +134,7 @@ export function renderEmbedCardHtml(embed) {
132134export function renderEmbedChipHtml ( embed ) {
133135 if ( ! embed ?. url ) return ''
134136 const url = String ( embed . url )
137+ if ( ! isSafeHtmlUrl ( url ) ) return ''
135138 let hostname = ''
136139 try { hostname = new URL ( url ) . hostname } catch { /* ignore */ }
137140 const title = truncateText ( embed . title || hostname || url , TITLE_DISPLAY_MAX )
@@ -149,14 +152,14 @@ export function renderEmbedChipHtml(embed) {
149152}
150153
151154/**
152- * @param {HTMLElement } el 占位链接
155+ * @param {HTMLElement } element 占位链接
153156 * @returns {Promise<void> }
154157 */
155- async function hydrateOne ( el ) {
156- const mode = el . getAttribute ( ATTR )
158+ async function hydrateOne ( element ) {
159+ const mode = element . getAttribute ( ATTR )
157160 if ( ! mode ) return
158- el . removeAttribute ( ATTR )
159- const url = el . getAttribute ( 'href' ) || el . href
161+ element . removeAttribute ( ATTR )
162+ const url = element . getAttribute ( 'href' ) || element . href
160163 if ( ! url ) return
161164 let meta
162165 try {
@@ -173,16 +176,16 @@ async function hydrateOne(el) {
173176 wrap . innerHTML = html
174177 const replacement = wrap . firstElementChild
175178 if ( mode === 'card' ) {
176- const parent = el . parentElement
177- if ( parent ?. tagName === 'P' && [ ...parent . childNodes ] . every ( n =>
178- n === el || ( n . nodeType === Node . TEXT_NODE && ! n . textContent ?. trim ( ) ) ,
179+ const parent = element . parentElement
180+ if ( parent ?. tagName === 'P' && [ ...parent . childNodes ] . every ( childNode =>
181+ childNode === element || ( childNode . nodeType === Node . TEXT_NODE && ! childNode . textContent ?. trim ( ) ) ,
179182 ) )
180183 parent . replaceWith ( replacement )
181184 else
182- el . replaceWith ( replacement )
185+ element . replaceWith ( replacement )
183186 return
184187 }
185- el . replaceWith ( replacement )
188+ element . replaceWith ( replacement )
186189}
187190
188191/**
@@ -192,7 +195,7 @@ async function hydrateOne(el) {
192195function hydrateIn ( root ) {
193196 const list = root instanceof Element && root . hasAttribute ( ATTR ) ? [ root ] : [ ]
194197 list . push ( ...root . querySelectorAll ( `[${ ATTR } ]` ) )
195- for ( const el of list ) void hydrateOne ( /** @type {HTMLElement } */ el )
198+ for ( const element of list ) void hydrateOne ( /** @type {HTMLElement } */ element )
196199}
197200
198201let observerStarted = false
0 commit comments