Compare commits
	
		
			3 Commits
		
	
	
		
			main
			...
			pierremtb/
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e82ebece5e | |||
| bf2bbd2ef7 | |||
| c4143bd7de | 
							
								
								
									
										14
									
								
								.github/workflows/build-apps.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/build-apps.yml
									
									
									
									
										vendored
									
									
								
							| @ -5,6 +5,7 @@ on: | |||||||
|   push: |   push: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - pierremtb/fix/hook-into-markdown-generated-anchors | ||||||
|     tags: |     tags: | ||||||
|       - 'v[0-9]+.[0-9]+.[0-9]+' |       - 'v[0-9]+.[0-9]+.[0-9]+' | ||||||
|   schedule: |   schedule: | ||||||
| @ -13,7 +14,8 @@ on: | |||||||
|   # Will checkout the last commit from the default branch (main as of 2023-10-04) |   # Will checkout the last commit from the default branch (main as of 2023-10-04) | ||||||
|  |  | ||||||
| env: | env: | ||||||
|   IS_RELEASE: ${{ github.ref_type == 'tag' }} |   # IS_RELEASE: ${{ github.ref_type == 'tag' }} | ||||||
|  |   IS_RELEASE: true | ||||||
|   IS_NIGHTLY: ${{ github.event_name == 'schedule' }} |   IS_NIGHTLY: ${{ github.event_name == 'schedule' }} | ||||||
|  |  | ||||||
| concurrency: | concurrency: | ||||||
| @ -51,11 +53,11 @@ jobs: | |||||||
|         if: ${{ env.IS_NIGHTLY == 'true' }} |         if: ${{ env.IS_NIGHTLY == 'true' }} | ||||||
|         run: yarn files:flip-to-nightly |         run: yarn files:flip-to-nightly | ||||||
|  |  | ||||||
|       - name: Set release version |       # - name: Set release version | ||||||
|         if: ${{ env.IS_RELEASE == 'true' }} |       #   if: ${{ env.IS_RELEASE == 'true' }} | ||||||
|         run: | |       #   run: | | ||||||
|           export VERSION=${GITHUB_REF_NAME#v} |       #     export VERSION=${GITHUB_REF_NAME#v} | ||||||
|           yarn files:set-version |       #     yarn files:set-version | ||||||
|  |  | ||||||
|       - uses: actions/upload-artifact@v4 |       - uses: actions/upload-artifact@v4 | ||||||
|         with: |         with: | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								interface.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								interface.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -84,5 +84,6 @@ export interface IElectronAPI { | |||||||
| declare global { | declare global { | ||||||
|   interface Window { |   interface Window { | ||||||
|     electron: IElectronAPI |     electron: IElectronAPI | ||||||
|  |     openExternalLink: (e: React.MouseEvent<HTMLAnchorElement>) => void | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -150,4 +150,31 @@ describe('ToastUpdate tests', () => { | |||||||
|     expect(restartButton).toBeEnabled() |     expect(restartButton).toBeEnabled() | ||||||
|     expect(dismissButton).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 toast from 'react-hot-toast' | ||||||
| import { ActionButton } from './ActionButton' | import { ActionButton } from './ActionButton' | ||||||
| import { openExternalBrowserIfDesktop } from 'lib/openWindow' | 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 { getReleaseUrl } from 'routes/Settings' | ||||||
|  | import { SafeRenderer } from 'lib/markdown' | ||||||
|  |  | ||||||
| export function ToastUpdate({ | export function ToastUpdate({ | ||||||
|   version, |   version, | ||||||
| @ -19,6 +20,14 @@ export function ToastUpdate({ | |||||||
|     ?.toLocaleLowerCase() |     ?.toLocaleLowerCase() | ||||||
|     .includes('breaking') |     .includes('breaking') | ||||||
|  |  | ||||||
|  |   const markedOptions: MarkedOptions = { | ||||||
|  |     gfm: true, | ||||||
|  |     breaks: true, | ||||||
|  |     sanitize: true, | ||||||
|  |     unescape, | ||||||
|  |     escape, | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md"> |     <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"> |       <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" |               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={{ |               dangerouslySetInnerHTML={{ | ||||||
|                 __html: Marked.parse(releaseNotes, { |                 __html: Marked.parse(releaseNotes, { | ||||||
|                   gfm: true, |                   renderer: new SafeRenderer(markedOptions), | ||||||
|                   breaks: true, |                   ...markedOptions, | ||||||
|                   sanitize: true, |  | ||||||
|                 }), |                 }), | ||||||
|               }} |               }} | ||||||
|             ></div> |             ></div> | ||||||
|  | |||||||
							
								
								
									
										51
									
								
								src/lib/markdown.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/lib/markdown.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | |||||||
|  | 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 ( | ||||||
|  |         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
	