Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/main/settings-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -911,12 +911,32 @@ function normalizeReleaseNotes(releaseNotes: unknown): string {
}

function stripReleaseNotesBoilerplate(releaseNotes: string): string {
return releaseNotes
.replace(/\r\n?/g, '\n')
const normalized = releaseNotes.replace(/\r\n?/g, '\n')
if (isHtmlReleaseNotes(normalized)) {
return stripHtmlReleaseNotesBoilerplate(normalized)
}

return normalized
.replace(
/<!--\s*hush-release-header:start\s*-->[\s\S]*?<!--\s*hush-release-header:end\s*-->/gi,
''
)
.replace(/^\s*##\s+版本变更\s*\/\s*Changelog\s*\n+/i, '')
.trim()
}

function isHtmlReleaseNotes(releaseNotes: string): boolean {
return /<\/?(?:h[1-6]|p|ul|ol|li|a|code|pre|blockquote|table|thead|tbody|tr|th|td|hr|br|strong|em)\b/i.test(
releaseNotes
)
}

function stripHtmlReleaseNotesBoilerplate(releaseNotes: string): string {
const changelogHeading = /<h[1-6]\b[^>]*>\s*版本变更\s*\/\s*Changelog\s*<\/h[1-6]>/i.exec(
releaseNotes
)
if (changelogHeading) {
return releaseNotes.slice(changelogHeading.index + changelogHeading[0].length).trim()
}
return releaseNotes.trim()
}
118 changes: 116 additions & 2 deletions src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import type { JSX, ReactNode } from 'react'
import type { JSX, MouseEvent as ReactMouseEvent, ReactNode } from 'react'

import {
AlertCircle,
Expand Down Expand Up @@ -1026,7 +1026,7 @@ function AboutTab(props: {
</button>
</header>
<div className="release-notes-body">
<MarkdownContent markdown={releaseNotes || text.about.noReleaseNotes} />
<ReleaseNotesContent content={releaseNotes || text.about.noReleaseNotes} />
</div>
<footer className="dialog-actions">
<button
Expand All @@ -1052,6 +1052,49 @@ type MarkdownBlock =
| { type: 'quote'; content: string }
| { type: 'rule' }

const ALLOWED_RELEASE_NOTES_HTML_TAGS = new Set([
'a',
'blockquote',
'br',
'code',
'em',
'h1',
'h2',
'h3',
'h4',
'hr',
'li',
'ol',
'p',
'pre',
'strong',
'table',
'tbody',
'td',
'th',
'thead',
'tr',
'ul'
])

function ReleaseNotesContent(props: { content: string }): JSX.Element {
if (isReleaseNotesHtml(props.content)) {
return <HtmlContent html={props.content} />
}
return <MarkdownContent markdown={props.content} />
}

function HtmlContent(props: { html: string }): JSX.Element {
const sanitizedHtml = sanitizeReleaseNotesHtml(props.html)
return (
<div
className="markdown-content"
onClick={handleReleaseNotesHtmlClick}
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
/>
)
}

function MarkdownContent(props: { markdown: string }): JSX.Element {
const blocks = parseMarkdownBlocks(props.markdown)

Expand Down Expand Up @@ -1193,6 +1236,77 @@ function parseMarkdownBlocks(markdown: string): MarkdownBlock[] {
return blocks
}

function isReleaseNotesHtml(content: string): boolean {
return /<\/?(?:h[1-6]|p|ul|ol|li|a|code|pre|blockquote|table|thead|tbody|tr|th|td|hr|br|strong|em)\b/i.test(
content
)
}

function sanitizeReleaseNotesHtml(html: string): string {
const parser = new DOMParser()
const source = parser.parseFromString(html, 'text/html')
const target = document.implementation.createHTMLDocument('')
const container = target.createElement('div')

for (const child of Array.from(source.body.childNodes)) {
for (const safeChild of sanitizeReleaseNotesHtmlNode(child, target)) {
container.appendChild(safeChild)
}
}

return container.innerHTML.trim()
}

function sanitizeReleaseNotesHtmlNode(node: Node, target: Document): Node[] {
if (node.nodeType === 3) {
return [target.createTextNode(node.textContent ?? '')]
}
if (node.nodeType !== 1) {
return []
}

const element = node as Element
const tagName = element.tagName.toLowerCase()
const children = Array.from(element.childNodes).flatMap((child) =>
sanitizeReleaseNotesHtmlNode(child, target)
)

if (!ALLOWED_RELEASE_NOTES_HTML_TAGS.has(tagName)) {
return children
}

const safeElement = target.createElement(tagName)
if (tagName === 'a') {
const href = normalizeReleaseNotesHref(element.getAttribute('href') ?? '')
if (!href) {
return children
}
safeElement.setAttribute('href', href)
safeElement.setAttribute('rel', 'noopener noreferrer')
}

children.forEach((child) => safeElement.appendChild(child))
return [safeElement]
}

function handleReleaseNotesHtmlClick(event: ReactMouseEvent<HTMLDivElement>): void {
const target = event.target
if (!(target instanceof Element)) {
return
}

const anchor = target.closest('a')
if (!(anchor instanceof HTMLAnchorElement)) {
return
}

const href = normalizeReleaseNotesHref(anchor.getAttribute('href') ?? '')
event.preventDefault()
if (href) {
void window.api.openExternal(href)
}
}

function renderMarkdownInline(text: string, keyPrefix: string): ReactNode[] {
const nodes: ReactNode[] = []
const tokenPattern = /(\[[^\]]+\]\([^)]+\)|`[^`]+`|\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_)/g
Expand Down
23 changes: 23 additions & 0 deletions src/renderer/src/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -3664,6 +3664,29 @@ button:disabled {
padding-left: 20px;
}

.markdown-content table {
width: 100%;
border-collapse: collapse;
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--app-fg) 10%, transparent);
border-radius: var(--app-radius-md);
font-size: 12px;
}

.markdown-content th,
.markdown-content td {
padding: 7px 8px;
border: 1px solid color-mix(in srgb, var(--app-fg) 10%, transparent);
text-align: left;
vertical-align: top;
}

.markdown-content th {
color: var(--app-fg);
font-weight: 780;
background: color-mix(in srgb, var(--app-fg) 6%, transparent);
}

.markdown-content blockquote {
padding: 8px 10px;
border-left: 3px solid color-mix(in srgb, var(--app-primary) 55%, transparent);
Expand Down
Loading