Compare commits
3 Commits
jtran/json
...
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