Open updater toast changelog links externally (#4970)

* fix: Hook into markdown-generated anchors to avoid e.g breaking the desktop app

* add comment

* Disable eslint on copied line from ts-stack

---------

Co-authored-by: marc2332 <mespinsanz@gmail.com>
This commit is contained in:
Pierre Jacquier
2025-01-08 09:15:18 -05:00
committed by GitHub
parent f89d191425
commit 5b1dcfecd6
4 changed files with 92 additions and 4 deletions

1
interface.d.ts vendored
View File

@ -93,5 +93,6 @@ export interface IElectronAPI {
declare global {
interface Window {
electron: IElectronAPI
openExternalLink: (e: React.MouseEvent<HTMLAnchorElement>) => void
}
}

View File

@ -150,4 +150,31 @@ describe('ToastUpdate tests', () => {
expect(restartButton).toBeEnabled()
expect(dismissButton).toBeEnabled()
})
test('Happy path: external links render correctly', () => {
const releaseNotesWithBreakingChanges = `
## Some markdown release notes
- [Zoo](https://zoo.dev/)
`
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={releaseNotesWithBreakingChanges}
/>
)
// Locators and other constants
const zooDev = screen.getByText('Zoo', {
selector: 'a',
})
expect(zooDev).toHaveAttribute('href', 'https://zoo.dev/')
expect(zooDev).toHaveAttribute('target', '_blank')
expect(zooDev).toHaveAttribute('onClick')
})
})

View File

@ -1,8 +1,9 @@
import toast from 'react-hot-toast'
import { ActionButton } from './ActionButton'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { Marked } from '@ts-stack/markdown'
import { escape, Marked, MarkedOptions, unescape } from '@ts-stack/markdown'
import { getReleaseUrl } from 'routes/Settings'
import { SafeRenderer } from 'lib/markdown'
export function ToastUpdate({
version,
@ -19,6 +20,14 @@ export function ToastUpdate({
?.toLocaleLowerCase()
.includes('breaking')
const markedOptions: MarkedOptions = {
gfm: true,
breaks: true,
sanitize: true,
unescape,
escape,
}
return (
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md">
<div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
@ -58,9 +67,8 @@ export function ToastUpdate({
className="parsed-markdown py-2 px-4 mt-2 border-t border-chalkboard-30 dark:border-chalkboard-60 max-h-60 overflow-y-auto"
dangerouslySetInnerHTML={{
__html: Marked.parse(releaseNotes, {
gfm: true,
breaks: true,
sanitize: true,
renderer: new SafeRenderer(markedOptions),
...markedOptions,
}),
}}
></div>

52
src/lib/markdown.ts Normal file
View File

@ -0,0 +1,52 @@
import { MarkedOptions, Renderer, unescape } from '@ts-stack/markdown'
import { openExternalBrowserIfDesktop } from './openWindow'
/**
* Main goal of this custom renderer is to prevent links from changing the current location
* this is specially important for the desktop app.
*/
export class SafeRenderer extends Renderer {
constructor(options: MarkedOptions) {
super(options)
// Attach a global function for non-react anchor elements that need safe navigation
window.openExternalLink = (e: React.MouseEvent<HTMLAnchorElement>) => {
openExternalBrowserIfDesktop()(e)
}
}
// Extended from https://github.com/ts-stack/markdown/blob/c5c1925c1153ca2fe9051c356ef0ddc60b3e1d6a/packages/markdown/src/renderer.ts#L116
link(href: string, title: string, text: string): string {
if (this.options.sanitize) {
let prot: string
try {
prot = decodeURIComponent(unescape(href))
.replace(/[^\w:]/g, '')
.toLowerCase()
} catch (e) {
return text
}
if (
// eslint-disable-next-line no-script-url
prot.indexOf('javascript:') === 0 ||
prot.indexOf('vbscript:') === 0 ||
prot.indexOf('data:') === 0
) {
return text
}
}
let out =
'<a onclick="openExternalLink(event)" target="_blank" href="' + href + '"'
if (title) {
out += ' title="' + title + '"'
}
out += '>' + text + '</a>'
return out
}
}