fix: Hook into markdown-generated anchors to avoid e.g breaking the desktop app
This commit is contained in:
		
							
								
								
									
										1
									
								
								interface.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								interface.d.ts
									
									
									
									
										vendored
									
									
								
							@ -84,5 +84,6 @@ export interface IElectronAPI {
 | 
			
		||||
declare global {
 | 
			
		||||
  interface Window {
 | 
			
		||||
    electron: IElectronAPI
 | 
			
		||||
    openExternalLink: (e: React.MouseEvent<HTMLAnchorElement>) => void
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										50
									
								
								src/lib/markdown.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/lib/markdown.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
			
		||||
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)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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 (
 | 
			
		||||
        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
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user