Skip to content

Commit f2ea1ec

Browse files
committed
1
1 parent bfce896 commit f2ea1ec

6 files changed

Lines changed: 38 additions & 27 deletions

File tree

path/src/git.ps1

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,16 +78,16 @@ function script:git_fetch_remote_branch($Branch) {
7878
# Return PR number if target names a GitHub pull request (pr/N, pull/N, #N, or github.com/…/pull/N URL); else $null.
7979
function script:git_parse_pr_number($Target) {
8080
if ([string]::IsNullOrEmpty($Target)) { return $null }
81-
if ($Target -match '^(?i)pr/(\d+)$') { return $Matches[1] }
82-
if ($Target -match '^(?i)pull/(\d+)$') { return $Matches[1] }
83-
if ($Target -match '^#(\d+)$') { return $Matches[1] }
84-
if ($Target -match '^https?://github\.com/[^/]+/[^/]+/pull/(\d+)(?:[/?#].*)?$') { return $Matches[1] }
81+
if ($Target -match '^(?i)pr/([0-9]+)$') { return $Matches[1] }
82+
if ($Target -match '^(?i)pull/([0-9]+)$') { return $Matches[1] }
83+
if ($Target -match '^#([0-9]+)$') { return $Matches[1] }
84+
if ($Target -match '^https?://github\.com/[^/]+/[^/]+/pull/([0-9]+)(?:[/?#].*)?$') { return $Matches[1] }
8585
return $null
8686
}
8787

8888
# One-shot map of GitHub pull/<n>/head into origin/pr/<n> (does not widen remote.origin.fetch).
8989
function script:git_fetch_pull_request($Pr) {
90-
if ($Pr -notmatch '^\d+$') {
90+
if ($Pr -notmatch '^[0-9]+$') {
9191
$global:LastExitCode = 1
9292
return
9393
}

path/test/git.test.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ const INVALID_PR_TARGETS = [
261261
'pr/0x1',
262262
'pr/12/3',
263263
'pr/12a',
264+
'pr/١', // Unicode digit — must stay rejected (ASCII [0-9] only)
264265
'pull/',
265266
'#',
266267
'#abc',

src/public/pages/scripts/features/embedCard.mjs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { escapeHtml } from '../lib/escapeHtml.mjs'
22
import { memoizePromise } from '../lib/memo.mjs'
3+
import { isSafeHtmlUrl } from '../lib/sanitizeHtml.mjs'
34

45
const TITLE_DISPLAY_MAX = 120
56
const DESC_DISPLAY_MAX = 200
@@ -98,14 +99,15 @@ export const unfurl = memoizePromise(url => url, fetchUnfurl, { max: 128, ttlMs:
9899
export 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) {
132134
export 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) {
192195
function 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

198201
let observerStarted = false

src/public/parts/shells/chat/public/src/groupMode.mjs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,11 @@ export function attachOffscreenEmbedGuard(root) {
2424

2525
const observer = new IntersectionObserver(
2626
(entries) => {
27-
for (const entry of entries) {
27+
for (const entry of entries)
2828
if (entry.isIntersecting)
2929
entry.target.querySelectorAll('iframe[data-suspended-src],video[data-suspended-src]').forEach(resume)
3030
else
3131
entry.target.querySelectorAll('iframe[src],video[src]').forEach(suspend)
32-
}
3332
},
3433
{ root: null, rootMargin: '120px 0px', threshold: 0 },
3534
)
@@ -45,13 +44,23 @@ export function attachOffscreenEmbedGuard(root) {
4544
*/
4645
export function attachUntrustedMarkdownOffscreenGuard(bubble, { onReveal }) {
4746
const observer = new IntersectionObserver(
48-
(entries) => {
47+
async (entries) => {
4948
for (const entry of entries) {
5049
if (entry.isIntersecting) continue
51-
if (bubble.dataset.mdHydrated !== '1' || bubble.querySelector('.markdown-reveal-button')) continue
50+
if (
51+
bubble.dataset.mdHydrated !== '1'
52+
|| bubble.dataset.mdMounting === '1'
53+
|| bubble.querySelector('.markdown-reveal-button')
54+
) continue
55+
bubble.dataset.mdMounting = '1'
5256
bubble.dataset.mdStash = bubble.innerHTML
5357
bubble.replaceChildren()
54-
void mountMdRevealButton(bubble, onReveal)
58+
try {
59+
await mountMdRevealButton(bubble, onReveal)
60+
}
61+
finally {
62+
delete bubble.dataset.mdMounting
63+
}
5564
}
5665
},
5766
{ root: null, rootMargin: '80px 0px', threshold: 0 },

src/scripts/test/node/launch.mjs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,6 @@ async function launchNodeOnce(options = {}) {
548548

549549
await finishListenHold()
550550

551-
const denoBin = Deno.execPath()
552551
let captureEnabled = false
553552
let startupOutput = ''
554553
let capturedOutput = ''
@@ -568,7 +567,7 @@ async function launchNodeOnce(options = {}) {
568567
}
569568

570569
// stderr 始终 pipe:否则 EADDRINUSE 走 inherit 进不了 startupOutput,换口重试无法识别。
571-
child = spawn(denoBin, workerArgs, {
570+
child = spawn(Deno.execPath(), workerArgs, {
572571
cwd: REPO_ROOT,
573572
stdio: ['ignore', 'pipe', 'pipe'],
574573
env: {

src/scripts/test/schedule_heap_snapshot_analysis.mjs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ const ANALYZE_SCRIPT = join(dirname(fileURLToPath(import.meta.url)), 'tools/anal
1212
* @param {string} snapshotPath 快照绝对路径
1313
*/
1414
export function scheduleHeapSnapshotAnalysis(snapshotPath) {
15-
const deno = Deno.execPath()
1615
const reportPath = `${snapshotPath}.analysis.txt`
17-
spawn(deno, [
16+
spawn(Deno.execPath(), [
1817
'run', '--allow-read', '--allow-write',
1918
'--v8-flags=--max-old-space-size=12288',
2019
'-c', join(REPO_ROOT, 'deno.json'),

0 commit comments

Comments
 (0)