mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 21:23:45 +01:00
Merge pull request #36208 from overleaf/ab-labs-description-headings-code
[web] Support headings and code in labs experiment descriptions GitOrigin-RevId: 2a85f92fb5f0ce0696c1056087108ef2599547de
This commit is contained in:
committed by
Copybot
parent
329eca74c1
commit
c861628684
@@ -3,7 +3,21 @@ import { micromark } from 'micromark'
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
const PURIFY_CONFIG = {
|
||||
ALLOWED_TAGS: ['#text', 'p', 'em', 'strong', 'a'],
|
||||
ALLOWED_TAGS: [
|
||||
'#text',
|
||||
'p',
|
||||
'em',
|
||||
'strong',
|
||||
'a',
|
||||
'code',
|
||||
'pre',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
],
|
||||
ALLOWED_ATTR: ['href'],
|
||||
}
|
||||
|
||||
@@ -27,12 +41,19 @@ function sanitizeDescription(description: string) {
|
||||
|
||||
/**
|
||||
* Renders a labs experiment description from markdown to sanitized HTML.
|
||||
* Only bold, italic, and links are supported.
|
||||
* Only bold, italic, links, headings, inline code and code blocks are
|
||||
* supported. Note that the language of a fenced code block is discarded, as
|
||||
* the `class` attribute micromark puts it in is not allowed through.
|
||||
*/
|
||||
export const LabsDescription: FC<{ description: string }> = ({
|
||||
description,
|
||||
}) => {
|
||||
const html = useMemo(() => sanitizeDescription(description), [description])
|
||||
|
||||
return <div dangerouslySetInnerHTML={{ __html: html }} />
|
||||
return (
|
||||
<div
|
||||
className="labs-description"
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -91,6 +91,96 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Markdown rendered from an experiment description, see labs-description.tsx.
|
||||
// Shown in the labs page widget, the experiment detail panel, and the split
|
||||
// test admin preview, so the styles are not scoped to any of them.
|
||||
.labs-description {
|
||||
// Body text here is --font-size-02, and the experiment's own title is 18-20px,
|
||||
// so the scale has to stay compact. There are not six distinct sizes to spend
|
||||
// below the title, so the lower levels are separated by weight and colour
|
||||
// instead — every level still differs from its neighbours.
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: var(--spacing-04) 0 var(--spacing-01);
|
||||
color: var(--content-primary);
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
// Both the `code` rule below and `.labs-opt-in-wrapper strong` pin a size,
|
||||
// which would otherwise shrink inline markup inside a heading
|
||||
code,
|
||||
strong {
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--font-size-04);
|
||||
line-height: var(--line-height-03);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--font-size-03);
|
||||
line-height: var(--line-height-03);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-02);
|
||||
line-height: var(--line-height-02);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: var(--font-size-02);
|
||||
line-height: var(--line-height-02);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: var(--font-size-01);
|
||||
line-height: var(--line-height-01);
|
||||
font-weight: 700;
|
||||
color: var(--content-secondary);
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: var(--font-size-01);
|
||||
line-height: var(--line-height-01);
|
||||
font-weight: 600;
|
||||
color: var(--content-secondary);
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0 var(--spacing-02);
|
||||
border-radius: var(--border-radius-base);
|
||||
background-color: var(--bg-light-secondary);
|
||||
color: var(--content-primary);
|
||||
font-size: var(--font-size-02);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0 0 var(--spacing-04);
|
||||
padding: var(--spacing-04);
|
||||
border-radius: var(--border-radius-base);
|
||||
background-color: var(--bg-light-secondary);
|
||||
overflow-x: auto;
|
||||
|
||||
code {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Feature Preview Modal
|
||||
.labs-feature-preview-modal {
|
||||
.modal-header {
|
||||
|
||||
@@ -27,4 +27,71 @@ describe('<LabsDescription />', function () {
|
||||
expect(link?.getAttribute('rel')).to.equal('noreferrer noopener')
|
||||
expect(link?.getAttribute('target')).to.equal('_BLANK')
|
||||
})
|
||||
|
||||
it('renders inline code', function () {
|
||||
const { container } = render(
|
||||
<LabsDescription description="Set the `foo` option." />
|
||||
)
|
||||
|
||||
const code = container.querySelector('code')
|
||||
expect(code?.textContent).to.equal('foo')
|
||||
expect(code?.parentElement?.nodeName).to.equal('P')
|
||||
})
|
||||
|
||||
it('renders fenced code blocks without the language class', function () {
|
||||
const { container } = render(
|
||||
<LabsDescription description={'```js\nconst foo = 1\n```'} />
|
||||
)
|
||||
|
||||
const code = container.querySelector('pre > code')
|
||||
expect(code?.textContent).to.equal('const foo = 1\n')
|
||||
expect(code?.getAttribute('class')).to.equal(null)
|
||||
})
|
||||
|
||||
it('renders indented code blocks', function () {
|
||||
const { container } = render(
|
||||
<LabsDescription description=" const foo = 1" />
|
||||
)
|
||||
|
||||
const code = container.querySelector('pre > code')
|
||||
expect(code?.textContent).to.equal('const foo = 1\n')
|
||||
})
|
||||
|
||||
// The visual hierarchy between the levels is in labs.scss, which the jsdom
|
||||
// suite does not load, so this only covers each level surviving sanitization
|
||||
it('renders headings at every level', function () {
|
||||
const levels = [1, 2, 3, 4, 5, 6]
|
||||
const { container } = render(
|
||||
<LabsDescription
|
||||
description={levels
|
||||
.map(level => `${'#'.repeat(level)} Level ${level}`)
|
||||
.join('\n\n')}
|
||||
/>
|
||||
)
|
||||
|
||||
for (const level of levels) {
|
||||
expect(container.querySelector(`h${level}`)?.textContent).to.equal(
|
||||
`Level ${level}`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('escapes HTML inside code', function () {
|
||||
const { container } = render(
|
||||
<LabsDescription description="Use `<script>alert(1)</script>` here." />
|
||||
)
|
||||
|
||||
expect(container.querySelector('script')).to.equal(null)
|
||||
expect(container.querySelector('code')?.textContent).to.equal(
|
||||
'<script>alert(1)</script>'
|
||||
)
|
||||
})
|
||||
|
||||
it('strips tags outside the supported subset', function () {
|
||||
const { container } = render(
|
||||
<LabsDescription description='<img src=x onerror="alert(1)">' />
|
||||
)
|
||||
|
||||
expect(container.querySelector('img')).to.equal(null)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user