Skip to content

Commit 26dcde1

Browse files
heiskrCopilot
andauthored
Render the MarkdownContent string fallback React-natively and drop the imperative enhancers (#6619) (#61967)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6c0d9ea commit 26dcde1

5 files changed

Lines changed: 16 additions & 278 deletions

File tree

src/frame/components/CodeTabs.tsx

Lines changed: 0 additions & 189 deletions
This file was deleted.

src/frame/components/article/ArticlePage.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { RestRedirect } from '@/rest/components/RestRedirect'
1717
import { Breadcrumbs } from '@/frame/components/page-header/Breadcrumbs'
1818
import { LinkPreviewPopover } from '@/links/components/LinkPreviewPopover'
1919
import { UtmPreserver } from '@/frame/components/UtmPreserver'
20-
import { CodeTabs } from '@/frame/components/CodeTabs'
2120
import { JourneyTrackCard, JourneyTrackNav } from '@/journeys/components'
2221
import { CopyMarkdownMenu } from './ViewMarkdownButton'
2322
import { ExperimentContentSwap } from '@/events/components/experiments/ExperimentContentSwap'
@@ -103,9 +102,6 @@ export const ArticlePage = () => {
103102
<CodeTabsProvider>
104103
<LinkPreviewPopover />
105104
<UtmPreserver />
106-
{/* The imperative CodeTabs enhancer only runs for the string fallback
107-
path; on the hast path, CodeTabsGroup renders tabs React-natively. */}
108-
{!renderedPageHast && <CodeTabs />}
109105
{isDev && <ClientSideRefresh />}
110106
{router.pathname.includes('/rest/') && <RestRedirect />}
111107
{currentLayout === 'inline' ? (

src/frame/components/ui/MarkdownContent/MarkdownContent.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { Root as HastRoot } from 'hast'
66
import cx from 'classnames'
77

88
import { markdownComponents } from './markdownComponents'
9+
import { renderHTMLString } from '@/frame/components/ui/RenderedHTML/render-html-string'
910
import styles from './MarkdownContent.module.scss'
1011

1112
export type MarkdownContentPropsT = {
@@ -15,22 +16,23 @@ export type MarkdownContentPropsT = {
1516
as?: keyof JSX.IntrinsicElements
1617
}
1718

18-
// Memoized so that re-renders of the parent (e.g. when ToolPicker/PlatformPicker
19-
// state updates) don't cause React 19 to re-apply `dangerouslySetInnerHTML` and
20-
// wipe out the inline `display` styles set imperatively by the pickers.
19+
// Memoized so that unrelated parent re-renders (e.g. ToolPicker/PlatformPicker
20+
// state updates) don't re-parse the HTML string and rebuild the body's element
21+
// tree, which is the expensive part of rendering an article.
2122
export const MarkdownContent = memo(function MarkdownContent({
2223
children,
2324
hast,
2425
as: Component = 'div',
2526
className,
2627
...restProps
2728
}: MarkdownContentPropsT) {
28-
// When a hast (HTML AST) tree is provided, render it as real React elements
29-
// instead of injecting an HTML string via dangerouslySetInnerHTML (#6619).
29+
// Render trusted HTML as real React elements instead of injecting it as raw
30+
// innerHTML (#6619). Prefer a hast (HTML AST) tree when provided; otherwise
31+
// parse a rendered HTML string. Non-string children render as-is.
3032
const childProps = hast
3133
? { children: toJsxRuntime(hast, { Fragment, jsx, jsxs, components: markdownComponents }) }
3234
: typeof children === 'string'
33-
? { dangerouslySetInnerHTML: { __html: children } }
35+
? { children: renderHTMLString(children, markdownComponents) }
3436
: { children }
3537

3638
return (

src/tools/components/PlatformPicker.tsx

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,40 +15,8 @@ const platforms = [
1515

1616
// Nota bene: platform === os
1717

18-
// Imperatively modify article content to show only the selected platform
19-
// find all platform-specific *block* elements and hide or show as appropriate
20-
// example: {% mac %} block content {% endmac %}
21-
function showPlatformSpecificContent(platform: string) {
22-
const markdowns = Array.from(document.querySelectorAll<HTMLElement>('.ghd-tool'))
23-
const platformMarkdowns = markdowns.filter((xel) =>
24-
platforms.some((platformValue) => xel.classList.contains(platformValue.value)),
25-
)
26-
for (const el of platformMarkdowns) {
27-
el.style.display = el.classList.contains(platform) ? '' : 'none'
28-
29-
// hack: special handling for minitoc links -- we can't pass the tool classes
30-
// directly to the Primer NavList.Item generated <li>, it gets passed down
31-
// to the child <a>. So if we find an <a> that has the tool class and its
32-
// parent is an <li>, we hide/unhide that element as well.
33-
if (el.tagName === 'A' && el.parentElement && el.parentElement.tagName === 'LI') {
34-
el.parentElement.style.display = el.classList.contains(platform) ? '' : 'none'
35-
}
36-
}
37-
38-
// find all platform-specific *inline* elements and hide or show as appropriate
39-
// example: <span class="platform-mac">inline content</span>
40-
const platformEls = Array.from(
41-
document.querySelectorAll<HTMLElement>(
42-
platforms.map((platformOption) => `.platform-${platformOption.value}`).join(', '),
43-
),
44-
)
45-
for (const el of platformEls) {
46-
el.style.display = el.classList.contains(`platform-${platform}`) ? '' : 'none'
47-
}
48-
}
49-
5018
export const PlatformPicker = () => {
51-
const { defaultPlatform, detectedPlatforms, renderedPageHast } = useArticleContext()
19+
const { defaultPlatform, detectedPlatforms } = useArticleContext()
5220
const { setPlatform } = useSelection()
5321

5422
const [defaultUA, setDefaultUA] = useState('')
@@ -78,11 +46,10 @@ export const PlatformPicker = () => {
7846
cookieKey={OS_PREFERRED_COOKIE_NAME}
7947
queryStringKey={platformQueryKey}
8048
onValue={(value: string) => {
81-
// Drive visibility through React state on the hast path (#6619). Only the
82-
// string fallback (renderedPageHast undefined) still needs the imperative
83-
// DOM mutation, since that markup isn't React-owned.
49+
// Visibility is driven by React state via ToggleableContent/MiniTocs
50+
// (#6619); the article body is React-owned on both the hast and string
51+
// paths, so no imperative DOM mutation is needed.
8452
setPlatform(value)
85-
if (!renderedPageHast) showPlatformSpecificContent(value)
8653
}}
8754
preferenceName="os"
8855
ariaLabel="Platform"

src/tools/components/ToolPicker.tsx

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { preserveAnchorNodePosition } from 'scroll-anchoring'
2-
31
import { useArticleContext } from '@/frame/components/context/ArticleContext'
42
import { InArticlePicker } from './InArticlePicker'
53
import { useSelection } from './SelectionContext'
@@ -10,38 +8,6 @@ import { TOOL_PREFERRED_COOKIE_NAME } from '@/frame/lib/constants'
108
// Nota bene: tool === application
119
// Nota bene: picker === switcher
1210

13-
// Imperatively modify article content to show only the selected tool
14-
// find all platform-specific *block* elements and hide or show as appropriate
15-
// example: {% webui %} block content {% endwebui %}
16-
function showToolSpecificContent(tool: string, supportedTools: Array<string>) {
17-
const markdowns = Array.from(document.querySelectorAll<HTMLElement>('.ghd-tool'))
18-
const supportedMarkdowns = markdowns.filter((xel) =>
19-
supportedTools.some((toolName) => xel.classList.contains(toolName)),
20-
)
21-
for (const el of supportedMarkdowns) {
22-
el.style.display = el.classList.contains(tool) ? '' : 'none'
23-
24-
// hack: special handling for minitoc links -- we can't pass the tool classes
25-
// directly to the Primer NavList.Item generated <li>, it gets passed down
26-
// to the child <a>. So if we find an <a> that has the tool class and its
27-
// parent is an <li>, we hide/unhide that element as well.
28-
if (el.tagName === 'A' && el.parentElement && el.parentElement.tagName === 'LI') {
29-
el.parentElement.style.display = el.classList.contains(tool) ? '' : 'none'
30-
}
31-
}
32-
33-
// find all tool-specific *inline* elements and hide or show as appropriate
34-
// example: <span class="tool-webui">inline content</span>
35-
const toolEls = Array.from(
36-
document.querySelectorAll<HTMLElement>(
37-
supportedTools.map((toolOption) => `.tool-${toolOption}`).join(', '),
38-
),
39-
)
40-
for (const el of toolEls) {
41-
el.style.display = el.classList.contains(`tool-${tool}`) ? '' : 'none'
42-
}
43-
}
44-
4511
function getDefaultTool(defaultTool: string | undefined, detectedTools: Array<string>): string {
4612
// If there is a default tool and the tool is present on this page
4713
if (defaultTool && detectedTools.includes(defaultTool)) return defaultTool
@@ -59,7 +25,7 @@ function getDefaultTool(defaultTool: string | undefined, detectedTools: Array<st
5925
const toolQueryKey = 'tool'
6026
export const ToolPicker = () => {
6127
// allTools comes from the ArticleContext which contains the list of tools available
62-
const { defaultTool, detectedTools, allTools, renderedPageHast } = useArticleContext()
28+
const { defaultTool, detectedTools, allTools } = useArticleContext()
6329
const { setTool } = useSelection()
6430

6531
if (!detectedTools.length) return null
@@ -74,14 +40,10 @@ export const ToolPicker = () => {
7440
cookieKey={TOOL_PREFERRED_COOKIE_NAME}
7541
queryStringKey={toolQueryKey}
7642
onValue={(value: string) => {
77-
// Drive visibility through React state on the hast path (#6619). Only the
78-
// string fallback still needs the imperative DOM mutation.
43+
// Visibility is driven by React state via ToggleableContent/MiniTocs
44+
// (#6619); the article body is React-owned on both the hast and string
45+
// paths, so no imperative DOM mutation is needed.
7946
setTool(value)
80-
if (!renderedPageHast) {
81-
preserveAnchorNodePosition(document, () => {
82-
showToolSpecificContent(value, Object.keys(allTools))
83-
})
84-
}
8547
}}
8648
preferenceName="application"
8749
ariaLabel="Tool"

0 commit comments

Comments
 (0)